diff --git a/appicons/apple-touch-icon-114x114.png b/appicons/apple-touch-icon-114x114.png new file mode 100644 index 0000000..315647c Binary files /dev/null and b/appicons/apple-touch-icon-114x114.png differ diff --git a/appicons/apple-touch-icon-120x120.png b/appicons/apple-touch-icon-120x120.png new file mode 100644 index 0000000..7170d4b Binary files /dev/null and b/appicons/apple-touch-icon-120x120.png differ diff --git a/appicons/apple-touch-icon-144x144.png b/appicons/apple-touch-icon-144x144.png new file mode 100644 index 0000000..2d5a5d3 Binary files /dev/null and b/appicons/apple-touch-icon-144x144.png differ diff --git a/appicons/apple-touch-icon-152x152.png b/appicons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..0ab2b90 Binary files /dev/null and b/appicons/apple-touch-icon-152x152.png differ diff --git a/appicons/apple-touch-icon-57x57.png b/appicons/apple-touch-icon-57x57.png new file mode 100644 index 0000000..b5837dd Binary files /dev/null and b/appicons/apple-touch-icon-57x57.png differ diff --git a/appicons/apple-touch-icon-60x60.png b/appicons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..8367d31 Binary files /dev/null and b/appicons/apple-touch-icon-60x60.png differ diff --git a/appicons/apple-touch-icon-72x72.png b/appicons/apple-touch-icon-72x72.png new file mode 100644 index 0000000..1e92a0e Binary files /dev/null and b/appicons/apple-touch-icon-72x72.png differ diff --git a/appicons/apple-touch-icon-76x76.png b/appicons/apple-touch-icon-76x76.png new file mode 100644 index 0000000..6544fa5 Binary files /dev/null and b/appicons/apple-touch-icon-76x76.png differ diff --git a/appicons/favicon-128.png b/appicons/favicon-128.png new file mode 100644 index 0000000..0b27bea Binary files /dev/null and b/appicons/favicon-128.png differ diff --git a/appicons/favicon-16x16.png b/appicons/favicon-16x16.png new file mode 100644 index 0000000..6fbdbf9 Binary files /dev/null and b/appicons/favicon-16x16.png differ diff --git a/appicons/favicon-196x196.png b/appicons/favicon-196x196.png new file mode 100644 index 0000000..75b7ed6 Binary files /dev/null and b/appicons/favicon-196x196.png differ diff --git a/appicons/favicon-32x32.png b/appicons/favicon-32x32.png new file mode 100644 index 0000000..b7e0f57 Binary files /dev/null and b/appicons/favicon-32x32.png differ diff --git a/appicons/favicon-96x96.png b/appicons/favicon-96x96.png new file mode 100644 index 0000000..528aac1 Binary files /dev/null and b/appicons/favicon-96x96.png differ diff --git a/appicons/mstile-144x144.png b/appicons/mstile-144x144.png new file mode 100644 index 0000000..2d5a5d3 Binary files /dev/null and b/appicons/mstile-144x144.png differ diff --git a/appicons/mstile-150x150.png b/appicons/mstile-150x150.png new file mode 100644 index 0000000..0948567 Binary files /dev/null and b/appicons/mstile-150x150.png differ diff --git a/appicons/mstile-310x150.png b/appicons/mstile-310x150.png new file mode 100644 index 0000000..6883b02 Binary files /dev/null and b/appicons/mstile-310x150.png differ diff --git a/appicons/mstile-310x310.png b/appicons/mstile-310x310.png new file mode 100644 index 0000000..cffdc56 Binary files /dev/null and b/appicons/mstile-310x310.png differ diff --git a/appicons/mstile-70x70.png b/appicons/mstile-70x70.png new file mode 100644 index 0000000..0b27bea Binary files /dev/null and b/appicons/mstile-70x70.png differ diff --git a/appicons/splash.png b/appicons/splash.png new file mode 100644 index 0000000..ba293ae Binary files /dev/null and b/appicons/splash.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..2dbfbd5 Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html index a60cbb9..b77e885 100644 --- a/index.html +++ b/index.html @@ -7,9 +7,31 @@ + - + content="default-src 'self'; script-src 'self' 'unsafe-eval' 'sha256-TJ4T4ywZLNJftBZ/C40uficokKH5JwSWtCX5PNccNbI='; child-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; style-src 'self';"> + + + + + + + + + + + + + + + + + + + + + + @@ -28,6 +50,21 @@ + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..134759d --- /dev/null +++ b/manifest.json @@ -0,0 +1,31 @@ +{ + "name": "Vanilla.js: Modern 1st Party JavaScript", + "short_name": "vanilla-js", + "description": "A Single Page Application built in 100% pure modern JavaScript with no frameworks.", + "icons": [{ + "src": "/appicons/favicon-128.png", + "sizes": "128x128", + "type": "image/png" + }, { + "src": "/appicons/apple-touch-icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, { + "src": "/appicons/apple-touch-icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, { + "src": "/appicons/favicon-196x196.png", + "sizes": "196x196", + "type": "image/png" + }, { + "src": "/appicons/splash.png", + "sizes": "512x512", + "type": "image/png" + }], + "start_url": "/index.html", + "display": "standalone", + "orientation" : "any", + "background_color": "#FFFFFF", + "theme_color": "#FFFFFF" + } \ No newline at end of file diff --git a/pwa.js b/pwa.js new file mode 100644 index 0000000..7571bbc --- /dev/null +++ b/pwa.js @@ -0,0 +1,265 @@ +const CACHE_VERSION = 1.0; + +const BASE_CACHE_FILES = [ + '/index.html', + '/pwa/404.html', + '/pwa/offline.html', + '/css/style.css', + '/css/animations.css', + '/manifest.json', + '/images/logo.png', + '/js/app.js', +]; + +const OFFLINE_CACHE_FILES = [ + '/pwa/offline.html' +]; + +const NOT_FOUND_CACHE_FILES = [ + '/pwa/404.html' +]; + +const OFFLINE_PAGE = '/pwa/offline.html'; +const NOT_FOUND_PAGE = '/pwa/404.html'; + +const CACHE_VERSIONS = { + assets: 'assets-v' + CACHE_VERSION, + content: 'content-v' + CACHE_VERSION, + offline: 'offline-v' + CACHE_VERSION, + notFound: '404-v' + CACHE_VERSION, +}; + +// Define MAX_TTL's in SECONDS for specific file extensions +const MAX_TTL = { + '/': 3600, + html: 43200, + json: 43200, + js: 86400, + css: 86400, +}; + +const SUPPORTED_METHODS = [ + 'GET', +]; + +/** + * getFileExtension + * @param {string} url + * @returns {string} + */ +function getFileExtension(url) { + let extension = url.split('.').reverse()[0].split('?')[0]; + return (extension.endsWith('/')) ? '/' : extension; +} + +/** + * getTTL + * @param {string} url + */ +function getTTL(url) { + if (typeof url === 'string') { + let extension = getFileExtension(url); + if (typeof MAX_TTL[extension] === 'number') { + return MAX_TTL[extension]; + } else { + return null; + } + } else { + return null; + } +} + +/** + * installServiceWorker + * @returns {Promise} + */ +async function installServiceWorker() { + try { + await Promise.all([ + caches.open(CACHE_VERSIONS.assets).then((cache) => { + return cache.addAll(BASE_CACHE_FILES); + }, err => console.error(`Error with ${CACHE_VERSIONS.assets}`, err)), + caches.open(CACHE_VERSIONS.offline).then((cache_1) => { + return cache_1.addAll(OFFLINE_CACHE_FILES); + }, err_1 => console.error(`Error with ${CACHE_VERSIONS.offline}`, err_1)), + caches.open(CACHE_VERSIONS.notFound).then((cache_2) => { + return cache_2.addAll(NOT_FOUND_CACHE_FILES); + }, err_2 => console.error(`Error with ${CACHE_VERSIONS.notFound}`, err_2))]); + return self.skipWaiting(); + } + catch (err_3) { + return console.error("Error with installation: ", err_3); + } +} + +/** + * cleanupLegacyCache + * @returns {Promise} + */ +function cleanupLegacyCache() { + + const currentCaches = Object.keys(CACHE_VERSIONS).map((key) => { + return CACHE_VERSIONS[key]; + }); + + return new Promise( + (resolve, reject) => { + + caches.keys().then((keys) => { + return legacyKeys = keys.filter((key) => { + return !~currentCaches.indexOf(key); + }); + }).then((legacy) => { + if (legacy.length) { + Promise.all(legacy.map((legacyKey) => { + return caches.delete(legacyKey) + }) + ).then(() => { + resolve() + }).catch((err) => { + console.error("Error in legacy cleanup: ", err); + reject(err); + }); + } else { + resolve(); + } + }).catch((err) => { + console.error("Error in legacy cleanup: ", err); + reject(err); + }); + + }); +} + +function preCacheUrl(url) { + caches.open(CACHE_VERSIONS.content).then((cache) => { + cache.match(url).then((response) => { + if (!response) { + return fetch(url); + } else { + // already in cache, nothing to do. + return null; + } + }).then((response) => { + if (response) { + return cache.put(url, response.clone()); + } else { + return null; + } + }); + }); + +} + +self.addEventListener('install', event => { + event.waitUntil( + Promise.all([ + installServiceWorker(), + self.skipWaiting(), + ])); +}); + +// The activate handler takes care of cleaning up old caches. +self.addEventListener('activate', event => { + event.waitUntil(Promise.all( + [cleanupLegacyCache(), + self.clients.claim(), + self.skipWaiting()]).catch((err) => { + console.error("Activation error: ", err); + event.skipWaiting(); + })); +}); + +self.addEventListener('fetch', event => { + event.respondWith( + caches.open(CACHE_VERSIONS.content).then((cache) => { + return cache.match(event.request).then((response) => { + if (response) { + let headers = response.headers.entries(); + let date = null; + + for (let pair of headers) { + if (pair[0] === 'date') { + date = new Date(pair[1]); + } + } + + if (date) { + let age = parseInt((new Date().getTime() - date.getTime()) / 1000); + let ttl = getTTL(event.request.url); + + if (ttl && age > ttl) { + return new Promise(async (resolve) => { + try { + const updatedResponse = await fetch(event.request.clone()); + if (updatedResponse) { + cache.put(event.request, updatedResponse.clone()); + resolve(updatedResponse); + } + else { + resolve(response); + } + } + catch (e) { + resolve(response); + } + }).catch(() => { + return response; + }); + } else { + return response; + } + } else { + return response; + } + } else { + return null; + } + }).then((response) => { + if (response) { + return response; + } else { + return fetch(event.request.clone()).then( + (response) => { + if (response.status < 400) { + if (~SUPPORTED_METHODS.indexOf(event.request.method)) { + cache.put(event.request, response.clone()); + } + return response; + } else { + return caches.open(CACHE_VERSIONS.notFound).then((cache) => { + return cache.match(NOT_FOUND_PAGE); + }); + } + }).then((response) => { + if (response) { + return response; + } + }).catch(() => { + return caches.open(CACHE_VERSIONS.offline).then( + (offlineCache) => { + return offlineCache.match(OFFLINE_PAGE) + }); + }); + } + }).catch((error) => { + console.error('Error in fetch handler:', error); + throw error; + }); + }) + ); +}); + +self.addEventListener('message', (event) => { + if (typeof event.data === 'object' && + typeof event.data.action === 'string') { + switch (event.data.action) { + case 'cache': + preCacheUrl(event.data.url); + break; + default: + console.log('Unknown action: ' + event.data.action); + break; + } + } +}); \ No newline at end of file diff --git a/pwa/404.html b/pwa/404.html new file mode 100644 index 0000000..d56d1d6 --- /dev/null +++ b/pwa/404.html @@ -0,0 +1,56 @@ + + + + + Vanilla.js - Oops! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Oops!

+

I'm afraid this isn't the HTML page you're looking for.

+

Perhaps try this (first slide) instead?

+
+
+ + --- + + + + + \ No newline at end of file diff --git a/pwa/offline.html b/pwa/offline.html new file mode 100644 index 0000000..958d617 --- /dev/null +++ b/pwa/offline.html @@ -0,0 +1,54 @@ + + + + + Vanilla.js - No Network! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

I can't find the network!

+

If you revisit this page online, I'll be sure to cache it for offline access.

+
+
+ + --- + + + + \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..14267e9 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / \ No newline at end of file