├── .gitignore ├── chapter-3 ├── nocache │ ├── js │ │ └── script.js │ ├── images │ │ └── hello.png │ └── no-cache.html ├── cachefirst │ ├── js │ │ └── script.js │ ├── sw-cachefirst.js │ └── index.html ├── precache │ ├── js │ │ └── script.js │ ├── images │ │ └── hello.png │ ├── service-worker.js │ └── index.html ├── progressive-times │ ├── data │ │ ├── data-2.json │ │ ├── latest.json │ │ ├── data-3.json │ │ ├── data-4.json │ │ └── data-1.json │ ├── article.html │ ├── index.html │ ├── images │ │ └── newspaper.svg │ ├── css │ │ └── site.css │ ├── js │ │ ├── article.js │ │ └── main.js │ └── sw.js └── readme.md ├── chapter-7 ├── images │ ├── homescreen.png │ ├── homescreen-144.png │ └── newspaper.svg ├── manifest.json ├── data │ ├── data-2.json │ ├── latest.json │ ├── data-3.json │ ├── data-4.json │ └── data-1.json ├── readme.md ├── offline-page.html ├── article.html ├── index.html ├── sw.js ├── css │ └── site.css └── js │ ├── article.js │ └── main.js ├── chapter-8 ├── images │ ├── homescreen.png │ ├── homescreen-144.png │ └── newspaper.svg ├── readme.md ├── manifest.json ├── data │ ├── data-2.json │ ├── latest.json │ ├── data-3.json │ ├── data-4.json │ └── data-1.json ├── offline-page.html ├── article.html ├── index.html ├── css │ └── site.css ├── js │ ├── article.js │ └── main.js └── sw.js ├── chapter-9 ├── public │ ├── images │ │ ├── homescreen.png │ │ ├── homescreen-144.png │ │ └── newspaper.svg │ ├── manifest.json │ ├── data │ │ ├── data-2.json │ │ ├── latest.json │ │ ├── data-3.json │ │ ├── data-4.json │ │ └── data-1.json │ ├── js │ │ ├── article.js │ │ ├── contact.js │ │ ├── main.js │ │ └── idb-keyval.js │ ├── css │ │ └── site.css │ └── sw.js ├── package.json ├── readme.md ├── server.js ├── offline-page.html ├── article.html ├── index.html ├── contact.html └── yarn.lock ├── chapter-4 ├── WebP-Images │ ├── images │ │ ├── brooklyn.jpg │ │ └── brooklyn.webp │ ├── index.html │ └── service-worker.js ├── save-data │ ├── data │ │ ├── data-2.json │ │ ├── latest.json │ │ ├── data-3.json │ │ ├── data-4.json │ │ └── data-1.json │ ├── article.html │ ├── index.html │ ├── images │ │ └── newspaper.svg │ ├── css │ │ └── site.css │ ├── js │ │ ├── article.js │ │ └── main.js │ └── sw.js └── readme.md ├── chapter-5 ├── look-and-feel │ ├── images │ │ ├── homescreen.png │ │ ├── homescreen-144.png │ │ └── newspaper.svg │ ├── manifest.json │ ├── data │ │ ├── data-2.json │ │ ├── latest.json │ │ ├── data-3.json │ │ ├── data-4.json │ │ └── data-1.json │ ├── css │ │ └── site.css │ ├── article.html │ ├── js │ │ ├── article.js │ │ └── main.js │ ├── sw.js │ └── index.html └── readme.md ├── chapter-10 ├── streaming-render │ ├── images │ │ ├── homescreen.png │ │ ├── homescreen-144.png │ │ └── newspaper.svg │ ├── footer.html │ ├── manifest.json │ ├── data │ │ ├── data-2.html │ │ ├── data-index.html │ │ ├── data-3.html │ │ └── data-4.html │ ├── js │ │ └── main.js │ ├── header.html │ ├── offline-page.html │ ├── article.html │ ├── css │ │ └── site.css │ ├── index.html │ └── sw.js ├── with-and-without-streaming │ ├── with.html │ ├── without.html │ └── sw.js └── readme.md ├── chapter-6 ├── push-notifications │ ├── public │ │ ├── images │ │ │ ├── homescreen.png │ │ │ ├── homescreen-144.png │ │ │ └── newspaper.svg │ │ ├── data │ │ │ ├── data-2.json │ │ │ ├── latest.json │ │ │ ├── data-3.json │ │ │ ├── data-4.json │ │ │ └── data-1.json │ │ ├── manifest.json │ │ ├── css │ │ │ └── site.css │ │ ├── js │ │ │ ├── article.js │ │ │ └── main.js │ │ └── sw.js │ ├── readme.md │ ├── package.json │ ├── manifest.json │ ├── .vscode │ │ └── launch.json │ ├── server.js │ ├── index.html │ ├── article.html │ └── yarn.lock └── readme.md └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /chapter-3/nocache/js/script.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log('This script is not cached'); 3 | } 4 | -------------------------------------------------------------------------------- /chapter-3/cachefirst/js/script.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log('Hello Service Worker caching!'); 3 | } 4 | -------------------------------------------------------------------------------- /chapter-3/precache/js/script.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log('Hello Service Worker caching!'); 3 | } 4 | -------------------------------------------------------------------------------- /chapter-7/images/homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-7/images/homescreen.png -------------------------------------------------------------------------------- /chapter-8/images/homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-8/images/homescreen.png -------------------------------------------------------------------------------- /chapter-3/nocache/images/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-3/nocache/images/hello.png -------------------------------------------------------------------------------- /chapter-3/precache/images/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-3/precache/images/hello.png -------------------------------------------------------------------------------- /chapter-7/images/homescreen-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-7/images/homescreen-144.png -------------------------------------------------------------------------------- /chapter-8/images/homescreen-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-8/images/homescreen-144.png -------------------------------------------------------------------------------- /chapter-9/public/images/homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-9/public/images/homescreen.png -------------------------------------------------------------------------------- /chapter-4/WebP-Images/images/brooklyn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-4/WebP-Images/images/brooklyn.jpg -------------------------------------------------------------------------------- /chapter-4/WebP-Images/images/brooklyn.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-4/WebP-Images/images/brooklyn.webp -------------------------------------------------------------------------------- /chapter-9/public/images/homescreen-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-9/public/images/homescreen-144.png -------------------------------------------------------------------------------- /chapter-5/look-and-feel/images/homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-5/look-and-feel/images/homescreen.png -------------------------------------------------------------------------------- /chapter-10/streaming-render/images/homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-10/streaming-render/images/homescreen.png -------------------------------------------------------------------------------- /chapter-5/look-and-feel/images/homescreen-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-5/look-and-feel/images/homescreen-144.png -------------------------------------------------------------------------------- /chapter-10/streaming-render/images/homescreen-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-10/streaming-render/images/homescreen-144.png -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/images/homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-6/push-notifications/public/images/homescreen.png -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/images/homescreen-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/HEAD/chapter-6/push-notifications/public/images/homescreen-144.png -------------------------------------------------------------------------------- /chapter-3/nocache/no-cache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello Caching World! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /chapter-9/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter-9", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.17.1", 14 | "express": "^4.15.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /chapter-10/with-and-without-streaming/with.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/readme.md: -------------------------------------------------------------------------------- 1 | In order to get started with this repo, please follow the steps below: 2 | 3 | - Start by navigating to the directory of this code in your terminal / command prompt 4 | - Run npm install (or yarn) in your terminal to install the dependencies 5 | - Next, start up the application by typing node server.js in your terminal / command prompt 6 | - Finally, navigate to http://localhost:3111 in your browser and start receiving notifications! -------------------------------------------------------------------------------- /chapter-8/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 8 2 | 3 | This is the sample code for chapter 8 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - Building more resilient applications 8 | - Understanding Single Point of Failure 9 | - The Service Worker toolbox 10 | 11 | The sample code in this chapter explores: 12 | 13 | - [Building resilient web applications](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-8) 14 | -------------------------------------------------------------------------------- /chapter-7/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Progressive Times web app", 3 | "short_name": "Progressive Times", 4 | "start_url": "./index.html", 5 | "theme_color": "#FFDF00", 6 | "background_color": "#FFDF00", 7 | "icons": [ 8 | { 9 | "src": "./images/homescreen.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "./images/homescreen-144.png", 15 | "sizes": "144x144", 16 | "type": "image/png" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /chapter-8/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Progressive Times web app", 3 | "short_name": "Progressive Times", 4 | "start_url": "./index.html", 5 | "theme_color": "#FFDF00", 6 | "background_color": "#FFDF00", 7 | "icons": [ 8 | { 9 | "src": "./images/homescreen.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "./images/homescreen-144.png", 15 | "sizes": "144x144", 16 | "type": "image/png" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /chapter-9/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Progressive Times web app", 3 | "short_name": "Progressive Times", 4 | "start_url": "./index.html", 5 | "theme_color": "#FFDF00", 6 | "background_color": "#FFDF00", 7 | "icons": [ 8 | { 9 | "src": "./images/homescreen.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "./images/homescreen-144.png", 15 | "sizes": "144x144", 16 | "type": "image/png" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /chapter-6/push-notifications/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-notifications", 3 | "version": "1.0.0", 4 | "description": "Simple example using push notifications", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "Dean Hume", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.15.2", 14 | "express": "^4.14.0", 15 | "web-push": "^3.2.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Progressive Times web app", 3 | "short_name": "Progressive Times", 4 | "start_url": "./index.html", 5 | "theme_color": "#FFDF00", 6 | "background_color": "#FFDF00", 7 | "icons": [ 8 | { 9 | "src": "./images/homescreen.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "./images/homescreen-144.png", 15 | "sizes": "144x144", 16 | "type": "image/png" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /chapter-6/push-notifications/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Progressive Times web app", 3 | "short_name": "Progressive Times", 4 | "start_url": "./index.html", 5 | "theme_color": "#FFDF00", 6 | "background_color": "#FFDF00", 7 | "icons": [ 8 | { 9 | "src": "./images/homescreen.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "./images/homescreen-144.png", 15 | "sizes": "144x144", 16 | "type": "image/png" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /chapter-10/with-and-without-streaming/without.html: -------------------------------------------------------------------------------- 1 |

This specification provides APIs for creating, composing, and consuming streams of data. These streams are designed to map efficiently to low-level I/O primitives, and allow easy composition with built-in backpressure and queuing. On top of streams, the web platform can build higher-level abstractions, such as filesystem or socket APIs, while at the same time users can use the supplied tools to build their own streams which integrate well with those of the web platform.

2 | -------------------------------------------------------------------------------- /chapter-7/data/data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "title": "Australian police find gun in biker's bottom", 4 | "description": "The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-8/data/data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "title": "Australian police find gun in biker's bottom", 4 | "description": "The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-7/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 7 2 | 3 | This is the sample code for chapter 7 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - Offline browsing 8 | - Unlocking the cache 9 | - Serving files while offline 10 | - A few gotchas to look out for 11 | - Cache isn’t forever 12 | - Offline User Experience 13 | - Tracking offline usage 14 | 15 | The sample code in this chapter explores: 16 | 17 | - [Offline Browsing](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-7/) 18 | -------------------------------------------------------------------------------- /chapter-4/save-data/data/data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "title": "Australian police find gun in biker's bottom", 4 | "description": "The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Progressive Times web app", 3 | "short_name": "Progressive Times", 4 | "start_url": "./index.html", 5 | "theme_color": "#FFDF00", 6 | "background_color": "#FFDF00", 7 | "display": "standalone", 8 | "icons": [ 9 | { 10 | "src": "./images/homescreen.png", 11 | "sizes": "192x192", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "./images/homescreen-144.png", 16 | "sizes": "144x144", 17 | "type": "image/png" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /chapter-9/public/data/data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "title": "Australian police find gun in biker's bottom", 4 | "description": "The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/data/data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "title": "Australian police find gun in biker's bottom", 4 | "description": "The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/data/data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "title": "Australian police find gun in biker's bottom", 4 | "description": "The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/data/data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "title": "Australian police find gun in biker's bottom", 4 | "description": "The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Progressive Times web app", 3 | "short_name": "Progressive Times", 4 | "start_url": "./index.html", 5 | "theme_color": "#FFDF00", 6 | "background_color": "#FFDF00", 7 | "icons": [ 8 | { 9 | "src": "./images/homescreen.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "./images/homescreen-144.png", 15 | "sizes": "144x144", 16 | "type": "image/png" 17 | } 18 | ], 19 | "permissions": [ 20 | "gcm" 21 | ] 22 | } -------------------------------------------------------------------------------- /chapter-10/streaming-render/data/data-2.html: -------------------------------------------------------------------------------- 1 |
2 |
Australian police find gun in biker's bottom
3 |
4 |

The loaded pistol was uncovered on Monday after officers in Brisbane, Queensland, discovered a second handgun in his car.

A body search revealed the biker had the handgun 'secreted between his buttocks', Queensland Police said.

He was charged with weapons and drug offences.

Police also searched his inner-city apartment where they discovered a Taser, explosives and drugs.

source: BBC News

5 | -------------------------------------------------------------------------------- /chapter-3/precache/service-worker.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'helloWorld'; 2 | 3 | self.addEventListener('install', event => { 4 | event.waitUntil( 5 | caches.open(cacheName) 6 | .then(cache => cache.addAll([ 7 | './js/script.js', 8 | './images/hello.png' 9 | ])) 10 | ); 11 | }); 12 | 13 | self.addEventListener('fetch', function(event) { 14 | event.respondWith( 15 | caches.match(event.request) 16 | .then(function(response) { 17 | if (response) { 18 | return response; 19 | } 20 | return fetch(event.request); 21 | }) 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /chapter-4/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 4 2 | 3 | This is the sample code for chapter 4 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - Intercepting Network Requests 8 | - The Fetch API 9 | - The Fetch event 10 | - The service worker lifecycle 11 | - Fetch in action 12 | 13 | The sample code in this chapter explores: 14 | 15 | - [An example using WebP images](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-4/WebP-Images) 16 | - [An example using the Save-Data header](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-4/save-data) 17 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/js/main.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | // Show an offline notification if the user if offline 4 | function showIndicator() { 5 | offlineNotification.innerHTML = 'You are currently offline.'; 6 | offlineNotification.className = 'showOfflineNotification'; 7 | } 8 | 9 | // Hide the offline notification when the user comes back online 10 | function hideIndicator() { 11 | offlineNotification.className = 'hideOfflineNotification'; 12 | } 13 | 14 | // Update the online status icon based on connectivity 15 | window.addEventListener('online', hideIndicator); 16 | window.addEventListener('offline', showIndicator); 17 | -------------------------------------------------------------------------------- /chapter-10/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 10 2 | 3 | This is the sample code for chapter 10 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - Streaming data 8 | - Understanding Web Streams 9 | - What’s the big deal with Web Streams? 10 | - Readable Streams 11 | - Supercharging your page render times 12 | - The future of the Web Stream API 13 | 14 | The sample code in this chapter explores: 15 | 16 | - [Comparison: With and without web streams](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-10/with-and-without-streaming) 17 | - [Streaming Rendering](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-10/streaming-render) 18 | -------------------------------------------------------------------------------- /chapter-5/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 5 2 | 3 | This is the sample code for chapter 5 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - Look and feel 8 | - The Web App Manifest 9 | - Add to homescreen 10 | - Customizing the icons 11 | - Add a splash screen 12 | - Set the launch style and URL 13 | - Advanced Add to Homescreen usage 14 | - Cancelling the prompt 15 | - Determining usage 16 | - Deferring the prompt 17 | - Debugging your manifest file 18 | 19 | 20 | The sample code in this chapter explores: 21 | 22 | - [How to set up your manifest file to improve the look and feel of your PWA](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-5/look-and-feel) 23 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/server.js", 12 | "cwd": "${workspaceRoot}" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "attach", 17 | "name": "Attach to Process", 18 | "port": 5858 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /chapter-10/streaming-render/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /chapter-4/WebP-Images/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Brooklyn Bridge - New York City 6 | 7 | 8 |

Brooklyn Bridge

9 | Brooklyn Bridge - New York 10 | 22 | 23 | -------------------------------------------------------------------------------- /chapter-3/precache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello Caching World! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/data/data-index.html: -------------------------------------------------------------------------------- 1 | 6 |

Nose picking ban for Manila police

A new directive tells police in Manila not to pick their noses whilst on duty

Australian police find gun in biker's bottom

Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist

Town's giant dog dropping goes missing

Local police on the trail of a missing inflatable dog mess

Austrian man wins right to be called Zebra

An Austrian man has won a court case enabling him to change his family name back to 'Zebra'

7 | -------------------------------------------------------------------------------- /chapter-3/cachefirst/sw-cachefirst.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'helloWorld'; 2 | 3 | self.addEventListener('fetch', function(event) { 4 | event.respondWith( 5 | caches.match(event.request) 6 | .then(function(response) { 7 | if (response) { 8 | return response; 9 | } 10 | 11 | var fetchRequest = event.request.clone(); 12 | 13 | return fetch(fetchRequest).then( 14 | function(fetchResponse) { 15 | if(!fetchResponse || fetchResponse.status !== 200) { 16 | return fetchResponse; 17 | } 18 | 19 | var responseToCache = fetchResponse.clone(); 20 | 21 | caches.open(cacheName) 22 | .then(function(cache) { 23 | cache.put(event.request, responseToCache); 24 | }); 25 | 26 | return fetchResponse; 27 | } 28 | ); 29 | }) 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /chapter-7/data/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestNews": [ 3 | { 4 | "id": 1, 5 | "title": "Nose picking ban for Manila police", 6 | "description": "A new directive tells police in Manila not to pick their noses whilst on duty" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Australian police find gun in biker's bottom", 11 | "description": "Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist" 12 | }, 13 | { 14 | "id": 3, 15 | "title": "Town's giant dog dropping goes missing", 16 | "description": "Local police on the trail of a missing inflatable dog mess" 17 | }, 18 | { 19 | "id": 4, 20 | "title": "Austrian man wins right to be called Zebra", 21 | "description": "An Austrian man has won a court case enabling him to change his family name back to 'Zebra'" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /chapter-8/data/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestNews": [ 3 | { 4 | "id": 1, 5 | "title": "Nose picking ban for Manila police", 6 | "description": "A new directive tells police in Manila not to pick their noses whilst on duty" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Australian police find gun in biker's bottom", 11 | "description": "Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist" 12 | }, 13 | { 14 | "id": 3, 15 | "title": "Town's giant dog dropping goes missing", 16 | "description": "Local police on the trail of a missing inflatable dog mess" 17 | }, 18 | { 19 | "id": 4, 20 | "title": "Austrian man wins right to be called Zebra", 21 | "description": "An Austrian man has won a court case enabling him to change his family name back to 'Zebra'" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /chapter-4/WebP-Images/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | // Listen to fetch events 5 | self.addEventListener('fetch', function(event) { 6 | 7 | // Check if the image is a jpeg 8 | if (/\.jpg$|.png$/.test(event.request.url)) { 9 | 10 | // Inspect the accept header for WebP support 11 | var supportsWebp = false; 12 | if (event.request.headers.has('accept')) { 13 | supportsWebp = event.request.headers 14 | .get('accept') 15 | .includes('webp'); 16 | } 17 | 18 | // If we support WebP 19 | if (supportsWebp) { 20 | // Clone the request 21 | var req = event.request.clone(); 22 | 23 | // Build the return URL 24 | var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp"; 25 | 26 | event.respondWith( 27 | fetch(returnUrl, { 28 | mode: 'no-cors' 29 | }) 30 | ); 31 | } 32 | } 33 | }); -------------------------------------------------------------------------------- /chapter-9/public/data/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestNews": [ 3 | { 4 | "id": 1, 5 | "title": "Nose picking ban for Manila police", 6 | "description": "A new directive tells police in Manila not to pick their noses whilst on duty" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Australian police find gun in biker's bottom", 11 | "description": "Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist" 12 | }, 13 | { 14 | "id": 3, 15 | "title": "Town's giant dog dropping goes missing", 16 | "description": "Local police on the trail of a missing inflatable dog mess" 17 | }, 18 | { 19 | "id": 4, 20 | "title": "Austrian man wins right to be called Zebra", 21 | "description": "An Austrian man has won a court case enabling him to change his family name back to 'Zebra'" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /chapter-4/save-data/data/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestNews": [ 3 | { 4 | "id": 1, 5 | "title": "Nose picking ban for Manila police", 6 | "description": "A new directive tells police in Manila not to pick their noses whilst on duty" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Australian police find gun in biker's bottom", 11 | "description": "Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist" 12 | }, 13 | { 14 | "id": 3, 15 | "title": "Town's giant dog dropping goes missing", 16 | "description": "Local police on the trail of a missing inflatable dog mess" 17 | }, 18 | { 19 | "id": 4, 20 | "title": "Austrian man wins right to be called Zebra", 21 | "description": "An Austrian man has won a court case enabling him to change his family name back to 'Zebra'" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/data/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestNews": [ 3 | { 4 | "id": 1, 5 | "title": "Nose picking ban for Manila police", 6 | "description": "A new directive tells police in Manila not to pick their noses whilst on duty" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Australian police find gun in biker's bottom", 11 | "description": "Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist" 12 | }, 13 | { 14 | "id": 3, 15 | "title": "Town's giant dog dropping goes missing", 16 | "description": "Local police on the trail of a missing inflatable dog mess" 17 | }, 18 | { 19 | "id": 4, 20 | "title": "Austrian man wins right to be called Zebra", 21 | "description": "An Austrian man has won a court case enabling him to change his family name back to 'Zebra'" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/data/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestNews": [ 3 | { 4 | "id": 1, 5 | "title": "Nose picking ban for Manila police", 6 | "description": "A new directive tells police in Manila not to pick their noses whilst on duty" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Australian police find gun in biker's bottom", 11 | "description": "Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist" 12 | }, 13 | { 14 | "id": 3, 15 | "title": "Town's giant dog dropping goes missing", 16 | "description": "Local police on the trail of a missing inflatable dog mess" 17 | }, 18 | { 19 | "id": 4, 20 | "title": "Austrian man wins right to be called Zebra", 21 | "description": "An Austrian man has won a court case enabling him to change his family name back to 'Zebra'" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/data/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestNews": [ 3 | { 4 | "id": 1, 5 | "title": "Nose picking ban for Manila police", 6 | "description": "A new directive tells police in Manila not to pick their noses whilst on duty" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Australian police find gun in biker's bottom", 11 | "description": "Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist" 12 | }, 13 | { 14 | "id": 3, 15 | "title": "Town's giant dog dropping goes missing", 16 | "description": "Local police on the trail of a missing inflatable dog mess" 17 | }, 18 | { 19 | "id": 4, 20 | "title": "Austrian man wins right to be called Zebra", 21 | "description": "An Austrian man has won a court case enabling him to change his family name back to 'Zebra'" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /chapter-3/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 3 2 | 3 | This is the sample code for chapter 3 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - The basics of HTTP caching 8 | - The basics of caching using Service Workers 9 | - Precaching during Service Worker installation 10 | - Intercept and cache 11 | - A performance comparison: before and after caching 12 | - Diving deeper into Service Worker caching 13 | 14 | The sample code in this chapter explores: 15 | 16 | - [A cache first approach](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-3/cachefirst) 17 | - [Precaching during Service Worker installation](https://github.com/deanhume/progressive-web-apps-book/tree/master/precache) 18 | - [Progressive Times](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-3/progressive-times) - The base code for the sample application that is used throughout the book 19 | -------------------------------------------------------------------------------- /chapter-9/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 9 2 | 3 | This is the sample code for chapter 9 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - Keeping your data synchronised 8 | - Understanding BackgroundSync 9 | - Testing 10 | - Notifying the user 11 | - Periodic Synchronisation 12 | 13 | The sample code in this chapter explores: 14 | 15 | - [Using BackgroundSync](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-9) 16 | 17 | In order to get the example code in this repo running, please follow the steps below: 18 | 19 | - Start by navigating to the directory of this code in your terminal / command prompt 20 | - Run npm install (or yarn) in your terminal to install the dependencies 21 | - Next, start up the application by typing node server.js in your terminal / command prompt 22 | - Finally, navigate to http://localhost:3111 in your browser and start receiving notifications! 23 | -------------------------------------------------------------------------------- /chapter-7/data/data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "title": "Town's giant dog dropping goes missing", 4 | "description": "A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-8/data/data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "title": "Town's giant dog dropping goes missing", 4 | "description": "A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-4/save-data/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |

17 |
18 | 19 | 20 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /chapter-4/save-data/data/data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "title": "Town's giant dog dropping goes missing", 4 | "description": "A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-9/public/data/data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "title": "Town's giant dog dropping goes missing", 4 | "description": "A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |

17 |
18 | 19 | 20 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/data/data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "title": "Town's giant dog dropping goes missing", 4 | "description": "A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/data/data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "title": "Town's giant dog dropping goes missing", 4 | "description": "A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/data/data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "title": "Town's giant dog dropping goes missing", 4 | "description": "A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-7/data/data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "title": "Austrian man wins right to be called Zebra", 4 | "description": "The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-8/data/data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "title": "Austrian man wins right to be called Zebra", 4 | "description": "The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/data/data-3.html: -------------------------------------------------------------------------------- 1 |
2 |
Town's giant dog dropping goes missing
3 |
4 |

A town in Spain is facing an unexpected bill after thieves apparently made off with a giant dog dropping being used as part of a local campaign.

Torrelodones, a municipality just outside the capital Madrid, is 2,400 euros ($2,726; £1,885) out of pocket after the three-metre high inflatable bought as part of a campaign to encourage pet-lovers to pick up after their dogs went missing, El Pais newspaper reports. The bizarre inflatable disappeared after it had been packed away in its carry-case and the police are now on the trail of the 30 kilogramme dog poop, town officials say.

Speaking to the ABC newspaper, town councillor Angel Guirao said staff were shocked and perplexed by the theft, and a replacement excrement was already on order because 'we know that the campaign has been a great success'.

source: BBC News

5 | -------------------------------------------------------------------------------- /chapter-4/save-data/data/data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "title": "Austrian man wins right to be called Zebra", 4 | "description": "The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-9/public/data/data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "title": "Austrian man wins right to be called Zebra", 4 | "description": "The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/data/data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "title": "Austrian man wins right to be called Zebra", 4 | "description": "The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/data/data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "title": "Austrian man wins right to be called Zebra", 4 | "description": "The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/data/data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "title": "Austrian man wins right to be called Zebra", 4 | "description": "The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-3/cachefirst/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello Caching World! 6 | 7 | 11 | 12 | 13 |

Hello Service Worker Cache!

14 | 15 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /chapter-4/save-data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Online News 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 |
19 |
20 | 21 | 22 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/data/data-4.html: -------------------------------------------------------------------------------- 1 |
2 |
Austrian man wins right to be called Zebra
3 |
4 |

The man appealed to the Austrian Constitutional Court when a lower court rejected his proposed name change.

The man's grandfather changed his name from Zebra in the 1950s. Zebra had been the family name for centuries, so reverting to it was quite legal, the Constitutional Court ruled.

The lower court had argued that Zebra was a non-Austrian invented name.

In that earlier ruling, a judge said Zebra could only refer to a type of horse living in the African savannah.

Some other animal names would have been acceptable, as they already existed in Austrian official records, he argued. He gave as examples the family names 'Fuchs' (meaning fox), 'Biber' (beaver) and 'Strauss' (ostrich).

The last family member with the name Zebra died in 1991 - a great-uncle of the plaintiff. The plaintiff's current name was not disclosed.

source: BBC News

5 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Online News 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 |
19 |
20 | 21 | 22 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /chapter-6/readme.md: -------------------------------------------------------------------------------- 1 | ## Chapter 6 2 | 3 | This is the sample code for chapter 6 of the book *Progressive Web Apps in Action**. 4 | 5 | This chapter covers the following: 6 | 7 | - Push Notifications 8 | - Engaging with your users 9 | - Engagement Insight: The Weather Channel 10 | - Browser Support 11 | - Your first push notification 12 | - Subscribing to notifications 13 | - Sending notifications 14 | - Receiving notifications and interacting with them 15 | - Unsubscribing 16 | - Third party push notifications 17 | 18 | The sample code in this chapter explores: 19 | 20 | - [A Node.js server side web push notification implementation](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-6/push-notifications) 21 | 22 | In order to get the example code in this repo running, please follow the steps below: 23 | 24 | - Start by navigating to the directory of this code in your terminal / command prompt 25 | - Run npm install (or yarn) in your terminal to install the dependencies 26 | - Next, start up the application by typing node server.js in your terminal / command prompt 27 | - Finally, navigate to http://localhost:3111 in your browser and start receiving notifications! 28 | -------------------------------------------------------------------------------- /chapter-9/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const path = require('path'); 4 | const app = express(); 5 | 6 | // Express setup 7 | app.use(express.static('public')); 8 | app.use(bodyParser.json()); 9 | app.use(bodyParser.urlencoded({ // to support URL-encoded bodies 10 | extended: true 11 | })); 12 | 13 | 14 | // Home page 15 | app.get('/', function (req, res) { 16 | res.sendFile(path.join(__dirname + '/index.html')); 17 | }); 18 | 19 | // Article page 20 | app.get('/article', function (req, res) { 21 | res.sendFile(path.join(__dirname + '/article.html')); 22 | }); 23 | 24 | // Article page 25 | app.get('/contact', function (req, res) { 26 | res.sendFile(path.join(__dirname + '/contact.html')); 27 | }); 28 | 29 | // Offline page 30 | app.get('/offline', function (req, res) { 31 | res.sendFile(path.join(__dirname + '/offline-page.html')); 32 | }); 33 | 34 | // Send a message 35 | app.post('/sendMessage', function (req, res) { 36 | res.json(`Message sent to ${req.body.email}`); 37 | }); 38 | 39 | // The server 40 | app.listen(3111, function () { 41 | console.log('Example app listening on port 3111!') 42 | }); 43 | -------------------------------------------------------------------------------- /chapter-7/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-8/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-9/public/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-4/save-data/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/images/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-7/offline-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - You are offline 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Progressive Times

21 |
Whoops! It looks like you are currently offline.
22 |
23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /chapter-8/offline-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - You are offline 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Progressive Times

21 |
Whoops! It looks like you are currently offline.
22 |
23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /chapter-9/offline-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - You are offline 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Progressive Times

21 |
Whoops! It looks like you are currently offline.
22 |
23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /chapter-4/save-data/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | @media screen and (max-width: 550px) { 16 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 17 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 18 | } 19 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | @media screen and (max-width: 550px) { 16 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 17 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 18 | } 19 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | @media screen and (max-width: 550px) { 16 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 17 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 18 | } 19 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |

23 |
24 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/offline-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - You are offline 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Progressive Times

21 |
Whoops! It looks like you are currently offline.
22 |
23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | @media screen and (max-width: 550px) { 16 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 17 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 18 | } 19 | -------------------------------------------------------------------------------- /chapter-7/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |

23 |
24 |
25 | 26 | 27 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /chapter-8/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |

23 |
24 |
25 | 26 | 27 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /chapter-9/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |

23 |
24 |
25 | 26 | 27 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /chapter-4/save-data/js/article.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the article contents into the page 26 | function loadArticle(){ 27 | // Get the details for the article 28 | var articleId = findGetParameter('id'); 29 | var articleUrl = './data/data-' + articleId + '.json'; 30 | retrieveData(articleUrl); 31 | } 32 | 33 | // Build the web page with the resulting data 34 | function buildWebPage(result){ 35 | document.getElementById('article').innerHTML = result.description; 36 | document.getElementById('article-title').innerHTML = result.title; 37 | } 38 | 39 | loadArticle(); 40 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/js/article.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the article contents into the page 26 | function loadArticle(){ 27 | // Get the details for the article 28 | var articleId = findGetParameter('id'); 29 | var articleUrl = './data/data-' + articleId + '.json'; 30 | retrieveData(articleUrl); 31 | } 32 | 33 | // Build the web page with the resulting data 34 | function buildWebPage(result){ 35 | document.getElementById('article').innerHTML = result.description; 36 | document.getElementById('article-title').innerHTML = result.title; 37 | } 38 | 39 | loadArticle(); 40 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Article 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |

23 |
24 |
25 | 26 | 27 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/js/article.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the article contents into the page 26 | function loadArticle(){ 27 | // Get the details for the article 28 | var articleId = findGetParameter('id'); 29 | var articleUrl = './data/data-' + articleId + '.json'; 30 | retrieveData(articleUrl); 31 | } 32 | 33 | // Build the web page with the resulting data 34 | function buildWebPage(result){ 35 | document.getElementById('article').innerHTML = result.description; 36 | document.getElementById('article-title').innerHTML = result.title; 37 | } 38 | 39 | loadArticle(); 40 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/js/article.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the article contents into the page 26 | function loadArticle(){ 27 | // Get the details for the article 28 | var articleId = findGetParameter('id'); 29 | var articleUrl = './data/data-' + articleId + '.json'; 30 | retrieveData(articleUrl); 31 | } 32 | 33 | // Build the web page with the resulting data 34 | function buildWebPage(result){ 35 | document.getElementById('article').innerHTML = result.description; 36 | document.getElementById('article-title').innerHTML = result.title; 37 | } 38 | 39 | loadArticle(); 40 | -------------------------------------------------------------------------------- /chapter-4/save-data/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'latestNews-v1'; 2 | 3 | // Cache our known resources during install 4 | self.addEventListener('install', event => { 5 | event.waitUntil( 6 | caches.open(cacheName) 7 | .then(cache => cache.addAll([ 8 | './js/main.js', 9 | './js/article.js', 10 | './images/newspaper.svg', 11 | './css/site.css', 12 | './data/latest.json', 13 | './data/data-1.json', 14 | './article.html', 15 | './index.html' 16 | ])) 17 | ); 18 | }); 19 | 20 | // Cache any new resources as they are fetched 21 | self.addEventListener('fetch', function(event) { 22 | event.respondWith( 23 | caches.match(event.request, { ignoreSearch: true }) 24 | .then(function(response) { 25 | if (response) { 26 | return response; 27 | } 28 | var fetchRequest = event.request.clone(); 29 | 30 | return fetch(fetchRequest).then( 31 | function(response) { 32 | if(!response || response.status !== 200) { 33 | return response; 34 | } 35 | 36 | var responseToCache = response.clone(); 37 | caches.open(cacheName) 38 | .then(function(cache) { 39 | cache.put(event.request, responseToCache); 40 | }); 41 | 42 | return response; 43 | } 44 | ); 45 | }) 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'latestNews-v1'; 2 | 3 | // Cache our known resources during install 4 | self.addEventListener('install', event => { 5 | event.waitUntil( 6 | caches.open(cacheName) 7 | .then(cache => cache.addAll([ 8 | './js/main.js', 9 | './js/article.js', 10 | './images/newspaper.svg', 11 | './css/site.css', 12 | './data/latest.json', 13 | './data/data-1.json', 14 | './article.html', 15 | './index.html' 16 | ])) 17 | ); 18 | }); 19 | 20 | // Cache any new resources as they are fetched 21 | self.addEventListener('fetch', function(event) { 22 | event.respondWith( 23 | caches.match(event.request, { ignoreSearch: true }) 24 | .then(function(response) { 25 | if (response) { 26 | return response; 27 | } 28 | var fetchRequest = event.request.clone(); 29 | 30 | return fetch(fetchRequest).then( 31 | function(response) { 32 | if(!response || response.status !== 200) { 33 | return response; 34 | } 35 | 36 | var responseToCache = response.clone(); 37 | caches.open(cacheName) 38 | .then(function(cache) { 39 | cache.put(event.request, responseToCache); 40 | }); 41 | 42 | return response; 43 | } 44 | ); 45 | }) 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'latestNews-v1'; 2 | 3 | // Cache our known resources during install 4 | self.addEventListener('install', event => { 5 | event.waitUntil( 6 | caches.open(cacheName) 7 | .then(cache => cache.addAll([ 8 | './js/main.js', 9 | './js/article.js', 10 | './images/newspaper.svg', 11 | './css/site.css', 12 | './data/latest.json', 13 | './data/data-1.json', 14 | './article.html', 15 | './index.html' 16 | ])) 17 | ); 18 | }); 19 | 20 | // Cache any new resources as they are fetched 21 | self.addEventListener('fetch', function(event) { 22 | event.respondWith( 23 | caches.match(event.request, { ignoreSearch: true }) 24 | .then(function(response) { 25 | if (response) { 26 | return response; 27 | } 28 | var fetchRequest = event.request.clone(); 29 | 30 | return fetch(fetchRequest).then( 31 | function(response) { 32 | if(!response || response.status !== 200) { 33 | return response; 34 | } 35 | 36 | var responseToCache = response.clone(); 37 | caches.open(cacheName) 38 | .then(function(cache) { 39 | cache.put(event.request, responseToCache); 40 | }); 41 | 42 | return response; 43 | } 44 | ); 45 | }) 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Online News 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 |
25 |
26 | 27 | 28 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /chapter-7/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Online News 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /chapter-8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Online News 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /chapter-9/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Online News 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /chapter-7/data/data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "Nose picking ban for Manila police", 4 | "description": "Police in the Philippines have been warned not to take selfies or pick their noses while on duty, it's reported.

A memorandum issued by the National Capital Region Police Office (NCRPO) reminds officers not to do anything that might create a negative impression among members of the public, the Philippine Star reports. As well as leaving their noses alone, officers in the capital, Manila, have been told not to play online games, smoke or chew gum during their shifts.

The list also specifies that any bodily itches must remain unscratched, and officers should avoid unseemly posture such as standing on one leg, the paper says. The instructions are a response to police officers posting photos of themselves on social media while on duty in the city, GMA News Online reports.

Police leaders have repeatedly issued no-selfie orders, including after Typhoon Haiyan in 2013, when some officers reportedly posted pictures from the disaster zone.

Manila's residents will have seen an increase in police activity of late - the NCRPO says 85% of its force is now on patrol. President Rodrigo Duterte recently announced a major increase in government funding for the national police, as part of his controversial war on drugs. He's committed to boosting its budget by 24.6% next year, in order to hire more officers and buy more vehicles and guns.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-8/data/data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "Nose picking ban for Manila police", 4 | "description": "Police in the Philippines have been warned not to take selfies or pick their noses while on duty, it's reported.

A memorandum issued by the National Capital Region Police Office (NCRPO) reminds officers not to do anything that might create a negative impression among members of the public, the Philippine Star reports. As well as leaving their noses alone, officers in the capital, Manila, have been told not to play online games, smoke or chew gum during their shifts.

The list also specifies that any bodily itches must remain unscratched, and officers should avoid unseemly posture such as standing on one leg, the paper says. The instructions are a response to police officers posting photos of themselves on social media while on duty in the city, GMA News Online reports.

Police leaders have repeatedly issued no-selfie orders, including after Typhoon Haiyan in 2013, when some officers reportedly posted pictures from the disaster zone.

Manila's residents will have seen an increase in police activity of late - the NCRPO says 85% of its force is now on patrol. President Rodrigo Duterte recently announced a major increase in government funding for the national police, as part of his controversial war on drugs. He's committed to boosting its budget by 24.6% next year, in order to hire more officers and buy more vehicles and guns.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-4/save-data/data/data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "Nose picking ban for Manila police", 4 | "description": "Police in the Philippines have been warned not to take selfies or pick their noses while on duty, it's reported.

A memorandum issued by the National Capital Region Police Office (NCRPO) reminds officers not to do anything that might create a negative impression among members of the public, the Philippine Star reports. As well as leaving their noses alone, officers in the capital, Manila, have been told not to play online games, smoke or chew gum during their shifts.

The list also specifies that any bodily itches must remain unscratched, and officers should avoid unseemly posture such as standing on one leg, the paper says. The instructions are a response to police officers posting photos of themselves on social media while on duty in the city, GMA News Online reports.

Police leaders have repeatedly issued no-selfie orders, including after Typhoon Haiyan in 2013, when some officers reportedly posted pictures from the disaster zone.

Manila's residents will have seen an increase in police activity of late - the NCRPO says 85% of its force is now on patrol. President Rodrigo Duterte recently announced a major increase in government funding for the national police, as part of his controversial war on drugs. He's committed to boosting its budget by 24.6% next year, in order to hire more officers and buy more vehicles and guns.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-9/public/data/data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "Nose picking ban for Manila police", 4 | "description": "Police in the Philippines have been warned not to take selfies or pick their noses while on duty, it's reported.

A memorandum issued by the National Capital Region Police Office (NCRPO) reminds officers not to do anything that might create a negative impression among members of the public, the Philippine Star reports. As well as leaving their noses alone, officers in the capital, Manila, have been told not to play online games, smoke or chew gum during their shifts.

The list also specifies that any bodily itches must remain unscratched, and officers should avoid unseemly posture such as standing on one leg, the paper says. The instructions are a response to police officers posting photos of themselves on social media while on duty in the city, GMA News Online reports.

Police leaders have repeatedly issued no-selfie orders, including after Typhoon Haiyan in 2013, when some officers reportedly posted pictures from the disaster zone.

Manila's residents will have seen an increase in police activity of late - the NCRPO says 85% of its force is now on patrol. President Rodrigo Duterte recently announced a major increase in government funding for the national police, as part of his controversial war on drugs. He's committed to boosting its budget by 24.6% next year, in order to hire more officers and buy more vehicles and guns.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/data/data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "Nose picking ban for Manila police", 4 | "description": "Police in the Philippines have been warned not to take selfies or pick their noses while on duty, it's reported.

A memorandum issued by the National Capital Region Police Office (NCRPO) reminds officers not to do anything that might create a negative impression among members of the public, the Philippine Star reports. As well as leaving their noses alone, officers in the capital, Manila, have been told not to play online games, smoke or chew gum during their shifts.

The list also specifies that any bodily itches must remain unscratched, and officers should avoid unseemly posture such as standing on one leg, the paper says. The instructions are a response to police officers posting photos of themselves on social media while on duty in the city, GMA News Online reports.

Police leaders have repeatedly issued no-selfie orders, including after Typhoon Haiyan in 2013, when some officers reportedly posted pictures from the disaster zone.

Manila's residents will have seen an increase in police activity of late - the NCRPO says 85% of its force is now on patrol. President Rodrigo Duterte recently announced a major increase in government funding for the national police, as part of his controversial war on drugs. He's committed to boosting its budget by 24.6% next year, in order to hire more officers and buy more vehicles and guns.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/data/data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "Nose picking ban for Manila police", 4 | "description": "Police in the Philippines have been warned not to take selfies or pick their noses while on duty, it's reported.

A memorandum issued by the National Capital Region Police Office (NCRPO) reminds officers not to do anything that might create a negative impression among members of the public, the Philippine Star reports. As well as leaving their noses alone, officers in the capital, Manila, have been told not to play online games, smoke or chew gum during their shifts.

The list also specifies that any bodily itches must remain unscratched, and officers should avoid unseemly posture such as standing on one leg, the paper says. The instructions are a response to police officers posting photos of themselves on social media while on duty in the city, GMA News Online reports.

Police leaders have repeatedly issued no-selfie orders, including after Typhoon Haiyan in 2013, when some officers reportedly posted pictures from the disaster zone.

Manila's residents will have seen an increase in police activity of late - the NCRPO says 85% of its force is now on patrol. President Rodrigo Duterte recently announced a major increase in government funding for the national police, as part of his controversial war on drugs. He's committed to boosting its budget by 24.6% next year, in order to hire more officers and buy more vehicles and guns.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/data/data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "Nose picking ban for Manila police", 4 | "description": "Police in the Philippines have been warned not to take selfies or pick their noses while on duty, it's reported.

A memorandum issued by the National Capital Region Police Office (NCRPO) reminds officers not to do anything that might create a negative impression among members of the public, the Philippine Star reports. As well as leaving their noses alone, officers in the capital, Manila, have been told not to play online games, smoke or chew gum during their shifts.

The list also specifies that any bodily itches must remain unscratched, and officers should avoid unseemly posture such as standing on one leg, the paper says. The instructions are a response to police officers posting photos of themselves on social media while on duty in the city, GMA News Online reports.

Police leaders have repeatedly issued no-selfie orders, including after Typhoon Haiyan in 2013, when some officers reportedly posted pictures from the disaster zone.

Manila's residents will have seen an increase in police activity of late - the NCRPO says 85% of its force is now on patrol. President Rodrigo Duterte recently announced a major increase in government funding for the national police, as part of his controversial war on drugs. He's committed to boosting its budget by 24.6% next year, in order to hire more officers and buy more vehicles and guns.

source: BBC News" 5 | } 6 | -------------------------------------------------------------------------------- /chapter-4/save-data/js/main.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the latest news data and populate content 26 | function loadLatestNews(){ 27 | var dataUrl = './data/latest.json'; 28 | var result = retrieveData(dataUrl); 29 | } 30 | 31 | function buildWebPage(result) { 32 | // Build up our HTML 33 | var latestNews = ''; 34 | 35 | // Loop through the results 36 | for (var i = 0; i < result.latestNews.length; i++) { 37 | 38 | var title = '

' + result.latestNews[i].title + '

'; 39 | var description = '

' + result.latestNews[i].description + '

' 40 | 41 | latestNews += title + description; 42 | } 43 | 44 | // Update the DOM with the data 45 | document.getElementById('latest').innerHTML = latestNews; 46 | } 47 | 48 | loadLatestNews(); 49 | -------------------------------------------------------------------------------- /chapter-3/progressive-times/js/main.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the latest news data and populate content 26 | function loadLatestNews(){ 27 | var dataUrl = './data/latest.json'; 28 | var result = retrieveData(dataUrl); 29 | } 30 | 31 | function buildWebPage(result) { 32 | // Build up our HTML 33 | var latestNews = ''; 34 | 35 | // Loop through the results 36 | for (var i = 0; i < result.latestNews.length; i++) { 37 | 38 | var title = '

' + result.latestNews[i].title + '

'; 39 | var description = '

' + result.latestNews[i].description + '

' 40 | 41 | latestNews += title + description; 42 | } 43 | 44 | // Update the DOM with the data 45 | document.getElementById('latest').innerHTML = latestNews; 46 | } 47 | 48 | loadLatestNews(); 49 | -------------------------------------------------------------------------------- /chapter-5/look-and-feel/js/main.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the latest news data and populate content 26 | function loadLatestNews(){ 27 | var dataUrl = './data/latest.json'; 28 | var result = retrieveData(dataUrl); 29 | } 30 | 31 | function buildWebPage(result) { 32 | // Build up our HTML 33 | var latestNews = ''; 34 | 35 | // Loop through the results 36 | for (var i = 0; i < result.latestNews.length; i++) { 37 | 38 | var title = '

' + result.latestNews[i].title + '

'; 39 | var description = '

' + result.latestNews[i].description + '

' 40 | 41 | latestNews += title + description; 42 | } 43 | 44 | // Update the DOM with the data 45 | document.getElementById('latest').innerHTML = latestNews; 46 | } 47 | 48 | loadLatestNews(); 49 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/js/main.js: -------------------------------------------------------------------------------- 1 | function retrieveData(url) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState == 4 && this.status == 200) { 5 | var result = JSON.parse(this.responseText); 6 | buildWebPage(result); 7 | } 8 | }; 9 | xhttp.open('GET', url, true); 10 | xhttp.send(); 11 | } 12 | 13 | // Get a value from the querystring 14 | function findGetParameter(parameterName) { 15 | var result = null, 16 | tmp = []; 17 | var items = location.search.substr(1).split("&"); 18 | for (var index = 0; index < items.length; index++) { 19 | tmp = items[index].split("="); 20 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 21 | } 22 | return result; 23 | } 24 | 25 | // Load the latest news data and populate content 26 | function loadLatestNews(){ 27 | var dataUrl = './data/latest.json'; 28 | var result = retrieveData(dataUrl); 29 | } 30 | 31 | function buildWebPage(result) { 32 | // Build up our HTML 33 | var latestNews = ''; 34 | 35 | // Loop through the results 36 | for (var i = 0; i < result.latestNews.length; i++) { 37 | 38 | var title = '

' + result.latestNews[i].title + '

'; 39 | var description = '

' + result.latestNews[i].description + '

' 40 | 41 | latestNews += title + description; 42 | } 43 | 44 | // Update the DOM with the data 45 | document.getElementById('latest').innerHTML = latestNews; 46 | } 47 | 48 | loadLatestNews(); 49 | -------------------------------------------------------------------------------- /chapter-7/sw.js: -------------------------------------------------------------------------------- 1 | const cacheName = 'latestNews-v1'; 2 | const offlineUrl = 'offline-page.html'; 3 | 4 | // Cache our known resources during install 5 | self.addEventListener('install', event => { 6 | event.waitUntil( 7 | caches.open(cacheName) 8 | .then(cache => cache.addAll([ 9 | './js/main.js', 10 | './js/article.js', 11 | './images/newspaper.svg', 12 | './css/site.css', 13 | './data/latest.json', 14 | './data/data-1.json', 15 | './article.html', 16 | './index.html', 17 | offlineUrl 18 | ])) 19 | ); 20 | }); 21 | 22 | // Cache any new resources as they are fetched 23 | self.addEventListener('fetch', function(event) { 24 | 25 | event.respondWith( 26 | caches.match(event.request) 27 | .then(function(response) { 28 | if (response) { 29 | return response; 30 | } 31 | var fetchRequest = event.request.clone(); 32 | 33 | return fetch(fetchRequest).then( 34 | function(response) { 35 | if(!response || response.status !== 200) { 36 | return response; 37 | } 38 | 39 | var responseToCache = response.clone(); 40 | caches.open(cacheName) 41 | .then(function(cache) { 42 | cache.put(event.request, responseToCache); 43 | }); 44 | 45 | return response; 46 | } 47 | ).catch(error => { 48 | // Check if the user is offline first and is trying to navigate to a web page 49 | if (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html')) { 50 | // Return the offline page 51 | return caches.match(offlineUrl); 52 | } 53 | }); 54 | }) 55 | ); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /chapter-7/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | /* Offline Page */ 16 | #header-offline { background-color: aquamarine; width: 100%; position: absolute; top: 0; left: 0; height: 600px;} 17 | #logo-offline { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 125px; } 18 | 19 | .showOfflineNotification { background-color: black;height: 40px;color: white;text-align: center;padding-top: 20px;font-size: 20px;margin-top: 0px;position: absolute;top: 0;left: 0;right: 0;overflow: hidden;z-index: 101; } 20 | .hideOfflineNotification { display:none; } 21 | 22 | @media screen and (max-width: 550px) { 23 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 24 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 25 | } 26 | -------------------------------------------------------------------------------- /chapter-8/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | /* Offline Page */ 16 | #header-offline { background-color: aquamarine; width: 100%; position: absolute; top: 0; left: 0; height: 600px;} 17 | #logo-offline { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 125px; } 18 | 19 | .showOfflineNotification { background-color: black;height: 40px;color: white;text-align: center;padding-top: 20px;font-size: 20px;margin-top: 0px;position: absolute;top: 0;left: 0;right: 0;overflow: hidden;z-index: 101; } 20 | .hideOfflineNotification { display:none; } 21 | 22 | @media screen and (max-width: 550px) { 23 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 24 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 25 | } 26 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | /* Offline Page */ 16 | #header-offline { background-color: aquamarine; width: 100%; position: absolute; top: 0; left: 0; height: 600px;} 17 | #logo-offline { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 125px; } 18 | 19 | .showOfflineNotification { background-color: black;height: 40px;color: white;text-align: center;padding-top: 20px;font-size: 20px;margin-top: 0px;position: absolute;top: 0;left: 0;right: 0;overflow: hidden;z-index: 101; } 20 | .hideOfflineNotification { display:none; } 21 | 22 | @media screen and (max-width: 550px) { 23 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 24 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 25 | } 26 | -------------------------------------------------------------------------------- /chapter-7/js/article.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | function retrieveData(url) { 4 | var xhttp = new XMLHttpRequest(); 5 | xhttp.onreadystatechange = function() { 6 | if (this.readyState == 4 && this.status == 200) { 7 | var result = JSON.parse(this.responseText); 8 | buildWebPage(result); 9 | } 10 | }; 11 | xhttp.open('GET', url, true); 12 | xhttp.send(); 13 | } 14 | 15 | // Get a value from the querystring 16 | function findGetParameter(parameterName) { 17 | var result = null, 18 | tmp = []; 19 | var items = location.search.substr(1).split("&"); 20 | for (var index = 0; index < items.length; index++) { 21 | tmp = items[index].split("="); 22 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 23 | } 24 | return result; 25 | } 26 | 27 | // Load the article contents into the page 28 | function loadArticle(){ 29 | // Get the details for the article 30 | var articleId = findGetParameter('id'); 31 | var articleUrl = './data/data-' + articleId + '.json'; 32 | retrieveData(articleUrl); 33 | } 34 | 35 | // Build the web page with the resulting data 36 | function buildWebPage(result){ 37 | document.getElementById('article').innerHTML = result.description; 38 | document.getElementById('article-title').innerHTML = result.title; 39 | } 40 | 41 | // Show an offline notification if the user if offline 42 | function showIndicator() { 43 | offlineNotification.innerHTML = 'You are currently offline.'; 44 | offlineNotification.className = 'showOfflineNotification'; 45 | } 46 | 47 | // Hide the offline notification when the user comes back online 48 | function hideIndicator() { 49 | offlineNotification.className = 'hideOfflineNotification'; 50 | } 51 | 52 | // Update the online status icon based on connectivity 53 | window.addEventListener('online', hideIndicator); 54 | window.addEventListener('offline', showIndicator); 55 | 56 | loadArticle(); 57 | -------------------------------------------------------------------------------- /chapter-8/js/article.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | function retrieveData(url) { 4 | var xhttp = new XMLHttpRequest(); 5 | xhttp.onreadystatechange = function() { 6 | if (this.readyState == 4 && this.status == 200) { 7 | var result = JSON.parse(this.responseText); 8 | buildWebPage(result); 9 | } 10 | }; 11 | xhttp.open('GET', url, true); 12 | xhttp.send(); 13 | } 14 | 15 | // Get a value from the querystring 16 | function findGetParameter(parameterName) { 17 | var result = null, 18 | tmp = []; 19 | var items = location.search.substr(1).split("&"); 20 | for (var index = 0; index < items.length; index++) { 21 | tmp = items[index].split("="); 22 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 23 | } 24 | return result; 25 | } 26 | 27 | // Load the article contents into the page 28 | function loadArticle(){ 29 | // Get the details for the article 30 | var articleId = findGetParameter('id'); 31 | var articleUrl = './data/data-' + articleId + '.json'; 32 | retrieveData(articleUrl); 33 | } 34 | 35 | // Build the web page with the resulting data 36 | function buildWebPage(result){ 37 | document.getElementById('article').innerHTML = result.description; 38 | document.getElementById('article-title').innerHTML = result.title; 39 | } 40 | 41 | // Show an offline notification if the user if offline 42 | function showIndicator() { 43 | offlineNotification.innerHTML = 'You are currently offline.'; 44 | offlineNotification.className = 'showOfflineNotification'; 45 | } 46 | 47 | // Hide the offline notification when the user comes back online 48 | function hideIndicator() { 49 | offlineNotification.className = 'hideOfflineNotification'; 50 | } 51 | 52 | // Update the online status icon based on connectivity 53 | window.addEventListener('online', hideIndicator); 54 | window.addEventListener('offline', showIndicator); 55 | 56 | loadArticle(); 57 | -------------------------------------------------------------------------------- /chapter-9/public/js/article.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | function retrieveData(url) { 4 | var xhttp = new XMLHttpRequest(); 5 | xhttp.onreadystatechange = function() { 6 | if (this.readyState == 4 && this.status == 200) { 7 | var result = JSON.parse(this.responseText); 8 | buildWebPage(result); 9 | } 10 | }; 11 | xhttp.open('GET', url, true); 12 | xhttp.send(); 13 | } 14 | 15 | // Get a value from the querystring 16 | function findGetParameter(parameterName) { 17 | var result = null, 18 | tmp = []; 19 | var items = location.search.substr(1).split("&"); 20 | for (var index = 0; index < items.length; index++) { 21 | tmp = items[index].split("="); 22 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 23 | } 24 | return result; 25 | } 26 | 27 | // Load the article contents into the page 28 | function loadArticle(){ 29 | // Get the details for the article 30 | var articleId = findGetParameter('id'); 31 | var articleUrl = './data/data-' + articleId + '.json'; 32 | retrieveData(articleUrl); 33 | } 34 | 35 | // Build the web page with the resulting data 36 | function buildWebPage(result){ 37 | document.getElementById('article').innerHTML = result.description; 38 | document.getElementById('article-title').innerHTML = result.title; 39 | } 40 | 41 | // Show an offline notification if the user if offline 42 | function showIndicator() { 43 | offlineNotification.innerHTML = 'You are currently offline.'; 44 | offlineNotification.className = 'showOfflineNotification'; 45 | } 46 | 47 | // Hide the offline notification when the user comes back online 48 | function hideIndicator() { 49 | offlineNotification.className = 'hideOfflineNotification'; 50 | } 51 | 52 | // Update the online status icon based on connectivity 53 | window.addEventListener('online', hideIndicator); 54 | window.addEventListener('offline', showIndicator); 55 | 56 | loadArticle(); 57 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Progressive Web Apps - A book by Dean Hume 2 | This repository contains the source code for the book *Progressive Web Apps* by Dean Hume. 3 | 4 | Dean Hume - Progressive Web Apps 5 | > Progressive Web Apps takes you, step-by-step, through real world examples and teaches you how to build fast, engaging, and reliable websites. You'll begin by getting the big picture of what Progressive Web Apps are, how they work, and their benefits. Then you'll explore the different approaches that you can use to build them. This hands-on guide gives you a closer look as you dissect a real-world PWA and break down each of its elements. 6 | > 7 | > -- [Progressive Web Apps - Manning - Dean Hume](https://www.manning.com/books/progressive-web-apps) 8 | 9 | ## Contents 10 | 11 | Sample code for each chapter: 12 | 13 | - Chapter 3 - [Caching](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-3) 14 | - Chapter 4 - [Intercepting Network Requests](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-4) 15 | - Chapter 5 - [Look and feel](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-5) 16 | - Chapter 6 - [Push notifications](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-6) 17 | - Chapter 7 - [Offline browsing](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-7) 18 | - Chapter 8 - [Building more resilient applications](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-8) 19 | - Chapter 9 - [Keeping your data synchronised](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-9) 20 | - Chapter 10 - [Streaming data](https://github.com/deanhume/progressive-web-apps-book/tree/master/chapter-10) 21 | - Chapter 12 - [Troubleshooting Progressive Web Apps](https://github.com/deanhume/pwa-tips-tricks) 22 | -------------------------------------------------------------------------------- /chapter-10/with-and-without-streaming/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', event => { 2 | self.skipWaiting(); 3 | }); 4 | 5 | self.addEventListener('activate', event => { 6 | clients.claim(); 7 | }); 8 | 9 | self.addEventListener('fetch', event => { 10 | const requestURL = new URL(event.request.url); 11 | 12 | if (requestURL.origin != location.origin) return; 13 | 14 | if (requestURL.pathname.endsWith('with.html')) { 15 | event.respondWith(htmlStream()); 16 | } 17 | }); 18 | 19 | function retab(str) { 20 | // remove blank lines 21 | str = str.replace(/^\s*\n|\n\s*$/g, ''); 22 | const firstIndent = /^\s*/.exec(str)[0]; 23 | return str.replace(RegExp('^' + firstIndent, 'mg'), ''); 24 | } 25 | 26 | self.addEventListener('fetch', event => { 27 | event.respondWith(htmlStream()); 28 | }); 29 | 30 | function htmlStream() { 31 | const html = retab(`

This specification provides APIs for creating, composing, and consuming streams of data. These streams are designed to map efficiently to low-level I/O primitives, and allow easy composition with built-in backpressure and queuing. On top of streams, the web platform can build higher-level abstractions, such as filesystem or socket APIs, while at the same time users can use the supplied tools to build their own streams which integrate well with those of the web platform.

`); 32 | 33 | const stream = new ReadableStream({ 34 | start: controller => { 35 | const encoder = new TextEncoder(); 36 | let pos = 0; 37 | let chunkSize = 1; 38 | 39 | function push() { 40 | if (pos >= html.length) { 41 | controller.close(); 42 | return; 43 | } 44 | 45 | controller.enqueue( 46 | encoder.encode(html.slice(pos, pos + chunkSize)) 47 | ); 48 | 49 | pos += chunkSize; 50 | setTimeout(push, 50); 51 | } 52 | 53 | push(); 54 | } 55 | }); 56 | 57 | return new Response(stream, { 58 | headers: { 59 | 'Content-Type': 'text/html' 60 | } 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Online News 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 24 | 25 |

Nose picking ban for Manila police

A new directive tells police in Manila not to pick their noses whilst on duty

Australian police find gun in biker's bottom

Police in Australia find a loaded hangun wedged in the behind of a gang linked motorcyclist

Town's giant dog dropping goes missing

Local police on the trail of a missing inflatable dog mess

Austrian man wins right to be called Zebra

An Austrian man has won a court case enabling him to change his family name back to 'Zebra'

26 |
27 | 28 | 29 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /chapter-9/public/js/contact.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | // Show an offline notification if the user if offline 4 | function showIndicator() { 5 | offlineNotification.innerHTML = 'You are currently offline.'; 6 | offlineNotification.className = 'showOfflineNotification'; 7 | } 8 | 9 | // Hide the offline notification when the user comes back online 10 | function hideIndicator() { 11 | offlineNotification.className = 'hideOfflineNotification'; 12 | } 13 | 14 | // Notify the user that the message is either queued or sent 15 | function displayMessageNotification(notificationText){ 16 | var messageNotification = document.getElementById('message'); 17 | messageNotification.innerHTML = notificationText; 18 | messageNotification.className = 'showMessageNotification'; 19 | } 20 | 21 | // Send the actual message 22 | function sendMessage(){ 23 | console.log('sendMessage'); 24 | 25 | var payload = { 26 | name: document.getElementById('name').value, 27 | email: document.getElementById('email').value, 28 | subject: document.getElementById('subject').value, 29 | message: document.getElementById('message').value, 30 | }; 31 | 32 | // Send the POST request to the server 33 | return fetch('/sendMessage/', { 34 | method: 'post', 35 | headers: new Headers({ 36 | 'content-type': 'application/json' 37 | }), 38 | body: JSON.stringify(payload) 39 | }); 40 | } 41 | 42 | // Queue the message till the sync takes place 43 | function queueMessage(){ 44 | console.log('Message queued'); 45 | 46 | var payload = { 47 | name: document.getElementById('name').value, 48 | email: document.getElementById('email').value, 49 | subject: document.getElementById('subject').value, 50 | message: document.getElementById('message').value, 51 | }; 52 | 53 | // Save to indexdb 54 | idbKeyval.set('sendMessage', payload); 55 | } 56 | 57 | // Update the online status icon based on connectivity 58 | window.addEventListener('online', hideIndicator); 59 | window.addEventListener('offline', showIndicator); 60 | -------------------------------------------------------------------------------- /chapter-7/js/main.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | function retrieveData(url) { 4 | var xhttp = new XMLHttpRequest(); 5 | xhttp.onreadystatechange = function() { 6 | if (this.readyState == 4 && this.status == 200) { 7 | var result = JSON.parse(this.responseText); 8 | buildWebPage(result); 9 | } 10 | }; 11 | xhttp.open('GET', url, true); 12 | xhttp.send(); 13 | } 14 | 15 | // Get a value from the querystring 16 | function findGetParameter(parameterName) { 17 | var result = null, 18 | tmp = []; 19 | var items = location.search.substr(1).split("&"); 20 | for (var index = 0; index < items.length; index++) { 21 | tmp = items[index].split("="); 22 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 23 | } 24 | return result; 25 | } 26 | 27 | // Load the latest news data and populate content 28 | function loadLatestNews(){ 29 | var dataUrl = './data/latest.json'; 30 | var result = retrieveData(dataUrl); 31 | } 32 | 33 | function buildWebPage(result) { 34 | // Build up our HTML 35 | var latestNews = ''; 36 | 37 | // Loop through the results 38 | for (var i = 0; i < result.latestNews.length; i++) { 39 | 40 | var title = '

' + result.latestNews[i].title + '

'; 41 | var description = '

' + result.latestNews[i].description + '

' 42 | 43 | latestNews += title + description; 44 | } 45 | 46 | // Update the DOM with the data 47 | document.getElementById('latest').innerHTML = latestNews; 48 | } 49 | 50 | // Show an offline notification if the user if offline 51 | function showIndicator() { 52 | offlineNotification.innerHTML = 'You are currently offline.'; 53 | offlineNotification.className = 'showOfflineNotification'; 54 | } 55 | 56 | // Hide the offline notification when the user comes back online 57 | function hideIndicator() { 58 | offlineNotification.className = 'hideOfflineNotification'; 59 | } 60 | 61 | // Update the online status icon based on connectivity 62 | window.addEventListener('online', hideIndicator); 63 | window.addEventListener('offline', showIndicator); 64 | 65 | loadLatestNews(); 66 | -------------------------------------------------------------------------------- /chapter-8/js/main.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | function retrieveData(url) { 4 | var xhttp = new XMLHttpRequest(); 5 | xhttp.onreadystatechange = function() { 6 | if (this.readyState == 4 && this.status == 200) { 7 | var result = JSON.parse(this.responseText); 8 | buildWebPage(result); 9 | } 10 | }; 11 | xhttp.open('GET', url, true); 12 | xhttp.send(); 13 | } 14 | 15 | // Get a value from the querystring 16 | function findGetParameter(parameterName) { 17 | var result = null, 18 | tmp = []; 19 | var items = location.search.substr(1).split("&"); 20 | for (var index = 0; index < items.length; index++) { 21 | tmp = items[index].split("="); 22 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 23 | } 24 | return result; 25 | } 26 | 27 | // Load the latest news data and populate content 28 | function loadLatestNews(){ 29 | var dataUrl = './data/latest.json'; 30 | var result = retrieveData(dataUrl); 31 | } 32 | 33 | function buildWebPage(result) { 34 | // Build up our HTML 35 | var latestNews = ''; 36 | 37 | // Loop through the results 38 | for (var i = 0; i < result.latestNews.length; i++) { 39 | 40 | var title = '

' + result.latestNews[i].title + '

'; 41 | var description = '

' + result.latestNews[i].description + '

' 42 | 43 | latestNews += title + description; 44 | } 45 | 46 | // Update the DOM with the data 47 | document.getElementById('latest').innerHTML = latestNews; 48 | } 49 | 50 | // Show an offline notification if the user if offline 51 | function showIndicator() { 52 | offlineNotification.innerHTML = 'You are currently offline.'; 53 | offlineNotification.className = 'showOfflineNotification'; 54 | } 55 | 56 | // Hide the offline notification when the user comes back online 57 | function hideIndicator() { 58 | offlineNotification.className = 'hideOfflineNotification'; 59 | } 60 | 61 | // Update the online status icon based on connectivity 62 | window.addEventListener('online', hideIndicator); 63 | window.addEventListener('offline', showIndicator); 64 | 65 | loadLatestNews(); 66 | -------------------------------------------------------------------------------- /chapter-9/public/js/main.js: -------------------------------------------------------------------------------- 1 | var offlineNotification = document.getElementById('offline'); 2 | 3 | function retrieveData(url) { 4 | var xhttp = new XMLHttpRequest(); 5 | xhttp.onreadystatechange = function() { 6 | if (this.readyState == 4 && this.status == 200) { 7 | var result = JSON.parse(this.responseText); 8 | buildWebPage(result); 9 | } 10 | }; 11 | xhttp.open('GET', url, true); 12 | xhttp.send(); 13 | } 14 | 15 | // Get a value from the querystring 16 | function findGetParameter(parameterName) { 17 | var result = null, 18 | tmp = []; 19 | var items = location.search.substr(1).split("&"); 20 | for (var index = 0; index < items.length; index++) { 21 | tmp = items[index].split("="); 22 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 23 | } 24 | return result; 25 | } 26 | 27 | // Load the latest news data and populate content 28 | function loadLatestNews(){ 29 | var dataUrl = './data/latest.json'; 30 | var result = retrieveData(dataUrl); 31 | } 32 | 33 | function buildWebPage(result) { 34 | // Build up our HTML 35 | var latestNews = ''; 36 | 37 | // Loop through the results 38 | for (var i = 0; i < result.latestNews.length; i++) { 39 | 40 | var title = '

' + result.latestNews[i].title + '

'; 41 | var description = '

' + result.latestNews[i].description + '

' 42 | 43 | latestNews += title + description; 44 | } 45 | 46 | // Update the DOM with the data 47 | document.getElementById('latest').innerHTML = latestNews; 48 | } 49 | 50 | // Show an offline notification if the user if offline 51 | function showIndicator() { 52 | offlineNotification.innerHTML = 'You are currently offline.'; 53 | offlineNotification.className = 'showOfflineNotification'; 54 | } 55 | 56 | // Hide the offline notification when the user comes back online 57 | function hideIndicator() { 58 | offlineNotification.className = 'hideOfflineNotification'; 59 | } 60 | 61 | // Update the online status icon based on connectivity 62 | window.addEventListener('online', hideIndicator); 63 | window.addEventListener('offline', showIndicator); 64 | 65 | loadLatestNews(); 66 | -------------------------------------------------------------------------------- /chapter-8/sw.js: -------------------------------------------------------------------------------- 1 | const cacheName = 'latestNews-v1'; 2 | const offlineUrl = 'offline-page.html'; 3 | 4 | // Cache our known resources during install 5 | self.addEventListener('install', event => { 6 | event.waitUntil( 7 | caches.open(cacheName) 8 | .then(cache => cache.addAll([ 9 | './js/main.js', 10 | './js/article.js', 11 | './images/newspaper.svg', 12 | './css/site.css', 13 | './data/latest.json', 14 | './data/data-1.json', 15 | './article.html', 16 | './index.html', 17 | offlineUrl 18 | ])) 19 | ); 20 | }); 21 | 22 | function timeout(delay) { 23 | return new Promise(function (resolve, reject) { 24 | setTimeout(function () { 25 | resolve(new Response('', { 26 | status: 408, 27 | statusText: 'Request timed out.' 28 | })); 29 | }, delay); 30 | }); 31 | } 32 | 33 | self.addEventListener('fetch', function(event) { 34 | 35 | // Check for the googleapis domain 36 | if (/googleapis/.test(event.request.url)) { 37 | return event.respondWith(Promise.race([timeout(3000),fetch(event.request.url)])); 38 | } 39 | 40 | // Else process all other requests as expected 41 | return event.respondWith( 42 | caches.match(event.request) 43 | .then(function(response) { 44 | if (response) { 45 | return response; 46 | } 47 | var requestToCache = event.request.clone(); 48 | 49 | return fetch(requestToCache).then( 50 | function(response) { 51 | if(!response || response.status !== 200) { 52 | return response; 53 | } 54 | 55 | var responseToCache = response.clone(); 56 | caches.open(cacheName) 57 | .then(function(cache) { 58 | cache.put(requestToCache, responseToCache); 59 | }); 60 | 61 | return response; 62 | } 63 | ).catch(error => { 64 | // Check if the user is offline first and is trying to navigate to a web page 65 | if (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html')) { 66 | // Return the offline page 67 | return caches.match(offlineUrl); 68 | } 69 | }); 70 | }) 71 | ); 72 | }); 73 | -------------------------------------------------------------------------------- /chapter-9/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Progressive Times - Contact Us 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/public/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'latestNews-v1'; 2 | 3 | // Cache our known resources during install 4 | self.addEventListener('install', event => { 5 | event.waitUntil( 6 | caches.open(cacheName) 7 | .then(cache => cache.addAll([ 8 | './js/main.js', 9 | './js/article.js', 10 | './images/newspaper.svg', 11 | './css/site.css', 12 | './data/latest.json', 13 | './data/data-1.json', 14 | './article', 15 | './' 16 | ])) 17 | ); 18 | }); 19 | 20 | self.addEventListener('push', function (event) { 21 | 22 | var payload = event.data ? JSON.parse(event.data.text()) : 'no payload'; 23 | 24 | var title = 'Progressive Times'; 25 | 26 | // Determine the type of notification to display 27 | if (payload.type === 'register') { 28 | event.waitUntil( 29 | self.registration.showNotification(title, { 30 | body: payload.msg, 31 | url: payload.url, 32 | icon: payload.icon 33 | }) 34 | ); 35 | } else if (payload.type === 'actionMessage') { 36 | event.waitUntil( 37 | self.registration.showNotification(title, { 38 | body: payload.msg, 39 | url: payload.url, 40 | icon: payload.icon, 41 | actions: [ 42 | { action: 'voteup', title: '👍 Vote Up' }, 43 | { action: 'votedown', title: '👎 Vote Down' }] 44 | }) 45 | ); 46 | } 47 | }); 48 | 49 | self.addEventListener('notificationclick', function (event) { 50 | event.notification.close(); 51 | 52 | // Check if any actions were added 53 | if (event.action === 'voteup') { 54 | clients.openWindow('http://localhost:3111/voteup'); 55 | } 56 | else if (event.action === 'voteup') { 57 | clients.openWindow('http://localhost:3111/votedown'); 58 | } 59 | else { 60 | clients.openWindow('http://localhost:3111'); 61 | } 62 | }, false); 63 | 64 | // Cache any new resources as they are fetched 65 | self.addEventListener('fetch', function (event) { 66 | event.respondWith( 67 | caches.match(event.request, { ignoreSearch: true }) 68 | .then(function (response) { 69 | if (response) { 70 | return response; 71 | } 72 | var fetchRequest = event.request.clone(); 73 | 74 | return fetch(fetchRequest).then( 75 | function (response) { 76 | if (!response || response.status !== 200) { 77 | return response; 78 | } 79 | 80 | var responseToCache = response.clone(); 81 | caches.open(cacheName) 82 | .then(function (cache) { 83 | cache.put(event.request, responseToCache); 84 | }); 85 | 86 | return response; 87 | } 88 | ); 89 | }) 90 | ); 91 | }); 92 | -------------------------------------------------------------------------------- /chapter-9/public/js/idb-keyval.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | var db; 4 | 5 | function getDB() { 6 | if (!db) { 7 | db = new Promise(function(resolve, reject) { 8 | var openreq = indexedDB.open('keyval-store', 1); 9 | 10 | openreq.onerror = function() { 11 | reject(openreq.error); 12 | }; 13 | 14 | openreq.onupgradeneeded = function() { 15 | // First time setup: create an empty object store 16 | openreq.result.createObjectStore('keyval'); 17 | }; 18 | 19 | openreq.onsuccess = function() { 20 | resolve(openreq.result); 21 | }; 22 | }); 23 | } 24 | return db; 25 | } 26 | 27 | function withStore(type, callback) { 28 | return getDB().then(function(db) { 29 | return new Promise(function(resolve, reject) { 30 | var transaction = db.transaction('keyval', type); 31 | transaction.oncomplete = function() { 32 | resolve(); 33 | }; 34 | transaction.onerror = function() { 35 | reject(transaction.error); 36 | }; 37 | callback(transaction.objectStore('keyval')); 38 | }); 39 | }); 40 | } 41 | 42 | var idbKeyval = { 43 | get: function(key) { 44 | var req; 45 | return withStore('readonly', function(store) { 46 | req = store.get(key); 47 | }).then(function() { 48 | return req.result; 49 | }); 50 | }, 51 | set: function(key, value) { 52 | return withStore('readwrite', function(store) { 53 | store.put(value, key); 54 | }); 55 | }, 56 | delete: function(key) { 57 | return withStore('readwrite', function(store) { 58 | store.delete(key); 59 | }); 60 | }, 61 | clear: function() { 62 | return withStore('readwrite', function(store) { 63 | store.clear(); 64 | }); 65 | }, 66 | keys: function() { 67 | var keys = []; 68 | return withStore('readonly', function(store) { 69 | // This would be store.getAllKeys(), but it isn't supported by Edge or Safari. 70 | // And openKeyCursor isn't supported by Safari. 71 | (store.openKeyCursor || store.openCursor).call(store).onsuccess = function() { 72 | if (!this.result) return; 73 | keys.push(this.result.key); 74 | this.result.continue(); 75 | }; 76 | }).then(function() { 77 | return keys; 78 | }); 79 | } 80 | }; 81 | 82 | if (typeof module != 'undefined' && module.exports) { 83 | module.exports = idbKeyval; 84 | } else if (typeof define === 'function' && define.amd) { 85 | define('idbKeyval', [], function() { 86 | return idbKeyval; 87 | }); 88 | } else { 89 | self.idbKeyval = idbKeyval; 90 | } 91 | }()); -------------------------------------------------------------------------------- /chapter-9/public/css/site.css: -------------------------------------------------------------------------------- 1 | body{ font-family: 'Raleway', sans-serif; } 2 | h1 { text-align: center;font-size: 60px;font-family: 'Merriweather', cursive; margin-top: 0px;} 3 | h6 {text-align: center; font-size: 20px; margin-top: -32px;} 4 | hr { border-top: 1px dashed #8c8b8b; width: 300px; } 5 | a { text-decoration: none; color: black; } 6 | a:hover { text-decoration: underline; } 7 | p { width: 70%; margin: auto; text-align: center;padding-bottom: 20px; font-size: 18px; } 8 | #header { background-color: gold; width: 100%; position: absolute; top: 0; left: 0; height: 200px;} 9 | #latest { text-align: center; margin-top: 220px; } 10 | #article-header { background-color: #03A9F4; width: 100%; position: absolute; top: 0; left: 0; height: 200px; } 11 | #article-title { text-align: center; font-size: 54px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 12 | #article { margin-top: 220px;} 13 | #logo { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 25px; } 14 | 15 | /* Offline Page */ 16 | #header-offline { background-color: aquamarine; width: 100%; position: absolute; top: 0; left: 0; height: 600px;} 17 | #logo-offline { margin-left: auto; margin-right: auto; width: 50px; display: block; padding-top: 125px; } 18 | 19 | .showOfflineNotification { background-color: black;height: 40px;color: white;text-align: center;padding-top: 20px;font-size: 20px;margin-top: 0px;position: absolute;top: 0;left: 0;right: 0;overflow: hidden;z-index: 101; } 20 | .showMessageNotification { background-color: black;height: 40px;color: white;text-align: center;padding-top: 20px;font-size: 20px;margin-top: 0px;position: absolute;bottom: 0;left: 0;right: 0;overflow: hidden;z-index: 101; } 21 | .hideOfflineNotification { display:none; } 22 | 23 | @media screen and (max-width: 550px) { 24 | h1 { text-align: center;font-size: 40px;font-family: 'Merriweather', cursive; margin-top: 0px;} 25 | #article-title { text-align: center; font-size: 40px; font-family: 'Merriweather', cursive; color: white;padding-top: 50px; } 26 | } 27 | 28 | .contact-form{ 29 | margin-top: 220px; 30 | width: 100%; 31 | } 32 | 33 | input{ 34 | font-size: 16px; 35 | padding: 12px 16px; 36 | height: 58px; 37 | width: 80%; 38 | display: block; 39 | border: 1px solid rgba(0,0,0,.12); 40 | border-bottom-width: 2px; 41 | border-bottom-color: #dedede; 42 | background: #fff; 43 | border-radius: 2px; 44 | } 45 | 46 | button{ 47 | background: 0 0; 48 | border: none; 49 | border-radius: 2px; 50 | color: #000; 51 | height: 58px; 52 | margin: 6px 0 0; 53 | width: 60%; 54 | max-width: 60%; 55 | padding: 0 16px; 56 | display: inline-block; 57 | font-weight: 300; 58 | text-transform: uppercase; 59 | letter-spacing: .025rem; 60 | overflow: hidden; 61 | will-change: box-shadow; 62 | transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1); 63 | outline: 0; 64 | cursor: pointer; 65 | text-decoration: none; 66 | text-align: center; 67 | line-height: 58px; 68 | -webkit-appearance: none; 69 | color: black; 70 | background-color: gold; 71 | font-size: 14px; 72 | font-weight: 500; 73 | } 74 | 75 | #container { 76 | margin: 0 auto; 77 | width: 80%; 78 | } 79 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/server.js: -------------------------------------------------------------------------------- 1 | const webpush = require('web-push'); 2 | const express = require('express'); 3 | var bodyParser = require('body-parser'); 4 | var path = require('path'); 5 | const app = express(); 6 | 7 | // Express setup 8 | app.use(express.static('public')); 9 | app.use(bodyParser.json()); 10 | app.use(bodyParser.urlencoded({ // to support URL-encoded bodies 11 | extended: true 12 | })); 13 | 14 | function saveRegistrationDetails(endpoint, key, authSecret) { 15 | // Save the users details in a DB 16 | } 17 | 18 | webpush.setVapidDetails( 19 | 'mailto:contact@deanhume.com', 20 | 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY', 21 | 'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0' 22 | ); 23 | 24 | // Home page 25 | app.get('/', function (req, res) { 26 | res.sendFile(path.join(__dirname + '/index.html')); 27 | }); 28 | 29 | // Article page 30 | app.get('/article', function (req, res) { 31 | res.sendFile(path.join(__dirname + '/article.html')); 32 | }); 33 | 34 | // Send a message 35 | app.post('/sendMessage', function (req, res) { 36 | 37 | var endpoint = req.body.endpoint; 38 | var authSecret = req.body.authSecret; 39 | var key = req.body.key; 40 | 41 | const pushSubscription = { 42 | endpoint: req.body.endpoint, 43 | keys: { 44 | auth: authSecret, 45 | p256dh: key 46 | } 47 | }; 48 | 49 | var body = 'Breaking News: Nose picking ban for Manila police'; 50 | var iconUrl = 'https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/master/chapter-6/push-notifications/public/images/homescreen.png'; 51 | 52 | webpush.sendNotification(pushSubscription, 53 | JSON.stringify({ 54 | msg: body, 55 | url: 'http://localhost:3111/article?id=1', 56 | icon: iconUrl, 57 | type: 'actionMessage' 58 | })) 59 | .then(result => { 60 | console.log(result); 61 | res.sendStatus(201); 62 | }) 63 | .catch(err => { 64 | console.log(err); 65 | }); 66 | }); 67 | 68 | // Register the user 69 | app.post('/register', function (req, res) { 70 | 71 | var endpoint = req.body.endpoint; 72 | var authSecret = req.body.authSecret; 73 | var key = req.body.key; 74 | 75 | // Store the users registration details 76 | saveRegistrationDetails(endpoint, key, authSecret); 77 | 78 | const pushSubscription = { 79 | endpoint: req.body.endpoint, 80 | keys: { 81 | auth: authSecret, 82 | p256dh: key 83 | } 84 | }; 85 | 86 | var body = 'Thank you for registering'; 87 | var iconUrl = 'https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/master/chapter-6/push-notifications/public/images/homescreen.png'; 88 | 89 | webpush.sendNotification(pushSubscription, 90 | JSON.stringify({ 91 | msg: body, 92 | url: 'https://localhost:3111', 93 | icon: iconUrl, 94 | type: 'register' 95 | })) 96 | .then(result => { 97 | console.log(result); 98 | res.sendStatus(201); 99 | }) 100 | .catch(err => { 101 | console.log(err); 102 | }); 103 | 104 | }); 105 | 106 | // The server 107 | app.listen(3111, function () { 108 | console.log('Example app listening on port 3111!') 109 | }); 110 | -------------------------------------------------------------------------------- /chapter-9/public/sw.js: -------------------------------------------------------------------------------- 1 | // Load the sw-toolbox library. 2 | importScripts('./js/idb-keyval.js'); 3 | 4 | const cacheName = 'latestNews-v1'; 5 | const offlineUrl = '/offline'; 6 | 7 | // Cache our known resources during install 8 | self.addEventListener('install', event => { 9 | event.waitUntil( 10 | caches.open(cacheName) 11 | .then(cache => cache.addAll([ 12 | './js/main.js', 13 | './js/article.js', 14 | './images/newspaper.svg', 15 | './css/site.css', 16 | './data/latest.json', 17 | './data/data-1.json', 18 | './article', 19 | './', 20 | offlineUrl 21 | ])) 22 | ); 23 | }); 24 | 25 | // Handle network delays 26 | function timeout(delay) { 27 | return new Promise(function (resolve, reject) { 28 | setTimeout(function () { 29 | resolve(new Response('', { 30 | status: 408, 31 | statusText: 'Request timed out.' 32 | })); 33 | }, delay); 34 | }); 35 | } 36 | 37 | function resolveFirstPromise(promises) { 38 | return new Promise((resolve, reject) => { 39 | 40 | promises = promises.map(p => Promise.resolve(p)); 41 | 42 | promises.forEach(p => p.then(resolve)); 43 | 44 | promises.reduce((a, b) => a.catch(() => b)) 45 | .catch(() => reject(Error("All failed"))); 46 | }); 47 | }; 48 | 49 | 50 | self.addEventListener('fetch', function(event) { 51 | 52 | // Check for the googleapis domain 53 | if (/googleapis/.test(event.request.url)) { 54 | event.respondWith( 55 | resolveFirstPromise([ 56 | timeout(500), 57 | fetch(event.request) 58 | ]) 59 | ); 60 | } else { 61 | 62 | // Else process all other requests as expected 63 | event.respondWith( 64 | caches.match(event.request) 65 | .then(function(response) { 66 | if (response) { 67 | return response; 68 | } 69 | var fetchRequest = event.request.clone(); 70 | 71 | fetch(fetchRequest).then( 72 | function(response) { 73 | if(!response || response.status !== 200) { 74 | return response; 75 | } 76 | 77 | var responseToCache = response.clone(); 78 | caches.open(cacheName) 79 | .then(function(cache) { 80 | cache.put(event.request, responseToCache); 81 | }); 82 | 83 | return response; 84 | } 85 | ).catch(error => { 86 | // Check if the user is offline first and is trying to navigate to a web page 87 | if (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html')) { 88 | // Return the offline page 89 | caches.match(offlineUrl); 90 | } 91 | }); 92 | }) 93 | )} 94 | }); 95 | 96 | // The sync event for the contact form 97 | self.addEventListener('sync', function (event) { 98 | if (event.tag === 'contact-email') { 99 | event.waitUntil( 100 | idbKeyval.get('sendMessage').then(value => 101 | fetch('/sendMessage/', { 102 | method: 'POST', 103 | headers: new Headers({ 'content-type': 'application/json' }), 104 | body: JSON.stringify(value) 105 | }))); 106 | 107 | // Remove the value from the DB 108 | idbKeyval.delete('sendMessage'); 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Progressive Times - Online News 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 |
27 |
28 | 29 | 30 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Progressive Times - Article 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 |

24 |
25 | 26 | 27 | 101 | 102 | -------------------------------------------------------------------------------- /chapter-10/streaming-render/sw.js: -------------------------------------------------------------------------------- 1 | const cacheName = 'latestNews-v1'; 2 | const offlineUrl = 'offline-page.html'; 3 | 4 | // Cache our known resources during install 5 | self.addEventListener('install', event => { 6 | // Force the current service worker to become the active one 7 | self.skipWaiting(); 8 | 9 | event.waitUntil( 10 | caches.open(cacheName) 11 | .then(cache => cache.addAll([ 12 | './js/main.js', 13 | './images/newspaper.svg', 14 | './css/site.css', 15 | './header.html', 16 | './footer.html', 17 | offlineUrl 18 | ])) 19 | ); 20 | }); 21 | 22 | // We want the service worker to start controlling as soon as possible 23 | self.addEventListener('activate', function(event) { 24 | self.clients.claim(); 25 | }); 26 | 27 | function timeout(delay) { 28 | return new Promise(function (resolve, reject) { 29 | setTimeout(function () { 30 | resolve(new Response('', { 31 | status: 408, 32 | statusText: 'Request timed out.' 33 | })); 34 | }, delay); 35 | }); 36 | } 37 | 38 | function resolveFirstPromise(promises) { 39 | return new Promise((resolve, reject) => { 40 | 41 | promises = promises.map(p => Promise.resolve(p)); 42 | 43 | promises.forEach(p => p.then(resolve)); 44 | 45 | promises.reduce((a, b) => a.catch(() => b)) 46 | .catch(() => reject(Error("All failed"))); 47 | }); 48 | }; 49 | 50 | function streamArticle(url) { 51 | try { 52 | new ReadableStream({}); 53 | } 54 | catch (e) { 55 | return new Response("Streams not supported"); 56 | } 57 | const stream = new ReadableStream({ 58 | start(controller) { 59 | const startFetch = caches.match('./header.html'); 60 | const bodyData = fetch(`./data/${url}.html`).catch(() => new Response('Body fetch failed')); 61 | const endFetch = caches.match('./footer.html'); 62 | 63 | function pushStream(stream) { 64 | const reader = stream.getReader(); 65 | function read() { 66 | return reader.read().then(result => { 67 | if (result.done) return; 68 | controller.enqueue(result.value); 69 | return read(); 70 | }); 71 | } 72 | return read(); 73 | } 74 | 75 | startFetch 76 | .then(response => pushStream(response.body)) 77 | .then(() => bodyData) 78 | .then(response => pushStream(response.body)) 79 | .then(() => endFetch) 80 | .then(response => pushStream(response.body)) 81 | .then(() => controller.close()); 82 | } 83 | }); 84 | 85 | return new Response(stream, { 86 | headers: { 'Content-Type': 'text/html' } 87 | }) 88 | } 89 | 90 | // Get the value from the querystring 91 | function getQueryString ( field, url = window.location.href ) { 92 | var reg = new RegExp( '[?&]' + field + '=([^&#]*)', 'i' ); 93 | var string = reg.exec(url); 94 | return string ? string[1] : null; 95 | }; 96 | 97 | 98 | self.addEventListener('fetch', function (event) { 99 | 100 | const url = new URL(event.request.url); 101 | 102 | if (url.pathname.endsWith('/article.html')) { 103 | 104 | // Get the ID of the article 105 | const articleId = getQueryString('id'); 106 | 107 | // Get the URL of the article 108 | const articleUrl = `data-${articleId}`; 109 | 110 | // Respond with a stream 111 | event.respondWith(streamArticle(articleUrl)); 112 | 113 | } else if (url.pathname.endsWith('/index.html')) { 114 | 115 | const indexUrl = 'data-index'; 116 | 117 | // Respond with a stream 118 | event.respondWith(streamArticle(indexUrl)); 119 | 120 | } else if (/googleapis/.test(event.request.url)) { 121 | 122 | // Check for the googleapis domain 123 | event.respondWith( 124 | resolveFirstPromise([ 125 | timeout(500), 126 | fetch(event.request) 127 | ]) 128 | ); 129 | 130 | } else { 131 | 132 | // Else process all other requests as expected 133 | event.respondWith( 134 | caches.match(event.request) 135 | .then(function(response) { 136 | if (response) { 137 | return response; 138 | } 139 | var fetchRequest = event.request.clone(); 140 | 141 | return fetch(fetchRequest).then( 142 | function(response) { 143 | if(!response || response.status !== 200) { 144 | return response; 145 | } 146 | 147 | var responseToCache = response.clone(); 148 | caches.open(cacheName) 149 | .then(function(cache) { 150 | cache.put(event.request, responseToCache); 151 | }); 152 | 153 | return response; 154 | } 155 | ).catch(error => { 156 | // Check if the user is offline first and is trying to navigate to a web page 157 | if (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html')) { 158 | // Return the offline page 159 | return caches.match(offlineUrl); 160 | } 161 | }); 162 | }) 163 | ); 164 | 165 | } 166 | }); 167 | -------------------------------------------------------------------------------- /chapter-9/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | accepts@~1.3.3: 4 | version "1.3.3" 5 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 6 | dependencies: 7 | mime-types "~2.1.11" 8 | negotiator "0.6.1" 9 | 10 | array-flatten@1.1.1: 11 | version "1.1.1" 12 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 13 | 14 | body-parser: 15 | version "1.17.1" 16 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47" 17 | dependencies: 18 | bytes "2.4.0" 19 | content-type "~1.0.2" 20 | debug "2.6.1" 21 | depd "~1.1.0" 22 | http-errors "~1.6.1" 23 | iconv-lite "0.4.15" 24 | on-finished "~2.3.0" 25 | qs "6.4.0" 26 | raw-body "~2.2.0" 27 | type-is "~1.6.14" 28 | 29 | bytes@2.4.0: 30 | version "2.4.0" 31 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" 32 | 33 | content-disposition@0.5.2: 34 | version "0.5.2" 35 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 36 | 37 | content-type@~1.0.2: 38 | version "1.0.2" 39 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 40 | 41 | cookie-signature@1.0.6: 42 | version "1.0.6" 43 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 44 | 45 | cookie@0.3.1: 46 | version "0.3.1" 47 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 48 | 49 | debug@2.6.1: 50 | version "2.6.1" 51 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" 52 | dependencies: 53 | ms "0.7.2" 54 | 55 | debug@2.6.3: 56 | version "2.6.3" 57 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" 58 | dependencies: 59 | ms "0.7.2" 60 | 61 | depd@~1.1.0, depd@1.1.0: 62 | version "1.1.0" 63 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 64 | 65 | destroy@~1.0.4: 66 | version "1.0.4" 67 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 68 | 69 | ee-first@1.1.1: 70 | version "1.1.1" 71 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 72 | 73 | encodeurl@~1.0.1: 74 | version "1.0.1" 75 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 76 | 77 | escape-html@~1.0.3: 78 | version "1.0.3" 79 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 80 | 81 | etag@~1.8.0: 82 | version "1.8.0" 83 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" 84 | 85 | express: 86 | version "4.15.2" 87 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" 88 | dependencies: 89 | accepts "~1.3.3" 90 | array-flatten "1.1.1" 91 | content-disposition "0.5.2" 92 | content-type "~1.0.2" 93 | cookie "0.3.1" 94 | cookie-signature "1.0.6" 95 | debug "2.6.1" 96 | depd "~1.1.0" 97 | encodeurl "~1.0.1" 98 | escape-html "~1.0.3" 99 | etag "~1.8.0" 100 | finalhandler "~1.0.0" 101 | fresh "0.5.0" 102 | merge-descriptors "1.0.1" 103 | methods "~1.1.2" 104 | on-finished "~2.3.0" 105 | parseurl "~1.3.1" 106 | path-to-regexp "0.1.7" 107 | proxy-addr "~1.1.3" 108 | qs "6.4.0" 109 | range-parser "~1.2.0" 110 | send "0.15.1" 111 | serve-static "1.12.1" 112 | setprototypeof "1.0.3" 113 | statuses "~1.3.1" 114 | type-is "~1.6.14" 115 | utils-merge "1.0.0" 116 | vary "~1.1.0" 117 | 118 | finalhandler@~1.0.0: 119 | version "1.0.1" 120 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.1.tgz#bcd15d1689c0e5ed729b6f7f541a6df984117db8" 121 | dependencies: 122 | debug "2.6.3" 123 | encodeurl "~1.0.1" 124 | escape-html "~1.0.3" 125 | on-finished "~2.3.0" 126 | parseurl "~1.3.1" 127 | statuses "~1.3.1" 128 | unpipe "~1.0.0" 129 | 130 | forwarded@~0.1.0: 131 | version "0.1.0" 132 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 133 | 134 | fresh@0.5.0: 135 | version "0.5.0" 136 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" 137 | 138 | http-errors@~1.6.1: 139 | version "1.6.1" 140 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" 141 | dependencies: 142 | depd "1.1.0" 143 | inherits "2.0.3" 144 | setprototypeof "1.0.3" 145 | statuses ">= 1.3.1 < 2" 146 | 147 | iconv-lite@0.4.15: 148 | version "0.4.15" 149 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" 150 | 151 | inherits@2.0.3: 152 | version "2.0.3" 153 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 154 | 155 | ipaddr.js@1.2.0: 156 | version "1.2.0" 157 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4" 158 | 159 | media-typer@0.3.0: 160 | version "0.3.0" 161 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 162 | 163 | merge-descriptors@1.0.1: 164 | version "1.0.1" 165 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 166 | 167 | methods@~1.1.2: 168 | version "1.1.2" 169 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 170 | 171 | mime-db@~1.26.0: 172 | version "1.26.0" 173 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" 174 | 175 | mime-types@~2.1.11, mime-types@~2.1.13: 176 | version "2.1.14" 177 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" 178 | dependencies: 179 | mime-db "~1.26.0" 180 | 181 | mime@1.3.4: 182 | version "1.3.4" 183 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 184 | 185 | ms@0.7.2: 186 | version "0.7.2" 187 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" 188 | 189 | negotiator@0.6.1: 190 | version "0.6.1" 191 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 192 | 193 | on-finished@~2.3.0: 194 | version "2.3.0" 195 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 196 | dependencies: 197 | ee-first "1.1.1" 198 | 199 | parseurl@~1.3.1: 200 | version "1.3.1" 201 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 202 | 203 | path-to-regexp@0.1.7: 204 | version "0.1.7" 205 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 206 | 207 | proxy-addr@~1.1.3: 208 | version "1.1.3" 209 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074" 210 | dependencies: 211 | forwarded "~0.1.0" 212 | ipaddr.js "1.2.0" 213 | 214 | qs@6.4.0: 215 | version "6.4.0" 216 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 217 | 218 | range-parser@~1.2.0: 219 | version "1.2.0" 220 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 221 | 222 | raw-body@~2.2.0: 223 | version "2.2.0" 224 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" 225 | dependencies: 226 | bytes "2.4.0" 227 | iconv-lite "0.4.15" 228 | unpipe "1.0.0" 229 | 230 | send@0.15.1: 231 | version "0.15.1" 232 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" 233 | dependencies: 234 | debug "2.6.1" 235 | depd "~1.1.0" 236 | destroy "~1.0.4" 237 | encodeurl "~1.0.1" 238 | escape-html "~1.0.3" 239 | etag "~1.8.0" 240 | fresh "0.5.0" 241 | http-errors "~1.6.1" 242 | mime "1.3.4" 243 | ms "0.7.2" 244 | on-finished "~2.3.0" 245 | range-parser "~1.2.0" 246 | statuses "~1.3.1" 247 | 248 | serve-static@1.12.1: 249 | version "1.12.1" 250 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.1.tgz#7443a965e3ced647aceb5639fa06bf4d1bbe0039" 251 | dependencies: 252 | encodeurl "~1.0.1" 253 | escape-html "~1.0.3" 254 | parseurl "~1.3.1" 255 | send "0.15.1" 256 | 257 | setprototypeof@1.0.3: 258 | version "1.0.3" 259 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 260 | 261 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 262 | version "1.3.1" 263 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 264 | 265 | type-is@~1.6.14: 266 | version "1.6.14" 267 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" 268 | dependencies: 269 | media-typer "0.3.0" 270 | mime-types "~2.1.13" 271 | 272 | unpipe@~1.0.0, unpipe@1.0.0: 273 | version "1.0.0" 274 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 275 | 276 | utils-merge@1.0.0: 277 | version "1.0.0" 278 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 279 | 280 | vary@~1.1.0: 281 | version "1.1.1" 282 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" 283 | 284 | -------------------------------------------------------------------------------- /chapter-6/push-notifications/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.3: 6 | version "1.3.3" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 8 | dependencies: 9 | mime-types "~2.1.11" 10 | negotiator "0.6.1" 11 | 12 | array-flatten@1.1.1: 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 15 | 16 | asn1.js@^4.8.1: 17 | version "4.9.1" 18 | resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" 19 | dependencies: 20 | bn.js "^4.0.0" 21 | inherits "^2.0.1" 22 | minimalistic-assert "^1.0.0" 23 | 24 | base64url@2.0.0, base64url@^2.0.0: 25 | version "2.0.0" 26 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 27 | 28 | bn.js@^4.0.0: 29 | version "4.11.6" 30 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" 31 | 32 | body-parser@^1.15.2: 33 | version "1.15.2" 34 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.2.tgz#d7578cf4f1d11d5f6ea804cef35dc7a7ff6dae67" 35 | dependencies: 36 | bytes "2.4.0" 37 | content-type "~1.0.2" 38 | debug "~2.2.0" 39 | depd "~1.1.0" 40 | http-errors "~1.5.0" 41 | iconv-lite "0.4.13" 42 | on-finished "~2.3.0" 43 | qs "6.2.0" 44 | raw-body "~2.1.7" 45 | type-is "~1.6.13" 46 | 47 | buffer-equal-constant-time@1.0.1: 48 | version "1.0.1" 49 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 50 | 51 | bytes@2.4.0: 52 | version "2.4.0" 53 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" 54 | 55 | content-disposition@0.5.1: 56 | version "0.5.1" 57 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" 58 | 59 | content-type@~1.0.2: 60 | version "1.0.2" 61 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 62 | 63 | cookie-signature@1.0.6: 64 | version "1.0.6" 65 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 66 | 67 | cookie@0.3.1: 68 | version "0.3.1" 69 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 70 | 71 | debug@~2.2.0: 72 | version "2.2.0" 73 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" 74 | dependencies: 75 | ms "0.7.1" 76 | 77 | depd@~1.1.0: 78 | version "1.1.0" 79 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 80 | 81 | destroy@~1.0.4: 82 | version "1.0.4" 83 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 84 | 85 | ecdsa-sig-formatter@1.0.9: 86 | version "1.0.9" 87 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 88 | dependencies: 89 | base64url "^2.0.0" 90 | safe-buffer "^5.0.1" 91 | 92 | ee-first@1.1.1: 93 | version "1.1.1" 94 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 95 | 96 | encodeurl@~1.0.1: 97 | version "1.0.1" 98 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 99 | 100 | escape-html@~1.0.3: 101 | version "1.0.3" 102 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 103 | 104 | etag@~1.7.0: 105 | version "1.7.0" 106 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" 107 | 108 | express: 109 | version "4.14.0" 110 | resolved "https://registry.yarnpkg.com/express/-/express-4.14.0.tgz#c1ee3f42cdc891fb3dc650a8922d51ec847d0d66" 111 | dependencies: 112 | accepts "~1.3.3" 113 | array-flatten "1.1.1" 114 | content-disposition "0.5.1" 115 | content-type "~1.0.2" 116 | cookie "0.3.1" 117 | cookie-signature "1.0.6" 118 | debug "~2.2.0" 119 | depd "~1.1.0" 120 | encodeurl "~1.0.1" 121 | escape-html "~1.0.3" 122 | etag "~1.7.0" 123 | finalhandler "0.5.0" 124 | fresh "0.3.0" 125 | merge-descriptors "1.0.1" 126 | methods "~1.1.2" 127 | on-finished "~2.3.0" 128 | parseurl "~1.3.1" 129 | path-to-regexp "0.1.7" 130 | proxy-addr "~1.1.2" 131 | qs "6.2.0" 132 | range-parser "~1.2.0" 133 | send "0.14.1" 134 | serve-static "~1.11.1" 135 | type-is "~1.6.13" 136 | utils-merge "1.0.0" 137 | vary "~1.1.0" 138 | 139 | finalhandler@0.5.0: 140 | version "0.5.0" 141 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" 142 | dependencies: 143 | debug "~2.2.0" 144 | escape-html "~1.0.3" 145 | on-finished "~2.3.0" 146 | statuses "~1.3.0" 147 | unpipe "~1.0.0" 148 | 149 | forwarded@~0.1.0: 150 | version "0.1.0" 151 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 152 | 153 | fresh@0.3.0: 154 | version "0.3.0" 155 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" 156 | 157 | http-errors@~1.5.0: 158 | version "1.5.1" 159 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" 160 | dependencies: 161 | inherits "2.0.3" 162 | setprototypeof "1.0.2" 163 | statuses ">= 1.3.1 < 2" 164 | 165 | http_ece@^0.5.2: 166 | version "0.5.2" 167 | resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-0.5.2.tgz#5654d7ec9d996b749ce00a276e18d54b6d8f905f" 168 | dependencies: 169 | urlsafe-base64 "~1.0.0" 170 | 171 | iconv-lite@0.4.13: 172 | version "0.4.13" 173 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" 174 | 175 | inherits@2.0.3, inherits@^2.0.1: 176 | version "2.0.3" 177 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 178 | 179 | ipaddr.js@1.1.1: 180 | version "1.1.1" 181 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.1.1.tgz#c791d95f52b29c1247d5df80ada39b8a73647230" 182 | 183 | jwa@^1.1.4: 184 | version "1.1.5" 185 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 186 | dependencies: 187 | base64url "2.0.0" 188 | buffer-equal-constant-time "1.0.1" 189 | ecdsa-sig-formatter "1.0.9" 190 | safe-buffer "^5.0.1" 191 | 192 | jws@^3.1.3: 193 | version "3.1.4" 194 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 195 | dependencies: 196 | base64url "^2.0.0" 197 | jwa "^1.1.4" 198 | safe-buffer "^5.0.1" 199 | 200 | media-typer@0.3.0: 201 | version "0.3.0" 202 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 203 | 204 | merge-descriptors@1.0.1: 205 | version "1.0.1" 206 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 207 | 208 | methods@~1.1.2: 209 | version "1.1.2" 210 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 211 | 212 | mime-db@~1.25.0: 213 | version "1.25.0" 214 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" 215 | 216 | mime-types@~2.1.11, mime-types@~2.1.13: 217 | version "2.1.13" 218 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" 219 | dependencies: 220 | mime-db "~1.25.0" 221 | 222 | mime@1.3.4: 223 | version "1.3.4" 224 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 225 | 226 | minimalistic-assert@^1.0.0: 227 | version "1.0.0" 228 | resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" 229 | 230 | minimist@^1.2.0: 231 | version "1.2.0" 232 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 233 | 234 | ms@0.7.1: 235 | version "0.7.1" 236 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" 237 | 238 | negotiator@0.6.1: 239 | version "0.6.1" 240 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 241 | 242 | on-finished@~2.3.0: 243 | version "2.3.0" 244 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 245 | dependencies: 246 | ee-first "1.1.1" 247 | 248 | parseurl@~1.3.1: 249 | version "1.3.1" 250 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 251 | 252 | path-to-regexp@0.1.7: 253 | version "0.1.7" 254 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 255 | 256 | proxy-addr@~1.1.2: 257 | version "1.1.2" 258 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37" 259 | dependencies: 260 | forwarded "~0.1.0" 261 | ipaddr.js "1.1.1" 262 | 263 | qs@6.2.0: 264 | version "6.2.0" 265 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" 266 | 267 | range-parser@~1.2.0: 268 | version "1.2.0" 269 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 270 | 271 | raw-body@~2.1.7: 272 | version "2.1.7" 273 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" 274 | dependencies: 275 | bytes "2.4.0" 276 | iconv-lite "0.4.13" 277 | unpipe "1.0.0" 278 | 279 | safe-buffer@^5.0.1: 280 | version "5.0.1" 281 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" 282 | 283 | send@0.14.1: 284 | version "0.14.1" 285 | resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a" 286 | dependencies: 287 | debug "~2.2.0" 288 | depd "~1.1.0" 289 | destroy "~1.0.4" 290 | encodeurl "~1.0.1" 291 | escape-html "~1.0.3" 292 | etag "~1.7.0" 293 | fresh "0.3.0" 294 | http-errors "~1.5.0" 295 | mime "1.3.4" 296 | ms "0.7.1" 297 | on-finished "~2.3.0" 298 | range-parser "~1.2.0" 299 | statuses "~1.3.0" 300 | 301 | serve-static@~1.11.1: 302 | version "1.11.1" 303 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.1.tgz#d6cce7693505f733c759de57befc1af76c0f0805" 304 | dependencies: 305 | encodeurl "~1.0.1" 306 | escape-html "~1.0.3" 307 | parseurl "~1.3.1" 308 | send "0.14.1" 309 | 310 | setprototypeof@1.0.2: 311 | version "1.0.2" 312 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" 313 | 314 | "statuses@>= 1.3.1 < 2", statuses@~1.3.0: 315 | version "1.3.1" 316 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 317 | 318 | type-is@~1.6.13: 319 | version "1.6.14" 320 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" 321 | dependencies: 322 | media-typer "0.3.0" 323 | mime-types "~2.1.13" 324 | 325 | unpipe@1.0.0, unpipe@~1.0.0: 326 | version "1.0.0" 327 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 328 | 329 | urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0: 330 | version "1.0.0" 331 | resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" 332 | 333 | utils-merge@1.0.0: 334 | version "1.0.0" 335 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 336 | 337 | vary@~1.1.0: 338 | version "1.1.0" 339 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" 340 | 341 | web-push: 342 | version "3.2.1" 343 | resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.2.1.tgz#ab35e41dd055c5f201e45a428612b1d7f1d706f3" 344 | dependencies: 345 | asn1.js "^4.8.1" 346 | http_ece "^0.5.2" 347 | jws "^3.1.3" 348 | minimist "^1.2.0" 349 | urlsafe-base64 "^1.0.0" 350 | --------------------------------------------------------------------------------