├── .firebaserc ├── .gitignore ├── README.md ├── firebase.json └── public ├── css └── style.css ├── images ├── 144x144.png ├── 168x168.png ├── 192x192.png ├── 48x48.png ├── 72x72.png ├── 96x96.png ├── Home.svg ├── books.png ├── ic_refresh_white_24px.svg ├── profile.png ├── push-off.png └── push-on.png ├── index.html ├── js ├── app.js ├── latest.js ├── menu.js ├── notification.js ├── offline.js └── toast.js ├── latest.html ├── manifest.json └── sw.js /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ril-pwa" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Progressive Web App - GitHub Commits 2 | 3 | ![](https://img.shields.io/badge/unicodeveloper-approved-brightgreen.svg) 4 | 5 | This is the progressive web app that accompanies the series of `Introduction to Progressive Web Apps` Article on [auth0's blog](https://auth0.com/blog). 6 | 7 | ## Installation 8 | 9 | 1. Clone this repository: `git@github.com:unicodeveloper/pwa-commits.git pwa/` 10 | 2. `cd` into the `pwa` folder. 11 | 3. Run a local server like `http-server` and see the application served on `localhost:8080` 12 | 13 | 14 | ## Features 15 | 16 | 17 | - [x] - App Shell Architecture 18 | 19 | - [x] - Service Worker 20 | 21 | - [x] - Add to home screen 22 | 23 | - [x] - Fallback when offline 24 | 25 | - [x] - Online/Offline events 26 | 27 | - [x] - Fetch API 28 | 29 | - [x] - Push notification 30 | 31 | 32 | ## Tutorial Project 33 | 34 | This progressive web app was built for the sole purpose of: 35 | 36 | * [Introduction to Progressive Web Apps - Part 1](https://auth0.com/blog/introduction-to-progressive-apps-part-one) 37 | * [Introduction to Progressive Web Apps - Part 2](https://auth0.com/blog/introduction-to-progressive-web-apps-instant-loading-part-2) 38 | * [Introduction to Progressive Web Apps - Part 3](link-to-part-3) 39 | 40 | **Note:** You can learn from the codebase, but it's not advisable to just copy and paste everything into production because there are lots of edge cases that I didn't handle. 41 | 42 | ### License 43 | PWA-Commits is open-sourced software licensed under the [MIT license](https://github.com/unicodeveloper/pwa-api/blob/master/LICENSE) 44 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "public" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # BEM (BLOCK, ELEMENT, MODIFIER) METHEDOLOGY 4 | 5 |
6 |
7 |
8 | 9 |
10 |
11 | 12 | .card - BLOCK 13 | 14 | .card__title - ELEMENT 15 | 16 | .card--show - MODIFIER 17 | 18 | */ 19 | 20 | 21 | /* RESET styles */ 22 | 23 | *, 24 | *::after, 25 | *::before { 26 | box-sizing: border-box; 27 | } 28 | 29 | html, 30 | body, 31 | p { 32 | margin: 0; 33 | padding: 0; 34 | } 35 | 36 | a { 37 | text-decoration: none; 38 | color: inherit; 39 | } 40 | 41 | ul, 42 | li { 43 | list-style: none; 44 | padding: 0; 45 | margin: 0; 46 | } 47 | 48 | .no--select { 49 | -moz-user-select: none; 50 | -ms-user-select: none; 51 | -webkit-user-select: none; 52 | user-select: none; 53 | } 54 | 55 | h3 { 56 | text-align: left; 57 | margin-top: 20px; 58 | margin-bottom: 30px; 59 | font-weight: 500; 60 | } 61 | 62 | /* MAIN styles */ 63 | 64 | body { 65 | font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; 66 | font-size: 1rem; 67 | -webkit-font-smoothing: antialiased; 68 | -webkit-text-size-adjust: 100%; 69 | padding: 0; 70 | margin: 0; 71 | scroll-behavior: smooth; 72 | } 73 | 74 | .app-layout { 75 | position: absolute; 76 | top: 0; 77 | left: 0; 78 | right: 0; 79 | bottom: 0; 80 | width: 100%; 81 | height: 100%; 82 | } 83 | 84 | header { 85 | position: fixed; 86 | width: 100%; 87 | height: 56px; 88 | top: 0; 89 | background-color: #1E88E5; 90 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.19); 91 | display: -webkit-box; 92 | display: -ms-flexbox; 93 | display: flex; 94 | overflow: hidden; 95 | z-index: 1; 96 | color: #fff; 97 | -webkit-user-select: none; 98 | -moz-user-select: none; 99 | -ms-user-select: none; 100 | user-select: none; 101 | -webkit-transition: background-color 250ms linear; 102 | transition: background-color 250ms linear; 103 | } 104 | 105 | .app__offline { 106 | background-color: #6b6b6b; 107 | } 108 | 109 | .offline { 110 | background: #6b6b6b; 111 | } 112 | 113 | .container h4{ 114 | font-family: Consolas, "courier new"; 115 | } 116 | 117 | .header__icon { 118 | width: 48px; 119 | height: 48px; 120 | margin: 4px; 121 | display: -webkit-box; 122 | display: -ms-flexbox; 123 | display: flex; 124 | -webkit-box-align: center; 125 | -ms-flex-align: center; 126 | align-items: center; 127 | -webkit-box-pack: center; 128 | -ms-flex-pack: center; 129 | justify-content: center; 130 | cursor: pointer; 131 | } 132 | 133 | .header__icon:active { 134 | opacity: 0.8; 135 | outline: 1px solid #fff; 136 | } 137 | 138 | .header__title { 139 | color: #fff; 140 | font-size: 20px; 141 | -ms-flex-item-align: center; 142 | -ms-grid-row-align: center; 143 | align-self: center; 144 | margin-left: 10px; 145 | } 146 | 147 | .menu { 148 | width: 280px; 149 | height: 100%; 150 | background: #fff; 151 | position: fixed; 152 | top: 0; 153 | bottom: 0; 154 | box-shadow: 0px 0px 11px 0px rgba(0, 0, 0, 0.4); 155 | z-index: 1; 156 | -webkit-transition: transform 0.3s cubic-bezier(0, 0, 0.30, 1); 157 | -webkit-transition: -webkit-transform 0.3s cubic-bezier(0, 0, 0.30, 1); 158 | transition: -webkit-transform 0.3s cubic-bezier(0, 0, 0.30, 1); 159 | transition: transform 0.3s cubic-bezier(0, 0, 0.30, 1); 160 | transition: transform 0.3s cubic-bezier(0, 0, 0.30, 1), -webkit-transform 0.3s cubic-bezier(0, 0, 0.30, 1); 161 | -webkit-transform: translateX(-110%); 162 | transform: translateX(-110%); 163 | will-change: transform; 164 | z-index: 2; 165 | } 166 | 167 | .menu--show { 168 | -webkit-transform: translateX(0); 169 | transform: translateX(0); 170 | } 171 | 172 | .menu__overlay { 173 | width: 100%; 174 | height: 100%; 175 | position: fixed; 176 | top: 0; 177 | left: 0; 178 | right: 0; 179 | bottom: 0; 180 | background: rgba(0, 0, 0, 0.3); 181 | -webkit-transition: opacity 0.15s cubic-bezier(0, 0, 0.30, 1); 182 | transition: opacity 0.15s cubic-bezier(0, 0, 0.30, 1); 183 | visibility: hidden; 184 | opacity: 0; 185 | z-index: 1; 186 | } 187 | 188 | .menu__overlay--show { 189 | visibility: visible; 190 | opacity: 1; 191 | } 192 | 193 | .menu__header { 194 | height: 150px; 195 | background: #1E88E5; 196 | color: #fff; 197 | border-bottom: 1px solid #ddd; 198 | } 199 | 200 | .menu__list { 201 | width: inherit; 202 | height: inherit; 203 | overflow: auto; 204 | overflow-x: hidden; 205 | -webkit-overflow-scrolling: touch; 206 | } 207 | 208 | .menu__list li a { 209 | padding: 20px; 210 | color: rgba(0,0,0,0.87); 211 | cursor: pointer; 212 | display: block; 213 | } 214 | 215 | .menu__list li a:active, 216 | .menu__list li a:hover { 217 | background: #e7e7e7; 218 | } 219 | 220 | .app__content { 221 | width: 320px; 222 | height: 100%; 223 | margin: 0 auto; 224 | margin-top: 56px; 225 | padding-top: 10px; 226 | } 227 | 228 | .toast__container { 229 | position: fixed; 230 | bottom: 20px; 231 | left: 20px; 232 | pointer-events: none; 233 | } 234 | 235 | .profile-pic { 236 | width: 70%; 237 | max-width: 300px; 238 | display: block; 239 | margin: 0 auto; 240 | } 241 | 242 | .home-note { 243 | font-style: oblique; 244 | margin-top: 20px; 245 | padding: 12px 15px; 246 | } 247 | 248 | .toast__msg { 249 | width: 250px; 250 | min-height: 50px; 251 | background: rgba(0, 0, 0, 0.9); 252 | color: #fff; 253 | display: -webkit-box; 254 | display: -ms-flexbox; 255 | display: flex; 256 | -webkit-box-align: center; 257 | -ms-flex-align: center; 258 | align-items: center; 259 | -webkit-box-pack: justify; 260 | -ms-flex-pack: justify; 261 | justify-content: space-between; 262 | font-size: 14px; 263 | font-weight: 500; 264 | padding-left: 15px; 265 | padding-right: 10px; 266 | word-break: break-all; 267 | -webkit-transition: opacity 3s cubic-bezier(0, 0, 0.30, 1) 0; 268 | -webkit-transition: opacity 0.30s cubic-bezier(0, 0, 0.30, 1) 0; 269 | transition: opacity 0.30s cubic-bezier(0, 0, 0.30, 1) 0; 270 | text-transform: initial; 271 | margin-bottom: 10px; 272 | border-radius: 2px; 273 | } 274 | 275 | .toast__msg--hide { 276 | opacity: 0; 277 | } 278 | 279 | button { 280 | min-width: 60px; 281 | font-size: 14px; 282 | border: 0; 283 | background: #4F8EFA; 284 | color: #fff; 285 | border-radius: 2px; 286 | margin: 0 auto -5px; 287 | display: inline-block; 288 | cursor: pointer; 289 | outline: 0; 290 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.38); 291 | -webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.38); 292 | -moz-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.38); 293 | -o-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.38); 294 | -webkit-user-select: none; 295 | } 296 | 297 | button:active { 298 | box-shadow: none; 299 | } 300 | 301 | button:disabled { 302 | background: #ccc; 303 | color: #000; 304 | cursor: not-allowed; 305 | } 306 | 307 | .custom__button p { 308 | position: initial; 309 | margin: 0; 310 | padding-left: 10px; 311 | } 312 | 313 | .custom__button { 314 | padding: 10px 15px; 315 | font-family: 'Roboto', arial, sans-serif; 316 | text-align: left; 317 | } 318 | 319 | .turn-on-sync { 320 | min-width: 75px; 321 | height: 30px; 322 | margin-left: 10px; 323 | } 324 | 325 | .custom__input:checked + .custom__checkbox { 326 | background: rgb(195, 195, 195); 327 | } 328 | 329 | .custom__input:checked + .custom__checkbox::before { 330 | left: 25px; 331 | background: #0288d1; 332 | } 333 | 334 | .card__container { 335 | margin-top: 30px; 336 | display: -webkit-box; 337 | display: -ms-flexbox; 338 | display: flex; 339 | -webkit-box-orient: vertical; 340 | -webkit-box-direction: normal; 341 | -ms-flex-direction: column; 342 | flex-direction: column; 343 | } 344 | 345 | .card { 346 | width: 80%; 347 | min-height: 130px; 348 | background: #fff; 349 | margin: 20px auto; 350 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 351 | border-radius: 2px; 352 | position: relative; 353 | padding: 4px 1em; 354 | } 355 | 356 | .card a { 357 | color: red; 358 | cursor: pointer; 359 | } 360 | 361 | .card__title, 362 | .card__desc { 363 | display: block; 364 | font-style: italic; 365 | font-size: 14px; 366 | text-align: center; 367 | } 368 | 369 | .card__title { 370 | margin-left: 5px; 371 | font-weight: 500; 372 | } 373 | 374 | .card__temp { 375 | padding: 20px; 376 | padding-bottom: 10px; 377 | } 378 | 379 | .card__following, 380 | .card__followers { 381 | padding: 10px 20px 5px; 382 | } 383 | 384 | .card__desc { 385 | padding: 12px 15px; 386 | vertical-align: top; 387 | } 388 | 389 | .card__img { 390 | width: 60px; 391 | height: 60px; 392 | display: block; 393 | margin: 20px auto 10px; 394 | border-radius: 50%; 395 | } 396 | 397 | b { 398 | font-family: inherit; 399 | font-weight: 500; 400 | } 401 | 402 | .card b { 403 | margin-right: 5px; 404 | } 405 | 406 | .card__temp, 407 | .card__followers, 408 | .card__following { 409 | display: -webkit-box; 410 | display: -ms-flexbox; 411 | display: flex; 412 | -webkit-box-orient: horizontal; 413 | -webkit-box-direction: normal; 414 | -ms-flex-direction: row; 415 | flex-direction: row; 416 | margin-bottom: 5px; 417 | } 418 | 419 | .card__followers { 420 | margin-bottom: 20px; 421 | } 422 | 423 | .fab { 424 | width: 56px; 425 | height: 56px; 426 | background: #6b6b6b; 427 | border-radius: 50%; 428 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28); 429 | color: #fff; 430 | display: -webkit-box; 431 | display: -ms-flexbox; 432 | display: flex; 433 | -webkit-box-pack: center; 434 | -ms-flex-pack: center; 435 | justify-content: center; 436 | -webkit-box-align: center; 437 | -ms-flex-align: center; 438 | align-items: center; 439 | cursor: pointer; 440 | position: fixed; 441 | bottom: 0; 442 | right: 0; 443 | margin: 25px; 444 | -webkit-tap-highlight-color: transparent; 445 | -webkit-backface-visibility: hidden; 446 | backface-visibility: hidden; 447 | overflow: hidden; 448 | } 449 | 450 | .fab.active { 451 | background: #F44336; 452 | } 453 | 454 | .fab__ripple { 455 | position: absolute; 456 | left: -17px; 457 | bottom: -12px; 458 | width: 56px; 459 | height: 56px; 460 | -webkit-transform: scale(0.5); 461 | transform: scale(0.5); 462 | background: #fff; 463 | border-radius: 50%; 464 | -webkit-transform-origin: 50%; 465 | transform-origin: 50%; 466 | -webkit-transition: -webkit-transform 0.35s cubic-bezier(0, 0, 0.3, 1) 0ms; 467 | transition: -webkit-transform 0.35s cubic-bezier(0, 0, 0.3, 1) 0ms; 468 | transition: transform 0.35s cubic-bezier(0, 0, 0.3, 1) 0ms; 469 | transition: transform 0.35s cubic-bezier(0, 0, 0.3, 1) 0ms, -webkit-transform 0.35s cubic-bezier(0, 0, 0.3, 1) 0ms; 470 | -webkit-backface-visibility: hidden; 471 | backface-visibility: hidden; 472 | will-change: transform; 473 | z-index: 2; 474 | opacity: 0; 475 | -webkit-user-select: none; 476 | -moz-user-select: none; 477 | -ms-user-select: none; 478 | user-select: none; 479 | } 480 | 481 | .fab:active .fab__ripple { 482 | opacity: 0.2; 483 | -webkit-transform: scale(1) translate(31%, -22%); 484 | transform: scale(1) translate(31%, -22%); 485 | } 486 | 487 | .fab__image { 488 | overflow: hidden; 489 | z-index: 3; 490 | } 491 | 492 | .add__card { 493 | margin: 40px auto; 494 | text-align: center; 495 | } 496 | 497 | .add__input { 498 | width: 170px; 499 | height: 35px; 500 | border: 1px solid #ccc; 501 | padding-left: 10px; 502 | font-size: 13px; 503 | display: block; 504 | margin: 10px auto; 505 | } 506 | 507 | .add__btn { 508 | height: 34px; 509 | min-width: 70px; 510 | margin-top: 10px; 511 | display: block; 512 | margin-left: 0; 513 | } 514 | 515 | .add__card ul, 516 | .add__card li, 517 | .share__container li { 518 | width: 280px; 519 | text-align: left; 520 | margin: 15px auto; 521 | } 522 | 523 | .add__card p { 524 | font-weight: 500; 525 | font-size: 18px; 526 | margin-top: 40px; 527 | } 528 | 529 | .card span { 530 | display: block; 531 | } 532 | 533 | .add__to-card { 534 | display: -webkit-box; 535 | display: -ms-flexbox; 536 | display: flex; 537 | -webkit-box-orient: horizontal; 538 | -webkit-box-direction: normal; 539 | -ms-flex-direction: row; 540 | flex-direction: row; 541 | margin-bottom: 20px; 542 | } 543 | 544 | .bg-sync__text { 545 | font-size: 12px; 546 | padding-left: 5px; 547 | color: #008000; 548 | } 549 | 550 | .custom__button.custom__button-bg { 551 | padding: 0; 552 | margin: 0; 553 | display: inline-block; 554 | } 555 | 556 | .custom__button.custom__button-bg.hide { 557 | display: none; 558 | } 559 | 560 | b i a { 561 | text-decoration: underline; 562 | color: #1E88E5; 563 | } 564 | 565 | .add__card ul + p { 566 | margin-top: 20px; 567 | } 568 | 569 | .card__spinner { 570 | position: absolute; 571 | left: 0; 572 | right: 0; 573 | bottom: 0; 574 | top: 0; 575 | margin: auto; 576 | background: rgba(0, 0, 0, 0.16); 577 | display: none; 578 | } 579 | 580 | .card__spinner::after { 581 | content: "Loading..."; 582 | color: #1E88E5; 583 | background: #fff; 584 | position: absolute; 585 | left: 0; 586 | right: 0; 587 | bottom: 0; 588 | top: 0; 589 | margin: auto; 590 | text-align: center; 591 | line-height: 380px; 592 | font-size: 18px; 593 | } 594 | 595 | .loader { 596 | left: 50%; 597 | top: 50%; 598 | position: fixed; 599 | -webkit-transform: translate(-50%, -50%); 600 | transform: translate(-50%, -50%); 601 | 602 | } 603 | 604 | .loader #spinner { 605 | box-sizing: border-box; 606 | stroke: #673AB7; 607 | stroke-width: 3px; 608 | -webkit-transform-origin: 50%; 609 | transform-origin: 50%; 610 | -webkit-animation: line 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite, rotate 1.6s linear infinite; 611 | animation: line 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite, rotate 1.6s linear infinite; 612 | } 613 | 614 | .card__spinner.show { 615 | display: block; 616 | } 617 | 618 | .share__container a { 619 | text-decoration: underline; 620 | color: #1E88E5; 621 | } 622 | 623 | .share__container { 624 | margin-bottom: 50px; 625 | } 626 | 627 | .share { 628 | margin: 20px auto; 629 | text-align: center; 630 | display: block; 631 | } 632 | 633 | .headerButton { 634 | width: 24px; 635 | margin-right: 16px; 636 | overflow: hidden; 637 | /** opacity: 0.54; **/ 638 | -webkit-transition: opacity 0.333s cubic-bezier(0, 0, 0.21, 1); 639 | transition: opacity 0.333s cubic-bezier(0, 0, 0.21, 1); 640 | border: none; 641 | outline: none; 642 | cursor: pointer; 643 | } 644 | 645 | #butRefresh { 646 | background: url(/images/ic_refresh_white_24px.svg) center center no-repeat; 647 | } -------------------------------------------------------------------------------- /public/images/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/144x144.png -------------------------------------------------------------------------------- /public/images/168x168.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/168x168.png -------------------------------------------------------------------------------- /public/images/192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/192x192.png -------------------------------------------------------------------------------- /public/images/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/48x48.png -------------------------------------------------------------------------------- /public/images/72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/72x72.png -------------------------------------------------------------------------------- /public/images/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/96x96.png -------------------------------------------------------------------------------- /public/images/Home.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /public/images/books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/books.png -------------------------------------------------------------------------------- /public/images/ic_refresh_white_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/profile.png -------------------------------------------------------------------------------- /public/images/push-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/push-off.png -------------------------------------------------------------------------------- /public/images/push-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicodeveloper/pwa-commits/55509ec765e7365d47771b1bd0134c39c5b12bd7/public/images/push-on.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Commits PWA 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | PWA - Home 21 |
22 | 23 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |

Stay Up to Date with R-I-L

36 | Hello, World! 37 | 38 |

Latest Commits on Resources I like!

39 |
40 | 41 | 42 |
43 |
44 | Push Notification 45 |
46 | 47 | 48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // TODO add service worker code here 5 | if ('serviceWorker' in navigator) { 6 | navigator.serviceWorker 7 | .register('./sw.js') 8 | .then(function() { console.log('Service Worker Registered'); }); 9 | } 10 | 11 | 12 | })(); -------------------------------------------------------------------------------- /public/js/latest.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var app = { 5 | spinner: document.querySelector('.loader') 6 | }; 7 | 8 | var container = document.querySelector('.container'); 9 | 10 | document.getElementById('butRefresh').addEventListener('click', function() { 11 | // Get fresh, updated data from Github whenever you are clicked 12 | toast('Fetching latest data...'); 13 | fetchCommits(); 14 | console.log("Getting fresh data!!!"); 15 | }); 16 | 17 | var commitContainer = ['.first', '.second', '.third', '.fourth', '.fifth']; 18 | var posData = ['first', 'second', 'third', 'fourth', 'fifth']; 19 | 20 | // Check that localStorage is both supported and available 21 | function storageAvailable(type) { 22 | try { 23 | var storage = window[type], 24 | x = '__storage_test__'; 25 | storage.setItem(x, x); 26 | storage.removeItem(x); 27 | return true; 28 | } 29 | catch(e) { 30 | return false; 31 | } 32 | } 33 | 34 | // Get Commit Data from Github API 35 | function fetchCommits() { 36 | var url = 'https://api.github.com/repos/unicodeveloper/resources-i-like/commits'; 37 | 38 | app.spinner.setAttribute('visible', true); 39 | 40 | fetch(url) 41 | .then(function(fetchResponse){ 42 | return fetchResponse.json(); 43 | }) 44 | .then(function(response) { 45 | console.log("Response from Github", response); 46 | 47 | var commitData = {}; 48 | 49 | for (var i = 0; i < posData.length; i++) { 50 | commitData[posData[i]] = { 51 | message: response[i].commit.message, 52 | author: response[i].commit.author.name, 53 | time: response[i].commit.author.date, 54 | link: response[i].html_url 55 | }; 56 | } 57 | 58 | localStorage.setItem('commitData', JSON.stringify(commitData)); 59 | 60 | for (var i = 0; i < commitContainer.length; i++) { 61 | 62 | container.querySelector("" + commitContainer[i]).innerHTML = 63 | "

Message: " + response[i].commit.message + "

" + 64 | "

Author: " + response[i].commit.author.name + "

" + 65 | "

Time committed: " + (new Date(response[i].commit.author.date)).toUTCString() + "

" + 66 | "

" + "Click me to see more!" + "

"; 67 | 68 | } 69 | 70 | app.spinner.setAttribute('hidden', true); // hide spinner 71 | }) 72 | .catch(function (error) { 73 | console.error(error); 74 | }); 75 | }; 76 | 77 | // Get the commits Data from the Web Storage 78 | function fetchCommitsFromLocalStorage(data) { 79 | var localData = JSON.parse(data); 80 | 81 | app.spinner.setAttribute('hidden', true); //hide spinner 82 | 83 | for (var i = 0; i < commitContainer.length; i++) { 84 | 85 | container.querySelector("" + commitContainer[i]).innerHTML = 86 | "

Message: " + localData[posData[i]].message + "

" + 87 | "

Author: " + localData[posData[i]].author + "

" + 88 | "

Time committed: " + (new Date(localData[posData[i]].time)).toUTCString() + "

" + 89 | "

" + "Click me to see more!" + "

"; 90 | 91 | } 92 | }; 93 | 94 | if (storageAvailable('localStorage')) { 95 | if (localStorage.getItem('commitData') === null) { 96 | /* The user is using the app for the first time, or the user has not 97 | * saved any commit data, so show the user some fake data. 98 | */ 99 | fetchCommits(); 100 | console.log("Fetch from API"); 101 | } else { 102 | fetchCommitsFromLocalStorage(localStorage.getItem('commitData')); 103 | console.log("Fetch from Local Storage"); 104 | } 105 | } 106 | else { 107 | toast("We can't cache your app data yet.."); 108 | } 109 | })(); -------------------------------------------------------------------------------- /public/js/menu.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var menuIconElement = document.querySelector('.header__icon'); 5 | var menuElement = document.querySelector('.menu'); 6 | var menuOverlayElement = document.querySelector('.menu__overlay'); 7 | 8 | //Menu click event 9 | menuIconElement.addEventListener('click', showMenu, false); 10 | menuOverlayElement.addEventListener('click', hideMenu, false); 11 | menuElement.addEventListener('transitionend', onTransitionEnd, false); 12 | 13 | //To show menu 14 | function showMenu() { 15 | menuElement.style.transform = "translateX(0)"; 16 | menuElement.classList.add('menu--show'); 17 | menuOverlayElement.classList.add('menu__overlay--show'); 18 | } 19 | 20 | //To hide menu 21 | function hideMenu() { 22 | menuElement.style.transform = "translateX(-110%)"; 23 | menuElement.classList.remove('menu--show'); 24 | menuOverlayElement.classList.remove('menu__overlay--show'); 25 | menuElement.addEventListener('transitionend', onTransitionEnd, false); 26 | } 27 | 28 | var touchStartPoint, touchMovePoint; 29 | 30 | /*Swipe from edge to open menu*/ 31 | 32 | //`TouchStart` event to find where user start the touch 33 | document.body.addEventListener('touchstart', function(event) { 34 | touchStartPoint = event.changedTouches[0].pageX; 35 | touchMovePoint = touchStartPoint; 36 | }, false); 37 | 38 | //`TouchMove` event to determine user touch movement 39 | document.body.addEventListener('touchmove', function(event) { 40 | touchMovePoint = event.touches[0].pageX; 41 | if (touchStartPoint < 10 && touchMovePoint > 30) { 42 | menuElement.style.transform = "translateX(0)"; 43 | } 44 | }, false); 45 | 46 | function onTransitionEnd() { 47 | if (touchStartPoint < 10) { 48 | menuElement.style.transform = "translateX(0)"; 49 | menuOverlayElement.classList.add('menu__overlay--show'); 50 | menuElement.removeEventListener('transitionend', onTransitionEnd, false); 51 | } 52 | } 53 | })(); -------------------------------------------------------------------------------- /public/js/notification.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | 'use strict'; 3 | 4 | //Push notification button 5 | var fabPushElement = document.querySelector('.fab__push'); 6 | var fabPushImgElement = document.querySelector('.fab__image'); 7 | 8 | //To check `push notification` is supported or not 9 | function isPushSupported() { 10 | //To check `push notification` permission is denied by user 11 | if (Notification.permission === 'denied') { 12 | alert('User has blocked push notification.'); 13 | return; 14 | } 15 | 16 | //Check `push notification` is supported or not 17 | if (!('PushManager' in window)) { 18 | alert('Sorry, Push notification isn\'t supported in your browser.'); 19 | return; 20 | } 21 | 22 | //Get `push notification` subscription 23 | //If `serviceWorker` is registered and ready 24 | navigator.serviceWorker.ready 25 | .then(function (registration) { 26 | registration.pushManager.getSubscription() 27 | .then(function (subscription) { 28 | //If already access granted, enable push button status 29 | if (subscription) { 30 | changePushStatus(true); 31 | } 32 | else { 33 | changePushStatus(false); 34 | } 35 | }) 36 | .catch(function (error) { 37 | console.error('Error occurred while enabling push ', error); 38 | }); 39 | }); 40 | } 41 | 42 | // Ask User if he/she wants to subscribe to push notifications and then 43 | // ..subscribe and send push notification 44 | function subscribePush() { 45 | navigator.serviceWorker.ready.then(function(registration) { 46 | if (!registration.pushManager) { 47 | alert('Your browser doesn\'t support push notification.'); 48 | return false; 49 | } 50 | 51 | //To subscribe `push notification` from push manager 52 | registration.pushManager.subscribe({ 53 | userVisibleOnly: true //Always show notification when received 54 | }) 55 | .then(function (subscription) { 56 | toast('Subscribed successfully.'); 57 | console.info('Push notification subscribed.'); 58 | console.log(subscription); 59 | saveSubscriptionID(subscription); 60 | changePushStatus(true); 61 | }) 62 | .catch(function (error) { 63 | changePushStatus(false); 64 | console.error('Push notification subscription error: ', error); 65 | }); 66 | }) 67 | } 68 | 69 | // Unsubscribe the user from push notifications 70 | function unsubscribePush() { 71 | navigator.serviceWorker.ready 72 | .then(function(registration) { 73 | //Get `push subscription` 74 | registration.pushManager.getSubscription() 75 | .then(function (subscription) { 76 | //If no `push subscription`, then return 77 | if(!subscription) { 78 | alert('Unable to unregister push notification.'); 79 | return; 80 | } 81 | 82 | //Unsubscribe `push notification` 83 | subscription.unsubscribe() 84 | .then(function () { 85 | toast('Unsubscribed successfully.'); 86 | console.info('Push notification unsubscribed.'); 87 | console.log(subscription); 88 | deleteSubscriptionID(subscription); 89 | changePushStatus(false); 90 | }) 91 | .catch(function (error) { 92 | console.error(error); 93 | }); 94 | }) 95 | .catch(function (error) { 96 | console.error('Failed to unsubscribe push notification.'); 97 | }); 98 | }) 99 | } 100 | 101 | //To change status 102 | function changePushStatus(status) { 103 | fabPushElement.dataset.checked = status; 104 | fabPushElement.checked = status; 105 | if (status) { 106 | fabPushElement.classList.add('active'); 107 | fabPushImgElement.src = '../images/push-on.png'; 108 | } 109 | else { 110 | fabPushElement.classList.remove('active'); 111 | fabPushImgElement.src = '../images/push-off.png'; 112 | } 113 | } 114 | 115 | //Click event for subscribe push 116 | fabPushElement.addEventListener('click', function () { 117 | var isSubscribed = (fabPushElement.dataset.checked === 'true'); 118 | if (isSubscribed) { 119 | unsubscribePush(); 120 | } 121 | else { 122 | subscribePush(); 123 | } 124 | }); 125 | 126 | function saveSubscriptionID(subscription) { 127 | var subscription_id = subscription.endpoint.split('gcm/send/')[1]; 128 | 129 | console.log("Subscription ID", subscription_id); 130 | 131 | fetch('https://rilapi.herokuapp.com/api/users', { 132 | method: 'post', 133 | headers: { 134 | 'Accept': 'application/json', 135 | 'Content-Type': 'application/json' 136 | }, 137 | body: JSON.stringify({ user_id : subscription_id }) 138 | }); 139 | } 140 | 141 | function deleteSubscriptionID(subscription) { 142 | var subscription_id = subscription.endpoint.split('gcm/send/')[1]; 143 | 144 | fetch('https://rilapi.herokuapp.com/api/user/' + subscription_id, { 145 | method: 'delete', 146 | headers: { 147 | 'Accept': 'application/json', 148 | 'Content-Type': 'application/json' 149 | } 150 | }); 151 | } 152 | 153 | isPushSupported(); //Check for push notification support 154 | })(window); 155 | -------------------------------------------------------------------------------- /public/js/offline.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var header = document.querySelector('header'); 5 | var menuHeader = document.querySelector('.menu__header'); 6 | 7 | //After DOM Loaded 8 | document.addEventListener('DOMContentLoaded', function(event) { 9 | //On initial load to check connectivity 10 | if (!navigator.onLine) { 11 | updateNetworkStatus(); 12 | } 13 | 14 | window.addEventListener('online', updateNetworkStatus, false); 15 | window.addEventListener('offline', updateNetworkStatus, false); 16 | }); 17 | 18 | //To update network status 19 | function updateNetworkStatus() { 20 | if (navigator.onLine) { 21 | header.classList.remove('app__offline'); 22 | menuHeader.style.background = '#1E88E5'; 23 | } 24 | else { 25 | toast('You are now offline..'); 26 | header.classList.add('app__offline'); 27 | menuHeader.style.background = '#9E9E9E'; 28 | } 29 | } 30 | })(); -------------------------------------------------------------------------------- /public/js/toast.js: -------------------------------------------------------------------------------- 1 | (function (exports) { 2 | 'use strict'; 3 | 4 | var toastContainer = document.querySelector('.toast__container'); 5 | 6 | //To show notification 7 | function toast(msg, options) { 8 | if (!msg) return; 9 | 10 | options = options || 3000; 11 | 12 | var toastMsg = document.createElement('div'); 13 | 14 | toastMsg.className = 'toast__msg'; 15 | toastMsg.textContent = msg; 16 | 17 | toastContainer.appendChild(toastMsg); 18 | 19 | //Show toast for 3secs and hide it 20 | setTimeout(function () { 21 | toastMsg.classList.add('toast__msg--hide'); 22 | }, options); 23 | 24 | //Remove the element after hiding 25 | toastMsg.addEventListener('transitionend', function (event) { 26 | event.target.parentNode.removeChild(event.target); 27 | }); 28 | } 29 | 30 | exports.toast = toast; //Make this method available in global 31 | })(typeof window === 'undefined' ? module.exports : window); 32 | -------------------------------------------------------------------------------- /public/latest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Commits PWA 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | PWA - Commits 20 | 21 |
22 | 23 | 30 | 31 | 32 | 33 |
34 |

Latest Commits!

35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 |
56 | 57 |
58 | 59 | 60 | 61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PWA - Commits", 3 | "short_name": "PWA", 4 | "description": "Progressive Web Apps for Resources I like", 5 | "start_url": "./index.html?utm=homescreen", 6 | "display": "standalone", 7 | "orientation": "portrait", 8 | "background_color": "#4c4849", 9 | "theme_color": "#4c4849", 10 | "icons": [ 11 | { 12 | "src": "./images/192x192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "./images/168x168.png", 18 | "type": "image/png", 19 | "sizes": "168x168" 20 | }, 21 | { 22 | "src": "./images/144x144.png", 23 | "type": "image/png", 24 | "sizes": "144x144" 25 | }, 26 | { 27 | "src": "./images/96x96.png", 28 | "type": "image/png", 29 | "sizes": "96x96" 30 | }, 31 | { 32 | "src": "./images/72x72.png", 33 | "type": "image/png", 34 | "sizes": "72x72" 35 | }, 36 | { 37 | "src": "./images/48x48.png", 38 | "type": "image/png", 39 | "sizes": "48x48" 40 | } 41 | ], 42 | "author": { 43 | "name": "Prosper Otemuyiwa", 44 | "website": "https://twitter.com/unicodeveloper", 45 | "github": "https://github.com/unicodeveloper", 46 | "source-repo": "https://github.com/unicodeveloper/pwa-commits" 47 | }, 48 | "gcm_sender_id": "571712848651" 49 | } 50 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'pwa-commits-v3'; 2 | 3 | var filesToCache = [ 4 | './', 5 | './css/style.css', 6 | './images/books.png', 7 | './images/Home.svg', 8 | './images/ic_refresh_white_24px.svg', 9 | './images/profile.png', 10 | './images/push-off.png', 11 | './images/push-on.png', 12 | './js/app.js', 13 | './js/menu.js', 14 | './js/offline.js', 15 | './js/toast.js' 16 | ]; 17 | 18 | // Install Service Worker 19 | self.addEventListener('install', function(event) { 20 | 21 | console.log('Service Worker: Installing....'); 22 | 23 | event.waitUntil( 24 | 25 | // Open the Cache 26 | caches.open(cacheName).then(function(cache) { 27 | console.log('Service Worker: Caching App Shell at the moment......'); 28 | 29 | // Add Files to the Cache 30 | return cache.addAll(filesToCache); 31 | }) 32 | ); 33 | }); 34 | 35 | 36 | // Fired when the Service Worker starts up 37 | self.addEventListener('activate', function(event) { 38 | 39 | console.log('Service Worker: Activating....'); 40 | 41 | event.waitUntil( 42 | caches.keys().then(function(cacheNames) { 43 | return Promise.all(cacheNames.map(function(key) { 44 | if( key !== cacheName) { 45 | console.log('Service Worker: Removing Old Cache', key); 46 | return caches.delete(key); 47 | } 48 | })); 49 | }) 50 | ); 51 | return self.clients.claim(); 52 | }); 53 | 54 | 55 | self.addEventListener('fetch', function(event) { 56 | 57 | console.log('Service Worker: Fetch', event.request.url); 58 | 59 | console.log("Url", event.request.url); 60 | 61 | event.respondWith( 62 | caches.match(event.request).then(function(response) { 63 | return response || fetch(event.request); 64 | }) 65 | ); 66 | }); 67 | 68 | 69 | // triggered everytime, when a push notification is received. 70 | self.addEventListener('push', function(event) { 71 | 72 | console.info('Event: Push'); 73 | 74 | var title = 'New commit on Github Repo: RIL'; 75 | 76 | var body = { 77 | 'body': 'Click to see the latest commit', 78 | 'tag': 'pwa', 79 | 'icon': './images/48x48.png' 80 | }; 81 | 82 | event.waitUntil( 83 | self.registration.showNotification(title, body) 84 | ); 85 | }); 86 | 87 | 88 | self.addEventListener('notificationclick', function(event) { 89 | 90 | var url = './latest.html'; 91 | 92 | event.notification.close(); //Close the notification 93 | 94 | // Open the app and navigate to latest.html after clicking the notification 95 | event.waitUntil( 96 | clients.openWindow(url) 97 | ); 98 | 99 | }); 100 | 101 | --------------------------------------------------------------------------------