diff --git a/controllers/about.js b/controllers/about.js index e7b1955..a7235af 100644 --- a/controllers/about.js +++ b/controllers/about.js @@ -1,4 +1,4 @@ module.exports = (req, res) => res.render('about.html', { - bodyClass: 'about', - pageTitle: 'About' + bodyClass: 'about', + pageTitle: 'About' }); diff --git a/controllers/error.js b/controllers/error.js index 4711d5f..b34f0c2 100644 --- a/controllers/error.js +++ b/controllers/error.js @@ -1,6 +1,7 @@ +/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "next" }] */ module.exports = (err, req, res, next) => { - const statusCode = err.statusCode || 500; - const error = err.statusMessage || 'Something went wrong'; - console.error(err); - res.status(statusCode).render('error.html', { error }); + const statusCode = err.statusCode || 500; + const error = err.statusMessage || 'Something went wrong'; + console.error(err); + res.status(statusCode).render('error.html', {error}); }; diff --git a/controllers/error404.js b/controllers/error404.js index 8835b36..7ae7601 100644 --- a/controllers/error404.js +++ b/controllers/error404.js @@ -1,5 +1,5 @@ module.exports = (req, res) => { - const statusCode = 404; - const error = '404 Not Found'; - res.status(statusCode).render('error.html', { error }); + const statusCode = 404; + const error = '404 Not Found'; + res.status(statusCode).render('error.html', {error}); }; diff --git a/controllers/index.js b/controllers/index.js index a1ebb81..ba7b15e 100644 --- a/controllers/index.js +++ b/controllers/index.js @@ -1,8 +1,8 @@ module.exports = { - listing: require('./listing'), - node: require('./node'), - about: require('./about'), - noConnection: require('./no-connection'), - error: require('./error'), - error404: require('./error404') + listing: require('./listing'), + node: require('./node'), + about: require('./about'), + noConnection: require('./no-connection'), + error: require('./error'), + error404: require('./error404') }; diff --git a/controllers/listing.js b/controllers/listing.js index 8f7e5b1..fb924dc 100644 --- a/controllers/listing.js +++ b/controllers/listing.js @@ -1,33 +1,32 @@ const tor = require('../lib/tor'); module.exports = (req, res, next) => { + let title = 'Top nodes by consensus weight'; + const query = { + limit: 10 + }; + if (req.query.s) { + title = `Search results for "${req.query.s}":`; + query.search = req.query.s; + } else { + query.order = '-consensus_weight'; + query.running = true; + } + if (req.query.p) { + query.offset = (query.limit * req.query.p) - query.limit; + } - let title = 'Top nodes by consensus weight'; - const query = { - limit: 10 - }; - if(req.query.s) { - title = `Search results for "${req.query.s}":`; - query.search = req.query.s; - } else { - query.order = '-consensus_weight'; - query.running = true; - } - if(req.query.p) { - query.offset = (query.limit * req.query.p) - query.limit; - } - - tor.listNodes(query) - .then(nodes => res.render('listing.html', { - pageTitle: req.query.s ? `Search: ${req.query.s}` : false, - title: title, - nodes: nodes, - numOfNodes: query.limit - })) - .catch(err => { - if(err.statusCode == 400 && req.query.s) { - err.statusMessage = 'Bad Search Query'; - } - next(err); - }); -} + tor.listNodes(query) + .then(nodes => res.render('listing.html', { + pageTitle: req.query.s ? `Search: ${req.query.s}` : false, + title: title, + nodes: nodes, + numOfNodes: query.limit + })) + .catch(err => { + if (err.statusCode === 400 && req.query.s) { + err.statusMessage = 'Bad Search Query'; + } + next(err); + }); +}; diff --git a/controllers/no-connection.js b/controllers/no-connection.js index ebee8ac..948191d 100644 --- a/controllers/no-connection.js +++ b/controllers/no-connection.js @@ -1,4 +1,4 @@ module.exports = (req, res) => res.render('no-connection.html', { - bodyClass: 'no-connection', - pageTitle: 'No Connection' + bodyClass: 'no-connection', + pageTitle: 'No Connection' }); diff --git a/controllers/node.js b/controllers/node.js index 0b79ced..de24e4b 100644 --- a/controllers/node.js +++ b/controllers/node.js @@ -1,30 +1,30 @@ -const tor = require('../lib/tor'); -const bandwidthChart = require('../lib/bandwidth-chart'); +const tor = require('../lib/tor'); +const bandwidthChart = require('../lib/bandwidth-chart'); module.exports = (req, res, next) => { - Promise.all([ - tor.node(req.params.id), - tor.bandwidth(req.params.id) - ]) - .then(data => { - // Throw 404 if node doesn't exist - if(!data[0]) { - const err = new Error('Node doesn\'t exist'); - err.statusMessage = err.message; - err.statusCode = 404; - throw err; - } + Promise.all([ + tor.node(req.params.id), + tor.bandwidth(req.params.id) + ]) + .then(data => { + // Throw 404 if node doesn't exist + if (!data[0]) { + const err = new Error('Node doesn\'t exist'); + err.statusMessage = err.message; + err.statusCode = 404; + throw err; + } - res.render('node.html', { - pageTitle: `${data[0].type}: ${data[0].nickname}`, - node: data[0], - bandwidth: bandwidthChart(data[1]) - }) - }) - .catch(err => { - if(err.statusCode == 400) { - err.statusMessage = 'Invalid node'; - } - next(err); - }); -} + res.render('node.html', { + pageTitle: `${data[0].type}: ${data[0].nickname}`, + node: data[0], + bandwidth: bandwidthChart(data[1]) + }); + }) + .catch(err => { + if (err.statusCode === 400) { + err.statusMessage = 'Invalid node'; + } + next(err); + }); +}; diff --git a/index.js b/index.js index 90dfff5..7a07b6e 100644 --- a/index.js +++ b/index.js @@ -1,24 +1,25 @@ -const express = require('express'); -const anonlytics = require('anonlytics-express'); -const nunjucks = require('nunjucks'); -const nunjucksFilters = require('./lib/nunjucks-filters'); -const nunjucksMiddleware = require('./lib/nunjucks-middleware'); -const compression = require('compression'); -const minify = require('./lib/minify'); -const controllers = require('./controllers'); -const app = express(); -const port = process.env.port || 3000; +const express = require('express'); +const anonlytics = require('anonlytics-express'); +const nunjucks = require('nunjucks'); +const compression = require('compression'); +const nunjucksFilters = require('./lib/nunjucks-filters'); +const nunjucksMiddleware = require('./lib/nunjucks-middleware'); +const minify = require('./lib/minify'); +const controllers = require('./controllers'); + +const app = express(); +const port = process.env.port || 3000; // Trust proxy headers if we're deployed on now -if(process.env.NOW) { - app.enable('trust proxy'); +if (process.env.NOW) { + app.enable('trust proxy'); } // Analytics app.use(anonlytics()); // Setup nunjucks -nunjucks.configure('views', { express: app }); +nunjucks.configure('views', {express: app}); nunjucksFilters(app); app.use(nunjucksMiddleware); @@ -35,8 +36,8 @@ app.get('/about', controllers.about); app.get('/no-connection', controllers.noConnection); // Serve assets with cache headers -app.use('/sw.js', express.static(`${__dirname}/public/sw.js`, { maxAge: '1 hour' })); -app.use(express.static(`${__dirname}/public`, { maxAge: '1 year' })); +app.use('/sw.js', express.static(`${__dirname}/public/sw.js`, {maxAge: '1 hour'})); +app.use(express.static(`${__dirname}/public`, {maxAge: '1 year'})); // Errors app.use(controllers.error404); diff --git a/lib/bandwidth-chart.js b/lib/bandwidth-chart.js index 7507496..e13f85f 100644 --- a/lib/bandwidth-chart.js +++ b/lib/bandwidth-chart.js @@ -1,51 +1,49 @@ const chart = require('ascii-chart'); function pointsFromBandwidthData(values, numPoints) { + // Define vars + const len = values.length; + const points = []; + let i = 0; + let size; - // Define vars - const len = values.length; - const points = []; - let i = 0; - let size; + // Split values into n points + if (numPoints < 2) { + points.push(values); + } else { + if (len % numPoints === 0) { + size = Math.floor(len / numPoints); + while (i < len) { + points.push(values.slice(i, i += size)); + } + } + while (i < len) { + size = Math.ceil((len - i) / numPoints--); + points.push(values.slice(i, i += size)); + } + } - // Split values into n points - if(numPoints < 2) { - points.push(values); - } else { - if(len % numPoints === 0) { - size = Math.floor(len / numPoints); - while (i < len) { - points.push(values.slice(i, i += size)); - } - } - while (i < len) { - size = Math.ceil((len - i) / numPoints--); - points.push(values.slice(i, i += size)); - } - } + // Return points + return points - // Return points - return points + // Calculate average value of each point + .map(point => Math.round(point.reduce((a, b) => a + b) / point.length)) - // Calculate average value of each point - .map(point => Math.round(point.reduce((a,b) => a + b) / point.length)) - - // Convert bytes to megabytes - .map(bytes => Number((bytes / 1000000).toPrecision(3))); + // Convert bytes to megabytes + .map(bytes => Number((bytes / 1000000).toPrecision(3))); } module.exports = values => { - if(values && values.length) { - const points = pointsFromBandwidthData(values, 57); - return chart(points, { - width: 120, - height: 20, - padding: 0, - pointChar: '*', - negativePointChar: '.', - axisChar: '.' - }); - } else { - return ''; - } -} + if (!values || values.length < 1) { + return ''; + } + const points = pointsFromBandwidthData(values, 57); + return chart(points, { + width: 120, + height: 20, + padding: 0, + pointChar: '*', + negativePointChar: '.', + axisChar: '.' + }); +}; diff --git a/lib/minify.js b/lib/minify.js index 25eaf9b..14a7c21 100644 --- a/lib/minify.js +++ b/lib/minify.js @@ -1,20 +1,21 @@ -const minify = require('express-minify'); -const minifyHTML = require('express-minify-html'); -const CleanCSS = require('clean-css'); -const cleanCSS = new CleanCSS(); +const minify = require('express-minify'); +const minifyHTML = require('express-minify-html'); +const CleanCSS = require('clean-css'); + +const cleanCSS = new CleanCSS(); module.exports = [ - minify({ - cssmin: source => cleanCSS.minify(source).styles - }), - minifyHTML({ - htmlMinifier: { - removeComments: true, - collapseWhitespace: true, - collapseBooleanAttributes: true, - removeAttributeQuotes: true, - removeEmptyAttributes: true, - removeOptionalTags: true - } - }) + minify({ + cssmin: source => cleanCSS.minify(source).styles + }), + minifyHTML({ + htmlMinifier: { + removeComments: true, + collapseWhitespace: true, + collapseBooleanAttributes: true, + removeAttributeQuotes: true, + removeEmptyAttributes: true, + removeOptionalTags: true + } + }) ]; diff --git a/lib/nunjucks-filters.js b/lib/nunjucks-filters.js index cf4aade..a5fe402 100644 --- a/lib/nunjucks-filters.js +++ b/lib/nunjucks-filters.js @@ -1,70 +1,78 @@ -const prettyBytes = require('pretty-bytes'); -const moment = require('moment'); const querystring = require('querystring'); +const prettyBytes = require('pretty-bytes'); +const moment = require('moment'); function humanTimeAgo(utcDate) { - const diff = moment.utc().diff(moment.utc(utcDate)); - const uptime = {}; + const diff = moment.utc().diff(moment.utc(utcDate)); + const uptime = {}; - uptime.s = Math.round(diff / 1000); - uptime.m = Math.floor(uptime.s / 60); - uptime.h = Math.floor(uptime.m / 60); - uptime.d = Math.floor(uptime.h / 24); + uptime.s = Math.round(diff / 1000); + uptime.m = Math.floor(uptime.s / 60); + uptime.h = Math.floor(uptime.m / 60); + uptime.d = Math.floor(uptime.h / 24); - uptime.s %= 60; - uptime.m %= 60; - uptime.h %= 24; + uptime.s %= 60; + uptime.m %= 60; + uptime.h %= 24; - let readableUptime = ''; - readableUptime += uptime.d ? ` ${uptime.d}d` : ''; - readableUptime += uptime.h ? ` ${uptime.h}h` : ''; - readableUptime += !uptime.d || !uptime.h && uptime.m ? ` ${uptime.m}m` : ''; + let readableUptime = ''; + readableUptime += uptime.d ? ` ${uptime.d}d` : ''; + readableUptime += uptime.h ? ` ${uptime.h}h` : ''; + if ((!uptime.d || !uptime.h) && uptime.m) { + readableUptime += ` ${uptime.m}m`; + } - return readableUptime.trim(); + return readableUptime.trim(); } const filters = { - bandwidth: node => `${prettyBytes(node.advertised_bandwidth)}/s`, - uptime: node => { - + bandwidth: node => `${prettyBytes(node.advertised_bandwidth)}/s`, + uptime: node => { // Check node is up - if(!node.running) { - return 'Down'; - } + if (!node.running) { + return 'Down'; + } // Check uptime - return humanTimeAgo(node.last_restarted); - }, - pagination: (req, direction) => { - + return humanTimeAgo(node.last_restarted); + }, + pagination: (req, direction) => { // Clone query string - const query = Object.assign({}, req.query); + const query = Object.assign({}, req.query); // Set page as 1 by default - query.p = query.p ? query.p : 1; + query.p = query.p ? query.p : 1; // Update page - if(direction == 'next') { - query.p++; - } else if(direction == 'prev') { - query.p--; - } + if (direction === 'next') { + query.p++; + } else if (direction === 'prev') { + query.p--; + } // Don't add p var if it's page 1 - query.p == 1 && delete query.p + if (query.p === 1) { + delete query.p; + } // Encode query string - const queryString = querystring.encode(query); - return queryString ? `/?${queryString}` : '/'; - }, - name: node => node.nickname - || node.fingerprint && node.fingerprint.slice(0, 8) - || node.hashed_fingerprint && node.hashed_fingerprint.slice(0, 8), - status: node => node.running ? + const queryString = querystring.encode(query); + return queryString ? `/?${queryString}` : '/'; + }, + name: node => { + let name = ''; + if (node.nickname) { + name = node.nickname; + } else if (node.fingerprint || node.hashed_fingerprint) { + name = (node.fingerprint || node.hashed_fingerprint).slice(0, 8); + } + return name; + }, + status: node => node.running ? `Up for ${humanTimeAgo(node.last_restarted)}` : `Down for ${humanTimeAgo(node.last_seen)}` }; module.exports = app => Object.keys(filters).forEach(filter => { - app.settings.nunjucksEnv.addFilter(filter, filters[filter]) + app.settings.nunjucksEnv.addFilter(filter, filters[filter]); }); diff --git a/lib/nunjucks-middleware.js b/lib/nunjucks-middleware.js index 8ca062d..8564aee 100644 --- a/lib/nunjucks-middleware.js +++ b/lib/nunjucks-middleware.js @@ -1,5 +1,5 @@ module.exports = (req, res, next) => { - res.locals.req = req; - res.locals.res = res; - next(); -} + res.locals.req = req; + res.locals.res = res; + next(); +}; diff --git a/lib/tor.js b/lib/tor.js index e0dbacb..4276249 100644 --- a/lib/tor.js +++ b/lib/tor.js @@ -1,43 +1,48 @@ const Onionoo = require('onionoo'); + const onionoo = new Onionoo(); +const setNodeType = type => node => { + node.type = type; + return node; +}; + module.exports = { - listNodes: query => { - return onionoo - .details(query) - .then(response => { - const details = response.body; - details.relays.forEach(node => node.type = 'relay'); - details.bridges.forEach(node => node.type = 'bridge'); - return details.relays.concat(details.bridges); - }); - }, - node: id => { - return onionoo - .details({ lookup: id }) - .then(response => { - const details = response.body; - if(details.relays[0]) { - details.relays[0].type = 'relay'; - return details.relays[0]; - } else if(details.bridges[0]) { - details.bridges[0].type = 'bridge'; - return details.bridges[0]; - } - }); - }, - bandwidth: id => { - return onionoo - .bandwidth({ lookup: id }) - .then(response => { - const bandwidth = response.body; - try { - const lastMonth = bandwidth.relays[0].write_history['1_month']; - return lastMonth.values.map(value => value * lastMonth.factor) - } - catch(e) { - return []; - } - }); - } + listNodes: query => { + return onionoo + .details(query) + .then(response => { + const details = response.body; + details.relays.map(setNodeType('relay')); + details.bridges.map(setNodeType('bridge')); + return details.relays.concat(details.bridges); + }); + }, + node: id => { + return onionoo + .details({lookup: id}) + .then(response => { + const details = response.body; + if (details.relays[0]) { + details.relays[0].type = 'relay'; + return details.relays[0]; + } else if (details.bridges[0]) { + details.bridges[0].type = 'bridge'; + return details.bridges[0]; + } + }); + }, + bandwidth: id => { + return onionoo + .bandwidth({lookup: id}) + .then(response => { + const bandwidth = response.body; + try { + const lastMonth = bandwidth.relays[0].write_history['1_month']; + return lastMonth.values.map(value => value * lastMonth.factor); + } catch (err) { + return []; + } + }); + } }; diff --git a/public/assets/enhancements.js b/public/assets/enhancements.js index 39477c4..6742a79 100644 --- a/public/assets/enhancements.js +++ b/public/assets/enhancements.js @@ -1,261 +1,254 @@ -(function() { - - // Space optimisations - var doc = document; - var find = doc.querySelector.bind(doc); - var create = doc.createElement.bind(doc); - - // Run callback when DOM is ready - function DOMReady(cb) { - - // Run now if DOM has already loaded as we're loading async - if(['interactive', 'complete'].indexOf(doc.readyState) >= 0) { - cb(); - - // Otherwise wait for DOM - } else { - doc.addEventListener('DOMContentLoaded', cb); - } - } - - // Feature detection - var supports = { - test: function(features) { - var self = this; - if(!features || !features.length) { - return false; - } - return features.every(function(feature) { - return self.tests[feature]; - }); - }, - tests: { - localStorage: (function() { - try { - localStorage.setItem('test', 'test'); - localStorage.removeItem('test'); - return true; - } catch (e) { - return false; - } - })(), - inlineSVG: (function() { - var div = create('div'); - div.innerHTML = ''; - return ( - typeof SVGRect != 'undefined' - && div.firstChild - && div.firstChild.namespaceURI - ) == 'http://www.w3.org/2000/svg'; - })(), - querySelector: typeof doc.querySelector === 'function', - classList: (function() { - var div = create('div'); - div.innerHTML = ''; - return 'classList' in div.firstChild; - })(), - serviceWorker: 'serviceWorker' in navigator - } - }; - - // Favourite nodes - var favouriteNodes = { - - // Key used in localStorage - storageKey: 'heartedNodes', - - // Url to heart SVG - heartPath: '/assets/heart.svg', - - // Class added to heart SVG element when active - activeClass: 'hearted', - - // Gets current node hash - getCurrentNode: function() { - var node = /^\/node\/([a-zA-Z0-9]+)/.exec(window.location.pathname); - return node ? node[1] : node; - }, - - // Gets current node title - getCurrentNodeTitle: function() { - return find('h2.node-title .name').innerText; - }, - - // Gets hearted nodes - getHeartedNodes: function() { - return JSON.parse(localStorage.getItem(favouriteNodes.storageKey)) || {}; - }, - - // Saves hearted nodes - saveHeartedNodes: function(heartedNodes) { - return localStorage.setItem(favouriteNodes.storageKey, JSON.stringify(heartedNodes)); - }, - - // Checks if node is hearted - isHearted: function(node) { - return typeof favouriteNodes.getHeartedNodes()[node] !== 'undefined'; - }, - - // Heart node - heart: function(node) { - var heartedNodes = favouriteNodes.getHeartedNodes(); - heartedNodes[node] = favouriteNodes.getCurrentNodeTitle(); - favouriteNodes.saveHeartedNodes(heartedNodes); - favouriteNodes.updateHeartedNodesList(); - return heartedNodes; - }, - - // Unheart node - unHeart: function(node) { - var heartedNodes = favouriteNodes.getHeartedNodes(); - delete heartedNodes[node]; - favouriteNodes.saveHeartedNodes(heartedNodes); - favouriteNodes.updateHeartedNodesList(); - return heartedNodes; - }, - - // Get list of hearted nodes - updateHeartedNodesList: function() { - var menu = find('.menu'); - if(!menu) { - return false; - } - var menuHTML = ''; - var heartedNodes = favouriteNodes.getHeartedNodes(); - var nodeHashes = Object.keys(heartedNodes); - if(nodeHashes.length) { - menuHTML += ''; - } else { - menuHTML += '
Click the heart next to a node\'s title on it\'s own page to save it here for easy access :)
'; - } - return menu.innerHTML = menuHTML; - }, - - // Load SVG, run callback when loaded - loadSVG: function(cb) { - - // Get heart SVG - var xhr = new XMLHttpRequest(); - xhr.open('GET', favouriteNodes.heartPath); - xhr.addEventListener('load', function() { - cb(xhr.responseText); - }); - xhr.send(); - }, - - // Initiate node favouriting - init: function() { - - // Start loading heart SVG before DOM - favouriteNodes.loadSVG(function(svg) { - - // Create heart SVG elem - var div = create('div'); - div.innerHTML = svg; - var heartEl = div.firstChild; - - // Show heart as active if we've already hearted this node - var node = favouriteNodes.getCurrentNode(); - if(favouriteNodes.isHearted(node)) { - heartEl.classList.add(favouriteNodes.activeClass); - } - - // Add click handler - heartEl.addEventListener('click', function() { - - // Heart/unheart node - var node = favouriteNodes.getCurrentNode(); - if(favouriteNodes.isHearted(node)) { - heartEl.classList.remove(favouriteNodes.activeClass); - favouriteNodes.unHeart(node); - } else { - heartEl.classList.add(favouriteNodes.activeClass); - favouriteNodes.heart(node); - } - }); - - // Then inject into DOM when it's ready - DOMReady(function() { - var headerHeight = find('.title').offsetHeight; - var headerBoxShadow = 3; - - // Heart - var titleEl = find('h2.node-title'); - if(titleEl) { - titleEl.insertBefore(heartEl, titleEl.firstChild); - } - - // Menu button - var menuButton = create('div'); - menuButton.classList.add('menu-button'); - menuButton.style.height = headerHeight + 'px'; - menuButton.innerHTML = svg; - menuButton.addEventListener('click', function() { - favouriteNodes.updateHeartedNodesList(); - find('.menu').classList.toggle('active'); - }); - find('header .wrapper').appendChild(menuButton); - - // Menu - var menu = create('div'); - menu.classList.add('menu'); - menu.style.top = (headerHeight + headerBoxShadow) + 'px'; - menu.style.height = 'calc(100% - ' + (headerHeight + headerBoxShadow) + 'px)'; - document.body.appendChild(menu); - favouriteNodes.updateHeartedNodesList(); - }); - }); - - // If current node is hearted - var node = favouriteNodes.getCurrentNode(); - if(favouriteNodes.isHearted(node)) { - - // Heart it again so we get the new name if it's updated - favouriteNodes.heart(node); - } - } - }; - - // Service worker - if(supports.test(['serviceWorker', 'querySelector', 'classList'])) { - - // Register service worker - navigator.serviceWorker.register('/sw.js'); - - // Show cache message on stale pages - DOMReady(function() { - if(window.cacheDate) { - var offlineMessage = create('div'); - offlineMessage.classList.add('cache-message'); - offlineMessage.innerText = '*There seems to be an issue connecting to the server. This data is cached from ' + window.cacheDate; - var main = find('main'); - if(main) { - doc.body.classList.add('no-connection'); - main.insertBefore(offlineMessage, main.firstChild); - } - } - }); - } - - // Init favourite nodes - if(supports.test(['localStorage', 'inlineSVG', 'querySelector', 'classList'])) { - favouriteNodes.init(); - } - - // Add ios class to body on iOS devices - if(supports.test(['classList'])) { - DOMReady(function() { - if( - /iPad|iPhone|iPod/.test(navigator.userAgent) - && !window.MSStream - ) { - doc.body.classList.add('ios'); - } - }); - } - +/* eslint-env browser */ +(function () { + // Space optimisations + var doc = document; + var find = doc.querySelector.bind(doc); + var create = doc.createElement.bind(doc); + + // Run callback when DOM is ready + function domReady(cb) { + // Run now if DOM has already loaded as we're loading async + if (['interactive', 'complete'].indexOf(doc.readyState) >= 0) { + cb(); + + // Otherwise wait for DOM + } else { + doc.addEventListener('DOMContentLoaded', cb); + } + } + + // Feature detection + var supports = { + test: function (features) { + var self = this; + if (!features || features.length < 1) { + return false; + } + return features.every(function (feature) { + return self.tests[feature]; + }); + }, + tests: { + localStorage: (function () { + try { + localStorage.setItem('test', 'test'); + localStorage.removeItem('test'); + return true; + } catch (err) { + return false; + } + })(), + inlineSVG: (function () { + var div = create('div'); + div.innerHTML = ''; + return ( + typeof SVGRect !== 'undefined' && + div.firstChild && + div.firstChild.namespaceURI + ) === 'http://www.w3.org/2000/svg'; + })(), + querySelector: typeof doc.querySelector === 'function', + classList: (function () { + var div = create('div'); + div.innerHTML = ''; + return 'classList' in div.firstChild; + })(), + serviceWorker: 'serviceWorker' in navigator + } + }; + + // Favourite nodes + var favouriteNodes = { + + // Key used in localStorage + storageKey: 'heartedNodes', + + // Url to heart SVG + heartPath: '/assets/heart.svg', + + // Class added to heart SVG element when active + activeClass: 'hearted', + + // Gets current node hash + getCurrentNode: function () { + var node = /^\/node\/([a-zA-Z0-9]+)/.exec(window.location.pathname); + return node ? node[1] : node; + }, + + // Gets current node title + getCurrentNodeTitle: function () { + return find('h2.node-title .name').innerText; + }, + + // Gets hearted nodes + getHeartedNodes: function () { + return JSON.parse(localStorage.getItem(favouriteNodes.storageKey)) || {}; + }, + + // Saves hearted nodes + saveHeartedNodes: function (heartedNodes) { + return localStorage.setItem(favouriteNodes.storageKey, JSON.stringify(heartedNodes)); + }, + + // Checks if node is hearted + isHearted: function (node) { + return typeof favouriteNodes.getHeartedNodes()[node] !== 'undefined'; + }, + + // Heart node + heart: function (node) { + var heartedNodes = favouriteNodes.getHeartedNodes(); + heartedNodes[node] = favouriteNodes.getCurrentNodeTitle(); + favouriteNodes.saveHeartedNodes(heartedNodes); + favouriteNodes.updateHeartedNodesList(); + return heartedNodes; + }, + + // Unheart node + unHeart: function (node) { + var heartedNodes = favouriteNodes.getHeartedNodes(); + delete heartedNodes[node]; + favouriteNodes.saveHeartedNodes(heartedNodes); + favouriteNodes.updateHeartedNodesList(); + return heartedNodes; + }, + + // Get list of hearted nodes + updateHeartedNodesList: function () { + var menu = find('.menu'); + if (!menu) { + return false; + } + var menuHTML = ''; + var heartedNodes = favouriteNodes.getHeartedNodes(); + var nodeHashes = Object.keys(heartedNodes); + if (nodeHashes.length > 0) { + menuHTML += ''; + } else { + menuHTML += '
Click the heart next to a node\'s title on it\'s own page to save it here for easy access :)
'; + } + menu.innerHTML = menuHTML; + return menu.innerHTML; + }, + + // Load SVG, run callback when loaded + loadSVG: function (cb) { + // Get heart SVG + var xhr = new XMLHttpRequest(); + xhr.open('GET', favouriteNodes.heartPath); + xhr.addEventListener('load', function () { + cb(xhr.responseText); + }); + xhr.send(); + }, + + // Initiate node favouriting + init: function () { + // Start loading heart SVG before DOM + favouriteNodes.loadSVG(function (svg) { + // Create heart SVG elem + var div = create('div'); + div.innerHTML = svg; + var heartEl = div.firstChild; + + // Show heart as active if we've already hearted this node + var node = favouriteNodes.getCurrentNode(); + if (favouriteNodes.isHearted(node)) { + heartEl.classList.add(favouriteNodes.activeClass); + } + + // Add click handler + heartEl.addEventListener('click', function () { + // Heart/unheart node + var node = favouriteNodes.getCurrentNode(); + if (favouriteNodes.isHearted(node)) { + heartEl.classList.remove(favouriteNodes.activeClass); + favouriteNodes.unHeart(node); + } else { + heartEl.classList.add(favouriteNodes.activeClass); + favouriteNodes.heart(node); + } + }); + + // Then inject into DOM when it's ready + domReady(function () { + var headerHeight = find('.title').offsetHeight; + var headerBoxShadow = 3; + + // Heart + var titleEl = find('h2.node-title'); + if (titleEl) { + titleEl.insertBefore(heartEl, titleEl.firstChild); + } + + // Menu button + var menuButton = create('div'); + menuButton.classList.add('menu-button'); + menuButton.style.height = headerHeight + 'px'; + menuButton.innerHTML = svg; + menuButton.addEventListener('click', function () { + favouriteNodes.updateHeartedNodesList(); + find('.menu').classList.toggle('active'); + }); + find('header .wrapper').appendChild(menuButton); + + // Menu + var menu = create('div'); + menu.classList.add('menu'); + menu.style.top = (headerHeight + headerBoxShadow) + 'px'; + menu.style.height = 'calc(100% - ' + (headerHeight + headerBoxShadow) + 'px)'; + document.body.appendChild(menu); + favouriteNodes.updateHeartedNodesList(); + }); + }); + + // If current node is hearted + var node = favouriteNodes.getCurrentNode(); + if (favouriteNodes.isHearted(node)) { + // Heart it again so we get the new name if it's updated + favouriteNodes.heart(node); + } + } + }; + + // Service worker + if (supports.test(['serviceWorker', 'querySelector', 'classList'])) { + // Register service worker + navigator.serviceWorker.register('/sw.js'); + + // Show cache message on stale pages + domReady(function () { + if (window.cacheDate) { + var offlineMessage = create('div'); + offlineMessage.classList.add('cache-message'); + offlineMessage.innerText = '*There seems to be an issue connecting to the server. This data is cached from ' + window.cacheDate; + var main = find('main'); + if (main) { + doc.body.classList.add('no-connection'); + main.insertBefore(offlineMessage, main.firstChild); + } + } + }); + } + + // Init favourite nodes + if (supports.test(['localStorage', 'inlineSVG', 'querySelector', 'classList'])) { + favouriteNodes.init(); + } + + // Add ios class to body on iOS devices + if (supports.test(['classList'])) { + domReady(function () { + if ( + /iPad|iPhone|iPod/.test(navigator.userAgent) && + !window.MSStream + ) { + doc.body.classList.add('ios'); + } + }); + } })(); diff --git a/public/sw.js b/public/sw.js index 602bb38..d430dbd 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,92 +1,85 @@ -var cacheName = 'onionite-cache-v1'; -var offlineUrl = '/no-connection'; +/* eslint-env browser */ +var cacheName = 'onionite-cache-v1'; +var offlineUrl = '/no-connection'; // Install -self.addEventListener('install', function(event) { - - // Cache assets - event.waitUntil( - caches.open(cacheName) - .then(function(cache) { - return cache.addAll([ - offlineUrl, - '/', - '/about', - '/assets/style.css', - '/assets/enhancements.js?v2', - '/assets/iconfont.woff', - '/assets/heart.svg' - ]); - }) - ); +self.addEventListener('install', function (event) { + // Cache assets + event.waitUntil( + caches.open(cacheName) + .then(function (cache) { + return cache.addAll([ + offlineUrl, + '/', + '/about', + '/assets/style.css', + '/assets/enhancements.js?v2', + '/assets/iconfont.woff', + '/assets/heart.svg' + ]); + }) + ); }); // Fetch -self.addEventListener('fetch', function(event) { - var requestUrl = new URL(event.request.url); - - // Only do stuff with our own urls - if(requestUrl.origin !== location.origin) { - return; - } - - // Try cache first for assets - if(requestUrl.pathname.startsWith('/assets/')) { - event.respondWith( - caches.match(event.request) - .then(function(response) { - - // If we don't have it make the request - return response || fetch(event.request); - } - ) - ); - return; - } - - // If we navigate to a page - if ( - event.request.mode === 'navigate' || - (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html')) - ) { - event.respondWith( - - // Make the request - fetch(event.request) - .then(function(response) { - - // If it's the homepage or a node page - if(requestUrl.pathname === '/' || requestUrl.pathname.startsWith('/node/')) { - - // Clone the response and read the html - response.clone().text().then(function(html) { - - // Modify the html so we know when it was cached - html = html.replace('window.cacheDate=false;', 'window.cacheDate="'+Date()+'";'); - var modifiedResponse = new Response(new Blob([html]), { headers: response.headers }); - - // Cache the modified response - caches.open(cacheName).then(function(cache) { - cache.put(event.request, modifiedResponse); - }); - }); - } - - // Always return the original response - return response; - }) - - // If it fails - .catch(function() { - - // Try and return a previously cached version - return caches.match(event.request) - .then(function(response) { - - // If we don't have a cached version show pretty offline page - return response || caches.match(offlineUrl); - }); - }) - ); - } +self.addEventListener('fetch', function (event) { + var requestUrl = new URL(event.request.url); + + // Only do stuff with our own urls + if (requestUrl.origin !== location.origin) { + return; + } + + // Try cache first for assets + if (requestUrl.pathname.startsWith('/assets/')) { + event.respondWith( + caches.match(event.request) + .then(function (response) { + // If we don't have it make the request + return response || fetch(event.request); + }) + ); + return; + } + + // If we navigate to a page + if ( + event.request.mode === 'navigate' || + (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html')) + ) { + event.respondWith( + + // Make the request + fetch(event.request) + .then(function (response) { + // If it's the homepage or a node page + if (requestUrl.pathname === '/' || requestUrl.pathname.startsWith('/node/')) { + // Clone the response and read the html + response.clone().text().then(function (html) { + // Modify the html so we know when it was cached + html = html.replace('window.cacheDate=false;', 'window.cacheDate="' + Date() + '";'); + var modifiedResponse = new Response(new Blob([html]), {headers: response.headers}); + + // Cache the modified response + caches.open(cacheName).then(function (cache) { + cache.put(event.request, modifiedResponse); + }); + }); + } + + // Always return the original response + return response; + }) + + // If it fails + .catch(function () { + // Try and return a previously cached version + return caches.match(event.request) + .then(function (response) { + // If we don't have a cached version show pretty offline page + return response || caches.match(offlineUrl); + }); + }) + ); + } });