├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.js
├── 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
│ └── VTag.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
23 | # Coverage Reports
24 | coverage
25 |
--------------------------------------------------------------------------------
/.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 | [](http://realworld.io)
2 | [](https://standardjs.com)
3 |
4 | ----
5 | ##New Maintainers wanted##
6 | Anyone up for the challenge of maintaining this repo?
7 | Reach out on twitter @vilsbole
8 | ----
9 |
10 |
11 |
12 | # 
13 |
14 | > 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.
15 |
16 | Project demo is available at https://vue-vuex-realworld.netlify.app/
17 |
18 | This codebase was created to demonstrate a fully fledged fullstack application built with **Vue.js** including CRUD operations, authentication, routing, pagination, and more.
19 |
20 | We've gone to great lengths to adhere to the **Vue.js** community styleguides & best practices.
21 |
22 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.
23 |
24 | ## Getting started
25 |
26 | Before contributing please read the following:
27 |
28 | 1. [RealWorld guidelines](https://github.com/gothinkster/realworld/tree/master/spec) for implementing a new framework,
29 | 2. [RealWorld frontend instructions](https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md)
30 | 3. [Realworld API endpoints](https://github.com/gothinkster/realworld/tree/master/api)
31 | 4. [Vue.js styleguide](https://vuejs.org/v2/style-guide/index.html). Priority A and B categories must be respected.
32 | 5. [Editorconfig setup](https://editorconfig.org/#download). Most of the common editors support editorconfig by default (check the editorconfig download link for your ide), but editorconfig npm package have to installed globally for it to work,
33 |
34 | ```bash
35 | # install editorconfig globally
36 | > npm install -g editorconfig
37 | ```
38 |
39 | The stack is built using [vue-cli webpack](https://github.com/vuejs-templates/webpack) so to get started all you have to do is:
40 |
41 | ``` bash
42 | # install dependencies
43 | > yarn install
44 | # serve with hot reload at localhost:8080
45 | > yarn serve
46 | ```
47 |
48 | Other commands available are:
49 |
50 | ``` bash
51 | # build for production with minification
52 | yarn run build
53 |
54 | # run unit tests
55 | yarn test
56 | ```
57 |
58 | ## To know
59 |
60 | Current arbitrary choices are:
61 |
62 | - Vuex modules for store
63 | - Vue-axios for ajax requests
64 | - 'rwv' as prefix for components
65 |
66 | These can be changed when the contributors reach a consensus.
67 |
68 | ## FAQ
69 |
70 |
71 | Where can I find the service worker file?
72 |
73 | The service worker file is generated automatically. The implementation can be found under [`src/registerServiceWorker.js`](https://github.com/gothinkster/vue-realworld-example-app/blob/eeaeb34fa440d00cd400545301ea203bd2a59284/src/registerServiceWorker.js). You can find the dependencies implementation in this repo: [yyx990803/register-service-worker](https://github.com/yyx990803/register-service-worker#readme).
74 |
75 | Also, Google provided a good documentation on how to register a service worker: https://developers.google.com/web/fundamentals/primers/service-workers/registration
76 |
77 |
78 |
79 | Vue.js Function API / Migration to Vue.js 3
80 |
81 | Related resources:
82 |
83 | - [Vue.js Function API RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md)
84 | - [`vue-function-api` plugin](https://github.com/vuejs/vue-function-api)
85 |
86 | Vue.js 3 will likely introduce breaking changes on how Vue.js applications will look like. For example, the Vue.js Function API might be introduced. This would cause a lot of our components to change in the overall structure. The changes would be minimal though. With the `vue-function-api` plugin, these changes could be applied already. The problem is that multiple integrations are not working with the plugin. There are intentions to make this work, but for the time being, we should rather focus on different areas. If you still want to be experimental with it, we are happy to get a Pull Request with some experimental feature implementations.
87 |
88 |
89 | ## Connect
90 |
91 | Join us on [Discord](https://discord.gg/NE2jNmg)
92 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | dev: {
4 | presets: ["@vue/app"]
5 | },
6 | test: {
7 | presets: ["@babel/preset-env"]
8 | }
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/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 | "^.+\\.(js|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 | transformIgnorePatterns: ["/node_modules/"]
18 | };
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Emmanuel Vilsbol ",
3 | "dependencies": {
4 | "axios": "^0.19.0",
5 | "date-fns": "^1.30.1",
6 | "marked": "^0.7.0",
7 | "register-service-worker": "^1.6.2",
8 | "vue": "^2.6.10",
9 | "vue-axios": "^2.1.4",
10 | "vue-router": "^3.1.3",
11 | "vuex": "^3.1.1"
12 | },
13 | "description": "TodoMVC for the RealWorld™",
14 | "devDependencies": {
15 | "@babel/core": "^7.6.2",
16 | "@babel/preset-env": "^7.6.2",
17 | "@vue/cli-plugin-babel": "^3.12.0",
18 | "@vue/cli-plugin-eslint": "^3.10.0",
19 | "@vue/cli-plugin-pwa": "^3.10.0",
20 | "@vue/cli-plugin-unit-jest": "^3.10.0",
21 | "@vue/cli-service": "^3.10.0",
22 | "@vue/eslint-config-prettier": "^4.0.1",
23 | "@vue/test-utils": "^1.0.0-beta.29",
24 | "babel-core": "7.0.0-bridge.0",
25 | "babel-jest": "^24.9.0",
26 | "cross-env": "^6.0.3",
27 | "lint-staged": "^8.2.1",
28 | "node-sass": "^4.12.0",
29 | "nyc": "^14.1.1",
30 | "sass-loader": "^7.1.0",
31 | "vue-template-compiler": "^2.6.10"
32 | },
33 | "gitHooks": {
34 | "pre-commit": "lint-staged"
35 | },
36 | "lint-staged": {
37 | "*.js": [
38 | "vue-cli-service lint",
39 | "git add"
40 | ],
41 | "*.vue": [
42 | "vue-cli-service lint",
43 | "git add"
44 | ]
45 | },
46 | "name": "realworld-vue",
47 | "scripts": {
48 | "build": "cross-env BABEL_ENV=dev vue-cli-service build",
49 | "lint": "vue-cli-service lint",
50 | "serve": "cross-env BABEL_ENV=dev vue-cli-service serve",
51 | "test": "cross-env BABEL_ENV=test jest --coverage"
52 | },
53 | "version": "0.1.0"
54 | }
55 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/public/img/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/public/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/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 |
20 | We're sorry but vue-realworld-example-app doesn't work properly without JavaScript enabled. Please enable
21 | it to continue.
22 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 | Edit Article
6 |
7 |
8 |
9 | Delete Article
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
102 |
--------------------------------------------------------------------------------
/src/components/ArticleList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Loading articles...
4 |
5 |
6 | No articles are here... yet.
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
126 |
--------------------------------------------------------------------------------
/src/components/ArticleMeta.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
13 | {{ article.author.username }}
14 |
15 | {{ article.createdAt | date }}
16 |
17 |
22 |
31 |
32 | {{ article.favoritesCount }}
33 |
34 |
35 |
36 |
37 |
79 |
--------------------------------------------------------------------------------
/src/components/Comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ comment.body }}
5 |
6 |
21 |
22 |
23 |
24 |
50 |
--------------------------------------------------------------------------------
/src/components/CommentEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 |
21 |
22 |
55 |
--------------------------------------------------------------------------------
/src/components/ListErrors.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
18 |
--------------------------------------------------------------------------------
/src/components/TagList.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
21 |
--------------------------------------------------------------------------------
/src/components/TheFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | conduit
6 |
7 |
8 | An interactive learning project from
9 | Thinkster . Code & design licensed under MIT.
12 |
13 |
14 |
15 |
16 |
17 |
22 |
--------------------------------------------------------------------------------
/src/components/TheHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | conduit
6 |
7 |
8 |
9 |
15 | Home
16 |
17 |
18 |
19 |
25 | Sign in
26 |
27 |
28 |
29 |
35 | Sign up
36 |
37 |
38 |
39 |
40 |
41 |
47 | Home
48 |
49 |
50 |
51 |
56 | New Article
57 |
58 |
59 |
60 |
66 | Settings
67 |
68 |
69 |
70 |
79 | {{ currentUser.username }}
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
97 |
--------------------------------------------------------------------------------
/src/components/VArticlePreview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Read more...
8 |
9 |
10 |
11 |
12 |
13 |
38 |
--------------------------------------------------------------------------------
/src/components/VPagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
17 |
44 |
--------------------------------------------------------------------------------
/src/components/VTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
--------------------------------------------------------------------------------
/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, slug) {
67 | const { data } = await FavoriteService.add(slug);
68 | context.commit(UPDATE_ARTICLE_IN_LIST, data.article, { root: true });
69 | context.commit(SET_ARTICLE, data.article);
70 | },
71 | async [FAVORITE_REMOVE](context, slug) {
72 | const { data } = await FavoriteService.remove(slug);
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 = "updateArticleInList";
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 |
2 |
3 |
4 |
5 |
{{ article.title }}
6 |
7 |
8 |
9 |
10 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
35 |
36 | Sign in
37 | or
38 | sign up
39 | to add comments on this article.
40 |
41 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
96 |
--------------------------------------------------------------------------------
/src/views/ArticleEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
66 |
67 |
68 |
151 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
conduit
6 |
A place to share your knowledge.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 | Your Feed
21 |
22 |
23 |
24 |
30 | Global Feed
31 |
32 |
33 |
34 |
39 | {{ tag }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
81 |
--------------------------------------------------------------------------------
/src/views/HomeGlobal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
--------------------------------------------------------------------------------
/src/views/HomeMyFeed.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
--------------------------------------------------------------------------------
/src/views/HomeTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
--------------------------------------------------------------------------------
/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Sign in
7 |
8 |
9 | Need an account?
10 |
11 |
12 |
13 | {{ k }} {{ v | error }}
14 |
15 |
16 |
17 |
23 |
24 |
25 |
31 |
32 |
33 | Sign in
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
68 |
--------------------------------------------------------------------------------
/src/views/Profile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
{{ profile.username }}
9 |
{{ profile.bio }}
10 |
11 |
15 | Edit Profile Settings
16 |
17 |
18 |
19 |
24 | Unfollow
25 | {{ profile.username }}
26 |
27 |
32 | Follow
33 | {{ profile.username }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
53 | My Articles
54 |
55 |
56 |
57 |
63 | Favorited Articles
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
113 |
--------------------------------------------------------------------------------
/src/views/ProfileArticles.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
22 |
--------------------------------------------------------------------------------
/src/views/ProfileFavorited.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
23 |
--------------------------------------------------------------------------------
/src/views/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
48 |
49 |
50 |
81 |
--------------------------------------------------------------------------------
/src/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
63 |
64 |
65 |
89 |
--------------------------------------------------------------------------------
/static/rwv-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/vue-realworld-example-app/3df3773b6be7181aadb513572458fba049fe66f9/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/components/VTag.spec.js:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from "@vue/test-utils";
2 |
3 | import router from "../../../src/router/index";
4 | import VTag from "../../../src/components/VTag";
5 |
6 | const localVue = createLocalVue();
7 | const createWrapper = () => {
8 | return mount(VTag, {
9 | localVue,
10 | router,
11 | propsData: {
12 | name: "Foo"
13 | }
14 | });
15 | };
16 |
17 | describe("VTag", () => {
18 | it("should update the route on click", async () => {
19 | const wrapper = createWrapper();
20 | const routerBefore = wrapper.vm.$route.path;
21 | wrapper.find("a").trigger("click");
22 | await localVue.nextTick();
23 | expect(wrapper.vm.$route.path).not.toBe(routerBefore);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/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("updateArticleInList");
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("updateArticleInList");
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 |
--------------------------------------------------------------------------------