-
3.browserslistrc
-
9.editorconfig
-
14.eslintrc.js
-
24.gitignore
-
4.prettierrc
-
21LICENSE
-
10babel.config.js
-
18jest.config.js
-
54package.json
-
5postcss.config.js
-
BINpublic/favicon.ico
-
BINpublic/img/icons/android-chrome-192x192.png
-
BINpublic/img/icons/android-chrome-512x512.png
-
BINpublic/img/icons/apple-touch-icon-120x120.png
-
BINpublic/img/icons/apple-touch-icon-152x152.png
-
BINpublic/img/icons/apple-touch-icon-180x180.png
-
BINpublic/img/icons/apple-touch-icon-60x60.png
-
BINpublic/img/icons/apple-touch-icon-76x76.png
-
BINpublic/img/icons/apple-touch-icon.png
-
BINpublic/img/icons/favicon-16x16.png
-
BINpublic/img/icons/favicon-32x32.png
-
BINpublic/img/icons/msapplication-icon-144x144.png
-
BINpublic/img/icons/mstile-150x150.png
-
149public/img/icons/safari-pinned-tab.svg
-
28public/index.html
-
20public/manifest.json
-
2public/robots.txt
-
22src/App.vue
-
108src/common/api.service.js
-
5src/common/config.js
-
5src/common/date.filter.js
-
3src/common/error.filter.js
-
15src/common/jwt.service.js
-
101src/components/ArticleActions.vue
-
125src/components/ArticleList.vue
-
78src/components/ArticleMeta.vue
-
49src/components/Comment.vue
-
54src/components/CommentEditor.vue
-
17src/components/ListErrors.vue
-
20src/components/TagList.vue
-
21src/components/TheFooter.vue
-
96src/components/TheHeader.vue
-
37src/components/VArticlePreview.vue
-
43src/components/VPagination.vue
-
22src/components/VTag.vue
-
27src/main.js
-
28src/registerServiceWorker.js
-
76src/router/index.js
-
22src/store/actions.type.js
-
132src/store/article.module.js
-
112src/store/auth.module.js
-
87src/store/home.module.js
-
18src/store/index.js
-
13src/store/mutations.type.js
-
74src/store/profile.module.js
-
45src/store/settings.module.js
-
95src/views/Article.vue
-
150src/views/ArticleEdit.vue
-
80src/views/Home.vue
-
13src/views/HomeGlobal.vue
-
14src/views/HomeMyFeed.vue
-
19src/views/HomeTag.vue
-
67src/views/Login.vue
-
112src/views/Profile.vue
-
21src/views/ProfileArticles.vue
-
22src/views/ProfileFavorited.vue
-
80src/views/Register.vue
-
88src/views/Settings.vue
-
BINstatic/rwv-logo.png
-
11322yarn.lock
@ -0,0 +1,3 @@ |
|||
> 1% |
|||
last 2 versions |
|||
not ie <= 8 |
|||
@ -0,0 +1,9 @@ |
|||
root = true |
|||
|
|||
[*] |
|||
charset = utf-8 |
|||
indent_style = space |
|||
indent_size = 2 |
|||
end_of_line = lf |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
@ -0,0 +1,14 @@ |
|||
module.exports = { |
|||
root: true, |
|||
env: { |
|||
node: true |
|||
}, |
|||
extends: ["plugin:vue/essential", "@vue/prettier"], |
|||
rules: { |
|||
"no-console": process.env.NODE_ENV === "production" ? "error" : "off", |
|||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" |
|||
}, |
|||
parserOptions: { |
|||
parser: "babel-eslint" |
|||
} |
|||
}; |
|||
@ -0,0 +1,24 @@ |
|||
.DS_Store |
|||
node_modules |
|||
/dist |
|||
|
|||
# local env files |
|||
.env.local |
|||
.env.*.local |
|||
|
|||
# Log files |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
|
|||
# Editor directories and files |
|||
.idea |
|||
.vscode |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw* |
|||
|
|||
# Coverage Reports |
|||
coverage |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"singleQuote": false, |
|||
"trailingComma": "none" |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) 2017-present all contributors listed here https://github.com/gothinkster/vue-realworld-example-app/graphs/contributors |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
@ -0,0 +1,10 @@ |
|||
module.exports = { |
|||
env: { |
|||
dev: { |
|||
presets: ["@vue/app"] |
|||
}, |
|||
test: { |
|||
presets: ["@babel/preset-env"] |
|||
} |
|||
} |
|||
}; |
|||
@ -0,0 +1,18 @@ |
|||
module.exports = { |
|||
moduleFileExtensions: ["js", "jsx", "json", "vue"], |
|||
transform: { |
|||
"^.+\\.vue$": "vue-jest", |
|||
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": |
|||
"jest-transform-stub", |
|||
"^.+\\.(js|jsx)?$": "babel-jest" |
|||
}, |
|||
moduleNameMapper: { |
|||
"^@/(.*)$": "<rootDir>/src/$1" |
|||
}, |
|||
snapshotSerializers: ["jest-serializer-vue"], |
|||
testMatch: [ |
|||
"<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))" |
|||
], |
|||
testURL: "http://localhost/", |
|||
transformIgnorePatterns: ["<rootDir>/node_modules/"] |
|||
}; |
|||
@ -0,0 +1,54 @@ |
|||
{ |
|||
"author": "Emmanuel Vilsbol <emmanuel@vilsbol.com>", |
|||
"dependencies": { |
|||
"axios": "^0.19.0", |
|||
"date-fns": "^1.30.1", |
|||
"marked": "^0.7.0", |
|||
"register-service-worker": "^1.6.2", |
|||
"vue": "^2.6.10", |
|||
"vue-axios": "^2.1.4", |
|||
"vue-router": "^3.1.3", |
|||
"vuex": "^3.1.1" |
|||
}, |
|||
"description": "TodoMVC for the RealWorld™", |
|||
"devDependencies": { |
|||
"@babel/core": "^7.6.2", |
|||
"@babel/preset-env": "^7.6.2", |
|||
"@vue/cli-plugin-babel": "^3.12.0", |
|||
"@vue/cli-plugin-eslint": "^3.10.0", |
|||
"@vue/cli-plugin-pwa": "^3.10.0", |
|||
"@vue/cli-plugin-unit-jest": "^3.10.0", |
|||
"@vue/cli-service": "^3.10.0", |
|||
"@vue/eslint-config-prettier": "^4.0.1", |
|||
"@vue/test-utils": "^1.0.0-beta.29", |
|||
"babel-core": "7.0.0-bridge.0", |
|||
"babel-jest": "^24.9.0", |
|||
"cross-env": "^6.0.3", |
|||
"lint-staged": "^8.2.1", |
|||
"node-sass": "^4.12.0", |
|||
"nyc": "^14.1.1", |
|||
"sass-loader": "^7.1.0", |
|||
"vue-template-compiler": "^2.6.10" |
|||
}, |
|||
"gitHooks": { |
|||
"pre-commit": "lint-staged" |
|||
}, |
|||
"lint-staged": { |
|||
"*.js": [ |
|||
"vue-cli-service lint", |
|||
"git add" |
|||
], |
|||
"*.vue": [ |
|||
"vue-cli-service lint", |
|||
"git add" |
|||
] |
|||
}, |
|||
"name": "realworld-vue", |
|||
"scripts": { |
|||
"build": "cross-env BABEL_ENV=dev vue-cli-service build", |
|||
"lint": "vue-cli-service lint", |
|||
"serve": "cross-env BABEL_ENV=dev vue-cli-service serve", |
|||
"test": "cross-env BABEL_ENV=test jest --coverage" |
|||
}, |
|||
"version": "0.1.0" |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
module.exports = { |
|||
plugins: { |
|||
autoprefixer: {} |
|||
} |
|||
}; |
|||
|
After Width: 192 | Height: 192 | Size: 9.2 KiB |
|
After Width: 512 | Height: 512 | Size: 29 KiB |
|
After Width: 120 | Height: 120 | Size: 3.3 KiB |
|
After Width: 152 | Height: 152 | Size: 4.0 KiB |
|
After Width: 180 | Height: 180 | Size: 4.6 KiB |
|
After Width: 60 | Height: 60 | Size: 1.5 KiB |
|
After Width: 76 | Height: 76 | Size: 1.8 KiB |
|
After Width: 180 | Height: 180 | Size: 4.6 KiB |
|
After Width: 16 | Height: 16 | Size: 799 B |
|
After Width: 32 | Height: 32 | Size: 1.2 KiB |
|
After Width: 144 | Height: 144 | Size: 1.1 KiB |
|
After Width: 270 | Height: 270 | Size: 4.2 KiB |
@ -0,0 +1,149 @@ |
|||
<?xml version="1.0" standalone="no"?> |
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" |
|||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> |
|||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" |
|||
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000" |
|||
preserveAspectRatio="xMidYMid meet"> |
|||
<metadata> |
|||
Created by potrace 1.11, written by Peter Selinger 2001-2013 |
|||
</metadata> |
|||
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)" |
|||
fill="#000000" stroke="none"> |
|||
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55 |
|||
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153 |
|||
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13 |
|||
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16 |
|||
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123 |
|||
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75 |
|||
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261 |
|||
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60 |
|||
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55 |
|||
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64 |
|||
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57 |
|||
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120 |
|||
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10 |
|||
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87 |
|||
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305 |
|||
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17 |
|||
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212 |
|||
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152 |
|||
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12 |
|||
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59 |
|||
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27 |
|||
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239 |
|||
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24 |
|||
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262 |
|||
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10 |
|||
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28 |
|||
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181 |
|||
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83 |
|||
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90 |
|||
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117 |
|||
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200 |
|||
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94 |
|||
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297 |
|||
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5 |
|||
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135 |
|||
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38 |
|||
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13 |
|||
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181 |
|||
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82 |
|||
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535 |
|||
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9 |
|||
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280 |
|||
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111 |
|||
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255 |
|||
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150 |
|||
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4 |
|||
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382 |
|||
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77 |
|||
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302 |
|||
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70 |
|||
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132 |
|||
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460 |
|||
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5 |
|||
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415 |
|||
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8 |
|||
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67 |
|||
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64 |
|||
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381 |
|||
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298 |
|||
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103 |
|||
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81 |
|||
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6 |
|||
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140 |
|||
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45 |
|||
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52 |
|||
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40 |
|||
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171 |
|||
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46 |
|||
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21 |
|||
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26 |
|||
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40 |
|||
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86 |
|||
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100 |
|||
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113 |
|||
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111 |
|||
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49 |
|||
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70 |
|||
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0 |
|||
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248 |
|||
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134 |
|||
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123 |
|||
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277 |
|||
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649 |
|||
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32 |
|||
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305 |
|||
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50 |
|||
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90 |
|||
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7 |
|||
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9 |
|||
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690 |
|||
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170 |
|||
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32 |
|||
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32 |
|||
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171 |
|||
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9 |
|||
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91 |
|||
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117 |
|||
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210 |
|||
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35 |
|||
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42 |
|||
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53 |
|||
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31 |
|||
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202 |
|||
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139 |
|||
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106 |
|||
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245 |
|||
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96 |
|||
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175 |
|||
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56 |
|||
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12 |
|||
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6 |
|||
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115 |
|||
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478 |
|||
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12 |
|||
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109 |
|||
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13 |
|||
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53 |
|||
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63 |
|||
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24 |
|||
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6 |
|||
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165 |
|||
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109 |
|||
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388 |
|||
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185 |
|||
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142 |
|||
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710 |
|||
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82 |
|||
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228 |
|||
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64 |
|||
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171 |
|||
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242 |
|||
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335 |
|||
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235 |
|||
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324 |
|||
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0 |
|||
-9615 0 20 -32z"/> |
|||
</g> |
|||
</svg> |
|||
@ -0,0 +1,28 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
|
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1.0"> |
|||
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> |
|||
<title>Conduit</title> |
|||
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on --> |
|||
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"> |
|||
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" |
|||
rel="stylesheet" type="text/css"> |
|||
<!-- Import the custom Bootstrap 4 theme from our hosted CDN --> |
|||
<link rel="stylesheet" href="https://demo.productionready.io/main.css"> |
|||
</head> |
|||
|
|||
<body> |
|||
<noscript> |
|||
<strong>We're sorry but vue-realworld-example-app doesn't work properly without JavaScript enabled. Please enable |
|||
it to continue.</strong> |
|||
</noscript> |
|||
<div id="app"></div> |
|||
<!-- built files will be auto injected --> |
|||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=default,Symbol "></script> |
|||
</body> |
|||
|
|||
</html> |
|||
@ -0,0 +1,20 @@ |
|||
{ |
|||
"name": "vue-realworld-example-app", |
|||
"short_name": "vue-realworld-example-app", |
|||
"icons": [ |
|||
{ |
|||
"src": "/img/icons/android-chrome-192x192.png", |
|||
"sizes": "192x192", |
|||
"type": "image/png" |
|||
}, |
|||
{ |
|||
"src": "/img/icons/android-chrome-512x512.png", |
|||
"sizes": "512x512", |
|||
"type": "image/png" |
|||
} |
|||
], |
|||
"start_url": "/index.html", |
|||
"display": "standalone", |
|||
"background_color": "#000000", |
|||
"theme_color": "#4DBA87" |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
User-agent: * |
|||
Disallow: |
|||
@ -0,0 +1,22 @@ |
|||
<template> |
|||
<div id="app"> |
|||
<RwvHeader /> |
|||
<router-view></router-view> |
|||
<RwvFooter /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RwvHeader from "@/components/TheHeader"; |
|||
import RwvFooter from "@/components/TheFooter"; |
|||
|
|||
export default { |
|||
name: "App", |
|||
components: { |
|||
RwvHeader, |
|||
RwvFooter |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style></style> |
|||
@ -0,0 +1,108 @@ |
|||
import Vue from "vue"; |
|||
import axios from "axios"; |
|||
import VueAxios from "vue-axios"; |
|||
import JwtService from "@/common/jwt.service"; |
|||
import { API_URL } from "@/common/config"; |
|||
|
|||
const ApiService = { |
|||
init() { |
|||
Vue.use(VueAxios, axios); |
|||
Vue.axios.defaults.baseURL = API_URL; |
|||
}, |
|||
|
|||
setHeader() { |
|||
Vue.axios.defaults.headers.common[ |
|||
"Authorization" |
|||
] = `Token ${JwtService.getToken()}`; |
|||
}, |
|||
|
|||
query(resource, params) { |
|||
return Vue.axios.get(resource, params).catch(error => { |
|||
throw new Error(`[RWV] ApiService ${error}`); |
|||
}); |
|||
}, |
|||
|
|||
get(resource, slug = "") { |
|||
return Vue.axios.get(`${resource}/${slug}`).catch(error => { |
|||
throw new Error(`[RWV] ApiService ${error}`); |
|||
}); |
|||
}, |
|||
|
|||
post(resource, params) { |
|||
return Vue.axios.post(`${resource}`, params); |
|||
}, |
|||
|
|||
update(resource, slug, params) { |
|||
return Vue.axios.put(`${resource}/${slug}`, params); |
|||
}, |
|||
|
|||
put(resource, params) { |
|||
return Vue.axios.put(`${resource}`, params); |
|||
}, |
|||
|
|||
delete(resource) { |
|||
return Vue.axios.delete(resource).catch(error => { |
|||
throw new Error(`[RWV] ApiService ${error}`); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
export default ApiService; |
|||
|
|||
export const TagsService = { |
|||
get() { |
|||
return ApiService.get("tags"); |
|||
} |
|||
}; |
|||
|
|||
export const ArticlesService = { |
|||
query(type, params) { |
|||
return ApiService.query("articles" + (type === "feed" ? "/feed" : ""), { |
|||
params: params |
|||
}); |
|||
}, |
|||
get(slug) { |
|||
return ApiService.get("articles", slug); |
|||
}, |
|||
create(params) { |
|||
delete params.author |
|||
return ApiService.post("articles", { article: params }); |
|||
}, |
|||
update(slug, params) { |
|||
delete params.author |
|||
return ApiService.update("articles", slug, { article: params }); |
|||
}, |
|||
destroy(slug) { |
|||
return ApiService.delete(`articles/${slug}`); |
|||
} |
|||
}; |
|||
|
|||
export const CommentsService = { |
|||
get(slug) { |
|||
if (typeof slug !== "string") { |
|||
throw new Error( |
|||
"[RWV] CommentsService.get() article slug required to fetch comments" |
|||
); |
|||
} |
|||
return ApiService.get("articles", `${slug}/comments`); |
|||
}, |
|||
|
|||
post(slug, payload) { |
|||
return ApiService.post(`articles/${slug}/comments`, { |
|||
comment: { body: payload } |
|||
}); |
|||
}, |
|||
|
|||
destroy(slug, commentId) { |
|||
return ApiService.delete(`articles/${slug}/comments/${commentId}`); |
|||
} |
|||
}; |
|||
|
|||
export const FavoriteService = { |
|||
add(slug) { |
|||
return ApiService.post(`articles/${slug}/favorite`); |
|||
}, |
|||
remove(slug) { |
|||
return ApiService.delete(`articles/${slug}/favorite`); |
|||
} |
|||
}; |
|||
@ -0,0 +1,5 @@ |
|||
export const API_URL = |
|||
process.env.NODE_ENV === "production" |
|||
? "https://tobedefined.io/api" |
|||
: "http://localhost:2020/api"; |
|||
export default API_URL; |
|||
@ -0,0 +1,5 @@ |
|||
import { default as format } from "date-fns/format"; |
|||
|
|||
export default date => { |
|||
return format(new Date(date), "MMMM D, YYYY"); |
|||
}; |
|||
@ -0,0 +1,3 @@ |
|||
export default errorValue => { |
|||
return `${errorValue[0]}`; |
|||
}; |
|||
@ -0,0 +1,15 @@ |
|||
const ID_TOKEN_KEY = "id_token"; |
|||
|
|||
export const getToken = () => { |
|||
return window.localStorage.getItem(ID_TOKEN_KEY); |
|||
}; |
|||
|
|||
export const saveToken = token => { |
|||
window.localStorage.setItem(ID_TOKEN_KEY, token); |
|||
}; |
|||
|
|||
export const destroyToken = () => { |
|||
window.localStorage.removeItem(ID_TOKEN_KEY); |
|||
}; |
|||
|
|||
export default { getToken, saveToken, destroyToken }; |
|||
@ -0,0 +1,101 @@ |
|||
<template> |
|||
<!-- Used when user is also author --> |
|||
<span v-if="canModify"> |
|||
<router-link class="btn btn-sm btn-outline-secondary" :to="editArticleLink"> |
|||
<i class="ion-edit"></i> <span> Edit Article</span> |
|||
</router-link> |
|||
<span> </span> |
|||
<button class="btn btn-outline-danger btn-sm" @click="deleteArticle"> |
|||
<i class="ion-trash-a"></i> <span> Delete Article</span> |
|||
</button> |
|||
</span> |
|||
<!-- Used in ArticleView when not author --> |
|||
<span v-else> |
|||
<button class="btn btn-sm btn-outline-secondary" @click="toggleFollow"> |
|||
<i class="ion-plus-round"></i> <span> </span> |
|||
<span v-text="followUserLabel" /> |
|||
</button> |
|||
<span> </span> |
|||
<button |
|||
class="btn btn-sm" |
|||
@click="toggleFavorite" |
|||
:class="toggleFavoriteButtonClasses" |
|||
> |
|||
<i class="ion-heart"></i> <span> </span> |
|||
<span v-text="favoriteArticleLabel" /> <span> </span> |
|||
<span class="counter" v-text="favoriteCounter" /> |
|||
</button> |
|||
</span> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import { |
|||
FAVORITE_ADD, |
|||
FAVORITE_REMOVE, |
|||
ARTICLE_DELETE, |
|||
FETCH_PROFILE_FOLLOW, |
|||
FETCH_PROFILE_UNFOLLOW |
|||
} from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvArticleActions", |
|||
props: { |
|||
article: { type: Object, required: true }, |
|||
canModify: { type: Boolean, required: true } |
|||
}, |
|||
computed: { |
|||
...mapGetters(["profile", "isAuthenticated"]), |
|||
editArticleLink() { |
|||
return { name: "article-edit", params: { slug: this.article.slug } }; |
|||
}, |
|||
toggleFavoriteButtonClasses() { |
|||
return { |
|||
"btn-primary": this.article.favorited, |
|||
"btn-outline-primary": !this.article.favorited |
|||
}; |
|||
}, |
|||
followUserLabel() { |
|||
return `${this.profile.following ? "Unfollow" : "Follow"} ${ |
|||
this.article.author.username |
|||
}`; |
|||
}, |
|||
favoriteArticleLabel() { |
|||
return this.article.favorited ? "Unfavorite Article" : "Favorite Article"; |
|||
}, |
|||
favoriteCounter() { |
|||
return `(${this.article.favoritesCount})`; |
|||
} |
|||
}, |
|||
methods: { |
|||
toggleFavorite() { |
|||
if (!this.isAuthenticated) { |
|||
this.$router.push({ name: "login" }); |
|||
return; |
|||
} |
|||
const action = this.article.favorited ? FAVORITE_REMOVE : FAVORITE_ADD; |
|||
this.$store.dispatch(action, this.article.slug); |
|||
}, |
|||
toggleFollow() { |
|||
if (!this.isAuthenticated) { |
|||
this.$router.push({ name: "login" }); |
|||
return; |
|||
} |
|||
const action = this.article.following |
|||
? FETCH_PROFILE_UNFOLLOW |
|||
: FETCH_PROFILE_FOLLOW; |
|||
this.$store.dispatch(action, { |
|||
username: this.profile.username |
|||
}); |
|||
}, |
|||
async deleteArticle() { |
|||
try { |
|||
await this.$store.dispatch(ARTICLE_DELETE, this.article.slug); |
|||
this.$router.push("/"); |
|||
} catch (err) { |
|||
console.error(err); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,125 @@ |
|||
<template> |
|||
<div> |
|||
<div v-if="isLoading" class="article-preview">Loading articles...</div> |
|||
<div v-else> |
|||
<div v-if="articles.length === 0" class="article-preview"> |
|||
No articles are here... yet. |
|||
</div> |
|||
<RwvArticlePreview |
|||
v-for="(article, index) in articles" |
|||
:article="article" |
|||
:key="article.title + index" |
|||
/> |
|||
<VPagination :pages="pages" :currentPage.sync="currentPage" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import RwvArticlePreview from "./VArticlePreview"; |
|||
import VPagination from "./VPagination"; |
|||
import { FETCH_ARTICLES } from "../store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvArticleList", |
|||
components: { |
|||
RwvArticlePreview, |
|||
VPagination |
|||
}, |
|||
props: { |
|||
type: { |
|||
type: String, |
|||
required: false, |
|||
default: "all" |
|||
}, |
|||
author: { |
|||
type: String, |
|||
required: false |
|||
}, |
|||
tag: { |
|||
type: String, |
|||
required: false |
|||
}, |
|||
favorited: { |
|||
type: String, |
|||
required: false |
|||
}, |
|||
itemsPerPage: { |
|||
type: Number, |
|||
required: false, |
|||
default: 10 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
currentPage: 1 |
|||
}; |
|||
}, |
|||
computed: { |
|||
listConfig() { |
|||
const { type } = this; |
|||
const filters = { |
|||
offset: (this.currentPage - 1) * this.itemsPerPage, |
|||
limit: this.itemsPerPage |
|||
}; |
|||
if (this.author) { |
|||
filters.author = this.author; |
|||
} |
|||
if (this.tag) { |
|||
filters.tag = this.tag; |
|||
} |
|||
if (this.favorited) { |
|||
filters.favorited = this.favorited; |
|||
} |
|||
return { |
|||
type, |
|||
filters |
|||
}; |
|||
}, |
|||
pages() { |
|||
if (this.isLoading || this.articlesCount <= this.itemsPerPage) { |
|||
return []; |
|||
} |
|||
return [ |
|||
...Array(Math.ceil(this.articlesCount / this.itemsPerPage)).keys() |
|||
].map(e => e + 1); |
|||
}, |
|||
...mapGetters(["articlesCount", "isLoading", "articles"]) |
|||
}, |
|||
watch: { |
|||
currentPage(newValue) { |
|||
this.listConfig.filters.offset = (newValue - 1) * this.itemsPerPage; |
|||
this.fetchArticles(); |
|||
}, |
|||
type() { |
|||
this.resetPagination(); |
|||
this.fetchArticles(); |
|||
}, |
|||
author() { |
|||
this.resetPagination(); |
|||
this.fetchArticles(); |
|||
}, |
|||
tag() { |
|||
this.resetPagination(); |
|||
this.fetchArticles(); |
|||
}, |
|||
favorited() { |
|||
this.resetPagination(); |
|||
this.fetchArticles(); |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.fetchArticles(); |
|||
}, |
|||
methods: { |
|||
fetchArticles() { |
|||
this.$store.dispatch(FETCH_ARTICLES, this.listConfig); |
|||
}, |
|||
resetPagination() { |
|||
this.listConfig.offset = 0; |
|||
this.currentPage = 1; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,78 @@ |
|||
<template> |
|||
<div class="article-meta"> |
|||
<router-link |
|||
:to="{ name: 'profile', params: { username: article.author.username } }" |
|||
> |
|||
<img :src="article.author.image" /> |
|||
</router-link> |
|||
<div class="info"> |
|||
<router-link |
|||
:to="{ name: 'profile', params: { username: article.author.username } }" |
|||
class="author" |
|||
> |
|||
{{ article.author.username }} |
|||
</router-link> |
|||
<span class="date">{{ article.createdAt | date }}</span> |
|||
</div> |
|||
<rwv-article-actions |
|||
v-if="actions" |
|||
:article="article" |
|||
:canModify="isCurrentUser()" |
|||
></rwv-article-actions> |
|||
<button |
|||
v-else |
|||
class="btn btn-sm pull-xs-right" |
|||
@click="toggleFavorite" |
|||
:class="{ |
|||
'btn-primary': article.favorited, |
|||
'btn-outline-primary': !article.favorited |
|||
}" |
|||
> |
|||
<i class="ion-heart"></i> |
|||
<span class="counter"> {{ article.favoritesCount }} </span> |
|||
</button> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import RwvArticleActions from "@/components/ArticleActions"; |
|||
import { FAVORITE_ADD, FAVORITE_REMOVE } from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvArticleMeta", |
|||
components: { |
|||
RwvArticleActions |
|||
}, |
|||
props: { |
|||
article: { |
|||
type: Object, |
|||
required: true |
|||
}, |
|||
actions: { |
|||
type: Boolean, |
|||
required: false, |
|||
default: false |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters(["currentUser", "isAuthenticated"]) |
|||
}, |
|||
methods: { |
|||
isCurrentUser() { |
|||
if (this.currentUser.username && this.article.author.username) { |
|||
return this.currentUser.username === this.article.author.username; |
|||
} |
|||
return false; |
|||
}, |
|||
toggleFavorite() { |
|||
if (!this.isAuthenticated) { |
|||
this.$router.push({ name: "login" }); |
|||
return; |
|||
} |
|||
const action = this.article.favorited ? FAVORITE_REMOVE : FAVORITE_ADD; |
|||
this.$store.dispatch(action, this.article.slug); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,49 @@ |
|||
<template> |
|||
<div class="card"> |
|||
<div class="card-block"> |
|||
<p class="card-text">{{ comment.body }}</p> |
|||
</div> |
|||
<div class="card-footer"> |
|||
<a href="" class="comment-author"> |
|||
<img :src="comment.author.image" class="comment-author-img" /> |
|||
</a> |
|||
<router-link |
|||
class="comment-author" |
|||
:to="{ name: 'profile', params: { username: comment.author.username } }" |
|||
> |
|||
{{ comment.author.username }} |
|||
</router-link> |
|||
<span class="date-posted">{{ comment.createdAt | date }}</span> |
|||
<span v-if="isCurrentUser" class="mod-options"> |
|||
<i class="ion-trash-a" @click="destroy(slug, comment.id)"></i> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import { COMMENT_DESTROY } from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvComment", |
|||
props: { |
|||
slug: { type: String, required: true }, |
|||
comment: { type: Object, required: true } |
|||
}, |
|||
computed: { |
|||
isCurrentUser() { |
|||
if (this.currentUser.username && this.comment.author.username) { |
|||
return this.comment.author.username === this.currentUser.username; |
|||
} |
|||
return false; |
|||
}, |
|||
...mapGetters(["currentUser"]) |
|||
}, |
|||
methods: { |
|||
destroy(slug, commentId) { |
|||
this.$store.dispatch(COMMENT_DESTROY, { slug, commentId }); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,54 @@ |
|||
<template> |
|||
<div> |
|||
<RwvListErrors :errors="errors" /> |
|||
<form class="card comment-form" @submit.prevent="onSubmit(slug, comment)"> |
|||
<div class="card-block"> |
|||
<textarea |
|||
class="form-control" |
|||
v-model="comment" |
|||
placeholder="Write a comment..." |
|||
rows="3" |
|||
> |
|||
</textarea> |
|||
</div> |
|||
<div class="card-footer"> |
|||
<img :src="userImage" class="comment-author-img" /> |
|||
<button class="btn btn-sm btn-primary">Post Comment</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RwvListErrors from "./ListErrors.vue"; |
|||
import { COMMENT_CREATE } from "../store/actions.type.js"; |
|||
|
|||
export default { |
|||
name: "RwvCommentEditor", |
|||
components: { RwvListErrors }, |
|||
props: { |
|||
slug: { type: String, required: true }, |
|||
content: { type: String, required: false }, |
|||
userImage: { type: String, required: false } |
|||
}, |
|||
data() { |
|||
return { |
|||
comment: this.content || null, |
|||
errors: {} |
|||
}; |
|||
}, |
|||
methods: { |
|||
onSubmit(slug, comment) { |
|||
this.$store |
|||
.dispatch(COMMENT_CREATE, { slug, comment }) |
|||
.then(() => { |
|||
this.comment = null; |
|||
this.errors = {}; |
|||
}) |
|||
.catch(({ response }) => { |
|||
this.errors = response.data.errors; |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,17 @@ |
|||
<template v-show="errors"> |
|||
<ul class="error-messages"> |
|||
<li v-for="(value, key) in errors" :key="key"> |
|||
<span v-text="key" /> |
|||
<span v-for="err in value" :key="err" v-text="err" /> |
|||
</li> |
|||
</ul> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "RwvListErorrs", |
|||
props: { |
|||
errors: { type: Object, required: true } |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,20 @@ |
|||
<template> |
|||
<ul class="tag-list"> |
|||
<li |
|||
class="tag-default tag-pill tag-outline" |
|||
v-for="(tag, index) of tags" |
|||
:key="index" |
|||
> |
|||
<span v-text="tag" /> |
|||
</li> |
|||
</ul> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "TagList", |
|||
props: { |
|||
tags: Array |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,21 @@ |
|||
<template> |
|||
<footer> |
|||
<div class="container"> |
|||
<router-link class="logo-font" :to="{ name: 'home', params: {} }"> |
|||
conduit |
|||
</router-link> |
|||
<span class="attribution"> |
|||
An interactive learning project from |
|||
<a rel="noopener noreferrer" target="blank" href="https://thinkster.io" |
|||
>Thinkster</a |
|||
>. Code & design licensed under MIT. |
|||
</span> |
|||
</div> |
|||
</footer> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "RwvFooter" |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,96 @@ |
|||
<template> |
|||
<nav class="navbar navbar-light"> |
|||
<div class="container"> |
|||
<router-link class="navbar-brand" :to="{ name: 'home' }"> |
|||
conduit |
|||
</router-link> |
|||
<ul v-if="!isAuthenticated" class="nav navbar-nav pull-xs-right"> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ name: 'home' }" |
|||
> |
|||
Home |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ name: 'login' }" |
|||
> |
|||
<i class="ion-compose"></i>Sign in |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ name: 'register' }" |
|||
> |
|||
<i class="ion-compose"></i>Sign up |
|||
</router-link> |
|||
</li> |
|||
</ul> |
|||
<ul v-else class="nav navbar-nav pull-xs-right"> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ name: 'home' }" |
|||
> |
|||
Home |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
:to="{ name: 'article-edit' }" |
|||
> |
|||
<i class="ion-compose"></i> New Article |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ name: 'settings' }" |
|||
> |
|||
<i class="ion-gear-a"></i> Settings |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item" v-if="currentUser.username"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ |
|||
name: 'profile', |
|||
params: { username: currentUser.username } |
|||
}" |
|||
> |
|||
{{ currentUser.username }} |
|||
</router-link> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</nav> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
|
|||
export default { |
|||
name: "RwvHeader", |
|||
computed: { |
|||
...mapGetters(["currentUser", "isAuthenticated"]) |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,37 @@ |
|||
<template> |
|||
<div class="article-preview"> |
|||
<RwvArticleMeta isPreview :article="article" /> |
|||
<router-link :to="articleLink" class="preview-link"> |
|||
<h1 v-text="article.title" /> |
|||
<p v-text="article.description" /> |
|||
<span>Read more...</span> |
|||
<TagList :tags="article.tagList" /> |
|||
</router-link> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RwvArticleMeta from "./ArticleMeta"; |
|||
import TagList from "./TagList"; |
|||
|
|||
export default { |
|||
name: "RwvArticlePreview", |
|||
components: { |
|||
RwvArticleMeta, |
|||
TagList |
|||
}, |
|||
props: { |
|||
article: { type: Object, required: true } |
|||
}, |
|||
computed: { |
|||
articleLink() { |
|||
return { |
|||
name: "article", |
|||
params: { |
|||
slug: this.article.slug |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,43 @@ |
|||
<template> |
|||
<nav> |
|||
<ul class="pagination"> |
|||
<li |
|||
v-for="page in pages" |
|||
:data-test="`page-link-${page}`" |
|||
:key="page" |
|||
:class="paginationClass(page)" |
|||
@click.prevent="changePage(page)" |
|||
> |
|||
<a class="page-link" href v-text="page" /> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "Pagination", |
|||
props: { |
|||
pages: { |
|||
type: Array, |
|||
required: true |
|||
}, |
|||
currentPage: { |
|||
type: Number, |
|||
required: true |
|||
} |
|||
}, |
|||
methods: { |
|||
changePage(goToPage) { |
|||
if (goToPage === this.currentPage) return; |
|||
this.$emit("update:currentPage", goToPage); |
|||
}, |
|||
paginationClass(page) { |
|||
return { |
|||
"page-item": true, |
|||
active: this.currentPage === page |
|||
}; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,22 @@ |
|||
<template> |
|||
<router-link :to="homeRoute" :class="className" v-text="name"></router-link> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "RwvTag", |
|||
props: { |
|||
name: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
className: { |
|||
type: String, |
|||
default: "tag-pill tag-default" |
|||
} |
|||
}, |
|||
computed: { |
|||
homeRoute: () => ({ name: "home-tag", params: { tag: name } }) |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,27 @@ |
|||
import Vue from "vue"; |
|||
import App from "./App.vue"; |
|||
import router from "./router"; |
|||
import store from "./store"; |
|||
import "./registerServiceWorker"; |
|||
|
|||
import { CHECK_AUTH } from "./store/actions.type"; |
|||
import ApiService from "./common/api.service"; |
|||
import DateFilter from "./common/date.filter"; |
|||
import ErrorFilter from "./common/error.filter"; |
|||
|
|||
Vue.config.productionTip = false; |
|||
Vue.filter("date", DateFilter); |
|||
Vue.filter("error", ErrorFilter); |
|||
|
|||
ApiService.init(); |
|||
|
|||
// Ensure we checked auth before each page load.
|
|||
router.beforeEach((to, from, next) => |
|||
Promise.all([store.dispatch(CHECK_AUTH)]).then(next) |
|||
); |
|||
|
|||
new Vue({ |
|||
router, |
|||
store, |
|||
render: h => h(App) |
|||
}).$mount("#app"); |
|||
@ -0,0 +1,28 @@ |
|||
/* eslint-disable no-console */ |
|||
|
|||
import { register } from "register-service-worker"; |
|||
|
|||
if (process.env.NODE_ENV === "production") { |
|||
register(`${process.env.BASE_URL}service-worker.js`, { |
|||
ready() { |
|||
console.log( |
|||
"App is being served from cache by a service worker.\n" + |
|||
"For more details, visit https://goo.gl/AFskqB" |
|||
); |
|||
}, |
|||
cached() { |
|||
console.log("Content has been cached for offline use."); |
|||
}, |
|||
updated() { |
|||
console.log("New content is available; please refresh."); |
|||
}, |
|||
offline() { |
|||
console.log( |
|||
"No internet connection found. App is running in offline mode." |
|||
); |
|||
}, |
|||
error(error) { |
|||
console.error("Error during service worker registration:", error); |
|||
} |
|||
}); |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
import Vue from "vue"; |
|||
import Router from "vue-router"; |
|||
|
|||
Vue.use(Router); |
|||
|
|||
export default new Router({ |
|||
routes: [ |
|||
{ |
|||
path: "/", |
|||
component: () => import("@/views/Home"), |
|||
children: [ |
|||
{ |
|||
path: "", |
|||
name: "home", |
|||
component: () => import("@/views/HomeGlobal") |
|||
}, |
|||
{ |
|||
path: "my-feed", |
|||
name: "home-my-feed", |
|||
component: () => import("@/views/HomeMyFeed") |
|||
}, |
|||
{ |
|||
path: "tag/:tag", |
|||
name: "home-tag", |
|||
component: () => import("@/views/HomeTag") |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
name: "login", |
|||
path: "/login", |
|||
component: () => import("@/views/Login") |
|||
}, |
|||
{ |
|||
name: "register", |
|||
path: "/register", |
|||
component: () => import("@/views/Register") |
|||
}, |
|||
{ |
|||
name: "settings", |
|||
path: "/settings", |
|||
component: () => import("@/views/Settings") |
|||
}, |
|||
// Handle child routes with a default, by giving the name to the
|
|||
// child.
|
|||
// SO: https://github.com/vuejs/vue-router/issues/777
|
|||
{ |
|||
path: "/@:username", |
|||
component: () => import("@/views/Profile"), |
|||
children: [ |
|||
{ |
|||
path: "", |
|||
name: "profile", |
|||
component: () => import("@/views/ProfileArticles") |
|||
}, |
|||
{ |
|||
name: "profile-favorites", |
|||
path: "favorites", |
|||
component: () => import("@/views/ProfileFavorited") |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
name: "article", |
|||
path: "/articles/:slug", |
|||
component: () => import("@/views/Article"), |
|||
props: true |
|||
}, |
|||
{ |
|||
name: "article-edit", |
|||
path: "/editor/:slug?", |
|||
props: true, |
|||
component: () => import("@/views/ArticleEdit") |
|||
} |
|||
] |
|||
}); |
|||
@ -0,0 +1,22 @@ |
|||
export const ARTICLE_PUBLISH = "publishArticle"; |
|||
export const ARTICLE_DELETE = "deleteArticle"; |
|||
export const ARTICLE_EDIT = "editArticle"; |
|||
export const ARTICLE_EDIT_ADD_TAG = "addTagToArticle"; |
|||
export const ARTICLE_EDIT_REMOVE_TAG = "removeTagFromArticle"; |
|||
export const ARTICLE_RESET_STATE = "resetArticleState"; |
|||
export const CHECK_AUTH = "checkAuth"; |
|||
export const COMMENT_CREATE = "createComment"; |
|||
export const COMMENT_DESTROY = "destroyComment"; |
|||
export const FAVORITE_ADD = "addFavorite"; |
|||
export const FAVORITE_REMOVE = "removeFavorite"; |
|||
export const FETCH_ARTICLE = "fetchArticle"; |
|||
export const FETCH_ARTICLES = "fetchArticles"; |
|||
export const FETCH_COMMENTS = "fetchComments"; |
|||
export const FETCH_PROFILE = "fetchProfile"; |
|||
export const FETCH_PROFILE_FOLLOW = "fetchProfileFollow"; |
|||
export const FETCH_PROFILE_UNFOLLOW = "fetchProfileUnfollow"; |
|||
export const FETCH_TAGS = "fetchTags"; |
|||
export const LOGIN = "login"; |
|||
export const LOGOUT = "logout"; |
|||
export const REGISTER = "register"; |
|||
export const UPDATE_USER = "updateUser"; |
|||
@ -0,0 +1,132 @@ |
|||
import Vue from "vue"; |
|||
import { |
|||
ArticlesService, |
|||
CommentsService, |
|||
FavoriteService |
|||
} from "@/common/api.service"; |
|||
import { |
|||
FETCH_ARTICLE, |
|||
FETCH_COMMENTS, |
|||
COMMENT_CREATE, |
|||
COMMENT_DESTROY, |
|||
FAVORITE_ADD, |
|||
FAVORITE_REMOVE, |
|||
ARTICLE_PUBLISH, |
|||
ARTICLE_EDIT, |
|||
ARTICLE_EDIT_ADD_TAG, |
|||
ARTICLE_EDIT_REMOVE_TAG, |
|||
ARTICLE_DELETE, |
|||
ARTICLE_RESET_STATE |
|||
} from "./actions.type"; |
|||
import { |
|||
RESET_STATE, |
|||
SET_ARTICLE, |
|||
SET_COMMENTS, |
|||
TAG_ADD, |
|||
TAG_REMOVE, |
|||
UPDATE_ARTICLE_IN_LIST |
|||
} from "./mutations.type"; |
|||
|
|||
const initialState = { |
|||
article: { |
|||
author: {}, |
|||
title: "", |
|||
description: "", |
|||
body: "", |
|||
tagList: [] |
|||
}, |
|||
comments: [] |
|||
}; |
|||
|
|||
export const state = { ...initialState }; |
|||
|
|||
export const actions = { |
|||
async [FETCH_ARTICLE](context, articleSlug, prevArticle) { |
|||
// avoid extronuous network call if article exists
|
|||
if (prevArticle !== undefined) { |
|||
return context.commit(SET_ARTICLE, prevArticle); |
|||
} |
|||
const { data } = await ArticlesService.get(articleSlug); |
|||
context.commit(SET_ARTICLE, data.article); |
|||
return data; |
|||
}, |
|||
async [FETCH_COMMENTS](context, articleSlug) { |
|||
const { data } = await CommentsService.get(articleSlug); |
|||
context.commit(SET_COMMENTS, data.comments); |
|||
return data.comments; |
|||
}, |
|||
async [COMMENT_CREATE](context, payload) { |
|||
await CommentsService.post(payload.slug, payload.comment); |
|||
context.dispatch(FETCH_COMMENTS, payload.slug); |
|||
}, |
|||
async [COMMENT_DESTROY](context, payload) { |
|||
await CommentsService.destroy(payload.slug, payload.commentId); |
|||
context.dispatch(FETCH_COMMENTS, payload.slug); |
|||
}, |
|||
async [FAVORITE_ADD](context, slug) { |
|||
const { data } = await FavoriteService.add(slug); |
|||
context.commit(UPDATE_ARTICLE_IN_LIST, data.article, { root: true }); |
|||
context.commit(SET_ARTICLE, data.article); |
|||
}, |
|||
async [FAVORITE_REMOVE](context, slug) { |
|||
const { data } = await FavoriteService.remove(slug); |
|||
// Update list as well. This allows us to favorite an article in the Home view.
|
|||
context.commit(UPDATE_ARTICLE_IN_LIST, data.article, { root: true }); |
|||
context.commit(SET_ARTICLE, data.article); |
|||
}, |
|||
[ARTICLE_PUBLISH]({ state }) { |
|||
return ArticlesService.create(state.article); |
|||
}, |
|||
[ARTICLE_DELETE](context, slug) { |
|||
return ArticlesService.destroy(slug); |
|||
}, |
|||
[ARTICLE_EDIT]({ state }) { |
|||
return ArticlesService.update(state.article.slug, state.article); |
|||
}, |
|||
[ARTICLE_EDIT_ADD_TAG](context, tag) { |
|||
context.commit(TAG_ADD, tag); |
|||
}, |
|||
[ARTICLE_EDIT_REMOVE_TAG](context, tag) { |
|||
context.commit(TAG_REMOVE, tag); |
|||
}, |
|||
[ARTICLE_RESET_STATE]({ commit }) { |
|||
commit(RESET_STATE); |
|||
} |
|||
}; |
|||
|
|||
/* eslint no-param-reassign: ["error", { "props": false }] */ |
|||
export const mutations = { |
|||
[SET_ARTICLE](state, article) { |
|||
state.article = article; |
|||
}, |
|||
[SET_COMMENTS](state, comments) { |
|||
state.comments = comments; |
|||
}, |
|||
[TAG_ADD](state, tag) { |
|||
state.article.tagList = state.article.tagList.concat([tag]); |
|||
}, |
|||
[TAG_REMOVE](state, tag) { |
|||
state.article.tagList = state.article.tagList.filter(t => t !== tag); |
|||
}, |
|||
[RESET_STATE]() { |
|||
for (let f in state) { |
|||
Vue.set(state, f, initialState[f]); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const getters = { |
|||
article(state) { |
|||
return state.article; |
|||
}, |
|||
comments(state) { |
|||
return state.comments; |
|||
} |
|||
}; |
|||
|
|||
export default { |
|||
state, |
|||
actions, |
|||
mutations, |
|||
getters |
|||
}; |
|||
@ -0,0 +1,112 @@ |
|||
import ApiService from "@/common/api.service"; |
|||
import JwtService from "@/common/jwt.service"; |
|||
import { |
|||
LOGIN, |
|||
LOGOUT, |
|||
REGISTER, |
|||
CHECK_AUTH, |
|||
UPDATE_USER |
|||
} from "./actions.type"; |
|||
import { SET_AUTH, PURGE_AUTH, SET_ERROR } from "./mutations.type"; |
|||
|
|||
const state = { |
|||
errors: null, |
|||
user: {}, |
|||
isAuthenticated: !!JwtService.getToken() |
|||
}; |
|||
|
|||
const getters = { |
|||
currentUser(state) { |
|||
return state.user; |
|||
}, |
|||
isAuthenticated(state) { |
|||
return state.isAuthenticated; |
|||
} |
|||
}; |
|||
|
|||
const actions = { |
|||
[LOGIN](context, credentials) { |
|||
return new Promise(resolve => { |
|||
ApiService.post("users/login", { user: credentials }) |
|||
.then(({ data }) => { |
|||
context.commit(SET_AUTH, data.user); |
|||
resolve(data); |
|||
}) |
|||
.catch(({ response }) => { |
|||
context.commit(SET_ERROR, response.data.errors); |
|||
}); |
|||
}); |
|||
}, |
|||
[LOGOUT](context) { |
|||
context.commit(PURGE_AUTH); |
|||
}, |
|||
[REGISTER](context, credentials) { |
|||
return new Promise((resolve, reject) => { |
|||
ApiService.post("users", { user: credentials }) |
|||
.then(({ data }) => { |
|||
context.commit(SET_AUTH, data.user); |
|||
resolve(data); |
|||
}) |
|||
.catch(({ response }) => { |
|||
context.commit(SET_ERROR, response.data.errors); |
|||
reject(response); |
|||
}); |
|||
}); |
|||
}, |
|||
[CHECK_AUTH](context) { |
|||
if (JwtService.getToken()) { |
|||
ApiService.setHeader(); |
|||
ApiService.get("user") |
|||
.then(({ data }) => { |
|||
context.commit(SET_AUTH, data.user); |
|||
}) |
|||
.catch(({ response }) => { |
|||
context.commit(SET_ERROR, response.data.errors); |
|||
}); |
|||
} else { |
|||
context.commit(PURGE_AUTH); |
|||
} |
|||
}, |
|||
[UPDATE_USER](context, payload) { |
|||
const { email, username, password, image, bio } = payload; |
|||
const user = { |
|||
email, |
|||
username, |
|||
bio, |
|||
image |
|||
}; |
|||
if (password) { |
|||
user.password = password; |
|||
} |
|||
|
|||
return ApiService.put("user", user).then(({ data }) => { |
|||
context.commit(SET_AUTH, data.user); |
|||
return data; |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
const mutations = { |
|||
[SET_ERROR](state, error) { |
|||
state.errors = error; |
|||
}, |
|||
[SET_AUTH](state, user) { |
|||
state.isAuthenticated = true; |
|||
state.user = user; |
|||
state.errors = {}; |
|||
JwtService.saveToken(state.user.token); |
|||
}, |
|||
[PURGE_AUTH](state) { |
|||
state.isAuthenticated = false; |
|||
state.user = {}; |
|||
state.errors = {}; |
|||
JwtService.destroyToken(); |
|||
} |
|||
}; |
|||
|
|||
export default { |
|||
state, |
|||
actions, |
|||
mutations, |
|||
getters |
|||
}; |
|||
@ -0,0 +1,87 @@ |
|||
import { TagsService, ArticlesService } from "@/common/api.service"; |
|||
import { FETCH_ARTICLES, FETCH_TAGS } from "./actions.type"; |
|||
import { |
|||
FETCH_START, |
|||
FETCH_END, |
|||
SET_TAGS, |
|||
UPDATE_ARTICLE_IN_LIST |
|||
} from "./mutations.type"; |
|||
|
|||
const state = { |
|||
tags: [], |
|||
articles: [], |
|||
isLoading: true, |
|||
articlesCount: 0 |
|||
}; |
|||
|
|||
const getters = { |
|||
articlesCount(state) { |
|||
return state.articlesCount; |
|||
}, |
|||
articles(state) { |
|||
return state.articles; |
|||
}, |
|||
isLoading(state) { |
|||
return state.isLoading; |
|||
}, |
|||
tags(state) { |
|||
return state.tags; |
|||
} |
|||
}; |
|||
|
|||
const actions = { |
|||
[FETCH_ARTICLES]({ commit }, params) { |
|||
commit(FETCH_START); |
|||
return ArticlesService.query(params.type, params.filters) |
|||
.then(({ data }) => { |
|||
commit(FETCH_END, data); |
|||
}) |
|||
.catch(error => { |
|||
throw new Error(error); |
|||
}); |
|||
}, |
|||
[FETCH_TAGS]({ commit }) { |
|||
return TagsService.get() |
|||
.then(({ data }) => { |
|||
commit(SET_TAGS, data.tags); |
|||
}) |
|||
.catch(error => { |
|||
throw new Error(error); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
/* eslint no-param-reassign: ["error", { "props": false }] */ |
|||
const mutations = { |
|||
[FETCH_START](state) { |
|||
state.isLoading = true; |
|||
}, |
|||
[FETCH_END](state, { articles, articlesCount }) { |
|||
state.articles = articles; |
|||
state.articlesCount = articlesCount; |
|||
state.isLoading = false; |
|||
}, |
|||
[SET_TAGS](state, tags) { |
|||
state.tags = tags; |
|||
}, |
|||
[UPDATE_ARTICLE_IN_LIST](state, data) { |
|||
state.articles = state.articles.map(article => { |
|||
if (article.slug !== data.slug) { |
|||
return article; |
|||
} |
|||
// We could just return data, but it seems dangerous to
|
|||
// mix the results of different api calls, so we
|
|||
// protect ourselves by copying the information.
|
|||
article.favorited = data.favorited; |
|||
article.favoritesCount = data.favoritesCount; |
|||
return article; |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
export default { |
|||
state, |
|||
getters, |
|||
actions, |
|||
mutations |
|||
}; |
|||
@ -0,0 +1,18 @@ |
|||
import Vue from "vue"; |
|||
import Vuex from "vuex"; |
|||
|
|||
import home from "./home.module"; |
|||
import auth from "./auth.module"; |
|||
import article from "./article.module"; |
|||
import profile from "./profile.module"; |
|||
|
|||
Vue.use(Vuex); |
|||
|
|||
export default new Vuex.Store({ |
|||
modules: { |
|||
home, |
|||
auth, |
|||
article, |
|||
profile |
|||
} |
|||
}); |
|||
@ -0,0 +1,13 @@ |
|||
export const FETCH_END = "setArticles"; |
|||
export const FETCH_START = "setLoading"; |
|||
export const PURGE_AUTH = "logOut"; |
|||
export const SET_ARTICLE = "setArticle"; |
|||
export const SET_AUTH = "setUser"; |
|||
export const SET_COMMENTS = "setComments"; |
|||
export const SET_ERROR = "setError"; |
|||
export const SET_PROFILE = "setProfile"; |
|||
export const SET_TAGS = "setTags"; |
|||
export const TAG_ADD = "addTag"; |
|||
export const TAG_REMOVE = "removeTag"; |
|||
export const UPDATE_ARTICLE_IN_LIST = "updateArticleInList"; |
|||
export const RESET_STATE = "resetModuleState"; |
|||
@ -0,0 +1,74 @@ |
|||
import ApiService from "@/common/api.service"; |
|||
import { |
|||
FETCH_PROFILE, |
|||
FETCH_PROFILE_FOLLOW, |
|||
FETCH_PROFILE_UNFOLLOW |
|||
} from "./actions.type"; |
|||
import { SET_PROFILE } from "./mutations.type"; |
|||
|
|||
const state = { |
|||
errors: {}, |
|||
profile: {} |
|||
}; |
|||
|
|||
const getters = { |
|||
profile(state) { |
|||
return state.profile; |
|||
} |
|||
}; |
|||
|
|||
const actions = { |
|||
[FETCH_PROFILE](context, payload) { |
|||
const { username } = payload; |
|||
return ApiService.get("profiles", username) |
|||
.then(({ data }) => { |
|||
context.commit(SET_PROFILE, data.profile); |
|||
return data; |
|||
}) |
|||
.catch(() => { |
|||
// #todo SET_ERROR cannot work in multiple states
|
|||
// context.commit(SET_ERROR, response.data.errors)
|
|||
}); |
|||
}, |
|||
[FETCH_PROFILE_FOLLOW](context, payload) { |
|||
const { username } = payload; |
|||
return ApiService.post(`profiles/${username}/follow`) |
|||
.then(({ data }) => { |
|||
context.commit(SET_PROFILE, data.profile); |
|||
return data; |
|||
}) |
|||
.catch(() => { |
|||
// #todo SET_ERROR cannot work in multiple states
|
|||
// context.commit(SET_ERROR, response.data.errors)
|
|||
}); |
|||
}, |
|||
[FETCH_PROFILE_UNFOLLOW](context, payload) { |
|||
const { username } = payload; |
|||
return ApiService.delete(`profiles/${username}/follow`) |
|||
.then(({ data }) => { |
|||
context.commit(SET_PROFILE, data.profile); |
|||
return data; |
|||
}) |
|||
.catch(() => { |
|||
// #todo SET_ERROR cannot work in multiple states
|
|||
// context.commit(SET_ERROR, response.data.errors)
|
|||
}); |
|||
} |
|||
}; |
|||
|
|||
const mutations = { |
|||
// [SET_ERROR] (state, error) {
|
|||
// state.errors = error
|
|||
// },
|
|||
[SET_PROFILE](state, profile) { |
|||
state.profile = profile; |
|||
state.errors = {}; |
|||
} |
|||
}; |
|||
|
|||
export default { |
|||
state, |
|||
actions, |
|||
mutations, |
|||
getters |
|||
}; |
|||
@ -0,0 +1,45 @@ |
|||
import { ArticlesService, CommentsService } from "@/common/api.service"; |
|||
import { FETCH_ARTICLE, FETCH_COMMENTS } from "./actions.type"; |
|||
import { SET_ARTICLE, SET_COMMENTS } from "./mutations.type"; |
|||
|
|||
export const state = { |
|||
article: {}, |
|||
comments: [] |
|||
}; |
|||
|
|||
export const actions = { |
|||
[FETCH_ARTICLE](context, articleSlug) { |
|||
return ArticlesService.get(articleSlug) |
|||
.then(({ data }) => { |
|||
context.commit(SET_ARTICLE, data.article); |
|||
}) |
|||
.catch(error => { |
|||
throw new Error(error); |
|||
}); |
|||
}, |
|||
[FETCH_COMMENTS](context, articleSlug) { |
|||
return CommentsService.get(articleSlug) |
|||
.then(({ data }) => { |
|||
context.commit(SET_COMMENTS, data.comments); |
|||
}) |
|||
.catch(error => { |
|||
throw new Error(error); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
/* eslint no-param-reassign: ["error", { "props": false }] */ |
|||
export const mutations = { |
|||
[SET_ARTICLE](state, article) { |
|||
state.article = article; |
|||
}, |
|||
[SET_COMMENTS](state, comments) { |
|||
state.comments = comments; |
|||
} |
|||
}; |
|||
|
|||
export default { |
|||
state, |
|||
actions, |
|||
mutations |
|||
}; |
|||
@ -0,0 +1,95 @@ |
|||
<template> |
|||
<div class="article-page"> |
|||
<div class="banner"> |
|||
<div class="container"> |
|||
<h1>{{ article.title }}</h1> |
|||
<RwvArticleMeta :article="article" :actions="true"></RwvArticleMeta> |
|||
</div> |
|||
</div> |
|||
<div class="container page"> |
|||
<div class="row article-content"> |
|||
<div class="col-xs-12"> |
|||
<div v-html="parseMarkdown(article.body)"></div> |
|||
<ul class="tag-list"> |
|||
<li v-for="(tag, index) of article.tagList" :key="tag + index"> |
|||
<RwvTag |
|||
:name="tag" |
|||
className="tag-default tag-pill tag-outline" |
|||
></RwvTag> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<hr /> |
|||
<div class="article-actions"> |
|||
<RwvArticleMeta :article="article" :actions="true"></RwvArticleMeta> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-xs-12 col-md-8 offset-md-2"> |
|||
<RwvCommentEditor |
|||
v-if="isAuthenticated" |
|||
:slug="slug" |
|||
:userImage="currentUser.image" |
|||
> |
|||
</RwvCommentEditor> |
|||
<p v-else> |
|||
<router-link :to="{ name: 'login' }">Sign in</router-link> |
|||
or |
|||
<router-link :to="{ name: 'register' }">sign up</router-link> |
|||
to add comments on this article. |
|||
</p> |
|||
<RwvComment |
|||
v-for="(comment, index) in comments" |
|||
:slug="slug" |
|||
:comment="comment" |
|||
:key="index" |
|||
> |
|||
</RwvComment> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import marked from "marked"; |
|||
import store from "@/store"; |
|||
import RwvArticleMeta from "@/components/ArticleMeta"; |
|||
import RwvComment from "@/components/Comment"; |
|||
import RwvCommentEditor from "@/components/CommentEditor"; |
|||
import RwvTag from "@/components/VTag"; |
|||
import { FETCH_ARTICLE, FETCH_COMMENTS } from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "rwv-article", |
|||
props: { |
|||
slug: { |
|||
type: String, |
|||
required: true |
|||
} |
|||
}, |
|||
components: { |
|||
RwvArticleMeta, |
|||
RwvComment, |
|||
RwvCommentEditor, |
|||
RwvTag |
|||
}, |
|||
beforeRouteEnter(to, from, next) { |
|||
Promise.all([ |
|||
store.dispatch(FETCH_ARTICLE, to.params.slug), |
|||
store.dispatch(FETCH_COMMENTS, to.params.slug) |
|||
]).then(() => { |
|||
next(); |
|||
}); |
|||
}, |
|||
computed: { |
|||
...mapGetters(["article", "currentUser", "comments", "isAuthenticated"]) |
|||
}, |
|||
methods: { |
|||
parseMarkdown(content) { |
|||
return marked(content); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,150 @@ |
|||
<template> |
|||
<div class="editor-page"> |
|||
<div class="container page"> |
|||
<div class="row"> |
|||
<div class="col-md-10 offset-md-1 col-xs-12"> |
|||
<RwvListErrors :errors="errors" /> |
|||
<form @submit.prevent="onPublish(article.slug)"> |
|||
<fieldset :disabled="inProgress"> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
type="text" |
|||
class="form-control form-control-lg" |
|||
v-model="article.title" |
|||
placeholder="Article Title" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
v-model="article.description" |
|||
placeholder="What's this article about?" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<textarea |
|||
class="form-control" |
|||
rows="8" |
|||
v-model="article.body" |
|||
placeholder="Write your article (in markdown)" |
|||
> |
|||
</textarea> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
placeholder="Enter tags" |
|||
v-model="tagInput" |
|||
@keypress.enter.prevent="addTag(tagInput)" |
|||
/> |
|||
<div class="tag-list"> |
|||
<span |
|||
class="tag-default tag-pill" |
|||
v-for="(tag, index) of article.tagList" |
|||
:key="tag + index" |
|||
> |
|||
<i class="ion-close-round" @click="removeTag(tag)"> </i> |
|||
{{ tag }} |
|||
</span> |
|||
</div> |
|||
</fieldset> |
|||
</fieldset> |
|||
<button |
|||
:disabled="inProgress" |
|||
class="btn btn-lg pull-xs-right btn-primary" |
|||
type="submit" |
|||
> |
|||
Publish Article |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import store from "@/store"; |
|||
import RwvListErrors from "@/components/ListErrors"; |
|||
import { |
|||
ARTICLE_PUBLISH, |
|||
ARTICLE_EDIT, |
|||
FETCH_ARTICLE, |
|||
ARTICLE_EDIT_ADD_TAG, |
|||
ARTICLE_EDIT_REMOVE_TAG, |
|||
ARTICLE_RESET_STATE |
|||
} from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvArticleEdit", |
|||
components: { RwvListErrors }, |
|||
props: { |
|||
previousArticle: { |
|||
type: Object, |
|||
required: false |
|||
} |
|||
}, |
|||
async beforeRouteUpdate(to, from, next) { |
|||
// Reset state if user goes from /editor/:id to /editor |
|||
// The component is not recreated so we use to hook to reset the state. |
|||
await store.dispatch(ARTICLE_RESET_STATE); |
|||
return next(); |
|||
}, |
|||
async beforeRouteEnter(to, from, next) { |
|||
// SO: https://github.com/vuejs/vue-router/issues/1034 |
|||
// If we arrive directly to this url, we need to fetch the article |
|||
await store.dispatch(ARTICLE_RESET_STATE); |
|||
if (to.params.slug !== undefined) { |
|||
await store.dispatch( |
|||
FETCH_ARTICLE, |
|||
to.params.slug, |
|||
to.params.previousArticle |
|||
); |
|||
} |
|||
return next(); |
|||
}, |
|||
async beforeRouteLeave(to, from, next) { |
|||
await store.dispatch(ARTICLE_RESET_STATE); |
|||
next(); |
|||
}, |
|||
data() { |
|||
return { |
|||
tagInput: null, |
|||
inProgress: false, |
|||
errors: {} |
|||
}; |
|||
}, |
|||
computed: { |
|||
...mapGetters(["article"]) |
|||
}, |
|||
methods: { |
|||
onPublish(slug) { |
|||
let action = slug ? ARTICLE_EDIT : ARTICLE_PUBLISH; |
|||
this.inProgress = true; |
|||
this.$store |
|||
.dispatch(action) |
|||
.then(({ data }) => { |
|||
this.inProgress = false; |
|||
this.$router.push({ |
|||
name: "article", |
|||
params: { slug: data.article.slug } |
|||
}); |
|||
}) |
|||
.catch(({ response }) => { |
|||
this.inProgress = false; |
|||
this.errors = response.data.errors; |
|||
}); |
|||
}, |
|||
removeTag(tag) { |
|||
this.$store.dispatch(ARTICLE_EDIT_REMOVE_TAG, tag); |
|||
}, |
|||
addTag(tag) { |
|||
this.$store.dispatch(ARTICLE_EDIT_ADD_TAG, tag); |
|||
this.tagInput = null; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,80 @@ |
|||
<template> |
|||
<div class="home-page"> |
|||
<div class="banner"> |
|||
<div class="container"> |
|||
<h1 class="logo-font">conduit</h1> |
|||
<p>A place to share your knowledge.</p> |
|||
</div> |
|||
</div> |
|||
<div class="container page"> |
|||
<div class="row"> |
|||
<div class="col-md-9"> |
|||
<div class="feed-toggle"> |
|||
<ul class="nav nav-pills outline-active"> |
|||
<li v-if="isAuthenticated" class="nav-item"> |
|||
<router-link |
|||
:to="{ name: 'home-my-feed' }" |
|||
class="nav-link" |
|||
active-class="active" |
|||
> |
|||
Your Feed |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
:to="{ name: 'home' }" |
|||
exact |
|||
class="nav-link" |
|||
active-class="active" |
|||
> |
|||
Global Feed |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item" v-if="tag"> |
|||
<router-link |
|||
:to="{ name: 'home-tag', params: { tag } }" |
|||
class="nav-link" |
|||
active-class="active" |
|||
> |
|||
<i class="ion-pound"></i> {{ tag }} |
|||
</router-link> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<router-view></router-view> |
|||
</div> |
|||
<div class="col-md-3"> |
|||
<div class="sidebar"> |
|||
<p>Popular Tags</p> |
|||
<div class="tag-list"> |
|||
<RwvTag v-for="(tag, index) in tags" :name="tag" :key="index"> |
|||
</RwvTag> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import RwvTag from "@/components/VTag"; |
|||
import { FETCH_TAGS } from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "home", |
|||
components: { |
|||
RwvTag |
|||
}, |
|||
mounted() { |
|||
this.$store.dispatch(FETCH_TAGS); |
|||
}, |
|||
computed: { |
|||
...mapGetters(["isAuthenticated", "tags"]), |
|||
tag() { |
|||
return this.$route.params.tag; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<div class="home-global"><RwvArticleList type="all" /></div> |
|||
</template> |
|||
<script> |
|||
import RwvArticleList from "@/components/ArticleList"; |
|||
|
|||
export default { |
|||
name: "rwv-home-global", |
|||
components: { |
|||
RwvArticleList |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,14 @@ |
|||
<template> |
|||
<div class="home-my-feed"><RwvArticleList type="feed" /></div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RwvArticleList from "@/components/ArticleList"; |
|||
|
|||
export default { |
|||
name: "rwv-home-my-feed", |
|||
components: { |
|||
RwvArticleList |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,19 @@ |
|||
<template> |
|||
<div class="home-tag"><RwvArticleList :tag="tag"></RwvArticleList></div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RwvArticleList from "@/components/ArticleList"; |
|||
|
|||
export default { |
|||
name: "RwvHomeTag", |
|||
components: { |
|||
RwvArticleList |
|||
}, |
|||
computed: { |
|||
tag() { |
|||
return this.$route.params.tag; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,67 @@ |
|||
<template> |
|||
<div class="auth-page"> |
|||
<div class="container page"> |
|||
<div class="row"> |
|||
<div class="col-md-6 offset-md-3 col-xs-12"> |
|||
<h1 class="text-xs-center">Sign in</h1> |
|||
<p class="text-xs-center"> |
|||
<router-link :to="{ name: 'register' }"> |
|||
Need an account? |
|||
</router-link> |
|||
</p> |
|||
<ul v-if="errors" class="error-messages"> |
|||
<li v-for="(v, k) in errors" :key="k">{{ k }} {{ v | error }}</li> |
|||
</ul> |
|||
<form @submit.prevent="onSubmit(email, password)"> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="text" |
|||
v-model="email" |
|||
placeholder="Email" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="password" |
|||
v-model="password" |
|||
placeholder="Password" |
|||
/> |
|||
</fieldset> |
|||
<button class="btn btn-lg btn-primary pull-xs-right"> |
|||
Sign in |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from "vuex"; |
|||
import { LOGIN } from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvLogin", |
|||
data() { |
|||
return { |
|||
email: null, |
|||
password: null |
|||
}; |
|||
}, |
|||
methods: { |
|||
onSubmit(email, password) { |
|||
this.$store |
|||
.dispatch(LOGIN, { email, password }) |
|||
.then(() => this.$router.push({ name: "home" })); |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapState({ |
|||
errors: state => state.auth.errors |
|||
}) |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,112 @@ |
|||
<template> |
|||
<div class="profile-page"> |
|||
<div class="user-info"> |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col-xs-12 col-md-10 offset-md-1"> |
|||
<img :src="profile.image" class="user-img" /> |
|||
<h4>{{ profile.username }}</h4> |
|||
<p>{{ profile.bio }}</p> |
|||
<div v-if="isCurrentUser()"> |
|||
<router-link |
|||
class="btn btn-sm btn-outline-secondary action-btn" |
|||
:to="{ name: 'settings' }" |
|||
> |
|||
<i class="ion-gear-a"></i> Edit Profile Settings |
|||
</router-link> |
|||
</div> |
|||
<div v-else> |
|||
<button |
|||
class="btn btn-sm btn-secondary action-btn" |
|||
v-if="profile.following" |
|||
@click.prevent="unfollow()" |
|||
> |
|||
<i class="ion-plus-round"></i> Unfollow |
|||
{{ profile.username }} |
|||
</button> |
|||
<button |
|||
class="btn btn-sm btn-outline-secondary action-btn" |
|||
v-if="!profile.following" |
|||
@click.prevent="follow()" |
|||
> |
|||
<i class="ion-plus-round"></i> Follow |
|||
{{ profile.username }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col-xs-12 col-md-10 offset-md-1"> |
|||
<div class="articles-toggle"> |
|||
<ul class="nav nav-pills outline-active"> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ name: 'profile' }" |
|||
> |
|||
My Articles |
|||
</router-link> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<router-link |
|||
class="nav-link" |
|||
active-class="active" |
|||
exact |
|||
:to="{ name: 'profile-favorites' }" |
|||
> |
|||
Favorited Articles |
|||
</router-link> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<router-view></router-view> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import { |
|||
FETCH_PROFILE, |
|||
FETCH_PROFILE_FOLLOW, |
|||
FETCH_PROFILE_UNFOLLOW |
|||
} from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvProfile", |
|||
mounted() { |
|||
this.$store.dispatch(FETCH_PROFILE, this.$route.params); |
|||
}, |
|||
computed: { |
|||
...mapGetters(["currentUser", "profile", "isAuthenticated"]) |
|||
}, |
|||
methods: { |
|||
isCurrentUser() { |
|||
if (this.currentUser.username && this.profile.username) { |
|||
return this.currentUser.username === this.profile.username; |
|||
} |
|||
return false; |
|||
}, |
|||
follow() { |
|||
if (!this.isAuthenticated) return; |
|||
this.$store.dispatch(FETCH_PROFILE_FOLLOW, this.$route.params); |
|||
}, |
|||
unfollow() { |
|||
this.$store.dispatch(FETCH_PROFILE_UNFOLLOW, this.$route.params); |
|||
} |
|||
}, |
|||
watch: { |
|||
$route(to) { |
|||
this.$store.dispatch(FETCH_PROFILE, to.params); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,21 @@ |
|||
<template> |
|||
<div class="profile-page"> |
|||
<RwvArticleList :author="author" :items-per-page="5"></RwvArticleList> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RwvArticleList from "@/components/ArticleList"; |
|||
|
|||
export default { |
|||
name: "RwvProfileArticles", |
|||
components: { |
|||
RwvArticleList |
|||
}, |
|||
computed: { |
|||
author() { |
|||
return this.$route.params.username; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,22 @@ |
|||
<template> |
|||
<div class="profile-page"> |
|||
<RwvArticleList :favorited="favorited" :items-per-page="5"> |
|||
</RwvArticleList> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RwvArticleList from "@/components/ArticleList"; |
|||
|
|||
export default { |
|||
name: "RwvProfileFavorited", |
|||
components: { |
|||
RwvArticleList |
|||
}, |
|||
computed: { |
|||
favorited() { |
|||
return this.$route.params.username; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,80 @@ |
|||
<template> |
|||
<div class="auth-page"> |
|||
<div class="container page"> |
|||
<div class="row"> |
|||
<div class="col-md-6 offset-md-3 col-xs-12"> |
|||
<h1 class="text-xs-center">Sign up</h1> |
|||
<p class="text-xs-center"> |
|||
<router-link :to="{ name: 'login' }"> |
|||
Have an account? |
|||
</router-link> |
|||
</p> |
|||
<ul v-if="errors" class="error-messages"> |
|||
<li v-for="(v, k) in errors" :key="k">{{ k }} {{ v | error }}</li> |
|||
</ul> |
|||
<form @submit.prevent="onSubmit"> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="text" |
|||
v-model="username" |
|||
placeholder="Username" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="text" |
|||
v-model="email" |
|||
placeholder="Email" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="password" |
|||
v-model="password" |
|||
placeholder="Password" |
|||
/> |
|||
</fieldset> |
|||
<button class="btn btn-lg btn-primary pull-xs-right"> |
|||
Sign up |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from "vuex"; |
|||
import { REGISTER } from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvRegister", |
|||
data() { |
|||
return { |
|||
username: "", |
|||
email: "", |
|||
password: "" |
|||
}; |
|||
}, |
|||
computed: { |
|||
...mapState({ |
|||
errors: state => state.auth.errors |
|||
}) |
|||
}, |
|||
methods: { |
|||
onSubmit() { |
|||
this.$store |
|||
.dispatch(REGISTER, { |
|||
email: this.email, |
|||
password: this.password, |
|||
username: this.username |
|||
}) |
|||
.then(() => this.$router.push({ name: "home" })); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,88 @@ |
|||
<template> |
|||
<div class="settings-page"> |
|||
<div class="container page"> |
|||
<div class="row"> |
|||
<div class="col-md-6 offset-md-3 col-xs-12"> |
|||
<h1 class="text-xs-center">Your Settings</h1> |
|||
<form @submit.prevent="updateSettings()"> |
|||
<fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control" |
|||
type="text" |
|||
v-model="currentUser.image" |
|||
placeholder="URL of profile picture" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="text" |
|||
v-model="currentUser.username" |
|||
placeholder="Your username" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<textarea |
|||
class="form-control form-control-lg" |
|||
rows="8" |
|||
v-model="currentUser.bio" |
|||
placeholder="Short bio about you" |
|||
></textarea> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="text" |
|||
v-model="currentUser.email" |
|||
placeholder="Email" |
|||
/> |
|||
</fieldset> |
|||
<fieldset class="form-group"> |
|||
<input |
|||
class="form-control form-control-lg" |
|||
type="password" |
|||
v-model="currentUser.password" |
|||
placeholder="Password" |
|||
/> |
|||
</fieldset> |
|||
<button class="btn btn-lg btn-primary pull-xs-right"> |
|||
Update Settings |
|||
</button> |
|||
</fieldset> |
|||
</form> |
|||
<!-- Line break for logout button --> |
|||
<hr /> |
|||
<button @click="logout" class="btn btn-outline-danger"> |
|||
Or click here to logout. |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
import { LOGOUT, UPDATE_USER } from "@/store/actions.type"; |
|||
|
|||
export default { |
|||
name: "RwvSettings", |
|||
computed: { |
|||
...mapGetters(["currentUser"]) |
|||
}, |
|||
methods: { |
|||
updateSettings() { |
|||
this.$store.dispatch(UPDATE_USER, this.currentUser).then(() => { |
|||
// #todo, nice toast and no redirect |
|||
this.$router.push({ name: "home" }); |
|||
}); |
|||
}, |
|||
logout() { |
|||
this.$store.dispatch(LOGOUT).then(() => { |
|||
this.$router.push({ name: "home" }); |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
After Width: 1669 | Height: 257 | Size: 36 KiB |