Luke Childs
8 years ago
15 changed files with 575 additions and 576 deletions
@ -1,4 +1,4 @@ |
|||
module.exports = (req, res) => res.render('about.html', { |
|||
bodyClass: 'about', |
|||
pageTitle: 'About' |
|||
bodyClass: 'about', |
|||
pageTitle: 'About' |
|||
}); |
|||
|
@ -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}); |
|||
}; |
|||
|
@ -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}); |
|||
}; |
|||
|
@ -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') |
|||
}; |
|||
|
@ -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); |
|||
}); |
|||
}; |
|||
|
@ -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' |
|||
}); |
|||
|
@ -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); |
|||
}); |
|||
}; |
|||
|
@ -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: '.' |
|||
}); |
|||
}; |
|||
|
@ -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 |
|||
} |
|||
}) |
|||
]; |
|||
|
@ -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]); |
|||
}); |
|||
|
@ -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(); |
|||
}; |
|||
|
@ -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 []; |
|||
} |
|||
}); |
|||
} |
|||
}; |
|||
|
@ -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'); |
|||
} |
|||
}); |
|||
} |
|||
})(); |
|||
|
@ -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…
Reference in new issue