├── .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 |
101 |
192 |
199 |
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 |
--------------------------------------------------------------------------------