├── api
├── shared
│ ├── models.js
│ ├── discount-data.js
│ └── product-data.js
├── discounts-get
│ ├── sample.dat
│ ├── index.js
│ └── function.json
├── .funcignore
├── .vscode
│ └── extensions.json
├── host.json
├── package.json
├── products-get
│ ├── index.js
│ └── function.json
├── products-delete
│ ├── index.js
│ └── function.json
├── products-post
│ ├── function.json
│ └── index.js
├── products-put
│ ├── function.json
│ └── index.js
├── .gitignore
└── README.md
├── react-app
├── src
│ ├── App.css
│ ├── globe.png
│ ├── store
│ │ ├── config.js
│ │ ├── discount.api.js
│ │ ├── discount.actions.js
│ │ ├── action-utils.js
│ │ ├── index.js
│ │ ├── discount.reducer.js
│ │ ├── discount.saga.js
│ │ ├── product.api.js
│ │ ├── product.actions.js
│ │ ├── product.saga.js
│ │ └── product.reducer.js
│ ├── components
│ │ ├── CardContent.js
│ │ ├── HeaderBar.js
│ │ ├── NotFound.js
│ │ ├── AuthLogout.js
│ │ ├── index.js
│ │ ├── AuthLogin.js
│ │ ├── Modal.js
│ │ ├── InputDetail.js
│ │ ├── ButtonFooter.js
│ │ ├── HeaderBarBrand.js
│ │ ├── ModalYesNo.js
│ │ ├── ListHeader.js
│ │ └── NavBar.js
│ ├── index.css
│ ├── useDiscounts.js
│ ├── products
│ │ ├── useProducts.js
│ │ ├── ProductList.js
│ │ └── ProductDetail.js
│ ├── index.js
│ ├── Home.js
│ ├── App.js
│ ├── Discounts.js
│ └── logo.svg
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ ├── some-legacy-discounts-page.html
│ ├── 404.html
│ ├── staticwebapp.config.json
│ └── index.html
├── .prettierrc
├── .gitignore
├── .eslintrc.json
├── package.json
└── README.md
├── angular-app
├── src
│ ├── assets
│ │ ├── .gitkeep
│ │ └── staticwebapp.config.json
│ ├── app
│ │ ├── build-specific
│ │ │ ├── index.prod.ts
│ │ │ └── index.ts
│ │ ├── core
│ │ │ ├── model
│ │ │ │ ├── index.ts
│ │ │ │ ├── discount.ts
│ │ │ │ ├── product.ts
│ │ │ │ └── user-info.ts
│ │ │ ├── index.ts
│ │ │ └── components
│ │ │ │ ├── header-bar.component.ts
│ │ │ │ ├── auth-logout.component.ts
│ │ │ │ ├── not-found.component.ts
│ │ │ │ ├── auth-login.component.ts
│ │ │ │ ├── header-bar-brand.component.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── nav.component.ts
│ │ ├── app.component.ts
│ │ ├── discount.service.ts
│ │ ├── shared
│ │ │ ├── card-content.component.ts
│ │ │ ├── shared.module.ts
│ │ │ ├── button-footer.component.ts
│ │ │ ├── list-header.component.ts
│ │ │ └── modal.component.ts
│ │ ├── router.ts
│ │ ├── products
│ │ │ ├── products.module.ts
│ │ │ ├── product.service.ts
│ │ │ └── product-list.component.ts
│ │ ├── app.module.ts
│ │ ├── home.component.ts
│ │ └── discounts.component.ts
│ ├── favicon.ico
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ ├── tslint.json
│ ├── main.ts
│ ├── test.ts
│ ├── index.html
│ ├── public
│ │ ├── some-legacy-discounts-page.html
│ │ └── 404.html
│ ├── karma.conf.js
│ └── polyfills.ts
├── proxy.conf.json
├── .prettierrc
├── .browserslistrc
├── tsconfig.json
├── .gitignore
├── package.json
├── tslint.json
└── README.md
├── vue-app
├── .env
├── .browserslistrc
├── postcss.config.js
├── public
│ ├── favicon.ico
│ ├── some-legacy-discounts-page.html
│ ├── 404.html
│ ├── index.html
│ └── staticwebapp.config.json
├── src
│ ├── assets
│ │ └── logo.png
│ ├── store
│ │ ├── config.js
│ │ ├── modules
│ │ │ ├── mutation-types.js
│ │ │ ├── action-utils.js
│ │ │ ├── discounts.js
│ │ │ └── products.js
│ │ └── index.js
│ ├── main.js
│ ├── components
│ │ ├── page-not-found.vue
│ │ ├── header-bar.vue
│ │ ├── card-content.vue
│ │ ├── auth-logout.vue
│ │ ├── auth-login.vue
│ │ ├── header-bar-brand.vue
│ │ ├── button-footer.vue
│ │ ├── modal.vue
│ │ ├── list-header.vue
│ │ └── nav-bar.vue
│ ├── app.vue
│ ├── router.js
│ └── views
│ │ ├── home.vue
│ │ ├── discounts.vue
│ │ └── products
│ │ └── product-list.vue
├── .babelrc
├── .prettierrc
├── vue.config.js
├── .gitignore
├── .eslintrc.js
├── package.json
└── README.md
├── svelte-app
├── .env
├── src
│ ├── models
│ │ ├── index.ts
│ │ ├── discount.ts
│ │ └── product.ts
│ ├── vite-env.d.ts
│ ├── global.d.ts
│ ├── store
│ │ ├── index.ts
│ │ ├── discount-data.ts
│ │ ├── http-utils.ts
│ │ ├── store.ts
│ │ └── product-data.ts
│ ├── main.ts
│ ├── components
│ │ ├── Redirect.svelte
│ │ ├── PageNotFound.svelte
│ │ ├── HeaderBar.svelte
│ │ ├── CardContent.svelte
│ │ ├── AuthLogout.svelte
│ │ ├── HeaderBarBrand.svelte
│ │ ├── AuthLogin.svelte
│ │ ├── index.ts
│ │ ├── ButtonFooter.svelte
│ │ ├── ListHeader.svelte
│ │ ├── Modal.svelte
│ │ └── NavBar.svelte
│ ├── config.ts
│ ├── App.svelte
│ ├── Home.svelte
│ ├── global.css
│ ├── Discounts.svelte
│ ├── products
│ │ ├── ProductList.svelte
│ │ └── ProductDetail.svelte
│ └── assets
│ │ └── svelte.svg
├── public
│ ├── favicon.png
│ ├── svelte-icon.png
│ ├── some-legacy-discounts-page.html
│ ├── 404.html
│ └── staticwebapp.config.json
├── .gitignore
├── .prettierrc
├── tsconfig.node.json
├── svelte.config.js
├── index.html
├── tsconfig.json
├── vite.config.ts
├── package.json
└── README.md
├── .vscode
├── extensions.json
├── launch.json
├── tasks.json
└── settings.json
├── fastify-api-server
├── src
│ ├── routes
│ │ ├── discounts.js
│ │ ├── index.js
│ │ └── products.js
│ ├── shared
│ │ ├── discount-data.js
│ │ └── product-data.js
│ └── server.js
├── package.json
└── README.md
├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── LICENSE
└── .github
└── workflows
├── azure-static-web-apps-zealous-ground-07634b41e.yml
├── azure-static-web-apps-thankful-ground-025b83e1e.yml
├── azure-static-web-apps-purple-cliff-0e5d9b80f.yml
├── azure-static-web-apps-purple-pond-08f780f0f.yml
├── azure-static-web-apps-gentle-cliff-0bc570010.yml
└── azure-static-web-apps-brave-mushroom-0741e3c1e.yml
/api/shared/models.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/react-app/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/angular-app/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-app/.env:
--------------------------------------------------------------------------------
1 | VITE_API='/api'
2 | DEV=false
3 |
--------------------------------------------------------------------------------
/svelte-app/.env:
--------------------------------------------------------------------------------
1 | VITE_API='/api'
2 | DEV=false
3 |
--------------------------------------------------------------------------------
/api/discounts-get/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/vue-app/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not ie <= 8
4 |
--------------------------------------------------------------------------------
/angular-app/src/app/build-specific/index.prod.ts:
--------------------------------------------------------------------------------
1 | export const externalModules = [];
2 |
--------------------------------------------------------------------------------
/svelte-app/src/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './discount';
2 | export * from './product';
3 |
--------------------------------------------------------------------------------
/react-app/src/globe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/shopathome/HEAD/react-app/src/globe.png
--------------------------------------------------------------------------------
/api/.funcignore:
--------------------------------------------------------------------------------
1 | *.js.map
2 | *.ts
3 | .git*
4 | .vscode
5 | local.settings.json
6 | test
7 | tsconfig.json
--------------------------------------------------------------------------------
/svelte-app/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/vue-app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/vue-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/shopathome/HEAD/vue-app/public/favicon.ico
--------------------------------------------------------------------------------
/angular-app/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/shopathome/HEAD/angular-app/src/favicon.ico
--------------------------------------------------------------------------------
/react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/shopathome/HEAD/react-app/public/favicon.ico
--------------------------------------------------------------------------------
/svelte-app/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/shopathome/HEAD/svelte-app/public/favicon.png
--------------------------------------------------------------------------------
/vue-app/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/shopathome/HEAD/vue-app/src/assets/logo.png
--------------------------------------------------------------------------------
/vue-app/src/store/config.js:
--------------------------------------------------------------------------------
1 | const API = process.env.VUE_APP_API || 'api';
2 |
3 | export { API as default };
4 |
--------------------------------------------------------------------------------
/react-app/src/store/config.js:
--------------------------------------------------------------------------------
1 | const API = process.env.REACT_APP_API || '/api';
2 |
3 | export { API as default };
4 |
--------------------------------------------------------------------------------
/svelte-app/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /public/build/
3 |
4 | .DS_Store
5 |
6 | dist
7 | dist-ssr
8 | *.local
9 |
--------------------------------------------------------------------------------
/api/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-azuretools.vscode-azurefunctions"
4 | ]
5 | }
--------------------------------------------------------------------------------
/svelte-app/public/svelte-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/shopathome/HEAD/svelte-app/public/svelte-icon.png
--------------------------------------------------------------------------------
/vue-app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@vue/babel-preset-app",
4 | "@babel/preset-env"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["ms-azuretools.vscode-azurefunctions", "svelte.svelte-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/angular-app/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://localhost:7071",
4 | "secure": false
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/angular-app/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | API: 'api',
4 | };
5 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './discount';
2 | export * from './product';
3 | export * from './user-info';
4 |
--------------------------------------------------------------------------------
/svelte-app/src/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module 'svelte-routing';
4 | declare const process: { env: any };
5 |
--------------------------------------------------------------------------------
/svelte-app/src/models/discount.ts:
--------------------------------------------------------------------------------
1 | export class Discount {
2 | id: number;
3 | store: string;
4 | percentage: number;
5 | code: string;
6 | }
7 |
--------------------------------------------------------------------------------
/svelte-app/src/models/product.ts:
--------------------------------------------------------------------------------
1 | export class Product {
2 | id: number;
3 | name: string;
4 | description: string;
5 | quantity?: number;
6 | }
7 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/model/discount.ts:
--------------------------------------------------------------------------------
1 | export class Discount {
2 | id: number;
3 | store: string;
4 | percentage: number;
5 | code: string;
6 | }
7 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/model/product.ts:
--------------------------------------------------------------------------------
1 | export class Product {
2 | id: number;
3 | name: string;
4 | description: string;
5 | quantity: number;
6 | }
7 |
--------------------------------------------------------------------------------
/react-app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 80,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "useTabs": false
8 | }
9 |
--------------------------------------------------------------------------------
/vue-app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 80,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "useTabs": false
8 | }
9 |
--------------------------------------------------------------------------------
/angular-app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 80,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "useTabs": false
8 | }
9 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/model/user-info.ts:
--------------------------------------------------------------------------------
1 | export interface UserInfo {
2 | identityProvider: string;
3 | userId: string;
4 | userDetails: string;
5 | userRoles: string[];
6 | }
7 |
--------------------------------------------------------------------------------
/svelte-app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 80,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "useTabs": false
8 | }
9 |
--------------------------------------------------------------------------------
/svelte-app/src/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../config';
2 | export * from './http-utils';
3 | export * from './store';
4 | export * from './discount-data';
5 | export * from './product-data';
6 |
--------------------------------------------------------------------------------
/svelte-app/src/main.ts:
--------------------------------------------------------------------------------
1 | import './styles.scss';
2 | import App from './App.svelte';
3 |
4 | const app = new App({
5 | target: document.getElementById('app'),
6 | });
7 |
8 | export default app;
9 |
--------------------------------------------------------------------------------
/svelte-app/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components';
2 | export * from './model';
3 | import { environment } from './../../environments/environment';
4 |
5 | export const API = environment.API || 'api';
6 |
--------------------------------------------------------------------------------
/svelte-app/src/components/Redirect.svelte:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/api/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/angular-app/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "main.ts",
9 | "polyfills.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "func start",
7 | "test": "echo \"No tests yet...\""
8 | },
9 | "dependencies": {},
10 | "devDependencies": {}
11 | }
12 |
--------------------------------------------------------------------------------
/vue-app/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import App from './app.vue';
3 | import router from './router';
4 | import store from './store';
5 |
6 | const app = createApp(App);
7 |
8 | app.use(router);
9 | app.use(store);
10 |
11 | app.mount('#app');
12 |
--------------------------------------------------------------------------------
/vue-app/src/store/modules/mutation-types.js:
--------------------------------------------------------------------------------
1 | export const GET_DISCOUNTS = 'GET_DISCOUNTS';
2 | export const GET_PRODUCTS = 'GET_PRODUCTS';
3 | export const ADD_PRODUCT = 'ADD_PRODUCT';
4 | export const UPDATE_PRODUCT = 'UPDATE_PRODUCT';
5 | export const DELETE_PRODUCT = 'DELETE_PRODUCT';
6 |
--------------------------------------------------------------------------------
/react-app/src/store/discount.api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { parseList } from './action-utils';
3 | import API from './config';
4 |
5 | export const loadDiscountsApi = async () => {
6 | const response = await axios.get(`${API}/discounts`);
7 | return parseList(response, 200);
8 | };
9 |
--------------------------------------------------------------------------------
/svelte-app/src/components/PageNotFound.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | These aren't the bits you're looking for
6 |
7 |
8 |
--------------------------------------------------------------------------------
/svelte-app/src/components/HeaderBar.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/vue-app/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | configureWebpack: {
3 | devtool: 'source-map',
4 | },
5 | devServer: {
6 | proxy: {
7 | '/api': {
8 | target: 'http://localhost:7071',
9 | ws: true,
10 | changeOrigin: true,
11 | },
12 | },
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/react-app/src/store/discount.actions.js:
--------------------------------------------------------------------------------
1 | export const LOAD_DISCOUNT = '[Discounts] LOAD_DISCOUNT';
2 | export const LOAD_DISCOUNT_SUCCESS = '[Discounts] LOAD_DISCOUNT_SUCCESS';
3 | export const LOAD_DISCOUNT_ERROR = '[Discounts] LOAD_DISCOUNT_ERROR';
4 |
5 | export const loadDiscountsAction = () => ({ type: LOAD_DISCOUNT });
6 |
--------------------------------------------------------------------------------
/svelte-app/src/components/CardContent.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
{name}
9 |
{description}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/vue-app/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | # .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
--------------------------------------------------------------------------------
/api/products-get/index.js:
--------------------------------------------------------------------------------
1 | const data = require('../shared/product-data');
2 |
3 | module.exports = async function (context, req) {
4 | try {
5 | const products = data.getProducts();
6 | context.res.status(200).json(products);
7 | } catch (error) {
8 | context.res.status(500).send(error);
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/vue-app/src/components/page-not-found.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | These aren't the bits you're looking for
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/api/discounts-get/index.js:
--------------------------------------------------------------------------------
1 | const data = require('../shared/discount-data');
2 |
3 | module.exports = async function (context, req) {
4 | try {
5 | const discounts = data.getDiscounts();
6 | context.res.status(200).json(discounts);
7 | } catch (error) {
8 | context.res.status(500).send(error);
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/angular-app/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "test.ts",
12 | "polyfills.ts"
13 | ],
14 | "include": [
15 | "**/*.spec.ts",
16 | "**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/api/products-delete/index.js:
--------------------------------------------------------------------------------
1 | const data = require('../shared/product-data');
2 |
3 | module.exports = async function (context, req) {
4 | const id = parseInt(req.params.id, 10);
5 |
6 | try {
7 | data.deleteProduct(id);
8 | context.res.status(200).json({});
9 | } catch (error) {
10 | context.res.status(500).send(error);
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/react-app/src/components/CardContent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CardContent = ({ name, description }) => (
4 |
5 |
6 |
{name}
7 |
{description}
8 |
9 |
10 | );
11 |
12 | export default CardContent;
13 |
--------------------------------------------------------------------------------
/api/products-get/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get"],
9 | "route": "products"
10 | },
11 | {
12 | "type": "http",
13 | "direction": "out",
14 | "name": "res"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/api/discounts-get/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get"],
9 | "route": "discounts"
10 | },
11 | {
12 | "type": "http",
13 | "direction": "out",
14 | "name": "res"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/api/products-post/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["post"],
9 | "route": "products"
10 | },
11 | {
12 | "type": "http",
13 | "direction": "out",
14 | "name": "res"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/api/products-put/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["put"],
9 | "route": "products/{id}"
10 | },
11 | {
12 | "type": "http",
13 | "direction": "out",
14 | "name": "res"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/api/products-delete/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["delete"],
9 | "route": "products/{id}"
10 | },
11 | {
12 | "type": "http",
13 | "direction": "out",
14 | "name": "res"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/fastify-api-server/src/routes/discounts.js:
--------------------------------------------------------------------------------
1 | const data = require('../shared/discount-data');
2 |
3 | async function routes(fastify, options) {
4 | // add route to get all discounts using the data module
5 | fastify.get('/discounts', async (request, reply) => {
6 | const discounts = data.getDiscounts();
7 | return discounts;
8 | });
9 | }
10 |
11 | module.exports = routes;
12 |
--------------------------------------------------------------------------------
/angular-app/src/app/build-specific/index.ts:
--------------------------------------------------------------------------------
1 | import { StoreDevtoolsModule } from '@ngrx/store-devtools';
2 |
3 | /**
4 | * Put dev specific code here, and prod specific code in index.prod.ts
5 | * https://ngrx.io/guide/store-devtools/recipes/exclude
6 | */
7 | export const externalModules = [
8 | StoreDevtoolsModule.instrument({
9 | maxAge: 25
10 | , connectInZone: true})
11 | ];
12 |
--------------------------------------------------------------------------------
/react-app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/react-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/angular-app/src/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "app",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "app",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/fastify-api-server/src/routes/index.js:
--------------------------------------------------------------------------------
1 | const discountsRoute = require('./discounts');
2 | const productsRoute = require('./products');
3 | async function routes(fastify, options) {
4 | fastify.get('/', async (request, reply) => {
5 | return { message: 'hello world' };
6 | });
7 |
8 | fastify.register(discountsRoute);
9 | fastify.register(productsRoute);
10 | }
11 |
12 | module.exports = routes;
13 |
--------------------------------------------------------------------------------
/react-app/src/components/HeaderBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HeaderBarBrand from './HeaderBarBrand';
3 |
4 | const HeaderBar = () => (
5 |
14 | );
15 |
16 | export default HeaderBar;
17 |
--------------------------------------------------------------------------------
/vue-app/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'vuex';
2 | import discountsModule from './modules/discounts';
3 | import productsModule from './modules/products';
4 |
5 | export * from './modules/mutation-types';
6 |
7 | export default createStore({
8 | strict: process.env.NODE_ENV !== 'production',
9 | modules: {
10 | products: productsModule,
11 | discounts: discountsModule,
12 | },
13 | state: {},
14 | });
15 |
--------------------------------------------------------------------------------
/angular-app/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/react-app/src/components/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotFound = () => (
4 |
5 |
6 |
7 |
8 | {`These aren't the bits you're looking for`}
9 |
10 |
11 | );
12 |
13 | export default NotFound;
14 |
--------------------------------------------------------------------------------
/angular-app/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 | #
5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
6 |
7 | > 0.5%
8 | last 2 versions
9 | Firefox ESR
10 | not dead
11 | not IE 9-11
--------------------------------------------------------------------------------
/react-app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/svelte-app/src/components/AuthLogout.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
17 | Logout
18 |
19 |
--------------------------------------------------------------------------------
/angular-app/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | @Component({
3 | selector: 'app-root',
4 | template: `
5 |
14 | `,
15 | })
16 | export class AppComponent {}
17 |
--------------------------------------------------------------------------------
/react-app/src/components/AuthLogout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function AuthLogout() {
4 | function goAuth() {
5 | const { pathname } = window.location;
6 | const redirect = `post_logout_redirect_uri=${pathname}`;
7 | const url = `/.auth/logout?${redirect}`;
8 | window.location.href = url;
9 | }
10 |
11 | return (
12 |
13 | Logout
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/svelte-app/src/config.ts:
--------------------------------------------------------------------------------
1 | const api = 'http://localhost:7071/api';
2 | const production = process.env.NODE_ENV === 'production';
3 | const API = import.meta.env.VITE_API
4 | ? import.meta.env.VITE_API
5 | : production
6 | ? '/api'
7 | : api;
8 | const DEV: boolean = import.meta.env.DEV || true;
9 |
10 | function logEnvironment() {
11 | console.log('API:', API);
12 | console.log('DEV:', DEV);
13 | }
14 |
15 | logEnvironment();
16 |
17 | export { API, DEV, logEnvironment };
18 |
--------------------------------------------------------------------------------
/vue-app/src/components/header-bar.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
20 |
21 |
--------------------------------------------------------------------------------
/svelte-app/src/components/HeaderBarBrand.svelte:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | SHOP
11 | AT
12 | HOME
13 |
14 |
15 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/components/header-bar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-header-bar',
5 | template: `
6 |
15 | `,
16 | })
17 | export class HeaderBarComponent {}
18 |
--------------------------------------------------------------------------------
/svelte-app/svelte.config.js:
--------------------------------------------------------------------------------
1 | import sveltePreprocess from 'svelte-preprocess';
2 |
3 | export default {
4 | // Consult https://github.com/sveltejs/svelte-preprocess
5 | // for more information about preprocessors
6 | preprocess: [
7 | sveltePreprocess({
8 | scss: {
9 | prependData: `
10 | @use "src/styles.scss" as *;
11 | @use "src/variables.scss" as *;
12 | @use "src/app.scss" as *;
13 | `,
14 | },
15 | }),
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/components/auth-logout.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-auth-logout',
5 | template: ` Logout
`,
6 | })
7 | export class AuthLogoutComponent {
8 | goAuth() {
9 | const { pathname } = window.location;
10 | const redirect = `post_logout_redirect_uri=${pathname}`;
11 | const url = `/.auth/logout?${redirect}`;
12 | window.location.href = url;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/components/not-found.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-not-found',
5 | template: `
6 |
7 |
8 |
9 | These aren't the bits you're looking for
10 |
11 |
12 | `,
13 | })
14 | export class NotFoundComponent {}
15 |
--------------------------------------------------------------------------------
/react-app/src/store/action-utils.js:
--------------------------------------------------------------------------------
1 | export const parseList = response => {
2 | if (response.status !== 200) throw Error(response.message);
3 | let list = response.data;
4 | if (typeof list !== 'object') {
5 | list = [];
6 | }
7 | return list;
8 | };
9 |
10 | export const parseItem = (response, code) => {
11 | if (response.status !== code) throw Error(response.message);
12 | let item = response.data;
13 | if (typeof item !== 'object') {
14 | item = undefined;
15 | }
16 | return item;
17 | };
18 |
--------------------------------------------------------------------------------
/svelte-app/src/components/AuthLogin.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
19 | {provider}
20 |
21 |
--------------------------------------------------------------------------------
/angular-app/src/app/discount.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { API, Discount } from './core';
3 | import { HttpClient } from '@angular/common/http';
4 | import { Observable } from 'rxjs';
5 |
6 | @Injectable({ providedIn: 'root' })
7 | export class DiscountService {
8 | private readonly apiUrl = `${API}/discounts`;
9 |
10 | constructor(private http: HttpClient) {}
11 |
12 | getDiscounts(): Observable {
13 | return this.http.get(this.apiUrl);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/vue-app/src/store/modules/action-utils.js:
--------------------------------------------------------------------------------
1 | export const parseList = (response) => {
2 | if (response.status !== 200) throw Error(response.message);
3 | let list = response.data;
4 | if (typeof list !== 'object') {
5 | list = [];
6 | }
7 | return list;
8 | };
9 |
10 | export const parseItem = (response, code) => {
11 | if (response.status !== code) throw Error(response.message);
12 | let item = response.data;
13 | if (typeof item !== 'object') {
14 | item = undefined;
15 | }
16 | return item;
17 | };
18 |
--------------------------------------------------------------------------------
/api/products-post/index.js:
--------------------------------------------------------------------------------
1 | const data = require('../shared/product-data');
2 |
3 | module.exports = async function (context, req) {
4 | const product = {
5 | id: undefined,
6 | name: req.body.name,
7 | description: req.body.description,
8 | quantity: parseInt(req.body.quantity, 10),
9 | };
10 |
11 | try {
12 | const newProduct = data.addProduct(product);
13 | context.res.status(201).json(newProduct);
14 | } catch (error) {
15 | context.res.status(500).send(error);
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/react-app/src/components/index.js:
--------------------------------------------------------------------------------
1 | import ButtonFooter from './ButtonFooter';
2 | import CardContent from './CardContent';
3 | import HeaderBar from './HeaderBar';
4 | import InputDetail from './InputDetail';
5 | import ListHeader from './ListHeader';
6 | import ModalYesNo from './ModalYesNo';
7 | import NavBar from './NavBar';
8 | import NotFound from './NotFound';
9 |
10 | export {
11 | ButtonFooter,
12 | CardContent,
13 | HeaderBar,
14 | InputDetail,
15 | ListHeader,
16 | NavBar,
17 | NotFound,
18 | ModalYesNo
19 | };
20 |
--------------------------------------------------------------------------------
/react-app/src/components/AuthLogin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function AuthLogin(props) {
4 | const { provider } = props;
5 |
6 | function goAuth() {
7 | const { pathname } = window.location;
8 | const redirect = `post_login_redirect_uri=${pathname}`;
9 | const url = `/.auth/login/${provider}?${redirect}`;
10 | window.location.href = url;
11 | }
12 |
13 | return (
14 |
15 | {provider}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/react-app/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { createPortal } from 'react-dom';
3 |
4 | const modalRoot = document.getElementById('modal');
5 |
6 | function Modal(props) {
7 | let el = document.createElement('div');
8 |
9 | useEffect(() => {
10 | modalRoot.appendChild(el);
11 | }, [el]);
12 |
13 | useEffect(() => {
14 | return () => {
15 | modalRoot.removeChild(el);
16 | };
17 | }, [el]);
18 |
19 | return createPortal(props.children, el);
20 | }
21 |
22 | export default Modal;
23 |
--------------------------------------------------------------------------------
/vue-app/src/components/card-content.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
{{ name }}
21 |
{{ description }}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/angular-app/src/app/shared/card-content.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-card-content',
5 | template: `
6 |
7 |
8 |
{{ name }}
9 |
{{ description }}
10 |
11 |
12 | `
13 | })
14 | export class CardContentComponent implements OnInit {
15 | @Input() name;
16 | @Input() description;
17 |
18 | ngOnInit() {}
19 | }
20 |
--------------------------------------------------------------------------------
/react-app/src/components/InputDetail.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const InputDetail = ({ name, value, placeholder, onChange, readOnly }) => (
4 |
5 |
6 | {name}
7 |
8 |
17 |
18 | );
19 |
20 | export default InputDetail;
21 |
--------------------------------------------------------------------------------
/api/products-put/index.js:
--------------------------------------------------------------------------------
1 | const data = require('../shared/product-data');
2 |
3 | module.exports = async function (context, req) {
4 | const product = {
5 | id: parseInt(req.params.id, 10),
6 | name: req.body.name,
7 | description: req.body.description,
8 | quantity: parseInt(req.body.quantity, 10),
9 | };
10 |
11 | try {
12 | const updatedProduct = data.updateProduct(product);
13 | context.res.status(200).json(updatedProduct);
14 | } catch (error) {
15 | context.res.status(500).send(error);
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/components/auth-login.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-auth-login',
5 | template: ` {{ provider }}
`,
6 | })
7 | export class AuthLoginComponent {
8 | @Input() provider = '';
9 |
10 | goAuth() {
11 | const { pathname } = window.location;
12 | const redirect = `post_login_redirect_uri=${pathname}`;
13 | const url = `/.auth/login/${this.provider}?${redirect}`;
14 | window.location.href = url;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/svelte-app/src/components/index.ts:
--------------------------------------------------------------------------------
1 | import ButtonFooter from './ButtonFooter.svelte';
2 | import CardContent from './CardContent.svelte';
3 | import HeaderBar from './HeaderBar.svelte';
4 | import ListHeader from './ListHeader.svelte';
5 | import Modal from './Modal.svelte';
6 | import NavBar from './NavBar.svelte';
7 | import PageNotFound from './PageNotFound.svelte';
8 | import Redirect from './Redirect.svelte';
9 |
10 | export {
11 | ButtonFooter,
12 | CardContent,
13 | HeaderBar,
14 | ListHeader,
15 | Modal,
16 | NavBar,
17 | PageNotFound,
18 | Redirect,
19 | };
20 |
--------------------------------------------------------------------------------
/svelte-app/src/store/discount-data.ts:
--------------------------------------------------------------------------------
1 | import * as store from './store';
2 | import { parseList } from './http-utils';
3 | import { API } from '../config';
4 | import { Discount } from '../models';
5 |
6 | export async function getDiscountsAction() {
7 | try {
8 | const response = await fetch(`${API}/discounts`, {
9 | method: 'GET',
10 | });
11 | const discounts: Discount[] = await parseList(response);
12 | store.getDiscounts(discounts);
13 | return discounts;
14 | } catch (err) {
15 | console.log(err);
16 | throw new Error(err);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/vue-app/src/components/auth-logout.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 | Logout
21 |
22 |
23 |
--------------------------------------------------------------------------------
/angular-app/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | // First, initialize the Angular testing environment.
11 | getTestBed().initTestEnvironment(
12 | BrowserDynamicTestingModule,
13 | platformBrowserDynamicTesting(), {
14 | teardown: { destroyAfterEach: false }
15 | }
16 | );
17 |
--------------------------------------------------------------------------------
/api/shared/discount-data.js:
--------------------------------------------------------------------------------
1 | const data = {
2 | discounts: [
3 | {
4 | id: 10,
5 | store: 'Contoso Market',
6 | percentage: 30,
7 | code: 'contoso30',
8 | },
9 | {
10 | id: 20,
11 | store: 'Tailwind Trader',
12 | percentage: 20,
13 | code: 'tailwind20',
14 | },
15 | {
16 | id: 30,
17 | store: 'Northwind-Mart',
18 | percentage: 10,
19 | code: 'northwind10',
20 | },
21 | ],
22 | };
23 |
24 | const getDiscounts = () => {
25 | return data.discounts;
26 | };
27 |
28 | module.exports = { getDiscounts };
29 |
--------------------------------------------------------------------------------
/react-app/src/components/ButtonFooter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ButtonFooter = ({
4 | label,
5 | className,
6 | iconClasses,
7 | onClick,
8 | dataIndex,
9 | dataId,
10 | }) => {
11 | return (
12 |
20 |
21 | {label}
22 |
23 | );
24 | };
25 |
26 | export default ButtonFooter;
27 |
--------------------------------------------------------------------------------
/angular-app/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Angular
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/fastify-api-server/src/shared/discount-data.js:
--------------------------------------------------------------------------------
1 | const data = {
2 | discounts: [
3 | {
4 | id: 10,
5 | store: 'Contoso Market',
6 | percentage: 30,
7 | code: 'contoso30',
8 | },
9 | {
10 | id: 20,
11 | store: 'Tailwind Trader',
12 | percentage: 20,
13 | code: 'tailwind20',
14 | },
15 | {
16 | id: 30,
17 | store: 'Northwind-Mart',
18 | percentage: 10,
19 | code: 'northwind10',
20 | },
21 | ],
22 | };
23 |
24 | const getDiscounts = () => {
25 | return data.discounts;
26 | };
27 |
28 | module.exports = { getDiscounts };
29 |
--------------------------------------------------------------------------------
/react-app/public/some-legacy-discounts-page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
Legacy Discounts Placeholder Page
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/svelte-app/public/some-legacy-discounts-page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
Legacy Discounts Placeholder Page
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/vue-app/public/some-legacy-discounts-page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
Legacy Discounts Placeholder Page
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/angular-app/src/public/some-legacy-discounts-page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
Legacy Discounts Placeholder Page
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/svelte-app/src/store/http-utils.ts:
--------------------------------------------------------------------------------
1 | export const parseList = async (response: Response) => {
2 | if (response.status !== 200) throw Error(`Error, status ${response.status}`);
3 | let list: T[] = await response.json();
4 | if (typeof list !== 'object') {
5 | list = [];
6 | }
7 | return list;
8 | };
9 |
10 | export const parseItem = async (response: Response, code: number) => {
11 | if (response.status !== code) throw Error(`Error, status ${response.status}`);
12 | let item = await response.json();
13 | if (typeof item !== 'object') {
14 | item = undefined;
15 | }
16 | return item as T;
17 | };
18 |
--------------------------------------------------------------------------------
/vue-app/src/app.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 |
--------------------------------------------------------------------------------
/angular-app/src/app/router.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 | import { HomeComponent } from './home.component';
3 | import { DiscountComponent } from './discounts.component';
4 | import { NotFoundComponent } from './core';
5 |
6 | export const routes: Routes = [
7 | { path: '', pathMatch: 'full', redirectTo: 'home' },
8 | { path: 'home', component: HomeComponent },
9 | {
10 | path: 'products',
11 | loadChildren: () =>
12 | import('./products/products.module').then((m) => m.ProductsModule),
13 | },
14 | { path: 'discounts', component: DiscountComponent },
15 | { path: '**', component: NotFoundComponent },
16 | ];
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | // Existing configurations...
5 | {
6 | "type": "node",
7 | "request": "launch",
8 | "name": "Launch Fastify API Server",
9 | "runtimeExecutable": "npm",
10 | "runtimeArgs": ["run-script", "start"],
11 | "port": 9229,
12 | "cwd": "${workspaceFolder}/fastify-api-server",
13 | "skipFiles": ["/**"]
14 | },
15 | {
16 | "name": "Attach to Node Functions",
17 | "type": "node",
18 | "request": "attach",
19 | "port": 9229,
20 | "preLaunchTask": "func: host start"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/angular-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "angularCompilerOptions": {
4 | "strictTemplates": true,
5 | "strictInjectionParameters": true
6 | },
7 | "compilerOptions": {
8 | "baseUrl": "./",
9 | "importHelpers": true,
10 | "outDir": "./dist/out-tsc",
11 | "sourceMap": true,
12 | "declaration": false,
13 | "module": "es2020",
14 | "moduleResolution": "node",
15 | "experimentalDecorators": true,
16 | "target": "ES2022",
17 | "typeRoots": [
18 | "node_modules/@types"
19 | ],
20 | "lib": [
21 | "es2018",
22 | "dom"
23 | ],
24 | "useDefineForClassFields": false
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "func",
6 | "command": "host start",
7 | "problemMatcher": "$func-watch",
8 | "isBackground": true,
9 | "dependsOn": "npm install",
10 | "options": {
11 | "cwd": "${workspaceFolder}/api"
12 | }
13 | },
14 | {
15 | "type": "shell",
16 | "label": "npm install",
17 | "command": "npm install",
18 | "options": {
19 | "cwd": "${workspaceFolder}/api"
20 | }
21 | },
22 | {
23 | "type": "shell",
24 | "label": "npm prune",
25 | "command": "npm prune --production",
26 | "problemMatcher": [],
27 | "options": {
28 | "cwd": "${workspaceFolder}/api"
29 | }
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/react-app/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { selectedProductReducer, productsReducer } from './product.reducer';
3 | import { discountsReducer } from './discount.reducer';
4 |
5 | export * from './product.actions';
6 | export * from './product.reducer';
7 | export * from './product.saga';
8 | export * from './product.api';
9 | export * from './discount.actions';
10 | export * from './discount.reducer';
11 | export * from './discount.saga';
12 | export * from './discount.api';
13 |
14 | const store = combineReducers({
15 | products: productsReducer,
16 | discounts: discountsReducer,
17 | selectedProduct: selectedProductReducer,
18 | });
19 |
20 | export default store;
21 |
--------------------------------------------------------------------------------
/vue-app/src/components/auth-login.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 | {{ provider }}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/react-app/src/store/discount.reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOAD_DISCOUNT_SUCCESS,
3 | LOAD_DISCOUNT,
4 | LOAD_DISCOUNT_ERROR,
5 | } from './discount.actions';
6 |
7 | let initState = {
8 | loading: false,
9 | data: [],
10 | error: void 0,
11 | };
12 |
13 | export const discountsReducer = (state = initState, action) => {
14 | switch (action.type) {
15 | case LOAD_DISCOUNT:
16 | return { ...state, loading: true, error: '' };
17 | case LOAD_DISCOUNT_SUCCESS:
18 | return { ...state, loading: false, data: [...action.payload] };
19 | case LOAD_DISCOUNT_ERROR:
20 | return { ...state, loading: false, error: action.payload };
21 |
22 | default:
23 | return state;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/fastify-api-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fastify-api-server",
3 | "version": "1.0.0",
4 | "description": "Fastify API server",
5 | "main": "src/server.js",
6 | "scripts": {
7 | "start": "node src/server.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "keywords": [
11 | "fastify",
12 | "api",
13 | "server"
14 | ],
15 | "engines": {
16 | "node": "^20.0.0"
17 | },
18 | "author": "Your Name",
19 | "license": "MIT",
20 | "dependencies": {
21 | "@fastify/cors": "^9.0.1",
22 | "@fastify/helmet": "^11.1.1",
23 | "fastify": "^4.28.0",
24 | "helmet": "^7.1.0"
25 | },
26 | "devDependencies": {
27 | "concurrently": "^8.2.2"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/react-app/src/components/HeaderBarBrand.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | const HeaderBarBrand = () => (
5 |
6 |
12 |
13 |
14 |
15 | SHOP
16 | AT
17 | HOME
18 |
19 |
20 | );
21 |
22 | export default HeaderBarBrand;
23 |
--------------------------------------------------------------------------------
/angular-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 | node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.angular/cache
30 | /.sass-cache
31 | /connect.lock
32 | /coverage
33 | /libpeerconnection.log
34 | npm-debug.log
35 | yarn-error.log
36 | testem.log
37 | /typings
38 |
39 | # System Files
40 | .DS_Store
41 | Thumbs.db
42 |
--------------------------------------------------------------------------------
/react-app/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/svelte-app/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/vue-app/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/angular-app/src/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 - Page Not Found
7 |
16 |
17 |
18 | 404 - Page Not Found
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/vue-app/src/components/header-bar-brand.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 | SHOP
21 | AT
22 | HOME
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # Find the Dockerfile for mcr.microsoft.com/azure-functions/node at the following URLs:
2 | # Node 10: https://github.com/Azure/azure-functions-docker/blob/master/host/3.0/buster/amd64/node/node10/node10-core-tools.Dockerfile
3 | # Node 12: https://github.com/Azure/azure-functions-docker/blob/master/host/3.0/buster/amd64/node/node12/node12-core-tools.Dockerfile
4 | ARG VARIANT=14
5 | FROM mcr.microsoft.com/azure-functions/node:3.0-node${VARIANT}-core-tools
6 |
7 | # [Optional] Uncomment this section to install additional OS packages.
8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
9 | # && apt-get -y install --no-install-recommends
10 |
11 | # DONT DO THIS
12 | # RUN npm install -g @azure/static-web-apps-cli
--------------------------------------------------------------------------------
/angular-app/src/app/core/components/header-bar-brand.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-header-bar-brand',
5 | template: `
6 |
21 | `,
22 | })
23 | export class HeaderBarBrandComponent {}
24 |
--------------------------------------------------------------------------------
/angular-app/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | API: 'api'
8 | };
9 |
10 | /*
11 | * For easier debugging in development mode, you can import the following file
12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
13 | *
14 | * This import should be commented out in production mode because it will have a negative impact
15 | * on performance if an error is thrown.
16 | */
17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
18 |
--------------------------------------------------------------------------------
/react-app/src/useDiscounts.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import { loadDiscountsAction } from './store';
5 |
6 | /** Custom hook for accessing Discount state in redux store */
7 | function useDiscounts() {
8 | const dispatch = useDispatch();
9 |
10 | return {
11 | // Selectors
12 | discounts: useSelector((state) => state.discounts.data),
13 | error: useSelector((state) => state.discounts.error),
14 |
15 | // Dispatchers
16 | // Wrap any dispatcher that could be called within a useEffect() in a useCallback()
17 | getDiscounts: useCallback((/* e */) => dispatch(loadDiscountsAction()), [
18 | dispatch,
19 | ]), // called within a useEffect()
20 | };
21 | }
22 |
23 | export default useDiscounts;
24 |
--------------------------------------------------------------------------------
/svelte-app/src/components/ButtonFooter.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
31 |
--------------------------------------------------------------------------------
/angular-app/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4 | import { ListHeaderComponent } from './list-header.component';
5 | import { CardContentComponent } from './card-content.component';
6 | import { ButtonFooterComponent } from './button-footer.component';
7 | import { ModalComponent } from './modal.component';
8 |
9 | const components = [
10 | ButtonFooterComponent,
11 | CardContentComponent,
12 | ListHeaderComponent,
13 | ModalComponent
14 | ];
15 |
16 | @NgModule({
17 | imports: [CommonModule, FormsModule, ReactiveFormsModule],
18 | declarations: [components],
19 | exports: [components, FormsModule, ReactiveFormsModule]
20 | })
21 | export class SharedModule {}
22 |
--------------------------------------------------------------------------------
/react-app/src/store/discount.saga.js:
--------------------------------------------------------------------------------
1 | import { put, takeEvery, call, all } from 'redux-saga/effects';
2 | import {
3 | LOAD_DISCOUNT,
4 | LOAD_DISCOUNT_SUCCESS,
5 | LOAD_DISCOUNT_ERROR,
6 | } from './discount.actions';
7 | import { loadDiscountsApi } from './discount.api';
8 |
9 | export function* loadingDiscountsAsync() {
10 | try {
11 | const data = yield call(loadDiscountsApi);
12 | const discounts = [...data];
13 |
14 | yield put({ type: LOAD_DISCOUNT_SUCCESS, payload: discounts });
15 | } catch (err) {
16 | yield put({ type: LOAD_DISCOUNT_ERROR, payload: err.message });
17 | }
18 | }
19 |
20 | export function* watchLoadingDiscountsAsync() {
21 | yield takeEvery(LOAD_DISCOUNT, loadingDiscountsAsync);
22 | }
23 |
24 | export function* discountSaga() {
25 | yield all([watchLoadingDiscountsAsync()]);
26 | }
27 |
--------------------------------------------------------------------------------
/angular-app/src/app/shared/button-footer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-button-footer',
5 | template: `
6 |
14 | {{ label }}
15 |
16 | `
17 | })
18 | export class ButtonFooterComponent implements OnInit {
19 | @Input() label;
20 | @Input() className;
21 | @Input() iconClasses;
22 | @Input() item;
23 | @Input() dataId;
24 |
25 | @Output() clicked = new EventEmitter();
26 |
27 | ngOnInit() {}
28 |
29 | handleClick() {
30 | this.clicked.emit(this.item);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/angular-app/src/app/products/products.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 | import { RouterModule, Routes } from '@angular/router';
4 | import { SharedModule } from '../shared/shared.module';
5 | import { ProductDetailComponent } from './product-detail.component';
6 | import { ProductListComponent } from './product-list.component';
7 | import { ProductsComponent } from './products.component';
8 |
9 | const routes: Routes = [
10 | {
11 | path: '',
12 | component: ProductsComponent,
13 | },
14 | ];
15 |
16 | @NgModule({
17 | imports: [CommonModule, RouterModule.forChild(routes), SharedModule],
18 | exports: [RouterModule, ProductsComponent],
19 | declarations: [
20 | ProductsComponent,
21 | ProductListComponent,
22 | ProductDetailComponent,
23 | ],
24 | })
25 | export class ProductsModule {}
26 |
--------------------------------------------------------------------------------
/svelte-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Svelte app
8 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/svelte-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 |
4 | // "exclude": ["node_modules/*", "__sapper__/*", "public/*"],
5 | "compilerOptions": {
6 | "target": "ESNext",
7 | "useDefineForClassFields": true,
8 | "module": "ESNext",
9 | "resolveJsonModule": true,
10 | /**
11 | * Typecheck JS in `.svelte` and `.js` files by default.
12 | * Disable checkJs if you'd like to use dynamic types in JS.
13 | * Note that setting allowJs false does not prevent the use
14 | * of JS in `.svelte` files.
15 | */
16 | "allowJs": true,
17 | "checkJs": true,
18 | "isolatedModules": true,
19 | // "importsNotUsedAsValues": "remove",
20 | "sourceMap": true,
21 | },
22 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
23 | "references": [{ "path": "./tsconfig.node.json" }]
24 | }
25 |
--------------------------------------------------------------------------------
/react-app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:react/recommended",
5 | "plugin:jsx-a11y/recommended",
6 | "prettier"
7 | ],
8 | "rules": {
9 | "react/prop-types": 0,
10 | "jsx-a11y/label-has-for": 0,
11 | "jsx-a11y/anchor-is-valid": 0,
12 | "jsx-a11y/click-events-have-key-events": 0,
13 | "no-console": 1,
14 | "quotes": [
15 | 2,
16 | "single",
17 | {
18 | "avoidEscape": true,
19 | "allowTemplateLiterals": true
20 | }
21 | ]
22 | },
23 | "plugins": ["react", "import", "jsx-a11y"],
24 | "parser": "babel-eslint",
25 | "parserOptions": {
26 | "ecmaVersion": 2018,
27 | "sourceType": "module",
28 | "ecmaFeatures": {
29 | "jsx": true
30 | }
31 | },
32 | "env": {
33 | "es6": true,
34 | "browser": true,
35 | "node": true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth-login.component';
2 | export * from './auth-logout.component';
3 | export * from './header-bar.component';
4 | export * from './header-bar-brand.component';
5 | export * from './nav.component';
6 | export * from './not-found.component';
7 |
8 | import { AuthLoginComponent } from './auth-login.component';
9 | import { AuthLogoutComponent } from './auth-logout.component';
10 | import { HeaderBarBrandComponent } from './header-bar-brand.component';
11 | import { HeaderBarComponent } from './header-bar.component';
12 | import { NavComponent } from './nav.component';
13 | import { NotFoundComponent } from './not-found.component';
14 |
15 | export const declarations = [
16 | AuthLoginComponent,
17 | AuthLogoutComponent,
18 | NavComponent,
19 | HeaderBarComponent,
20 | HeaderBarBrandComponent,
21 | NotFoundComponent,
22 | ];
23 |
--------------------------------------------------------------------------------
/react-app/src/components/ModalYesNo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Modal from './Modal';
4 |
5 | const ModalYesNo = ({ message, onYes, onNo }) => (
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 | No
17 |
18 |
19 | Yes
20 |
21 |
22 |
23 |
24 |
25 | );
26 |
27 | export default ModalYesNo;
28 |
--------------------------------------------------------------------------------
/svelte-app/src/components/ListHeader.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
{title}
18 |
19 | {#if showAdd}
20 | add()} aria-label="add">
21 |
22 |
23 | {/if}
24 | dispatch('refresh')}
27 | aria-label="refresh">
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/fastify-api-server/src/server.js:
--------------------------------------------------------------------------------
1 | const fastify = require('fastify')();
2 | const cors = require('@fastify/cors');
3 | const helmet = require('@fastify/helmet');
4 |
5 | fastify.register(helmet);
6 | fastify.register(cors, {
7 | origin: '*',
8 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
9 | allowedHeaders: ['Content-Type', 'Authorization'],
10 | exposedHeaders: ['Content-Range', 'X-Content-Range'],
11 | credentials: true,
12 | });
13 |
14 | const routes = require('./routes');
15 | const options = {
16 | prefix: '/api',
17 | logger: true,
18 | };
19 | fastify.register(routes, options);
20 |
21 | const start = async () => {
22 | try {
23 | await fastify.listen({ port: 3000, host: '0.0.0.0' });
24 | console.log('Server started on port 3000');
25 | } catch (err) {
26 | fastify.log.error(err);
27 | console.error(err);
28 | process.exit(1);
29 | }
30 | };
31 |
32 | start();
33 |
--------------------------------------------------------------------------------
/react-app/src/store/product.api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { parseItem, parseList } from './action-utils';
3 | import API from './config';
4 |
5 | const captains = console;
6 |
7 | export const deleteProductApi = async (product) => {
8 | const response = await axios.delete(`${API}/products/${product.id}`);
9 | return parseItem(response, 200);
10 | };
11 |
12 | export const updateProductApi = async (product) => {
13 | captains.log(product.id);
14 | const response = await axios.put(`${API}/products/${product.id}`, product);
15 | return parseItem(response, 200);
16 | };
17 |
18 | export const addProductApi = async (product) => {
19 | const response = await axios.post(`${API}/products`, product);
20 | return parseItem(response, 201);
21 | };
22 |
23 | export const loadProductsApi = async () => {
24 | const response = await axios.get(`${API}/products`);
25 | return parseList(response, 200);
26 | };
27 |
--------------------------------------------------------------------------------
/svelte-app/src/App.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/svelte-app/src/components/Modal.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
31 |
--------------------------------------------------------------------------------
/react-app/src/components/ListHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | const ListHeader = ({
5 | title,
6 | handleAdd,
7 | handleRefresh,
8 | routePath,
9 | hideAdd,
10 | }) => {
11 | return (
12 |
13 |
14 | {title}
15 |
16 | {!hideAdd && (
17 |
22 |
23 |
24 | )}
25 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default ListHeader;
37 |
--------------------------------------------------------------------------------
/angular-app/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
4 |
5 | import { routes } from './router';
6 | import { AppComponent } from './app.component';
7 | import { HomeComponent } from './home.component';
8 | import { RouterModule } from '@angular/router';
9 | import { externalModules } from './build-specific';
10 | import { declarations } from './core';
11 | import { DiscountComponent } from './discounts.component';
12 | import { SharedModule } from './shared/shared.module';
13 |
14 | @NgModule({ declarations: [AppComponent, HomeComponent, DiscountComponent, declarations],
15 | bootstrap: [AppComponent], imports: [BrowserModule,
16 | RouterModule.forRoot(routes, {}),
17 | SharedModule,
18 | externalModules], providers: [provideHttpClient(withInterceptorsFromDi())] })
19 | export class AppModule {}
20 |
--------------------------------------------------------------------------------
/vue-app/src/router.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from 'vue-router';
2 | import PageNotFound from './components/page-not-found.vue';
3 |
4 | const routes = [
5 | {
6 | path: '/',
7 | redirect: '/home',
8 | },
9 | {
10 | path: '/products',
11 | name: 'products',
12 | component: () =>
13 | import(
14 | /* webpackChunkName: "products" */ './views/products/products.vue'
15 | ),
16 | },
17 | {
18 | path: '/discounts',
19 | name: 'discounts',
20 | component: () =>
21 | import(/* webpackChunkName: "discount" */ './views/discounts.vue'),
22 | },
23 | {
24 | path: '/home',
25 | name: 'home',
26 | component: () => import(/* webpackChunkName: "home" */ './views/home.vue'),
27 | },
28 | {
29 | path: '/:pathMatch(.*)*',
30 | name: 'NotFound',
31 | component: PageNotFound,
32 | },
33 | ];
34 |
35 | const router = createRouter({
36 | history: createWebHistory(process.env.BASE_URL),
37 | routes,
38 | });
39 |
40 | export default router;
41 |
--------------------------------------------------------------------------------
/svelte-app/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import { svelte } from '@sveltejs/vite-plugin-svelte';
3 | import sveltePreprocess from 'svelte-preprocess';
4 | // import autoprefixer from 'autoprefixer';
5 |
6 | const production = process.env.NODE_ENV === 'production';
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | plugins: [
11 | svelte({
12 | emitCss: production,
13 |
14 | // preprocess sass and scss files
15 |
16 | preprocess: sveltePreprocess({
17 | scss: {
18 | // includePaths: ['src/styles'],
19 |
20 | prependData: `
21 | @import "styles.scss";
22 | `,
23 | },
24 | }),
25 |
26 | compilerOptions: {
27 | outputFilename: 'bundle.js',
28 | cssOutputFilename: 'bundle.css',
29 | dev: !production,
30 | },
31 | }),
32 | ],
33 | // css: {
34 | // postcss: {
35 | // plugins: [autoprefixer()],
36 | // // extract: 'bundle.css',
37 | // },
38 | // },
39 | });
40 |
--------------------------------------------------------------------------------
/vue-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
19 | Vue App
20 |
21 |
22 |
23 |
24 | We're sorry but vue-app doesn't work properly without JavaScript
26 | enabled. Please enable it to continue.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/vue-app/src/store/modules/discounts.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import API from '../config';
3 | import { parseList } from './action-utils';
4 | import { GET_DISCOUNTS } from './mutation-types';
5 |
6 | const captains = console;
7 |
8 | export default {
9 | strict: process.env.NODE_ENV !== 'production',
10 | namespaced: true,
11 | state: {
12 | discounts: [],
13 | },
14 | mutations: {
15 | [GET_DISCOUNTS](state, discounts) {
16 | state.discounts = discounts;
17 | },
18 | },
19 | actions: {
20 | // actions let us get to ({ state, getters, commit, dispatch }) {
21 | async getDiscountsAction({ commit }) {
22 | try {
23 | const response = await axios.get(`${API}/discounts`);
24 | const discounts = parseList(response);
25 | commit(GET_DISCOUNTS, discounts);
26 | return discounts;
27 | } catch (error) {
28 | captains.error(error);
29 | throw new Error(error);
30 | }
31 | },
32 | },
33 | getters: {
34 | discounts: (state) => state.discounts,
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/vue-app/public/staticwebapp.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platform": { "apiRuntime": "node:18" },
3 | "routes": [
4 | {
5 | "route": "/api/products/*",
6 | "methods": ["GET", "PUT", "POST", "DELETE"],
7 | "allowedRoles": ["authenticated"]
8 | },
9 | {
10 | "route": "/api/discounts/*",
11 | "allowedRoles": ["preferred"]
12 | },
13 | {
14 | "route": "/api/*",
15 | "allowedRoles": ["authenticated"]
16 | },
17 | {
18 | "route": "/logout",
19 | "redirect": "/.auth/logout"
20 | },
21 | {
22 | "route": "/deals",
23 | "redirect": "/some-legacy-discounts-page.html",
24 | "statusCode": 301
25 | },
26 | {
27 | "route": "/.auth/login/aad",
28 | "statusCode": 404
29 | }
30 | ],
31 | "navigationFallback": {
32 | "rewrite": "index.html",
33 | "exclude": ["/*.{css,scss,js,png,gif,ico,jpg}"]
34 | },
35 | "responseOverrides": {
36 | "404": {
37 | "rewrite": "/404.html"
38 | }
39 | },
40 | "mimeTypes": {
41 | ".json": "text/json"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/react-app/public/staticwebapp.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platform": { "apiRuntime": "node:18" },
3 | "routes": [
4 | {
5 | "route": "/api/products/*",
6 | "methods": ["GET", "PUT", "POST", "DELETE"],
7 | "allowedRoles": ["authenticated"]
8 | },
9 | {
10 | "route": "/api/discounts/*",
11 | "allowedRoles": ["preferred"]
12 | },
13 | {
14 | "route": "/api/*",
15 | "allowedRoles": ["authenticated"]
16 | },
17 | {
18 | "route": "/logout",
19 | "redirect": "/.auth/logout"
20 | },
21 | {
22 | "route": "/deals",
23 | "redirect": "/some-legacy-discounts-page.html",
24 | "statusCode": 301
25 | },
26 | {
27 | "route": "/.auth/login/aad",
28 | "statusCode": 404
29 | }
30 | ],
31 | "navigationFallback": {
32 | "rewrite": "index.html",
33 | "exclude": ["/*.{css,scss,js,png,gif,ico,jpg}"]
34 | },
35 | "responseOverrides": {
36 | "404": {
37 | "rewrite": "/404.html"
38 | }
39 | },
40 | "mimeTypes": {
41 | ".json": "text/json"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/svelte-app/public/staticwebapp.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platform": { "apiRuntime": "node:18" },
3 | "routes": [
4 | {
5 | "route": "/api/products/*",
6 | "methods": ["GET", "PUT", "POST", "DELETE"],
7 | "allowedRoles": ["authenticated"]
8 | },
9 | {
10 | "route": "/api/discounts/*",
11 | "allowedRoles": ["preferred"]
12 | },
13 | {
14 | "route": "/api/*",
15 | "allowedRoles": ["authenticated"]
16 | },
17 | {
18 | "route": "/logout",
19 | "redirect": "/.auth/logout"
20 | },
21 | {
22 | "route": "/deals",
23 | "redirect": "/some-legacy-discounts-page.html",
24 | "statusCode": 301
25 | },
26 | {
27 | "route": "/.auth/login/aad",
28 | "statusCode": 404
29 | }
30 | ],
31 | "navigationFallback": {
32 | "rewrite": "index.html",
33 | "exclude": ["/*.{css,scss,js,png,gif,ico,jpg}"]
34 | },
35 | "responseOverrides": {
36 | "404": {
37 | "rewrite": "/404.html"
38 | }
39 | },
40 | "mimeTypes": {
41 | ".json": "text/json"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/angular-app/src/assets/staticwebapp.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platform": { "apiRuntime": "node:18" },
3 | "routes": [
4 | {
5 | "route": "/api/products/*",
6 | "methods": ["GET", "PUT", "POST", "DELETE"],
7 | "allowedRoles": ["authenticated"]
8 | },
9 | {
10 | "route": "/api/discounts/*",
11 | "allowedRoles": ["preferred"]
12 | },
13 | {
14 | "route": "/api/*",
15 | "allowedRoles": ["authenticated"]
16 | },
17 | {
18 | "route": "/logout",
19 | "redirect": "/.auth/logout"
20 | },
21 | {
22 | "route": "/deals",
23 | "redirect": "/some-legacy-discounts-page.html",
24 | "statusCode": 301
25 | },
26 | {
27 | "route": "/.auth/login/aad",
28 | "statusCode": 404
29 | }
30 | ],
31 | "navigationFallback": {
32 | "rewrite": "index.html",
33 | "exclude": ["/*.{css,scss,js,png,gif,ico,jpg}", "/images/*"]
34 | },
35 | "responseOverrides": {
36 | "404": {
37 | "rewrite": "/404.html"
38 | }
39 | },
40 | "mimeTypes": {
41 | ".json": "text/json"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/angular-app/src/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../coverage'),
20 | reports: ['html', 'lcovonly'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false
30 | });
31 | };
--------------------------------------------------------------------------------
/vue-app/src/components/button-footer.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
52 |
53 |
--------------------------------------------------------------------------------
/angular-app/src/app/products/product.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Product, API } from '../core';
3 | import { HttpClient } from '@angular/common/http';
4 | import { Observable } from 'rxjs';
5 |
6 | @Injectable({ providedIn: 'root' })
7 | export class ProductService {
8 | private readonly apiUrl = `${API}/products`;
9 |
10 | constructor(private http: HttpClient) {}
11 |
12 | getProducts(): Observable {
13 | return this.http.get(this.apiUrl);
14 | }
15 |
16 | getProduct(id: number): Observable {
17 | const url = `${this.apiUrl}/${id}`;
18 | return this.http.get(url);
19 | }
20 |
21 | updateProduct(product: Product): Observable {
22 | const url = `${this.apiUrl}/${product.id}`;
23 | return this.http.put(url, product);
24 | }
25 |
26 | deleteProduct(product: Product): Observable {
27 | const url = `${this.apiUrl}/${product.id}`;
28 | return this.http.delete(url);
29 | }
30 |
31 | addProduct(product: Product): Observable {
32 | return this.http.post(this.apiUrl, product);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/vue-app/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: ['@vue/airbnb', 'plugin:vue/essential', '@vue/prettier'],
7 | plugins: ['prettier'],
8 | // watch this for explaining why some of this is here
9 | // https://www.youtube.com/watch?time_continue=239&v=YIvjKId9m2c
10 | rules: {
11 | 'no-console': 'off', // process.env.NODE_ENV === 'production' ? 'error' : 'off',
12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
13 | 'consistent-return': 0,
14 | quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }],
15 | 'prettier/prettier': [
16 | 'error',
17 | {
18 | trailingComma: 'all',
19 | singleQuote: true,
20 | printWidth: 80,
21 | },
22 | ],
23 | 'vue/no-unused-components': [
24 | 'error',
25 | {
26 | ignoreWhenBindingPresent: true,
27 | },
28 | ],
29 | 'vuejs-accessibility/label-has-for': 'off',
30 | 'vue/multi-word-component-names':'off',
31 | 'no-restricted-exports': 'off',
32 | },
33 | parserOptions: {
34 | parser: '@babel/eslint-parser',
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/vue-app/src/components/modal.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
45 |
46 |
--------------------------------------------------------------------------------
/angular-app/src/app/shared/list-header.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-list-header',
5 | template: `
6 |
7 |
8 | {{ title }}
9 |
10 |
16 |
17 |
18 |
23 |
24 |
25 |
26 | `,
27 | })
28 | export class ListHeaderComponent implements OnInit {
29 | @Input() title: string;
30 | @Input() showAdd: boolean = true;
31 | @Output() add = new EventEmitter();
32 | @Output() refresh = new EventEmitter();
33 |
34 | ngOnInit() {}
35 |
36 | handleAdd() {
37 | this.add.emit();
38 | }
39 | handleRefresh() {
40 | this.refresh.emit();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/angular-app/src/app/shared/modal.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, OnInit, Input, Output } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-modal',
5 | template: `
6 |
21 | `
22 | })
23 | export class ModalComponent implements OnInit {
24 | @Input() message;
25 | @Input() isOpen = false;
26 | @Output() handleYes = new EventEmitter();
27 | @Output() handleNo = new EventEmitter();
28 |
29 | ngOnInit() {}
30 |
31 | onNo = () => {
32 | this.handleNo.emit();
33 | }
34 |
35 | onYes = () => {
36 | this.handleYes.emit();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) John Papa.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/svelte-app/src/Home.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
Shop at Home
8 |
9 | Manage your shopping list! Become a preferred customer and gain access to
10 | discount codes, too.
11 |
12 |
Log in to start enjoying your benefits
13 |
14 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/vue-app/src/components/list-header.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 | {{ title }}
33 |
34 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/react-app/src/products/useProducts.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import {
5 | addProductAction,
6 | deleteProductAction,
7 | loadProductsAction,
8 | selectProductAction,
9 | updateProductAction,
10 | } from '../store';
11 |
12 | /** Custom hook for accessing Product state in redux store */
13 | function useProducts() {
14 | const dispatch = useDispatch();
15 |
16 | return {
17 | // Selectors
18 | products: useSelector((state) => state.products.data),
19 | selectedProduct: useSelector((state) => state.selectedProduct),
20 | error: useSelector((state) => state.products.error),
21 |
22 | // Dispatchers
23 | // Wrap any dispatcher that could be called within a useEffect() in a useCallback()
24 | addProduct: (product) => dispatch(addProductAction(product)),
25 | deleteProduct: (product) => dispatch(deleteProductAction(product)),
26 | getProducts: useCallback(() => dispatch(loadProductsAction()), [dispatch]), // called within a useEffect()
27 | selectProduct: (product) => dispatch(selectProductAction(product)),
28 | updateProduct: (product) => dispatch(updateProductAction(product)),
29 | };
30 | }
31 |
32 | export default useProducts;
33 |
--------------------------------------------------------------------------------
/svelte-app/src/global.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | position: relative;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | body {
9 | color: #333;
10 | margin: 0;
11 | padding: 8px;
12 | box-sizing: border-box;
13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
14 | Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
15 | }
16 |
17 | a {
18 | color: rgb(0, 100, 200);
19 | text-decoration: none;
20 | }
21 |
22 | a:hover {
23 | text-decoration: underline;
24 | }
25 |
26 | a:visited {
27 | color: rgb(0, 80, 160);
28 | }
29 |
30 | label {
31 | display: block;
32 | }
33 |
34 | input,
35 | button,
36 | select,
37 | textarea {
38 | font-family: inherit;
39 | font-size: inherit;
40 | padding: 0.4em;
41 | margin: 0 0 0.5em 0;
42 | box-sizing: border-box;
43 | border: 1px solid #ccc;
44 | border-radius: 2px;
45 | }
46 |
47 | input:disabled {
48 | color: #ccc;
49 | }
50 |
51 | input[type='range'] {
52 | height: 0;
53 | }
54 |
55 | button {
56 | color: #333;
57 | background-color: #f4f4f4;
58 | outline: none;
59 | }
60 |
61 | button:disabled {
62 | color: #999;
63 | }
64 |
65 | button:not(:disabled):active {
66 | background-color: #eee;
67 | }
68 |
69 | button:focus {
70 | border-color: #666;
71 | }
72 |
--------------------------------------------------------------------------------
/vue-app/src/views/home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Shop at Home
5 |
6 | Manage your shopping list! Become a preferred customer and gain access
7 | to discount codes, too.
8 |
9 |
Log in to start enjoying your benefits
10 |
11 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/react-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import { applyMiddleware, compose, createStore } from 'redux';
6 | import createSagaMiddleware from 'redux-saga';
7 | import App from './App';
8 | import './index.css';
9 | import * as serviceWorker from './serviceWorker';
10 | import app, { productSaga, discountSaga } from './store';
11 |
12 | // create and configure reduxer middleware ( saga is a middleware )
13 | const sagaMiddleware = createSagaMiddleware();
14 |
15 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
16 | const store = createStore(
17 | app,
18 | composeEnhancers(applyMiddleware(sagaMiddleware)),
19 | );
20 |
21 | sagaMiddleware.run(productSaga);
22 | sagaMiddleware.run(discountSaga);
23 |
24 | ReactDOM.render(
25 |
26 |
27 |
28 |
29 | ,
30 |
31 | document.getElementById('root'),
32 | );
33 |
34 | // If you want your app to work offline and load faster, you can change
35 | // unregister() to register() below. Note this comes with some pitfalls.
36 | // Learn more about service workers: http://bit.ly/CRA-PWA
37 | serviceWorker.unregister();
38 |
--------------------------------------------------------------------------------
/react-app/src/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Home = () => (
4 |
5 |
6 |
Shop at Home
7 |
8 | Manage your shopping list! Become a preferred customer and gain access
9 | to discount codes, too.
10 |
11 |
Log in to start enjoying your benefits
12 |
13 |
{' '}
37 |
38 |
39 | );
40 |
41 | export default Home;
42 |
--------------------------------------------------------------------------------
/svelte-app/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import type { Writable } from 'svelte/store';
2 | import { writable } from 'svelte/store';
3 | import type { Discount, Product } from '../models';
4 |
5 | interface AppState {
6 | discounts: Writable;
7 | products: Writable;
8 | }
9 |
10 | const state: AppState = {
11 | discounts: writable([]),
12 | products: writable([]),
13 | };
14 |
15 | const getDiscounts = (discounts: Discount[]) => {
16 | state.discounts.update((/* old: Discount[] */) => discounts);
17 | };
18 | const getProducts = (products: Product[]) => {
19 | state.products.update((/* old: Product[] */) => products);
20 | };
21 |
22 | const addProduct = (product: Product) => {
23 | state.products.update((old: Product[]) => {
24 | old.unshift(product);
25 | return old;
26 | });
27 | };
28 |
29 | const deleteProduct = (product: Product) => {
30 | state.products.update((old: Product[]) => [
31 | ...old.filter((p) => p.id !== product.id),
32 | ]);
33 | };
34 |
35 | const updateProduct = (product: Product) => {
36 | state.products.update((old: Product[]) => {
37 | const index = old.findIndex((p) => p.id === product.id);
38 | old.splice(index, 1, product);
39 | return [...old];
40 | });
41 | };
42 |
43 | export {
44 | state,
45 | addProduct,
46 | getProducts,
47 | updateProduct,
48 | deleteProduct,
49 | getDiscounts,
50 | };
51 |
--------------------------------------------------------------------------------
/api/shared/product-data.js:
--------------------------------------------------------------------------------
1 | const data = {
2 | products: [
3 | {
4 | id: 10,
5 | name: 'Strawberries',
6 | description: '16oz package of fresh organic strawberries',
7 | quantity: '1',
8 | },
9 | {
10 | id: 20,
11 | name: 'Sliced bread',
12 | description: 'Loaf of fresh sliced wheat bread',
13 | quantity: 1,
14 | },
15 | {
16 | id: 30,
17 | name: 'Apples',
18 | description: 'Bag of 7 fresh McIntosh apples',
19 | quantity: 1,
20 | },
21 | ],
22 | };
23 |
24 | const getRandomInt = () => {
25 | const max = 1000;
26 | const min = 100;
27 | return Math.floor(Math.random() * Math.floor(max) + min);
28 | };
29 |
30 | const addProduct = (product) => {
31 | product.id = getRandomInt();
32 | data.products.push(product);
33 | return product;
34 | };
35 |
36 | const updateProduct = (product) => {
37 | const index = data.products.findIndex((v) => v.id === product.id);
38 | console.log(product);
39 | data.products.splice(index, 1, product);
40 | return product;
41 | };
42 |
43 | const deleteProduct = (id) => {
44 | const value = parseInt(id, 10);
45 | data.products = data.products.filter((v) => v.id !== value);
46 | return true;
47 | };
48 |
49 | const getProducts = () => {
50 | return data.products;
51 | };
52 |
53 | module.exports = { addProduct, updateProduct, deleteProduct, getProducts };
54 |
--------------------------------------------------------------------------------
/fastify-api-server/src/shared/product-data.js:
--------------------------------------------------------------------------------
1 | const data = {
2 | products: [
3 | {
4 | id: 10,
5 | name: 'Strawberries',
6 | description: '16oz package of fresh organic strawberries',
7 | quantity: '1',
8 | },
9 | {
10 | id: 20,
11 | name: 'Sliced bread',
12 | description: 'Loaf of fresh sliced wheat bread',
13 | quantity: 1,
14 | },
15 | {
16 | id: 30,
17 | name: 'Apples',
18 | description: 'Bag of 7 fresh McIntosh apples',
19 | quantity: 1,
20 | },
21 | ],
22 | };
23 |
24 | const getRandomInt = () => {
25 | const max = 1000;
26 | const min = 100;
27 | return Math.floor(Math.random() * Math.floor(max) + min);
28 | };
29 |
30 | const addProduct = (product) => {
31 | product.id = getRandomInt();
32 | data.products.push(product);
33 | return product;
34 | };
35 |
36 | const updateProduct = (product) => {
37 | const index = data.products.findIndex((v) => v.id === product.id);
38 | console.log(product);
39 | data.products.splice(index, 1, product);
40 | return product;
41 | };
42 |
43 | const deleteProduct = (id) => {
44 | const value = parseInt(id, 10);
45 | data.products = data.products.filter((v) => v.id !== value);
46 | return true;
47 | };
48 |
49 | const getProducts = () => {
50 | return data.products;
51 | };
52 |
53 | module.exports = { addProduct, updateProduct, deleteProduct, getProducts };
54 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "azureFunctions.deploySubpath": "api",
3 | "azureFunctions.postDeployTask": "npm install",
4 | "azureFunctions.projectLanguage": "JavaScript",
5 | "azureFunctions.projectRuntime": "~3",
6 | "debug.internalConsoleOptions": "neverOpen",
7 | "azureFunctions.preDeployTask": "npm prune",
8 | "peacock.color": "#832561",
9 | "staticWebApps.appSubpath": "/svelte-app",
10 | "staticWebApps.apiSubpath": "api",
11 | "staticWebApps.outputSubpath": "public",
12 | "chat.promptFiles": true,
13 | "workbench.colorCustomizations": {
14 | "activityBar.activeBackground": "#832561",
15 | "activityBar.activeBorder": "#121907",
16 | "activityBar.background": "#832561",
17 | "activityBar.foreground": "#e7e7e7",
18 | "activityBar.inactiveForeground": "#e7e7e799",
19 | "activityBarBadge.background": "#121907",
20 | "activityBarBadge.foreground": "#e7e7e7",
21 | "sash.hoverBorder": "#832561",
22 | "statusBar.background": "#832561",
23 | "statusBar.foreground": "#e7e7e7",
24 | "statusBarItem.hoverBackground": "#ab307e",
25 | "statusBarItem.remoteBackground": "#832561",
26 | "statusBarItem.remoteForeground": "#e7e7e7",
27 | "titleBar.activeBackground": "#832561",
28 | "titleBar.activeForeground": "#e7e7e7",
29 | "titleBar.inactiveBackground": "#83256199",
30 | "titleBar.inactiveForeground": "#e7e7e799",
31 | "commandCenter.border": "#e7e7e799",
32 | "peacock.remoteColor": "832561"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/react-app/src/store/product.actions.js:
--------------------------------------------------------------------------------
1 | export const LOAD_PRODUCT = '[Products] LOAD_PRODUCT';
2 | export const LOAD_PRODUCT_SUCCESS = '[Products] LOAD_PRODUCT_SUCCESS';
3 | export const LOAD_PRODUCT_ERROR = '[Products] LOAD_PRODUCT_ERROR';
4 |
5 | export const UPDATE_PRODUCT = '[Products] UPDATE_PRODUCT';
6 | export const UPDATE_PRODUCT_SUCCESS = '[Products] UPDATE_PRODUCT_SUCCESS';
7 | export const UPDATE_PRODUCT_ERROR = '[Products] UPDATE_PRODUCT_ERROR';
8 |
9 | export const DELETE_PRODUCT = '[Products] DELETE_PRODUCT';
10 | export const DELETE_PRODUCT_SUCCESS = '[Products] DELETE_PRODUCT_SUCCESS';
11 | export const DELETE_PRODUCT_ERROR = '[Products] DELETE_PRODUCT_ERROR';
12 |
13 | export const ADD_PRODUCT = '[Products] ADD_PRODUCT';
14 | export const ADD_PRODUCT_SUCCESS = '[Products] ADD_PRODUCT_SUCCESS';
15 | export const ADD_PRODUCT_ERROR = '[Products] ADD_PRODUCT_ERROR';
16 |
17 | export const SELECT_PRODUCT = '[Product] SELECT_PRODUCT';
18 |
19 | export const selectProductAction = (product) => ({
20 | type: SELECT_PRODUCT,
21 | payload: product,
22 | });
23 | export const loadProductsAction = () => ({ type: LOAD_PRODUCT });
24 |
25 | export const updateProductAction = (product) => ({
26 | type: UPDATE_PRODUCT,
27 | payload: product,
28 | });
29 | export const deleteProductAction = (product) => ({
30 | type: DELETE_PRODUCT,
31 | payload: product,
32 | });
33 | export const addProductAction = (product) => ({
34 | type: ADD_PRODUCT,
35 | payload: product,
36 | });
37 |
--------------------------------------------------------------------------------
/angular-app/src/app/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-home',
5 | template: `
6 |
7 |
8 |
Shop at Home
9 |
10 | Manage your shopping list! Become a preferred customer and gain access
11 | to discount codes, too.
12 |
13 |
Log in to start enjoying your benefits
14 |
15 |
16 |
40 |
41 |
42 | `,
43 | })
44 | export class HomeComponent {}
45 |
--------------------------------------------------------------------------------
/react-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, lazy, Suspense } from 'react';
2 | import 'bulma/css/bulma.css';
3 | import './styles.scss';
4 | import { Route, Routes } from 'react-router-dom';
5 | import { HeaderBar, NavBar, NotFound } from './components';
6 | import Home from './Home';
7 |
8 | // const Products = withRouter(
9 | // lazy(() => import(/* webpackChunkName: "products" */ './products/Products')),
10 | // );
11 | const Products = lazy(() =>
12 | import(/* webpackChunkName: "products" */ './products/Products'),
13 | );
14 |
15 | const Discounts = lazy(() =>
16 | import(/* webpackChunkName: "discounts" */ './Discounts'),
17 | );
18 |
19 | class App extends Component {
20 | render() {
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | Loading...
}>
28 |
29 | } />
30 | {/* */}
31 | } />
32 | } />
33 | } />
34 | } />
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 | export default App;
45 |
--------------------------------------------------------------------------------
/fastify-api-server/src/routes/products.js:
--------------------------------------------------------------------------------
1 | const data = require('../shared/product-data');
2 |
3 | async function routes(fastify, options) {
4 | // Get all products
5 | fastify.get('/products', async (request, reply) => {
6 | const products = data.getProducts();
7 | return products;
8 | });
9 |
10 | // Get a single product
11 | fastify.get('/products/:id', async (request, reply) => {
12 | const product = data.getProduct(request.params.id);
13 | return product;
14 | });
15 |
16 | // Add a new product
17 | fastify.post('/products', async (request, reply) => {
18 | const product = {
19 | id: undefined,
20 | name: request.body.name,
21 | description: request.body.description,
22 | quantity: parseInt(request.body.quantity, 10),
23 | };
24 |
25 | const newProduct = data.addProduct(product);
26 | return newProduct;
27 | });
28 |
29 | // Update an existing product
30 | fastify.put('/products/:id', async (request, reply) => {
31 | const product = {
32 | id: parseInt(request.params.id, 10),
33 | name: request.body.name,
34 | description: request.body.description,
35 | quantity: parseInt(request.body.quantity, 10),
36 | };
37 | const updatedProduct = data.updateProduct(product);
38 | return updatedProduct;
39 | });
40 |
41 | // Delete a product
42 | fastify.delete('/products/:id', async (request, reply) => {
43 | const id = parseInt(request.params.id, 10);
44 |
45 | const deletedProduct = data.deleteProduct(id);
46 | return deletedProduct;
47 | });
48 | }
49 |
50 | module.exports = routes;
51 |
--------------------------------------------------------------------------------
/fastify-api-server/README.md:
--------------------------------------------------------------------------------
1 | # Fastify API Server
2 |
3 | This is a Fastify API server project.
4 |
5 | ## Project Structure
6 |
7 | The project has the following files:
8 |
9 | - `src/server.js`: This file is the entry point of the Fastify API server. It creates an instance of the Fastify server and sets up middleware, routes, and plugins.
10 | - `src/routes/index.js`: This file exports a function `setRoutes` which sets up the routes for the API server. It defines the various routes and their corresponding handlers.
11 | - `src/plugins/index.js`: This file exports a function `loadPlugins` which loads and registers plugins for the Fastify server. It can be used to add additional functionality to the server.
12 | - `test/server.test.js`: This file contains the tests for the API server. It can be used to test the routes and handlers defined in the `src/routes` directory.
13 | - `package.json`: This file is the configuration file for npm. It lists the dependencies and scripts for the project.
14 |
15 | ## Getting Started
16 |
17 | To get started with the Fastify API server, follow these steps:
18 |
19 | 1. Clone the repository: `git clone `
20 | 2. Install the dependencies: `npm install`
21 | 3. Start the server: `npm start`
22 |
23 | ## Testing
24 |
25 | To run the tests for the API server, use the following command:
26 |
27 | ```
28 | npm test
29 | ```
30 |
31 | ## License
32 |
33 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
34 | ```
35 |
36 | Please note that you may need to modify the `` placeholder in the `Getting Started` section with the actual URL of your repository.
--------------------------------------------------------------------------------
/svelte-app/src/Discounts.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 | {#if errorMessage}
29 |
{errorMessage}
30 | {/if}
31 | {#if !$discounts.length && !errorMessage}
32 |
Loading data ...
33 | {/if}
34 |
35 | {#each $discounts as { id, store, percentage, code }, _i (id)}
36 |
37 |
38 |
39 |
40 | Store:
41 | {store}
42 | Discount:
43 | {percentage}%
44 | Code:
45 | {code}
46 |
47 |
48 |
49 |
50 | {/each}
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/svelte-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "scripts": {
6 | "start": "sirv dist --cors --single --no-clear --port 5001",
7 | "validate": "svelte-check",
8 | "dev": "vite --port 5001",
9 | "build": "vite build",
10 | "preview": "vite preview",
11 | "check": "svelte-check --tsconfig ./tsconfig.json",
12 | "fastify-dev": "npm --prefix ../fastify-api-server start",
13 | "start-svelte-fastify": "concurrently \"npm run fastify-dev\" \"VITE_API=http://0.0.0.0:3000/api npm run dev\"",
14 | "start-svelte-func-swa": "npx @azure/static-web-apps-cli@latest start http://localhost:5001 --api-location ../api --run \"npm run dev\"",
15 | "start-svelte-fastify-swa": "concurrently \"npm run fastify-dev\" \"npx @azure/static-web-apps-cli@latest start http://0.0.0.0:5001 --api-devserver-url http://0.0.0.0:3000 --host=0.0.0.0 --run 'VITE_API=/api npm run dev -- --host 0.0.0.0'\""
16 | },
17 | "engines": {
18 | "node": ">=20.0.0"
19 | },
20 | "devDependencies": {
21 | "@sveltejs/vite-plugin-svelte": "^2.5.2",
22 | "@tsconfig/svelte": "^5.0.2",
23 | "autoprefixer": "^10.4.16",
24 | "concurrently": "^8.2.2",
25 | "prettier": "^3.1.0",
26 | "query-string": "^8.1.0",
27 | "sass": "^1.53.0",
28 | "svelte": "^4.2.3",
29 | "svelte-check": "^3.6.0",
30 | "svelte-preprocess": "^5.1.0",
31 | "svelte-routing": "^2.6.0",
32 | "tslib": "^2.6.2",
33 | "typescript": "^5.2.2",
34 | "vite": "^4.5.0"
35 | },
36 | "dependencies": {
37 | "@fortawesome/fontawesome-svg-core": "^1.2.35",
38 | "@fortawesome/free-solid-svg-icons": "^5.15.3",
39 | "bulma": "^0.9.2",
40 | "sirv-cli": "^2.0.0"
41 | }
42 | }
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (https://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 | .env.test
64 |
65 | # parcel-bundler cache (https://parceljs.org/)
66 | .cache
67 |
68 | # next.js build output
69 | .next
70 |
71 | # nuxt.js build output
72 | .nuxt
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless/
79 |
80 | # FuseBox cache
81 | .fusebox/
82 |
83 | # DynamoDB Local files
84 | .dynamodb/
85 |
86 | # TypeScript output
87 | dist
88 | out
89 |
90 | # Azure Functions artifacts
91 | bin
92 | obj
93 | appsettings.json
94 | local.settings.json
95 | .telemetry
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.179.0/containers/azure-functions-node
3 | {
4 | "name": "Azure Functions & Node.js",
5 | "build": {
6 | "dockerfile": "Dockerfile",
7 | // Update 'VARIANT' to pick a Node.js version: 10, 12
8 | "args": { "VARIANT": "14" }
9 | },
10 | "forwardPorts": [7071, 5001, 4280],
11 | "portsAttributes": {
12 | "localhost:4280": {
13 | "label": "SWA CLI Hosting the Svelte App and API",
14 | "onAutoForward": "openBrowser"
15 | },
16 | "7071": {
17 | "label": "Azure Functions API",
18 |
19 | },
20 | "5000": {
21 | "label": "Svelte App"
22 | }
23 | },
24 |
25 | // Set *default* container specific settings.json values on container create.
26 | "settings": {
27 | "terminal.integrated.shell.linux": "/bin/bash"
28 | },
29 |
30 | // Add the IDs of extensions you want installed when the container is created.
31 | "extensions": [
32 | "ms-azuretools.vscode-azurefunctions",
33 | "dbaeumer.vscode-eslint",
34 | "svelte.svelte-vscode",
35 | "esbenp.prettier-vscode",
36 | "github.vscode-pull-request-github",
37 | "ms-vscode.azure-account",
38 | "ms-azuretools.vscode-azureresourcegroups",
39 | "ms-azuretools.vscode-azurestaticwebapps"
40 | ],
41 |
42 | // Use 'postCreateCommand' to run commands after the container is created.
43 | "postCreateCommand": "(cd svelte-app && npm install) ; (cd api && npm install)",
44 | "postStartCommand": "(cd svelte-app && npm run dev) & (cd svelte-app && npm run local)",
45 |
46 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
47 | "remoteUser": "node"
48 | }
49 |
--------------------------------------------------------------------------------
/react-app/src/Discounts.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import useDiscounts from './useDiscounts';
3 | import { ListHeader } from './components';
4 |
5 | function Discounts() {
6 | const { getDiscounts, discounts, error: errorMessage } = useDiscounts();
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | await getDiscounts();
11 | };
12 | fetchData();
13 | }, [getDiscounts]);
14 |
15 | return (
16 |
17 |
23 |
24 | {errorMessage &&
{errorMessage}
}
25 | {(!discounts || !discounts.length) && !errorMessage && (
26 |
Loading data ...
27 | )}
28 |
29 | {discounts &&
30 | discounts.map((discount /*, index */) => (
31 |
32 |
33 |
34 |
35 | Store:
36 | {discount.store}
37 | Discount:
38 | {discount.percentage}%
39 | Code:
40 | {discount.code}
41 |
42 |
43 |
44 |
45 | ))}
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default Discounts;
53 |
--------------------------------------------------------------------------------
/svelte-app/src/products/ProductList.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 | {#if errorMessage}
33 |
{errorMessage}
34 | {/if}
35 | {#if !products.length && !errorMessage}
36 |
Loading data ...
37 | {/if}
38 |
39 | {#each products as { id, name, description }, i (id)}
40 |
41 |
42 |
43 |
57 |
58 |
59 | {/each}
60 |
61 |
62 |
--------------------------------------------------------------------------------
/vue-app/src/views/discounts.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
43 |
{{ errorMessage }}
44 |
Loading data ...
45 |
46 |
51 |
52 |
53 |
54 | Store: {{ discount.store }}
55 | Discount: {{ discount.percentage }}%
56 | Code: {{ discount.code }}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/svelte-app/src/assets/svelte.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-app/src/components/nav-bar.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
48 |
61 |
62 |
Welcome
63 |
{{ userInfo.userDetails }}
64 |
{{ userInfo.identityProvider }}
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/angular-app/src/app/core/components/nav.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { UserInfo } from '../model';
3 |
4 | @Component({
5 | selector: 'app-nav',
6 | template: `
7 |
21 |
32 |
33 |
Welcome
34 |
{{ userInfo?.userDetails }}
35 |
{{ userInfo?.identityProvider }}
36 |
37 | `,
38 | })
39 | export class NavComponent implements OnInit {
40 | providers = ['github', 'Microsoft Entra ID'];
41 | userInfo: UserInfo;
42 |
43 | async ngOnInit() {
44 | this.userInfo = await this.getUserInfo();
45 | }
46 |
47 | async getUserInfo() {
48 | try {
49 | const response = await fetch('/.auth/me');
50 | const payload = await response.json();
51 | const { clientPrincipal } = payload;
52 | return clientPrincipal;
53 | } catch (error) {
54 | console.error('No profile could be found');
55 | return undefined;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/angular-app/src/app/products/product-list.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | EventEmitter,
4 | Input,
5 | Output,
6 | ChangeDetectionStrategy,
7 | } from '@angular/core';
8 | import { Product } from '../core';
9 |
10 | @Component({
11 | selector: 'app-product-list',
12 | template: `
13 |
44 | `,
45 | changeDetection: ChangeDetectionStrategy.OnPush,
46 | })
47 | export class ProductListComponent {
48 | @Input() products: Product[];
49 | @Output() deleted = new EventEmitter();
50 | @Output() selected = new EventEmitter();
51 |
52 | trackByProduct(index: number, product: Product): number {
53 | return product.id;
54 | }
55 |
56 | selectProduct(product: Product) {
57 | this.selected.emit(product);
58 | }
59 |
60 | deleteProduct(product: Product) {
61 | this.deleted.emit(product);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/svelte-app/src/store/product-data.ts:
--------------------------------------------------------------------------------
1 | import * as store from './store';
2 | import { parseItem, parseList } from './http-utils';
3 | import { API } from '../config';
4 | import type { Product } from '../models';
5 |
6 | export async function getProductsAction() {
7 | try {
8 | const response = await fetch(`${API}/products`, {
9 | method: 'GET',
10 | });
11 | const products: Product[] = await parseList(response);
12 | store.getProducts(products);
13 | return products;
14 | } catch (err) {
15 | console.log(err);
16 | throw new Error(err);
17 | }
18 | }
19 |
20 | export async function deleteProductAction(product: Product) {
21 | try {
22 | const response = await fetch(`${API}/products/${product.id}`, {
23 | method: 'DELETE',
24 | });
25 | await parseItem(response, 200);
26 | store.deleteProduct(product);
27 | return null;
28 | } catch (error) {
29 | console.error(error);
30 | }
31 | }
32 | export async function updateProductAction(product: Product) {
33 | try {
34 | const response = await fetch(`${API}/products/${product.id}`, {
35 | method: 'PUT',
36 | headers: {
37 | 'Content-Type': 'application/json',
38 | },
39 | body: JSON.stringify(product),
40 | });
41 | const updatedProduct: Product = await parseItem(response, 200);
42 | store.updateProduct(updatedProduct);
43 | return updatedProduct;
44 | } catch (error) {
45 | console.error(error);
46 | }
47 | }
48 | export async function addProductAction(product: Product) {
49 | try {
50 | const response = await fetch(`${API}/products`, {
51 | method: 'POST',
52 | headers: {
53 | 'Content-Type': 'application/json',
54 | },
55 | body: JSON.stringify(product),
56 | });
57 | const addedProduct: Product = await parseItem(response, 201);
58 | store.addProduct(addedProduct);
59 | return addedProduct;
60 | } catch (error) {
61 | console.error(error);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/.github/workflows/azure-static-web-apps-zealous-ground-07634b41e.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | types: [opened, synchronize, reopened, closed]
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build_and_deploy_job:
14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
15 | runs-on: ubuntu-latest
16 | name: Build and Deploy Job
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | submodules: true
21 | lfs: false
22 | - name: Build And Deploy
23 | id: builddeploy
24 | uses: Azure/static-web-apps-deploy@v1
25 | with:
26 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ZEALOUS_GROUND_07634B41E }}
27 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
28 | action: "upload"
29 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
30 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
31 | app_location: "./svelte-app" # App source code path
32 | api_location: "" # Api source code path - optional
33 | output_location: "." # Built app content directory - optional
34 | ###### End of Repository/Build Configurations ######
35 |
36 | close_pull_request_job:
37 | if: github.event_name == 'pull_request' && github.event.action == 'closed'
38 | runs-on: ubuntu-latest
39 | name: Close Pull Request Job
40 | steps:
41 | - name: Close Pull Request
42 | id: closepullrequest
43 | uses: Azure/static-web-apps-deploy@v1
44 | with:
45 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ZEALOUS_GROUND_07634B41E }}
46 | action: "close"
47 |
--------------------------------------------------------------------------------
/.github/workflows/azure-static-web-apps-thankful-ground-025b83e1e.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | types: [opened, synchronize, reopened, closed]
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build_and_deploy_job:
14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
15 | runs-on: ubuntu-latest
16 | name: Build and Deploy Job
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | submodules: true
21 | lfs: false
22 | - name: Build And Deploy
23 | id: builddeploy
24 | uses: Azure/static-web-apps-deploy@v1
25 | with:
26 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_THANKFUL_GROUND_025B83E1E }}
27 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
28 | action: "upload"
29 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
30 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
31 | app_location: "./svelte-app" # App source code path
32 | api_location: "" # Api source code path - optional
33 | output_location: "dist" # Built app content directory - optional
34 | ###### End of Repository/Build Configurations ######
35 |
36 | close_pull_request_job:
37 | if: github.event_name == 'pull_request' && github.event.action == 'closed'
38 | runs-on: ubuntu-latest
39 | name: Close Pull Request Job
40 | steps:
41 | - name: Close Pull Request
42 | id: closepullrequest
43 | uses: Azure/static-web-apps-deploy@v1
44 | with:
45 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_THANKFUL_GROUND_025B83E1E }}
46 | action: "close"
47 |
--------------------------------------------------------------------------------
/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
22 |
31 | React App
32 |
33 |
34 |
35 |
36 | You need to enable JavaScript to run this app.
37 |
38 |
39 |
40 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "react-scripts start",
7 | "build": "react-scripts build",
8 | "test": "react-scripts test",
9 | "eject": "react-scripts eject",
10 | "format": "prettier --write \"src/**/*.{js,jsx}\"",
11 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet",
12 | "fastify-dev": "npm --prefix ../fastify-api-server start",
13 | "start-react-fastify": "concurrently \"npm run fastify-dev\" \"VITE_API=http://0.0.0.0:3000/api npm run dev\"",
14 | "start-react-func-swa": "npx @azure/static-web-apps-cli@latest start http://localhost:3000 --api-location ../api --run \"npm run start\"",
15 | "start-react-fastify-swa": "concurrently \"npm run fastify-dev\" \"npx @azure/static-web-apps-cli@latest start http://0.0.0.0:3000 --api-devserver-url http://0.0.0.0:3000 --host=0.0.0.0 --run 'VITE_API=/api npm run start -- --host 0.0.0.0'\""
16 | },
17 | "eslintConfig": {
18 | "extends": "react-app"
19 | },
20 | "browserslist": [
21 | ">0.2%",
22 | "not dead",
23 | "not ie <= 11",
24 | "not op_mini all"
25 | ],
26 | "proxy": "http://localhost:7071/",
27 | "engines": {
28 | "node": ">=20.0.0"
29 | },
30 | "dependencies": {
31 | "@fortawesome/fontawesome-free": "^5.15.3",
32 | "axios": "^1.7.2",
33 | "bulma": "^0.9.2",
34 | "history": "^5.1.0",
35 | "react": "^17.0.2",
36 | "react-dom": "^17.0.2",
37 | "react-redux": "^7.2.3",
38 | "react-router-dom": "^6.0.0",
39 | "react-scripts": "^5.0.1",
40 | "redux": "^4.0.4",
41 | "redux-saga": "^1.0.5",
42 | "redux-thunk": "^2.3.0",
43 | "sass": "^1.43.4"
44 | },
45 | "devDependencies": {
46 | "babel-eslint": "^10.1.0",
47 | "eslint-config-airbnb": "^18.2.1",
48 | "eslint-config-prettier": "^8.1.0",
49 | "eslint-plugin-import": "^2.22.1",
50 | "eslint-plugin-jsx-a11y": "^6.4.1",
51 | "eslint-plugin-prettier": "^3.3.1",
52 | "eslint-plugin-react": "^7.23.1",
53 | "prettier": "^2.2.1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/vue-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve --port 8080",
7 | "build": "vue-cli-service build",
8 | "fastify-dev": "npm --prefix ../fastify-api-server start",
9 | "start-vue-fastify": "concurrently \"npm run fastify-dev\" \"VITE_API=http://0.0.0.0:3000/api npm run serve\"",
10 | "start-vue-func-swa": "npx @azure/static-web-apps-cli@latest start http://localhost:8080 --api-location ../api --run \"npm run serve\"",
11 | "start-vue-fastify-swa": "concurrently \"npm run fastify-dev\" \"npx @azure/static-web-apps-cli@latest start http://0.0.0.0:8080 --api-devserver-url http://0.0.0.0:3000 --host=0.0.0.0 --run 'VITE_API=/api npm run serve -- --host 0.0.0.0'\""
12 | },
13 | "engines": {
14 | "node": ">=20.0.0"
15 | },
16 | "dependencies": {
17 | "@fortawesome/fontawesome-svg-core": "^1.2.35",
18 | "@fortawesome/free-solid-svg-icons": "^5.15.3",
19 | "@fortawesome/vue-fontawesome": "^3.0.8",
20 | "axios": "^0.24.0",
21 | "bulma": "^0.9.2",
22 | "core-js": "^3.10.0",
23 | "vue": "^3.0.0",
24 | "vue-router": "^4.0.0",
25 | "vuex": "^4.0.0"
26 | },
27 | "devDependencies": {
28 | "@babel/core": "^7.24.7",
29 | "@babel/eslint-parser": "^7.24.7",
30 | "@babel/preset-env": "^7.24.7",
31 | "@vue/babel-preset-app": "^5.0.8",
32 | "@vue/cli-plugin-babel": "~5.0.8",
33 | "@vue/cli-plugin-eslint": "~5.0.8",
34 | "@vue/cli-plugin-router": "~5.0.8",
35 | "@vue/cli-service": "^5.0.0",
36 | "@vue/compiler-sfc": "^3.0.0",
37 | "@vue/eslint-config-airbnb": "^8.0.0",
38 | "@vue/eslint-config-prettier": "^8.0.0",
39 | "babel-eslint": "^10.1.0",
40 | "concurrently": "^8.2.2",
41 | "eslint": "^8.6.0",
42 | "eslint-plugin-import": "^2.29.1",
43 | "eslint-plugin-prettier": "^5.1.3",
44 | "eslint-plugin-vue": "^9.27.0",
45 | "prettier": "^3.3.2",
46 | "sass": "^1.43.4",
47 | "sass-loader": "^8.0.2",
48 | "vue-template-compiler": "^2.6.12",
49 | "webpack": "^5.92.1"
50 | }
51 | }
--------------------------------------------------------------------------------
/vue-app/src/views/products/product-list.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
{{ errorMessage }}
39 |
Loading data ...
40 |
41 |
46 |
47 |
51 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/react-app/src/products/ProductList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | import { ButtonFooter, CardContent } from '../components';
5 |
6 | function ProductList({
7 | handleDeleteProduct,
8 | handleSelectProduct,
9 | products,
10 | errorMessage,
11 | }) {
12 | const navigate = useNavigate();
13 |
14 | function selectProduct(e) {
15 | const product = getSelectedProduct(e);
16 | handleSelectProduct(product);
17 | navigate(`/products/${product.id}`, { state: {} });
18 | }
19 |
20 | function deleteProduct(e) {
21 | const product = getSelectedProduct(e);
22 | handleDeleteProduct(product);
23 | }
24 |
25 | function getSelectedProduct(e) {
26 | const index = +e.currentTarget.dataset.index;
27 | return products[index];
28 | }
29 |
30 | return (
31 |
32 | {errorMessage &&
{errorMessage}
}
33 | {(!products || !products.length) && !errorMessage && (
34 |
Loading data ...
35 | )}
36 |
66 |
67 | );
68 | }
69 |
70 | export default ProductList;
71 |
--------------------------------------------------------------------------------
/angular-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-app",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve --proxy-config proxy.conf.json --open",
7 | "build": "ng build --configuration production",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "fastify-dev": "npm --prefix ../fastify-api-server start",
11 | "start-angular-fastify": "concurrently \"npm run fastify-dev\" \"VITE_API=http://0.0.0.0:3000/api npm run dev\"",
12 | "start-angular-func-swa": "npx @azure/static-web-apps-cli@latest start http://localhost:4200 --api-location ../api --run \"npm run start\"",
13 | "start-angular-fastify-swa": "concurrently \"npm run fastify-dev\" \"npx @azure/static-web-apps-cli@latest start http://0.0.0.0:4200 --api-devserver-url http://0.0.0.0:3000 --host=0.0.0.0 --run 'VITE_API=/api npm run start -- --host 0.0.0.0'\""
14 | },
15 | "private": true,
16 | "engines": {
17 | "node": ">=20.0.0"
18 | },
19 | "dependencies": {
20 | "@angular/animations": "^18.2.6",
21 | "@angular/common": "^18.2.6",
22 | "@angular/compiler": "^18.2.6",
23 | "@angular/core": "^18.2.6",
24 | "@angular/forms": "^18.2.6",
25 | "@angular/platform-browser": "^18.2.6",
26 | "@angular/platform-browser-dynamic": "^18.2.6",
27 | "@angular/router": "^18.2.6",
28 | "@ngrx/data": "^18.0.2",
29 | "@ngrx/effects": "^18.0.2",
30 | "@ngrx/entity": "^18.0.2",
31 | "@ngrx/store": "^18.0.2",
32 | "@ngrx/store-devtools": "^18.0.2",
33 | "bulma": "^0.9.2",
34 | "font-awesome": "^4.7.0",
35 | "rxjs": "~7.5.0",
36 | "tslib": "^2.3.0",
37 | "zone.js": "~0.14.10",
38 | "@ngrx/operators": "^18.0.0"
39 | },
40 | "devDependencies": {
41 | "@angular-devkit/build-angular": "^18.2.6",
42 | "@angular/cli": "^18.2.6",
43 | "@angular/compiler-cli": "^18.2.6",
44 | "@angular/language-service": "^18.2.6",
45 | "@types/jasmine": "~3.10.0",
46 | "@types/node": "^16.0.0",
47 | "jasmine-core": "~4.0.0",
48 | "karma": "~6.3.0",
49 | "karma-chrome-launcher": "~3.1.0",
50 | "karma-coverage": "~2.1.0",
51 | "karma-jasmine": "~4.0.0",
52 | "karma-jasmine-html-reporter": "~1.7.0",
53 | "typescript": "~5.4.5"
54 | }
55 | }
--------------------------------------------------------------------------------
/.github/workflows/azure-static-web-apps-purple-cliff-0e5d9b80f.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - vue-app/**
9 | - api/**
10 | - .github/workflows/*purple-cliff*.yml
11 | pull_request:
12 | types: [opened, synchronize, reopened, closed]
13 | branches:
14 | - main
15 | paths:
16 | - vue-app/**
17 | - api/**
18 | - .github/workflows/*purple-cliff*.yml
19 |
20 | jobs:
21 | build_and_deploy_job:
22 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
23 | runs-on: ubuntu-latest
24 | name: Build and Deploy Job
25 | steps:
26 | - uses: actions/checkout@v2
27 | with:
28 | submodules: true
29 | - name: Build And Deploy
30 | id: builddeploy
31 | uses: Azure/static-web-apps-deploy@v1
32 | with:
33 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_CLIFF_0E5D9B80F }}
34 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
35 | action: "upload"
36 | ###### Repository/Build Configurations - These values can be configured to match you app requirements. ######
37 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
38 | app_location: "vue-app" # App source code path
39 | api_location: "api" # Api source code path - optional
40 | app_artifact_location: "dist" # Built app content directory - optional
41 | ###### End of Repository/Build Configurations ######
42 |
43 | close_pull_request_job:
44 | if: github.event_name == 'pull_request' && github.event.action == 'closed'
45 | runs-on: ubuntu-latest
46 | name: Close Pull Request Job
47 | steps:
48 | - name: Close Pull Request
49 | id: closepullrequest
50 | uses: Azure/static-web-apps-deploy@v1
51 | with:
52 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_CLIFF_0E5D9B80F }}
53 | action: "close"
54 |
--------------------------------------------------------------------------------
/.github/workflows/azure-static-web-apps-purple-pond-08f780f0f.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - svelte-app/**
9 | - api/**
10 | - .github/workflows/*purple-pond*.yml
11 | pull_request:
12 | types: [opened, synchronize, reopened, closed]
13 | branches:
14 | - main
15 | paths:
16 | - svelte-app/**
17 | - api/**
18 | - .github/workflows/*purple-pond*.yml
19 |
20 | jobs:
21 | build_and_deploy_job:
22 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
23 | runs-on: ubuntu-latest
24 | name: Build and Deploy Job
25 | steps:
26 | - uses: actions/checkout@v2
27 | with:
28 | submodules: true
29 | - name: Build And Deploy
30 | id: builddeploy
31 | uses: Azure/static-web-apps-deploy@v1
32 | with:
33 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_POND_08F780F0F }}
34 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
35 | action: "upload"
36 | ###### Repository/Build Configurations - These values can be configured to match you app requirements. ######
37 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
38 | app_location: "svelte-app" # App source code path
39 | api_location: "api" # Api source code path - optional
40 | app_artifact_location: "dist" # Built app content directory - optional
41 | ###### End of Repository/Build Configurations ######
42 |
43 | close_pull_request_job:
44 | if: github.event_name == 'pull_request' && github.event.action == 'closed'
45 | runs-on: ubuntu-latest
46 | name: Close Pull Request Job
47 | steps:
48 | - name: Close Pull Request
49 | id: closepullrequest
50 | uses: Azure/static-web-apps-deploy@v1
51 | with:
52 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_POND_08F780F0F }}
53 | action: "close"
54 |
--------------------------------------------------------------------------------
/.github/workflows/azure-static-web-apps-gentle-cliff-0bc570010.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - angular-app/**
9 | - api/**
10 | - .github/workflows/*gentle-cliff*.yml
11 | pull_request:
12 | types: [opened, synchronize, reopened, closed]
13 | branches:
14 | - main
15 | paths:
16 | - angular-app/**
17 | - api/**
18 | - .github/workflows/*gentle-cliff*.yml
19 |
20 | jobs:
21 | build_and_deploy_job:
22 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
23 | runs-on: ubuntu-latest
24 | name: Build and Deploy Job
25 | steps:
26 | - uses: actions/checkout@v2
27 | with:
28 | submodules: true
29 | - name: Build And Deploy
30 | id: builddeploy
31 | uses: Azure/static-web-apps-deploy@v1
32 | with:
33 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_CLIFF_0BC570010 }}
34 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
35 | action: "upload"
36 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
37 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
38 | app_location: "/angular-app" # App source code path
39 | api_location: "api" # Api source code path - optional
40 | output_location: "dist/angular-app" # Built app content directory - optional
41 | ###### End of Repository/Build Configurations ######
42 |
43 | close_pull_request_job:
44 | if: github.event_name == 'pull_request' && github.event.action == 'closed'
45 | runs-on: ubuntu-latest
46 | name: Close Pull Request Job
47 | steps:
48 | - name: Close Pull Request
49 | id: closepullrequest
50 | uses: Azure/static-web-apps-deploy@v1
51 | with:
52 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_CLIFF_0BC570010 }}
53 | action: "close"
54 |
--------------------------------------------------------------------------------
/.github/workflows/azure-static-web-apps-brave-mushroom-0741e3c1e.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - react-app/**
9 | - api/**
10 | - .github/workflows/*purple-pond*.yml
11 | pull_request:
12 | types: [opened, synchronize, reopened, closed]
13 | branches:
14 | - main
15 | paths:
16 | - react-app/**
17 | - api/**
18 | - .github/workflows/*brave-mushroom*.yml
19 |
20 | jobs:
21 | build_and_deploy_job:
22 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
23 | runs-on: ubuntu-latest
24 | name: Build and Deploy Job
25 | steps:
26 | - uses: actions/checkout@v3
27 | with:
28 | submodules: true
29 | lfs: false
30 | - name: Build And Deploy
31 | id: builddeploy
32 | uses: Azure/static-web-apps-deploy@v1
33 | with:
34 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_BRAVE_MUSHROOM_0741E3C1E }}
35 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
36 | action: "upload"
37 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
38 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
39 | app_location: "./react-app" # App source code path
40 | api_location: "" # Api source code path - optional
41 | output_location: "build" # Built app content directory - optional
42 | ###### End of Repository/Build Configurations ######
43 |
44 | close_pull_request_job:
45 | if: github.event_name == 'pull_request' && github.event.action == 'closed'
46 | runs-on: ubuntu-latest
47 | name: Close Pull Request Job
48 | steps:
49 | - name: Close Pull Request
50 | id: closepullrequest
51 | uses: Azure/static-web-apps-deploy@v1
52 | with:
53 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_BRAVE_MUSHROOM_0741E3C1E }}
54 | action: "close"
55 |
--------------------------------------------------------------------------------
/api/README.md:
--------------------------------------------------------------------------------
1 | # Azure Functions API
2 |
3 | This project is an Azure Functions app, that responds to GET, POST, PUT, and DELETE endpoints for products.
4 |
5 | ## Learn how
6 |
7 | Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/en-us/learn/modules/publish-app-service-static-web-app-api/?WT.mc_id=shopathome-github-jopapa)
8 |
9 | ## Getting Started
10 |
11 | 1. Create a repository from this template repository
12 |
13 | 1. Enter the name of your new repository as _mslearn-staticwebapp_
14 |
15 | 1. Clone your new repository
16 |
17 | ```bash
18 | git clone https://github.com/your-github-organization/mslearn-staticwebapp
19 | cd mslearn-staticwebapp/api
20 | ```
21 |
22 | 1. Create the file `api/local.settings.json` and modify its contents as follows:
23 |
24 | ```json
25 | {
26 | "IsEncrypted": false,
27 | "Values": {
28 | "AzureWebJobsStorage": "",
29 | "FUNCTIONS_WORKER_RUNTIME": "node"
30 | },
31 | "Host": {
32 | "CORS": "http://localhost:3000,http://localhost:4200,http://localhost:5000,http://localhost:8080"
33 | }
34 | }
35 | ```
36 |
37 | 1. Run the app
38 |
39 | ```bash
40 | npm start
41 | ```
42 |
43 | ## Resources
44 |
45 | - [Azure Free Trial](https://azure.microsoft.com/en-us/free/?wt.mc_id=mslearn_shopathome-github-jopapa)
46 | - [VS Code](https://code.visualstudio.com?wt.mc_id=mslearn_shopathome-github-jopapa)
47 | - [VS Code Extension for Node on Azure](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack&WT.mc_id=mslearn_shopathome-github-jopapa)
48 | - Azure Functions [local.settings.json](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#local-settings-file?WT.mc_id=mslearn_shopathome-github-jopapa) file
49 |
50 | ### Debugging Resources
51 |
52 | - [Debugging Angular in VS Code](https://code.visualstudio.com/docs/nodejs/angular-tutorial?wt.mc_id=mslearn_shopathome-github-jopapa)
53 | - [Debugging React in VS Code](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial?wt.mc_id=mslearn_shopathome-github-jopapa)
54 | - [Debugging Vue in VS Code](https://code.visualstudio.com/docs/nodejs/vuejs-tutorial?wt.mc_id=mslearn_shopathome-github-jopapa)
55 |
--------------------------------------------------------------------------------
/angular-app/src/app/discounts.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { Discount } from './core';
3 | import { Observable, catchError } from 'rxjs';
4 | import { DiscountService } from './discount.service';
5 |
6 | @Component({
7 | selector: 'app-discount',
8 | template: `
9 |
10 |
11 |
16 |
{{ errorMessage }}
17 |
18 |
19 | Loading data ...
20 |
21 |
22 |
30 |
31 |
32 |
33 | Store: {{ discount.store }}
34 | Discount: {{ discount.percentage }}% Code: {{ discount.code }}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | `,
46 | })
47 | export class DiscountComponent {
48 | errorMessage: string;
49 | showAdd = false;
50 | discounts$: Observable;
51 |
52 | constructor(private discountService: DiscountService) {}
53 |
54 | ngOnInit() {
55 | this.getDiscounts();
56 | }
57 |
58 | getDiscounts() {
59 | this.errorMessage = undefined;
60 | this.discounts$ = this.discountService.getDiscounts().pipe(
61 | catchError((error: any) => {
62 | this.errorMessage = 'Unauthorized';
63 | return [];
64 | }),
65 | );
66 | }
67 |
68 | trackByDiscount(index: number, discount: Discount): number {
69 | return discount.id;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/svelte-app/src/components/NavBar.svelte:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
54 |
67 | {#if userInfo}
68 |
69 |
Welcome
70 |
{userInfo && userInfo.userDetails}
71 |
{userInfo && userInfo.identityProvider}
72 |
73 | {/if}
74 |
75 |
--------------------------------------------------------------------------------
/angular-app/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags.ts';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/react-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/react-app/src/store/product.saga.js:
--------------------------------------------------------------------------------
1 | import { put, takeEvery, call, all } from 'redux-saga/effects';
2 | import {
3 | LOAD_PRODUCT,
4 | LOAD_PRODUCT_SUCCESS,
5 | LOAD_PRODUCT_ERROR,
6 | UPDATE_PRODUCT,
7 | UPDATE_PRODUCT_SUCCESS,
8 | UPDATE_PRODUCT_ERROR,
9 | DELETE_PRODUCT,
10 | DELETE_PRODUCT_SUCCESS,
11 | DELETE_PRODUCT_ERROR,
12 | ADD_PRODUCT,
13 | ADD_PRODUCT_SUCCESS,
14 | ADD_PRODUCT_ERROR,
15 | } from './product.actions';
16 | import {
17 | addProductApi,
18 | deleteProductApi,
19 | loadProductsApi,
20 | updateProductApi,
21 | } from './product.api';
22 |
23 | export function* loadingProductsAsync() {
24 | try {
25 | const data = yield call(loadProductsApi);
26 | const productes = [...data];
27 |
28 | yield put({ type: LOAD_PRODUCT_SUCCESS, payload: productes });
29 | } catch (err) {
30 | yield put({ type: LOAD_PRODUCT_ERROR, payload: err.message });
31 | }
32 | }
33 |
34 | export function* watchLoadingProductsAsync() {
35 | yield takeEvery(LOAD_PRODUCT, loadingProductsAsync);
36 | }
37 |
38 | export function* updatingProductAsync({ payload }) {
39 | try {
40 | const data = yield call(updateProductApi, payload);
41 | const updatedProduct = data;
42 |
43 | yield put({ type: UPDATE_PRODUCT_SUCCESS, payload: updatedProduct });
44 | } catch (err) {
45 | yield put({ type: UPDATE_PRODUCT_ERROR, payload: err.message });
46 | }
47 | }
48 |
49 | export function* watchUpdatingProductAsync() {
50 | yield takeEvery(UPDATE_PRODUCT, updatingProductAsync);
51 | }
52 |
53 | export function* deletingProductAsync({ payload }) {
54 | try {
55 | yield call(deleteProductApi, payload);
56 |
57 | yield put({ type: DELETE_PRODUCT_SUCCESS, payload: null });
58 | } catch (err) {
59 | yield put({ type: DELETE_PRODUCT_ERROR, payload: err.message });
60 | }
61 | }
62 |
63 | export function* watchDeletingProductAsync() {
64 | yield takeEvery(DELETE_PRODUCT, deletingProductAsync);
65 | }
66 |
67 | export function* addingProductAsync({ payload }) {
68 | try {
69 | const data = yield call(addProductApi, payload);
70 | const addedProduct = data;
71 |
72 | yield put({ type: ADD_PRODUCT_SUCCESS, payload: addedProduct });
73 | } catch (err) {
74 | yield put({ type: ADD_PRODUCT_ERROR, payload: err.message });
75 | }
76 | }
77 |
78 | export function* watchAddingProductAsync() {
79 | yield takeEvery(ADD_PRODUCT, addingProductAsync);
80 | }
81 |
82 | export function* productSaga() {
83 | yield all([
84 | watchLoadingProductsAsync(),
85 | watchUpdatingProductAsync(),
86 | watchDeletingProductAsync(),
87 | watchAddingProductAsync(),
88 | ]);
89 | }
90 |
--------------------------------------------------------------------------------
/react-app/src/components/NavBar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { AuthLogin } from './AuthLogin';
4 | import { AuthLogout } from './AuthLogout';
5 |
6 | const captains = console;
7 |
8 | function NavBar(props) {
9 | const providers = ['github', 'Microsoft Entra ID'];
10 | const [userInfo, setUserInfo] = useState();
11 |
12 | useEffect(() => {
13 | (async () => {
14 | setUserInfo(await getUserInfo());
15 | })();
16 | }, []);
17 |
18 | async function getUserInfo() {
19 | try {
20 | const response = await fetch('/.auth/me');
21 | const payload = await response.json();
22 | const { clientPrincipal } = payload;
23 | return clientPrincipal;
24 | } catch (error) {
25 | captains.error('No profile could be found');
26 | return undefined;
27 | }
28 | }
29 |
30 | return (
31 |
32 |
33 | Menu
34 |
35 |
38 | 'nav-link' + (isActive ? ' active-link' : '')
39 | }
40 | >
41 | Home
42 |
43 |
46 | 'nav-link' + (isActive ? ' active-link' : '')
47 | }
48 | >
49 | My List
50 |
51 |
54 | 'nav-link' + (isActive ? ' active-link' : '')
55 | }
56 | >
57 | My Discounts
58 |
59 |
60 | {props.children}
61 |
62 |
63 | Auth
64 |
65 | {!userInfo && (
66 |
67 | {providers.map((provider) => (
68 |
69 | ))}
70 |
71 | )}
72 | {userInfo && (
73 |
76 | )}
77 |
78 |
79 | {userInfo && (
80 |
81 |
82 |
Welcome
83 |
{userInfo && userInfo.userDetails}
84 |
{userInfo && userInfo.identityProvider}
85 |
86 |
87 | )}
88 |
89 | );
90 | }
91 | export default NavBar;
92 |
--------------------------------------------------------------------------------
/vue-app/src/store/modules/products.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import API from '../config';
3 | import { parseItem, parseList } from './action-utils';
4 | import {
5 | ADD_PRODUCT,
6 | DELETE_PRODUCT,
7 | GET_PRODUCTS,
8 | UPDATE_PRODUCT,
9 | } from './mutation-types';
10 |
11 | const captains = console;
12 |
13 | export default {
14 | strict: process.env.NODE_ENV !== 'production',
15 | namespaced: true,
16 | state: {
17 | products: [],
18 | },
19 | mutations: {
20 | [ADD_PRODUCT](state, product) {
21 | state.products.unshift(product);
22 | },
23 | [UPDATE_PRODUCT](state, product) {
24 | const index = state.products.findIndex((v) => v.id === product.id);
25 | state.products.splice(index, 1, product);
26 | state.products = [...state.products];
27 | },
28 | [GET_PRODUCTS](state, products) {
29 | state.products = products;
30 | },
31 | [DELETE_PRODUCT](state, product) {
32 | state.products = [...state.products.filter((p) => p.id !== product.id)];
33 | },
34 | },
35 | actions: {
36 | // actions let us get to ({ state, getters, commit, dispatch }) {
37 | async getProductsAction({ commit }) {
38 | try {
39 | const response = await axios.get(`${API}/products`);
40 | const products = parseList(response);
41 | commit(GET_PRODUCTS, products);
42 | return products;
43 | } catch (error) {
44 | captains.error(error);
45 | throw new Error(error);
46 | }
47 | },
48 | async deleteProductAction({ commit }, product) {
49 | try {
50 | const response = await axios.delete(`${API}/products/${product.id}`);
51 | parseItem(response, 200);
52 | commit(DELETE_PRODUCT, product);
53 | return null;
54 | } catch (error) {
55 | captains.error(error);
56 | throw new Error(error);
57 | }
58 | },
59 | async updateProductAction({ commit }, product) {
60 | try {
61 | const response = await axios.put(
62 | `${API}/products/${product.id}`,
63 | product,
64 | );
65 | const updatedproduct = parseItem(response, 200);
66 | commit(UPDATE_PRODUCT, updatedproduct);
67 | return updatedproduct;
68 | } catch (error) {
69 | captains.error(error);
70 | throw new Error(error);
71 | }
72 | },
73 | async addProductAction({ commit }, product) {
74 | try {
75 | const response = await axios.post(`${API}/products`, product);
76 | const addedProduct = parseItem(response, 201);
77 | commit(ADD_PRODUCT, addedProduct);
78 | return addedProduct;
79 | } catch (error) {
80 | captains.error(error);
81 | throw new Error(error);
82 | }
83 | },
84 | },
85 | getters: {
86 | products: (state) => state.products,
87 | },
88 | };
89 |
--------------------------------------------------------------------------------
/angular-app/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": ["node_modules/codelyzer"],
3 | "rules": {
4 | "arrow-return-shorthand": true,
5 | "callable-types": true,
6 | "class-name": true,
7 | "comment-format": [true, "check-space"],
8 | "curly": true,
9 | "deprecation": {
10 | "severity": "warn"
11 | },
12 | "eofline": true,
13 | "forin": true,
14 | "import-blacklist": [true],
15 | "import-spacing": true,
16 | "indent": [true, "spaces"],
17 | "interface-over-type-literal": true,
18 | "label-position": true,
19 | "max-line-length": [true, 140],
20 | "member-access": false,
21 | "member-ordering": [
22 | true,
23 | {
24 | "order": [
25 | "static-field",
26 | "instance-field",
27 | "static-method",
28 | "instance-method"
29 | ]
30 | }
31 | ],
32 | "no-arg": true,
33 | "no-bitwise": true,
34 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
35 | "no-construct": true,
36 | "no-debugger": true,
37 | "no-duplicate-super": true,
38 | "no-empty": false,
39 | "no-empty-interface": true,
40 | "no-eval": true,
41 | "no-inferrable-types": [true, "ignore-params"],
42 | "no-misused-new": true,
43 | "no-non-null-assertion": true,
44 | "no-redundant-jsdoc": true,
45 | "no-shadowed-variable": true,
46 | "no-string-literal": false,
47 | "no-string-throw": true,
48 | "no-switch-case-fall-through": true,
49 | "no-trailing-whitespace": true,
50 | "no-unnecessary-initializer": true,
51 | "no-unused-expression": true,
52 | "no-var-keyword": true,
53 | "object-literal-sort-keys": false,
54 | "one-line": [
55 | true,
56 | "check-open-brace",
57 | "check-catch",
58 | "check-else",
59 | "check-whitespace"
60 | ],
61 | "prefer-const": true,
62 | "quotemark": [true, "single"],
63 | "radix": true,
64 | "semicolon": [true, "always"],
65 | "triple-equals": [true, "allow-null-check"],
66 | "typedef-whitespace": [
67 | true,
68 | {
69 | "call-signature": "nospace",
70 | "index-signature": "nospace",
71 | "parameter": "nospace",
72 | "property-declaration": "nospace",
73 | "variable-declaration": "nospace"
74 | }
75 | ],
76 | "unified-signatures": true,
77 | "variable-name": false,
78 | "whitespace": [
79 | true,
80 | "check-branch",
81 | "check-decl",
82 | "check-operator",
83 | "check-separator",
84 | "check-type"
85 | ],
86 | "no-output-on-prefix": true,
87 | "no-inputs-metadata-property": true,
88 | "no-outputs-metadata-property": true,
89 | "no-host-metadata-property": true,
90 | "no-input-rename": true,
91 | "no-output-rename": true,
92 | "use-lifecycle-interface": true,
93 | "use-pipe-transform-interface": true,
94 | "component-class-suffix": true,
95 | "directive-class-suffix": true
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/react-app/src/products/ProductDetail.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | import { ButtonFooter, InputDetail } from '../components';
5 |
6 | function ProductDetail({
7 | product: initProduct,
8 | handleCancelProduct,
9 | handleSaveProduct,
10 | }) {
11 | const [product, setProduct] = useState(Object.assign({}, initProduct));
12 | const navigate = useNavigate();
13 |
14 | useEffect(() => {
15 | if (!product) {
16 | navigate('/products', { state: {} }); // no product, bail out of Details
17 | }
18 | }, [product]);
19 |
20 | function handleSave() {
21 | const chgProduct = { ...product, id: product.id || null };
22 | handleSaveProduct(chgProduct);
23 | }
24 |
25 | function handleNameChange(e) {
26 | setProduct({ ...product, name: e.target.value });
27 | }
28 |
29 | function handleDescriptionChange(e) {
30 | setProduct({ ...product, description: e.target.value });
31 | }
32 |
33 | function handleQuantityChange(e) {
34 | setProduct({ ...product, quantity: e.target.value });
35 | }
36 |
37 | return (
38 |
39 |
45 |
46 |
47 | {product.id && (
48 |
49 | )}
50 |
56 |
62 |
63 |
64 | quantity
65 |
66 |
76 |
77 |
78 |
79 |
93 |
94 | );
95 | }
96 |
97 | export default ProductDetail;
98 |
--------------------------------------------------------------------------------
/vue-app/README.md:
--------------------------------------------------------------------------------
1 | # Static Web App with Vue
2 |
3 | This project was created to help represent a fundamental app written with Vue. The Shop at Home theme is used throughout the app. View it live at .
4 |
5 | ## Learn how
6 |
7 | Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/en-us/learn/modules/publish-app-service-static-web-app-api/?WT.mc_id=shopathome-github-jopapa)
8 |
9 | ## Install and Setup
10 |
11 | 1. Create a repository from this template repository
12 |
13 | 1. Enter the name of your new repository as _my-static-web-app_
14 |
15 | 1. Clone your new repository
16 |
17 | ```bash
18 | git clone https://github.com/your-github-organization/my-static-web-app
19 | cd my-static-web-app/vue-app
20 | ```
21 |
22 | 1. Install the npm packages
23 |
24 | ```bash
25 | npm install
26 | ```
27 |
28 | ## Getting Started - Running with Static Web Apps and Serverless Functions API
29 |
30 | 1. Run the app
31 |
32 | ```bash
33 | npm run start-vue-func-swa
34 | ```
35 |
36 | ## Getting Started - Running with Static Web Apps and Fastify API
37 |
38 | 1. Run the app
39 |
40 | ```bash
41 | npm run start-vue-fastify-swa
42 | ```
43 |
44 | ## Authentication / Authorization
45 |
46 | The app does not require authentication to launch or see the default page. However to view the products or discounts, the user must be authenticated using one of the options. These options are defined in the `/public/staticwebapp.config.json` file.
47 |
48 | | Endpoint | Roles |
49 | | ----------------- | --------------------------------------------- |
50 | | /api/\* | no auth |
51 | | /api/products/\* | authenticated users |
52 | | /api/discounts/\* | authenticated users with the _preferred_ role |
53 |
54 | ## Resources
55 |
56 | ### Azure Static Web Apps
57 |
58 | - Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/learn/modules/publish-app-service-static-web-app-api?wt.mc_id=shopathome-github-jopapa)
59 | - [API support in Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/apis?wt.mc_id=shopathome-github-jopapa)
60 | - [Add an API to Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/add-api?wt.mc_id=shopathome-github-jopapa)
61 | - [Authentication and authorization](https://docs.microsoft.com/azure/static-web-apps/authentication-authorization?wt.mc_id=shopathome-github-jopapa)
62 | - [Routes](https://docs.microsoft.com/azure/static-web-apps/routes?wt.mc_id=shopathome-github-jopapa)
63 | - [Review pre-production environments](https://docs.microsoft.com/azure/static-web-apps/review-publish-pull-requests?wt.mc_id=shopathome-github-jopapa)
64 | - [Azure Free Trial](https://azure.microsoft.com/free/?wt.mc_id=shopathome-github-jopapa)
65 |
--------------------------------------------------------------------------------
/react-app/README.md:
--------------------------------------------------------------------------------
1 | # Static Web App
2 |
3 | This project was created to help represent a fundamental app written with React. The Shop at Home theme is used throughout the app. View it live at .
4 |
5 | ## Learn how
6 |
7 | Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/en-us/learn/modules/publish-app-service-static-web-app-api/?WT.mc_id=shopathome-github-jopapa)
8 |
9 | ## Install and Setup
10 |
11 | 1. Create a repository from this template repository
12 |
13 | 1. Enter the name of your new repository as _my-static-web-app_
14 |
15 | 1. Clone your new repository
16 |
17 | ```bash
18 | git clone https://github.com/your-github-organization/my-static-web-app
19 | cd my-static-web-app/react-app
20 | ```
21 |
22 | 1. Install the npm packages
23 |
24 | ```bash
25 | npm install
26 | ```
27 |
28 | ## Getting Started - Running with Static Web Apps and Serverless Functions API
29 |
30 | 1. Run the app
31 |
32 | ```bash
33 | npm run start-react-func-swa
34 | ```
35 |
36 | ## Getting Started - Running with Static Web Apps and Fastify API
37 |
38 | 1. Run the app
39 |
40 | ```bash
41 | npm run start-react-fastify-swa
42 | ```
43 |
44 | ## Authentication / Authorization
45 |
46 | The app does not require authentication to launch or see the default page. However to view the products or discounts, the user must be authenticated using one of the options. These options are defined in the `/public/staticwebapp.config.json` file.
47 |
48 | | Endpoint | Roles |
49 | | ----------------- | --------------------------------------------- |
50 | | /api/\* | no auth |
51 | | /api/products/\* | authenticated users |
52 | | /api/discounts/\* | authenticated users with the _preferred_ role |
53 |
54 | ## Resources
55 |
56 | ### Azure Static Web Apps
57 |
58 | - Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/learn/modules/publish-app-service-static-web-app-api?wt.mc_id=shopathome-github-jopapa)
59 | - [API support in Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/apis?wt.mc_id=shopathome-github-jopapa)
60 | - [Add an API to Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/add-api?wt.mc_id=shopathome-github-jopapa)
61 | - [Authentication and authorization](https://docs.microsoft.com/azure/static-web-apps/authentication-authorization?wt.mc_id=shopathome-github-jopapa)
62 | - [Routes](https://docs.microsoft.com/azure/static-web-apps/routes?wt.mc_id=shopathome-github-jopapa)
63 | - [Review pre-production environments](https://docs.microsoft.com/azure/static-web-apps/review-publish-pull-requests?wt.mc_id=shopathome-github-jopapa)
64 | - [Azure Free Trial](https://azure.microsoft.com/free/?wt.mc_id=shopathome-github-jopapa)
65 |
--------------------------------------------------------------------------------
/svelte-app/README.md:
--------------------------------------------------------------------------------
1 | # Static Web App with Svelte
2 |
3 | This project was created to help represent a fundamental app written with Svelte. The Shop at Home theme is used throughout the app. View it live at .
4 |
5 | ## Learn how
6 |
7 | Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/en-us/learn/modules/publish-app-service-static-web-app-api/?WT.mc_id=shopathome-github-jopapa)
8 |
9 | ## Install and Setup
10 |
11 | 1. Create a repository from this template repository
12 |
13 | 1. Enter the name of your new repository as _my-static-web-app_
14 |
15 | 1. Clone your new repository
16 |
17 | ```bash
18 | git clone https://github.com/your-github-organization/my-static-web-app
19 | cd my-static-web-app/svelte-app
20 | ```
21 |
22 | 1. Install the npm packages
23 |
24 | ```bash
25 | npm install
26 | ```
27 |
28 | ## Getting Started - Running with Static Web Apps and Serverless Functions API
29 |
30 | 1. Run the app
31 |
32 | ```bash
33 | npm run start-svelte-func-swa
34 | ```
35 |
36 | ## Getting Started - Running with Static Web Apps and Fastify API
37 |
38 | 1. Run the app
39 |
40 | ```bash
41 | npm run start-svelte-fastify-swa
42 | ```
43 |
44 | ## Authentication / Authorization
45 |
46 | The app does not require authentication to launch or see the default page. However to view the products or discounts, the user must be authenticated using one of the options. These options are defined in the `/public/staticwebapp.config.json` file.
47 |
48 | | Endpoint | Roles |
49 | | ----------------- | --------------------------------------------- |
50 | | /api/\* | no auth |
51 | | /api/products/\* | authenticated users |
52 | | /api/discounts/\* | authenticated users with the _preferred_ role |
53 |
54 | ## Resources
55 |
56 | ### Azure Static Web Apps
57 |
58 | - Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/learn/modules/publish-app-service-static-web-app-api?wt.mc_id=shopathome-github-jopapa)
59 | - [API support in Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/apis?wt.mc_id=shopathome-github-jopapa)
60 | - [Add an API to Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/add-api?wt.mc_id=shopathome-github-jopapa)
61 | - [Authentication and authorization](https://docs.microsoft.com/azure/static-web-apps/authentication-authorization?wt.mc_id=shopathome-github-jopapa)
62 | - [Routes](https://docs.microsoft.com/azure/static-web-apps/routes?wt.mc_id=shopathome-github-jopapa)
63 | - [Review pre-production environments](https://docs.microsoft.com/azure/static-web-apps/review-publish-pull-requests?wt.mc_id=shopathome-github-jopapa)
64 | - [Azure Free Trial](https://azure.microsoft.com/free/?wt.mc_id=shopathome-github-jopapa)
65 |
--------------------------------------------------------------------------------
/svelte-app/src/products/ProductDetail.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 |
100 |
--------------------------------------------------------------------------------
/angular-app/README.md:
--------------------------------------------------------------------------------
1 | # Static Web App
2 |
3 | This project was created to help represent a fundamental app written with Angular. The Shop at Home theme is used throughout the app. View it live at .
4 |
5 | ## Learn how
6 |
7 | Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/en-us/learn/modules/publish-app-service-static-web-app-api/?WT.mc_id=shopathome-github-jopapa)
8 |
9 | ## Install and Setup
10 |
11 | 1. Create a repository from this template repository
12 |
13 | 1. Enter the name of your new repository as _my-static-web-app_
14 |
15 | 1. Clone your new repository
16 |
17 | ```bash
18 | git clone https://github.com/your-github-organization/my-static-web-app
19 | cd my-static-web-app/angular-app
20 | ```
21 |
22 | 1. Install the npm packages
23 |
24 | ```bash
25 | npm install
26 | ```
27 |
28 | ## Getting Started - Running with Static Web Apps and Serverless Functions API
29 |
30 | 1. Run the app
31 |
32 | ```bash
33 | npm run start-angular-func-swa
34 | ```
35 |
36 | ## Getting Started - Running with Static Web Apps and Fastify API
37 |
38 | 1. Run the app
39 |
40 | ```bash
41 | npm run start-angular-fastify-swa
42 | ```
43 |
44 | 1. Browse to your app at
45 |
46 | ## Authentication / Authorization
47 |
48 | The app does not require authentication to launch or see the default page. However to view the products or discounts, the user must be authenticated using one of the options. These options are defined in the `/public/staticwebapp.config.json` file.
49 |
50 | | Endpoint | Roles |
51 | | ----------------- | --------------------------------------------- |
52 | | /api/\* | no auth |
53 | | /api/products/\* | authenticated users |
54 | | /api/discounts/\* | authenticated users with the _preferred_ role |
55 |
56 | ## Resources
57 |
58 | ### Azure Static Web Apps
59 |
60 | - Learn how to [Publish an Angular, React, Svelte, or Vue JavaScript app and API with Azure Static Web Apps](https://docs.microsoft.com/learn/modules/publish-app-service-static-web-app-api?wt.mc_id=shopathome-github-jopapa)
61 | - [API support in Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/apis?wt.mc_id=shopathome-github-jopapa)
62 | - [Add an API to Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/add-api?wt.mc_id=shopathome-github-jopapa)
63 | - [Authentication and authorization](https://docs.microsoft.com/azure/static-web-apps/authentication-authorization?wt.mc_id=shopathome-github-jopapa)
64 | - [Routes](https://docs.microsoft.com/azure/static-web-apps/routes?wt.mc_id=shopathome-github-jopapa)
65 | - [Review pre-production environments](https://docs.microsoft.com/azure/static-web-apps/review-publish-pull-requests?wt.mc_id=shopathome-github-jopapa)
66 | - [Azure Free Trial](https://azure.microsoft.com/free/?wt.mc_id=shopathome-github-jopapa)
67 |
--------------------------------------------------------------------------------
/react-app/src/store/product.reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | SELECT_PRODUCT,
3 | LOAD_PRODUCT_SUCCESS,
4 | LOAD_PRODUCT,
5 | LOAD_PRODUCT_ERROR,
6 | UPDATE_PRODUCT,
7 | UPDATE_PRODUCT_SUCCESS,
8 | UPDATE_PRODUCT_ERROR,
9 | DELETE_PRODUCT,
10 | DELETE_PRODUCT_SUCCESS,
11 | DELETE_PRODUCT_ERROR,
12 | ADD_PRODUCT,
13 | ADD_PRODUCT_SUCCESS,
14 | ADD_PRODUCT_ERROR,
15 | } from './product.actions';
16 |
17 | let initState = {
18 | loading: false,
19 | data: [],
20 | error: void 0,
21 | };
22 |
23 | export const productsReducer = (state = initState, action) => {
24 | switch (action.type) {
25 | case LOAD_PRODUCT:
26 | return { ...state, loading: true, error: '' };
27 | case LOAD_PRODUCT_SUCCESS:
28 | return { ...state, loading: false, data: [...action.payload] };
29 | case LOAD_PRODUCT_ERROR:
30 | return { ...state, loading: false, error: action.payload };
31 |
32 | case UPDATE_PRODUCT:
33 | return {
34 | ...state,
35 | data: state.data.map((h) => {
36 | if (h.id === action.payload.id) {
37 | state.loading = true;
38 | }
39 | return h;
40 | }),
41 | };
42 | case UPDATE_PRODUCT_SUCCESS:
43 | return modifyProductState(state, action.payload);
44 | case UPDATE_PRODUCT_ERROR:
45 | return { ...state, loading: false, error: action.payload };
46 |
47 | case DELETE_PRODUCT: {
48 | return {
49 | ...state,
50 | loading: true,
51 | data: state.data.filter((h) => h !== action.payload),
52 | };
53 | }
54 |
55 | case DELETE_PRODUCT_SUCCESS: {
56 | const result = { ...state, loading: false };
57 | return result;
58 | }
59 |
60 | case DELETE_PRODUCT_ERROR: {
61 | return {
62 | ...state,
63 | data: [...state.data, action.payload.requestData],
64 | loading: false,
65 | };
66 | }
67 |
68 | case ADD_PRODUCT: {
69 | return { ...state, loading: true };
70 | }
71 |
72 | case ADD_PRODUCT_SUCCESS: {
73 | return {
74 | ...state,
75 | loading: false,
76 | data: [...state.data, { ...action.payload }],
77 | };
78 | }
79 |
80 | case ADD_PRODUCT_ERROR: {
81 | return { ...state, loading: false };
82 | }
83 |
84 | default:
85 | return state;
86 | }
87 | };
88 |
89 | const modifyProductState = (productState, productChanges) => {
90 | return {
91 | ...productState,
92 | loading: false,
93 | data: productState.data.map((h) => {
94 | if (h.id === productChanges.id) {
95 | return { ...h, ...productChanges };
96 | } else {
97 | return h;
98 | }
99 | }),
100 | };
101 | };
102 |
103 | let initialSelectedProduct = null;
104 |
105 | export const selectedProductReducer = (
106 | state = initialSelectedProduct,
107 | action
108 | ) => {
109 | switch (action.type) {
110 | case SELECT_PRODUCT:
111 | return action.payload ? { ...action.payload } : null;
112 | default:
113 | return state;
114 | }
115 | };
116 |
--------------------------------------------------------------------------------