├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── assets ├── README.md └── scss │ ├── _mixins.scss │ └── _variables.scss ├── components ├── Logo.vue ├── README.md └── polls │ ├── PollDetail.spec.ts │ ├── PollDetail.vue │ ├── PollList.spec.ts │ └── PollList.vue ├── docs ├── 01.init.md ├── 02.typescript.md ├── 03.codecontrol.md ├── 04.polls.md ├── 05.style.md ├── 06.test.md ├── 07.deploy.md ├── README.md └── screenshots │ ├── 01.01_create_project.png │ ├── 01.02_project_structure.png │ ├── 01.03_run.png │ ├── 01.04_project_is_working.png │ ├── 04.01_polls.png │ ├── 04.02_choice_select.png │ ├── 04.03_vote_nuxt.png │ ├── 04.04_voted_nuxt.png │ ├── 05.01_polls_page.png │ ├── 05.02_polls_page_voted.png │ ├── 05.03_votes_with_choices_name.png │ ├── 06.01_coverage.png │ └── 06.02_html_report.png ├── jest.config.js ├── layouts ├── README.md └── default.vue ├── lib └── polls │ ├── __mocks__ │ └── api.ts │ ├── api.spec.ts │ ├── api.ts │ ├── models.spec.ts │ └── models.ts ├── middleware └── README.md ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages ├── README.md ├── index.vue ├── polls.spec.ts └── polls.vue ├── plugins └── README.md ├── static ├── README.md └── favicon.ico ├── store ├── README.md ├── polls │ ├── __mocks__ │ │ └── state.mock.ts │ ├── actions.spec.ts │ ├── actions.ts │ ├── const.ts │ ├── getters.ts │ ├── mutations.spec.ts │ ├── mutations.ts │ ├── state.spec.ts │ ├── state.ts │ └── types.ts └── types.ts ├── tsconfig.json └── vue-shim.d.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Global CircleCI used version 2 | version: 2.1 3 | 4 | # ------------------------------------------------------------------------------ 5 | # Workflows 6 | # version is set at 2 7 | # ------------------------------------------------------------------------------ 8 | workflows: 9 | version: 2 10 | full-flow: 11 | jobs: 12 | - build 13 | - test: 14 | requires: 15 | - build 16 | - pre-deploy: 17 | requires: 18 | - build 19 | filters: 20 | branches: 21 | only: 22 | - master 23 | - deploy-surge: 24 | requires: 25 | - pre-deploy 26 | - test 27 | - deploy-s3: 28 | requires: 29 | - pre-deploy 30 | - test 31 | - deploy-heroku: 32 | requires: 33 | - pre-deploy # not required actually 34 | - test 35 | 36 | # ------------------------------------------------------------------------------ 37 | # Executors: list of executing environments (requires version: 2.1) 38 | # https://circleci.com/docs/2.0/configuration-reference/#executors-requires-version-21 39 | # ------------------------------------------------------------------------------ 40 | executors: 41 | app-builder: 42 | docker: 43 | - image: circleci/node:11-browsers-legacy 44 | working_directory: ~/repo 45 | 46 | # ------------------------------------------------------------------------------ 47 | # Commands: list of re-usable commands (requires version: 2.1) 48 | # https://circleci.com/docs/2.0/configuration-reference/#commands-requires-version-21 49 | # ------------------------------------------------------------------------------ 50 | commands: 51 | load-repo: 52 | description: 'Checkout repository and load dependencies' 53 | steps: 54 | - checkout 55 | - restore_cache: 56 | name: Restore dependencies 57 | keys: 58 | - nuxt-ts-dependencies-{{ checksum "package-lock.json" }} 59 | 60 | # ------------------------------------------------------------------------------ 61 | # Orbs: pre-configuration (requires version: 2.1) 62 | # https://circleci.com/docs/2.0/using-orbs/ 63 | # ------------------------------------------------------------------------------ 64 | orbs: 65 | # https://circleci.com/orbs/registry/orb/circleci/aws-s3 66 | # Config via environment variables: 67 | # https://circleci.com/orbs/registry/orb/circleci/aws-cli 68 | aws-s3: circleci/aws-s3@1.0.4 69 | 70 | # ------------------------------------------------------------------------------ 71 | # Jobs: list of workflow jobs 72 | # ------------------------------------------------------------------------------ 73 | # Jobs 74 | jobs: 75 | # ---------- Building job 76 | build: 77 | executor: app-builder 78 | steps: 79 | - load-repo 80 | - run: 81 | # https://docs.npmjs.com/cli/ci 82 | name: Install Dependencies 83 | command: npm ci 84 | - save_cache: 85 | name: Save dependencies 86 | key: nuxt-ts-dependencies-{{ checksum "package-lock.json" }} 87 | paths: 88 | - ~/repo/node_modules 89 | 90 | # ---------- Testing job 91 | test: 92 | executor: app-builder 93 | steps: 94 | - load-repo 95 | # Code Climate setup & initialise 96 | - run: 97 | name: Code Climate test-reporter setup 98 | command: | 99 | # download test reporter as a static binary 100 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 101 | chmod +x ./cc-test-reporter 102 | ./cc-test-reporter before-build 103 | # Testing configuration must be configured in package.json 104 | - run: 105 | name: Testing 106 | command: npm run test -- --coverage --silent 107 | # Code Climate sending report 108 | - run: 109 | name: Code Climate report 110 | command: | 111 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 112 | ./cc-test-reporter after-build --exit-code $? 113 | fi 114 | 115 | # ---------- Pre-deploy: generate 116 | pre-deploy: 117 | executor: app-builder 118 | steps: 119 | - load-repo 120 | - run: npm run generate 121 | - persist_to_workspace: 122 | root: ./ 123 | paths: 124 | - dist 125 | 126 | # ---------- Deploy: Surge 127 | deploy-surge: 128 | executor: app-builder 129 | steps: 130 | - load-repo 131 | - attach_workspace: 132 | at: ~/repo/ 133 | - run: ./node_modules/.bin/surge --project ~/repo/dist --domain nuxt-ts.surge.sh 134 | 135 | # ---------- Deploy: Surge 136 | deploy-s3: 137 | docker: 138 | - image: circleci/python:2.7 139 | steps: 140 | - attach_workspace: 141 | at: ~/repo/ 142 | - aws-s3/copy: 143 | from: ~/repo/dist 144 | to: 's3://nuxt-ts' 145 | arguments: '--recursive' 146 | 147 | # ---------- Deploy: Heroku 148 | deploy-heroku: 149 | docker: 150 | - image: buildpack-deps:trusty 151 | steps: 152 | - checkout 153 | - run: git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master 154 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .nuxt/ 2 | .vscode/ 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://eslint.org/docs/user-guide/configuring#using-configuration-files-1 3 | root: true, 4 | 5 | // https://eslint.org/docs/user-guide/configuring#specifying-environments 6 | env: { 7 | browser: true, 8 | node: true, 9 | jest: true 10 | }, 11 | 12 | extends: [ 13 | '@nuxtjs', 14 | '@nuxtjs/eslint-config-typescript', 15 | 'prettier', 16 | 'prettier/vue', 17 | 'plugin:prettier/recommended', 18 | 'plugin:nuxt/recommended' 19 | ], 20 | plugins: ['prettier'], 21 | 22 | rules: { 23 | '@typescript-eslint/no-empty-interface': 1, 24 | // https://github.com/typescript-eslint/typescript-eslint/issues/103 25 | '@typescript-eslint/no-parameter-properties': 0 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | # for Travis 22 | cc-test-reporter 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://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 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # Nuxt generate 74 | dist 75 | 76 | # vuepress build output 77 | .vuepress/dist 78 | 79 | # Serverless directories 80 | .serverless 81 | 82 | # IDE 83 | .idea 84 | 85 | # Service worker 86 | sw.* 87 | 88 | # VS Code config 89 | .vscode/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '11.10.1' # Developping under this version 4 | - 'node' # latest stable Node.js release 5 | cache: 6 | directories: 7 | - 'node_modules' 8 | 9 | env: 10 | global: 11 | # $GITHUB_TOKEN 12 | - secure: 'Ch/ME8IZrX4k9y+sYQtsVNc/bQFyBdwJqH1LqLX2HcVYuxIDDEXaYUaef0wI28mhILe8SnAHit0kSTpO8NXirrlgwhwRr/3IwUDVZOutc9hNYheQgu1f6rx46iFC72/rRFpjA4lbgTcNKNdMZbo6ueboFgxY+rx4SXGylvqPiNO1+1kBjUYhRE1H2K9CrlFkgoNHkLjtANtPsWbKBUroJCdNowG2vgGvMgJGLe1i5kH8ibQTQ7XNotL58H/d37MswNV77Rv9lLYfumBbApgQ0MMOpS/WNpw+thS2pGROK0Y5Qq6Xn4nt+XARpEdsM533TsYzpdyzajakPPwy7L2IQV6F2x6kjGUEt1ZE840xtdVleXGJk+M0bl80iQ9JUazYapSdCbINzDdRElKfsb+DkCMIcMn5wYGyWJtsIcEplWaNyc68q3QdVHSqqR6GQuZwsS78E/q0AVsecPURNPJBCOjAAByPMJZogZ7ZD6SaYwfDotO9kBbz53yJpRh97x3lXWU3a9zH2S5kmZKlu0sd54tgwFejjtz5lm4x8kz011oGk9ByZ1U4WPnbHMhkP8tLcMcgUKxBU1616mrYHxVyZY479erBtk3LAqqhQ/0zzOk4peHUCZhd2uBByAkmDyhmSKtBihq6LS8pG2onPx5X4G2O+0an4UFfS+4Qdr/zwCo=' 13 | # SURGE_LOGIN 14 | - secure: 'mqI2HZQFmym2VUOGn0F1BNYVdQdrSRzNR7z+q34kcXnzjAw1YUaMXOFUJV1/EiNJwOkh4sV/mY8x1MUQF6d2jLZNauzZA7xHfowHLKtvmH5OvJ28tioNiMX7xDD7fPSK5LCN9YYGRFMFSFuPOOLrW4DiqXq6OG2fAscqyps2xkA841nacrr7YOg6MBsvwwZBYnCzdr5pl4X7mOZDinMFcgad/W1ObKsVh9Z/VtRMkTp89m6eh2wUGzZOD6rMNRFNKW0cE5mi2+tvrusi4oI1ssWXg8wAA3eiFvHgiUA+uyRWySGekonY3yRqvjLyJrebZF/lgJ97zU3RknHKMu7a6w6fiNwqnusfXhN8tXevt/OtzikxzLOBLM8mOlyLYxCbYyDn6AqtO4mQXJKsQ7nF/EubB+rKQtLcxu+8JaqUvvh4alUoLjWTeg6RWT84/czTW4jO2GgHin3IzDg9kyzABHgDq04fMwD6RhFE1zS8rQJeb+aW8leSckLNXOOdh4I4FVEhGz0aK2eAuJ25HHQsDp0iuolOudIirVTjkDARBjTlOYmA4C6tK88PfqCxIP0xQmSd6jpH3V417hRYQ2qyJn03GFPB72sKMsH/+MHgBLRhHadi+CYoi32zg2mHg/16k6/Rej6B9kZpq845DurvYRVmghqCCeTnjDzHfncQOVo=' 15 | # SURGE_TOKEN 16 | - secure: 'n5m9MHVKkTURwGnAZGvZ4sZLtENVHbNmLMbKlb7jQW2Ml8eJHHkoH7tVaeGzghYa0oQWU/wQfQnPtPXYp2Y1PmUHteT4EhndKaNHPvR88FhhCy3a/gbCEwX58zmLBxwMv3pOTstcGOBplpQr20aNhZH++hdlKWTZgLFZO09LooC8xLknVXrU/77vC9L44wfGq7mINCRwm5TSB1UOW82e38JKAvm+bZOv0KBZ7veu8eF/PtgPrlLEEdqYLdztAaevAqMTNrQ03V053PhYBO067soktrnO07te8KYtBwq6atnPUfB3fQVy5w/9qtWWXSda3D/xYaXkb02XzX/7aQA3oQ6+9AN+V7KCdMfWdi5EZYyU0L0MqBedSHBt+ixytBR+gcA7lTUUSihH10gZ/mii7SjRYDFTKpVQ5SOpcoB3lSrMLYc6St7EPdQX9KvhXbCeqKzF317ZN8bkNHHAD4RdkkSLT1SYlKibRhLWBLr8cKNsTh6Jb3iwT84JbAWWeK4zu/+Fz4Ge7zx1+76H9bEtKS/2RjX0lURGKka3in/ixItNlAyzrMDRiGaCvutpsuPvsmNgS/AMNNkofSRAldVKoWUntswM3Khc2TowBzqEh2uQLuYrE2h0Qrj8fkJwdZWj6Fk0xovr/Uu7te8eK/F5DKkgkPP8Mnr/deLWei1MZgs=' 17 | # CC_TEST_REPORTER_ID 18 | - secure: 'f1SLhfyKPj9b03EMuw+t0GFAbLEKjyegcijbb9FkCVwIAAhAbKVU5Y2HgwPr8iKcAzwnO4sMmwZeUOvsA6v551cgKYI6aki6uq9yBNvudwxeMLj5va2KK/o67ibGhiOgCo5gOJ51VYHyJHDp+9COC07a9HXlvpWXs91DWraABW1kF4peCTGHWLzYx/d+uJ2G8wu9SVm0ZeP7kkBQPCVGuAC0G0qY5ojHoHSqFxG/1lZ+a1mjiY0Iox3HXRQYivydN+0fYD7FAVOwfmn3yQjTU4m4r5XRIw5Zjp7MwsqtBUpn+FHIw+cpz4yCHphgSH7jFMxyb11Fdbho5uLydOElrkioHBq5gRjT6UElJ2uI3SYgeUEhRpCmqUfAIynzvhZ3ebq0Cf6jtV0F9s3wxxCYGdPyCwvR8P0fMa1uykQUzVKwKqYnzaWCHpjHomkcFuZcFsnWKaSM5+g/LftkBFKWtX8IpnS9JkPffc0Y+1ZnULX5W0Nkgnhsngpv5qmNC49W88Hgvpv/WdcC6YgwowwjVJuKQhQWZNMjWKKQOETGlr8WRar/jmamHorlzed0n/FPiMKuAk/kfhCbKBUMxxWaGawbMFLCPja/wL7c6rgpZdthsK8B7mP+2dVdmI9GhYHtm5Uku07LVaa+dAd+vUAxrotYD0ax/OhZ+kxNwOu5P0Y=' 19 | 20 | install: 21 | # Code Climate: initialization 22 | # note: simplecov and simplecov-console gems required 23 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 24 | - chmod +x ./cc-test-reporter 25 | - ./cc-test-reporter before-build 26 | # Dependencies 27 | - npm ci 28 | 29 | script: 30 | - npm run test -- --coverage --silent 31 | 32 | after_script: 33 | # Code Climate: sending report 34 | - './cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT' 35 | 36 | before_deploy: 37 | # build static site 38 | - npm run generate 39 | 40 | deploy: 41 | # Surge configuration 42 | - provider: surge 43 | project: ./dist 44 | domain: nuxt-ts.surge.sh 45 | # Travis configuration 46 | skip-cleanup: true 47 | on: 48 | branch: master 49 | node_js: node 50 | # AWS S3 51 | - provider: s3 52 | access_key_id: 53 | secure: 'LTqnV85UF0/p8pPZm+6bRjGxpsiRFmSYuF0d0lF1DKMOiGfdiOUfZosAXHkaRilfASxRdlI4QF12BL7iq5h17zbgrlMVHF7VzmCHWGwf7tsuS0G7puV0npBSajD/7c2dQ2oOVp1GlI4zB9SUQco+CLk0TZdPrAxqpoJLs0ln+ti1PFxBBiB4pU/WP2vFob3JehZtN4hBhoHpjWDTqiyjRiF0B+f8jLUHPxvnjRoQfi/sdQs3No4Xx5tJ2IH34x6JSqJ3BidVcITaqLNCUNg6jm4aTZf/FTOIgCNO+hAfNr7GHXYilAFn6932GvrE0LIWo3DVrAX39T2ufyZIg7O79nimlusiV4Ss0y1qitbqRM2/hrYJ6hiA1wbEkjaA2Qc09cNNj7UsRtPvV029BTQQqfb7WFQZDtlRPfrd5urBalpu2rN34/gvDBu5ZNaIHDQbHRMt8jZZUZI78sIskJbBclydWKm53KX+6naRfI20HbMrI2akELo/vHx78evT1Nys6630aXwVKrTCcTjfm1dTVR65VFm8KlrD7l5KrJR/cPhdoT+buO3hiSQG6wCNqgr5ggCz6kjxyt78n/7vMXT6M52taD1Tp2e59onS2bwPt6FjOMSVUWGfuC4IhhmlpdjAQ+aSgz/sJdblHEGHLgmaMhiw78h0tTIydv2JwDOY7xU=' 54 | secret_access_key: 55 | secure: 'gRsIII8gAfZBijpJ395pms7FEV9L5bnhoyZnEkfO2tWmJpFaUfl4ejBIeWDh8xZ9CN0I2VzFVsdCJQD9aGOG0B1ST4l2yDmpwTbuh35hdLSXz4X8v3CcmkRnHDjbIDpud07KYcZbsAo1hPlVkbEj8k3Eqa66/FechVcyavrsPF3eqIWOJkeAbTlGoeM2L64BqizUQQ6G5YpHyb4WsMwHzcv8Aidsf3SyV2LlpOax0DEDFtT0UlEquG9noGD03kAX79xBvhWq0EpNw8uXV/1Rc7ORdh+eOJikwyqyOEks3S/xugCvwX2WnxBFGTSesJD6BPg47bCJ8ckkVhYxCMXD2cCjNhYbepsPlFMuwOBpQtFTgTX8UAyL8ZKfdG9P8ql0ECmAK18OxGRskiXGytTfz0zF75Ab/UAhz/paQo+KdwTR85TPDX2BE53K2nTA6JKywIY0QFieHhUzEIDsHli9tN0eENqURsaC2NnfmbjdrysNY2ttbC8UZEs6ZXAuaKg0HrTWD8ofZWYFB5ZQDfwKID1FlCYUJqOSfEWoUTmxU7NWTCZ+ogJR8kBw4lhTp/o6m6tlzzpUJol/sNsOXVEkiLaZ8syWP+KsHM2of30fq1aXD8UwwWXZYwxdlD0c7w6Y48as3r7m4SmUz+n8a7LVRgsw9VEQxIhKDRLBnTITi0w=' 56 | bucket: nuxt-ts 57 | region: eu-west-3 58 | skip-cleanup: true 59 | local_dir: dist 60 | on: 61 | branch: master 62 | node_js: node 63 | # HEROKU 64 | # https://devcenter.heroku.com/changelog-items/1557 65 | - provider: heroku 66 | api_key: 67 | secure: 'Kj3AzLHK0jBp7vK2vpF79tBKv4/4LsMsmxZVsKkjC5OleeNurYlj8r7eHtjE9/VDSk2iP6o1fUo6M/9xAFXckZ7pbZG/HyMyWvsvMClC4cnaLee+y9QhO0nYPuxVIWNCRy98f2zPVMBEx2j5/ZpOyfw+CZEjWslpZLCFecs96H/PCdxp6i+14H/E5HYFhEo6G5NkcN5xez21n91iSX2ypFddPfTC2N99exN58O5HjUAYbywWHNl7y/Qde3TCUJJJg08GrUZKudO8fsMSGss4p2PSIlCzctXrOEvcIUCKxzOxkl3YlLQQPOgLsb+rCtZ3e7rikJMkmBCSwDYMXytNF7McD8h7zK5JGpdzGiYNl+W3jpkEMN7tKk4RPrscZeSGrJ0ImqLMAzZnKHue0FKLPrHPAIcyYqnqhDLASwXFPmtB9MRknq/tOOFydOtoaBCAIj5t7lgqG+uk7lu9rhaKqqWgbrA27fBjqOSrX6Y/SbqB4FiRM/RyQPozxQTOcbsIf4YwDV3AukZxBoRhcLz6Euo51N+drGWruLOU+TG/uHM5j+It/KGC3LPYKaTbkqtWVvP2jwiCYBWb/cTgPl/jd7eQutZ2c/DYNrrit6f4Y9wLBMll6RvTaVf2OHzK/zcmfIJ0Ka4UwkgahrWHW/nh67RBVUeYUWvQDD1D6zNJpZE=' 68 | app: nuxt-ts 69 | on: 70 | branch: master 71 | node_js: node 72 | 73 | notifications: 74 | email: false 75 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /assets/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin lt-sm { 2 | @media screen and (max-width: 575px) { 3 | @content; 4 | } 5 | } 6 | @mixin gt-sm { 7 | @media screen and (min-width: 576px) { 8 | @content; 9 | } 10 | } 11 | 12 | /* https://medium.com/@ladyleet/adding-box-shadow-z-depth-to-angular-material-2-components-6bd0de303dcb */ 13 | %transition-box-shadown { 14 | transition: box-shadow 0.2; 15 | } 16 | %z-depth-1 { 17 | @extend %transition-box-shadown; 18 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 19 | 0 3px 1px -2px rgba(0, 0, 0, 0.2); 20 | } 21 | 22 | %z-depth-2 { 23 | @extend %transition-box-shadown; 24 | box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 25 | 0 2px 4px -1px rgba(0, 0, 0, 0.3); 26 | } 27 | 28 | %z-depth-3 { 29 | @extend %transition-box-shadown; 30 | box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 31 | 0 3px 5px -1px rgba(0, 0, 0, 0.3); 32 | } 33 | 34 | %z-depth-4 { 35 | @extend %transition-box-shadown; 36 | box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 37 | 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.3); 38 | } 39 | 40 | %z-depth-5 { 41 | @extend %transition-box-shadown; 42 | box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 43 | 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.3); 44 | } 45 | -------------------------------------------------------------------------------- /assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // https://www.canva.com/learn/website-color-schemes/ 11. cool vs warm 2 | $colorBgStart: #233237; 3 | $colorBgEnd: #18121e; 4 | 5 | // https://www.color-hex.com/color-palette/1294 6 | $colorPrimary: #005b96; 7 | $colorPrimaryDark: #03396c; 8 | // https://www.canva.com/learn/website-color-schemes/ 14. Tint and Tones 9 | $colorSecondary: #94618e; 10 | $colorSecondaryDark: #49274a; 11 | -------------------------------------------------------------------------------- /components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 80 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /components/polls/PollDetail.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { shallowMount, Wrapper, WrapperArray } from '@vue/test-utils'; 3 | 4 | import PollDetail from './PollDetail.vue'; 5 | import { DUMMY_POLLS } from '@/lib/polls/__mocks__/api'; 6 | import { Poll } from '@/lib/polls/models'; 7 | 8 | // Component config 9 | let wrapper: Wrapper; 10 | let mockedVote: jest.Mock; 11 | const poll: Poll = DUMMY_POLLS[0]; 12 | 13 | describe('PollDetail', () => { 14 | describe('initializing', () => { 15 | mockedVote = jest.fn(); 16 | 17 | beforeEach(() => { 18 | wrapper = shallowMount(PollDetail, { 19 | propsData: { poll }, 20 | methods: { vote: mockedVote } 21 | }); 22 | }); 23 | 24 | test('has no selected choice', () => { 25 | expect(wrapper.vm.$data.selectedChoiceId).toBe(-1); 26 | }); 27 | }); 28 | 29 | describe('Choices', () => { 30 | let choicesWrapper: WrapperArray; 31 | let choices: Wrapper[]; 32 | beforeEach(() => { 33 | choicesWrapper = wrapper.findAll('.poll__choice--container'); 34 | choices = choicesWrapper.wrappers; 35 | }); 36 | 37 | test('are properly rendered', () => { 38 | expect(choicesWrapper.length).toBe(poll.choices.length); 39 | 40 | choices.forEach((choiceWrapper, index) => { 41 | const choiceBox = choiceWrapper.find('.poll__choice--box'); 42 | const choiceText = choiceBox.findAll('span').wrappers[0].text(); 43 | const countText = choiceBox.findAll('span').wrappers[1].text(); 44 | expect(choiceText).toContain(poll.choices[index].text); 45 | expect(countText).toContain(poll.choices[index].count); 46 | }); 47 | }); 48 | 49 | test('clicking on a choice selects it', () => { 50 | choices.forEach((choiceWrapper, index) => { 51 | choiceWrapper.trigger('click'); 52 | const choiceId = poll.choices[index].id; 53 | expect(wrapper.vm.$data.selectedChoiceId).toBe(choiceId); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('when no choice is selected', () => { 59 | test('voting form is not rendered', () => { 60 | wrapper.setData({ selectedChoiceId: -1 }); 61 | 62 | const pollVote = wrapper.find('.poll__vote'); 63 | expect(pollVote.exists()).toBeFalsy(); 64 | }); 65 | }); 66 | 67 | describe('when a choice a selected', () => { 68 | const selectedChoiceId = poll.choices[1].id; 69 | test('is visible when a choice is selected', () => { 70 | wrapper.setData({ selectedChoiceId }); 71 | 72 | const pollVote = wrapper.find('.poll__vote'); 73 | expect(pollVote.exists()).toBeTruthy(); 74 | }); 75 | 76 | describe('voting', () => { 77 | test('is called when clicking vote button', () => { 78 | const voteBtn = wrapper.find('.poll__vote > button'); 79 | voteBtn.trigger('click'); 80 | expect(mockedVote).toHaveBeenCalledTimes(1); 81 | expect(mockedVote).toHaveBeenCalledWith({ 82 | choiceId: selectedChoiceId, 83 | comment: undefined 84 | }); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /components/polls/PollDetail.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 75 | 76 | 144 | -------------------------------------------------------------------------------- /components/polls/PollList.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount, Wrapper } from '@vue/test-utils'; 2 | 3 | import PollDetail from './PollDetail.vue'; 4 | import PollList from './PollList.vue'; 5 | import { DUMMY_POLLS, DUMMY_VOTES } from '@/lib/polls/__mocks__/api'; 6 | 7 | // Component config 8 | let wrapper: Wrapper; 9 | const propsData = { polls: DUMMY_POLLS, votes: DUMMY_VOTES }; 10 | 11 | describe('PollList', () => { 12 | beforeEach(() => { 13 | wrapper = shallowMount(PollList, { propsData }); 14 | }); 15 | 16 | test('renders all Polls', () => { 17 | const pollsWrapper = wrapper.findAll(PollDetail); 18 | expect(pollsWrapper.length).toBe(DUMMY_POLLS.length); 19 | }); 20 | 21 | test('renders all Votes', () => { 22 | const votesWrapper = wrapper.findAll('.poll__vote'); 23 | expect(votesWrapper.length).toBe(DUMMY_VOTES.length); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /components/polls/PollList.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 68 | 69 | 84 | -------------------------------------------------------------------------------- /docs/01.init.md: -------------------------------------------------------------------------------- 1 | ## Initialise project 2 | 3 | Create a NuxtJS project as usual ([Nuxt doc](https://nuxtjs.org/guide/installation)): 4 | 5 | ```sh 6 | yarn create nuxt-app {your project name} 7 | ``` 8 | 9 | For the sake of the tutorial, my _nuxt-ts_ project is bare metal: no 10 | plugins, no CSS framework, default server framework, no test 11 | framework. I selected _Yarn_ as my package manager. 12 | 13 | ![Project creation](screenshots/01.01_create_project.png) 14 | 15 | The Nuxt project structure is explained in [Nuxt doc](https://nuxtjs.org/guide/directory-structure): 16 | 17 | ![Project structure](screenshots/01.02_project_structure.png) 18 | 19 | Run the scaffold project: 20 | 21 | ```sh 22 | cd {your project folder} 23 | yarn dev 24 | ``` 25 | 26 | Nuxt should not encounter any error: 27 | 28 | ![Project run](screenshots/01.03_run.png) 29 | 30 | And the application is available on : 31 | 32 | ![Project is working](screenshots/01.04_project_is_working.png) 33 | -------------------------------------------------------------------------------- /docs/02.typescript.md: -------------------------------------------------------------------------------- 1 | 2 | ## Switch to TypeScript 3 | 4 | > [13-Apr-2020] Please use the official Nuxt TypeScript package: https://typescript.nuxtjs.org/guide/setup.html 5 | 6 | Before adding anything, let switch to TypeScript first. I was 7 | guided by the two following links: 8 | 9 | - [Nuxt-ts helloworld](https://codesandbox.io/s/github/nuxt/nuxt.js/tree/dev/examples/typescript) 10 | - [Hackernews on nuxt-ts](https://github.com/nuxt-community/hackernews-nuxt-ts) 11 | 12 | ### Adding Nuxt-ts 13 | 14 | ```sh 15 | yarn add nuxt-ts 16 | yarn add --dev nuxt-property-decorator 17 | yarn remove nuxt 18 | ``` 19 | 20 | - `nuxt-ts` is TypeScript counterpart of `nuxt` 21 | - [`nuxt-property-decorator`](https://github.com/nuxt-community/nuxt-property-decorator/) is 22 | Nuxt counter part of [`vue-property-decorator`](https://github.com/kaorun343/vue-property-decorator) 23 | 24 | Optionally, you can add TypeScript as a development dependency: 25 | 26 | ```sh 27 | yarn add --dev typescript 28 | ``` 29 | 30 | For VS Code users, you can force the usage of TypeScript from _node_modules/_ 31 | instead of using default VS Code TypeScript thanks to _.vscode/settings.json_: 32 | 33 | ```json 34 | { 35 | // Windows version 36 | "typescript.tsdk": "node_modules\\typescript\\lib", 37 | // Linux 38 | "typescript.tsdk": "node_modules/typescript/lib" 39 | } 40 | ``` 41 | 42 | ### Update configuration 43 | 44 | Few configurations have to be added or changed: 45 | 46 | Update _package.json_ by using `nuxt-ts` instead of `nuxt`: 47 | 48 | ```json 49 | { 50 | "scripts": { 51 | "dev": "nuxt-ts", 52 | "build": "nuxt-ts build", 53 | "start": "nuxt-ts start", 54 | "generate": "nuxt-ts generate" 55 | } 56 | } 57 | ``` 58 | 59 | Add a tsconfig.json: 60 | 61 | ```json 62 | { 63 | "extends": "@nuxt/typescript", 64 | "compilerOptions": { 65 | "baseUrl": ".", 66 | "types": ["@types/node", "@nuxt/vue-app"], 67 | "experimentalDecorators": true, 68 | "resolveJsonModule": true, 69 | "esModuleInterop": true, 70 | "paths": { 71 | "@/*": ["*"], 72 | "~/*": ["*"] 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | - `experimentalDecorators` is required when using Vue decorators 79 | - `resolveJsonModule` and `esModuleInterop` are required when 80 | [importing JSON in TypeScript](https://hackernoon.com/import-json-into-typescript-8d465beded79) 81 | - `paths` are Nuxt convention and [it rocks with VS Code](https://medium.com/@caludio/how-to-use-module-path-aliases-in-visual-studio-typescript-and-javascript-e7851df8eeaa) 82 | 83 | Renaming _nuxt.config.js_ into _nuxt.config.ts_. I simply change the first line: 84 | 85 | ```diff 86 | - var pkg = require('./package') 87 | + import pkg from "./package.json"; 88 | ``` 89 | 90 | and commented out the `build: { ... }` content to avoid having a _"declared but never used"_ 91 | and _"implicitly has an 'any' type"_ error 92 | 93 | ### Update existing code 94 | 95 | The application scaffold is not much, only _pages/index.vue_ has to 96 | be updated: 97 | 98 | ```vue 99 | 111 | ``` 112 | 113 | > Don't forget `lang="ts"`! 114 | -------------------------------------------------------------------------------- /docs/03.codecontrol.md: -------------------------------------------------------------------------------- 1 | 2 | ## Code control: formatter and linter 3 | 4 | ### Prettier 5 | 6 | [Prettier](https://prettier.io) is an opinionated formatter. 7 | 8 | ```sh 9 | yarn add --dev prettier 10 | ``` 11 | 12 | VS Code users, don't forget the [VS Code Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 13 | extension. 14 | 15 | Add a _.prettierrc_ file to configure prettier. Options can be found on 16 | [Prettier documentation](https://prettier.io/docs/en/options.html): 17 | 18 | ```json 19 | { 20 | "semi": true, 21 | "singleQuote": true 22 | } 23 | ``` 24 | 25 | ### ESLint 26 | 27 | At first, I planned to use [TSLint](https://palantir.github.io/tslint/) but 28 | [_TypeScript ecosystem is moving from TSLint to ESLint_](https://cmty.app/nuxt/nuxt.js/issues/c8742) 29 | so let's move as well. 30 | 31 | Let's add [ESLint](https://eslint.org) some plugins: 32 | 33 | - [`@typescript-eslint/eslint-plugin`](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin) 34 | - [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier) due 35 | to our _Prettier_ usage 36 | - [`eslint-plugin-vue`](https://vuejs.github.io/eslint-plugin-vue/) per [Nuxt documentation](https://nuxtjs.org/guide/development-tools/#eslint-and-prettier) 37 | 38 | ```sh 39 | yarn add --dev eslint @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-vue 40 | ``` 41 | 42 | Configure ESLint with the _.eslintrc.js_ file: 43 | 44 | ```js 45 | module.exports = { 46 | root: true, 47 | 48 | env: { 49 | browser: true, 50 | node: true 51 | }, 52 | 53 | parser: 'vue-eslint-parser', 54 | parserOptions: { 55 | parser: '@typescript-eslint/parser', 56 | ecmaVersion: 2017, 57 | sourceType: 'module', 58 | project: './tsconfig.json' 59 | }, 60 | 61 | extends: [ 62 | 'eslint:recommended', 63 | 'plugin:@typescript-eslint/recommended', 64 | 'plugin:vue/recommended', 65 | 'prettier', 66 | 'prettier/vue', 67 | 'prettier/@typescript-eslint' 68 | ], 69 | 70 | plugins: ['vue', '@typescript-eslint'] 71 | }; 72 | ``` 73 | 74 | Few explanations: 75 | 76 | - `vue-eslint-parser` is required by `eslint-plugin-vue` (check [doc](https://vuejs.github.io/eslint-plugin-vue/user-guide/#faq)) 77 | and as `@typescript-eslint/parser` is required, it is moved to `parserOptions` 78 | - Order in `extends` matters. Prettier configurations are at the end to ensure they 79 | override other rules 80 | - `env` is set to `browser` and `node` for SSR reasons (check [Nuxt doc](https://nuxtjs.org/guide/development-tools/#eslint-and-prettier)) 81 | -------------------------------------------------------------------------------- /docs/04.polls.md: -------------------------------------------------------------------------------- 1 | 2 | ## Polls: Components and Vuex 3 | 4 | Let's explorer further `nuxt-property-decorator`. Instead of the eternal _Counter_ example, 5 | let's try something, not that different: Polls. Polls have questions with multiple choices. 6 | The interface lets you vote for choice and you can optionally add some comment to your vote. 7 | 8 | ### Polls Models 9 | 10 | Before getting into VueJS/Nuxt affairs, Polls models must be defined. This is 11 | pure TypeScript writing so I won't dive into details. 12 | 13 | I define my models in _lib/polls/models.ts_. As models are not depending on 14 | the store nor components, I decided to create a _lib/_ folder to serve this 15 | purpose. 16 | 17 | ```ts 18 | /** 19 | * A vote for a given choice 20 | */ 21 | export class Vote { 22 | public constructor( 23 | public id: number, 24 | public choiceId: number, 25 | public comment?: string 26 | ) {} 27 | } 28 | 29 | /** 30 | * A choice to vote for within a Poll 31 | */ 32 | export class Choice { 33 | public count: number; 34 | 35 | public constructor( 36 | public id: number, 37 | public pollId: number, 38 | public text: string 39 | ) { 40 | this.count = 0; 41 | } 42 | } 43 | 44 | /** 45 | * A topic with which user is offered multiple choices to vote for 46 | */ 47 | export class Poll { 48 | public choices: Choice[]; 49 | 50 | public constructor( 51 | public id: number, 52 | public topic: string, 53 | choices?: Choice[] 54 | ) { 55 | this.choices = choices !== undefined ? choices : []; 56 | } 57 | } 58 | 59 | /** 60 | * An intention of voting a given choice with an optional comment 61 | */ 62 | export interface ChoiceVote { 63 | choiceId: number; 64 | comment?: string; 65 | } 66 | ``` 67 | 68 | To provide polls, I created a _lib/polls/api.ts_ file with a dummy variables: 69 | 70 | ```ts 71 | import { Poll } from './models'; 72 | 73 | /** 74 | * Dummy polls 75 | */ 76 | export const DUMMY_POLLS: Poll[] = [ 77 | { 78 | id: 1, 79 | topic: 'Which framework are you using?', 80 | choices: [ 81 | { id: 1, count: 0, pollId: 1, text: 'NuxtJS' }, 82 | { id: 2, count: 0, pollId: 1, text: 'Plain VueJS' }, 83 | { id: 3, count: 0, pollId: 1, text: 'Angular' }, 84 | { id: 4, count: 0, pollId: 1, text: 'React' } 85 | ] 86 | }, 87 | { 88 | id: 2, 89 | topic: 'What is your OS?', 90 | choices: [ 91 | { id: 5, count: 0, pollId: 2, text: 'Windows' }, 92 | { id: 6, count: 0, pollId: 2, text: 'Linux' }, 93 | { id: 7, count: 0, pollId: 2, text: 'MacOS' } 94 | ] 95 | } 96 | ]; 97 | ``` 98 | 99 | ### Polls Page 100 | 101 | Let's add an empty page _pages/polls.vue_. For lazy people like me, feel free to add a 102 | link : TypeScript components in _pages/index.vue_: 103 | 104 | ```html 105 | 111 | ``` 112 | 113 | ### Polls Components 114 | 115 | The _Polls_ page must display a list of polls. So let's create a _components/polls/PollList.vue_: 116 | 117 | ```vue 118 | 138 | 139 | 158 | ``` 159 | 160 | Notes: 161 | 162 | - **Props usage**: Polls and votes are provided as _props_. Why? My rule of thumb 163 | is that components should not care if a list is properly loaded or not: it is 164 | the responsibility of the appropriate page. 165 | - **Props decorator**: Please note the `@Prop` decorator usage. For people not comfortable with 166 | [_Class-Style Vue Components_](https://vuejs.org/v2/guide/typescript.html#Class-Style-Vue-Components), 167 | feel free to check the [Writing class based components with Vue.js and TypeScript](https://alligator.io/vuejs/typescript-class-components/) 168 | article. 169 | - **Distinct keys**: votes and polls are rendered within the same component. To 170 | avoid key conflict (ex: first poll key is "1" and first vote key is "1"), some 171 | prefix is prepended. 172 | 173 | For code flexibility reason, I defined poll details in a dedicated _components/polls/PollDetail.vue_: 174 | 175 | ```vue 176 | 197 | 198 | 234 | ``` 235 | 236 | Still following class style syntax, `data` are simply defined by class attributes. 237 | The usage of `undefined` is not recommended as data must be initialized to 238 | be reactive. Check the [Vue doc](https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties) 239 | for more information. 240 | 241 | Selected choice is first defined by `selectChoice()`. Once a choice has 242 | been selected, the `textarea` and `button` appear so that the choice 243 | can be voted for. 244 | 245 | We can now add `` to our _pages/polls.vue_ and populate it with 246 | dummy data: 247 | 248 | ```vue 249 | 252 | 253 | 280 | ``` 281 | 282 | > Because [_non-null assertion operator_](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator) 283 | > is used, `this.polls` and `this.votes` cannot be left undefined. In a 284 | > pure TypeScript program, they would need to be initialized in the `constructor()`. 285 | > The usage of `created()` over `mounted()` is simple: When `mounted()` is called, 286 | > HTML template is compiled but at this stage, both `this.polls` and `this.votes` 287 | > are still undefined. Consequently, they need to be defined earlier. To 288 | > know more about VueJS component lifecycle, please check [Vue doc](https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram). 289 | 290 | At this stage, should render an ugly list: 291 | 292 | ![Polls simple display](screenshots/04.01_polls.png) 293 | 294 | If a choice is clicked (no pointer cursor to show it), then _[SELECTED]_ is 295 | prepended and a `textbox` appears: 296 | 297 | ![Choice select](screenshots/04.02_choice_select.png) 298 | 299 | Clicking on _Vote!_ button does not do anything apart from logging into the 300 | console and resetting the selected choice. 301 | 302 | Note that as choice selection is defined at `PollDetail` level. Consequently, 303 | you can select a choice from a poll and another choice from another poll at 304 | the same first. 305 | 306 | ### Polls Store 307 | 308 | Static data are a bit boring: a store would make it a tad more lively. Nuxt 309 | requires a specific folder structure for Vuex modules. For those who need 310 | it, the [documentation is over there](https://nuxtjs.org/guide/vuex-store/). 311 | 312 | #### Root state 313 | 314 | For typing sake, let's start with an empty _store/types.ts_, our root state 315 | definition: 316 | 317 | ```ts 318 | export interface RootState {} 319 | ``` 320 | 321 | ESLint would complain about having an empty interface so I switched it from 322 | `error` to `warning` by adding in the _.eslintrc.js_: 323 | 324 | ```js 325 | rules: { 326 | "@typescript-eslint/no-empty-interface": 1 327 | } 328 | ``` 329 | 330 | #### Polls store types 331 | 332 | Let's first define all the types required for our polls store with a 333 | _store/polls/types.ts_. To summarize our polls store: 334 | 335 | - state only contains polls & votes 336 | - actions are "loading polls" and "vote" 337 | - mutations are similar with a "setPolls" and "vote" 338 | - there is no getter 339 | 340 | ```ts 341 | import { Poll, Vote, ChoiceVote } from '@/lib/polls/models'; 342 | import { ActionTree, ActionContext, MutationTree, GetterTree } from 'vuex'; 343 | import { RootState } from '../types'; 344 | 345 | export interface PollsState { 346 | polls: Poll[]; 347 | votes: Vote[]; 348 | } 349 | 350 | /** 351 | * Create a type for convenience 352 | */ 353 | export type PollActionContext = ActionContext; 354 | 355 | /** 356 | * Polls actions 357 | */ 358 | export interface PollsActions extends ActionTree { 359 | load: (ctx: PollActionContext) => void; 360 | vote: (ctx: PollActionContext, choiceVote: ChoiceVote) => void; 361 | } 362 | 363 | /** 364 | * Polls mutations 365 | */ 366 | export interface PollsMutations extends MutationTree { 367 | setPolls: (state: PollsState, polls: Poll[]) => void; 368 | vote: (state: PollsState, vote: Vote) => void; 369 | } 370 | 371 | /** 372 | * Polls getters is type instead of interface because it is 373 | * empty 374 | */ 375 | export type PollsGetters = GetterTree; 376 | ``` 377 | 378 | #### Polls state 379 | 380 | Let's start define our polls initial state. Nothing fancy, it just reflects 381 | the needs of _pages/polls.vue_. In _store/polls/state.ts_: 382 | 383 | ```ts 384 | import { PollsState } from './types'; 385 | 386 | export const initState = (): PollsState => ({ 387 | polls: [], 388 | votes: [] 389 | }); 390 | 391 | export default initState; 392 | ``` 393 | 394 | Notes: 395 | 396 | - I use [split modules files](https://nuxtjs.org/guide/vuex-store/#module-files) so 397 | each file requires a default export. 398 | - State must either be a value or a function. 399 | 400 | #### Polls getters 401 | 402 | Getters are not relevant for polls. You can skip this part. I added an empty 403 | _store/polls/getters.ts_: 404 | 405 | ```ts 406 | import { PollsGetters } from './types'; 407 | 408 | export const getters: PollsGetters = {}; 409 | 410 | export default getters; 411 | ``` 412 | 413 | #### Polls actions 414 | 415 | Actions are implemented in _store/polls/actions.ts_: 416 | 417 | ```ts 418 | import { PollsActions } from './types'; 419 | import { loadPolls } from '@/lib/polls/api'; 420 | import { Vote } from '@/lib/polls/models'; 421 | 422 | export const actions: PollsActions = { 423 | load: async ({ commit }) => { 424 | const polls = await loadPolls(); 425 | commit('setPolls', polls); 426 | }, 427 | 428 | vote: ({ commit, state }, { choiceId, comment }) => { 429 | const voteId = state.votes.length 430 | ? state.votes[state.votes.length - 1].id + 1 431 | : 1; 432 | const vote = new Vote(voteId, choiceId, comment); 433 | commit('vote', vote); 434 | } 435 | }; 436 | 437 | export default actions; 438 | ``` 439 | 440 | Notes: 441 | 442 | - Vote ID is artificially generated in the action. In real life, it should 443 | be generated by a back-end 444 | - `async / await` FTW! 445 | - _lib/polls/api.ts_ has a new method: `loadPolls`: 446 | ```ts 447 | export const loadPolls = async (): Promise => { 448 | return new Promise(resolve => 449 | setTimeout(() => resolve(DUMMY_POLLS), 500) 450 | ); 451 | }; 452 | ``` 453 | It simulates a back-end called with a 500ms latency. 454 | 455 | #### Polls mutations 456 | 457 | To update the state, the mutations called by actions must be defined 458 | in _store/polls/mutations.ts_: 459 | 460 | ```ts 461 | import { PollsMutations } from './types'; 462 | 463 | export const mutations: PollsMutations = { 464 | setPolls: (state, polls) => { 465 | state.polls = polls; 466 | }, 467 | 468 | vote: (state, vote) => { 469 | // add vote 470 | state.votes.push(vote); 471 | 472 | // update choice 473 | state.polls 474 | .map(poll => poll.choices) 475 | .reduce((prev, curr) => prev.concat(curr), []) 476 | .filter(choice => choice.id === vote.choiceId) 477 | .forEach(choice => (choice.count += 1)); 478 | } 479 | }; 480 | 481 | export default mutations; 482 | ``` 483 | 484 | #### Polls namespace 485 | 486 | For convenience purpose, let's define polls namespace in a _store/polls/const.ts_: 487 | 488 | ```ts 489 | import { namespace } from 'vuex-class'; 490 | 491 | export const pollsModule = namespace('polls/'); 492 | ``` 493 | 494 | #### Connect store to page/components 495 | 496 | Now our state is ready, let's connect it to our page and components. 497 | 498 | _pages/polls.vue_ script is updated as follows: 499 | 500 | ```diff 501 | import { Component, Vue } from 'nuxt-property-decorator'; 502 | 503 | import PollList from '@/components/polls/PollList.vue'; 504 | import { Poll, Vote } from '@/lib/polls/models'; 505 | +import { pollsModule } from '@/store/polls/const'; 506 | 507 | @Component({ 508 | components: { 509 | PollList 510 | } 511 | }) 512 | 513 | export default class Polls extends Vue { 514 | + @pollsModule.State('polls') 515 | public polls!: Poll[]; 516 | + @pollsModule.State('votes') 517 | public votes!: Vote[]; 518 | 519 | @pollsModule.Action('load') 520 | private loadPolls!: () => void; 521 | 522 | mounted() { 523 | this.loadPolls(); 524 | } 525 | 526 | - created() { 527 | - // provide some dummy data 528 | - this.polls = DUMMY_POLLS; 529 | - this.votes = [ 530 | - { id: 1, choiceId: 1 }, 531 | - { id: 2, choiceId: 2, comment: 'some comment' } 532 | - ]; 533 | - } 534 | } 535 | ``` 536 | 537 | - Because `nuxt-property-decorator` includes `vuex-class`, let's use it. An 538 | alternative I have not tested is [`vuex-typex`](https://github.com/mrcrowl/vuex-typex) 539 | (Check out [this _Writing Vuex Stores in TypeScript_ article](https://frontendsociety.com/writing-vuex-stores-in-typescript-b570ca34c2a?gi=f5f9d39cc2e1)) 540 | - Our previous data `polls` and `votes` are now mapped to state. Additionally, 541 | the `load` action is also mapped 542 | - Lifecycle hook is `mounted` instead of `created` 543 | 544 | Because `votes` is initialized as an empty list, you should have the same 545 | content at except that no vote is displayed. 546 | 547 | Time to vote! Let's update _components/polls/PollDetail.vue_ script: 548 | 549 | ```diff 550 | /** 551 | * Optional comment 552 | */ 553 | private comment: string = ''; 554 | /** 555 | * Avoid undefined to make it reactive 556 | */ 557 | private selectedChoiceId: number = -1; 558 | 559 | @Prop({ type: Object }) 560 | - public poll: Poll; 561 | + public poll!: Poll; 562 | 563 | + @pollsModule.Action('vote') 564 | + private vote!: (choiceVote: ChoiceVote) => void; 565 | 566 | public selectChoice(choice: Choice): void { 567 | this.selectedChoiceId = choice.id; 568 | } 569 | 570 | public voteChoice(): void { 571 | - console.log('Voting: ', { 572 | - choiceId: this.selectedChoiceId, 573 | - comment: this.comment.length > 0 ? this.comment : undefined 574 | - }); 575 | + this.vote({ 576 | + choiceId: this.selectedChoiceId, 577 | + comment: this.comment.length > 0 ? this.comment : undefined 578 | + }); 579 | 580 | // reset vote selection 581 | this.selectedChoiceId = -1; 582 | this.comment = ''; 583 | } 584 | ``` 585 | 586 | Our store is now operational. If you vote with a comment: 587 | 588 | ![Vote!](screenshots/04.03_vote_nuxt.png) 589 | 590 | The vote appears, with its comments and when selecting a choice from the same 591 | poll, the comment text is cleared: 592 | 593 | ![Voted!](screenshots/04.04_voted_nuxt.png) 594 | -------------------------------------------------------------------------------- /docs/05.style.md: -------------------------------------------------------------------------------- 1 | ## Style 2 | 3 | This part is not related to TypeScript but having a plain interface is somehow 4 | hurtful. Let's dress our pages. 5 | 6 | ### SCSS 7 | 8 | SCSS will help us to add some colours! Nuxt community makes our lives 9 | easier with the [`style-resources-module`](https://github.com/nuxt-community/style-resources-module). 10 | 11 | > Many thanks to [this article](https://hackernoon.com/how-i-use-scss-variables-mixins-functions-globally-in-nuxt-js-projects-while-compiling-css-utilit-58bb6ff30438) 12 | 13 | ```sh 14 | yarn add --dev sass-loader node-sass @nuxtjs/style-resources 15 | ``` 16 | 17 | Following the `style-resources-module` documentation, let's update our _nuxt.config.ts_: 18 | 19 | ```ts 20 | module.exports = { 21 | modules: ['@nuxtjs/style-resources'], 22 | 23 | styleResources: { 24 | // Don't forget to create both empty files 25 | // if you use this 26 | scss: [ 27 | // theme variables 28 | './assets/scss/_variables.scss', 29 | // mixins & abstract classes 30 | './assets/scss/_mixins.scss' 31 | ] 32 | } 33 | }; 34 | ``` 35 | 36 | SCSS can now be used in _.vue_ files: 37 | 38 | ```html 39 | 40 | ``` 41 | 42 | Additionally, thanks to `style-resources-module`, all variables in _./assets/scss/\_variables.scss_ 43 | and mixins from _./assets/scss/\_mixins.scss_ are available in _.vue_ files. 44 | 45 | ### Styling 46 | 47 | Nothing very exciting over here, just regular (S)CSS. As for the theme colors, I was never 48 | inspired when it comes to colors. Feel free to pick up you own colors. 49 | 50 | ```scss 51 | // assets/scss/_variables.scss 52 | 53 | $colorBgStart: #233237; 54 | $colorBgEnd: #18121e; 55 | $colorPrimary: #005b96; 56 | $colorPrimaryDark: #03396c; 57 | $colorSecondary: #94618e; 58 | $colorSecondaryDark: #49274a; 59 | ``` 60 | 61 | I just added some media queries, for responsiveness, and some material 62 | design inspired box shadows as mixins: 63 | 64 | ```scss 65 | @mixin lt-sm { 66 | @media screen and (max-width: 575px) { 67 | @content; 68 | } 69 | } 70 | @mixin gt-sm { 71 | @media screen and (min-width: 576px) { 72 | @content; 73 | } 74 | } 75 | 76 | /* https://medium.com/@ladyleet/adding-box-shadow-z-depth-to-angular-material-2-components-6bd0de303dcb */ 77 | %transition-box-shadown { 78 | transition: box-shadow 0.2; 79 | } 80 | %z-depth-1 { 81 | @extend %transition-box-shadown; 82 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 83 | 0 3px 1px -2px rgba(0, 0, 0, 0.2); 84 | } 85 | 86 | %z-depth-2 { 87 | @extend %transition-box-shadown; 88 | box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 89 | 0 2px 4px -1px rgba(0, 0, 0, 0.3); 90 | } 91 | 92 | %z-depth-3 { 93 | @extend %transition-box-shadown; 94 | box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 95 | 0 3px 5px -1px rgba(0, 0, 0, 0.3); 96 | } 97 | 98 | %z-depth-4 { 99 | @extend %transition-box-shadown; 100 | box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 101 | 0 5px 5px -3px rgba(0, 0, 0, 0.3); 102 | } 103 | 104 | %z-depth-5 { 105 | @extend %transition-box-shadown; 106 | box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 107 | 0 8px 10px -5px rgba(0, 0, 0, 0.3); 108 | } 109 | ``` 110 | 111 | Nuxt works with [layouts](https://nuxtjs.org/guide/views#layouts). For the moment, 112 | let's only use the default layout, namely: _layouts/default.vue_: 113 | 114 | ```vue 115 | 122 | 123 | 144 | ``` 145 | 146 | Thanks to our previous SCSS configuration, `$colorBgStart` and `$colorBgEnd` here 147 | resolve to appropriate values. 148 | 149 | > Don't forget `lang="scss"`! 150 | 151 | Similarly, let's have our `h2` colored. Our _components/polls/PollList.vue_ 152 | can be updated as follows: 153 | 154 | ```vue 155 | 179 | 180 | 183 | 184 | 199 | ``` 200 | 201 | > I used `scoped` because I want to apply this style only in this components. 202 | > [Vue _Scoped CSS_ documentation](https://vue-loader.vuejs.org/guide/scoped-css.html) 203 | > can be refered for more information. 204 | 205 | I have modified the vote section in the template. 206 | 207 | Let's keep going and update our _components/polls/PollDetail.vue_. The style is 208 | not relevant to this tutorial so the code below is only an example. Interesting 209 | SCSS points may be: 210 | 211 | - [SCSS mixins](https://sass-lang.com/guide) 212 | - [SCSS inheriance](https://sass-lang.com/guide) 213 | 214 | ```vue 215 | 242 | 243 | 246 | 247 | 315 | ``` 316 | 317 | > I am using a [_BEM-like_](http://getbem.com/introduction/) naming convention 318 | > for my CSS classes. Feel free to use your own convention. 319 | 320 | With all the examples above, my poll page looks like this: 321 | 322 | ![Polls page](screenshots/05.01_polls_page.png) 323 | 324 | After voting, my vote appears: 325 | 326 | ![Voted](screenshots/05.02_polls_page_voted.png) 327 | 328 | ### Filters 329 | 330 | While being a Vue.js feature, I add filters here because it is somehow 331 | related to appearances. 332 | 333 | Votes list displays the ID of voted choice but it would be better to have 334 | the choice name. This is where a [Filter](https://vuejs.org/v2/guide/filters.html) 335 | could help us. 336 | 337 | The idea would be, in _components/polls/PollList.vue_: 338 | 339 | - create a computed property `choices` to have a flat list of choices 340 | - loop through `choices` and return `choice.text` 341 | 342 | Filters do not have their own decorator (see [Github issue](https://github.com/kaorun343/vue-property-decorator/issues/98#issuecomment-390161771)) 343 | and must be defined in `@Component({})`. Computed properties, via 344 | `this.choices` are not available in `@Component({})`. The workaround 345 | is then to pass `this.choices` as an argument of the filter: 346 | 347 | ```ts 348 | @Component({ 349 | // unchanged 350 | filters: { 351 | choiceName: (value: number, choices: Choice[]): string => { 352 | const choice = choices.find(choice => choice.id === value); 353 | return choice !== undefined ? choice.text : 'Error no choice found'; 354 | } 355 | } 356 | }) 357 | export default class PollList extends Vue { 358 | // unchaged 359 | 360 | // Computed properties are defined as getters 361 | public get choices(): Choice[] { 362 | return this.polls 363 | .map(poll => poll.choices) 364 | .reduce((p1, p2) => p1.concat(p2), []); 365 | } 366 | } 367 | ``` 368 | 369 | We can now use the `choiceName` filter with the choices list provided by 370 | the `choices()` computed properties: 371 | 372 | ```html 373 | {{ vote.choiceId | choiceName(choices) }} 374 | ``` 375 | 376 | Now, votes display choice name: 377 | 378 | ![Voted](screenshots/05.03_votes_with_choices_name.png) 379 | -------------------------------------------------------------------------------- /docs/06.test.md: -------------------------------------------------------------------------------- 1 | 2 | ## Testing 3 | 4 | > I consider unit testing as part of the code itself. Following Angular 5 | > folder structure, I like having my `.spec.ts` along with the tested 6 | > files. May you prefer to have a `tests/` folder at the root folder 7 | > or a `__tests__/` at each folder level, feel free to adapt this tutorial 8 | > to your taste. 9 | > 10 | > As for the mocks, I follow Jest convention by having a `__mocks__/` folder 11 | > at each folder level 12 | 13 | ### Adding and configuring Jest 14 | 15 | Following dependencies will be used: 16 | 17 | - [Jest](https://jestjs.io/) 18 | 19 | Our test runner. As we are using TypeScript, [`@types/jest`](https://www.npmjs.com/package/@types/jest) 20 | is also added 21 | 22 | - [vue-jest](https://github.com/vuejs/vue-jest) 23 | 24 | Jest transformer for our vue components 25 | 26 | - [vue-test-utils](https://vue-test-utils.vuejs.org/) 27 | 28 | Vue official unit testing library. Equivalent of Enzyme for React 29 | 30 | - [ts-jest](https://github.com/kulshekhar/ts-jest) 31 | 32 | TypeScript preprocessor for Jest 33 | 34 | - [babel-core](https://github.com/babel/babel-bridge) 35 | 36 | Required for `vue-jest` ([StackOverflow link](https://stackoverflow.com/a/54689793/4906586)) 37 | 38 | ```sh 39 | yarn add --dev jest @types/jest vue-jest @vue/test-utils ts-jest babel-core@^7.0.0-bridge.0 40 | ``` 41 | 42 | Add Jest types in _tsconfig.json_: 43 | 44 | ```json 45 | { 46 | "compilerOptions": { 47 | "types": ["@types/node", "@nuxt/vue-app", "@types/jest"] 48 | } 49 | } 50 | ``` 51 | 52 | Add a `test` script in _package.json_: 53 | 54 | ```json 55 | { 56 | "scripts": { 57 | "dev": "nuxt-ts", 58 | "build": "nuxt-ts build", 59 | "start": "nuxt-ts start", 60 | "generate": "nuxt-ts generate", 61 | "test": "jest" 62 | } 63 | } 64 | ``` 65 | 66 | Add a Jest configuration file, _jest.config.js_: 67 | 68 | ```js 69 | module.exports = { 70 | moduleNameMapper: { 71 | '^@/(.*)$': '/$1', 72 | // this line is optional and the tilde shortcut 73 | // will not be used in this tutorial 74 | '^~/(.*)$': '/$1' 75 | }, 76 | transform: { 77 | '^.+\\.ts?$': 'ts-jest', 78 | '.*\\.(vue)$': 'vue-jest' 79 | }, 80 | moduleFileExtensions: ['ts', 'js', 'vue', 'json'], 81 | 82 | collectCoverageFrom: [ 83 | 'components/**/*.vue', 84 | 'layouts/**/*.vue', 85 | 'pages/**/*.vue', 86 | 'lib/**/*.ts', 87 | 'plugins/**/*.ts', 88 | 'store/**/*.ts' 89 | ] 90 | }; 91 | ``` 92 | 93 | For more detail: [Jest configuration documentation](https://jestjs.io/docs/en/configuration) 94 | 95 | > [`collectCoverageFrom`](https://jestjs.io/docs/en/cli#collectcoveragefrom-glob): we will 96 | > be using Jest coverage (which uses Istanbul behind the hoods). We are listing all folders 97 | > than have to be scanned for coverage so that files which do not have a corresponding 98 | > `.spec.ts` file are flagged as _non tested_ instead of being skipped. 99 | 100 | Finally, add a _ts-shim.d.ts_ at root level: 101 | 102 | ```ts 103 | declare module '*.vue' { 104 | import Vue from 'vue'; 105 | export default Vue; 106 | } 107 | ``` 108 | 109 | If this shim were missing, running tests against Vue components will trigger an 110 | error: `error TS2307: Cannot find module '{path to component}'.`. Kudos to 111 | [Beetaa](https://github.com/beetaa/) for 112 | [the solution](https://github.com/vuejs/vue/issues/5298#issuecomment-453343514) 113 | 114 | > Note: _lib/polls/_ testing is pure TypeScript testing and will be skipped in 115 | > in this tutorial. For the sake of completion, feel free to check: 116 | > 117 | > - [_/lib/polls/api.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/lib/polls/api.spec.ts) 118 | > - [_/lib/polls/models.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/lib/polls/models.spec.ts) 119 | 120 | You are now ready to run tests with 121 | 122 | ``` 123 | yarn test 124 | ``` 125 | 126 | May you are interested into test coverage, please run 127 | 128 | ``` 129 | yarn test --coverage 130 | ``` 131 | 132 | and then check _coverage/lcov-report/index.html_. 133 | 134 | ### Vuex testing 135 | 136 | As vuex files are plain TypeScript files, it is easier to start there. Let's 137 | create the following files: 138 | 139 | - _/store/polls/state.spec.ts_ 140 | - _/store/polls/mutations.spec.ts_ 141 | - _/store/polls/actions.spec.ts_ 142 | 143 | Getter is empty so there is nothing to test. 144 | 145 | I mocked a state in [_store/polls/\_\_mocks\_\_/state.mock.ts_](https://github.com/Al-un/nuxt-ts/blob/master/store/polls/__mocks__/state.mock.ts). 146 | 147 | State and mutations testing are pure TypeScript testing and 148 | present not much of interest. I then just add the link to the 149 | test files: 150 | 151 | - [_/store/polls/state.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/store/polls/state.spec.ts) 152 | - [_/store/polls/mutations.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/store/polls/mutations.spec.ts) 153 | 154 | Actions testing involve API calls. Right, we are not really calling 155 | any back-end but let's imagine we were. Testing should avoid any 156 | network call. To fix this, Jest has the [manual mock](https://jestjs.io/docs/en/manual-mocks.html) 157 | feature. 158 | 159 | Following Jest convention, my API mock is located at 160 | [_lib/polls/\_\_mocks\_\_/api.ts_](https://github.com/Al-un/nuxt-ts/blob/master/lib/polls/__mocks__/api.ts): 161 | 162 | ```ts 163 | import { Poll, Vote } from '../models'; 164 | 165 | export const DUMMY_POLLS: Poll[] = [ 166 | // ... 167 | ]; 168 | 169 | export const DUMMY_VOTES: Vote[] = [ 170 | // ... 171 | ]; 172 | 173 | export const loadPolls = jest.fn().mockImplementation( 174 | (): Promise => { 175 | return new Promise(resolve => resolve(DUMMY_POLLS)); 176 | } 177 | ); 178 | ``` 179 | 180 | Two points have to be noticed: 181 | 182 | - The `loadPolls` function is exactly identical to the real one 183 | - `loadPolls` definition is not a function but a `jest.fn()` mock 184 | 185 | With such mock, we just have to call 186 | 187 | ```ts 188 | // Beware of the star import !! 189 | import * as api from '@/lib/polls/api'; 190 | 191 | jest.mock('@/lib/polls/api.ts'); 192 | ``` 193 | 194 | at the top of our action testing file. There is no much to add besides 195 | the mock point so here is the link of the testing file: 196 | 197 | - [_/store/polls/actions.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/store/polls/actions.spec.ts) 198 | 199 | At this stage, API is only returning dummy values and votes are not 200 | processed by any back-end (e.g. vote ID has to be generated by a 201 | back-end). API structure will evolve, when Axios will enter the scene, 202 | and tests will have to be updated accordingly 203 | 204 | ### Components testing 205 | 206 | As Vue components testing relies on Vue Test Utils, please refer 207 | to the [Vue Test Utils documentation](https://vue-test-utils.vuejs.org/) 208 | if necessary 209 | 210 | `PollDetail` and `PollList` are tested by: 211 | 212 | - [_/components/polls/PollDetail.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/components/polls/PollDetail.spec.ts) 213 | - [_/components/polls/PollList.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/components/polls/PollList.spec.ts) 214 | 215 | As Nuxt pages are Vue components, polls page is tested by: 216 | 217 | - [_/pages/polls.spec.ts_](https://github.com/Al-un/nuxt-ts/blob/master/pages/polls.spec.ts) 218 | 219 | #### Shallow mounting 220 | 221 | When [shallow mounting](https://vue-test-utils.vuejs.org/guides/#common-tips) 222 | components, props and methods can be mocked: 223 | 224 | ```ts 225 | const options = {}; 226 | const wrapper: Wrapper = shallowMount(PollList, options); 227 | ``` 228 | 229 | Interesting options are: 230 | 231 | - `propsData` to mock props (from _PollList.spec.ts_): 232 | 233 | ```ts 234 | import { DUMMY_POLLS, DUMMY_VOTES } from '@/lib/polls/__mocks__/api'; 235 | 236 | const poll: Poll = DUMMY_POLLS[0]; 237 | const wrapper: Wrapper = shallowMount(PollList, { 238 | propsData: { polls: DUMMY_POLLS, votes: DUMMY_VOTES } 239 | }); 240 | ``` 241 | 242 | - `methods` to override component methods definition. This could help for 243 | mocking or simply using another implementation (from _PollDetail.spec.ts_): 244 | 245 | ```ts 246 | import { DUMMY_POLLS } from '@/lib/polls/__mocks__/api'; 247 | 248 | const poll: Poll = DUMMY_POLLS[0]; 249 | const mockedVote: jest.Mock = jest.fn(); 250 | 251 | const wrapper: Wrapper = shallowMount(PollDetail, { 252 | propsData: { poll }, 253 | methods: { vote: mockedVote } 254 | }); 255 | ``` 256 | 257 | Please check [Vue Test Utils mounting options](https://vue-test-utils.vuejs.org/api/options.html) 258 | for more details 259 | 260 | #### Mocking Vuex store 261 | 262 | Because _Polls_ page uses mapped state, Vuex Store has to be mocked as well. 263 | Vue Test Utils has some [documentation dedicated to testing 264 | Vuex in components](https://vue-test-utils.vuejs.org/guides/#testing-vuex-in-components) 265 | 266 | ```ts 267 | import { shallowMount, Wrapper, createLocalVue } from '@vue/test-utils'; 268 | import Vuex, { Store } from 'vuex'; 269 | 270 | import Polls from './polls.vue'; 271 | import { RootState } from '@/store/types'; 272 | import { mock1 } from '@/store/polls/__mocks__/state.mock'; 273 | 274 | // Vue config 275 | const localVue = createLocalVue(); 276 | localVue.use(Vuex); 277 | 278 | // Vuex config 279 | let store: Store; 280 | 281 | // Component config 282 | let wrapper: Wrapper; 283 | const loadPolls: jest.Mock = jest.fn(); 284 | 285 | store = new Vuex.Store({ 286 | modules: { 287 | polls: { 288 | namespaced: true, 289 | actions: { load: loadPolls }, 290 | state: mock1() 291 | } 292 | } 293 | }); 294 | 295 | const wrapper: Wrapper = shallowMount(Polls, { localVue, store }); 296 | ``` 297 | 298 | > Don't forget to have `namespaced: true`. All store within _store/{some folder}_ 299 | > are namespaced by default in Nuxt 300 | 301 | Only the required action and state are mocked. There is no need to mock 302 | mutations and other actions as they are not used by _Polls_ page. 303 | 304 | ### Coverage 305 | 306 | To generate coverage report, run 307 | 308 | ```sh 309 | yarn test --coverage 310 | ``` 311 | 312 | and you will have a nice output: 313 | 314 | ![console coverage](screenshots/06.01_coverage.png) 315 | 316 | Open _coverage/lcov-report/index.html_ to have a detail HTML report: 317 | 318 | ![HTML coverage](screenshots/06.02_html_report.png) 319 | -------------------------------------------------------------------------------- /docs/07.deploy.md: -------------------------------------------------------------------------------- 1 | ## Deployment 2 | 3 | ### Universal vs Pre-rendered vs SPA 4 | 5 | Nuxt offers three different ways to "build" our application 6 | (more on [Nuxt docs](https://nuxtjs.org/guide/commands#production-deployment)): 7 | 8 | - Universal, serving our app as an Node application 9 | - Pre-rendered, serving our app as a static website but with all routes pre-rendered 10 | - SPA, serving our app as a SPA website 11 | 12 | I will not use SPA here as pre-rendered does all the hard work to generate all routes 13 | for us, which is one of SPA weakness. 14 | 15 | To deploy an universal build: 16 | 17 | ```sh 18 | yarn build 19 | yarn start 20 | ``` 21 | 22 | To deploy a pre-rendered build: 23 | 24 | ```sh 25 | yarn generate 26 | # deploy /dist/ folder 27 | ``` 28 | 29 | You will find relevant documentation on [NuxtJS FAQ](https://nuxtjs.org/faq/). I have tested 30 | some deployments for my personal usage so I will cover these in the following paragraphs. 31 | 32 | ### Surge 33 | 34 | [Surge](https://surge.sh/) is a very easy to use static website hosting accessible via 35 | CLI only, which is quite cool for continuous deployment. 36 | 37 | Install Surge: 38 | 39 | ```sh 40 | # Install globally 41 | npm install --global surge 42 | # Add locally as a devDependency 43 | yarn add --dev surge 44 | ``` 45 | 46 | > For some reasons, I could not install Surge globally with yarn 47 | 48 | Then simply build and deploy a static website: 49 | 50 | ```sh 51 | # Generate 52 | yarn generate 53 | # Deploy 54 | surge --project ./dist/ 55 | ``` 56 | 57 | Output: 58 | 59 | ```sh 60 | 61 | Running as xxxx@xxxx.xxx (Student) 62 | 63 | project: ./dist/ 64 | domain: lively-quiver.surge.sh 65 | upload: [====================] 100% eta: 0.0s (12 files, 256921 bytes) 66 | CDN: [====================] 100% 67 | IP: 45.55.110.124 68 | 69 | Success! - Published to lively-quiver.surge.sh 70 | ``` 71 | 72 | That's it! 73 | 74 | For more details, check: 75 | 76 | - [Nuxtjs doc](https://nuxtjs.org/faq/surge-deployment) 77 | - [Surge docs](https://surge.sh/help/) 78 | 79 | ### AWS S3 80 | 81 | S3 bucket offers the possibility to host a static website ([AWS docs](https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html)): 82 | 83 | - Go to your AWS management console 84 | - Create a S3 bucket and open it 85 | - Go to properties and enable _Static website hosting_ 86 | - Make your bucket public by adding this _permissions > bucket policy_: 87 | 88 | ```json 89 | { 90 | "Version": "2012-10-17", 91 | "Statement": [ 92 | { 93 | "Sid": "AddPerm", 94 | "Effect": "Allow", 95 | "Principal": "*", 96 | "Action": "s3:GetObject", 97 | "Resource": "arn:aws:s3:::nuxt-ts/*" 98 | } 99 | ] 100 | } 101 | ``` 102 | 103 | - Upload your _dist/_ content into your bucket 104 | 105 | For those who prefer to use AWS CLI: 106 | 107 | ```sh 108 | # Create your bucket. Replace "nuxt-ts" with your bucket name 109 | aws s3 mb s3://nuxt-ts 110 | # Enable static website hosting with default values 111 | aws s3 website s3://nuxt-ts --index-document index.html --error-document error.html 112 | # Create a bucket policy files 113 | touch s3_policy.json 114 | # Edit accordingly. Use VS Code! 115 | vim s3_policy.json 116 | # Apply policy file to bucket 117 | aws s3api put-bucket-policy --bucket nuxt-ts --policy file://s3_policy.json 118 | # Generate and upload dist/ content 119 | yarn generate 120 | aws s3 cp dist/ s3://nuxt-ts --recursive 121 | # Tadaa!! 122 | firefox nuxt-ts.s3-website.eu-west-3.amazonaws.com 123 | ``` 124 | 125 | Docs references: 126 | 127 | - [Nuxtjs doc](https://nuxtjs.org/faq/deployment-aws-s3-cloudfront) 128 | - [`aws s3 mb`](https://docs.aws.amazon.com/cli/latest/reference/s3/mb.html) 129 | - [`aws s3 website`](https://docs.aws.amazon.com/cli/latest/reference/s3/website.html) 130 | - [`aws s3api put-bucket-policy`](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-policy.html) 131 | - [`aws s3 cp`](https://docs.aws.amazon.com/cli/latest/reference/s3/cp.html) 132 | 133 | ### Heroku 134 | 135 | Configure first your _package.json_. By convention, Nuxt uses `build` and `start` 136 | scripts which is properly triggered by Heroku after a `git push`. For the sake of 137 | the tutorial, let's use Heroku specific hooks in _package.json_: 138 | 139 | ```json 140 | "scripts": { 141 | "build": "nuxt build", // this will not run 142 | "heroku-postbuild": "nuxt build", // this will run instead 143 | "start": "nuxt start" // this will run after a successful build 144 | } 145 | ``` 146 | 147 | ```sh 148 | # == no app == : create app 149 | heroku create nuxt-ts 150 | # == app exists ==: add remote 151 | heroku git:remote --app nuxt-ts 152 | # check your remotes 153 | git remote -v 154 | # configure your app 155 | heroku config:set NPM_CONFIG_PRODUCTION=false --app nuxt-ts 156 | heroku config:set HOST=0.0.0.0 --app nuxt-ts 157 | heroku config:set NODE_ENV=production --app nuxt-ts 158 | # simply push your desired branch to Heroku 159 | git push heroku master 160 | ``` 161 | 162 | Docs references: 163 | 164 | - [Nuxtjs doc](https://nuxtjs.org/faq/heroku-deployment) 165 | - [Using NodeJS with Heroku](https://devcenter.heroku.com/articles/getting-started-with-nodejs#deploy-the-app) 166 | - [Customize build process](https://devcenter.heroku.com/articles/nodejs-support#customizing-the-build-process) 167 | - [Using Heroku CLI to deploy](https://devcenter.heroku.com/articles/git#creating-a-heroku-remote) 168 | 169 | ### Travis CI 170 | 171 | Thanks to providers, including those deployment into Travis is easy. 172 | Please check [_.travis.yml_](https://github.com/Al-un/learn-nuxt-ts/blob/master/.travis.yml) for 173 | the complete configuration file. 174 | 175 | When encrypting values, I prefer to copy paste the secured values into 176 | my _.travis.yml_ rather than having the CLI doing it for me because 177 | it sometimes breaks my formatting. 178 | 179 | #### Surge 180 | 181 | Configure Surge: 182 | 183 | ```sh 184 | # Generate a token 185 | surge token 186 | # Encrypt SURGE_LOGIN 187 | travis encrypt "SURGE_LOGIN={your email}" --com -r {your Github user}/{your repo} 188 | # Encrypt SURGE_TOKEN 189 | travis encrypt "SURGE_TOKEN={your token}" --com -r {your Github user}/{your repo} 190 | ``` 191 | 192 | Then add a Surge provider to your _.travis.yml_: 193 | 194 | ```yaml 195 | deploy: 196 | - provider: surge 197 | project: ./dist 198 | domain: nuxt-ts.surge.sh # optional 199 | skip-cleanup: true 200 | ``` 201 | 202 | Please check [Travis docs](https://docs.travis-ci.com/user/deployment/surge/) for more. 203 | 204 | #### AWS S3 205 | 206 | Similary to Surge, encrypt your _AccessKeyId_ and your _SecretAccessKey_. Then add the 207 | S3 provider: 208 | 209 | ```yaml 210 | deploy: 211 | - provider: s3 212 | access_key_id: 213 | secure: '...' 214 | secret_access_key: 215 | secure: '...' 216 | bucket: nuxt-ts 217 | region: eu-west-3 218 | skip-cleanup: true 219 | local_dir: dist 220 | ``` 221 | 222 | Don't forget to specify the bucket region and the folder (`local_dir`) to upload 223 | 224 | Please check [Travis docs](https://docs.travis-ci.com/user/deployment/s3/) for more. 225 | 226 | #### Heroku 227 | 228 | Encrypt your Heroku API key and add the Heroku provider: 229 | 230 | ```yaml 231 | deploy: 232 | - provider: heroku 233 | api_key: secure: '...' 234 | app: nuxt-ts 235 | on: 236 | branch: master 237 | node_js: node 238 | ``` 239 | 240 | Please check [Travis docs](https://docs.travis-ci.com/user/deployment/heroku/) for more. 241 | 242 | ### Circle CI 243 | 244 | When Travis is using _providers_, Circle CI uses _Orbs_. I will not 245 | focus on Circle CI orbs in this tutorial. Please check 246 | [_.circleci/config.yml_](https://github.com/Al-un/learn-nuxt-ts/blob/master/.circleci/config.yml) 247 | for the complete configuration 248 | 249 | #### Surge 250 | 251 | As mentioned earlier, you can either install Surge globally or as a _devDependency_. 252 | Having `SURGE_LOGIN` and `SURGE_TOKEN` environment variables defined, it is as 253 | simple as executing the `surge` command: 254 | 255 | ```yaml 256 | jobs: 257 | deploy-surge: 258 | docker: 259 | - image: circleci/node:11-browsers-legacy 260 | working_directory: ~/repo 261 | steps: 262 | - checkout 263 | - run: npm ci 264 | - run: npm run generate 265 | - run: ./node_modules/.bin/surge --project ~/repo/dist --domain nuxt-ts.surge.sh 266 | ``` 267 | 268 | Reference: [Surge doc](https://surge.sh/help/integrating-with-circleci) 269 | 270 | #### AWS S3 271 | 272 | Circle CI offers a [S3 orb](https://circleci.com/orbs/registry/orb/circleci/aws-s3) 273 | to ease S3 deployment, especially regarding AWS CLI configuration. Three environments 274 | variables are needed: 275 | 276 | - `AWS_ACCESS_KEY_ID` 277 | - `AWS_SECRET_ACCESS_KEY` 278 | - `AWS_REGION` 279 | 280 | Then, add the orb and execute it in a job: 281 | 282 | ```yaml 283 | orbs: 284 | aws-s3: circleci/aws-s3@1.0.4 285 | 286 | jobs: 287 | deploy-s3: 288 | docker: 289 | - image: circleci/python:2.7 290 | steps: 291 | - checkout 292 | - run: npm ci 293 | - run: npm run generate 294 | - aws-s3/copy: 295 | from: ~/repo/dist 296 | to: 's3://nuxt-ts' 297 | arguments: '--recursive' 298 | ``` 299 | 300 | Reference: [Circle CI doc](https://circleci.com/docs/2.0/deployment-integrations/#aws) 301 | 302 | #### Heroku 303 | 304 | Configure first required environment variables: 305 | 306 | - `HEROKU_API_KEY` 307 | - `HEROKU_APP_NAME` 308 | 309 | Once done, deploying to Heroku is a simple `git push`: 310 | 311 | ```yaml 312 | jobs: 313 | deploy-heroku: 314 | docker: 315 | - image: buildpack-deps:trusty 316 | steps: 317 | - checkout 318 | - run: 319 | name: Deploy Master to Heroku 320 | command: | 321 | git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master 322 | ``` 323 | 324 | Reference: [Circle CI doc](https://circleci.com/docs/2.0/deployment-integrations/#heroku) 325 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt-TS: Nuxt application powered by TypeScript 2 | 3 | Half tutorial, half exploration, I want to check out how far I can get 4 | with Nuxt+TypeScript in a full application from scratch. 5 | 6 | **Table of contents** 7 | 8 | - [01. Initialise Project](01.init.md) 9 | - [02. Switch to TypeScript](02.typescript.md) 10 | - [02.1 Adding nuxt-ts](02.typescript.md#adding-nuxt-ts) 11 | - [02.2 Update Nuxt configuration](02.typescript.md#update-configuration) 12 | - [02.3 Update existing code](02.typescript.md#update-existing-code) 13 | - [03. Code control: formatter and linter](03.codecontrol.md) 14 | - [03.1 Prettier](03.codecontrol.md#prettier) 15 | - [03.2 ESLint](03.codecontrol.md#eslint) 16 | - [04. Polls: components & vuex](04.polls.md) 17 | - [04.1 Models](04.polls.md#polls-models) 18 | - [04.2 Page](04.polls.md#polls-page) 19 | - [04.3 Components](04.polls.md#polls-components) 20 | - [04.4 Store](04.polls.md#polls-store) 21 | - [05. Style](05.style.md) 22 | - [05.1 SCSS](05.style.md#scss) 23 | - [05.2 styling](05.style.md#styling) 24 | - [05.3 filters](05.style.md#filters) 25 | - [06. Testing](06.test.md) 26 | - [06.1 Jest](06.test.md#adding-and-configuring-jest) 27 | - [06.2 Testing Vuex](06.test.md#vuex-testing) 28 | - [06.3 Testing Components](06.test.md#components-testing) 29 | - [06.3 Coverage](06.test.md#coverage) 30 | - [07. Deployment](07.deploy.md) 31 | - [07.1 Universal vs Pre-rendered vs SPA](07.deploy.md#universal-vs-pre-rendered-vs-spa) 32 | - [07.2 Surge](07.deploy.md#surge) 33 | - [07.3 AWS S3](07.deploy.md#aws-s3) 34 | - [07.4 Heroku](07.deploy.md#heroku) 35 | - [07.5 Travis CI](07.deploy.md#travis-ci) 36 | - [07.6 Circle CI](07.deploy.md#circle-ci) 37 | 38 | **Nuxt** 39 | 40 | - 13-Apr-2020: switch Nuxt official TypeScript support: https://typescript.nuxtjs.org/. Migration method is described here: https://typescript.nuxtjs.org/migration.html. 41 | 42 |
43 | 44 | Update steps 45 | 46 | Update dependencies 47 | ```sh 48 | # Bump node-sass 49 | rm -Rf node_modules 50 | rm package-lock.json 51 | npm uninstall node-sass 52 | npm install node-sass 53 | 54 | # Remove nuxt dependencies 55 | npm uninstall @nuxt/typescript nuxt nuxt-property-decorator 56 | # Ensure that we have at least nuxt 2.9.x 57 | npm install nuxt 58 | # Bump property decorator 59 | npm install nuxt-property-decorator 60 | # Upgrade 61 | npm install --save-dev @nuxt/typescript-build 62 | # Add TypeScript runtime configuration for nuxt-property-decorator 63 | # https://github.com/nuxt-community/nuxt-property-decorator/issues/62#issuecomment-577601678 64 | npm install @nuxt/typescript-runtime 65 | 66 | # Update linting 67 | npm uninstall @typescript-eslint/eslint-plugin 68 | npm install --save-dev @nuxtjs/eslint-module 69 | npm install --save-dev @nuxtjs/eslint-config-typescript 70 | npm uninstall eslint-plugin-vue 71 | npm install --save-dev eslint-plugin-nuxt eslint-plugin-prettier 72 | ``` 73 | 74 | Update `nuxt.config.js`: 75 | 76 | ```diff 77 | - import pkg from './package.json'; 78 | 79 | - module.exports = { 80 | + export default { 81 | 82 | - title: pkg.name, 83 | + titleTemplate: '%s - ' + process.env.npm_package_name, 84 | + title: process.env.npm_package_name || '', 85 | 86 | - { hid: 'description', name: 'description', content: pkg.description } 87 | + { 88 | + hid: 'description', 89 | + name: 'description', 90 | + content: process.env.npm_package_description || '' 91 | + } 92 | 93 | + /* 94 | + ** Nuxt.js dev-modules 95 | + */ 96 | + buildModules: [ 97 | + // Doc: https://github.com/nuxt-community/eslint-module 98 | + '@nuxtjs/eslint-module', 99 | + '@nuxt/typescript-build' 100 | + ], 101 | ``` 102 | 103 | Update `tsconfig.json` 104 | 105 | ```diff 106 | - "@nuxt/vue-app", 107 | + "@nuxt/types", 108 | ``` 109 | 110 | Update `.eslintrc.js`: 111 | 112 | ```diff 113 | - // https://eslint.org/docs/user-guide/configuring#specifying-parser 114 | - parser: 'vue-eslint-parser', 115 | - // https://vuejs.github.io/eslint-plugin-vue/user-guide/#faq 116 | - parserOptions: { 117 | - parser: '@typescript-eslint/parser', 118 | - ecmaVersion: 2017, 119 | - sourceType: 'module', 120 | - project: './tsconfig.json' 121 | - }, 122 | - 123 | - // https://eslint.org/docs/user-guide/configuring#extending-configuration-files 124 | - // order matters: from least important to most important in terms of overriding 125 | - // Prettier + Vue: https://medium.com/@gogl.alex/how-to-properly-set-up-eslint-with-prettier-for-vue-or-nuxt-in-vscode-e42532099a9c 126 | extends: [ 127 | - 'eslint:recommended', 128 | - 'plugin:@typescript-eslint/recommended', 129 | - 'plugin:vue/recommended', 130 | - 'prettier', 131 | - 'prettier/vue', 132 | - 'prettier/@typescript-eslint' 133 | + '@nuxtjs', 134 | + '@nuxtjs/eslint-config-typescript', 135 | + 'prettier', 136 | + 'prettier/vue', 137 | + 'plugin:prettier/recommended', 138 | + 'plugin:nuxt/recommended' 139 | ], 140 | - 141 | - // https://eslint.org/docs/user-guide/configuring#configuring-plugins 142 | - plugins: ['vue', '@typescript-eslint'], 143 | 144 | + plugins: ['prettier'], 145 | ``` 146 | 147 | Update `package.json` for runtime 148 | 149 | ```diff 150 | "scripts": { 151 | - "dev": "nuxt", 152 | + "dev": "nuxt-ts", 153 | - "build": "nuxt build", 154 | + "build": "nuxt-ts build", 155 | - "heroku-postbuild": "nuxt build", 156 | + "heroku-postbuild": "nuxt-ts build", 157 | "test": "jest", 158 | "lint": "eslint . --ext .vue,.ts,.js", 159 | - "start": "nuxt start", 160 | + "start": "nuxt-ts start", 161 | - "generate": "nuxt generate" 162 | + "generate": "nuxt-ts generate" 163 | }, 164 | ``` 165 |
166 | 167 | - 22-Mar-2019: [Nuxt 2.5.0](https://github.com/nuxt/nuxt.js/releases/tag/v2.5.0) 168 | 169 | `nuxt-ts` is not needed anymore. Nuxt Typescript support is done by adding 170 | `@nuxt/typescript` 171 | 172 |
173 | 174 | Update from Nuxt 2.4.0 is done with: 175 | 176 | ```sh 177 | yarn remove nuxt-ts 178 | yarn add nuxt @nuxt/typescript 179 | rm -Rf node_modules/ 180 | rm yarn.lock 181 | yarn 182 | ``` 183 |
184 | 185 | As-of 24-Mar-2019, Nuxt version is 2.5.1. 186 | 187 | Side-effect is that as-of Nuxt 2.5.1, Nuxt does not support `"extends": "@nuxt/typescript"` 188 | and _tsconfig.json_ is initialized by Nuxt: 189 | 190 | - `"resolveJsonModule": true` has to be added 191 | - `"types": ["@types/node", "@nuxt/vue-app", "@types/jest"]` has `@types/jest` added back 192 | 193 | - 28-Jan-2019: [Nuxt 2.4.0](https://github.com/nuxt/nuxt.js/releases/tag/v2.4.0) 194 | 195 | [Nuxt 2.4.0 release (Jan-2019)](https://dev.to/nuxt/nuxtjs-v240-is-out-typescript-smart-prefetching-and-more-18d) 196 | has pushed one step forward TypeScript integration into Nuxt thanks to `nuxt-ts` 197 | 198 | **Kudos to Nuxt team**. 199 | 200 | This tutorial has undergone a complete refactoring on March 2019. Old version 201 | is archived at the [`archive/2019-03-09_refactoring` branch](https://github.com/Al-un/nuxt-ts/tree/archive/2019-03-09_refactoring) 202 | -------------------------------------------------------------------------------- /docs/screenshots/01.01_create_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/01.01_create_project.png -------------------------------------------------------------------------------- /docs/screenshots/01.02_project_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/01.02_project_structure.png -------------------------------------------------------------------------------- /docs/screenshots/01.03_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/01.03_run.png -------------------------------------------------------------------------------- /docs/screenshots/01.04_project_is_working.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/01.04_project_is_working.png -------------------------------------------------------------------------------- /docs/screenshots/04.01_polls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/04.01_polls.png -------------------------------------------------------------------------------- /docs/screenshots/04.02_choice_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/04.02_choice_select.png -------------------------------------------------------------------------------- /docs/screenshots/04.03_vote_nuxt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/04.03_vote_nuxt.png -------------------------------------------------------------------------------- /docs/screenshots/04.04_voted_nuxt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/04.04_voted_nuxt.png -------------------------------------------------------------------------------- /docs/screenshots/05.01_polls_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/05.01_polls_page.png -------------------------------------------------------------------------------- /docs/screenshots/05.02_polls_page_voted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/05.02_polls_page_voted.png -------------------------------------------------------------------------------- /docs/screenshots/05.03_votes_with_choices_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/05.03_votes_with_choices_name.png -------------------------------------------------------------------------------- /docs/screenshots/06.01_coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/06.01_coverage.png -------------------------------------------------------------------------------- /docs/screenshots/06.02_html_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/docs/screenshots/06.02_html_report.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://jestjs.io/docs/en/tutorial-react-native#modulenamemapper 3 | moduleNameMapper: { 4 | '^@/(.*)$': '/$1', 5 | '^~/(.*)$': '/$1' 6 | }, 7 | // https://jestjs.io/docs/en/configuration#transform-object-string-string 8 | transform: { 9 | '^.+\\.ts?$': 'ts-jest', 10 | '.*\\.(vue)$': 'vue-jest' 11 | }, 12 | // https://jestjs.io/docs/en/configuration#modulefileextensions-array-string 13 | moduleFileExtensions: ['ts', 'js', 'vue', 'json'], 14 | 15 | // https://github.com/facebook/jest/issues/1211#issuecomment-247381553 16 | // https://jestjs.io/docs/en/configuration.html#coveragepathignorepatterns-array-string 17 | collectCoverageFrom: [ 18 | 'components/**/*.vue', 19 | 'layouts/**/*.vue', 20 | 'pages/**/*.vue', 21 | 'lib/**/*.ts', 22 | 'plugins/**/*.ts', 23 | 'store/**/*.ts' 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /lib/polls/__mocks__/api.ts: -------------------------------------------------------------------------------- 1 | import { Poll, Vote } from '../models'; 2 | 3 | /** 4 | * Dummy polls 5 | */ 6 | export const DUMMY_POLLS: Poll[] = [ 7 | { 8 | id: 1, 9 | topic: 'Poll1', 10 | choices: [ 11 | { id: 1, count: 0, pollId: 1, text: 'Choice1' }, 12 | { id: 2, count: 0, pollId: 1, text: 'Choice2' } 13 | ] 14 | }, 15 | { 16 | id: 2, 17 | topic: 'Poll2', 18 | choices: [ 19 | { id: 3, count: 0, pollId: 2, text: 'Choice3' }, 20 | { id: 4, count: 0, pollId: 2, text: 'Choice4' } 21 | ] 22 | } 23 | ]; 24 | 25 | export const DUMMY_VOTES: Vote[] = [ 26 | { id: 1, choiceId: 1 }, 27 | { id: 2, choiceId: 3, comment: 'comment' } 28 | ]; 29 | 30 | /** 31 | * Load polls with associated choices. Votes are not loaded here. 32 | * 33 | * It uses a timeout to simulate server response time 34 | */ 35 | export const loadPolls = jest.fn().mockImplementation( 36 | (): Promise => { 37 | return new Promise(resolve => resolve(DUMMY_POLLS)); 38 | } 39 | ); 40 | -------------------------------------------------------------------------------- /lib/polls/api.spec.ts: -------------------------------------------------------------------------------- 1 | import { loadPolls } from './api'; 2 | 3 | describe('Polls API', () => { 4 | describe('loadPolls', () => { 5 | test('works', async () => { 6 | const polls = await loadPolls(); 7 | expect(polls).toBeInstanceOf(Array); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/polls/api.ts: -------------------------------------------------------------------------------- 1 | import { Poll } from './models'; 2 | 3 | /** 4 | * Dummy polls 5 | */ 6 | const DUMMY_POLLS: Poll[] = [ 7 | { 8 | id: 1, 9 | topic: 'Which framework are you using?', 10 | choices: [ 11 | { id: 1, count: 0, pollId: 1, text: 'NuxtJS' }, 12 | { id: 2, count: 0, pollId: 1, text: 'Plain VueJS' }, 13 | { id: 3, count: 0, pollId: 1, text: 'Angular' }, 14 | { id: 4, count: 0, pollId: 1, text: 'React' } 15 | ] 16 | }, 17 | { 18 | id: 2, 19 | topic: 'What is your OS?', 20 | choices: [ 21 | { id: 5, count: 0, pollId: 2, text: 'Windows' }, 22 | { id: 6, count: 0, pollId: 2, text: 'Linux' }, 23 | { id: 7, count: 0, pollId: 2, text: 'MacOS' } 24 | ] 25 | } 26 | ]; 27 | 28 | /** 29 | * Load polls with associated choices. Votes are not loaded here. 30 | * 31 | * It uses a timeout to simulate server response time 32 | */ 33 | // eslint-disable-next-line require-await 34 | export const loadPolls = async (): Promise => { 35 | return new Promise(resolve => 36 | setTimeout(() => resolve(DUMMY_POLLS), 500) 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/polls/models.spec.ts: -------------------------------------------------------------------------------- 1 | import { Choice, Poll } from './models'; 2 | 3 | const pollId = 39; 4 | const choiceId = 42; 5 | 6 | describe('Choice', () => { 7 | let choice: Choice; 8 | 9 | beforeEach(() => { 10 | choice = new Choice(choiceId, pollId, 'some text'); 11 | }); 12 | 13 | test('initializes with zero count', () => { 14 | expect(choice.count).toBe(0); 15 | }); 16 | }); 17 | 18 | describe('Poll', () => { 19 | let poll: Poll; 20 | 21 | describe('when no choice is provided', () => { 22 | beforeEach(() => { 23 | poll = new Poll(pollId, 'some text'); 24 | }); 25 | 26 | test('poll is initialized with an empty array chocies', () => { 27 | expect(poll.choices).toEqual([]); 28 | }); 29 | }); 30 | 31 | describe('when choices are provided', () => { 32 | let choice: Choice; 33 | 34 | beforeEach(() => { 35 | choice = new Choice(choiceId, pollId, 'some text'); 36 | poll = new Poll(pollId, 'some text', [choice]); 37 | }); 38 | 39 | test('poll is initialized given chocies', () => { 40 | expect(poll.choices).toEqual([choice]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /lib/polls/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A vote for a given choice 3 | */ 4 | export class Vote { 5 | // eslint-disable-next-line no-useless-constructor 6 | public constructor( 7 | public id: number, 8 | public choiceId: number, 9 | public comment?: string 10 | ) {} 11 | } 12 | 13 | /** 14 | * A choice to vote for within a Poll 15 | */ 16 | export class Choice { 17 | public count: number; 18 | 19 | public constructor( 20 | public id: number, 21 | public pollId: number, 22 | public text: string 23 | ) { 24 | this.count = 0; 25 | } 26 | } 27 | 28 | /** 29 | * A topic with which user is offered multiple choices to vote for 30 | */ 31 | export class Poll { 32 | public choices: Choice[]; 33 | 34 | public constructor( 35 | public id: number, 36 | public topic: string, 37 | choices?: Choice[] 38 | ) { 39 | this.choices = choices !== undefined ? choices : []; 40 | } 41 | } 42 | 43 | /** 44 | * An intention of voting a given choice with an optional comment 45 | */ 46 | export interface ChoiceVote { 47 | choiceId: number; 48 | comment?: string; 49 | } 50 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'universal', 3 | 4 | /* 5 | ** Headers of the page 6 | */ 7 | head: { 8 | titleTemplate: '%s - ' + process.env.npm_package_name, 9 | title: process.env.npm_package_name || '', 10 | meta: [ 11 | { charset: 'utf-8' }, 12 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 13 | { 14 | hid: 'description', 15 | name: 'description', 16 | content: process.env.npm_package_description || '' 17 | } 18 | ], 19 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }] 20 | }, 21 | 22 | /* 23 | ** Customize the progress-bar color 24 | */ 25 | loading: { color: '#fff' }, 26 | 27 | /* 28 | ** Global CSS 29 | */ 30 | css: [], 31 | 32 | /* 33 | ** Plugins to load before mounting the App 34 | */ 35 | plugins: [], 36 | /* 37 | ** Nuxt.js dev-modules 38 | */ 39 | buildModules: [ 40 | // Doc: https://github.com/nuxt-community/eslint-module 41 | '@nuxtjs/eslint-module', 42 | '@nuxt/typescript-build' 43 | ], 44 | /* 45 | ** Nuxt.js modules 46 | */ 47 | modules: ['@nuxtjs/style-resources'], 48 | 49 | /* 50 | ** Build configuration 51 | */ 52 | build: { 53 | // /* 54 | // ** You can extend webpack config here 55 | // */ 56 | // extend(config, ctx) { 57 | // } 58 | }, 59 | 60 | /** 61 | * Style resources module configuration 62 | */ 63 | styleResources: { 64 | scss: ['./assets/scss/_variables.scss', './assets/scss/_mixins.scss'] 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-ts", 3 | "version": "1.0.0", 4 | "description": "TypeScript powered Nuxt tutorial", 5 | "author": "Al-un", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt-ts", 9 | "build": "nuxt-ts build", 10 | "heroku-postbuild": "nuxt-ts build", 11 | "test": "jest", 12 | "lint": "eslint . --ext .vue,.ts,.js", 13 | "start": "nuxt-ts start", 14 | "generate": "nuxt-ts generate" 15 | }, 16 | "dependencies": { 17 | "@nuxt/typescript-runtime": "^0.4.5", 18 | "@nuxtjs/style-resources": "^0.1.2", 19 | "cross-env": "^5.2.0", 20 | "nuxt": "^2.12.2", 21 | "nuxt-property-decorator": "^2.5.1", 22 | "sass-loader": "^7.1.0" 23 | }, 24 | "devDependencies": { 25 | "@nuxt/typescript-build": "^0.6.5", 26 | "@nuxtjs/eslint-config-typescript": "^1.0.2", 27 | "@nuxtjs/eslint-module": "^1.1.0", 28 | "@types/jest": "^24.0.11", 29 | "@types/lodash": "^4.14.122", 30 | "@vue/test-utils": "^1.0.0-beta.29", 31 | "babel-core": "^7.0.0-bridge.0", 32 | "eslint": "^5.15.1", 33 | "eslint-config-prettier": "^4.1.0", 34 | "eslint-plugin-nuxt": "^0.5.2", 35 | "eslint-plugin-prettier": "^3.1.3", 36 | "jest": "^24.5.0", 37 | "node-sass": "^4.13.1", 38 | "nodemon": "^1.18.9", 39 | "prettier": "^1.16.4", 40 | "surge": "^0.20.3", 41 | "ts-jest": "^24.0.0", 42 | "vue-jest": "^3.0.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | 36 | 67 | -------------------------------------------------------------------------------- /pages/polls.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount, Wrapper, createLocalVue } from '@vue/test-utils'; 2 | import Vuex, { Store } from 'vuex'; 3 | 4 | import Polls from './polls.vue'; 5 | import PollList from '@/components/polls/PollList.vue'; 6 | import { RootState } from '@/store/types'; 7 | import { mock1 } from '@/store/polls/__mocks__/state.mock'; 8 | 9 | // Vue config 10 | const localVue = createLocalVue(); 11 | localVue.use(Vuex); 12 | 13 | // Vuex config 14 | let store: Store; 15 | 16 | // Component config 17 | let wrapper: Wrapper; 18 | const loadPolls: jest.Mock = jest.fn(); 19 | 20 | describe('PollList', () => { 21 | beforeEach(() => { 22 | loadPolls.mockReset(); 23 | store = new Vuex.Store({ 24 | modules: { 25 | polls: { 26 | namespaced: true, 27 | actions: { load: loadPolls }, 28 | state: mock1() 29 | } 30 | } 31 | }); 32 | 33 | wrapper = shallowMount(Polls, { localVue, store }); 34 | }); 35 | 36 | test('is a Vue component', () => { 37 | expect(wrapper.isVueInstance()).toBeTruthy(); 38 | }); 39 | 40 | test('renders a PollList', () => { 41 | const pollList = wrapper.find(PollList); 42 | expect(pollList.exists()).toBeTruthy(); 43 | }); 44 | 45 | test('call "loadPolls"', () => { 46 | expect(loadPolls).toHaveBeenCalledTimes(1); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /pages/polls.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Al-un/learn-nuxt-ts/9cb2ae45ecca90c0ffeb9902362fec086ff48060/static/favicon.ico -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /store/polls/__mocks__/state.mock.ts: -------------------------------------------------------------------------------- 1 | import { Poll, Vote } from '@/lib/polls/models'; 2 | import { PollsState } from '@/store/polls/types'; 3 | 4 | /** 5 | * Mock1 polls list 6 | */ 7 | export const MOCK1_POLLS: Poll[] = [ 8 | { 9 | id: 1, 10 | topic: 'topic1', 11 | choices: [ 12 | { id: 1, pollId: 1, count: 1, text: 'choice1' }, 13 | { id: 2, pollId: 1, count: 0, text: 'choice2' } 14 | ] 15 | }, 16 | { 17 | id: 2, 18 | topic: 'topic2', 19 | choices: [ 20 | { id: 3, pollId: 2, count: 0, text: 'choice3' }, 21 | { id: 4, pollId: 2, count: 2, text: 'choice4' } 22 | ] 23 | } 24 | ]; 25 | 26 | /** 27 | * Mock1 votes list 28 | */ 29 | export const MOCK1_VOTES: Vote[] = [ 30 | { id: 1, choiceId: 1, comment: 'some comment' }, 31 | { id: 2, choiceId: 4 }, 32 | { id: 2, choiceId: 4 } 33 | ]; 34 | 35 | /** 36 | * Mock1 polls factory 37 | * @see MOCK1_POLLS 38 | */ 39 | export const mock1Polls = (): Poll[] => MOCK1_POLLS; 40 | 41 | /** 42 | * Mock1 votes factory 43 | * @see MOCK1_VOTES 44 | */ 45 | export const mock1Votes = (): Vote[] => MOCK1_VOTES; 46 | 47 | /** 48 | * Mock1 factory 49 | */ 50 | export const mock1 = (): PollsState => ({ 51 | polls: mock1Polls(), 52 | votes: mock1Votes() 53 | }); 54 | -------------------------------------------------------------------------------- /store/polls/actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { mock1 } from './__mocks__/state.mock'; 2 | import { actions } from '@/store/polls/actions'; 3 | import { PollsState, PollActionContext } from '@/store/polls/types'; 4 | import * as api from '@/lib/polls/api'; 5 | import { Vote } from '@/lib/polls/models'; 6 | 7 | let actionCxt: PollActionContext; 8 | let commit: jest.Mock; 9 | let state: PollsState; 10 | 11 | jest.mock('@/lib/polls/api.ts'); 12 | 13 | describe('Polls actions', () => { 14 | beforeEach(() => { 15 | commit = jest.fn(); 16 | state = mock1(); 17 | 18 | actionCxt = { 19 | state, 20 | commit, 21 | dispatch: jest.fn(), 22 | getters: jest.fn(), 23 | rootGetters: jest.fn(), 24 | rootState: {} 25 | }; 26 | }); 27 | 28 | describe('load', () => { 29 | beforeEach(async () => { 30 | await actions.load(actionCxt); 31 | }); 32 | 33 | test('call api.loadPolls', () => { 34 | expect(api.loadPolls).toHaveBeenCalledTimes(1); 35 | }); 36 | 37 | test('commits "setPolls" with polls from api call', async () => { 38 | expect(commit).toHaveBeenCalledTimes(1); 39 | 40 | const commitCall = commit.mock.calls[0]; 41 | const polls = await api.loadPolls(); 42 | expect(commitCall[1]).toEqual(polls); 43 | }); 44 | }); 45 | 46 | describe('vote', () => { 47 | const choiceId = 0; 48 | 49 | test('commits "vote"', () => { 50 | actions.vote(actionCxt, { choiceId }); 51 | expect(commit).toHaveBeenCalledTimes(1); 52 | 53 | const vote: Vote = commit.mock.calls[0][1]; 54 | expect(vote.choiceId).toBe(choiceId); 55 | }); 56 | 57 | describe('when there is no vote', () => { 58 | beforeEach(() => { 59 | state.votes = []; 60 | actions.vote(actionCxt, { choiceId }); 61 | }); 62 | 63 | test('vote ID is 1', () => { 64 | const vote: Vote = commit.mock.calls[0][1]; 65 | expect(vote.id).toBe(1); 66 | }); 67 | }); 68 | 69 | describe('when there is some votes', () => { 70 | beforeEach(() => { 71 | state.votes = [{ id: 1, choiceId: 1 }]; 72 | actions.vote(actionCxt, { choiceId }); 73 | }); 74 | 75 | test('vote ID is increment from votes length', () => { 76 | const vote: Vote = commit.mock.calls[0][1]; 77 | expect(vote.id).toBe(2); 78 | }); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /store/polls/actions.ts: -------------------------------------------------------------------------------- 1 | import { PollsActions } from './types'; 2 | import { loadPolls } from '@/lib/polls/api'; 3 | import { Vote } from '@/lib/polls/models'; 4 | 5 | export const actions: PollsActions = { 6 | load: async ({ commit }) => { 7 | const polls = await loadPolls(); 8 | commit('setPolls', polls); 9 | }, 10 | 11 | vote: ({ commit, state }, { choiceId, comment }) => { 12 | const voteId = state.votes.length 13 | ? state.votes[state.votes.length - 1].id + 1 14 | : 1; 15 | const vote = new Vote(voteId, choiceId, comment); 16 | commit('vote', vote); 17 | } 18 | }; 19 | 20 | export default actions; 21 | -------------------------------------------------------------------------------- /store/polls/const.ts: -------------------------------------------------------------------------------- 1 | import { namespace } from 'vuex-class'; 2 | 3 | /** 4 | * Polls namespace for vuex-class injection 5 | */ 6 | export const pollsModule = namespace('polls/'); 7 | -------------------------------------------------------------------------------- /store/polls/getters.ts: -------------------------------------------------------------------------------- 1 | import { PollsGetters } from './types'; 2 | 3 | /** 4 | * Poll getters 5 | */ 6 | export const getters: PollsGetters = {}; 7 | 8 | export default getters; 9 | -------------------------------------------------------------------------------- /store/polls/mutations.spec.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import { mock1 } from './__mocks__/state.mock'; 4 | import { mutations } from '@/store/polls/mutations'; 5 | import { PollsState } from '@/store/polls/types'; 6 | import { Poll, Vote } from '@/lib/polls/models'; 7 | 8 | let state: PollsState; 9 | 10 | describe('Polls mutations', () => { 11 | beforeEach(() => { 12 | state = mock1(); 13 | }); 14 | 15 | describe('setPolls', () => { 16 | const newPolls: Poll[] = [{ id: 4, topic: 'topic', choices: [] }]; 17 | 18 | test('works', () => { 19 | mutations.setPolls(state, newPolls); 20 | expect(state.polls).toEqual(newPolls); 21 | }); 22 | }); 23 | 24 | describe('vote', () => { 25 | // voting second choice of first poll 26 | const vote: Vote = { id: 5, choiceId: 2 }; 27 | let prevState: PollsState; 28 | 29 | beforeEach(() => { 30 | prevState = _.cloneDeep(state); 31 | mutations.vote(state, vote); 32 | }); 33 | 34 | test('adds the vote', () => { 35 | expect(state.votes).toEqual([...prevState.votes, vote]); 36 | }); 37 | 38 | test('update choice', () => { 39 | const prevChoice = prevState.polls[0].choices[1]; 40 | const choice = state.polls[0].choices[1]; 41 | expect(choice.count).toBe(prevChoice.count + 1); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /store/polls/mutations.ts: -------------------------------------------------------------------------------- 1 | import { PollsMutations } from './types'; 2 | 3 | export const mutations: PollsMutations = { 4 | setPolls: (state, polls) => { 5 | state.polls = polls; 6 | }, 7 | 8 | vote: (state, vote) => { 9 | // add vote 10 | state.votes.push(vote); 11 | 12 | // update choice 13 | state.polls 14 | .map(poll => poll.choices) 15 | .reduce((prev, curr) => prev.concat(curr), []) 16 | .filter(choice => choice.id === vote.choiceId) 17 | .forEach(choice => (choice.count += 1)); 18 | } 19 | }; 20 | 21 | export default mutations; 22 | -------------------------------------------------------------------------------- /store/polls/state.spec.ts: -------------------------------------------------------------------------------- 1 | import { initState } from '@/store/polls/state'; 2 | 3 | describe('Polls state', () => { 4 | describe('initState', () => { 5 | test('works', () => { 6 | const state = initState(); 7 | expect(state.polls).toEqual([]); 8 | expect(state.votes).toEqual([]); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /store/polls/state.ts: -------------------------------------------------------------------------------- 1 | import { PollsState } from './types'; 2 | 3 | /** 4 | * Poll state initialiser 5 | */ 6 | export const initState = (): PollsState => ({ 7 | polls: [], 8 | votes: [] 9 | }); 10 | 11 | export default initState; 12 | -------------------------------------------------------------------------------- /store/polls/types.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree, ActionContext, MutationTree, GetterTree } from 'vuex'; 2 | import { RootState } from '../types'; 3 | import { Poll, Vote, ChoiceVote } from '@/lib/polls/models'; 4 | 5 | export interface PollsState { 6 | polls: Poll[]; 7 | votes: Vote[]; 8 | } 9 | 10 | /** 11 | * Create a type to save some characters: 12 | * SO link for type alias: https://stackoverflow.com/a/28343437/4906586 13 | */ 14 | export type PollActionContext = ActionContext; 15 | 16 | /** 17 | * Polls actions 18 | */ 19 | export interface PollsActions extends ActionTree { 20 | load: (ctx: PollActionContext) => void; 21 | vote: (ctx: PollActionContext, choiceVote: ChoiceVote) => void; 22 | } 23 | 24 | /** 25 | * Polls mutations 26 | */ 27 | export interface PollsMutations extends MutationTree { 28 | setPolls: (state: PollsState, polls: Poll[]) => void; 29 | vote: (state: PollsState, vote: Vote) => void; 30 | } 31 | 32 | /** 33 | * Polls getters is type instead of interface because it is 34 | * empty 35 | */ 36 | export type PollsGetters = GetterTree; 37 | -------------------------------------------------------------------------------- /store/types.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 2 | export interface RootState {} 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "esnext", 8 | "esnext.asynciterable", 9 | "dom" 10 | ], 11 | "esModuleInterop": true, 12 | "experimentalDecorators": true, 13 | "allowJs": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "resolveJsonModule": true, 18 | "noEmit": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "~/*": [ 22 | "./*" 23 | ], 24 | "@/*": [ 25 | "./*" 26 | ] 27 | }, 28 | "types": [ 29 | "@types/node", 30 | "@nuxt/types", 31 | "@types/jest" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | --------------------------------------------------------------------------------