├── .github └── workflows │ └── azure-pages-gentle-smoke-0a1ecaa04.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── api ├── .funcignore ├── .gitignore ├── README.md ├── heroes-delete │ ├── function.json │ ├── heroes-get │ │ ├── function.json │ │ └── index.js │ ├── heroes-post │ │ ├── function.json │ │ └── index.js │ ├── heroes-put │ │ ├── function.json │ │ └── index.js │ └── index.js ├── heroes-get │ ├── function.json │ └── index.js ├── heroes-post │ ├── function.json │ └── index.js ├── heroes-put │ ├── function.json │ └── index.js ├── host.json ├── package-lock.json ├── package.json ├── proxies.json ├── shared │ └── data.js ├── villains-delete │ ├── function.json │ └── index.js ├── villains-get │ ├── function.json │ └── index.js ├── villains-post │ ├── function.json │ └── index.js └── villains-put │ ├── function.json │ └── index.js └── vue-app ├── .browserslistrc ├── .dockerignore ├── .env ├── .env.development ├── .eslintrc.js ├── .prettierrc.js ├── Dockerfile ├── README.md ├── babel.config.js ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── heroes.spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── db.js ├── db.json ├── docker-compose.debug.yml ├── docker-compose.yml ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html └── routes.json ├── routes.json ├── server.js ├── src ├── app.vue ├── assets │ └── logo.png ├── components │ ├── button-footer.vue │ ├── card-content.vue │ ├── header-bar-brand.vue │ ├── header-bar-links.vue │ ├── header-bar.vue │ ├── list-header.vue │ ├── modal.vue │ ├── nav-bar.vue │ └── page-not-found.vue ├── main.ts ├── router.ts ├── shims-vue.d.ts ├── store │ ├── config.ts │ ├── index.ts │ └── modules │ │ ├── action-utils.ts │ │ ├── heroes.ts │ │ ├── models.ts │ │ ├── mutation-types.ts │ │ ├── types.ts │ │ └── villains.ts ├── styles.scss └── views │ ├── about.vue │ ├── heroes │ ├── hero-detail.vue │ ├── hero-list.vue │ ├── heroes.vue │ └── use-heroes.ts │ └── villains │ ├── use-villains.ts │ ├── villain-detail.vue │ ├── villain-list.vue │ └── villains.vue ├── tsconfig.json └── vue.config.js /.github/workflows/azure-pages-gentle-smoke-0a1ecaa04.yml: -------------------------------------------------------------------------------- 1 | name: Azure Pages CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - master 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@v1 19 | - name: Build And Deploy 20 | id: builddeploy 21 | uses: joslinmicrosoft/staticsitesactionoryx@canary3 22 | with: 23 | azure_pages_api_token: ${{ secrets.AZURE_PAGES_API_TOKEN_GENTLE_SMOKE_0A1ECAA04 }} 24 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 25 | action: 'upload' 26 | ###### Repository/Build Configurations - These values can be configured to match you app requirements. ###### 27 | app_location: '/' # App source code path 28 | api_location: 'api' # Api source code path - optional 29 | app_artifact_location: 'dist' # Built app content directory - optional 30 | ###### End of Repository/Build Configurations ###### 31 | 32 | close_pull_request_job: 33 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 34 | runs-on: ubuntu-latest 35 | name: Close Pull Request Job 36 | steps: 37 | - name: Close Pull Request 38 | id: closepullrequest 39 | uses: joslinmicrosoft/staticsitesaction@canary2 40 | with: 41 | azure_pages_api_token: ${{ secrets.AZURE_PAGES_API_TOKEN_GENTLE_SMOKE_0A1ECAA04 }} 42 | action: 'close' 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | **/dist 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | # .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "bracketSpacing": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode.azure-account", 4 | "ms-azuretools.vscode-azurestaticwebapps", 5 | "ms-azuretools.vscode-azurefunctions", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Vue: Chrome", 11 | "preLaunchTask": "npm: quick", 12 | "url": "http://localhost:9626", 13 | "webRoot": "${workspaceFolder}/src", 14 | "breakOnLoad": true, 15 | "sourceMapPathOverrides": { 16 | "webpack:///src/*": "${webRoot}/*", 17 | "webpack:///./src/*": "${webRoot}/*", 18 | "webpack:///./*": "${webRoot}/*", 19 | "webpack:///*": "*", 20 | "webpack:///./~/*": "${webRoot}/node_modules/*", 21 | "meteor://💻app/*": "${webRoot}/*" 22 | } 23 | }, 24 | { 25 | "name": "Attach to Node Functions", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 9229, 29 | "preLaunchTask": "func: host start" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "peacock.color": "#42b883", 4 | "azureFunctions.deploySubpath": "api", 5 | "azureFunctions.postDeployTask": "npm install", 6 | "azureFunctions.projectLanguage": "JavaScript", 7 | "azureFunctions.projectRuntime": "~3", 8 | "debug.internalConsoleOptions": "neverOpen", 9 | "azureFunctions.preDeployTask": "npm prune", 10 | "workbench.colorCustomizations": { 11 | "activityBar.activeBackground": "#42b883", 12 | "activityBar.activeBorder": "#f0e8f7", 13 | "activityBar.background": "#42b883", 14 | "activityBar.foreground": "#15202b", 15 | "activityBar.inactiveForeground": "#15202b99", 16 | "activityBarBadge.background": "#f0e8f7", 17 | "activityBarBadge.foreground": "#15202b", 18 | "statusBar.background": "#42b883", 19 | "statusBar.foreground": "#15202b", 20 | "statusBarItem.hoverBackground": "#359268", 21 | "titleBar.activeBackground": "#42b883", 22 | "titleBar.activeForeground": "#15202b", 23 | "titleBar.inactiveBackground": "#42b88399", 24 | "titleBar.inactiveForeground": "#15202b99", 25 | "statusBar.debuggingBackground": "#b84277", 26 | "statusBar.debuggingForeground": "#e7e7e7" 27 | }, 28 | "peacock.remoteColor": "42b883" 29 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "quick", 9 | "isBackground": true, 10 | "presentation": { 11 | "focus": true, 12 | "panel": "dedicated" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "problemMatcher": { 19 | "owner": "typescript", 20 | "source": "ts", 21 | "applyTo": "closedDocuments", 22 | "fileLocation": [ 23 | "relative", 24 | "${cwd}" 25 | ], 26 | "pattern": "$tsc", 27 | "background": { 28 | "activeOnStart": true, 29 | "beginsPattern": { 30 | "regexp": "(.*?)" 31 | }, 32 | "endsPattern": { 33 | "regexp": "Compiled |Failed to compile." 34 | } 35 | } 36 | } 37 | }, 38 | { 39 | "type": "func", 40 | "command": "host start", 41 | "problemMatcher": "$func-node-watch", 42 | "isBackground": true, 43 | "dependsOn": "npm install", 44 | "options": { 45 | "cwd": "${workspaceFolder}/api" 46 | } 47 | }, 48 | { 49 | "type": "shell", 50 | "label": "npm build", 51 | "command": "npm run build", 52 | "dependsOn": "npm install", 53 | "problemMatcher": "$tsc", 54 | "options": { 55 | "cwd": "${workspaceFolder}/api" 56 | } 57 | }, 58 | { 59 | "type": "shell", 60 | "label": "npm install", 61 | "command": "npm install", 62 | "options": { 63 | "cwd": "${workspaceFolder}/api" 64 | } 65 | }, 66 | { 67 | "type": "shell", 68 | "label": "npm prune", 69 | "command": "npm prune --production", 70 | "dependsOn": "npm build", 71 | "problemMatcher": [], 72 | "options": { 73 | "cwd": "${workspaceFolder}/api" 74 | } 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tour of Heroes 2 | 3 | This project was created to help represent 4 fundamental apps, each written with Angular, React, Svelte, and Vue (respectively). The heroes and villains theme is used throughout the app. 4 | 5 | by [John Papa](http://twitter.com/john_papa) 6 | 7 | ## Why 8 | 9 | I love JavaScript and the Web! One of the most common questions I hear is "which framework is best?". I like to flip this around and ask you "which is best for you?". The best way to know this is to try it for yourself. I'll follow up with some articles on my experiences with these frameworks but in the meantime, please try it for yourself to gain your own experience with each. 10 | 11 | ## Live Demos 12 | 13 | Hosted in [Azure](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa) 14 | 15 | - [Tour of Heroes with Angular](https://papa-heroes-angular.azurewebsites.net) 16 | - [Tour of Heroes with React](https://papa-heroes-react.azurewebsites.net) 17 | - Tour of Heroes with Svelte - coming soon! 18 | - [Tour of Heroes with Vue](https://papa-heroes-vue.azurewebsites.net) 19 | 20 | ## Getting Started 21 | 22 | 1. Go to the folder where your preferred web framework is located, and open it's readme 23 | 24 | | Web Framework | Folder | 25 | | ------------- | ---------------------------------------------------------------------------------- | 26 | | Vue | [vue-app](https://github.com/johnpapa/heroes-vue/tree/main/vue-app#tour-of-heroes) | 27 | 28 | ## What's in the App 29 | 30 | Each of these apps contain: 31 | 32 | Each of the apps written in the various frameworks/libraries has been designed to have similar features. While consistency is key, I want these apps to be comparable, yet done in an way authentic to each respective framework. 33 | 34 | Each project represents heroes and villains. The user can list them and edit them. 35 | 36 | Here is a list of those features: 37 | 38 | - [x] Start from the official quick-start and CLI 39 | - [x] Client side routing 40 | - [x] Three main routes Heroes, Villains, About 41 | - [x] Handles an erroneous route, leading to a PageNotFound component 42 | - [x] Active route is highlighted in the nav menu 43 | - [x] Routing should use html5 mode, not hash routes 44 | - [x] API 45 | - [x] JSON server as a backend 46 | - [x] App served on one port which can access API on another port proxy or CORS) 47 | - [x] HTTP - Uses most common client http libraries for each framework 48 | - [x] Styling 49 | - [x] Bulma 50 | - [x] SASS 51 | - [x] Font Awesome 52 | - [x] Same exact css in every app 53 | - [x] Editing - Heroes and Villains will be editable (add, update, delete) 54 | - [x] State/Store - Uses a store for state management 55 | - [x] Web development server handles fallback routing 56 | - [x] Generic components 57 | - [x] Modal 58 | - [x] Button Tool 59 | - [x] Card 60 | - [x] Header bar 61 | - [x] List header 62 | - [x] Nav bar 63 | - [x] Props in and emit events out 64 | - [x] Environment variable for the API location 65 | 66 | ### Why Cypress? 67 | 68 | Cypress.io makes it easy to run all three apps simultaneously in end to end tests, so you can watch the results while developing. 69 | 70 | ### Why abstracted CSS? 71 | 72 | The goal of the project was to show how each framework can be designed to create the same app. Each uses their own specific techniques in a way that is tuned to each framework. However the one caveat I wanted to achieve was to make sure all of them look the same. While I could have used specific styling for each with scoped and styled components, I chose to create a single global styles file that they all share. This allowed me to provide the same look and feel, run the same cypress tests, and focus more on the HTML and JavaScript/TypeScript. 73 | 74 | ### Optional JSON Server 75 | 76 | The app uses an API with Azure Functions by default. But you can setup a JSON server for a backend. This allows you to run the code without needing any database engines or cloud accounts. Enjoy! 77 | 78 | ## Problems or Suggestions 79 | 80 | [Open an issue here](/issues) 81 | 82 | ## Thank You 83 | 84 | Thank you to [Sarah Drasner](https://twitter.com/), [Brian Holt](https://twitter.com/), [Chris Noring](https://twitter.com/), [Craig Shoemaker](https://twitter.com/), and [Ward Bell](https://twitter.com/wardbell) for providing input and reviewing the code in some of the repos for the Angular, React, Svelte, and Vue apps: 85 | 86 | - [heroes-angular](https://github.com/johnpapa/heroes-angular) 87 | - [heroes-react](https://github.com/johnpapa/heroes-react) 88 | - [heroes-svelte](https://github.com/johnpapa/heroes-svelte) 89 | - [heroes-vue](https://github.com/johnpapa/heroes-vue) 90 | 91 | ## Resources 92 | 93 | - [VS Code](https://code.visualstudio.com/?WT.mc_id=javascript-0000-jopapa) 94 | - [Azure Free Trial](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa) 95 | - [VS Code Extension for Node on Azure](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack&WT.mc_id=javascript-0000-jopapa) 96 | - [VS Code Extension Marketplace](https://marketplace.visualstudio.com/vscode?WT.mc_id=javascript-0000-jopapa) 97 | - [VS Code - macOS keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf?WT.mc_id=javascript-0000-jopapa) 98 | - [VS Code - Windows keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf?WT.mc_id=javascript-0000-jopapa) 99 | 100 | ### Debugging Resources 101 | 102 | - [Debugging Angular in VS Code](https://code.visualstudio.com/docs/nodejs/angular-tutorial?WT.mc_id=javascript-0000-jopapa) 103 | - [Debugging React in VS Code](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial?WT.mc_id=javascript-0000-jopapa) 104 | - [Debugging Vue in VS Code](https://code.visualstudio.com/docs/nodejs/vuejs-tutorial?WT.mc_id=javascript-0000-jopapa) 105 | -------------------------------------------------------------------------------- /api/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 heroes. 4 | 5 | ## Getting Started 6 | 7 | 1. Create a repository from this template repository 8 | 9 | 1. Enter the name of your new repository as _vue-heroes_ 10 | 11 | 1. Clone your new repository 12 | 13 | ```bash 14 | git clone https://github.com/your-github-organization/vue-heroes 15 | cd vue-heroes/api 16 | ``` 17 | 18 | 1. Create the file `api/local.setting.json` and modify its contents as follows: 19 | 20 | ```json 21 | { 22 | "IsEncrypted": false, 23 | "Values": { 24 | "AzureWebJobsStorage": "", 25 | "FUNCTIONS_WORKER_RUNTIME": "node" 26 | }, 27 | "Host": { 28 | "CORS": "http://localhost:8080" 29 | } 30 | } 31 | ``` 32 | 33 | 1. Run the app 34 | 35 | ```bash 36 | npm start 37 | ``` 38 | 39 | ## Resources 40 | 41 | - [Azure Free Trial](https://azure.microsoft.com/free/?wt.mc_id=javascript-0000-jopapa) 42 | - [VS Code](https://code.visualstudio.com?wt.mc_id=javascript-0000-jopapa) 43 | - [VS Code Extension for Node on Azure](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack&WT.mc_id=javascript-0000-jopapa) 44 | - Azure Functions [local.settings.json](https://docs.microsoft.com/azure/azure-functions/functions-run-local#local-settings-file?WT.mc_id=javascript-0000-jopapa) file 45 | 46 | ### Debugging Resources 47 | 48 | - [Debugging Angular in VS Code](https://code.visualstudio.com/docs/nodejs/angular-tutorial?wt.mc_id=javascript-0000-jopapa) 49 | - [Debugging React in VS Code](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial?wt.mc_id=javascript-0000-jopapa) 50 | - [Debugging Vue in VS Code](https://code.visualstudio.com/docs/nodejs/vuejs-tutorial?wt.mc_id=javascript-0000-jopapa) 51 | -------------------------------------------------------------------------------- /api/heroes-delete/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["delete"], 9 | "route": "x/heroes/{id}" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/heroes-delete/heroes-get/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["get"], 9 | "route": "heroes" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/heroes-delete/heroes-get/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | try { 5 | const heroes = data.getHeroes(); 6 | context.res.status(200).json(heroes); 7 | } catch (error) { 8 | context.res.status(500).send(error); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /api/heroes-delete/heroes-post/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["post"], 9 | "route": "x/heroes" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/heroes-delete/heroes-post/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const hero = { 5 | id: undefined, 6 | name: req.body.name, 7 | description: req.body.description, 8 | }; 9 | 10 | try { 11 | const newHero = data.addHero(hero); 12 | context.res.status(201).json(newHero); 13 | } catch (error) { 14 | context.res.status(500).send(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /api/heroes-delete/heroes-put/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["put"], 9 | "route": "x/heroes/{id}" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/heroes-delete/heroes-put/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const hero = { 5 | id: parseInt(req.params.id, 10), 6 | name: req.body.name, 7 | description: req.body.description, 8 | }; 9 | 10 | try { 11 | const updatedHero = data.updateHero(hero); 12 | context.res.status(200).json(updatedHero); 13 | } catch (error) { 14 | context.res.status(500).send(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /api/heroes-delete/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const id = parseInt(req.params.id, 10); 5 | 6 | try { 7 | data.deleteHero(id); 8 | context.res.status(200).json({}); 9 | } catch (error) { 10 | context.res.status(500).send(error); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /api/heroes-get/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["get"], 9 | "route": "heroes" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/heroes-get/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | try { 5 | const heroes = data.getHeroes(); 6 | context.res.status(200).json(heroes); 7 | } catch (error) { 8 | context.res.status(500).send(error); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /api/heroes-post/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["post"], 9 | "route": "heroes" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/heroes-post/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const hero = { 5 | id: undefined, 6 | name: req.body.name, 7 | description: req.body.description, 8 | }; 9 | 10 | try { 11 | const newHero = data.addHero(hero); 12 | context.res.status(201).json(newHero); 13 | } catch (error) { 14 | context.res.status(500).send(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /api/heroes-put/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["put"], 9 | "route": "heroes/{id}" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/heroes-put/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const hero = { 5 | id: parseInt(req.params.id, 10), 6 | name: req.body.name, 7 | description: req.body.description, 8 | }; 9 | 10 | try { 11 | const updatedHero = data.updateHero(hero); 12 | context.res.status(200).json(updatedHero); 13 | } catch (error) { 14 | context.res.status(500).send(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[1.*, 2.0.0)" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /api/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@azure/functions": { 8 | "version": "1.2.2", 9 | "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-1.2.2.tgz", 10 | "integrity": "sha512-p/dDHq1sG/iAib+eDY4NxskWHoHW1WFzD85s0SfWxc2wVjJbxB0xz/zBF4s7ymjVgTu+0ceipeBk+tmpnt98oA==", 11 | "dev": true 12 | }, 13 | "typescript": { 14 | "version": "3.9.7", 15 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", 16 | "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", 17 | "dev": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | "@azure/functions": "^1.0.2-beta2", 12 | "typescript": "^3.3.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /api/shared/data.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | heroes: [ 3 | { 4 | id: 10, 5 | name: 'Aslaug', 6 | description: 'warrior queen', 7 | }, 8 | { 9 | id: 11, 10 | name: 'Bjorn Ironside', 11 | description: 'king of 9th century Sweden', 12 | }, 13 | { 14 | id: 12, 15 | name: 'Ivar the Boneless', 16 | description: 'commander of the Great Heathen Army', 17 | }, 18 | { 19 | id: 13, 20 | name: 'Lagertha the Shieldmaiden', 21 | description: 'aka Hlaðgerðr', 22 | }, 23 | { 24 | id: 14, 25 | name: 'Ragnar Lothbrok', 26 | description: 'aka Ragnar Sigurdsson', 27 | }, 28 | { 29 | id: 15, 30 | name: 'Thora Town-hart', 31 | description: 'daughter of Earl Herrauðr of Götaland', 32 | }, 33 | ], 34 | villains: [ 35 | { 36 | id: 40, 37 | name: 'Madelyn', 38 | description: 'the cat whisperer', 39 | }, 40 | { 41 | id: 41, 42 | name: 'Haley', 43 | description: 'pen wielder', 44 | }, 45 | { 46 | id: 42, 47 | name: 'Ella', 48 | description: 'fashionista', 49 | }, 50 | { 51 | id: 43, 52 | name: 'Landon', 53 | description: 'Mandalorian mauler', 54 | }, 55 | ], 56 | }; 57 | 58 | const getRandomInt = () => { 59 | const max = 1000; 60 | const min = 100; 61 | return Math.floor(Math.random() * Math.floor(max) + min); 62 | }; 63 | 64 | const addHero = (hero) => { 65 | hero.id = getRandomInt(); 66 | data.heroes.push(hero); 67 | return hero; 68 | }; 69 | 70 | const updateHero = (hero) => { 71 | const index = data.heroes.findIndex((v) => v.id === hero.id); 72 | console.log(hero); 73 | data.heroes.splice(index, 1, hero); 74 | return hero; 75 | }; 76 | 77 | const deleteHero = (id) => { 78 | const value = parseInt(id, 10); 79 | data.heroes = data.heroes.filter((v) => v.id !== value); 80 | return true; 81 | }; 82 | 83 | const getHeroes = () => { 84 | return data.heroes; 85 | }; 86 | 87 | const addVillain = (villain) => { 88 | villain.id = getRandomInt(); 89 | data.villains.push(villain); 90 | return villain; 91 | }; 92 | 93 | const updateVillain = (villain) => { 94 | const index = data.villains.findIndex((v) => v.id === villain.id); 95 | console.log(villain); 96 | data.villains.splice(index, 1, villain); 97 | return villain; 98 | }; 99 | 100 | const deleteVillain = (id) => { 101 | const value = parseInt(id, 10); 102 | data.villains = data.villains.filter((v) => v.id !== value); 103 | return true; 104 | }; 105 | 106 | const getVillains = () => { 107 | return data.villains; 108 | }; 109 | 110 | module.exports = { 111 | addVillain, 112 | updateVillain, 113 | deleteVillain, 114 | getVillains, 115 | addHero, 116 | updateHero, 117 | deleteHero, 118 | getHeroes, 119 | }; 120 | -------------------------------------------------------------------------------- /api/villains-delete/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["delete"], 9 | "route": "villains/{id}" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/villains-delete/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const id = parseInt(req.params.id, 10); 5 | 6 | try { 7 | data.deleteVillain(id); 8 | context.res.status(200).json({}); 9 | } catch (error) { 10 | context.res.status(500).send(error); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /api/villains-get/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["get"], 9 | "route": "villains" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/villains-get/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | try { 5 | const villains = data.getVillains(); 6 | context.res.status(200).json(villains); 7 | } catch (error) { 8 | context.res.status(500).send(error); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /api/villains-post/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["post"], 9 | "route": "villains" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/villains-post/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const villain = { 5 | id: undefined, 6 | name: req.body.name, 7 | description: req.body.description, 8 | }; 9 | 10 | try { 11 | const newVillain = data.addVillain(villain); 12 | context.res.status(201).json(newVillain); 13 | } catch (error) { 14 | context.res.status(500).send(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /api/villains-put/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["put"], 9 | "route": "villains/{id}" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /api/villains-put/index.js: -------------------------------------------------------------------------------- 1 | const data = require('../shared/data'); 2 | 3 | module.exports = async function (context, req) { 4 | const hero = { 5 | id: parseInt(req.params.id, 10), 6 | name: req.body.name, 7 | description: req.body.description, 8 | }; 9 | 10 | try { 11 | const updatedVillain = data.updateHero(villain); 12 | context.res.status(200).json(updatedVillain); 13 | } catch (error) { 14 | context.res.status(500).send(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /vue-app/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /vue-app/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | .env 9 | */bin 10 | */obj 11 | README.md 12 | LICENSE 13 | .vscode -------------------------------------------------------------------------------- /vue-app/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | WWW=./dist 3 | PORT=9626 4 | VUE_APP_API=api 5 | -------------------------------------------------------------------------------- /vue-app/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | WWW=./dist 3 | PORT=9626 4 | VUE_APP_API=api 5 | -------------------------------------------------------------------------------- /vue-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | node: true, 6 | }, 7 | 8 | plugins: ['prettier'], 9 | 10 | // watch this for explaining why some of this is here 11 | // https://www.youtube.com/watch?time_continue=239&v=YIvjKId9m2c 12 | rules: { 13 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'consistent-return': 0, 16 | quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], 17 | 'max-classes-per-file': 'off', 18 | 'no-useless-constructor': 'off', 19 | 'no-empty-function': 'off', 20 | 'import/prefer-default-export': 'off', 21 | 'no-use-before-define': 'off', 22 | '@typescript-eslint/no-useless-constructor': 'error', 23 | '@typescript-eslint/no-unused-vars': ['error'], 24 | 'prettier/prettier': [ 25 | 'error', 26 | { 27 | trailingComma: 'es5', 28 | singleQuote: true, 29 | printWidth: 80, 30 | }, 31 | ], 32 | 'vue/no-unused-components': [ 33 | 'error', 34 | { 35 | ignoreWhenBindingPresent: true, 36 | }, 37 | ], 38 | }, 39 | 40 | parserOptions: { 41 | parser: '@typescript-eslint/parser', 42 | }, 43 | 44 | extends: [ 45 | '@vue/airbnb', 46 | 'plugin:vue/vue3-essential', 47 | '@vue/prettier', 48 | '@vue/typescript', 49 | ], 50 | }; 51 | -------------------------------------------------------------------------------- /vue-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | bracketSpacing: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /vue-app/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base Stage/Layer 2 | FROM node:10.16-alpine as node-layer 3 | WORKDIR /usr/src/app 4 | 5 | # Client App 6 | FROM node-layer as client-app 7 | LABEL authors="John Papa" 8 | COPY ["package.json", "npm-shrinkwrap.json*", "./"] 9 | # COPY package*.json ./ 10 | RUN npm install --silent 11 | COPY . . 12 | ARG VUE_APP_API 13 | ENV VUE_APP_API $VUE_APP_API 14 | RUN npm run build 15 | 16 | # Node server 17 | FROM node-layer as node-server 18 | COPY ["package.json", "npm-shrinkwrap.json*", "./"] 19 | # COPY package*.json ./ 20 | RUN npm install --production --silent && mv node_modules ../ 21 | COPY server.js . 22 | 23 | # Final image 24 | FROM node-layer 25 | WORKDIR /usr/src/app 26 | # get the node_modules 27 | COPY --from=node-server /usr/src /usr/src 28 | # get the client app 29 | COPY --from=client-app /usr/src/app/dist ./public 30 | EXPOSE 9626 31 | CMD ["node", "server.js"] 32 | -------------------------------------------------------------------------------- /vue-app/README.md: -------------------------------------------------------------------------------- 1 | # Tour of Heroes 2 | 3 | This project was created to help represent a fundamental app written with Vue. The heroes and villains theme is used throughout the app. 4 | 5 | by [John Papa](http://twitter.com/john_papa) 6 | 7 | Comparative apps can be found here with [Angular](https://github.com/johnpapa/heroes-angular), [React](https://github.com/johnpapa/heroes-react), and [Svelte](https://github.com/johnpapa/heroes-svelte) 8 | 9 | ## Why 10 | 11 | I love JavaScript and the Web! One of the most common questions I hear is "which framework is best?". I like to flip this around and ask you "which is best for you?". The best way to know this is to try it for yourself. I'll follow up with some articles on my experiences with these frameworks but in the meantime, please try it for yourself to gain your own experience with each. 12 | 13 | ## Live Demos 14 | 15 | Hosted in [Azure](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa) 16 | 17 | - [Tour of Heroes with Angular](https://papa-heroes-angular.azurewebsites.net) 18 | - [Tour of Heroes with React](https://papa-heroes-react.azurewebsites.net) 19 | - Tour of Heroes with Svelte - coming soon! 20 | - [Tour of Heroes with Vue](https://papa-heroes-vue.azurewebsites.net) 21 | 22 | ## Getting Started 23 | 24 | 1. Clone this repository 25 | 26 | ```bash 27 | git clone https://github.com/johnpapa/heroes-vue.git 28 | cd heroes-vue/vue-app 29 | ``` 30 | 31 | 1. Install the npm packages 32 | 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | 1. Run the app 38 | 39 | ```bash 40 | npm run serve 41 | ``` 42 | 43 | 1. Open a second terminal and run the API 44 | 45 | ```bash 46 | cd .. 47 | cd api 48 | func start 49 | ``` 50 | 51 | ## Cypress Tests 52 | 53 | 1. You can execute all of the UI tests by running the following steps 54 | 55 | ```bash 56 | npm run cypress 57 | ``` 58 | 59 | ## What's in the App 60 | 61 | Each of these apps contain: 62 | 63 | Each of the apps written in the various frameworks/libraries has been designed to have similar features. While consistency is key, I want these apps to be comparable, yet done in an way authentic to each respective framework. 64 | 65 | Each project represents heroes and villains. The user can list them and edit them. 66 | 67 | Here is a list of those features: 68 | 69 | - [x] Start from the official quick-start and CLI 70 | - [x] Client side routing 71 | - [x] Three main routes Heroes, Villains, About 72 | - [x] Handles an erroneous route, leading to a PageNotFound component 73 | - [x] Active route is highlighted in the nav menu 74 | - [x] Routing should use html5 mode, not hash routes 75 | - [x] API 76 | - [x] JSON server as a backend 77 | - [x] App served on one port which can access API on another port proxy or CORS) 78 | - [x] HTTP - Uses most common client http libraries for each framework 79 | - [x] Styling 80 | - [x] Bulma 81 | - [x] SASS 82 | - [x] Font Awesome 83 | - [x] Same exact css in every app 84 | - [x] Editing - Heroes and Villains will be editable (add, update, delete) 85 | - [x] State/Store - Uses a store for state management 86 | - [x] Web development server handles fallback routing 87 | - [x] Generic components 88 | - [x] Modal 89 | - [x] Button Tool 90 | - [x] Card 91 | - [x] Header bar 92 | - [x] List header 93 | - [x] Nav bar 94 | - [x] Props in and emit events out 95 | - [x] Environment variable for the API location 96 | 97 | ### Why Cypress? 98 | 99 | Cypress.io makes it easy to run all three apps simultaneously in end to end tests, so you can watch the results while developing. 100 | 101 | ### Why abstracted CSS? 102 | 103 | The goal of the project was to show how each framework can be designed to create the same app. Each uses their own specific techniques in a way that is tuned to each framework. However the one caveat I wanted to achieve was to make sure all of them look the same. While I could have used specific styling for each with scoped and styled components, I chose to create a single global styles file that they all share. This allowed me to provide the same look and feel, run the same cypress tests, and focus more on the HTML and JavaScript/TypeScript. 104 | 105 | ### Optional JSON Server 106 | 107 | The app uses an API with Azure Functions by default. But you can setup a JSON server for a backend. This allows you to run the code without needing any database engines or cloud accounts. Enjoy! 108 | 109 | ## Problems or Suggestions 110 | 111 | [Open an issue here](/issues) 112 | 113 | ## Thank You 114 | 115 | Thank you to [Sarah Drasner](https://twitter.com/), [Brian Holt](https://twitter.com/), [Chris Noring](https://twitter.com/), [Craig Shoemaker](https://twitter.com/), and [Ward Bell](https://twitter.com/wardbell) for providing input and reviewing the code in some of the repos for the Angular, React, Svelte, and Vue apps: 116 | 117 | - [heroes-angular](https://github.com/johnpapa/heroes-angular) 118 | - [heroes-react](https://github.com/johnpapa/heroes-react) 119 | - [heroes-svelte](https://github.com/johnpapa/heroes-svelte) 120 | - [heroes-vue](https://github.com/johnpapa/heroes-vue) 121 | 122 | ## Resources 123 | 124 | - [VS Code](https://code.visualstudio.com/?WT.mc_id=javascript-0000-jopapa) 125 | - [Azure Free Trial](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa) 126 | - [VS Code Extension for Node on Azure](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack&WT.mc_id=javascript-0000-jopapa) 127 | - [VS Code Extension Marketplace](https://marketplace.visualstudio.com/vscode?WT.mc_id=javascript-0000-jopapa) 128 | - [VS Code - macOS keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf?WT.mc_id=javascript-0000-jopapa) 129 | - [VS Code - Windows keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf?WT.mc_id=javascript-0000-jopapa) 130 | 131 | ### Debugging Resources 132 | 133 | - [Debugging Angular in VS Code](https://code.visualstudio.com/docs/nodejs/angular-tutorial?WT.mc_id=javascript-0000-jopapa) 134 | - [Debugging React in VS Code](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial?WT.mc_id=javascript-0000-jopapa) 135 | - [Debugging Vue in VS Code](https://code.visualstudio.com/docs/nodejs/vuejs-tutorial?WT.mc_id=javascript-0000-jopapa) 136 | -------------------------------------------------------------------------------- /vue-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /vue-app/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreTestFiles": "/**/examples/*", 3 | "env": { 4 | "port": "9626" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /vue-app/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /vue-app/cypress/integration/heroes.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-env mocha */ 3 | /* global cy expect Cypress */ 4 | 5 | import data from '../../db'; 6 | 7 | const hero = data.heroes[3]; 8 | const heroCount = 6; 9 | const heroToDelete = data.heroes[5]; 10 | const newHero = { 11 | id: 'heroMadelyn', 12 | name: 'Madelyn', 13 | description: 'the cat whisperer', 14 | }; 15 | 16 | const port = Cypress.env('port'); 17 | const url = `http://localhost:${port}`; 18 | 19 | const resetData = () => cy.request('POST', `${url}/api/reset`, data); 20 | 21 | const containsHeroes = count => 22 | cy.get('.list .name').should('have.length', count); 23 | 24 | const detailsAreVisible = visible => { 25 | const val = visible ? '' : 'not.'; 26 | // return cy.get('.edit-detail input[name=name]').should(`${val}be.visible`) 27 | return cy.get('.edit-detail input[name=name]').should(`${val}exist`); 28 | }; 29 | 30 | context('Heroes', () => { 31 | beforeEach(() => { 32 | resetData().then(() => { 33 | cy.visit(url); 34 | cy.get('nav ul.menu-list a') 35 | .contains('Heroes') 36 | .click(); 37 | }); 38 | }); 39 | 40 | after(() => resetData()); 41 | 42 | specify(`Contains ${hero.name}`, () => { 43 | cy.get('.list .name').contains(hero.name); 44 | }); 45 | 46 | specify('Contains 6 heroes', () => { 47 | containsHeroes(heroCount); 48 | }); 49 | 50 | specify(`Deletes ${heroToDelete.name}`, () => { 51 | cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).click(); 52 | cy.get(`#modal .modal-yes`).click(); 53 | 54 | containsHeroes(heroCount - 1); 55 | cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).should( 56 | 'not.exist' 57 | ); 58 | }); 59 | 60 | context(`${hero.name} has been Selected`, () => { 61 | beforeEach(() => { 62 | cy.get(`.list .edit-item[data-id=${hero.id}]`).click(); 63 | }); 64 | 65 | specify(`Shows Details for ${hero.name}`, () => { 66 | const match = new RegExp(hero.id); 67 | detailsAreVisible(true); 68 | cy.get('.edit-detail input[name=id]') 69 | .invoke('val') 70 | .should('match', match); 71 | }); 72 | 73 | specify(`Hero List is gone`, () => { 74 | containsHeroes(0); 75 | cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).should( 76 | 'not.exist' 77 | ); 78 | cy.get(`.list .delete-item[data-id=${hero.id}]`).should('not.exist'); 79 | }); 80 | 81 | specify(`Saves changes to ${hero.name}`, () => { 82 | const newDescription = 'slayer of javascript'; 83 | detailsAreVisible(true); 84 | cy.get('.edit-detail input[name=description]') 85 | .clear() 86 | .type(newDescription); 87 | cy.get('.edit-detail input[name=description]') 88 | .invoke('val') 89 | .should('not.match', new RegExp(hero.description)) 90 | .and('match', new RegExp(newDescription)); 91 | cy.get('.edit-detail .save-button').click(); 92 | detailsAreVisible(false); 93 | cy.get('.list .description').contains(newDescription); 94 | containsHeroes(heroCount); 95 | }); 96 | 97 | specify(`Cancels changes to ${hero.name}`, () => { 98 | const newDescription = 'slayer of javascript'; 99 | detailsAreVisible(true); 100 | cy.get('.edit-detail input[name=description]') 101 | .clear() 102 | .type(newDescription); 103 | cy.get('.edit-detail input[name=description]') 104 | .invoke('val') 105 | .should('not.match', new RegExp(hero.description)) 106 | .and('match', new RegExp(newDescription)); 107 | cy.get('.edit-detail .cancel-button').click(); 108 | detailsAreVisible(false); 109 | cy.get('.list .description').contains(hero.description); 110 | containsHeroes(heroCount); 111 | }); 112 | }); 113 | 114 | context(`Add New Hero`, () => { 115 | beforeEach(() => { 116 | cy.get('.content-container .add-button').click(); 117 | }); 118 | 119 | specify(`Saves changes to ${newHero.name}`, () => { 120 | detailsAreVisible(true); 121 | cy.get('.edit-detail input[name=name]') 122 | .clear() 123 | .type(newHero.name); 124 | cy.get('.edit-detail input[name=description]') 125 | .clear() 126 | .type(newHero.description); 127 | cy.get('.edit-detail .save-button').click(); 128 | detailsAreVisible(false); 129 | cy.get('.list .description').contains(newHero.description); 130 | containsHeroes(heroCount + 1); 131 | }); 132 | }); 133 | 134 | context(`Direct Routing`, () => { 135 | specify(`Routes to /heroes directly and see hero list`, () => { 136 | cy.visit(url); 137 | cy.wait(1000); 138 | cy.location().should(loc => { 139 | expect(loc.host).to.eq(`localhost:${port}`); 140 | expect(loc.hostname).to.eq('localhost'); 141 | expect(loc.href).to.eq(`${url}/heroes`); 142 | expect(loc.origin).to.eq(url); 143 | expect(loc.port).to.eq(port); 144 | expect(loc.protocol).to.eq('http:'); 145 | expect(loc.toString()).to.eq(`${url}/heroes`); 146 | }); 147 | detailsAreVisible(false); 148 | containsHeroes(heroCount); 149 | }); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /vue-app/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /vue-app/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /vue-app/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /vue-app/db.js: -------------------------------------------------------------------------------- 1 | const heroes = [ 2 | { 3 | id: 'HeroAslaug', 4 | name: 'Aslaug', 5 | description: 'warrior queen', 6 | }, 7 | { 8 | id: 'HeroBjorn', 9 | name: 'Bjorn Ironside', 10 | description: 'king of 9th century Sweden', 11 | }, 12 | { 13 | id: 'HeroIvar', 14 | name: 'Ivar the Boneless', 15 | description: 'commander of the Great Heathen Army', 16 | }, 17 | { 18 | id: 'HeroLagertha', 19 | name: 'Lagertha the Shieldmaiden', 20 | description: 'aka Hlaðgerðr', 21 | }, 22 | { 23 | id: 'HeroRagnar', 24 | name: 'Ragnar Lothbrok', 25 | description: 'aka Ragnar Sigurdsson', 26 | }, 27 | { 28 | id: 'HeroThora', 29 | name: 'Thora Town-hart', 30 | description: 'daughter of Earl Herrauðr of Götaland', 31 | }, 32 | ]; 33 | 34 | const villains = [ 35 | { 36 | id: 'VillainMadelyn', 37 | name: 'Madelyn', 38 | description: 'the cat whisperer', 39 | }, 40 | { 41 | id: 'VillainHaley', 42 | name: 'Haley', 43 | description: 'pen wielder', 44 | }, 45 | { 46 | id: 'VillainElla', 47 | name: 'Ella', 48 | description: 'fashionista', 49 | }, 50 | { 51 | id: 'VillainLandon', 52 | name: 'Landon', 53 | description: 'Mandalorian mauler', 54 | }, 55 | ]; 56 | 57 | const data = { heroes, villains }; 58 | 59 | module.exports = data; 60 | -------------------------------------------------------------------------------- /vue-app/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [], 3 | "heroes": [ 4 | { 5 | "id": "HeroAslaug", 6 | "name": "Aslaug", 7 | "description": "warrior queen" 8 | }, 9 | { 10 | "id": "HeroBjorn", 11 | "name": "Bjorn Ironside", 12 | "description": "king of 9th century Sweden" 13 | }, 14 | { 15 | "id": "HeroIvar", 16 | "name": "Ivar the Boneless", 17 | "description": "commander of the Great Heathen Army" 18 | }, 19 | { 20 | "id": "HeroLagertha", 21 | "name": "Lagertha the Shieldmaiden", 22 | "description": "aka Hlaðgerðr" 23 | }, 24 | { 25 | "id": "HeroRagnar", 26 | "name": "Ragnar Lothbrok", 27 | "description": "aka Ragnar Sigurdsson" 28 | }, 29 | { 30 | "id": "HeroThora", 31 | "name": "Thora Town-hart", 32 | "description": "daughter of Earl Herrauðr of Götaland" 33 | } 34 | ], 35 | "villains": [ 36 | { 37 | "id": "VillainMadelyn", 38 | "name": "Madelyn", 39 | "description": "the cat whisperer" 40 | }, 41 | { 42 | "id": "VillainHaley", 43 | "name": "Haley", 44 | "description": "pen wielder" 45 | }, 46 | { 47 | "id": "VillainElla", 48 | "name": "Ella", 49 | "description": "fashionista" 50 | }, 51 | { 52 | "id": "VillainLandon", 53 | "name": "Landon", 54 | "description": "Mandalorian mauler" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /vue-app/docker-compose.debug.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | heroes-vue: 5 | image: heroes-vue 6 | build: 7 | context: . 8 | args: 9 | VUE_APP_API: ${VUE_APP_API} 10 | env_file: 11 | - .env.development 12 | ports: 13 | - 9626:9626 14 | - 9229:9229 15 | command: node --inspect=0.0.0.0:9229 server.js -------------------------------------------------------------------------------- /vue-app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | heroes-vue: 5 | image: heroes-vue 6 | build: 7 | context: . 8 | args: 9 | VUE_APP_API: ${VUE_APP_API} 10 | env_file: 11 | - .env 12 | ports: 13 | - 9626:9626 14 | -------------------------------------------------------------------------------- /vue-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heroes-vue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:e2e": "vue-cli-service test:e2e", 9 | "lint": "vue-cli-service lint", 10 | "backend": "json-server-auth --watch db.json --routes routes.json --port 9627 --middlewares ./node_modules/json-server-reset", 11 | "cypress": "npx cypress open", 12 | "e2e": "concurrently \"npm run quick\" \"npm run cypress\"", 13 | "quick": "concurrently \"npm run backend\" \"npm run serve\"" 14 | }, 15 | "dependencies": { 16 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 17 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 18 | "@fortawesome/vue-fontawesome": "^2.0.0", 19 | "axios": "^0.21.0", 20 | "bulma": "^0.9.1", 21 | "core-js": "^3.6.4", 22 | "express": "^4.17.1", 23 | "vue": "^3.0.0-beta.1", 24 | "vue-router": "^4.0.0-0", 25 | "vuex": "^4.0.0-0" 26 | }, 27 | "devDependencies": { 28 | "@typescript-eslint/eslint-plugin": "^2.33.0", 29 | "@typescript-eslint/parser": "^2.33.0", 30 | "@vue/cli-plugin-babel": "^4.5.9", 31 | "@vue/cli-plugin-eslint": "^4.5.9", 32 | "@vue/cli-plugin-router": "^4.5.9", 33 | "@vue/cli-plugin-typescript": "^4.5.9", 34 | "@vue/cli-plugin-vuex": "^4.5.9", 35 | "@vue/cli-service": "^4.5.9", 36 | "@vue/compiler-sfc": "^3.0.3", 37 | "@vue/eslint-config-airbnb": "^5.0.2", 38 | "@vue/eslint-config-prettier": "^6.0.0", 39 | "@vue/eslint-config-typescript": "^5.0.2", 40 | "babel-eslint": "^10.0.3", 41 | "concurrently": "^5.3.0", 42 | "cypress": "^5.6.0", 43 | "eslint": "^6.0.0", 44 | "eslint-plugin-import": "^2.22.1", 45 | "eslint-plugin-prettier": "^3.1.4", 46 | "eslint-plugin-vue": "^7.1.0", 47 | "json-server": "^0.16.3", 48 | "json-server-auth": "^2.0.2", 49 | "json-server-reset": "^1.3.0", 50 | "node-sass": "^4.13.1", 51 | "prettier": "^2.1.2", 52 | "sass-loader": "^10.1.0", 53 | "typescript": "~3.9.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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/heroes-vue/80be41f6b69a7d511f2e84033d0d980cb3364515/vue-app/public/favicon.ico -------------------------------------------------------------------------------- /vue-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 19 | Heroes Vue 20 | 21 | 22 | 23 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /vue-app/public/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/*", 5 | "serve": "/index.html", 6 | "statusCode": 200 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /vue-app/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1", 3 | "/hero": "/heroes", 4 | "/reset": "/reset", 5 | "users": 600, 6 | "login": "login" 7 | } 8 | -------------------------------------------------------------------------------- /vue-app/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | const port = process.env.PORT || 9626; 5 | const www = process.env.WWW || './dist'; 6 | 7 | const captains = console; 8 | 9 | const start = () => { 10 | app.use(express.static(www)); 11 | captains.log(`serving ${www}`); 12 | app.get('*', (req, res) => { 13 | res.sendFile(`index.html`, { root: www }); 14 | }); 15 | app.listen(port, () => captains.log(`listening on http://localhost:${port}`)); 16 | }; 17 | 18 | start(); 19 | -------------------------------------------------------------------------------- /vue-app/src/app.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /vue-app/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/heroes-vue/80be41f6b69a7d511f2e84033d0d980cb3364515/vue-app/src/assets/logo.png -------------------------------------------------------------------------------- /vue-app/src/components/button-footer.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 69 | -------------------------------------------------------------------------------- /vue-app/src/components/card-content.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /vue-app/src/components/header-bar-brand.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /vue-app/src/components/header-bar-links.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | -------------------------------------------------------------------------------- /vue-app/src/components/header-bar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /vue-app/src/components/list-header.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 65 | -------------------------------------------------------------------------------- /vue-app/src/components/modal.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 53 | -------------------------------------------------------------------------------- /vue-app/src/components/nav-bar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /vue-app/src/components/page-not-found.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /vue-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from '@/app.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | 6 | createApp(App).use(router).use(router).use(store).mount('#app'); 7 | -------------------------------------------------------------------------------- /vue-app/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; 2 | import PageNotFound from '@/components/page-not-found.vue'; 3 | 4 | const routes: Array = [ 5 | { 6 | path: '/', 7 | redirect: '/heroes', 8 | }, 9 | { 10 | path: '/heroes', 11 | name: 'heroes', 12 | component: () => 13 | import(/* webpackChunkName: "heroes" */ './views/heroes/heroes.vue'), 14 | }, 15 | { 16 | path: '/villains', 17 | name: 'villains', 18 | component: () => 19 | import( 20 | /* webpackChunkName: "villains" */ './views/villains/villains.vue' 21 | ), 22 | }, 23 | { 24 | path: '/about', 25 | name: 'about', 26 | // route level code-splitting 27 | // this generates a separate chunk (about.[hash].js) for this route 28 | // which is lazy-loaded when the route is visited. 29 | component: () => 30 | import(/* webpackChunkName: "about" */ './views/about.vue'), 31 | }, 32 | { path: '/:pathMatch(.*)*', name: 'not-found', component: PageNotFound }, 33 | ]; 34 | 35 | const router = createRouter({ 36 | history: createWebHistory(process.env.BASE_URL), 37 | routes, 38 | }); 39 | 40 | export default router; 41 | -------------------------------------------------------------------------------- /vue-app/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /vue-app/src/store/config.ts: -------------------------------------------------------------------------------- 1 | const API = process.env.VUE_APP_API; 2 | 3 | export { API as default }; 4 | -------------------------------------------------------------------------------- /vue-app/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | import heroesModule from './modules/heroes'; 3 | import villainsModule from './modules/villains'; 4 | import type { RootState } from './modules/types'; 5 | 6 | export * from './modules/mutation-types'; 7 | 8 | const store = createStore({ 9 | strict: process.env.NODE_ENV !== 'production', 10 | modules: { 11 | heroes: heroesModule, 12 | villains: villainsModule, 13 | }, 14 | actions: {}, 15 | mutations: {}, 16 | state() { 17 | return {} as RootState; 18 | }, 19 | }); 20 | 21 | export default store; 22 | -------------------------------------------------------------------------------- /vue-app/src/store/modules/action-utils.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | 3 | export const parseList = (response: AxiosResponse) => { 4 | if (response.status !== 200) { 5 | throw Error( 6 | `Data could not be parsed due to status code ${response.status} not matching 200` 7 | ); 8 | } 9 | let list = response.data; 10 | if (typeof list !== 'object') { 11 | list = []; 12 | } 13 | return list; 14 | }; 15 | 16 | export const parseItem = (response: AxiosResponse, code: number) => { 17 | if (response.status !== code) { 18 | throw Error( 19 | `Data could not be parsed due to status code ${response.status} not matching ${code}` 20 | ); 21 | } 22 | let item = response.data; 23 | if (typeof item !== 'object') { 24 | item = undefined; 25 | } 26 | return item; 27 | }; 28 | -------------------------------------------------------------------------------- /vue-app/src/store/modules/heroes.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ActionContext } from 'vuex'; 3 | import { parseItem, parseList } from './action-utils'; 4 | import API from '../config'; 5 | import { 6 | ADD_HERO, 7 | DELETE_HERO, 8 | GET_HEROES, 9 | UPDATE_HERO, 10 | } from './mutation-types'; 11 | import { HeroesState, RootState } from './types'; 12 | import { Hero } from './models'; 13 | 14 | const captains = console; 15 | 16 | export default { 17 | strict: process.env.NODE_ENV !== 'production', 18 | // namespaced: true, 19 | state: { 20 | heroes: [], 21 | }, 22 | mutations: { 23 | [ADD_HERO](state: HeroesState, hero: Hero) { 24 | state.heroes.unshift(hero); // mutate the heroes array 25 | }, 26 | [UPDATE_HERO](state: HeroesState, hero: Hero) { 27 | const index = state.heroes.findIndex((h) => h.id === hero.id); 28 | state.heroes.splice(index, 1, hero); 29 | state.heroes = [...state.heroes]; // replace heroes 30 | }, 31 | [GET_HEROES](state: HeroesState, heroes: Hero[]) { 32 | state.heroes = heroes; // no changes, just get 33 | }, 34 | [DELETE_HERO](state: HeroesState, hero: Hero) { 35 | state.heroes = [...state.heroes.filter((p) => p.id !== hero.id)]; // replace heroes 36 | }, 37 | }, 38 | actions: { 39 | // actions let us get to ({ state, getters, commit, dispatch }) { 40 | async getHeroesAction({ commit }: ActionContext) { 41 | try { 42 | const response = await axios.get(`${API}/heroes`); 43 | const heroes = parseList(response); 44 | commit(GET_HEROES, heroes); 45 | return heroes; 46 | } catch (error) { 47 | return captains.log(error); 48 | } 49 | }, 50 | async deleteHeroAction( 51 | { commit }: ActionContext, 52 | hero: Hero 53 | ) { 54 | try { 55 | const response = await axios.delete(`${API}/heroes/${hero.id}`); 56 | parseItem(response, 200); 57 | commit(DELETE_HERO, hero); 58 | return null; 59 | } catch (error) { 60 | captains.error(error); 61 | } 62 | }, 63 | async updateHeroAction( 64 | { commit }: ActionContext, 65 | hero: Hero 66 | ) { 67 | try { 68 | const response = await axios.put(`${API}/heroes/${hero.id}`, hero); 69 | const updatedHero = parseItem(response, 200); 70 | commit(UPDATE_HERO, updatedHero); 71 | return updatedHero; 72 | } catch (error) { 73 | captains.error(error); 74 | } 75 | }, 76 | async addHeroAction( 77 | { commit }: ActionContext, 78 | hero: Hero 79 | ) { 80 | try { 81 | const response = await axios.post(`${API}/heroes`, hero); 82 | const addedHero = parseItem(response, 201); 83 | commit(ADD_HERO, addedHero); 84 | return addedHero; 85 | } catch (error) { 86 | captains.error(error); 87 | } 88 | }, 89 | }, 90 | getters: { 91 | heroes: (state: HeroesState) => state.heroes, 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /vue-app/src/store/modules/models.ts: -------------------------------------------------------------------------------- 1 | export class Hero { 2 | constructor( 3 | public id: string, 4 | public name: string = '', 5 | public description: string = '' 6 | ) {} 7 | } 8 | 9 | export class Villain { 10 | constructor( 11 | public id: string, 12 | public name: string = '', 13 | public description: string = '' 14 | ) {} 15 | } 16 | -------------------------------------------------------------------------------- /vue-app/src/store/modules/mutation-types.ts: -------------------------------------------------------------------------------- 1 | export const GET_HEROES = 'GET_HEROES'; 2 | export const ADD_HERO = 'ADD_HERO'; 3 | export const UPDATE_HERO = 'UPDATE_HERO'; 4 | export const DELETE_HERO = 'DELETE_HERO'; 5 | export const GET_VILLAINS = 'GET_VILLAINS'; 6 | export const ADD_VILLAIN = 'ADD_VILLAIN'; 7 | export const UPDATE_VILLAIN = 'UPDATE_VILLAIN'; 8 | export const DELETE_VILLAIN = 'DELETE_VILLAIN'; 9 | -------------------------------------------------------------------------------- /vue-app/src/store/modules/types.ts: -------------------------------------------------------------------------------- 1 | import type { Hero, Villain } from './models'; 2 | 3 | export interface RootState {} 4 | 5 | export interface HeroesState { 6 | heroes: Hero[]; 7 | } 8 | 9 | export interface VillainsState { 10 | villains: Villain[]; 11 | } 12 | -------------------------------------------------------------------------------- /vue-app/src/store/modules/villains.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ActionContext } from 'vuex'; 3 | import API from '../config'; 4 | import { parseItem, parseList } from './action-utils'; 5 | import { Villain } from './models'; 6 | import { 7 | ADD_VILLAIN, 8 | DELETE_VILLAIN, 9 | GET_VILLAINS, 10 | UPDATE_VILLAIN, 11 | } from './mutation-types'; 12 | import { RootState, VillainsState } from './types'; 13 | 14 | const captains = console; 15 | 16 | export default { 17 | strict: process.env.NODE_ENV !== 'production', 18 | // namespaced: true, 19 | state: { 20 | villains: [], 21 | }, 22 | mutations: { 23 | [ADD_VILLAIN](state: VillainsState, villain: Villain) { 24 | state.villains.unshift(villain); 25 | }, 26 | [UPDATE_VILLAIN](state: VillainsState, villain: Villain) { 27 | const index = state.villains.findIndex((v) => v.id === villain.id); 28 | state.villains.splice(index, 1, villain); 29 | state.villains = [...state.villains]; 30 | }, 31 | [GET_VILLAINS](state: VillainsState, villains: Villain[]) { 32 | state.villains = villains; 33 | }, 34 | [DELETE_VILLAIN](state: VillainsState, villain: Villain) { 35 | state.villains = [...state.villains.filter((p) => p.id !== villain.id)]; 36 | }, 37 | }, 38 | actions: { 39 | // actions let us get to ({ state, getters, commit, dispatch }) { 40 | async getVillainsAction({ 41 | commit, 42 | }: ActionContext) { 43 | try { 44 | const response = await axios.get(`${API}/villains`); 45 | const villains = parseList(response); 46 | commit(GET_VILLAINS, villains); 47 | return villains; 48 | } catch (error) { 49 | captains.error(error); 50 | } 51 | }, 52 | async deleteVillainAction( 53 | { commit }: ActionContext, 54 | villain: Villain 55 | ) { 56 | try { 57 | const response = await axios.delete(`${API}/villains/${villain.id}`); 58 | parseItem(response, 200); 59 | commit(DELETE_VILLAIN, villain); 60 | return null; 61 | } catch (error) { 62 | captains.error(error); 63 | } 64 | }, 65 | async updateVillainAction( 66 | { commit }: ActionContext, 67 | villain: Villain 68 | ) { 69 | try { 70 | const response = await axios.put( 71 | `${API}/villains/${villain.id}`, 72 | villain 73 | ); 74 | const updatedvillain = parseItem(response, 200); 75 | commit(UPDATE_VILLAIN, updatedvillain); 76 | return updatedvillain; 77 | } catch (error) { 78 | captains.error(error); 79 | } 80 | }, 81 | async addVillainAction( 82 | { commit }: ActionContext, 83 | villain: Villain 84 | ) { 85 | try { 86 | const response = await axios.post(`${API}/villains`, villain); 87 | const addedVillain = parseItem(response, 201); 88 | commit(ADD_VILLAIN, addedVillain); 89 | return addedVillain; 90 | } catch (error) { 91 | captains.error(error); 92 | } 93 | }, 94 | }, 95 | getters: { 96 | villains: (state: VillainsState) => state.villains, 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /vue-app/src/styles.scss: -------------------------------------------------------------------------------- 1 | $vue: #42b883; 2 | $vue-light: #42b883; 3 | $angular: #b52e31; 4 | $angular-light: #eb7a7c; 5 | $react: #00b3e6; 6 | $react-light: #61dafb; 7 | $primary: $vue; 8 | $primary-light: $vue-light; 9 | $link: $primary; // #00b3e6; // #ff4081; 10 | $info: $primary; 11 | $shade-light: #fafafa; 12 | 13 | @import 'bulma/bulma.sass'; 14 | // @import '~bulma/bulma'; 15 | 16 | .menu-list .active-link, 17 | .menu-list .router-link-active { 18 | color: #fff; 19 | background-color: $link; 20 | } 21 | 22 | .not-found { 23 | i { 24 | font-size: 20px; 25 | margin-right: 8px; 26 | } 27 | .title { 28 | letter-spacing: 0px; 29 | font-weight: normal; 30 | font-size: 24px; 31 | text-transform: none; 32 | } 33 | } 34 | 35 | header { 36 | font-weight: bold; 37 | font-family: Arial; 38 | span { 39 | letter-spacing: 0px; 40 | &.tour { 41 | color: #fff; 42 | } 43 | &.of { 44 | color: #ccc; 45 | } 46 | &.heroes { 47 | color: $primary-light; 48 | } 49 | } 50 | .navbar-item.nav-home { 51 | border: 3px solid transparent; 52 | border-radius: 0%; 53 | &:hover { 54 | border-right: 3px solid $primary-light; 55 | border-left: 3px solid $primary-light; 56 | } 57 | } 58 | .fab { 59 | font-size: 24px; 60 | &.js-logo { 61 | color: $primary-light; 62 | } 63 | } 64 | .buttons { 65 | i.fab { 66 | color: #fff; 67 | margin-left: 20px; 68 | margin-right: 10px; 69 | &:hover { 70 | color: $primary-light; 71 | } 72 | } 73 | } 74 | } 75 | 76 | .edit-detail { 77 | .input[readonly] { 78 | background-color: $shade-light; 79 | } 80 | } 81 | 82 | .content-title-group { 83 | margin-bottom: 16px; 84 | h2 { 85 | border-left: 16px solid $primary; 86 | border-bottom: 2px solid $primary; 87 | padding-left: 8px; 88 | padding-right: 16px; 89 | display: inline-block; 90 | text-transform: uppercase; 91 | color: #555; 92 | letter-spacing: 0px; 93 | &:hover { 94 | color: $link; 95 | } 96 | } 97 | button.button { 98 | border: 0; 99 | color: #999; 100 | &:hover { 101 | color: $link; 102 | } 103 | } 104 | } 105 | ul.list { 106 | box-shadow: none; 107 | } 108 | div.card-content { 109 | background-color: $shade-light; 110 | .name { 111 | font-size: 28px; 112 | color: #000; 113 | } 114 | .description { 115 | font-size: 20px; 116 | color: #999; 117 | } 118 | background-color: $shade-light; 119 | } 120 | .card { 121 | margin-bottom: 2em; 122 | } 123 | 124 | label.label { 125 | font-weight: normal; 126 | } 127 | 128 | p.card-header-title { 129 | background-color: $primary; 130 | text-transform: uppercase; 131 | letter-spacing: 4px; 132 | color: #fff; 133 | display: block; 134 | padding-left: 24px; 135 | } 136 | .card-footer button { 137 | font-size: 16px; 138 | i { 139 | margin-right: 10px; 140 | } 141 | color: #888; 142 | &:hover { 143 | color: $link; 144 | } 145 | } 146 | 147 | .modal-card-foot button { 148 | display: inline-block; 149 | width: 80px; 150 | } 151 | 152 | .modal-card-head, 153 | .modal-card-body { 154 | text-align: center; 155 | } 156 | 157 | .field { 158 | margin-bottom: 0.75rem; 159 | } 160 | 161 | .navbar-burger { 162 | margin-left: auto; 163 | } 164 | 165 | button.link { 166 | background: none; 167 | border: none; 168 | cursor: pointer; 169 | } 170 | -------------------------------------------------------------------------------- /vue-app/src/views/about.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 71 | -------------------------------------------------------------------------------- /vue-app/src/views/heroes/hero-detail.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 126 | -------------------------------------------------------------------------------- /vue-app/src/views/heroes/hero-list.vue: -------------------------------------------------------------------------------- 1 | 39 | 75 | 76 | 105 | -------------------------------------------------------------------------------- /vue-app/src/views/heroes/heroes.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 155 | -------------------------------------------------------------------------------- /vue-app/src/views/heroes/use-heroes.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import { Hero } from '@/store/modules/models'; 3 | 4 | export function useHeroes() { 5 | return { 6 | deleteHeroAction, 7 | getHeroesAction, 8 | updateHeroAction, 9 | addHeroAction, 10 | }; 11 | 12 | async function deleteHeroAction(hero: Hero) { 13 | await store.dispatch('deleteHeroAction', hero); 14 | } 15 | 16 | async function getHeroesAction() { 17 | await store.dispatch('getHeroesAction'); 18 | } 19 | 20 | async function updateHeroAction(hero: Hero) { 21 | await store.dispatch('updateHeroAction', hero); 22 | } 23 | 24 | async function addHeroAction(hero: Hero) { 25 | await store.dispatch('addHeroAction', hero); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vue-app/src/views/villains/use-villains.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import { Villain } from '@/store/modules/models'; 3 | 4 | export function useVillains() { 5 | return { 6 | deleteVillainAction, 7 | getVillainsAction, 8 | updateVillainAction, 9 | addVillainAction, 10 | }; 11 | 12 | async function deleteVillainAction(villains: Villain) { 13 | await store.dispatch('deleteVillainAction', villains); 14 | } 15 | 16 | async function getVillainsAction() { 17 | await store.dispatch('getVillainsAction'); 18 | } 19 | 20 | async function updateVillainAction(villains: Villain) { 21 | await store.dispatch('updateVillainAction', villains); 22 | } 23 | 24 | async function addVillainAction(villains: Villain) { 25 | await store.dispatch('addVillainAction', villains); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vue-app/src/views/villains/villain-detail.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 126 | -------------------------------------------------------------------------------- /vue-app/src/views/villains/villain-list.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 74 | -------------------------------------------------------------------------------- /vue-app/src/views/villains/villains.vue: -------------------------------------------------------------------------------- 1 | 132 | 133 | 167 | -------------------------------------------------------------------------------- /vue-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env"], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | "src/**/*.vue", 24 | "tests/**/*.ts", 25 | "tests/**/*.tsx" 26 | ], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------