').addClass('markers')
const select = (count: number) => {
- selected += count;
+ selected += count
if (selected > photos.length) {
- selected = 1;
+ selected = 1
} else if (selected < 1) {
- selected = photos.length;
+ selected = photos.length
}
- $('figure', $photos).hide();
- $('i', $markers).removeClass('selected');
- $('figure:nth-child(' + selected + ')', $photos).show();
+ $('figure', $photos).hide()
+ $('i', $markers).removeClass('selected')
+ $('figure:nth-child(' + selected + ')', $photos).show()
$('i:nth-child(' + selected + ')', $markers).addClass(
'selected'
- );
+ )
if (!navigatedPhoto) {
- navigatedPhoto = true;
+ navigatedPhoto = true
}
- };
+ }
const prev = () => {
- select(-1);
- };
+ select(-1)
+ }
const next = () => {
- select(1);
- };
+ select(1)
+ }
if (!mobileLayout()) {
- enableKeyNav(true, next, prev);
+ enableKeyNav(true, next, prev)
}
for (let i = 0; i < photos.length; i++) {
- const $p = html.photo(photos[i]);
+ const $p = html.photo(photos[i])
if ($p !== null) {
- $photos.append($p);
+ $photos.append($p)
}
}
if (photos.length > MAX_IN_CAROUSEL) {
- $markers.addClass('too-many');
+ $markers.addClass('too-many')
// use the same
tag that icons use to simplify CSS
for (let i = 0; i < photos.length; i++) {
- $markers.append($('').html((i + 1).toString()));
+ $markers.append($('').html((i + 1).toString()))
}
- $markers.append('of ' + photos.length);
+ $markers.append('of ' + photos.length)
} else {
for (let i = 0; i < photos.length; i++) {
- $markers.append(util.html.icon('place'));
+ $markers.append(util.html.icon('place'))
}
}
- $('i:first-child', $markers).addClass('selected');
+ $('i:first-child', $markers).addClass('selected')
html.photoPreview(
e,
@@ -394,86 +393,86 @@ $(function() {
.html('tap photo to view post')
)
.append(util.html.icon('arrow_forward', next))
- );
+ )
}
}
}
- };
+ }
if (qs.center) {
- enableZoomOut();
+ enableZoomOut()
}
- $legendToggle.click(handle.legendToggle);
+ $legendToggle.click(handle.legendToggle)
// legend nav button only visible on mobile
- $('nav button.toggle-legend').click(handle.legendToggle);
- $('nav button.map-link').click(handle.mapLink);
- $('nav button.copy-url').click(handle.copyUrl);
- $('nav button.toggle-photos').click(handle.photoLayerToggle);
+ $('nav button.toggle-legend').click(handle.legendToggle)
+ $('nav button.map-link').click(handle.mapLink)
+ $('nav button.copy-url').click(handle.copyUrl)
+ $('nav button.toggle-photos').click(handle.photoLayerToggle)
- window.addEventListener('resize', handle.windowResize);
+ window.addEventListener('resize', handle.windowResize)
if (legendVisible) {
// if set visible but user hid it then toggle off
if (!util.setting.showMapLegend) {
- $legendToggle.click();
+ $legendToggle.click()
}
} else {
// ensure that legend has 'collapsed' class to match its visibility
- $legendToggle.parents('ul').addClass('collapsed');
+ $legendToggle.parents('ul').addClass('collapsed')
}
map.addControl(nav, 'top-right').on('load', () => {
$.getJSON('/geo.json', data => {
- geoJSON = data;
+ geoJSON = data
if (geoJSON === null) {
- console.error('Unable to retrieve blog GeoJSON');
- return;
+ console.error('Unable to retrieve blog GeoJSON')
+ return
}
- $count.find('div').html(geoJSON.features.length.toString());
- addBaseLayers();
- addMapHandlers();
- addMoscowMountainLayers();
+ $count.find('div').html(geoJSON.features.length.toString())
+ addBaseLayers()
+ addMapHandlers()
+ addMoscowMountainLayers()
// set initial map dimensions
- handle.windowResize();
+ handle.windowResize()
// can't add post layers until base layers are ready
if (post) {
// Expand bounds so pictures aren't right at the edge. This should
// probably do something smarter like a percent of bounding box.
- post.bounds.sw[0] -= 0.01;
- post.bounds.sw[1] -= 0.01;
- post.bounds.ne[0] += 0.01;
- post.bounds.ne[1] += 0.01;
- $.getJSON('/' + post.key + '/geo.json', addPostLayers);
+ post.bounds.sw[0] -= 0.01
+ post.bounds.sw[1] -= 0.01
+ post.bounds.ne[0] += 0.01
+ post.bounds.ne[1] += 0.01
+ $.getJSON('/' + post.key + '/geo.json', addPostLayers)
} else {
- showPositionInUrl = true;
+ showPositionInUrl = true
}
- });
- });
+ })
+ })
/**
* Preview images may be 320 pixels on a side.
*/
function getPreviewPosition(e: mapboxgl.MapMouseEvent): CssPosition {
- let x = e.point.x;
- let y = e.point.y;
+ let x = e.point.x
+ let y = e.point.y
if (mobileLayout(767)) {
return {
top: (mapSize.height - previewSize.height) / 2,
right: 0
- };
+ }
} else {
const offset = {
x: x + previewSize.width - mapSize.width,
y: y + previewSize.height - mapSize.height
- };
- offset.x = offset.x < 0 ? 0 : offset.x + 10;
- offset.y = offset.y < 0 ? 0 : offset.y + 10;
+ }
+ offset.x = offset.x < 0 ? 0 : offset.x + 10
+ offset.y = offset.y < 0 ? 0 : offset.y + 10
- x -= offset.x;
- y -= offset.y;
+ x -= offset.x
+ y -= offset.y
if (offset.x + offset.y > 0) {
map.panBy(
@@ -495,9 +494,9 @@ $(function() {
// lngLat: null,
// originalEvent: null
// }
- );
+ )
}
- return { top: y + 15, left: x + 50 };
+ return { top: y + 15, left: x + 50 }
}
}
@@ -505,18 +504,16 @@ $(function() {
* Load map location from URL.
*/
function parseUrl(): UrlPosition {
- const parts = window.location.search.split(/[&\?]/g);
- const qs: UrlPosition = {};
+ const parts = window.location.search.split(/[&\?]/g)
+ const qs: UrlPosition = {}
for (let i = 0; i < parts.length; i++) {
- const pair = parts[i].split('=');
- if (pair.length == 2) {
- qs[pair[0]] = parseFloat(pair[1]);
- }
+ const pair = parts[i].split('=')
+ if (pair.length == 2) qs[pair[0]] = parseFloat(pair[1])
}
if (qs.lon !== undefined && qs.lat !== undefined) {
- qs.center = [qs.lon, qs.lat];
+ qs.center = [qs.lon, qs.lat]
}
- return qs;
+ return qs
}
/**
@@ -527,23 +524,19 @@ $(function() {
handle.keyNav = (e: KeyboardEvent) => {
switch (e.keyCode) {
case 27:
- handle.mapInteraction();
- break;
+ handle.mapInteraction()
+ break
case 37:
- if (prev !== undefined) {
- prev();
- }
- break;
+ if (prev !== undefined) prev()
+ break
case 39:
- if (next !== undefined) {
- next();
- }
- break;
+ if (next !== undefined) next()
+ break
}
- };
- document.addEventListener('keydown', handle.keyNav);
+ }
+ document.addEventListener('keydown', handle.keyNav)
} else if (handle.keyNav !== null) {
- document.removeEventListener('keydown', handle.keyNav);
+ document.removeEventListener('keydown', handle.keyNav)
}
}
@@ -555,57 +548,53 @@ $(function() {
lngLat: mapboxgl.LngLat,
count: number
): GeoJSON.Feature[] {
- const z = map.getZoom();
- const f = (z * 3) / Math.pow(2, z);
- const sw = [lngLat.lng - f, lngLat.lat - f];
- const ne = [lngLat.lng + f, lngLat.lat + f];
+ const z = map.getZoom()
+ const f = (z * 3) / Math.pow(2, z)
+ const sw = [lngLat.lng - f, lngLat.lat - f]
+ const ne = [lngLat.lng + f, lngLat.lat + f]
const photos =
geoJSON === null
? []
: geoJSON.features
.filter(f => {
- const coord = f.geometry.coordinates;
+ const coord = f.geometry.coordinates
return (
coord[0] >= sw[0] &&
coord[1] >= sw[1] &&
coord[0] <= ne[0] &&
coord[1] <= ne[1]
- );
+ )
})
.map(f => {
- (f.properties as MapPhoto).distance = distance(
+ ;(f.properties as MapPhoto).distance = distance(
lngLat,
f.geometry.coordinates
- );
- return f;
- });
+ )
+ return f
+ })
photos.sort((p1, p2) => {
- let d1 = (p1.properties as MapPhoto).distance;
- let d2 = (p2.properties as MapPhoto).distance;
+ let d1 = (p1.properties as MapPhoto).distance
+ let d2 = (p2.properties as MapPhoto).distance
- if (d1 === undefined) {
- d1 = 0;
- }
- if (d2 === undefined) {
- d2 = 0;
- }
+ if (d1 === undefined) d1 = 0
+ if (d2 === undefined) d2 = 0
- return d1 - d2;
- });
+ return d1 - d2
+ })
- return photos.slice(0, count);
+ return photos.slice(0, count)
}
/**
* Straight-line distance between two points.
*/
function distance(lngLat: mapboxgl.LngLat, point: number[]): number {
- const x1 = lngLat.lng;
- const y1 = lngLat.lat;
- const x2 = point[0];
- const y2 = point[1];
- return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+ const x1 = lngLat.lng
+ const y1 = lngLat.lat
+ const x2 = point[0]
+ const y2 = point[1]
+ return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
/**
@@ -613,7 +602,7 @@ $(function() {
*/
function updateUrl() {
if (showPositionInUrl) {
- const lngLat = map.getCenter();
+ const lngLat = map.getCenter()
const url =
slug +
'/map?lat=' +
@@ -621,8 +610,8 @@ $(function() {
'&lon=' +
lngLat.lng +
'&zoom=' +
- map.getZoom();
- window.history.replaceState(null, '', url);
+ map.getZoom()
+ window.history.replaceState(null, '', url)
}
}
@@ -631,18 +620,18 @@ $(function() {
*/
function enableZoomOut() {
if (initial.zoom === undefined) {
- return;
+ return
}
if (map.getZoom() > initial.zoom && !zoomOutEnabled) {
- zoomOutEnabled = true;
+ zoomOutEnabled = true
$zoomOut
.click(() => {
- map.easeTo(initial);
+ map.easeTo(initial)
})
- .removeClass('disabled');
+ .removeClass('disabled')
} else if (map.getZoom() <= initial.zoom && zoomOutEnabled) {
- zoomOutEnabled = false;
- $zoomOut.off('click').addClass('disabled');
+ zoomOutEnabled = false
+ $zoomOut.off('click').addClass('disabled')
}
}
@@ -650,8 +639,8 @@ $(function() {
* Curry function to update canvas cursor.
*/
const cursor = (name: string = '') => () => {
- canvas.style.cursor = name;
- };
+ canvas.style.cursor = name
+ }
/**
* Retrieve photo ID from preview URL and redirect to post with that photo.
@@ -659,9 +648,9 @@ $(function() {
* Example https://farm3.staticflickr.com/2853/33767184811_9eff6deb48_n.jpg
*/
function showPhotoInPost(url: string) {
- const path = url.split('/');
- const parts = path[path.length - 1].split('_');
- window.location.href = '/' + parts[0];
+ const path = url.split('/')
+ const parts = path[path.length - 1].split('_')
+ window.location.href = '/' + parts[0]
}
/**
@@ -690,22 +679,22 @@ $(function() {
}
},
'photo'
- );
+ )
- $('#legend .track').removeClass('hidden');
+ $('#legend .track').removeClass('hidden')
}
- $('nav > button.link').click(handle.buttonClick);
+ $('nav > button.link').click(handle.buttonClick)
// avoid updating URL with automatic reposition
map.once('zoomend', () => {
window.setTimeout(() => {
- showPositionInUrl = true;
- }, 500);
- });
+ showPositionInUrl = true
+ }, 500)
+ })
// https://www.mapbox.com/mapbox-gl-js/api/#map#fitbounds
- map.fitBounds([post.bounds.sw, post.bounds.ne]);
+ map.fitBounds([post.bounds.sw, post.bounds.ne])
}
/**
@@ -715,7 +704,7 @@ $(function() {
map.addSource('moscow-mountain', {
type: 'vector',
url: 'mapbox://jabbott7.1q8zrllv'
- });
+ })
map.addLayer({
id: 'mountain-labels',
@@ -727,7 +716,10 @@ $(function() {
'text-field': '{name}',
'text-size': {
base: 1,
- stops: [[10, 10], [14, 13]]
+ stops: [
+ [10, 10],
+ [14, 13]
+ ]
},
// "symbol-placement": {
// base: 1,
@@ -744,7 +736,7 @@ $(function() {
},
minzoom: 6,
maxzoom: 18
- });
+ })
map.addLayer(
{
@@ -756,14 +748,17 @@ $(function() {
'line-color': '#55f',
'line-width': {
base: 1,
- stops: [[8, 1], [13, 2]]
+ stops: [
+ [8, 1],
+ [13, 2]
+ ]
}
},
minzoom: style.minZoom,
maxzoom: style.maxZoom
},
'mountain-labels'
- );
+ )
}
/**
@@ -777,7 +772,7 @@ $(function() {
cluster: true,
clusterMaxZoom: 18,
clusterRadius: 30
- });
+ })
}
// https://www.mapbox.com/mapbox-gl-js/style-spec/#layers-circle
@@ -792,13 +787,17 @@ $(function() {
'circle-radius': {
property: 'point_count',
type: 'interval',
- stops: [[0, 10], [10, 12], [100, 15]]
+ stops: [
+ [0, 10],
+ [10, 12],
+ [100, 15]
+ ]
},
'circle-opacity': markerOpacity,
'circle-stroke-width': 3,
'circle-stroke-color': '#ccc'
}
- });
+ })
// https://www.mapbox.com/mapbox-gl-js/style-spec/#layers-symbol
map.addLayer({
@@ -814,7 +813,7 @@ $(function() {
paint: {
'text-color': '#fff'
}
- });
+ })
// https://www.mapbox.com/mapbox-gl-js/example/custom-marker-icons/
map.addLayer({
@@ -829,7 +828,7 @@ $(function() {
'circle-stroke-color': '#fdd',
'circle-opacity': markerOpacity
}
- });
+ })
}
/**
@@ -843,6 +842,6 @@ $(function() {
.on('zoomend', handle.zoomEnd)
.on('moveend', updateUrl)
.on('click', 'cluster', handle.clusterClick)
- .on('click', 'photo', handle.photoClick);
+ .on('click', 'photo', handle.photoClick)
}
-});
+})
diff --git a/src/client/mobile-menu.ts b/src/client/mobile-menu.ts
index 35ab541f..50557cff 100644
--- a/src/client/mobile-menu.ts
+++ b/src/client/mobile-menu.ts
@@ -4,45 +4,43 @@
///
$(function() {
- const $button = $('#mobile-menu-button');
- const $menu = $('#mobile-menu');
- const $body = $('body');
+ const $button = $('#mobile-menu-button')
+ const $menu = $('#mobile-menu')
+ const $body = $('body')
- let prepared = false;
- let visible = false;
+ let prepared = false
+ let visible = false
const close = () => {
$menu.hide(0, () => {
- visible = false;
- $body.css({ position: 'static' });
- });
- };
+ visible = false
+ $body.css({ position: 'static' })
+ })
+ }
$button.click(() => {
if (visible) {
- close();
+ close()
} else {
- $body.css({ position: 'fixed' });
- $menu.show(0, prepare);
+ $body.css({ position: 'fixed' })
+ $menu.show(0, prepare)
}
- });
+ })
/**
* Wire menu events
*/
function prepare() {
- visible = true;
- if (prepared) {
- return;
- }
+ visible = true
+ if (prepared) return
- $menu.find('.close').click(close);
+ $menu.find('.close').click(close)
$menu.find('.menu-categories').on('change', 'select', e => {
- close();
- window.location.assign($(e.target).val() as string);
- });
+ close()
+ window.location.assign($(e.target).val() as string)
+ })
- prepared = true;
+ prepared = true
}
-});
+})
diff --git a/src/client/photo-tag.ts b/src/client/photo-tag.ts
index 8adcab50..9d5d1fe9 100644
--- a/src/client/photo-tag.ts
+++ b/src/client/photo-tag.ts
@@ -6,95 +6,95 @@
/**
* Defined in /views/photo-tag.hbs
*/
-declare const selectedTag: string;
-declare const siteName: string;
+declare const selectedTag: string
+declare const siteName: string
$(function() {
- const emptyTag = '-';
- const css = 'selected';
- const $status = $('#status');
- const $letters = $('#letters');
- const $selectors = $('#selectors');
- const $thumbs = $('#thumbs');
- const id = 'item-' + selectedTag.substr(0, 1).toLowerCase();
+ const emptyTag = '-'
+ const css = 'selected'
+ const $status = $('#status')
+ const $letters = $('#letters')
+ const $selectors = $('#selectors')
+ const $thumbs = $('#thumbs')
+ const id = 'item-' + selectedTag.substr(0, 1).toLowerCase()
/** Tag selection list for current letter */
- let $selector = $selectors.find('#' + id);
+ let $selector = $selectors.find('#' + id)
/** Currently selected letter */
- let $li = $letters.find('li[data-for=' + id + ']');
+ let $li = $letters.find('li[data-for=' + id + ']')
- $selector.val(selectedTag).show();
- $li.addClass(css);
+ $selector.val(selectedTag).show()
+ $li.addClass(css)
- loadPhotoTag(selectedTag);
+ loadPhotoTag(selectedTag)
$letters.find('li').click(function(this: HTMLElement) {
- $li.removeClass(css);
- $li = $(this);
- $li.addClass(css);
+ $li.removeClass(css)
+ $li = $(this)
+ $li.addClass(css)
- $selector.val(emptyTag);
- $selector.hide();
- $selector = $('#' + $li.data('for'));
- $selector.show();
+ $selector.val(emptyTag)
+ $selector.hide()
+ $selector = $('#' + $li.data('for'))
+ $selector.show()
- const $options = $selector.find('option');
+ const $options = $selector.find('option')
if ($options.length == 2) {
- const tag = ($options[1] as HTMLOptionElement).value;
- $selector.val(tag);
- loadPhotoTag(tag);
+ const tag = ($options[1] as HTMLOptionElement).value
+ $selector.val(tag)
+ loadPhotoTag(tag)
} else {
- $thumbs.empty();
- $status.html('Waiting for selection …');
+ $thumbs.empty()
+ $status.html('Waiting for selection …')
}
- });
+ })
$selectors.on('change', 'select', function(
this: HTMLElement,
e: JQuery.Event
) {
- e.stopPropagation();
- e.preventDefault();
+ e.stopPropagation()
+ e.preventDefault()
- const tag = $selector.val() as string;
+ const tag = $selector.val() as string
- loadPhotoTag(tag);
+ loadPhotoTag(tag)
if (tag && tag != emptyTag) {
window.history.pushState(
null,
`${siteName} photos tagged with "${tag}"`,
`/photo-tag/${tag}`
- );
+ )
}
- });
+ })
/**
* Load photo-search.hbs rendered by server.
*/
function loadPhotoTag(tag: string) {
if (tag && tag != emptyTag) {
- const url = `/photo-tag/search/${tag}`;
+ const url = `/photo-tag/search/${tag}`
- $status.html('Retrieving images …');
+ $status.html('Retrieving images …')
$thumbs.load(url, function(
this: HTMLElement,
_response: JQueryResponse,
status: string
) {
- $status.empty();
+ $status.empty()
if (status === 'error') {
- $thumbs.empty();
+ $thumbs.empty()
alert(
`Sorry about that. Looking for "${tag}" photos caused an error.`
- );
+ )
}
- window.scrollTo(0, 0);
- });
+ window.scrollTo(0, 0)
+ })
} else {
- $thumbs.empty();
- $status.empty();
+ $thumbs.empty()
+ $status.empty()
}
}
-});
+})
diff --git a/src/client/post.ts b/src/client/post.ts
index 6fa938d9..be1947b3 100644
--- a/src/client/post.ts
+++ b/src/client/post.ts
@@ -5,7 +5,7 @@
///
interface JQuery {
- lazyload(options?: LazyLoadOptions): any;
+ lazyload(options?: LazyLoadOptions): any
}
/**
@@ -16,36 +16,36 @@ interface JQuery {
* @see http://www.appelsiini.net/projects/lazyload
*/
$(function() {
- const $photos = $('figure');
- const $lb = $('#light-box');
+ const $photos = $('figure')
+ const $lb = $('#light-box')
// clicking on lightbox hides it and re-enables page scroll
$lb.on('click', () => {
- $lb.off('mousemove').hide(0, enablePageScroll);
- });
+ $lb.off('mousemove').hide(0, enablePageScroll)
+ })
// clicking an image opens it in a lightbox
$photos
.find('img')
.on('click touchstart', lightBox)
- .lazyload();
+ .lazyload()
// hovering photo info button loads camera detail
$photos.find('.info-button').one('mouseover', function(this: Element) {
- const $button = $(this);
+ const $button = $(this)
$button
.addClass('loading')
.html(iconHtml('cloud_download', 'Loading …'))
.load($button.parent().data('exif'), function() {
- $button.removeClass('loading').addClass('loaded');
- });
- });
+ $button.removeClass('loading').addClass('loaded')
+ })
+ })
/**
* Material icon HTML
*/
function iconHtml(name: string, text: string): string {
- return util.html.icon(name).get(0).outerHTML + '' + text + '
';
+ return util.html.icon(name).get(0).outerHTML + '' + text + '
'
}
/**
@@ -53,133 +53,133 @@ $(function() {
* defining the big image URL and dimensions.
*/
function lightBox(this: EventTarget, event: JQuery.Event) {
- event.preventDefault();
+ event.preventDefault()
/** Post image */
- const $img = $(this);
+ const $img = $(this)
/** Big image */
- const $big = $lb.find('img');
+ const $big = $lb.find('img')
/** Whether big image is already browser cached */
- let loaded: boolean = $img.data('big-loaded');
- const isTouch = event.type == 'touchstart';
+ let loaded: boolean = $img.data('big-loaded')
+ const isTouch = event.type == 'touchstart'
- const size = new Size($img.data('big-width'), $img.data('big-height'));
+ const size = new Size($img.data('big-width'), $img.data('big-height'))
/** click position relative to image corner */
- const fromCorner = { top: 0, left: 0 };
+ const fromCorner = { top: 0, left: 0 }
/**
* Update image position and panning speed to accomodate window size
*/
const updateSize = (event: JQuery.Event) => {
- let cursor = 'zoom-out';
+ let cursor = 'zoom-out'
- size.update();
+ size.update()
if (isTouch) {
- $lb.on('touchstart', beginDrag);
- $lb.on('touchmove', updateDragPosition);
- centerImage();
+ $lb.on('touchstart', beginDrag)
+ $lb.on('touchmove', updateDragPosition)
+ centerImage()
} else if (size.needsToPan) {
- cursor = 'move';
- $lb.on('mousemove', updateHoverPosition);
+ cursor = 'move'
+ $lb.on('mousemove', updateHoverPosition)
} else {
- $lb.off('mousemove', updateHoverPosition);
+ $lb.off('mousemove', updateHoverPosition)
}
// set initial desktop position and cursor
if (!isTouch) {
- updateHoverPosition(event);
- $big.css('cursor', cursor);
+ updateHoverPosition(event)
+ $big.css('cursor', cursor)
}
- };
+ }
/**
* Update image position within light box
*/
const updateHoverPosition = (event: JQuery.Event) => {
- const x = event.clientX;
- const y = event.clientY;
+ const x = event.clientX
+ const y = event.clientY
if (x !== undefined && y !== undefined) {
- const dx = size.width.offset(x);
- const dy = size.height.offset(y);
- $big.css({ transform: `translate(${dx}px, ${dy}px)` });
+ const dx = size.width.offset(x)
+ const dy = size.height.offset(y)
+ $big.css({ transform: `translate(${dx}px, ${dy}px)` })
}
- };
+ }
const centerImage = () => {
- const dx = size.width.center();
- const dy = size.height.center();
- $big.css({ transform: `translate(${dx}px, ${dy}px)` });
- };
+ const dx = size.width.center()
+ const dy = size.height.center()
+ $big.css({ transform: `translate(${dx}px, ${dy}px)` })
+ }
const firstTouch = (
event: JQuery.Event | TouchEvent
): [number, number] => {
- let x = 0;
- let y = 0;
- const touches = event.targetTouches;
+ let x = 0
+ let y = 0
+ const touches = event.targetTouches
if (touches !== undefined) {
- x = touches[0].clientX;
- y = touches[0].clientY;
+ x = touches[0].clientX
+ y = touches[0].clientY
}
- return [x, y];
- };
+ return [x, y]
+ }
const beginDrag = (event: JQuery.Event | TouchEvent) => {
- const imageAt = $big.position();
- const [touchX, touchY] = firstTouch(event);
+ const imageAt = $big.position()
+ const [touchX, touchY] = firstTouch(event)
- fromCorner.left = imageAt.left - touchX;
- fromCorner.top = imageAt.top - touchY;
- };
+ fromCorner.left = imageAt.left - touchX
+ fromCorner.top = imageAt.top - touchY
+ }
const updateDragPosition = (event: JQuery.Event) => {
- const [touchX, touchY] = firstTouch(event);
- const dx = fromCorner.left + touchX;
- const dy = fromCorner.top + touchY;
+ const [touchX, touchY] = firstTouch(event)
+ const dx = fromCorner.left + touchX
+ const dy = fromCorner.top + touchY
- $big.css({ transform: `translate(${dx}px, ${dy}px)` });
- };
+ $big.css({ transform: `translate(${dx}px, ${dy}px)` })
+ }
if (loaded === undefined) {
- loaded = false;
+ loaded = false
}
if (loaded) {
// assign directly if big image has already been loaded
- $big.attr('src', $img.data('big'));
+ $big.attr('src', $img.data('big'))
} else {
// assign lower resolution image while the bigger one is loading
- $big.attr('src', $img.data('src'));
+ $big.attr('src', $img.data('src'))
// load photo in detached element
$('')
.bind('load', function(this: HTMLImageElement) {
// assign big image to light box once it's loaded
- $big.attr('src', this.src);
- $img.data('big-loaded', true);
+ $big.attr('src', this.src)
+ $img.data('big-loaded', true)
})
- .attr('src', $img.data('big'));
+ .attr('src', $img.data('big'))
}
- $big.height(size.height.image).width(size.width.image);
+ $big.height(size.height.image).width(size.width.image)
// position based on initial click
- updateSize(event as JQuery.Event);
+ updateSize(event as JQuery.Event)
- $lb.show(0, disablePageScroll);
+ $lb.show(0, disablePageScroll)
// update panning calculations if window resizes
- $(window).resize(updateSize);
+ $(window).resize(updateSize)
}
function disablePageScroll() {
- $('html').css({ overflow: 'hidden' });
+ $('html').css({ overflow: 'hidden' })
}
function enablePageScroll() {
- $(window).off('resize');
- $('html').css({ overflow: 'auto' });
+ $(window).off('resize')
+ $('html').css({ overflow: 'auto' })
}
/**
@@ -199,27 +199,27 @@ $(function() {
*/
class Length {
/** Image edge length */
- image: number;
+ image: number
/** Window edge length */
- window: number;
+ window: number
/** How much longer is window edge (usually a negative number) */
- extra: number;
+ extra: number
/** Ratio of mouse to image movement pixels for panning */
- panRatio: number;
+ panRatio: number
constructor(forImage: string) {
- this.image = parseInt(forImage);
- this.window = 0;
- this.extra = 0;
- this.panRatio = 0;
+ this.image = parseInt(forImage)
+ this.window = 0
+ this.extra = 0
+ this.panRatio = 0
}
/**
* Update window dimension and calculate how much larger it is than image.
*/
update(forWindow: number) {
- this.window = forWindow;
- this.extra = (this.window - this.image) / 2;
+ this.window = forWindow
+ this.extra = (this.window - this.image) / 2
}
/**
@@ -228,7 +228,7 @@ $(function() {
* reaches edge of window.
*/
ratio(): number {
- return 2 * ((this.window - this.image) / this.window);
+ return 2 * ((this.window - this.image) / this.window)
}
/**
@@ -237,16 +237,16 @@ $(function() {
*/
offset(m: number): number {
const subtract =
- this.extra > 0 ? 0 : (this.window / 2 - m) * this.panRatio;
+ this.extra > 0 ? 0 : (this.window / 2 - m) * this.panRatio
- return this.extra - subtract;
+ return this.extra - subtract
}
/**
* Get image offset necessary to center the image.
*/
center(): number {
- return this.extra / 2;
+ return this.extra / 2
}
}
@@ -254,23 +254,23 @@ $(function() {
* Represent image size.
*/
class Size {
- width: Length;
- height: Length;
+ width: Length
+ height: Length
/** Whether image needs to pan */
- needsToPan: boolean;
+ needsToPan: boolean
constructor(imageWidth: string, imageHeight: string) {
- this.width = new Length(imageWidth);
- this.height = new Length(imageHeight);
+ this.width = new Length(imageWidth)
+ this.height = new Length(imageHeight)
}
/**
* Update calculations if window is resized.
*/
update() {
- this.height.update(window.innerHeight);
- this.width.update(window.innerWidth);
- this.needsToPan = this.width.extra < 0 || this.height.extra < 0;
+ this.height.update(window.innerHeight)
+ this.width.update(window.innerWidth)
+ this.needsToPan = this.width.extra < 0 || this.height.extra < 0
if (this.needsToPan) {
// pan image using length with biggest ratio
@@ -278,8 +278,8 @@ $(function() {
this.height.panRatio = this.width.panRatio =
this.width.extra < this.height.extra && this.width.extra < 0
? this.width.ratio()
- : this.height.ratio();
+ : this.height.ratio()
}
}
}
-});
+})
diff --git a/src/client/responsive.ts b/src/client/responsive.ts
index f7041acc..ce0022ae 100644
--- a/src/client/responsive.ts
+++ b/src/client/responsive.ts
@@ -6,32 +6,32 @@
/**
* Defined in /views/post.hbs
*/
-declare const pageFeatures: PageFeature;
+declare const pageFeatures: PageFeature
/**
* Only load scripts and data for the current view port and features.
*/
$(function() {
/** Whether mobile resources have been loaded */
- let mobileLoaded = false;
+ let mobileLoaded = false
/** Whether desktop resources have been loaded */
- let desktopLoaded = false;
- const $view = $(window);
- let timer = 0;
+ let desktopLoaded = false
+ const $view = $(window)
+ let timer = 0
/** Page width below which mobile rather than desktop resources will be loaded */
- const breakAt = 1024;
+ const breakAt = 1024
// default features
const feature: PageFeature = {
facebook: false,
timestamp: 0
- };
+ }
// incorporate features set by page
- $.extend(feature, pageFeatures);
- $view.on('resize', resizeHandler);
+ $.extend(feature, pageFeatures)
+ $view.on('resize', resizeHandler)
// always check on first load
- checkResources();
+ checkResources()
/**
* Load different resources if view size crosses break boundary
@@ -39,12 +39,10 @@ $(function() {
function resizeHandler() {
if (mobileLoaded && desktopLoaded) {
// no need to check after everything is loaded
- $view.off('resize');
+ $view.off('resize')
} else {
- if (timer > 0) {
- window.clearTimeout(timer);
- }
- timer = window.setTimeout(checkResources, 500);
+ if (timer > 0) window.clearTimeout(timer)
+ timer = window.setTimeout(checkResources, 500)
}
}
@@ -52,11 +50,11 @@ $(function() {
* Load resources based on current view width.
*/
function checkResources() {
- const width = $view.width();
+ const width = $view.width()
if (width === undefined || width > breakAt) {
- loadDesktop();
+ loadDesktop()
} else {
- loadMobile();
+ loadMobile()
}
}
@@ -64,25 +62,24 @@ $(function() {
* Lazy-load mobile resources
*/
function loadMobile() {
- if (mobileLoaded) {
- return;
- }
- const imageStyle = { width: '100%', height: 'auto' };
+ if (mobileLoaded) return
+
+ const imageStyle = { width: '100%', height: 'auto' }
// could be optimized into a lazy-load
$('#mobile-menu').load('/mobile-menu?t=' + feature.timestamp, () => {
- $.getScript('/js/mobile-menu.js?t=' + feature.timestamp);
- });
+ $.getScript('/js/mobile-menu.js?t=' + feature.timestamp)
+ })
// make post images fill width
$('figure, .category.content a.thumb').each(function(this: HTMLElement) {
$(this)
.css(imageStyle)
.find('img')
- .css(imageStyle);
- });
+ .css(imageStyle)
+ })
- mobileLoaded = true;
+ mobileLoaded = true
}
/**
@@ -90,25 +87,25 @@ $(function() {
*/
function loadDesktop() {
if (desktopLoaded) {
- return;
+ return
}
// could be optimized into a lazy-load
$('#category-menu')
.load('/category-menu?t=' + feature.timestamp)
.on('change', 'select', e => {
- window.location.assign($(e.target).val() as string);
- });
+ window.location.assign($(e.target).val() as string)
+ })
if (feature.facebook) {
loadSource(
'facebook-jssdk',
'//connect.facebook.net/en_US/all.js#xfbml=1&appId=110860435668134',
true
- );
+ )
}
- desktopLoaded = true;
+ desktopLoaded = true
}
/**
@@ -116,25 +113,24 @@ $(function() {
* Twitter employ.
*/
function loadSource(id: string, url: string, async: boolean = false) {
- let js;
- const firstScript = document.getElementsByTagName('script')[0];
+ let js
+ const firstScript = document.getElementsByTagName('script')[0]
if (!document.getElementById(id)) {
- if (async === undefined) {
- async = false;
- }
- js = document.createElement('script');
- js.id = id;
- js.src = url;
- js.async = async;
+ if (async === undefined) async = false
+
+ js = document.createElement('script')
+ js.id = id
+ js.src = url
+ js.async = async
- const parent = firstScript.parentNode;
+ const parent = firstScript.parentNode
if (parent === null) {
- console.error('Failed to load script source');
+ console.error('Failed to load script source')
} else {
- parent.insertBefore(js, firstScript);
+ parent.insertBefore(js, firstScript)
}
}
}
-});
+})
diff --git a/src/client/static-map.ts b/src/client/static-map.ts
index 3e685a4b..1218f03a 100644
--- a/src/client/static-map.ts
+++ b/src/client/static-map.ts
@@ -9,19 +9,19 @@
*/
$(function() {
/** Must be fully qualified path for use with Mapbox */
- const pin = 'https://www.trailimage.com/p.png';
+ const pin = 'https://www.trailimage.com/p.png'
$('.static-map').each((_i, el) => {
- const $img = $(el);
+ const $img = $(el)
// jQuery automatically decodes data
- const locations: number[][] = $img.data('locations');
- const url: string = $img.data('href');
+ const locations: number[][] = $img.data('locations')
+ const url: string = $img.data('href')
if (locations && url && locations.length > 0) {
const pins = locations.map(
l => 'url-' + encodeURIComponent(`${pin}(${l[0]},${l[1]})`)
- );
- $img.attr('src', url.replace('-pins-', pins.join(',')));
+ )
+ $img.attr('src', url.replace('-pins-', pins.join(',')))
}
- });
-});
+ })
+})
diff --git a/src/client/util.ts b/src/client/util.ts
index e8d05e70..ce148263 100644
--- a/src/client/util.ts
+++ b/src/client/util.ts
@@ -11,29 +11,25 @@ const util = {
* @see https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage
*/
save(key: string, value: string): void {
- if (!window.localStorage) {
- return;
- }
- localStorage.setItem(key, value);
+ if (!window.localStorage) return
+ localStorage.setItem(key, value)
},
/**
* Load setting from browser storage.
*/
load(key: string): string | null {
- if (!window.localStorage) {
- return null;
- }
- return localStorage.getItem(key);
+ if (!window.localStorage) return null
+ return localStorage.getItem(key)
},
set showMapLegend(value: boolean) {
- util.setting.save('map-legend', value ? 'true' : 'false');
+ util.setting.save('map-legend', value ? 'true' : 'false')
},
get showMapLegend(): boolean {
- const value = util.setting.load('map-legend');
- return value ? value == 'true' : true;
+ const value = util.setting.load('map-legend')
+ return value ? value == 'true' : true
},
/**
@@ -41,16 +37,16 @@ const util = {
*/
set menuCategory(selected: (string | null)[] | null) {
if (typeof selected === 'string') {
- selected = [selected, null];
+ selected = [selected, null]
}
if (selected !== null) {
- util.setting.save('menu', selected.join());
+ util.setting.save('menu', selected.join())
}
},
get menuCategory(): (string | null)[] | null {
- const value = util.setting.load('menu');
- return value === null ? null : value[1].split(',');
+ const value = util.setting.load('menu')
+ return value === null ? null : value[1].split(',')
}
},
html: {
@@ -62,12 +58,11 @@ const util = {
icon(name: string, handler?: (e: JQuery.Event) => void): JQuery {
const $icon = $('')
.addClass('material-icons ' + name)
- .text(name);
+ .text(name)
- if (handler !== undefined) {
- $icon.click(handler);
- }
- return $icon;
+ if (handler !== undefined) $icon.click(handler)
+
+ return $icon
}
}
-};
+}
diff --git a/src/config/blogspot.ts b/src/config/blogspot.ts
index f41e79b3..63521d84 100644
--- a/src/config/blogspot.ts
+++ b/src/config/blogspot.ts
@@ -1,4 +1,4 @@
-export const domain = 'trailimage.blogspot.com';
+export const domain = 'trailimage.blogspot.com'
/**
* Match old blog URLs to new. Slug is always prefixed by `/YYYY/MM/`. Route
@@ -91,4 +91,4 @@ export const redirects: { [key: string]: string } = {
'lucky-peak-with-laura': 'lucky-peak-with-laura',
'cricket-ridge-ride': 'cricket-ridge-ride',
'caterpillar-ridge-ride': 'spring-caterpillars-on-the-boise-ridge'
-};
+}
diff --git a/src/config/index.ts b/src/config/index.ts
index 1e1f1ecf..4b421ad9 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -1,12 +1,12 @@
-import { Duration, env } from '@toba/node-tools';
-import { mapProvider } from './map-provider';
-import { postProvider } from './post-provider';
-import { owner, site, domain } from './models';
-import { redirects, photoTagChanges } from './redirects';
-import { bing, facebook, mapbox, google } from './vendors';
-import { keywords, style } from './views';
+import { Duration, env } from '@toba/node-tools'
+import { mapProvider } from './map-provider'
+import { postProvider } from './post-provider'
+import { owner, site, domain } from './models'
+import { redirects, photoTagChanges } from './redirects'
+import { bing, facebook, mapbox, google } from './vendors'
+import { keywords, style } from './views'
-const isProduction = process.env['NODE_ENV'] === 'production';
+const isProduction = process.env['NODE_ENV'] === 'production'
export const posts = {
/**
@@ -18,7 +18,7 @@ export const posts = {
artistNames: ['Abbott', 'Wright', 'Bowman', 'Thomas', 'Reed'],
/** Key (slug) of root category to display on home page */
defaultCategory: 'when'
-};
+}
export const config = {
env,
@@ -54,8 +54,8 @@ export const config = {
cache: {
/** Enable or disable all caching */
setAll(enabled: boolean) {
- this.views = enabled;
- this.maps = enabled;
+ this.views = enabled
+ this.maps = enabled
},
/** Whether to cache rendered template views */
views: isProduction,
@@ -78,4 +78,4 @@ export const config = {
photoTagChanges,
alwaysKeywords: 'Adventure, Scenery, Photography,',
keywords: keywords.join(', ')
-};
+}
diff --git a/src/config/map-provider.ts b/src/config/map-provider.ts
index cdd310fb..dda9c819 100644
--- a/src/config/map-provider.ts
+++ b/src/config/map-provider.ts
@@ -1,7 +1,7 @@
-import { env } from '@toba/node-tools';
-import { ProviderConfig } from '@trailimage/google-provider';
-import { domain } from './models';
-import { mapSource } from './mapsource';
+import { env } from '@toba/node-tools'
+import { ProviderConfig } from '@trailimage/google-provider'
+import { domain } from './models'
+import { mapSource } from './mapsource'
/**
* @see http://code.google.com/apis/console/#project:1033232213688
@@ -39,4 +39,4 @@ export const mapProvider: ProviderConfig = {
'https://www.gaiagps.com/map/?layer=GaiaTopoRasterFeet&lat={lat}&lon={lon}&zoom={zoom}'
},
source: mapSource
-};
+}
diff --git a/src/config/mapsource.ts b/src/config/mapsource.ts
index 25416e1b..68d06d31 100644
--- a/src/config/mapsource.ts
+++ b/src/config/mapsource.ts
@@ -1,5 +1,5 @@
-import { is, titleCase } from '@toba/node-tools';
-import { MapSource, MapProperties, relabel } from '@toba/map';
+import { is, titleCase } from '@toba/node-tools'
+import { MapSource, MapProperties, relabel } from '@toba/map'
const vehicle: { [key: string]: string } = {
ATV: 'ATV',
@@ -7,7 +7,7 @@ const vehicle: { [key: string]: string } = {
JEEP: 'Jeep',
MOTORCYCLE: 'Motorcycle',
UTV: 'UTV'
-};
+}
/**
* Update seasonal restriction field.
@@ -18,66 +18,58 @@ function seasonal(
out: MapProperties
): void {
if (is.defined(from, vehicleKey)) {
- out[vehicle[vehicleKey] + ' Allowed'] = from[vehicleKey];
+ out[vehicle[vehicleKey] + ' Allowed'] = from[vehicleKey]
}
}
function trails(from: MapProperties): MapProperties {
- const out: MapProperties = { description: '' };
- const miles: number = from['MILES'] as number;
- const who = 'Jurisdiction';
- let name: string = from['NAME'] as string;
- let label: string = from['name'] as string;
+ const out: MapProperties = { description: '' }
+ const miles: number = from['MILES'] as number
+ const who = 'Jurisdiction'
+ let name: string = from['NAME'] as string
+ let label: string = from['name'] as string
- if (miles && miles > 0) {
- out['Miles'] = miles;
- }
- if (is.value(label)) {
- label = label.trim();
- }
+ if (miles && miles > 0) out['Miles'] = miles
+ if (is.value(label)) label = label.trim()
if (!is.empty(name) && !is.empty(label)) {
- name = titleCase(name.trim());
+ name = titleCase(name.trim())
// label is usually just a number so prefer name when supplied
- const num = label.replace(/\D/g, '');
+ const num = label.replace(/\D/g, '')
// some names alread include the road or trail number and
// some have long numbers that aren't helpful
label =
(num.length > 1 && name.includes(num)) || num.length > 3
? name
- : name + ' ' + label;
+ : name + ' ' + label
}
- if (label) {
- out['Label'] = label;
- }
+ if (label) out['Label'] = label
- Object.keys(vehicle).forEach(key => {
- seasonal(key, from, out);
- });
+ Object.keys(vehicle).forEach(key => seasonal(key, from, out))
- relabel(from, out, { JURISDICTION: who });
+ relabel(from, out, { JURISDICTION: who })
if (is.defined(out, who)) {
- out[who] = titleCase(out[who] as string);
+ out[who] = titleCase(out[who] as string)
}
- return out;
+ return out
}
/**
* Normalize mining field names.
*/
function mines(from: MapProperties): MapProperties {
- const out: MapProperties = { description: '' };
+ const out: MapProperties = { description: '' }
// lowercase "name" is the county name
relabel(from, out, {
FSAgencyName: 'Forest Service Agency',
LandOwner: 'Land Owner',
DEPOSIT: 'Name',
Mining_District: 'Mining District'
- });
- return out;
+ })
+ return out
}
export const mapSource: { [key: string]: MapSource } = {
@@ -166,4 +158,4 @@ export const mapSource: { [key: string]: MapSource } = {
url:
'https://drive.google.com/uc?export=download&id=0B0lgcM9JCuSbbDV2UUNILWpUc28'
}
-};
+}
diff --git a/src/config/models.ts b/src/config/models.ts
index 92a2d83c..f5f491c4 100644
--- a/src/config/models.ts
+++ b/src/config/models.ts
@@ -1,10 +1,10 @@
-import { env } from '@toba/node-tools';
-import { OwnerConfig, SiteConfig } from '@trailimage/models';
+import { env } from '@toba/node-tools'
+import { OwnerConfig, SiteConfig } from '@trailimage/models'
/** Site domain name. */
-export const domain = 'trailimage.com';
+export const domain = 'trailimage.com'
-const url = `http://www.${domain}`;
+const url = `http://www.${domain}`
export const owner: OwnerConfig = {
name: 'Jason Abbott',
@@ -20,7 +20,7 @@ export const owner: OwnerConfig = {
'https://www.youtube.com/user/trailimage',
'https://twitter.com/trailimage'
]
-};
+}
export const site: SiteConfig = {
domain,
@@ -40,4 +40,4 @@ export const site: SiteConfig = {
width: 308,
height: 60
}
-};
+}
diff --git a/src/config/post-provider.ts b/src/config/post-provider.ts
index 44dffe92..5d2ea8a2 100644
--- a/src/config/post-provider.ts
+++ b/src/config/post-provider.ts
@@ -1,6 +1,6 @@
-import { env } from '@toba/node-tools';
-import { Flickr, ProviderConfig } from '@trailimage/flickr-provider';
-import { domain } from './models';
+import { env } from '@toba/node-tools'
+import { Flickr, ProviderConfig } from '@trailimage/flickr-provider'
+import { domain } from './models'
/** Preferred photo sizes */
export const sizes = {
@@ -16,7 +16,7 @@ export const sizes = {
Flickr.SizeCode.Large1600,
Flickr.SizeCode.Large1024
]
-};
+}
export const postProvider: ProviderConfig = {
/** Photo sizes that must be retrieved for certain contexts */
@@ -51,4 +51,4 @@ export const postProvider: ProviderConfig = {
}
}
}
-};
+}
diff --git a/src/config/redirects.ts b/src/config/redirects.ts
index 7b91b98c..3d1501bb 100644
--- a/src/config/redirects.ts
+++ b/src/config/redirects.ts
@@ -4,7 +4,7 @@ export const redirects: { [key: string]: string } = {
'backroads-to-college': 'panhandle-past-and-future',
'owyhee-snow-and-sands-uplands': 'owyhee-snow-and-sand',
'lunch-at-trinity-lookout': 'trinity-lookout-lunch'
-};
+}
/**
* Support for renamed photo tags. The key is the old name and value is the
@@ -15,4 +15,4 @@ export const photoTagChanges: { [key: string]: string } = {
jessica: 'jessicawright',
jime: 'jimeldredge',
jessicaabbott: 'jessicawright'
-};
+}
diff --git a/src/config/vendors.ts b/src/config/vendors.ts
index d5d96211..d495a895 100644
--- a/src/config/vendors.ts
+++ b/src/config/vendors.ts
@@ -1,19 +1,19 @@
-import { env } from '@toba/node-tools';
-import { mapSource } from './mapsource';
-import { MapSource } from '@toba/map';
+import { env } from '@toba/node-tools'
+import { mapSource } from './mapsource'
+import { MapSource } from '@toba/map'
export interface MapboxConfig {
- accessToken: string;
+ accessToken: string
style: {
- dynamic: string;
- static: string;
- };
- mapSource: { [key: string]: MapSource };
+ dynamic: string
+ static: string
+ }
+ mapSource: { [key: string]: MapSource }
}
export const bing = {
key: env('BING_KEY', null)
-};
+}
/**
* https://developers.facebook.com/docs/reference/plugins/like/
@@ -26,7 +26,7 @@ export const facebook = {
adminID: '1332883594',
enabled: true,
authorURL: 'https://www.facebook.com/jason.e.abbott'
-};
+}
export const google = {
apiKey: env('GOOGLE_KEY', null),
@@ -34,7 +34,7 @@ export const google = {
analyticsID: '22180727', // shown as 'UA-22180727-1
searchEngineID: env('GOOGLE_SEARCH_ID', null),
blogID: '118459106898417641'
-};
+}
export const mapbox: MapboxConfig = {
accessToken: env('MAPBOX_ACCESS_TOKEN'),
@@ -45,4 +45,4 @@ export const mapbox: MapboxConfig = {
static: 'jabbott7/cj1prg25g002o2ro2xtzos6cy'
},
mapSource
-};
+}
diff --git a/src/config/views.ts b/src/config/views.ts
index 40f651bc..9ed2cafa 100644
--- a/src/config/views.ts
+++ b/src/config/views.ts
@@ -1,4 +1,4 @@
-import { sizes } from './post-provider';
+import { sizes } from './post-provider'
export const keywords = [
'BMW R1200GS',
@@ -14,7 +14,7 @@ export const keywords = [
'scenery',
'idaho',
'mountains'
-];
+]
export const style = {
icon: {
@@ -57,4 +57,4 @@ export const style = {
* should match value in `settings.less`.
*/
contentWidth: 650
-};
+}
diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts
index 25c755c1..66916f27 100644
--- a/src/controllers/auth.ts
+++ b/src/controllers/auth.ts
@@ -1,31 +1,31 @@
-import { config as modelConfig, DataProvider } from '@trailimage/models';
-import { Response, Request } from 'express';
-import { is } from '@toba/node-tools';
-import { Page, Layout, view } from '../views/';
+import { config as modelConfig, DataProvider } from '@trailimage/models'
+import { Response, Request } from 'express'
+import { is } from '@toba/node-tools'
+import { Page, Layout, view } from '../views/'
/**
* Redirect to authorization URL for unauthorized providers.
*/
export function main(_req: Request, res: Response) {
- [
+ ;[
modelConfig.providers.post,
modelConfig.providers.map,
modelConfig.providers.video
].forEach(async p => {
if (is.value>(p) && !p.isAuthenticated) {
- const url = await p.authorizationURL();
- res.redirect(url);
- return;
+ const url = await p.authorizationURL()
+ res.redirect(url)
+ return
}
- });
+ })
}
export function postAuth(req: Request, res: Response) {
- authCallback(modelConfig.providers.post, req, res);
+ authCallback(modelConfig.providers.post, req, res)
}
export function mapAuth(req: Request, res: Response) {
- authCallback(modelConfig.providers.map, req, res);
+ authCallback(modelConfig.providers.map, req, res)
}
/**
@@ -41,15 +41,15 @@ async function authCallback(
return view.internalError(
res,
new ReferenceError('No data provider supplied for authorization')
- );
+ )
}
- const token = await p.getAccessToken(req);
+ const token = await p.getAccessToken(req)
res.render(Page.Authorize, {
title: 'Flickr Access',
token: token.access,
secret: token.secret,
layout: Layout.NONE
- });
+ })
}
-export const auth = { map: mapAuth, post: postAuth, main };
+export const auth = { map: mapAuth, post: postAuth, main }
diff --git a/src/controllers/category.test.ts b/src/controllers/category.test.ts
index a1c17894..f486d668 100644
--- a/src/controllers/category.test.ts
+++ b/src/controllers/category.test.ts
@@ -1,73 +1,71 @@
-import '@toba/test';
-import { MockRequest, MockResponse } from '@toba/test';
-import { RouteParam } from '../routes';
-import { Page } from '../views/index';
-import { category } from './index';
-import { loadMockData } from '../.test-data';
+import '@toba/test'
+import { MockRequest, MockResponse } from '@toba/test'
+import { RouteParam } from '../routes'
+import { Page } from '../views/index'
+import { category } from './index'
+import { loadMockData } from '../.test-data'
-const req = new MockRequest();
-const res = new MockResponse(req);
+const req = new MockRequest()
+const res = new MockResponse(req)
beforeAll(async done => {
- await loadMockData();
- console.debug = console.log = jest.fn();
- done();
-});
+ await loadMockData()
+ console.debug = console.log = jest.fn()
+ done()
+})
-beforeEach(() => {
- res.reset();
-});
+beforeEach(() => res.reset())
-const contextKeys = ['description', 'linkData', 'subtitle', 'title'];
+const contextKeys = ['description', 'linkData', 'subtitle', 'title']
test('renders home page for default category', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.Category);
- const context = res.rendered.context;
- expect(context).toHaveAllProperties('posts', ...contextKeys);
+ expect(res).toRenderTemplate(Page.Category)
+ const context = res.rendered.context
+ expect(context).toHaveAllProperties('posts', ...contextKeys)
// Link Data should be serialized to linkData field
- expect(context).not.toHaveProperty('jsonLD');
- expect(context).not.toHaveProperty('subcategories');
- expect(context!['posts']).toHaveLength(5);
- expect(context!.title).toBe('2016');
- expect(context!.subtitle).toBe('Five Adventures');
- done();
- };
- category.home(req, res);
-});
+ expect(context).not.toHaveProperty('jsonLD')
+ expect(context).not.toHaveProperty('subcategories')
+ expect(context!['posts']).toHaveLength(5)
+ expect(context!.title).toBe('2016')
+ expect(context!.subtitle).toBe('Five Adventures')
+ done()
+ }
+ category.home(req, res)
+})
test('renders a list of subcategories', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.CategoryList);
- const context = res.rendered.context;
- expect(context).toHaveAllProperties('subcategories', ...contextKeys);
- expect(context!['subcategories']).toHaveLength(7);
- expect(context!.title).toBe('What');
- done();
- };
- req.params[RouteParam.RootCategory] = 'what';
- category.list(req, res);
-});
+ expect(res).toRenderTemplate(Page.CategoryList)
+ const context = res.rendered.context
+ expect(context).toHaveAllProperties('subcategories', ...contextKeys)
+ expect(context!['subcategories']).toHaveLength(7)
+ expect(context!.title).toBe('What')
+ done()
+ }
+ req.params[RouteParam.RootCategory] = 'what'
+ category.list(req, res)
+})
test('displays category at path', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.CategoryList);
- const context = res.rendered.context;
- expect(context).toHaveAllProperties('subcategories', ...contextKeys);
- expect(context!.title).toBe('When');
- expect(context!.subtitle).toBe('Thirteen Subcategories');
- done();
- };
- req.params[RouteParam.RootCategory] = 'when';
- req.params[RouteParam.Category] = '2016';
- category.list(req, res);
-});
+ expect(res).toRenderTemplate(Page.CategoryList)
+ const context = res.rendered.context
+ expect(context).toHaveAllProperties('subcategories', ...contextKeys)
+ expect(context!.title).toBe('When')
+ expect(context!.subtitle).toBe('Thirteen Subcategories')
+ done()
+ }
+ req.params[RouteParam.RootCategory] = 'when'
+ req.params[RouteParam.Category] = '2016'
+ category.list(req, res)
+})
test('creates category menu', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.CategoryMenu);
- expect(res.rendered.context).toHaveAllProperties('description', 'blog');
- done();
- };
- category.menu(req, res);
-});
+ expect(res).toRenderTemplate(Page.CategoryMenu)
+ expect(res.rendered.context).toHaveAllProperties('description', 'blog')
+ done()
+ }
+ category.menu(req, res)
+})
diff --git a/src/controllers/category.ts b/src/controllers/category.ts
index ac168474..6fb8978b 100644
--- a/src/controllers/category.ts
+++ b/src/controllers/category.ts
@@ -1,23 +1,23 @@
-import { is, sayNumber } from '@toba/node-tools';
-import { Category, blog } from '@trailimage/models';
-import { Request, Response } from 'express';
-import { config } from '../config';
-import { RouteParam } from '../routes';
-import { Layout, Page } from '../views/template';
-import { view, ViewContext } from '../views/view';
+import { is, sayNumber } from '@toba/node-tools'
+import { Category, blog } from '@trailimage/models'
+import { Request, Response } from 'express'
+import { config } from '../config'
+import { RouteParam } from '../routes'
+import { Layout, Page } from '../views/template'
+import { view, ViewContext } from '../views/view'
function send(req: Request, res: Response, path: string) {
view.send(res, path, async render => {
// use renderer to build view that wasn't cached
- const category = blog.categoryWithKey(path);
+ const category = blog.categoryWithKey(path)
if (!is.value(category)) {
- return view.notFound(req, res);
+ return view.notFound(req, res)
}
- await category.ensureLoaded();
+ await category.ensureLoaded()
- const count = category.posts.size;
+ const count = category.posts.size
render(
Page.Category,
@@ -26,8 +26,8 @@ function send(req: Request, res: Response, path: string) {
subtitle: config.site.postAlias + (count > 1 ? 's' : ''),
posts: Array.from(category.posts)
})
- );
- });
+ )
+ })
}
/**
@@ -40,7 +40,7 @@ export function forPath(req: Request, res: Response) {
req.params[RouteParam.RootCategory] +
'/' +
req.params[RouteParam.Category]
- );
+ )
}
/**
@@ -49,11 +49,11 @@ export function forPath(req: Request, res: Response) {
* the default tag has years as child tags
*/
export function home(req: Request, res: Response) {
- const category = blog.categories.get(config.posts.defaultCategory);
- let year = new Date().getFullYear();
- let subcategory: Category | undefined = undefined;
- let postCount = 0;
- let tryCount = 0;
+ const category = blog.categories.get(config.posts.defaultCategory)
+ let year = new Date().getFullYear()
+ let subcategory: Category | undefined = undefined
+ let postCount = 0
+ let tryCount = 0
if (category === undefined) {
return view.internalError(
@@ -61,17 +61,17 @@ export function home(req: Request, res: Response) {
new Error(
`Unable to find default category ${config.posts.defaultCategory}`
)
- );
+ )
}
while (postCount == 0 && tryCount < 10) {
// step backwards until a year with posts is found
- subcategory = category.getSubcategory(year.toString());
+ subcategory = category.getSubcategory(year.toString())
if (is.value(subcategory)) {
- postCount = subcategory.posts.size;
+ postCount = subcategory.posts.size
}
- tryCount++;
- year--;
+ tryCount++
+ year--
}
if (subcategory === undefined) {
return view.internalError(
@@ -79,28 +79,24 @@ export function home(req: Request, res: Response) {
new Error(
`Unable to find year with posts in ${config.posts.defaultCategory}`
)
- );
+ )
}
- send(req, res, subcategory.key);
+ send(req, res, subcategory.key)
}
/**
* Show root category with list of subcategories.
*/
export function list(req: Request, res: Response) {
- const key = req.params[RouteParam.RootCategory] as string;
+ const key = req.params[RouteParam.RootCategory] as string
- if (is.empty(key)) {
- return view.notFound(req, res);
- }
+ if (is.empty(key)) return view.notFound(req, res)
view.send(res, key, render => {
// use renderer to build view that wasn't cached
- const category = blog.categoryWithKey(key);
+ const category = blog.categoryWithKey(key)
- if (!is.value(category)) {
- return view.notFound(req, res);
- }
+ if (!is.value(category)) return view.notFound(req, res)
render(
Page.CategoryList,
@@ -109,14 +105,14 @@ export function list(req: Request, res: Response) {
subtitle: 'Subcategories',
subcategories: Array.from(category.subcategories)
})
- );
- });
+ )
+ })
}
export function menu(_req: Request, res: Response) {
view.send(res, Page.CategoryMenu, render => {
- render(Page.CategoryMenu, { blog, layout: Layout.None });
- });
+ render(Page.CategoryMenu, { blog, layout: Layout.None })
+ })
}
/**
@@ -131,6 +127,6 @@ const standardContext = (
...context,
title: category.title,
subtitle: `${sayNumber(childCount)} ${context.subtitle}`
-});
+})
-export const category = { forPath, home, list, menu };
+export const category = { forPath, home, list, menu }
diff --git a/src/controllers/index.ts b/src/controllers/index.ts
index 043a2b59..235371d4 100644
--- a/src/controllers/index.ts
+++ b/src/controllers/index.ts
@@ -1,8 +1,8 @@
-export { post } from './post';
-export { category } from './category';
-export { photo } from './photo';
-export { staticPage } from './static';
-export { menu } from './menu';
-export { auth } from './auth';
-export { map } from './map';
-export { postFeed } from './rss';
+export { post } from './post'
+export { category } from './category'
+export { photo } from './photo'
+export { staticPage } from './static'
+export { menu } from './menu'
+export { auth } from './auth'
+export { map } from './map'
+export { postFeed } from './rss'
diff --git a/src/controllers/map.ts b/src/controllers/map.ts
index 0f71c7b0..e13efca8 100644
--- a/src/controllers/map.ts
+++ b/src/controllers/map.ts
@@ -1,4 +1,4 @@
-import { MapSource, loadSource } from '@toba/map';
+import { MapSource, loadSource } from '@toba/map'
import {
Encoding,
Header,
@@ -6,17 +6,17 @@ import {
is,
addCharSet,
inferMimeType
-} from '@toba/node-tools';
-import { Post, blog } from '@trailimage/models';
-import { Request, Response } from 'express';
-import * as compress from 'zlib';
-import { config } from '../config';
-import { RouteParam } from '../routes';
-import { Layout, Page, view } from '../views/';
-
-const mapPath = 'map';
+} from '@toba/node-tools'
+import { Post, blog } from '@trailimage/models'
+import { Request, Response } from 'express'
+import * as compress from 'zlib'
+import { config } from '../config'
+import { RouteParam } from '../routes'
+import { Layout, Page, view } from '../views/'
+
+const mapPath = 'map'
const googleMapURL =
- 'https://maps.google.com/?t=h&q=[lat-lon]&ll=[lat-lon]&z=13';
+ 'https://maps.google.com/?t=h&q=[lat-lon]&ll=[lat-lon]&z=13'
/**
* Render map screen for a post. Add photo ID to template context if given so
@@ -30,14 +30,14 @@ async function render(
res: Response
): Promise {
if (!is.value(post)) {
- return view.notFound(req, res);
+ return view.notFound(req, res)
}
- const key: string | undefined = post.isPartial ? post.seriesKey : post.key;
- const photoID: string = req.params[RouteParam.PhotoID];
+ const key: string | undefined = post.isPartial ? post.seriesKey : post.key
+ const photoID: string = req.params[RouteParam.PhotoID]
if (is.numeric(photoID) && post.photosLoaded) {
- const photo = post.photos?.find(p => p.id == photoID);
+ const photo = post.photos?.find(p => p.id == photoID)
if (photo !== undefined) {
res.redirect(
@@ -45,13 +45,13 @@ async function render(
/\[lat-lon\]/g,
photo.latitude + ',' + photo.longitude
)
- );
- return;
+ )
+ return
}
}
// ensure photos are loaded to calculate bounds for map zoom
- await post.getPhotos();
+ await post.getPhotos()
res.render(Page.Mapbox, {
layout: Layout.None,
@@ -61,7 +61,7 @@ async function render(
key,
photoID: is.numeric(photoID) ? photoID : 0,
config
- });
+ })
}
/**
@@ -72,14 +72,14 @@ function blogMap(_req: Request, res: Response) {
layout: Layout.None,
title: config.site.title + ' Map',
config
- });
+ })
}
/**
* Render map for a single post.
*/
function post(req: Request, res: Response) {
- render(blog.postWithKey(req.params[RouteParam.PostKey]), req, res);
+ render(blog.postWithKey(req.params[RouteParam.PostKey]), req, res)
}
/**
@@ -93,27 +93,27 @@ function series(req: Request, res: Response) {
),
req,
res
- );
+ )
}
/**
* Compressed GeoJSON of all site photos.
*/
function photoJSON(_req: Request, res: Response) {
- view.sendJSON(res, mapPath, blog.geoJSON.bind(blog));
+ view.sendJSON(res, mapPath, blog.geoJSON.bind(blog))
}
/**
* Compressed GeoJSON of post photos and possible track.
*/
async function trackJSON(req: Request, res: Response) {
- const slug = req.params[RouteParam.PostKey];
- const post = blog.postWithKey(slug);
+ const slug = req.params[RouteParam.PostKey]
+ const post = blog.postWithKey(slug)
if (is.value(post)) {
- view.sendJSON(res, `${slug}/${mapPath}`, post.geoJSON.bind(post));
+ view.sendJSON(res, `${slug}/${mapPath}`, post.geoJSON.bind(post))
} else {
- view.notFound(req, res);
+ view.notFound(req, res)
}
}
@@ -121,38 +121,34 @@ async function trackJSON(req: Request, res: Response) {
* Retrieve, parse and display a map source.
*/
async function source(req: Request, res: Response) {
- const key: string = req.params[RouteParam.MapSource];
+ const key: string = req.params[RouteParam.MapSource]
- if (!is.text(key)) {
- return view.notFound(req, res);
- }
+ if (!is.text(key)) return view.notFound(req, res)
- const geo = await loadSource(key.replace('.json', ''));
+ const geo = await loadSource(key.replace('.json', ''))
- if (!is.value(geo)) {
- return view.notFound(req, res);
- }
+ if (!is.value(geo)) return view.notFound(req, res)
- const geoText = JSON.stringify(geo);
+ const geoText = JSON.stringify(geo)
try {
compress.gzip(Buffer.from(geoText), (err: Error, buffer: Buffer) => {
if (is.value(err)) {
- view.internalError(res, err);
+ view.internalError(res, err)
} else {
- res.setHeader(Header.Content.Encoding, Encoding.GZip);
- res.setHeader(Header.CacheControl, 'max-age=86400, public'); // seconds
- res.setHeader(Header.Content.Type, addCharSet(MimeType.JSON));
+ res.setHeader(Header.Content.Encoding, Encoding.GZip)
+ res.setHeader(Header.CacheControl, 'max-age=86400, public') // seconds
+ res.setHeader(Header.Content.Type, addCharSet(MimeType.JSON))
res.setHeader(
Header.Content.Disposition,
`attachment; filename=${key}`
- );
- res.write(buffer);
- res.end();
+ )
+ res.write(buffer)
+ res.end()
}
- });
+ })
} catch (err) {
- view.internalError(res, err);
+ view.internalError(res, err)
}
}
@@ -162,26 +158,26 @@ async function source(req: Request, res: Response) {
function gpx(req: Request, res: Response) {
const post = config.providers.map.allowDownload
? blog.postWithKey(req.params[RouteParam.PostKey])
- : null;
+ : null
if (is.value(post)) {
- const fileName = post.title + '.gpx';
- const mimeType = inferMimeType(fileName);
+ const fileName = post.title + '.gpx'
+ const mimeType = inferMimeType(fileName)
res.setHeader(
Header.Content.Disposition,
`attachment; filename=${fileName}`
- );
+ )
if (mimeType !== null) {
- res.setHeader(Header.Content.Type, mimeType);
+ res.setHeader(Header.Content.Type, mimeType)
}
post.gpx(res).catch(err => {
- console.error(err);
- res.removeHeader(Header.Content.Type);
- res.removeHeader(Header.Content.Disposition);
- view.notFound(req, res);
- });
+ console.error(err)
+ res.removeHeader(Header.Content.Type)
+ res.removeHeader(Header.Content.Disposition)
+ view.notFound(req, res)
+ })
} else {
- view.notFound(req, res);
+ view.notFound(req, res)
}
}
@@ -196,4 +192,4 @@ export const map = {
post: trackJSON,
blog: photoJSON
}
-};
+}
diff --git a/src/controllers/menu.test.ts b/src/controllers/menu.test.ts
index b45949f7..89f77c8c 100644
--- a/src/controllers/menu.test.ts
+++ b/src/controllers/menu.test.ts
@@ -1,21 +1,21 @@
-import '@toba/test';
-import { MockRequest, MockResponse } from '@toba/test';
-import { menu } from '../controllers/';
-import { Page } from '../views/';
+import '@toba/test'
+import { MockRequest, MockResponse } from '@toba/test'
+import { menu } from '../controllers/'
+import { Page } from '../views/'
-const req = new MockRequest();
-const res = new MockResponse(req);
+const req = new MockRequest()
+const res = new MockResponse(req)
beforeEach(() => {
- res.reset();
- req.reset();
-});
+ res.reset()
+ req.reset()
+})
it('renders mobile menu', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.MobileMenu);
- expect(res.rendered.context).toHaveProperty('blog');
- done();
- };
- menu.mobile(req, res);
-});
+ expect(res).toRenderTemplate(Page.MobileMenu)
+ expect(res.rendered.context).toHaveProperty('blog')
+ done()
+ }
+ menu.mobile(req, res)
+})
diff --git a/src/controllers/menu.ts b/src/controllers/menu.ts
index 0ed6ae42..a0835a2b 100644
--- a/src/controllers/menu.ts
+++ b/src/controllers/menu.ts
@@ -1,9 +1,9 @@
-import { blog } from '@trailimage/models';
-import { Request, Response } from 'express';
-import { Page, Layout, view } from '../views/';
+import { blog } from '@trailimage/models'
+import { Request, Response } from 'express'
+import { Page, Layout, view } from '../views/'
export function mobile(_req: Request, res: Response) {
- view.send(res, Page.MobileMenu, { blog, layout: Layout.None });
+ view.send(res, Page.MobileMenu, { blog, layout: Layout.None })
}
-export const menu = { mobile };
+export const menu = { mobile }
diff --git a/src/controllers/photo.test.ts b/src/controllers/photo.test.ts
index 52422f7f..d5891c2a 100644
--- a/src/controllers/photo.test.ts
+++ b/src/controllers/photo.test.ts
@@ -1,74 +1,74 @@
-import '@toba/test';
-import { MockRequest, MockResponse } from '@toba/test';
-import { alphabet } from '@toba/node-tools';
-import { RouteParam } from '../routes';
-import { Page } from '../views/';
-import { photo } from './';
-import { normalizeTag } from './photo';
-import { loadMockData } from '../.test-data';
-import { config } from '../config';
+import '@toba/test'
+import { MockRequest, MockResponse } from '@toba/test'
+import { alphabet } from '@toba/node-tools'
+import { RouteParam } from '../routes'
+import { Page } from '../views/'
+import { photo } from './'
+import { normalizeTag } from './photo'
+import { loadMockData } from '../.test-data'
+import { config } from '../config'
-const req = new MockRequest();
-const res = new MockResponse(req);
+const req = new MockRequest()
+const res = new MockResponse(req)
beforeAll(async done => {
- await loadMockData();
- console.debug = console.log = jest.fn();
- done();
-});
+ await loadMockData()
+ console.debug = console.log = jest.fn()
+ done()
+})
beforeEach(() => {
- res.reset();
- req.reset();
-});
+ res.reset()
+ req.reset()
+})
test('normalizes photo tags', () => {
- config.photoTagChanges['old-slug'] = 'new-slug';
- expect(normalizeTag('Camel-Case')).toBe('camel-case');
- expect(normalizeTag('old-slug')).toBe('new-slug');
- expect(normalizeTag('')).toBeNull();
-});
+ config.photoTagChanges['old-slug'] = 'new-slug'
+ expect(normalizeTag('Camel-Case')).toBe('camel-case')
+ expect(normalizeTag('old-slug')).toBe('new-slug')
+ expect(normalizeTag('')).toBeNull()
+})
test('loads all photo tags', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.PhotoTag);
- const context = res.rendered.context;
- expect(context).toHaveProperty('alphabet', alphabet);
- expect(context).toHaveAllProperties('tags', 'selected');
- expect(context!.tags).toHaveAllProperties('a', 'b', 'c');
- expect(context!.tags['c']).toHaveProperty('cactus', 'Cactus');
- done();
- };
- photo.tags(req, res);
-});
+ expect(res).toRenderTemplate(Page.PhotoTag)
+ const context = res.rendered.context
+ expect(context).toHaveProperty('alphabet', alphabet)
+ expect(context).toHaveAllProperties('tags', 'selected')
+ expect(context!.tags).toHaveAllProperties('a', 'b', 'c')
+ expect(context!.tags['c']).toHaveProperty('cactus', 'Cactus')
+ done()
+ }
+ photo.tags(req, res)
+})
test('shows all photos with tag', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.PhotoSearch);
- const context = res.rendered.context;
- expect(context).toHaveProperty('photos');
- expect(context!.photos).toBeInstanceOf(Array);
- expect(context!.photos).toHaveLength(19);
- done();
- };
- req.params[RouteParam.PhotoTag] = 'horse';
- photo.withTag(req, res);
-});
+ expect(res).toRenderTemplate(Page.PhotoSearch)
+ const context = res.rendered.context
+ expect(context).toHaveProperty('photos')
+ expect(context!.photos).toBeInstanceOf(Array)
+ expect(context!.photos).toHaveLength(19)
+ done()
+ }
+ req.params[RouteParam.PhotoTag] = 'horse'
+ photo.withTag(req, res)
+})
test('loads EXIF', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.EXIF);
- const context = res.rendered.context;
- expect(context).toHaveProperty('EXIF');
+ expect(res).toRenderTemplate(Page.EXIF)
+ const context = res.rendered.context
+ expect(context).toHaveProperty('EXIF')
expect(context!.EXIF).toHaveAllProperties(
'ISO',
'artist',
'lens',
'model'
- );
- expect(context!.EXIF).toHaveProperty('sanitized', true);
- done();
- };
- req.params[RouteParam.PhotoID] = '8458410907';
- photo.exif(req, res);
-});
+ )
+ expect(context!.EXIF).toHaveProperty('sanitized', true)
+ done()
+ }
+ req.params[RouteParam.PhotoID] = '8458410907'
+ photo.exif(req, res)
+})
diff --git a/src/controllers/photo.ts b/src/controllers/photo.ts
index 8115458f..273c1900 100644
--- a/src/controllers/photo.ts
+++ b/src/controllers/photo.ts
@@ -1,62 +1,60 @@
-import { alphabet, is, sayNumber } from '@toba/node-tools';
-import { blog } from '@trailimage/models';
-import { Request, Response } from 'express';
-import { config } from '../config';
-import { RouteParam } from '../routes';
-import { Layout, Page, view } from '../views/';
+import { alphabet, is, sayNumber } from '@toba/node-tools'
+import { blog } from '@trailimage/models'
+import { Request, Response } from 'express'
+import { config } from '../config'
+import { RouteParam } from '../routes'
+import { Layout, Page, view } from '../views/'
/**
* Render HTML table of EXIF values for given photo.
*/
function exif(req: Request, res: Response) {
- const photoID = req.params[RouteParam.PhotoID];
+ const photoID = req.params[RouteParam.PhotoID]
blog
.getEXIF(photoID)
.then(exif => {
res.render(Page.EXIF, {
EXIF: exif,
layout: Layout.None
- });
+ })
})
.catch(err => {
- console.error(err, { photoID });
- view.notFound(req, res);
- });
+ console.error(err, { photoID })
+ view.notFound(req, res)
+ })
}
/**
* Photos with tag rendered in response to click on label in photo tags page.
*/
function withTag(req: Request, res: Response) {
- const slug = tagParam(req);
+ const slug = tagParam(req)
- if (slug === null) {
- return view.notFound(req, res);
- }
+ if (slug === null) return view.notFound(req, res)
blog
.getPhotosWithTags(slug)
.then(photos => {
if (photos === null || photos.length == 0) {
- view.notFound(req, res);
+ view.notFound(req, res)
} else {
- const tag = blog.tags.get(slug);
+ const tag = blog.tags.get(slug)
const title = `${sayNumber(
photos.length
- )} “${tag}” Image${photos.length != 1 ? 's' : ''}`;
+ )} “${tag}” Image${photos.length != 1 ? 's' : ''}`
res.render(Page.PhotoSearch, {
photos,
config,
title,
layout: Layout.None
- });
+ })
}
})
.catch(err => {
- view.notFound(req, res);
- console.error(err, { photoTag: slug });
- });
+ view.notFound(req, res)
+ console.error(err, { photoTag: slug })
+ })
}
/**
@@ -66,32 +64,32 @@ function withTag(req: Request, res: Response) {
const tagParam = (req: Request): string | null =>
is.defined(req.params, RouteParam.PhotoTag)
? normalizeTag(decodeURIComponent(req.params[RouteParam.PhotoTag]))
- : null;
+ : null
function tags(req: Request, res: Response) {
- let slug = tagParam(req);
- const list = blog.tags;
- const keys = Array.from(list.keys());
- const tags: { [key: string]: { [key: string]: string } } = {};
+ let slug = tagParam(req)
+ const list = blog.tags
+ const keys = Array.from(list.keys())
+ const tags: { [key: string]: { [key: string]: string } } = {}
if (is.empty(slug)) {
// select a random tag
- slug = keys[Math.floor(Math.random() * keys.length + 1)];
+ slug = keys[Math.floor(Math.random() * keys.length + 1)]
}
// group tags by first letter (character)
for (const c of alphabet) {
- tags[c] = {};
+ tags[c] = {}
}
for (const [key, value] of list.entries()) {
// key is sometimes a number
const c = key
.toString()
.substr(0, 1)
- .toLowerCase();
+ .toLowerCase()
if (alphabet.indexOf(c) >= 0) {
// ignore tags that don't start with a letter of the alphabet
- tags[c][key] = value;
+ tags[c][key] = value
}
}
@@ -101,7 +99,7 @@ function tags(req: Request, res: Response) {
alphabet,
title: keys.length + ' Photo Tags',
config
- });
+ })
}
/**
@@ -110,13 +108,13 @@ function tags(req: Request, res: Response) {
*/
export function normalizeTag(slug: string): string | null {
if (is.empty(slug)) {
- return null;
+ return null
} else {
- slug = slug.toLowerCase();
+ slug = slug.toLowerCase()
}
return is.defined(config.photoTagChanges, slug)
? config.photoTagChanges[slug]
- : slug;
+ : slug
}
-export const photo = { withTag, tags, exif };
+export const photo = { withTag, tags, exif }
diff --git a/src/controllers/post.test.ts b/src/controllers/post.test.ts
index 57127d5e..d4a0713d 100644
--- a/src/controllers/post.test.ts
+++ b/src/controllers/post.test.ts
@@ -1,83 +1,81 @@
-import '@toba/test';
-import { MockRequest, MockResponse } from '@toba/test';
-import { config } from '../config';
-import { RouteParam } from '../routes';
-import { Page, Layout } from '../views/';
-import { post } from './';
-import { loadMockData } from '../.test-data';
+import '@toba/test'
+import { MockRequest, MockResponse } from '@toba/test'
+import { config } from '../config'
+import { RouteParam } from '../routes'
+import { Page, Layout } from '../views/'
+import { post } from './'
+import { loadMockData } from '../.test-data'
-const req = new MockRequest();
-const res = new MockResponse(req);
+const req = new MockRequest()
+const res = new MockResponse(req)
beforeAll(async done => {
- await loadMockData();
- console.debug = console.log = jest.fn();
- done();
-});
+ await loadMockData()
+ console.debug = console.log = jest.fn()
+ done()
+})
-beforeEach(() => {
- res.reset();
-});
+beforeEach(() => res.reset())
test('shows latest', done => {
- res.endOnRender = false;
+ res.endOnRender = false
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.Post);
- const context = res.rendered.context;
- expect(context).toHaveProperty('slug', 'stanley-lake-snow-hike');
- expect(context!.layout).toBe(Layout.Main);
- done();
- };
- post.latest(req, res);
-});
+ expect(res).toRenderTemplate(Page.Post)
+ const context = res.rendered.context
+ expect(context).toHaveProperty('slug', 'stanley-lake-snow-hike')
+ expect(context!.layout).toBe(Layout.Main)
+ done()
+ }
+ post.latest(req, res)
+})
test('forwards to correct URL from Flickr set ID', done => {
res.onEnd = () => {
- expect(res).toRedirectTo('/ruminations');
- done();
- };
- expect(config.providers.post).toBeDefined();
- expect(config.providers.post!.featureSets).toBeDefined();
- req.params[RouteParam.PostID] = config.providers.post!.featureSets![0].id;
- post.withID(req, res);
-});
+ expect(res).toRedirectTo('/ruminations')
+ done()
+ }
+ expect(config.providers.post).toBeDefined()
+ expect(config.providers.post!.featureSets).toBeDefined()
+ req.params[RouteParam.PostID] = config.providers.post!.featureSets![0].id
+ post.withID(req, res)
+})
test('redirects to post containing photo', done => {
res.onEnd = () => {
- expect(res).toRedirectTo('/ruminations#8458410907');
- done();
- };
- req.params[RouteParam.PhotoID] = '8458410907';
- post.withPhoto(req, res);
-});
+ expect(res).toRedirectTo('/ruminations#8458410907')
+ done()
+ }
+ req.params[RouteParam.PhotoID] = '8458410907'
+ post.withPhoto(req, res)
+})
test('shows post with slug', done => {
- res.endOnRender = false;
+ res.endOnRender = false
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.Post);
- const context = res.rendered.context;
- expect(context).toHaveProperty('title', 'Kuna Cave Fails to Impress');
- expect(context).toHaveProperty('post');
- expect(context!.post).toHaveProperty('id', '72157668896453295');
- expect(context!.post).toHaveProperty('isPartial', false);
- done();
- };
- req.params[RouteParam.PostKey] = 'kuna-cave-fails-to-impress';
- post.withKey(req, res);
-});
+ expect(res).toRenderTemplate(Page.Post)
+ const context = res.rendered.context
+ expect(context).toHaveProperty('title', 'Kuna Cave Fails to Impress')
+ expect(context).toHaveProperty('post')
+ expect(context!.post).toHaveProperty('id', '72157668896453295')
+ expect(context!.post).toHaveProperty('isPartial', false)
+ done()
+ }
+ req.params[RouteParam.PostKey] = 'kuna-cave-fails-to-impress'
+ post.withKey(req, res)
+})
test('shows post in series', done => {
- res.endOnRender = false;
+ res.endOnRender = false
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.Post);
- const context = res.rendered.context;
- expect(context).toHaveProperty('title', 'Brother Ride 2015');
- expect(context).toHaveProperty('post');
- expect(context!.post).toHaveProperty('id', '72157658679070399');
- expect(context!.post).toHaveProperty('isPartial', true);
- done();
- };
- req.params[RouteParam.SeriesKey] = 'brother-ride-2015';
- req.params[RouteParam.PartKey] = 'huckleberry-lookout';
- post.inSeries(req, res);
-});
+ expect(res).toRenderTemplate(Page.Post)
+ const context = res.rendered.context
+ expect(context).toHaveProperty('title', 'Brother Ride 2015')
+ expect(context).toHaveProperty('post')
+ expect(context!.post).toHaveProperty('id', '72157658679070399')
+ expect(context!.post).toHaveProperty('isPartial', true)
+ done()
+ }
+ req.params[RouteParam.SeriesKey] = 'brother-ride-2015'
+ req.params[RouteParam.PartKey] = 'huckleberry-lookout'
+ post.inSeries(req, res)
+})
diff --git a/src/controllers/post.ts b/src/controllers/post.ts
index 412df362..19b09c67 100644
--- a/src/controllers/post.ts
+++ b/src/controllers/post.ts
@@ -1,8 +1,8 @@
-import { HttpStatus, is } from '@toba/node-tools';
-import { blog, Post } from '@trailimage/models';
-import { Request, Response } from 'express';
-import { RouteParam } from '../routes';
-import { Page, Layout, view } from '../views/';
+import { HttpStatus, is } from '@toba/node-tools'
+import { blog, Post } from '@trailimage/models'
+import { Request, Response } from 'express'
+import { RouteParam } from '../routes'
+import { Page, Layout, view } from '../views/'
function send(
req: Request,
@@ -11,10 +11,9 @@ function send(
viewName: string = Page.Post
) {
view.send(res, key, render => {
- const p = blog.postWithKey(key);
- if (!is.value(p)) {
- return view.notFound(req, res);
- }
+ const p = blog.postWithKey(key)
+ if (!is.value(p)) return view.notFound(req, res)
+
p.ensureLoaded()
.then(() => {
render(viewName, {
@@ -25,10 +24,10 @@ function send(
jsonLD: p.jsonLD(),
description: p.longDescription,
slug: key
- });
+ })
})
- .catch(err => view.internalError(res, err));
- });
+ .catch(err => view.internalError(res, err))
+ })
}
/**
@@ -39,26 +38,26 @@ function inSeries(req: Request, res: Response) {
req,
res,
req.params[RouteParam.SeriesKey] + '/' + req.params[RouteParam.PartKey]
- );
+ )
}
/**
* Render post with matching key.
*/
function withKey(req: Request, res: Response) {
- send(req, res, req.params[RouteParam.PostKey]);
+ send(req, res, req.params[RouteParam.PostKey])
}
/**
* Render post with matching provider (e.g. Flickr) ID. Redirect to normal URL.
*/
function withID(req: Request, res: Response) {
- const post = blog.postWithID(req.params[RouteParam.PostID]);
+ const post = blog.postWithID(req.params[RouteParam.PostID])
if (is.value(post)) {
- res.redirect(HttpStatus.PermanentRedirect, '/' + post.key);
+ res.redirect(HttpStatus.PermanentRedirect, '/' + post.key)
} else {
- view.notFound(req, res);
+ view.notFound(req, res)
}
}
@@ -66,7 +65,7 @@ function withID(req: Request, res: Response) {
* Render post that contains photo with given ID.
*/
function withPhoto(req: Request, res: Response) {
- const photoID = req.params[RouteParam.PhotoID];
+ const photoID = req.params[RouteParam.PhotoID]
blog
.postWithPhoto(photoID)
@@ -75,15 +74,15 @@ function withPhoto(req: Request, res: Response) {
res.redirect(
HttpStatus.PermanentRedirect,
`/${post.key}#${photoID}`
- );
+ )
} else {
- view.notFound(req, res);
+ view.notFound(req, res)
}
})
.catch(err => {
- console.error(err, { photoID });
- view.notFound(req, res);
- });
+ console.error(err, { photoID })
+ view.notFound(req, res)
+ })
}
/**
@@ -91,7 +90,7 @@ function withPhoto(req: Request, res: Response) {
* first.
*/
function latest(req: Request, res: Response) {
- send(req, res, blog.posts[0].key!);
+ send(req, res, blog.posts[0].key!)
}
-export const post = { latest, withID, withKey, withPhoto, inSeries };
+export const post = { latest, withID, withKey, withPhoto, inSeries }
diff --git a/src/controllers/rss.test.ts b/src/controllers/rss.test.ts
index 5f263ecb..6bf60cab 100644
--- a/src/controllers/rss.test.ts
+++ b/src/controllers/rss.test.ts
@@ -1,22 +1,22 @@
-import '@toba/test';
-import { Header, MimeType } from '@toba/node-tools';
-import { postFeed } from './rss';
-import { MockRequest, MockResponse } from '@toba/test';
-import { loadMockData } from '../.test-data';
+import '@toba/test'
+import { Header, MimeType } from '@toba/node-tools'
+import { postFeed } from './rss'
+import { MockRequest, MockResponse } from '@toba/test'
+import { loadMockData } from '../.test-data'
-const req = new MockRequest();
-const res = new MockResponse(req);
+const req = new MockRequest()
+const res = new MockResponse(req)
beforeAll(async done => {
- await loadMockData();
- done();
-});
+ await loadMockData()
+ done()
+})
test('generates valid Atom XML', done => {
res.onEnd = () => {
- expect(res.headers).toHaveKeyValue(Header.Content.Type, MimeType.XML);
- expect(res.content).toMatchSnapshot();
- done();
- };
- postFeed(req, res);
-});
+ expect(res.headers).toHaveKeyValue(Header.Content.Type, MimeType.XML)
+ expect(res.content).toMatchSnapshot()
+ done()
+ }
+ postFeed(req, res)
+})
diff --git a/src/controllers/rss.ts b/src/controllers/rss.ts
index f4ee4cbd..ca075d60 100644
--- a/src/controllers/rss.ts
+++ b/src/controllers/rss.ts
@@ -1,33 +1,33 @@
-import { MimeType, Header } from '@toba/node-tools';
-import { blog } from '@trailimage/models';
-import { Request, Response } from 'express';
-import { render } from '@toba/feed';
-import { view } from '../views/';
+import { MimeType, Header } from '@toba/node-tools'
+import { blog } from '@trailimage/models'
+import { Request, Response } from 'express'
+import { render } from '@toba/feed'
+import { view } from '../views/'
-const MAX_RSS_RETRIES = 10;
+const MAX_RSS_RETRIES = 10
-let rssRetries = 0;
+let rssRetries = 0
export function postFeed(req: Request, res: Response) {
if (!blog.postInfoLoaded) {
if (rssRetries >= MAX_RSS_RETRIES) {
- console.error(`Unable to load blog after ${MAX_RSS_RETRIES} tries`);
- view.notFound(req, res);
+ console.error(`Unable to load blog after ${MAX_RSS_RETRIES} tries`)
+ view.notFound(req, res)
// reset tries so page can be refreshed
- rssRetries = 0;
+ rssRetries = 0
} else {
- rssRetries++;
+ rssRetries++
console.error(
`Blog posts not ready when creating RSS feed — attempt ${rssRetries}`
- );
+ )
setTimeout(() => {
- postFeed(req, res);
- }, 1000);
+ postFeed(req, res)
+ }, 1000)
}
- return;
+ return
}
- res.set(Header.Content.Type, MimeType.XML);
- res.write(render(blog));
- res.end();
+ res.set(Header.Content.Type, MimeType.XML)
+ res.write(render(blog))
+ res.end()
}
diff --git a/src/controllers/static.test.ts b/src/controllers/static.test.ts
index 5c4b8c47..6f3d580e 100644
--- a/src/controllers/static.test.ts
+++ b/src/controllers/static.test.ts
@@ -1,47 +1,47 @@
-import '@toba/test';
-import { Header, MimeType, addCharSet } from '@toba/node-tools';
-import { MockRequest, MockResponse } from '@toba/test';
-import { Page } from '../views/index';
-import { staticPage } from './static';
-import { config } from '../config';
+import '@toba/test'
+import { Header, MimeType, addCharSet } from '@toba/node-tools'
+import { MockRequest, MockResponse } from '@toba/test'
+import { Page } from '../views/index'
+import { staticPage } from './static'
+import { config } from '../config'
-const req = new MockRequest();
-const res = new MockResponse(req);
-const wasCached = config.cache.views;
+const req = new MockRequest()
+const res = new MockResponse(req)
+const wasCached = config.cache.views
beforeAll(() => {
- config.cache.views = false;
-});
+ config.cache.views = false
+})
afterAll(() => {
- config.cache.views = wasCached;
-});
+ config.cache.views = wasCached
+})
beforeEach(() => {
- res.reset();
- req.reset();
- res.endOnRender = true;
-});
+ res.reset()
+ req.reset()
+ res.endOnRender = true
+})
test('renders sitemap', done => {
res.onEnd = () => {
- expect(res).toRenderTemplate(Page.Sitemap);
- const context = res.rendered.context;
- expect(context).toHaveAllProperties('posts', 'categories', 'tags');
+ expect(res).toRenderTemplate(Page.Sitemap)
+ const context = res.rendered.context
+ expect(context).toHaveAllProperties('posts', 'categories', 'tags')
expect(res.headers).toHaveKeyValue(
Header.Content.Type,
addCharSet(MimeType.XML)
- );
- done();
- };
- res.endOnRender = false;
- staticPage.siteMap(req, res);
-});
+ )
+ done()
+ }
+ res.endOnRender = false
+ staticPage.siteMap(req, res)
+})
test('redirects to issues page', done => {
res.onEnd = () => {
- expect(res).toRedirectTo('https://issues.' + config.domain);
- done();
- };
- staticPage.issues(req, res);
-});
+ expect(res).toRedirectTo('https://issues.' + config.domain)
+ done()
+ }
+ staticPage.issues(req, res)
+})
diff --git a/src/controllers/static.ts b/src/controllers/static.ts
index 0977d998..21962fbe 100644
--- a/src/controllers/static.ts
+++ b/src/controllers/static.ts
@@ -1,14 +1,14 @@
-import { HttpStatus, MimeType } from '@toba/node-tools';
-import { blog, owner } from '@trailimage/models';
-import { Request, Response } from 'express';
-import { config } from '../config';
-import { Page, Layout, view } from '../views/';
+import { HttpStatus, MimeType } from '@toba/node-tools'
+import { blog, owner } from '@trailimage/models'
+import { Request, Response } from 'express'
+import { config } from '../config'
+import { Page, Layout, view } from '../views/'
function about(_req: Request, res: Response) {
view.send(res, Page.About, {
title: 'About ' + config.site.title,
jsonLD: owner()
- });
+ })
}
/**
@@ -25,14 +25,11 @@ function siteMap(_req: Request, res: Response) {
tags: blog.tags
},
MimeType.XML
- );
+ )
}
function issues(_req: Request, res: Response) {
- res.redirect(
- HttpStatus.PermanentRedirect,
- 'https://issues.' + config.domain
- );
+ res.redirect(HttpStatus.PermanentRedirect, 'https://issues.' + config.domain)
}
-export const staticPage = { issues, about, siteMap };
+export const staticPage = { issues, about, siteMap }