Browse Source

Follow xo style

pull/11/head
Luke Childs 8 years ago
parent
commit
3b568f7b2e
  1. 4
      controllers/about.js
  2. 9
      controllers/error.js
  3. 6
      controllers/error404.js
  4. 12
      controllers/index.js
  5. 57
      controllers/listing.js
  6. 4
      controllers/no-connection.js
  7. 54
      controllers/node.js
  8. 31
      index.js
  9. 80
      lib/bandwidth-chart.js
  10. 35
      lib/minify.js
  11. 92
      lib/nunjucks-filters.js
  12. 8
      lib/nunjucks-middleware.js
  13. 81
      lib/tor.js
  14. 513
      public/assets/enhancements.js
  15. 165
      public/sw.js

4
controllers/about.js

@ -1,4 +1,4 @@
module.exports = (req, res) => res.render('about.html', {
bodyClass: 'about',
pageTitle: 'About'
bodyClass: 'about',
pageTitle: 'About'
});

9
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});
};

6
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});
};

12
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')
};

57
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);
});
};

4
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'
});

54
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);
});
};

31
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);

80
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: '.'
});
};

35
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
}
})
];

92
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]);
});

8
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();
};

81
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 [];
}
});
}
};

513
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 = '<svg/>';
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 = '<svg/>';
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 += '<ul>';
nodeHashes.forEach(function(node) {
menuHTML += '<li><a href="/node/' + node + '">' + heartedNodes[node] + '</a></li>';
});
menuHTML += '</ul>';
} else {
menuHTML += '<div class="empty">Click the heart next to a node\'s title on it\'s own page to save it here for easy access :)</div>';
}
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 = '<svg/>';
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 = '<svg/>';
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 += '<ul>';
nodeHashes.forEach(function (node) {
menuHTML += '<li><a href="/node/' + node + '">' + heartedNodes[node] + '</a></li>';
});
menuHTML += '</ul>';
} else {
menuHTML += '<div class="empty">Click the heart next to a node\'s title on it\'s own page to save it here for easy access :)</div>';
}
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');
}
});
}
})();

165
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);
});
})
);
}
});

Loading…
Cancel
Save