├── .browserslistrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.js ├── .firebase └── hosting.ZGlzdA.cache ├── .firebaserc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── firebase.json ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.vue ├── common │ ├── api.service.js │ ├── config.js │ ├── date.filter.js │ ├── error.filter.js │ └── jwt.service.js ├── components │ ├── ArticleActions.vue │ ├── ArticleList.vue │ ├── ArticleMeta.vue │ ├── Comment.vue │ ├── CommentEditor.vue │ ├── ListErrors.vue │ ├── TagList.vue │ ├── TheFooter.vue │ ├── TheHeader.vue │ ├── VArticlePreview.vue │ ├── VPagination.vue │ └── VTag.vue ├── main.js ├── registerServiceWorker.js ├── router │ └── index.js ├── store │ ├── actions.type.js │ ├── article.module.js │ ├── auth.module.js │ ├── home.module.js │ ├── index.js │ ├── mutations.type.js │ ├── profile.module.js │ └── settings.module.js └── views │ ├── Article.vue │ ├── ArticleEdit.vue │ ├── Home.vue │ ├── HomeGlobal.vue │ ├── HomeMyFeed.vue │ ├── HomeTag.vue │ ├── Login.vue │ ├── Profile.vue │ ├── ProfileArticles.vue │ ├── ProfileFavorited.vue │ ├── Register.vue │ └── Settings.vue ├── static └── rwv-logo.png ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── ListError.spec.js │ └── VPagination.spec.js │ ├── example.spec.js │ └── store │ ├── __snapshots__ │ └── article.module.spec.js.snap │ └── article.module.spec.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build-job: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.8.0 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | - run: yarn run build 31 | 32 | - save_cache: 33 | paths: 34 | - node_modules 35 | - dist 36 | - package.json 37 | - firebase.json 38 | - .firebaserc 39 | key: v1-dist-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 40 | 41 | # run tests! 42 | - run: yarn run test:unit 43 | 44 | deploy-job: 45 | docker: 46 | - image: circleci/node:10.8.0 47 | working_directory: ~/repo 48 | steps: 49 | - restore_cache: 50 | keys: 51 | - v1-dist-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 52 | - run: 53 | name: show directory 54 | command: pwd 55 | - run: 56 | name: Install Firebase 57 | command: npm install --save-dev firebase-tools 58 | - run: 59 | name: look in directory 60 | command: ls -ltr 61 | - run: 62 | name: Deploy Master to Firebase 63 | command: npm run firebase-deploy -- --token=$FIREBASE_DEPLOY_TOKEN 64 | 65 | workflows: 66 | version: 2 67 | 68 | -deploy: 69 | jobs: 70 | - build-job 71 | - deploy-job: 72 | requires: 73 | - build-job 74 | filters: 75 | branches: 76 | only: master -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "babel-eslint" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.firebase/hosting.ZGlzdA.cache: -------------------------------------------------------------------------------- 1 | favicon.ico,1554694922643,efd24a56239d255af28537949b97eb64b060ac198368fe3832f7d082cd34f091 2 | index.html,1554694922643,e1a1a038a1c65ddc6f939696bdd69b17ff85efdfe27a1083dfd0c05e42a8b99f 3 | manifest.json,1554694922649,25efec0ba57676b3444d670afceafef6a5f22bb539d6a668b10a692fe58242ce 4 | robots.txt,1554694922666,f30c4a1c220bd92c487bf016c9b4f4e6959c4c54f82c64c54ee375b1f0189156 5 | precache-manifest.670d99b9b610da66dc1334bca051c523.js,1554694922648,c8e14e220e865cfa009f45c18ac282f3afe263ac3b4d10f01466d5dfa95a0dc2 6 | service-worker.js,1554694922646,833af610ed53148fb343dfaa557e584ebf656147984fc1fee176cfdc02fff43b 7 | img/icons/apple-touch-icon-120x120.png,1554694922829,d31f44d26594559cdf6c57f7f651d8e2ab1ac312e9b8b033b3339f60717dc351 8 | img/icons/apple-touch-icon-152x152.png,1554694922819,56ee37cd4e4baa3ede37e90b9005e50747fb711603bad18cc642d1f983c06763 9 | img/icons/apple-touch-icon-60x60.png,1554694922823,f47f70c16bc06df8c393c0e213a316dcfb9720677a8ee09b4b690b3d8170b0b1 10 | img/icons/android-chrome-192x192.png,1554694922828,7bf5d00aade14cdbd9266476979eb376b1a221fac21bbcd540a9947384851978 11 | img/icons/apple-touch-icon-180x180.png,1554694922825,ccee97a725d3530f4a8111d58e0f61f94fadf4f6fb6468c4aef748c6bd649670 12 | img/icons/apple-touch-icon-76x76.png,1554694922830,081d64e417bfa5a36b47ae153551dff60f3806c1446a094e5d9ead01aa275f96 13 | img/icons/favicon-16x16.png,1554694922822,61c3d70ea4950e5c5cde41ef352a8d38a309083b3a1bb0739d5a76fe4908963a 14 | img/icons/favicon-32x32.png,1554694922823,0b88c724af5f8c9ac5edfcef209e8f8cab39a9e0048f6a6902f2aba55f45fda0 15 | img/icons/apple-touch-icon.png,1554694922824,999885b8893de7fb4b1dccd21b07f0d9c6aa337b85ea501f6ec87b54cfe323d0 16 | img/icons/mstile-150x150.png,1554694922821,4b854eb04911280c094681a76878dba4e8038aeaa70c8e2bb89267cb754109d8 17 | js/chunk-2d0b3289.067b7b50.js.map,1554694922797,ab74d02fe1967f152d2f465a535807f45fe2ee1a18ea58ad40835a2471257842 18 | js/chunk-2d0bac97.6a65e153.js,1554694922814,b5108d2a70745be3b23e6e31df8256492779834a9b7abc8c3c74db4567cfe679 19 | img/icons/msapplication-icon-144x144.png,1554694922830,97a4df445a8b6f4266282a93342cd85326166571466ef0203171cd32b44a2022 20 | js/chunk-2d0b3289.067b7b50.js,1554694922668,9ec514d614c9a7d5440d996b7fa68f4126af96be54e0d97bd2e3b36dee448ecc 21 | img/icons/safari-pinned-tab.svg,1554694922828,7a4ef0f00e8d04eb94d7884fd212c3d6523444777b3046e7130bff649ddeb40a 22 | js/chunk-2d0bd246.3d895320.js.map,1554694922736,f938ed422047a1a12e92582a1baae9d7ec665b99748e65c770bc8b7b3683f976 23 | js/chunk-2d0cedd0.3b979318.js,1554694922813,86bdc40416f484fcca4a459a120494868210bc2c3e04ee7564886fb75b78f699 24 | js/chunk-2d0cedd0.3b979318.js.map,1554694922743,2fa8b6f71801dbf0f6048cea0a209f42071569912409da198dc73c8a5372968c 25 | js/app.fbfff526.js,1554694922665,415ec5c94afa7faf7817c42cef01936b3e4555e643d013bf6d13909af8b880ab 26 | js/chunk-2d0bd246.3d895320.js,1554694922675,ee70d714c29c5a16fd288074bc479d89b04a39e36a67b4c1141bdceb13ef70cd 27 | js/chunk-2d0bac97.6a65e153.js.map,1554694922736,90500036758d7a67ac0d52b6641339d11f3cde5b24370efce54976cc91ac870a 28 | js/chunk-2d0f1193.69f90506.js,1554694922815,5bcc5407390753965f61a87b9f930871689b9f79f8e9ef47ead8940f46623b9d 29 | js/chunk-2d0f1193.69f90506.js.map,1554694922761,6fc7ab9534e45dbc124044049520f74c9c068b6274a5528d5e85c4b9e34cb042 30 | js/chunk-2d207fb4.9af37d21.js,1554694922675,2049a0f56a5454e14384fc22b395aa5cb99b307973161b7fcd8cfe798d56b80a 31 | js/chunk-2d2086b7.0ba954ee.js,1554694922691,93c2c387d25362495754f29888e9815a801724ef87a1cba5316eb24ec8de1421 32 | js/chunk-2d2086b7.0ba954ee.js.map,1554694922760,c87e594ab441fbe747944fb47b4dcaf2904fdc8e5f61f024ab7b562b07bfbe6d 33 | js/chunk-2d0d6d35.5df72040.js,1554694922789,c26d93c9824f9603484fb1777af2c8fd19d2b89127ee79cf27ada9badcdd8eb8 34 | js/chunk-2d217357.b0cdad71.js,1554694922676,b0f0e3368c42eebae179498a14859d642fb25d7dec09e0e217cdc9a66ed98fd2 35 | js/chunk-48c29e6c.a08d1b6f.js,1554694922753,dccaf8de2d00f453c57fe29f210befeb4464f88ff747aa2400ed9fd0f9c72567 36 | js/chunk-2d207fb4.9af37d21.js.map,1554694922760,a41cee0e3e4e60bc0439a8f6ee7379c38929dcfe1e865190cc5a7307154222aa 37 | js/chunk-52fabea2.a7e3b1f5.js,1554694922683,c679cbec3363f2226dda0b12e2fa187e764ed0b7f29b8c09737a97a1e0cf371a 38 | js/chunk-704fe663.c4f57594.js,1554694922742,55615d80d5bde9999ef8275e316ae6b274b09af8cfa402c709f1ca1980453964 39 | js/chunk-fee37f4e.0738a22b.js,1554694922735,6837afc87fc153c8e0a44c9876d5728392ed15253fe5661a99bcaac6dcd09563 40 | js/chunk-fee37f4e.0738a22b.js.map,1554694922772,788c93f79539443857c5e31c4e4778e72369283d41a42eead4a670348e9598f5 41 | js/chunk-704fe663.c4f57594.js.map,1554694922788,399bbef1c00b87f728c87910876c7f713fdaf357ffc9efa16162df6b3f5d2c91 42 | js/chunk-2d217357.b0cdad71.js.map,1554694922766,89d083cd213ed38d05d6eb5e1dd1cb31dee16b0bf02f075cc633003b41eeb702 43 | js/chunk-48c29e6c.a08d1b6f.js.map,1554694922792,7a592d0670bbfd35cc595e70fffaf5ec596f9a6aad114523f56bd1d42646c754 44 | img/icons/android-chrome-512x512.png,1554694922830,b306de640c0dbb4ad7a7a6467455bbda02efdea7eae33a8e861876dc13c888e5 45 | js/chunk-2d0d6d35.5df72040.js.map,1554694922759,61633290fc76a66388f7e087b04994d2095586ab741afada6fd19b20ba190211 46 | js/app.fbfff526.js.map,1554694922818,2e61ff34ca15f0056981ea20f827f21a334cf8ccd098e31b3764a6783950cc71 47 | js/chunk-52fabea2.a7e3b1f5.js.map,1554694922783,3c40c13320f4f49a72304e3b9f0569715773fe869c3129165855f439fb276ef5 48 | js/chunk-vendors.be94ad84.js,1554694922735,78806729cf85ed6ea6a435c07526cb456f53cf1568d9f9ad5ab9ed7428cfdc49 49 | js/chunk-vendors.be94ad84.js.map,1554694922812,a190b70c5964f410e538fce1a231f2dd33690c18efd0e008320e7a538f2b32fa 50 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "vue-realworld-f7520" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present all contributors listed here https://github.com/gothinkster/vue-realworld-example-app/graphs/contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![RealWorld Frontend](https://img.shields.io/badge/realworld-frontend-%23783578.svg)](http://realworld.io) 2 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 3 | 4 | # ![RealWorld Example App](./static/rwv-logo.png) 5 | 6 | (Erik Version) 7 | 8 | > Vue.js codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API. 9 | 10 | Project demo is available at https://vue-vuex-realworld.netlify.com/#/ 11 | 12 | This codebase was created to demonstrate a fully fledged fullstack application built with **Vue.js** including CRUD operations, authentication, routing, pagination, and more. 13 | 14 | We've gone to great lengths to adhere to the **Vue.js** community styleguides & best practices. 15 | 16 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. 17 | 18 | ## Getting started 19 | 20 | Before contributing please read the following: 21 | 22 | 1. [RealWorld guidelines](https://github.com/gothinkster/realworld/tree/master/spec) for implementing a new framework, 23 | 2. [RealWorld frontend instructions](https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md) 24 | 3. [Realworld API endpoints](https://github.com/gothinkster/realworld/tree/master/api) 25 | 4. [Vue.js styleguide](https://vuejs.org/v2/style-guide/index.html). Priority A and B categories must be respected. 26 | 27 | The stack is built using [vue-cli webpack](https://github.com/vuejs-templates/webpack) so to get started all you have to do is: 28 | 29 | ```bash 30 | # install dependencies 31 | > yarn install 32 | # serve with hot reload at localhost:8080 33 | > yarn serve 34 | ``` 35 | 36 | Other commands available are: 37 | 38 | ```bash 39 | # build for production with minification 40 | yarn run build 41 | 42 | # run unit tests 43 | yarn run test:unit 44 | ``` 45 | 46 | # To know 47 | 48 | Current arbitrary choices are: 49 | 50 | - Vuex modules for store 51 | - Vue-axios for ajax requests 52 | - 'rwv' as prefix for components 53 | 54 | These can be changed when the contributors reach a consensus. 55 | 56 | # Connect 57 | 58 | Join us on [Discord](https://discord.gg/NE2jNmg) 59 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.jsx?$": "babel-jest" 8 | }, 9 | moduleNameMapper: { 10 | "^@/(.*)$": "/src/$1" 11 | }, 12 | snapshotSerializers: ["jest-serializer-vue"], 13 | testMatch: [ 14 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 15 | ], 16 | testURL: "http://localhost/" 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Emmanuel Vilsbol ", 3 | "dependencies": { 4 | "axios": "^0.18.0", 5 | "date-fns": "^1.29.0", 6 | "marked": "^0.5.1", 7 | "register-service-worker": "^1.0.0", 8 | "vue": "^2.5.17", 9 | "vue-axios": "^2.1.4", 10 | "vue-router": "^3.0.1", 11 | "vuex": "^3.0.1" 12 | }, 13 | "description": "TodoMVC for the RealWorld™", 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "^3.2.0", 16 | "@vue/cli-plugin-eslint": "^3.2.1", 17 | "@vue/cli-plugin-pwa": "^3.2.0", 18 | "@vue/cli-plugin-unit-jest": "^3.2.0", 19 | "@vue/cli-service": "^3.2.0", 20 | "@vue/eslint-config-prettier": "^4.0.1", 21 | "@vue/test-utils": "^1.0.0-beta.26", 22 | "babel-core": "7.0.0-bridge.0", 23 | "babel-jest": "^23.6.0", 24 | "firebase-tools": "^6.5.3", 25 | "lint-staged": "^7.3.0", 26 | "node-sass": "^4.10.0", 27 | "sass-loader": "^7.1.0", 28 | "vue-template-compiler": "^2.5.17" 29 | }, 30 | "gitHooks": { 31 | "pre-commit": "lint-staged" 32 | }, 33 | "lint-staged": { 34 | "*.js": [ 35 | "vue-cli-service lint", 36 | "git add" 37 | ], 38 | "*.vue": [ 39 | "vue-cli-service lint", 40 | "git add" 41 | ] 42 | }, 43 | "name": "realworld-vue", 44 | "scripts": { 45 | "build": "vue-cli-service build", 46 | "lint": "vue-cli-service lint", 47 | "serve": "vue-cli-service serve", 48 | "test:unit": "vue-cli-service test:unit", 49 | "firebase-deploy": "firebase deploy" 50 | }, 51 | "version": "0.1.0" 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/favicon.ico -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Conduit 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-realworld-example-app", 3 | "short_name": "vue-realworld-example-app", 4 | "icons": [ 5 | { 6 | "src": "/img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/index.html", 17 | "display": "standalone", 18 | "background_color": "#000000", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/common/api.service.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import axios from "axios"; 3 | import VueAxios from "vue-axios"; 4 | import JwtService from "@/common/jwt.service"; 5 | import { API_URL } from "@/common/config"; 6 | 7 | const ApiService = { 8 | init() { 9 | Vue.use(VueAxios, axios); 10 | Vue.axios.defaults.baseURL = API_URL; 11 | }, 12 | 13 | setHeader() { 14 | Vue.axios.defaults.headers.common[ 15 | "Authorization" 16 | ] = `Token ${JwtService.getToken()}`; 17 | }, 18 | 19 | query(resource, params) { 20 | return Vue.axios.get(resource, params).catch(error => { 21 | throw new Error(`[RWV] ApiService ${error}`); 22 | }); 23 | }, 24 | 25 | get(resource, slug = "") { 26 | return Vue.axios.get(`${resource}/${slug}`).catch(error => { 27 | throw new Error(`[RWV] ApiService ${error}`); 28 | }); 29 | }, 30 | 31 | post(resource, params) { 32 | return Vue.axios.post(`${resource}`, params); 33 | }, 34 | 35 | update(resource, slug, params) { 36 | return Vue.axios.put(`${resource}/${slug}`, params); 37 | }, 38 | 39 | put(resource, params) { 40 | return Vue.axios.put(`${resource}`, params); 41 | }, 42 | 43 | delete(resource) { 44 | return Vue.axios.delete(resource).catch(error => { 45 | throw new Error(`[RWV] ApiService ${error}`); 46 | }); 47 | } 48 | }; 49 | 50 | export default ApiService; 51 | 52 | export const TagsService = { 53 | get() { 54 | return ApiService.get("tags"); 55 | } 56 | }; 57 | 58 | export const ArticlesService = { 59 | query(type, params) { 60 | return ApiService.query("articles" + (type === "feed" ? "/feed" : ""), { 61 | params: params 62 | }); 63 | }, 64 | get(slug) { 65 | return ApiService.get("articles", slug); 66 | }, 67 | create(params) { 68 | return ApiService.post("articles", { article: params }); 69 | }, 70 | update(slug, params) { 71 | return ApiService.update("articles", slug, { article: params }); 72 | }, 73 | destroy(slug) { 74 | return ApiService.delete(`articles/${slug}`); 75 | } 76 | }; 77 | 78 | export const CommentsService = { 79 | get(slug) { 80 | if (typeof slug !== "string") { 81 | throw new Error( 82 | "[RWV] CommentsService.get() article slug required to fetch comments" 83 | ); 84 | } 85 | return ApiService.get("articles", `${slug}/comments`); 86 | }, 87 | 88 | post(slug, payload) { 89 | return ApiService.post(`articles/${slug}/comments`, { 90 | comment: { body: payload } 91 | }); 92 | }, 93 | 94 | destroy(slug, commentId) { 95 | return ApiService.delete(`articles/${slug}/comments/${commentId}`); 96 | } 97 | }; 98 | 99 | export const FavoriteService = { 100 | add(slug) { 101 | return ApiService.post(`articles/${slug}/favorite`); 102 | }, 103 | remove(slug) { 104 | return ApiService.delete(`articles/${slug}/favorite`); 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | export const API_URL = "https://conduit.productionready.io/api"; 2 | export default API_URL; 3 | -------------------------------------------------------------------------------- /src/common/date.filter.js: -------------------------------------------------------------------------------- 1 | import { default as format } from "date-fns/format"; 2 | 3 | export default date => { 4 | return format(new Date(date), "MMMM D, YYYY"); 5 | }; 6 | -------------------------------------------------------------------------------- /src/common/error.filter.js: -------------------------------------------------------------------------------- 1 | export default errorValue => { 2 | return `${errorValue[0]}`; 3 | }; 4 | -------------------------------------------------------------------------------- /src/common/jwt.service.js: -------------------------------------------------------------------------------- 1 | const ID_TOKEN_KEY = "id_token"; 2 | 3 | export const getToken = () => { 4 | return window.localStorage.getItem(ID_TOKEN_KEY); 5 | }; 6 | 7 | export const saveToken = token => { 8 | window.localStorage.setItem(ID_TOKEN_KEY, token); 9 | }; 10 | 11 | export const destroyToken = () => { 12 | window.localStorage.removeItem(ID_TOKEN_KEY); 13 | }; 14 | 15 | export default { getToken, saveToken, destroyToken }; 16 | -------------------------------------------------------------------------------- /src/components/ArticleActions.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 93 | -------------------------------------------------------------------------------- /src/components/ArticleList.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 126 | -------------------------------------------------------------------------------- /src/components/ArticleMeta.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 82 | -------------------------------------------------------------------------------- /src/components/Comment.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /src/components/CommentEditor.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 58 | -------------------------------------------------------------------------------- /src/components/ListErrors.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /src/components/TagList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/components/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /src/components/TheHeader.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 97 | -------------------------------------------------------------------------------- /src/components/VArticlePreview.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | -------------------------------------------------------------------------------- /src/components/VPagination.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | -------------------------------------------------------------------------------- /src/components/VTag.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import "./registerServiceWorker"; 6 | 7 | import { CHECK_AUTH } from "./store/actions.type"; 8 | import ApiService from "./common/api.service"; 9 | import DateFilter from "./common/date.filter"; 10 | import ErrorFilter from "./common/error.filter"; 11 | 12 | Vue.config.productionTip = false; 13 | Vue.filter("date", DateFilter); 14 | Vue.filter("error", ErrorFilter); 15 | 16 | ApiService.init(); 17 | 18 | // Ensure we checked auth before each page load. 19 | router.beforeEach((to, from, next) => 20 | Promise.all([store.dispatch(CHECK_AUTH)]).then(next) 21 | ); 22 | 23 | new Vue({ 24 | router, 25 | store, 26 | render: h => h(App) 27 | }).$mount("#app"); 28 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from "register-service-worker"; 4 | 5 | if (process.env.NODE_ENV === "production") { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log( 9 | "App is being served from cache by a service worker.\n" + 10 | "For more details, visit https://goo.gl/AFskqB" 11 | ); 12 | }, 13 | cached() { 14 | console.log("Content has been cached for offline use."); 15 | }, 16 | updated() { 17 | console.log("New content is available; please refresh."); 18 | }, 19 | offline() { 20 | console.log( 21 | "No internet connection found. App is running in offline mode." 22 | ); 23 | }, 24 | error(error) { 25 | console.error("Error during service worker registration:", error); 26 | } 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | 4 | Vue.use(Router); 5 | 6 | export default new Router({ 7 | routes: [ 8 | { 9 | path: "/", 10 | component: () => import("@/views/Home"), 11 | children: [ 12 | { 13 | path: "", 14 | name: "home", 15 | component: () => import("@/views/HomeGlobal") 16 | }, 17 | { 18 | path: "my-feed", 19 | name: "home-my-feed", 20 | component: () => import("@/views/HomeMyFeed") 21 | }, 22 | { 23 | path: "tag/:tag", 24 | name: "home-tag", 25 | component: () => import("@/views/HomeTag") 26 | } 27 | ] 28 | }, 29 | { 30 | name: "login", 31 | path: "/login", 32 | component: () => import("@/views/Login") 33 | }, 34 | { 35 | name: "register", 36 | path: "/register", 37 | component: () => import("@/views/Register") 38 | }, 39 | { 40 | name: "settings", 41 | path: "/settings", 42 | component: () => import("@/views/Settings") 43 | }, 44 | // Handle child routes with a default, by giving the name to the 45 | // child. 46 | // SO: https://github.com/vuejs/vue-router/issues/777 47 | { 48 | path: "/@:username", 49 | component: () => import("@/views/Profile"), 50 | children: [ 51 | { 52 | path: "", 53 | name: "profile", 54 | component: () => import("@/views/ProfileArticles") 55 | }, 56 | { 57 | name: "profile-favorites", 58 | path: "favorites", 59 | component: () => import("@/views/ProfileFavorited") 60 | } 61 | ] 62 | }, 63 | { 64 | name: "article", 65 | path: "/articles/:slug", 66 | component: () => import("@/views/Article"), 67 | props: true 68 | }, 69 | { 70 | name: "article-edit", 71 | path: "/editor/:slug?", 72 | props: true, 73 | component: () => import("@/views/ArticleEdit") 74 | } 75 | ] 76 | }); 77 | -------------------------------------------------------------------------------- /src/store/actions.type.js: -------------------------------------------------------------------------------- 1 | export const ARTICLE_PUBLISH = "publishArticle"; 2 | export const ARTICLE_DELETE = "deleteArticle"; 3 | export const ARTICLE_EDIT = "editArticle"; 4 | export const ARTICLE_EDIT_ADD_TAG = "addTagToArticle"; 5 | export const ARTICLE_EDIT_REMOVE_TAG = "removeTagFromArticle"; 6 | export const ARTICLE_RESET_STATE = "resetArticleState"; 7 | export const CHECK_AUTH = "checkAuth"; 8 | export const COMMENT_CREATE = "createComment"; 9 | export const COMMENT_DESTROY = "destroyComment"; 10 | export const FAVORITE_ADD = "addFavorite"; 11 | export const FAVORITE_REMOVE = "removeFavorite"; 12 | export const FETCH_ARTICLE = "fetchArticle"; 13 | export const FETCH_ARTICLES = "fetchArticles"; 14 | export const FETCH_COMMENTS = "fetchComments"; 15 | export const FETCH_PROFILE = "fetchProfile"; 16 | export const FETCH_PROFILE_FOLLOW = "fetchProfileFollow"; 17 | export const FETCH_PROFILE_UNFOLLOW = "fetchProfileUnfollow"; 18 | export const FETCH_TAGS = "fetchTags"; 19 | export const LOGIN = "login"; 20 | export const LOGOUT = "logout"; 21 | export const REGISTER = "register"; 22 | export const UPDATE_USER = "updateUser"; 23 | -------------------------------------------------------------------------------- /src/store/article.module.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { 3 | ArticlesService, 4 | CommentsService, 5 | FavoriteService 6 | } from "@/common/api.service"; 7 | import { 8 | FETCH_ARTICLE, 9 | FETCH_COMMENTS, 10 | COMMENT_CREATE, 11 | COMMENT_DESTROY, 12 | FAVORITE_ADD, 13 | FAVORITE_REMOVE, 14 | ARTICLE_PUBLISH, 15 | ARTICLE_EDIT, 16 | ARTICLE_EDIT_ADD_TAG, 17 | ARTICLE_EDIT_REMOVE_TAG, 18 | ARTICLE_DELETE, 19 | ARTICLE_RESET_STATE 20 | } from "./actions.type"; 21 | import { 22 | RESET_STATE, 23 | SET_ARTICLE, 24 | SET_COMMENTS, 25 | TAG_ADD, 26 | TAG_REMOVE, 27 | UPDATE_ARTICLE_IN_LIST 28 | } from "./mutations.type"; 29 | 30 | const initialState = { 31 | article: { 32 | author: {}, 33 | title: "", 34 | description: "", 35 | body: "", 36 | tagList: [] 37 | }, 38 | comments: [] 39 | }; 40 | 41 | export const state = { ...initialState }; 42 | 43 | export const actions = { 44 | async [FETCH_ARTICLE](context, articleSlug, prevArticle) { 45 | // avoid extronuous network call if article exists 46 | if (prevArticle !== undefined) { 47 | return context.commit(SET_ARTICLE, prevArticle); 48 | } 49 | const { data } = await ArticlesService.get(articleSlug); 50 | context.commit(SET_ARTICLE, data.article); 51 | return data; 52 | }, 53 | async [FETCH_COMMENTS](context, articleSlug) { 54 | const { data } = await CommentsService.get(articleSlug); 55 | context.commit(SET_COMMENTS, data.comments); 56 | return data.comments; 57 | }, 58 | async [COMMENT_CREATE](context, payload) { 59 | await CommentsService.post(payload.slug, payload.comment); 60 | context.dispatch(FETCH_COMMENTS, payload.slug); 61 | }, 62 | async [COMMENT_DESTROY](context, payload) { 63 | await CommentsService.destroy(payload.slug, payload.commentId); 64 | context.dispatch(FETCH_COMMENTS, payload.slug); 65 | }, 66 | async [FAVORITE_ADD](context, payload) { 67 | const { data } = await FavoriteService.add(payload); 68 | context.commit(UPDATE_ARTICLE_IN_LIST, data.article, { root: true }); 69 | context.commit(SET_ARTICLE, data.article); 70 | }, 71 | async [FAVORITE_REMOVE](context, payload) { 72 | const { data } = await FavoriteService.remove(payload); 73 | // Update list as well. This allows us to favorite an article in the Home view. 74 | context.commit(UPDATE_ARTICLE_IN_LIST, data.article, { root: true }); 75 | context.commit(SET_ARTICLE, data.article); 76 | }, 77 | [ARTICLE_PUBLISH]({ state }) { 78 | return ArticlesService.create(state.article); 79 | }, 80 | [ARTICLE_DELETE](context, slug) { 81 | return ArticlesService.destroy(slug); 82 | }, 83 | [ARTICLE_EDIT]({ state }) { 84 | return ArticlesService.update(state.article.slug, state.article); 85 | }, 86 | [ARTICLE_EDIT_ADD_TAG](context, tag) { 87 | context.commit(TAG_ADD, tag); 88 | }, 89 | [ARTICLE_EDIT_REMOVE_TAG](context, tag) { 90 | context.commit(TAG_REMOVE, tag); 91 | }, 92 | [ARTICLE_RESET_STATE]({ commit }) { 93 | commit(RESET_STATE); 94 | } 95 | }; 96 | 97 | /* eslint no-param-reassign: ["error", { "props": false }] */ 98 | export const mutations = { 99 | [SET_ARTICLE](state, article) { 100 | state.article = article; 101 | }, 102 | [SET_COMMENTS](state, comments) { 103 | state.comments = comments; 104 | }, 105 | [TAG_ADD](state, tag) { 106 | state.article.tagList = state.article.tagList.concat([tag]); 107 | }, 108 | [TAG_REMOVE](state, tag) { 109 | state.article.tagList = state.article.tagList.filter(t => t !== tag); 110 | }, 111 | [RESET_STATE]() { 112 | for (let f in state) { 113 | Vue.set(state, f, initialState[f]); 114 | } 115 | } 116 | }; 117 | 118 | const getters = { 119 | article(state) { 120 | return state.article; 121 | }, 122 | comments(state) { 123 | return state.comments; 124 | } 125 | }; 126 | 127 | export default { 128 | state, 129 | actions, 130 | mutations, 131 | getters 132 | }; 133 | -------------------------------------------------------------------------------- /src/store/auth.module.js: -------------------------------------------------------------------------------- 1 | import ApiService from "@/common/api.service"; 2 | import JwtService from "@/common/jwt.service"; 3 | import { 4 | LOGIN, 5 | LOGOUT, 6 | REGISTER, 7 | CHECK_AUTH, 8 | UPDATE_USER 9 | } from "./actions.type"; 10 | import { SET_AUTH, PURGE_AUTH, SET_ERROR } from "./mutations.type"; 11 | 12 | const state = { 13 | errors: null, 14 | user: {}, 15 | isAuthenticated: !!JwtService.getToken() 16 | }; 17 | 18 | const getters = { 19 | currentUser(state) { 20 | return state.user; 21 | }, 22 | isAuthenticated(state) { 23 | return state.isAuthenticated; 24 | } 25 | }; 26 | 27 | const actions = { 28 | [LOGIN](context, credentials) { 29 | return new Promise(resolve => { 30 | ApiService.post("users/login", { user: credentials }) 31 | .then(({ data }) => { 32 | context.commit(SET_AUTH, data.user); 33 | resolve(data); 34 | }) 35 | .catch(({ response }) => { 36 | context.commit(SET_ERROR, response.data.errors); 37 | }); 38 | }); 39 | }, 40 | [LOGOUT](context) { 41 | context.commit(PURGE_AUTH); 42 | }, 43 | [REGISTER](context, credentials) { 44 | return new Promise((resolve, reject) => { 45 | ApiService.post("users", { user: credentials }) 46 | .then(({ data }) => { 47 | context.commit(SET_AUTH, data.user); 48 | resolve(data); 49 | }) 50 | .catch(({ response }) => { 51 | context.commit(SET_ERROR, response.data.errors); 52 | reject(response); 53 | }); 54 | }); 55 | }, 56 | [CHECK_AUTH](context) { 57 | if (JwtService.getToken()) { 58 | ApiService.setHeader(); 59 | ApiService.get("user") 60 | .then(({ data }) => { 61 | context.commit(SET_AUTH, data.user); 62 | }) 63 | .catch(({ response }) => { 64 | context.commit(SET_ERROR, response.data.errors); 65 | }); 66 | } else { 67 | context.commit(PURGE_AUTH); 68 | } 69 | }, 70 | [UPDATE_USER](context, payload) { 71 | const { email, username, password, image, bio } = payload; 72 | const user = { 73 | email, 74 | username, 75 | bio, 76 | image 77 | }; 78 | if (password) { 79 | user.password = password; 80 | } 81 | 82 | return ApiService.put("user", user).then(({ data }) => { 83 | context.commit(SET_AUTH, data.user); 84 | return data; 85 | }); 86 | } 87 | }; 88 | 89 | const mutations = { 90 | [SET_ERROR](state, error) { 91 | state.errors = error; 92 | }, 93 | [SET_AUTH](state, user) { 94 | state.isAuthenticated = true; 95 | state.user = user; 96 | state.errors = {}; 97 | JwtService.saveToken(state.user.token); 98 | }, 99 | [PURGE_AUTH](state) { 100 | state.isAuthenticated = false; 101 | state.user = {}; 102 | state.errors = {}; 103 | JwtService.destroyToken(); 104 | } 105 | }; 106 | 107 | export default { 108 | state, 109 | actions, 110 | mutations, 111 | getters 112 | }; 113 | -------------------------------------------------------------------------------- /src/store/home.module.js: -------------------------------------------------------------------------------- 1 | import { TagsService, ArticlesService } from "@/common/api.service"; 2 | import { FETCH_ARTICLES, FETCH_TAGS } from "./actions.type"; 3 | import { 4 | FETCH_START, 5 | FETCH_END, 6 | SET_TAGS, 7 | UPDATE_ARTICLE_IN_LIST 8 | } from "./mutations.type"; 9 | 10 | const state = { 11 | tags: [], 12 | articles: [], 13 | isLoading: true, 14 | articlesCount: 0 15 | }; 16 | 17 | const getters = { 18 | articlesCount(state) { 19 | return state.articlesCount; 20 | }, 21 | articles(state) { 22 | return state.articles; 23 | }, 24 | isLoading(state) { 25 | return state.isLoading; 26 | }, 27 | tags(state) { 28 | return state.tags; 29 | } 30 | }; 31 | 32 | const actions = { 33 | [FETCH_ARTICLES]({ commit }, params) { 34 | commit(FETCH_START); 35 | return ArticlesService.query(params.type, params.filters) 36 | .then(({ data }) => { 37 | commit(FETCH_END, data); 38 | }) 39 | .catch(error => { 40 | throw new Error(error); 41 | }); 42 | }, 43 | [FETCH_TAGS]({ commit }) { 44 | return TagsService.get() 45 | .then(({ data }) => { 46 | commit(SET_TAGS, data.tags); 47 | }) 48 | .catch(error => { 49 | throw new Error(error); 50 | }); 51 | } 52 | }; 53 | 54 | /* eslint no-param-reassign: ["error", { "props": false }] */ 55 | const mutations = { 56 | [FETCH_START](state) { 57 | state.isLoading = true; 58 | }, 59 | [FETCH_END](state, { articles, articlesCount }) { 60 | state.articles = articles; 61 | state.articlesCount = articlesCount; 62 | state.isLoading = false; 63 | }, 64 | [SET_TAGS](state, tags) { 65 | state.tags = tags; 66 | }, 67 | [UPDATE_ARTICLE_IN_LIST](state, data) { 68 | state.articles = state.articles.map(article => { 69 | if (article.slug !== data.slug) { 70 | return article; 71 | } 72 | // We could just return data, but it seems dangerous to 73 | // mix the results of different api calls, so we 74 | // protect ourselves by copying the information. 75 | article.favorited = data.favorited; 76 | article.favoritesCount = data.favoritesCount; 77 | return article; 78 | }); 79 | } 80 | }; 81 | 82 | export default { 83 | state, 84 | getters, 85 | actions, 86 | mutations 87 | }; 88 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | import home from "./home.module"; 5 | import auth from "./auth.module"; 6 | import article from "./article.module"; 7 | import profile from "./profile.module"; 8 | 9 | Vue.use(Vuex); 10 | 11 | export default new Vuex.Store({ 12 | modules: { 13 | home, 14 | auth, 15 | article, 16 | profile 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/store/mutations.type.js: -------------------------------------------------------------------------------- 1 | export const FETCH_END = "setArticles"; 2 | export const FETCH_START = "setLoading"; 3 | export const PURGE_AUTH = "logOut"; 4 | export const SET_ARTICLE = "setArticle"; 5 | export const SET_AUTH = "setUser"; 6 | export const SET_COMMENTS = "setComments"; 7 | export const SET_ERROR = "setError"; 8 | export const SET_PROFILE = "setProfile"; 9 | export const SET_TAGS = "setTags"; 10 | export const TAG_ADD = "addTag"; 11 | export const TAG_REMOVE = "removeTag"; 12 | export const UPDATE_ARTICLE_IN_LIST = "updateAricleInList"; 13 | export const RESET_STATE = "resetModuleState"; 14 | -------------------------------------------------------------------------------- /src/store/profile.module.js: -------------------------------------------------------------------------------- 1 | import ApiService from "@/common/api.service"; 2 | import { 3 | FETCH_PROFILE, 4 | FETCH_PROFILE_FOLLOW, 5 | FETCH_PROFILE_UNFOLLOW 6 | } from "./actions.type"; 7 | import { SET_PROFILE } from "./mutations.type"; 8 | 9 | const state = { 10 | errors: {}, 11 | profile: {} 12 | }; 13 | 14 | const getters = { 15 | profile(state) { 16 | return state.profile; 17 | } 18 | }; 19 | 20 | const actions = { 21 | [FETCH_PROFILE](context, payload) { 22 | const { username } = payload; 23 | return ApiService.get("profiles", username) 24 | .then(({ data }) => { 25 | context.commit(SET_PROFILE, data.profile); 26 | return data; 27 | }) 28 | .catch(() => { 29 | // #todo SET_ERROR cannot work in multiple states 30 | // context.commit(SET_ERROR, response.data.errors) 31 | }); 32 | }, 33 | [FETCH_PROFILE_FOLLOW](context, payload) { 34 | const { username } = payload; 35 | return ApiService.post(`profiles/${username}/follow`) 36 | .then(({ data }) => { 37 | context.commit(SET_PROFILE, data.profile); 38 | return data; 39 | }) 40 | .catch(() => { 41 | // #todo SET_ERROR cannot work in multiple states 42 | // context.commit(SET_ERROR, response.data.errors) 43 | }); 44 | }, 45 | [FETCH_PROFILE_UNFOLLOW](context, payload) { 46 | const { username } = payload; 47 | return ApiService.delete(`profiles/${username}/follow`) 48 | .then(({ data }) => { 49 | context.commit(SET_PROFILE, data.profile); 50 | return data; 51 | }) 52 | .catch(() => { 53 | // #todo SET_ERROR cannot work in multiple states 54 | // context.commit(SET_ERROR, response.data.errors) 55 | }); 56 | } 57 | }; 58 | 59 | const mutations = { 60 | // [SET_ERROR] (state, error) { 61 | // state.errors = error 62 | // }, 63 | [SET_PROFILE](state, profile) { 64 | state.profile = profile; 65 | state.errors = {}; 66 | } 67 | }; 68 | 69 | export default { 70 | state, 71 | actions, 72 | mutations, 73 | getters 74 | }; 75 | -------------------------------------------------------------------------------- /src/store/settings.module.js: -------------------------------------------------------------------------------- 1 | import { ArticlesService, CommentsService } from "@/common/api.service"; 2 | import { FETCH_ARTICLE, FETCH_COMMENTS } from "./actions.type"; 3 | import { SET_ARTICLE, SET_COMMENTS } from "./mutations.type"; 4 | 5 | export const state = { 6 | article: {}, 7 | comments: [] 8 | }; 9 | 10 | export const actions = { 11 | [FETCH_ARTICLE](context, articleSlug) { 12 | return ArticlesService.get(articleSlug) 13 | .then(({ data }) => { 14 | context.commit(SET_ARTICLE, data.article); 15 | }) 16 | .catch(error => { 17 | throw new Error(error); 18 | }); 19 | }, 20 | [FETCH_COMMENTS](context, articleSlug) { 21 | return CommentsService.get(articleSlug) 22 | .then(({ data }) => { 23 | context.commit(SET_COMMENTS, data.comments); 24 | }) 25 | .catch(error => { 26 | throw new Error(error); 27 | }); 28 | } 29 | }; 30 | 31 | /* eslint no-param-reassign: ["error", { "props": false }] */ 32 | export const mutations = { 33 | [SET_ARTICLE](state, article) { 34 | state.article = article; 35 | }, 36 | [SET_COMMENTS](state, comments) { 37 | state.comments = comments; 38 | } 39 | }; 40 | 41 | export default { 42 | state, 43 | actions, 44 | mutations 45 | }; 46 | -------------------------------------------------------------------------------- /src/views/Article.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 96 | -------------------------------------------------------------------------------- /src/views/ArticleEdit.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 152 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 81 | -------------------------------------------------------------------------------- /src/views/HomeGlobal.vue: -------------------------------------------------------------------------------- 1 | 4 | 14 | -------------------------------------------------------------------------------- /src/views/HomeMyFeed.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/views/HomeTag.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 68 | -------------------------------------------------------------------------------- /src/views/Profile.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 113 | -------------------------------------------------------------------------------- /src/views/ProfileArticles.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /src/views/ProfileFavorited.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 81 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 89 | -------------------------------------------------------------------------------- /static/rwv-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErikCH/vue-realworld-example-app/1e2b68577f96df310fcc95afd0a42268a168a610/static/rwv-logo.png -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/unit/components/ListError.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | 3 | import ListErrors from "../../../src/components/ListErrors.vue"; 4 | 5 | const createWrapper = ({ errors }) => { 6 | return mount(ListErrors, { 7 | propsData: { 8 | errors 9 | } 10 | }); 11 | }; 12 | 13 | describe("ListErrors", () => { 14 | let errors; 15 | 16 | beforeEach(() => { 17 | errors = { 18 | title: ["Title Error"], 19 | body: ["can't be blank"], 20 | description: ["can't be blank"] 21 | }; 22 | }); 23 | 24 | it("should display the correct error messages based on object from props", () => { 25 | const wrapper = createWrapper({ errors }); 26 | 27 | const errorMessages = wrapper.findAll("li"); 28 | expect(errorMessages.length).toEqual(3); 29 | expect(errorMessages.at(0).text()).toContain(errors.title); 30 | expect(errorMessages.at(1).text()).toContain(errors.body); 31 | expect(errorMessages.at(2).text()).toContain(errors.description); 32 | }); 33 | 34 | it("should have props with errors as type object", () => { 35 | const wrapper = createWrapper({ errors }); 36 | expect(typeof wrapper.props().errors).toBe("object"); 37 | }); 38 | 39 | it("should have no errors if no errors are passed into the props", () => { 40 | errors = {}; 41 | 42 | const wrapper = createWrapper({ errors }); 43 | 44 | const errorMessages = wrapper.findAll("li"); 45 | expect(errorMessages.length).toEqual(0); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/unit/components/VPagination.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | 3 | import VPagination from "../../../src/components/VPagination.vue"; 4 | 5 | const createWrapper = ({ currentPage = 1 }) => { 6 | return mount(VPagination, { 7 | propsData: { 8 | pages: [1, 2, 3, 4], 9 | currentPage 10 | } 11 | }); 12 | }; 13 | 14 | describe("VPagination", () => { 15 | it("should render active class to right element", () => { 16 | const wrapper = createWrapper({ currentPage: 2 }); 17 | const activeItem = wrapper.find(".active"); 18 | expect(activeItem.text()).toBe("2"); 19 | }); 20 | 21 | it("should emit an event if page is clicked which is not active", () => { 22 | const wrapper = createWrapper({ currentPage: 1 }); 23 | const pageItem = wrapper.find('[data-test="page-link-2"]'); 24 | pageItem.trigger("click"); 25 | expect(wrapper.emitted("update:currentPage")).toBeTruthy(); 26 | }); 27 | 28 | it("should have the right payload when event is emitted", () => { 29 | const wrapper = createWrapper({ currentPage: 1 }); 30 | const pageItem = wrapper.find('[data-test="page-link-2"]'); 31 | pageItem.trigger("click"); 32 | expect(wrapper.emitted("update:currentPage")[0][0]).toBe(2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { createLocalVue, mount } from "@vue/test-utils"; 2 | import Vuex from "vuex"; 3 | import VueRouter from "vue-router"; 4 | 5 | import Comment from "../../src/components/Comment.vue"; 6 | import DateFilter from "../../src/common/date.filter"; 7 | 8 | const localVue = createLocalVue(); 9 | localVue.filter("date", DateFilter); 10 | localVue.use(Vuex); 11 | localVue.use(VueRouter); 12 | 13 | describe("Comment", () => { 14 | it("should render correct contents", () => { 15 | const router = new VueRouter({ 16 | routes: [ 17 | { 18 | name: "profile", 19 | path: "/profile", 20 | component: null 21 | } 22 | ] 23 | }); 24 | let store = new Vuex.Store({ 25 | getters: { 26 | currentUser: () => ({ 27 | username: "user-3518518" 28 | }) 29 | } 30 | }); 31 | 32 | const wrapper = mount(Comment, { 33 | localVue, 34 | store, 35 | router, 36 | propsData: { 37 | slug: "super-cool-comment-slug-1245781274", 38 | comment: { 39 | body: "body of comment", 40 | author: { 41 | image: "https://vuejs.org/images/logo.png", 42 | username: "user-3518518" 43 | }, 44 | createdAt: "", 45 | id: 1245781274 46 | } 47 | } 48 | }); 49 | expect(wrapper.isVueInstance()).toBeTruthy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/unit/store/__snapshots__/article.module.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Vuex Article Module should return the data of the api call when calling the function 1`] = ` 4 | Object { 5 | "article": Object { 6 | "author": Object {}, 7 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 8 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 9 | "tagList": Array [ 10 | "lorem", 11 | "ipsum", 12 | "javascript", 13 | "vue", 14 | ], 15 | "title": "Lorem ipsum dolor sit amet", 16 | }, 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /tests/unit/store/article.module.spec.js: -------------------------------------------------------------------------------- 1 | import { actions } from "../../../src/store/article.module"; 2 | import { 3 | FETCH_ARTICLE, 4 | FETCH_COMMENTS, 5 | COMMENT_CREATE, 6 | COMMENT_DESTROY, 7 | FAVORITE_ADD, 8 | FAVORITE_REMOVE 9 | } from "../../../src/store/actions.type"; 10 | 11 | jest.mock("vue", () => { 12 | return { 13 | axios: { 14 | get: jest.fn().mockImplementation(async articleSlug => { 15 | if (articleSlug.includes("8371b051-cffc-4ff0-887c-2c477615a28e")) { 16 | return { 17 | data: { 18 | article: { 19 | author: {}, 20 | title: "Lorem ipsum dolor sit amet", 21 | description: 22 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 23 | body: 24 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 25 | tagList: ["lorem", "ipsum", "javascript", "vue"] 26 | } 27 | } 28 | }; 29 | } 30 | if (articleSlug.includes("f986b3d6-95c2-4c4f-a6b9-fbbf79d8cb0c")) { 31 | return { 32 | data: { 33 | comments: [ 34 | { 35 | id: 1, 36 | createdAt: "2018-12-01T15:43:41.235Z", 37 | updatedAt: "2018-12-01T15:43:41.235Z", 38 | body: "Lorem ipsum dolor sit amet.", 39 | author: { 40 | username: "dccf649a-5e7b-4040-b8c3-ecf74598eba2", 41 | bio: null, 42 | image: "https://via.placeholder.com/350x150", 43 | following: false 44 | } 45 | }, 46 | { 47 | id: 2, 48 | createdAt: "2018-12-01T15:43:39.077Z", 49 | updatedAt: "2018-12-01T15:43:39.077Z", 50 | body: 51 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse aliquet.", 52 | author: { 53 | username: "8568a50a-9656-4d55-a023-632029513a2d", 54 | bio: null, 55 | image: "https://via.placeholder.com/350x150", 56 | following: false 57 | } 58 | } 59 | ] 60 | } 61 | }; 62 | } 63 | throw new Error("Article not existing"); 64 | }), 65 | post: jest.fn().mockImplementation(async articleSlug => { 66 | if (articleSlug.includes("582e1e46-6b8b-4f4d-8848-f07b57e015a0")) { 67 | return null; 68 | } 69 | if (articleSlug.includes("5611ee1b-0b95-417f-a917-86687176a627")) { 70 | return { 71 | data: { 72 | article: { 73 | author: {}, 74 | title: "Lorem ipsum dolor sit amet", 75 | description: 76 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 77 | body: 78 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 79 | tagList: ["lorem", "ipsum", "javascript", "vue"] 80 | } 81 | } 82 | }; 83 | } 84 | throw new Error("Article not existing"); 85 | }), 86 | delete: jest.fn().mockImplementation(async articleSlug => { 87 | if (articleSlug.includes("657a6075-d269-4aec-83fa-b14f579a3e78")) { 88 | return null; 89 | } 90 | if (articleSlug.includes("480fdaf8-027c-43b1-8952-8403f90dcdab")) { 91 | return { 92 | data: { 93 | article: { 94 | author: {}, 95 | title: "Lorem ipsum dolor sit amet", 96 | description: 97 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 98 | body: 99 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 100 | tagList: ["lorem", "ipsum", "javascript", "vue"] 101 | } 102 | } 103 | }; 104 | } 105 | throw new Error("Article not existing"); 106 | }) 107 | } 108 | }; 109 | }); 110 | 111 | describe("Vuex Article Module", () => { 112 | it("should commit the previous article if it is given", async () => { 113 | const commitFunction = jest.fn(); 114 | const context = { commit: commitFunction }; 115 | const articleSlug = "8371b051-cffc-4ff0-887c-2c477615a28e"; 116 | const prevArticle = { 117 | author: {}, 118 | title: "Aye up, she's a reight bobby dazzler", 119 | description: 120 | "Yer flummoxed. Fair t' middlin, this is. Off f'r a sup down t'pub, to'neet. Ee bye ecky thump!", 121 | body: 122 | "Tha's better bi careful, lass - yer on a Scarborough warning! Tha meks a better door than a winder. Do I 'eckers like, You're in luck m'boy! Am proper knackered, aye I am that is I say.", 123 | tagList: ["aye", "ipsum", "javascript", "vue"] 124 | }; 125 | await actions[FETCH_ARTICLE](context, articleSlug, prevArticle); 126 | expect(commitFunction.mock.calls[0][0]).toBe("setArticle"); 127 | expect(commitFunction.mock.calls[0][1]).toBe(prevArticle); 128 | }); 129 | 130 | it("should return the data of the api call when calling the function", async () => { 131 | const context = { commit: () => {} }; 132 | const articleSlug = "8371b051-cffc-4ff0-887c-2c477615a28e"; 133 | const prevArticle = undefined; 134 | const actionCall = await actions[FETCH_ARTICLE]( 135 | context, 136 | articleSlug, 137 | prevArticle 138 | ); 139 | expect(actionCall).toMatchSnapshot(); 140 | }); 141 | 142 | it("should commit the right name when fetching comments for an existing article", async () => { 143 | const commitFunction = jest.fn(); 144 | const context = { commit: commitFunction }; 145 | const articleSlug = "f986b3d6-95c2-4c4f-a6b9-fbbf79d8cb0c"; 146 | await actions[FETCH_COMMENTS](context, articleSlug); 147 | expect(commitFunction.mock.calls[0][0]).toBe("setComments"); 148 | }); 149 | 150 | it("should commit the exact size of comments", async () => { 151 | const commitFunction = jest.fn(); 152 | const context = { commit: commitFunction }; 153 | const articleSlug = "f986b3d6-95c2-4c4f-a6b9-fbbf79d8cb0c"; 154 | await actions[FETCH_COMMENTS](context, articleSlug); 155 | expect(commitFunction.mock.calls[0][1]).toHaveLength(2); 156 | }); 157 | 158 | it("should return the comments from the fetch comments action", async () => { 159 | const context = { commit: () => {} }; 160 | const articleSlug = "f986b3d6-95c2-4c4f-a6b9-fbbf79d8cb0c"; 161 | const comments = await actions[FETCH_COMMENTS](context, articleSlug); 162 | expect(comments).toHaveLength(2); 163 | }); 164 | 165 | it("should dispatch a fetching comment action after successfully creating a comment", async () => { 166 | const dispatchFunction = jest.fn(); 167 | const context = { dispatch: dispatchFunction }; 168 | const payload = { 169 | slug: "582e1e46-6b8b-4f4d-8848-f07b57e015a0", 170 | comment: "Lorem Ipsum" 171 | }; 172 | await actions[COMMENT_CREATE](context, payload); 173 | expect(dispatchFunction).toHaveBeenLastCalledWith( 174 | "fetchComments", 175 | "582e1e46-6b8b-4f4d-8848-f07b57e015a0" 176 | ); 177 | }); 178 | 179 | it("should dispatch a fetching comment action after successfully deleting a comment", async () => { 180 | const dispatchFunction = jest.fn(); 181 | const context = { dispatch: dispatchFunction }; 182 | const payload = { 183 | slug: "657a6075-d269-4aec-83fa-b14f579a3e78", 184 | commentId: 1 185 | }; 186 | await actions[COMMENT_DESTROY](context, payload); 187 | expect(dispatchFunction).toHaveBeenLastCalledWith( 188 | "fetchComments", 189 | "657a6075-d269-4aec-83fa-b14f579a3e78" 190 | ); 191 | }); 192 | 193 | it("should commit updating the article in the list action favorize an article", async () => { 194 | const commitFunction = jest.fn(); 195 | const context = { commit: commitFunction }; 196 | const payload = "5611ee1b-0b95-417f-a917-86687176a627"; 197 | await actions[FAVORITE_ADD](context, payload); 198 | expect(commitFunction.mock.calls[0][0]).toBe("updateAricleInList"); 199 | expect(commitFunction.mock.calls[0][1]).toEqual({ 200 | author: {}, 201 | title: "Lorem ipsum dolor sit amet", 202 | description: 203 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 204 | body: 205 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 206 | tagList: ["lorem", "ipsum", "javascript", "vue"] 207 | }); 208 | }); 209 | 210 | it("should commit setting the article", async () => { 211 | const commitFunction = jest.fn(); 212 | const context = { commit: commitFunction }; 213 | const payload = "5611ee1b-0b95-417f-a917-86687176a627"; 214 | await actions[FAVORITE_ADD](context, payload); 215 | expect(commitFunction.mock.calls[1][0]).toBe("setArticle"); 216 | expect(commitFunction.mock.calls[1][1]).toEqual({ 217 | author: {}, 218 | title: "Lorem ipsum dolor sit amet", 219 | description: 220 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 221 | body: 222 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 223 | tagList: ["lorem", "ipsum", "javascript", "vue"] 224 | }); 225 | }); 226 | 227 | it("should commit updating the article in the list action favorize an article", async () => { 228 | const commitFunction = jest.fn(); 229 | const context = { commit: commitFunction }; 230 | const payload = "480fdaf8-027c-43b1-8952-8403f90dcdab"; 231 | await actions[FAVORITE_REMOVE](context, payload); 232 | expect(commitFunction.mock.calls[0][0]).toBe("updateAricleInList"); 233 | expect(commitFunction.mock.calls[0][1]).toEqual({ 234 | author: {}, 235 | title: "Lorem ipsum dolor sit amet", 236 | description: 237 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 238 | body: 239 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 240 | tagList: ["lorem", "ipsum", "javascript", "vue"] 241 | }); 242 | }); 243 | 244 | it("should commit setting the article", async () => { 245 | const commitFunction = jest.fn(); 246 | const context = { commit: commitFunction }; 247 | const payload = "480fdaf8-027c-43b1-8952-8403f90dcdab"; 248 | await actions[FAVORITE_REMOVE](context, payload); 249 | expect(commitFunction.mock.calls[1][0]).toBe("setArticle"); 250 | expect(commitFunction.mock.calls[1][1]).toEqual({ 251 | author: {}, 252 | title: "Lorem ipsum dolor sit amet", 253 | description: 254 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed cursus nisl. Morbi pulvinar nisl urna, tincidunt mattis tortor sollicitudin eget. Nulla viverra justo quis.", 255 | body: 256 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dictum efficitur justo, nec aliquam quam rutrum in. Pellentesque vulputate augue quis vulputate finibus. Phasellus auctor semper sapien sit amet interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas placerat auctor metus. Integer blandit lacinia volutpat.", 257 | tagList: ["lorem", "ipsum", "javascript", "vue"] 258 | }); 259 | }); 260 | }); 261 | --------------------------------------------------------------------------------