├── .gitignore ├── .browserslistrc ├── jsconfig.json ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── main.js ├── styles │ ├── todomvc-base.css │ └── todomvc-index.css └── App.vue ├── babel.config.js ├── .eslintrc.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-Pop/pwa-with-vue-3/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-Pop/pwa-with-vue-3/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Progressive Web Apps with Vue 3 2 | 3 | ## Project setup 4 | 5 | ``` 6 | yarn install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | yarn serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | yarn build 19 | ``` 20 | 21 | ### Lints and fixes files 22 | 23 | ``` 24 | yarn lint 25 | ``` 26 | 27 | ### Customize configuration 28 | 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-pwa-vue3", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@vue/component-compiler-utils": "^3.3.0", 12 | "core-js": "^3.6.5", 13 | "cross-spawn": "^7.0.6", 14 | "execa": "^9.5.2", 15 | "register-service-worker": "^1.7.1", 16 | "vue": "^3.0.0", 17 | "vue-loader": "^17.4.2" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^5.0.8", 21 | "@vue/cli-plugin-eslint": "^5.0.8", 22 | "@vue/cli-service": "^5.0.8", 23 | "@vue/compiler-sfc": "^3.0.0", 24 | "babel-eslint": "^10.1.0", 25 | "eslint": "^7.32.0", 26 | "eslint-plugin-vue": "^7.0.0" 27 | }, 28 | "resolutions": { 29 | "cross-spawn": "6.0.6", 30 | "postcss": "8.4.31", 31 | "yorkie/execa": "^5.1.1", 32 | "yorkie/cross-spawn": "6.0.6" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/todomvc-base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: 0.15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: 0.15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, 0.04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, 0.04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, 0.6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 200 | 201 | 218 | -------------------------------------------------------------------------------- /src/styles/todomvc-index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #4d4d4d; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); 49 | } 50 | 51 | .todoapp input::-webkit-input-placeholder { 52 | font-style: italic; 53 | font-weight: 300; 54 | color: #e6e6e6; 55 | } 56 | 57 | .todoapp input::-moz-placeholder { 58 | font-style: italic; 59 | font-weight: 300; 60 | color: #e6e6e6; 61 | } 62 | 63 | .todoapp input::input-placeholder { 64 | font-style: italic; 65 | font-weight: 300; 66 | color: #e6e6e6; 67 | } 68 | 69 | .todoapp h1 { 70 | position: absolute; 71 | top: -155px; 72 | width: 100%; 73 | font-size: 100px; 74 | font-weight: 100; 75 | text-align: center; 76 | color: rgba(175, 47, 47); 77 | -webkit-text-rendering: optimizeLegibility; 78 | -moz-text-rendering: optimizeLegibility; 79 | text-rendering: optimizeLegibility; 80 | } 81 | 82 | .new-todo, 83 | .edit { 84 | position: relative; 85 | margin: 0; 86 | width: 100%; 87 | font-size: 24px; 88 | font-family: inherit; 89 | font-weight: inherit; 90 | line-height: 1.4em; 91 | border: 0; 92 | color: inherit; 93 | padding: 6px; 94 | border: 1px solid #999; 95 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 96 | box-sizing: border-box; 97 | -webkit-font-smoothing: antialiased; 98 | -moz-osx-font-smoothing: grayscale; 99 | } 100 | 101 | .new-todo { 102 | padding: 16px 16px 16px 60px; 103 | border: none; 104 | background: rgba(0, 0, 0, 0.003); 105 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); 106 | } 107 | 108 | .main { 109 | position: relative; 110 | z-index: 2; 111 | border-top: 1px solid #e6e6e6; 112 | } 113 | 114 | .toggle-all { 115 | text-align: center; 116 | border: none; /* Mobile Safari */ 117 | opacity: 0; 118 | position: absolute; 119 | } 120 | 121 | .toggle-all + label { 122 | width: 60px; 123 | height: 34px; 124 | font-size: 0; 125 | position: absolute; 126 | top: -52px; 127 | left: -13px; 128 | -webkit-transform: rotate(90deg); 129 | transform: rotate(90deg); 130 | } 131 | 132 | .toggle-all + label:before { 133 | content: '❯'; 134 | font-size: 22px; 135 | color: #e6e6e6; 136 | padding: 10px 27px 10px 27px; 137 | } 138 | 139 | .toggle-all:checked + label:before { 140 | color: #737373; 141 | } 142 | 143 | .todo-list { 144 | margin: 0; 145 | padding: 0; 146 | list-style: none; 147 | } 148 | 149 | .todo-list li { 150 | position: relative; 151 | font-size: 24px; 152 | border-bottom: 1px solid #ededed; 153 | } 154 | 155 | .todo-list li:last-child { 156 | border-bottom: none; 157 | } 158 | 159 | .todo-list li.editing { 160 | border-bottom: none; 161 | padding: 0; 162 | } 163 | 164 | .todo-list li.editing .edit { 165 | display: block; 166 | width: 506px; 167 | padding: 12px 16px; 168 | margin: 0 0 0 43px; 169 | } 170 | 171 | .todo-list li.editing .view { 172 | display: none; 173 | } 174 | 175 | .todo-list li .toggle { 176 | text-align: center; 177 | width: 40px; 178 | /* auto, since non-WebKit browsers doesn't support input styling */ 179 | height: auto; 180 | position: absolute; 181 | top: 0; 182 | bottom: 0; 183 | left: 0; 184 | margin: auto 0; 185 | border: none; /* Mobile Safari */ 186 | -webkit-appearance: none; 187 | appearance: none; 188 | } 189 | 190 | .todo-list li .toggle { 191 | opacity: 0; 192 | } 193 | 194 | .todo-list li .toggle + label { 195 | /* 196 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 197 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 198 | */ 199 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 200 | background-repeat: no-repeat; 201 | background-position: center left; 202 | } 203 | 204 | .todo-list li .toggle:checked + label { 205 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 206 | } 207 | 208 | .todo-list li label { 209 | word-break: break-all; 210 | padding: 15px 15px 15px 60px; 211 | display: block; 212 | line-height: 1.2; 213 | transition: color 0.4s; 214 | } 215 | 216 | .todo-list li.completed label { 217 | color: #d9d9d9; 218 | text-decoration: line-through; 219 | } 220 | 221 | .todo-list li .destroy { 222 | display: none; 223 | position: absolute; 224 | top: 0; 225 | right: 10px; 226 | bottom: 0; 227 | width: 40px; 228 | height: 40px; 229 | margin: auto 0; 230 | font-size: 30px; 231 | color: #cc9a9a; 232 | margin-bottom: 11px; 233 | transition: color 0.2s ease-out; 234 | } 235 | 236 | .todo-list li .destroy:hover { 237 | color: #af5b5e; 238 | } 239 | 240 | .todo-list li .destroy:after { 241 | content: '×'; 242 | } 243 | 244 | .todo-list li:hover .destroy { 245 | display: block; 246 | } 247 | 248 | .todo-list li .edit { 249 | display: none; 250 | } 251 | 252 | .todo-list li.editing:last-child { 253 | margin-bottom: -1px; 254 | } 255 | 256 | .footer { 257 | display: flex; 258 | justify-content: space-between; 259 | align-items: center; 260 | color: #777; 261 | padding: 10px 15px; 262 | height: 20px; 263 | text-align: center; 264 | border-top: 1px solid #e6e6e6; 265 | } 266 | 267 | .footer:before { 268 | content: ''; 269 | position: absolute; 270 | right: 0; 271 | bottom: 0; 272 | left: 0; 273 | height: 50px; 274 | overflow: hidden; 275 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 276 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 277 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 278 | } 279 | 280 | .todo-count { 281 | float: left; 282 | text-align: left; 283 | } 284 | 285 | .todo-count strong { 286 | font-weight: 300; 287 | } 288 | 289 | .filters { 290 | margin: 0; 291 | padding: 0; 292 | list-style: none; 293 | position: absolute; 294 | right: 0; 295 | left: 0; 296 | } 297 | 298 | .filters li { 299 | display: inline; 300 | } 301 | 302 | .filters li button { 303 | color: inherit; 304 | margin: 3px; 305 | padding: 3px 7px; 306 | text-decoration: none; 307 | border: 1px solid transparent; 308 | border-radius: 3px; 309 | } 310 | 311 | .filters li button:hover { 312 | border-color: rgba(175, 47, 47, 0.1); 313 | } 314 | 315 | .filters li button.selected { 316 | border-color: rgba(175, 47, 47, 0.2); 317 | } 318 | 319 | .clear-completed, 320 | html .clear-completed:active { 321 | float: right; 322 | position: relative; 323 | line-height: 20px; 324 | text-decoration: none; 325 | cursor: pointer; 326 | } 327 | 328 | .clear-completed:hover { 329 | text-decoration: underline; 330 | } 331 | 332 | .info { 333 | margin: 65px auto 0; 334 | color: #bfbfbf; 335 | font-size: 10px; 336 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 337 | text-align: center; 338 | } 339 | 340 | .info p { 341 | line-height: 1; 342 | } 343 | 344 | .info a { 345 | color: inherit; 346 | text-decoration: none; 347 | font-weight: 400; 348 | } 349 | 350 | .info a:hover { 351 | text-decoration: underline; 352 | } 353 | 354 | /* 355 | Hack to remove background from Mobile Safari. 356 | Can't use it globally since it destroys checkboxes in Firefox 357 | */ 358 | @media screen and (-webkit-min-device-pixel-ratio: 0) { 359 | .toggle-all, 360 | .todo-list li .toggle { 361 | background: none; 362 | } 363 | 364 | .todo-list li .toggle { 365 | height: 40px; 366 | } 367 | } 368 | 369 | @media (max-width: 430px) { 370 | .footer { 371 | height: 50px; 372 | } 373 | 374 | .filters { 375 | bottom: 10px; 376 | } 377 | } 378 | --------------------------------------------------------------------------------