├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── App.vue ├── assets ├── images │ ├── cancel-icon.svg │ ├── edit-icon.svg │ ├── plus-icon.svg │ ├── redo-icon.svg │ ├── save-icon.svg │ ├── to-do-list-icon.svg │ ├── trash-icon.svg │ └── undo-icon.svg └── scss │ ├── common │ ├── button.scss │ ├── checkbox.scss │ ├── footer.scss │ ├── header.scss │ ├── input.scss │ └── modal.scss │ ├── components │ └── mini-note.scss │ ├── global.scss │ ├── mixins.scss │ ├── modals │ └── comfirm.scss │ ├── variables.scss │ └── views │ ├── home.scss │ ├── not-found.scss │ └── note.scss ├── components ├── MiniNote.vue ├── common │ ├── Button.vue │ ├── Footer.vue │ ├── Header.vue │ └── Modal.vue └── modals │ └── ConfirmModal.vue ├── main.js ├── router └── index.js ├── store ├── index.js └── types.js └── views ├── Home.vue ├── NotFound.vue └── Note.vue /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/airbnb' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'func-names': ['error', 'never'], 15 | 'arrow-parens': ['error', 'as-needed'], 16 | semi: ['error', 'never'], 17 | 'import/extensions': [2, 'never'], 18 | 'comma-dangle': ['error', 'never'], 19 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.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 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # test-todo 2 | 3 | Приложение состоит всего из 2х страниц. 4 | 5 | На главной странице отображается список всех заметок. 6 | Для каждой заметки отображается заголовок и Todo, сокращенный до нескольких пунктов, без возможности отмечать. 7 | Действия на главной: 8 | - перейти к созданию новой заметки 9 | - перейти к изменению 10 | - удалить (необходимо подтверждение) 11 | 12 | Страница изменения заметки позволяет определенную заметку отредактировать, отметить пункты Todo, а после сохранить изменения. 13 | Действия с заметкой: 14 | - сохранить изменения 15 | - отменить редактирование (необходимо подтверждение) 16 | - удалить (необходимо подтверждение) 17 | - отменить внесенное изменение 18 | - повторить отмененное изменение 19 | Действия с пунктами Todo: 20 | - добавить 21 | - удалить 22 | - отредактировать текст 23 | - отметить как выполненный 24 | 25 | Требования к функционалу: 26 | - Все действия на сайте должны происходить без перезагрузки страницы. 27 | - Подтверждение действий (удалить заметку) выполняется с помощью диалогового окна. 28 | - Интерфейс должен отвечать требованиям usability. 29 | - После перезагрузки страницы состояние списка заметок должно сохраняться. 30 | - Можно пренебречь несоответствием редактирования текста с помощью кнопок отменить/повторить и аналогичным действиям с помощью комбинаций клавиш (Ctrl+Z, Command+Z, etc.). 31 | 32 | Технические требования: 33 | - Диалоговые окна должны быть реализованы без использования "alert", "prompt" и "confirm". 34 | - В качестве языка разработки допускается использовать JavaScript или TypeScript. 35 | - В качестве сборщика, если это необходимо, используйте Webpack. 36 | - Верстка должна быть выполнена без использования UI библиотек (например Vuetify). 37 | - Адаптивность не обязательна, но приветствуется. 38 | - Логика приложения должна быть разбита на разумное количество самодостаточных Vue-компонентов. 39 | 40 | ## Project setup 41 | ``` 42 | npm install 43 | ``` 44 | 45 | ### Compiles and hot-reloads for development 46 | ``` 47 | npm run serve 48 | ``` 49 | 50 | ### Compiles and minifies for production 51 | ``` 52 | npm run build 53 | ``` 54 | 55 | ### Lints and fixes files 56 | ``` 57 | npm run lint 58 | ``` 59 | 60 | ### Customize configuration 61 | See [Configuration Reference](https://cli.vuejs.org/config/). 62 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-todo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.6.5", 13 | "debounce": "^1.2.0", 14 | "normalize.css": "^8.0.1", 15 | "uuid": "^8.2.0", 16 | "vue": "^2.6.11", 17 | "vue-router": "^3.2.0", 18 | "vuex": "^3.4.0" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~4.4.0", 22 | "@vue/cli-plugin-eslint": "~4.4.0", 23 | "@vue/cli-plugin-router": "~4.4.0", 24 | "@vue/cli-plugin-vuex": "~4.4.0", 25 | "@vue/cli-service": "~4.4.0", 26 | "@vue/eslint-config-airbnb": "^5.0.2", 27 | "babel-eslint": "^10.1.0", 28 | "eslint": "^6.7.2", 29 | "eslint-plugin-import": "^2.20.2", 30 | "eslint-plugin-vue": "^6.2.2", 31 | "node-sass": "^4.12.0", 32 | "sass-loader": "^8.0.2", 33 | "vue-template-compiler": "^2.6.11" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dDenysS/vue-todo/d99db9b8a1e7a351368f868d0130a3fdc8b988f3/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /src/assets/images/cancel-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/edit-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/images/plus-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/redo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/save-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/images/to-do-list-icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/images/trash-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/undo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/scss/common/button.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | @import "../mixins"; 3 | 4 | .button { 5 | display: flex; 6 | align-items: center; 7 | text-transform: uppercase; 8 | font-weight: bold; 9 | background-color: #212121; 10 | color: #b0b1aa; 11 | margin: 5px; 12 | padding: 5px 10px; 13 | border-radius: 10px; 14 | line-height: 1; 15 | outline: none; 16 | border: 1px solid transparent; 17 | 18 | &:hover { 19 | cursor: pointer; 20 | @include box-shadow(); 21 | } 22 | 23 | &:focus { 24 | border-color: $white; 25 | } 26 | 27 | &:active { 28 | opacity: .8; 29 | } 30 | 31 | &:disabled { 32 | opacity: .8; 33 | box-shadow: none; 34 | background-color: #ccc; 35 | } 36 | } 37 | 38 | .button__icon { 39 | width: 20px; 40 | margin:0 2.5px; 41 | height: auto; 42 | } 43 | 44 | .button__text { 45 | margin:0 2.5px; 46 | padding-top: 3px; 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/scss/common/checkbox.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .custom-checkbox { 4 | display: none; 5 | } 6 | 7 | .custom-checkbox + label { 8 | display: block; 9 | position: relative; 10 | width: 23px; 11 | min-width: 23px; 12 | height: 20px; 13 | font: 14px/20px "Open Sans", Arial, sans-serif; 14 | color: #ddd; 15 | cursor: pointer; 16 | -webkit-user-select: none; 17 | -moz-user-select: none; 18 | -ms-user-select: none; 19 | } 20 | 21 | .custom-checkbox:disabled + label { 22 | cursor: unset; 23 | } 24 | 25 | .custom-checkbox + label:last-child { 26 | margin-bottom: 0; 27 | } 28 | 29 | .custom-checkbox + label:before { 30 | content: ""; 31 | display: block; 32 | width: 20px; 33 | height: 20px; 34 | border: 1px solid #6cc0e5; 35 | position: absolute; 36 | left: 0; 37 | top: 0; 38 | opacity: .6; 39 | -webkit-transition: all .12s, border-color .08s; 40 | transition: all .12s, border-color .08s; 41 | } 42 | 43 | .custom-checkbox:checked + label:before { 44 | width: 10px; 45 | top: -5px; 46 | left: 5px; 47 | border-radius: 0; 48 | opacity: 1; 49 | border-top-color: transparent; 50 | border-left-color: transparent; 51 | -webkit-transform: rotate(45deg); 52 | transform: rotate(45deg); 53 | } 54 | -------------------------------------------------------------------------------- /src/assets/scss/common/footer.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins"; 2 | 3 | .footer { 4 | box-shadow: 0 -1px 10px 0 rgb(71, 71, 71); 5 | background-color: #272727; 6 | margin-top: 40px; 7 | } 8 | 9 | .footer__wrapper { 10 | display: flex; 11 | justify-content: space-between; 12 | flex-direction: column; 13 | align-items: center; 14 | 15 | @include media-tablet { 16 | flex-direction: row; 17 | } 18 | } 19 | 20 | .footer__paragraph { 21 | max-width: 300px; 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/scss/common/header.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | @import "../mixins"; 3 | 4 | .header { 5 | @include box-shadow(0, 1px); 6 | background-color: $black; 7 | } 8 | 9 | .logo { 10 | display: inline-flex; 11 | align-items: center; 12 | 13 | border-bottom: 2px solid transparent; 14 | 15 | &:hover { 16 | border-color: $white; 17 | } 18 | 19 | &:active { 20 | opacity: .7; 21 | } 22 | } 23 | 24 | .logo__icon { 25 | width: 40px; 26 | margin-right: 10px; 27 | } 28 | 29 | .logo__name { 30 | font-weight: bold; 31 | color: $white; 32 | font-size: 40px; 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/scss/common/input.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | 3 | .custom-input { 4 | margin: 5px; 5 | } 6 | 7 | .custom-input__description { 8 | margin-right: 15px; 9 | } 10 | 11 | .custom-input__field { 12 | border-width: 0; 13 | background-color: #302c2e; 14 | 15 | padding: 5px 5px 2px; 16 | color: #f5f5f5; 17 | 18 | border-bottom: 2px solid #3D3837; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/assets/scss/common/modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | 6 | width: 100vw; 7 | height: 100vh; 8 | 9 | background-color: rgba(0, 0, 0, .7); 10 | 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } 15 | 16 | .modal__content { 17 | width: 100%; 18 | border: 2px solid #262120; 19 | background-color: #3d3837; 20 | max-width: 600px; 21 | padding: 10px; 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/scss/components/mini-note.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | @import "../mixins"; 3 | 4 | .mini-note { 5 | display: flex; 6 | flex-direction: column; 7 | width: 100%; 8 | margin-bottom: 4%; 9 | margin-right: 2%; 10 | margin-left: 2%; 11 | padding: 15px 10px; 12 | background-color: $black; 13 | @include box-shadow(); 14 | 15 | @include media-tablet { 16 | max-width: 46%; 17 | } 18 | } 19 | 20 | .mini-note__title { 21 | margin: 0; 22 | } 23 | 24 | .mini-note__items { 25 | list-style: none; 26 | padding-left: 10px; 27 | position: relative; 28 | margin: 15px 0 10px; 29 | } 30 | 31 | .mini-note__items--more:before { 32 | content: ""; 33 | position: absolute; 34 | 35 | bottom: 0; 36 | left: 0; 37 | width: 100%; 38 | height: .1px; 39 | 40 | box-shadow: #d6bfbf -1px -2px 4px 0px; 41 | } 42 | 43 | .mini-note__item { 44 | display: flex; 45 | margin-bottom: 10px; 46 | } 47 | 48 | .mini-note__actions { 49 | margin-top: auto; 50 | display: flex; 51 | justify-content: flex-end; 52 | } 53 | -------------------------------------------------------------------------------- /src/assets/scss/global.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "~normalize.css/normalize"; 3 | @import "common/checkbox"; 4 | @import "common/input"; 5 | 6 | html { 7 | box-sizing: border-box; 8 | } 9 | 10 | *, *::before, *::after { 11 | box-sizing: inherit; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | background-color: #111; 17 | color: $white; 18 | font-family: Roboto, Arial, sans-serif; 19 | } 20 | 21 | img { 22 | max-width: 100%; 23 | height: auto; 24 | } 25 | 26 | a { 27 | text-decoration: none 28 | } 29 | 30 | #app { 31 | min-height: 100vh; 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: space-between; 35 | } 36 | 37 | .container { 38 | max-width: 1200px; 39 | width: 100%; 40 | margin: 0 auto; 41 | flex: 1; 42 | padding: 10px; 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | @mixin box-shadow($offsetX:0, $offsetY:0) { 4 | box-shadow: $offsetX $offsetY 10px 0 rgb(71, 71, 71); 5 | } 6 | 7 | @mixin media-tablet { 8 | @media (min-width: $mediaWidthTablet) { 9 | @content; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/scss/modals/comfirm.scss: -------------------------------------------------------------------------------- 1 | .confirm-modal__title { 2 | text-align: center; 3 | } 4 | 5 | .confirm-modal__actions { 6 | display: flex; 7 | justify-content: center; 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/scss/variables.scss: -------------------------------------------------------------------------------- 1 | $black: #272727; 2 | $white: #f5f5f5; 3 | 4 | // Mobile first 5 | $mediaWidthTablet: 768px; 6 | -------------------------------------------------------------------------------- /src/assets/scss/views/home.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins"; 2 | 3 | .head { 4 | display: flex; 5 | align-content: center; 6 | justify-content: space-between; 7 | flex-direction: column; 8 | margin-top: 20px; 9 | margin-bottom: 30px; 10 | 11 | @include media-tablet { 12 | flex-direction: row; 13 | } 14 | } 15 | 16 | .note-list { 17 | display: flex; 18 | flex-wrap: wrap; 19 | margin: 0 -2%; 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/scss/views/not-found.scss: -------------------------------------------------------------------------------- 1 | .not-found { 2 | overflow: hidden; 3 | position: relative; 4 | flex: 1; 5 | } 6 | 7 | .not-found__error { 8 | font-size: 95px; 9 | width: 100px; 10 | height: 60px; 11 | line-height: 60px; 12 | margin: auto; 13 | position: absolute; 14 | top: 0; 15 | bottom: 0; 16 | left: -60px; 17 | right: 0; 18 | animation: noise 2s linear infinite; 19 | } 20 | 21 | 22 | .not-found__error:after { 23 | content: "404"; 24 | font-size: 100px; 25 | font-style: italic; 26 | text-align: center; 27 | width: 150px; 28 | height: 60px; 29 | line-height: 60px; 30 | margin: auto; 31 | position: absolute; 32 | top: 0; 33 | bottom: 0; 34 | left: 0; 35 | right: 0; 36 | opacity: 0; 37 | color: blue; 38 | animation: noise-1 .2s linear infinite; 39 | } 40 | 41 | .not-found__error:before { 42 | content: "404"; 43 | font-size: 100px; 44 | font-style: italic; 45 | text-align: center; 46 | width: 100px; 47 | height: 60px; 48 | line-height: 60px; 49 | margin: auto; 50 | position: absolute; 51 | top: 0; 52 | bottom: 0; 53 | left: 0; 54 | right: 0; 55 | opacity: 0; 56 | color: red; 57 | animation: noise-2 .2s linear infinite; 58 | } 59 | 60 | .not-found__info { 61 | text-align: center; 62 | font-size: 15px; 63 | font-style: italic; 64 | width: 200px; 65 | height: 60px; 66 | line-height: 60px; 67 | margin: auto; 68 | position: absolute; 69 | top: 140px; 70 | bottom: 0; 71 | left: 0; 72 | right: 0; 73 | animation: noise-3 1s linear infinite; 74 | } 75 | 76 | @keyframes noise-1 { 77 | 0%, 20%, 40%, 60%, 70%, 90% { 78 | opacity: 0; 79 | } 80 | 10% { 81 | opacity: .1; 82 | } 83 | 50% { 84 | opacity: .5; 85 | left: -6px; 86 | } 87 | 80% { 88 | opacity: .3; 89 | } 90 | 100% { 91 | opacity: .6; 92 | left: 2px; 93 | } 94 | } 95 | 96 | @keyframes noise-2 { 97 | 0%, 20%, 40%, 60%, 70%, 90% { 98 | opacity: 0; 99 | } 100 | 10% { 101 | opacity: .1; 102 | } 103 | 50% { 104 | opacity: .5; 105 | left: 6px; 106 | } 107 | 80% { 108 | opacity: .3; 109 | } 110 | 100% { 111 | opacity: .6; 112 | left: -2px; 113 | } 114 | } 115 | 116 | @keyframes noise { 117 | 0%, 3%, 5%, 42%, 44%, 100% { 118 | opacity: 1; 119 | transform: scaleY(1); 120 | } 121 | 4.3% { 122 | opacity: 1; 123 | transform: scaleY(1.7); 124 | } 125 | 43% { 126 | opacity: 1; 127 | transform: scaleX(1.5); 128 | } 129 | } 130 | 131 | @keyframes noise-3 { 132 | 0%, 3%, 5%, 42%, 44%, 100% { 133 | opacity: 1; 134 | transform: scaleY(1); 135 | } 136 | 4.3% { 137 | opacity: 1; 138 | transform: scaleY(4); 139 | } 140 | 43% { 141 | opacity: 1; 142 | transform: scaleX(10) rotate(60deg); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/assets/scss/views/note.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | @import "../mixins"; 3 | 4 | .note { 5 | margin-top: 40px; 6 | } 7 | 8 | .note__wrapper { 9 | background-color: $black; 10 | @include box-shadow() 11 | } 12 | 13 | .todo { 14 | display: flex; 15 | align-items: center; 16 | margin: 5px 0; 17 | } 18 | 19 | .note__actions { 20 | margin-top: 30px; 21 | display: flex; 22 | align-items: center; 23 | 24 | flex-wrap: wrap; 25 | 26 | @include media-tablet { 27 | justify-content: flex-end; 28 | } 29 | } 30 | 31 | .note__action--add-task { 32 | margin-right: auto; 33 | } 34 | 35 | .note__action--mini { 36 | font-size: 14px; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/MiniNote.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /src/components/common/Button.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /src/components/common/Footer.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/components/common/Header.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /src/components/common/Modal.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/components/modals/ConfirmModal.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | 40 | 43 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import App from '@/App' 4 | 5 | import router from '@/router' 6 | import store from '@/store' 7 | 8 | import '@/assets/scss/global.scss' 9 | 10 | Vue.config.productionTip = false 11 | 12 | new Vue({ 13 | router, 14 | store, 15 | render: h => h(App) 16 | }).$mount('#app') 17 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import Home from '../views/Home' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | name: 'Home', 12 | component: Home 13 | }, 14 | { 15 | path: '/notes', 16 | redirect: { name: 'Home' } 17 | }, 18 | { 19 | path: '/notes/create', 20 | name: 'NoteCreate', 21 | component: () => import('../views/Note') 22 | }, 23 | { 24 | path: '/notes/:id', 25 | name: 'Note', 26 | component: () => import('../views/Note') 27 | }, 28 | { 29 | path: '/404', 30 | name: 'NotFound', 31 | component: () => import('../views/NotFound') 32 | }, 33 | { 34 | path: '*', 35 | redirect: { name: 'NotFound' } 36 | } 37 | ] 38 | 39 | const router = new VueRouter({ 40 | mode: 'history', 41 | base: process.env.BASE_URL, 42 | routes 43 | }) 44 | 45 | export default router 46 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import types from '@/store/types' 5 | 6 | Vue.use(Vuex) 7 | 8 | const notesLocalStorage = localStorage.getItem('notes') 9 | 10 | export default new Vuex.Store({ 11 | state: { 12 | notes: notesLocalStorage ? JSON.parse(notesLocalStorage) : [] 13 | }, 14 | getters: { 15 | getNoteById: state => id => state.notes.find(note => note.id === id) 16 | }, 17 | mutations: { 18 | [types.ADD_NOTE](state, { note }) { 19 | state.notes.push(note) 20 | localStorage.setItem('notes', JSON.stringify(state.notes)) 21 | }, 22 | [types.EDIT_NOTE](state, { note }) { 23 | const notes = state.notes.slice() 24 | const index = notes.findIndex(item => item.id === note.id) 25 | 26 | notes[index] = note 27 | 28 | state.notes = notes 29 | localStorage.setItem('notes', JSON.stringify(state.notes)) 30 | }, 31 | [types.REMOVE_NOTE](state, { id }) { 32 | state.notes = state.notes.filter(note => note.id !== id) 33 | localStorage.setItem('notes', JSON.stringify(state.notes)) 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /src/store/types.js: -------------------------------------------------------------------------------- 1 | const types = { 2 | ADD_NOTE: 'ADD_NOTE', 3 | EDIT_NOTE: 'EDIT_NOTE', 4 | REMOVE_NOTE: 'REMOVE_NOTE' 5 | } 6 | export default types 7 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /src/views/Note.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 217 | 218 | 221 | --------------------------------------------------------------------------------