Compare commits

...
14 Commits
Author SHA1 Message Date
m038andClaude Sonnet 4.6 ed25907545 fix: DOMContentLoaded wrapper + correct asset registration in map templates
- feed-map.html.twig: remove addCss/addJs calls (too late for <head>); merge
  two <script> blocks into one wrapped in DOMContentLoaded
- map.html.twig: move {% block map_assets %} above {% block content %} so
  map.css reaches <head>; add DOMContentLoaded around map init
- dailies.html.twig: add {% block map_assets %} override so map.css and
  map.js are registered for the feed-map partial

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-24 00:16:57 +02:00
m038 476c2c17ef fix: add whitespace-control to date-range macro declaration 2026-06-24 00:09:56 +02:00
m038andClaude Sonnet 4.6 24ffa42499 refactor: extract date range macro; fix stories.html.twig asset registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-24 00:07:26 +02:00
m038andClaude Sonnet 4.6 04bcab2d55 refactor: extract stats and cycling panels to Twig macros
Move stats computation and both panel HTML divs out of trip.html.twig
into dedicated macros/stats.html.twig and macros/cycling.html.twig.
trip.html.twig imports both at the top of {% block content %} and
calls them via stats_m.stats_panel() and cycling_m.cycling_panel().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 23:57:54 +02:00
m038andClaude Sonnet 4.6 1458c24aa3 fix: remove duplicate inline sort handlers from dailies and stories templates
main.js now registers initSortButton for feed-sort-toggle on all pages;
inline IIFEs caused double-fire reversing to original order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 09:36:49 +02:00
m038andClaude Sonnet 4.6 ede5ce1914 fix: move map_assets block to <head> so map.css registers before assets.css() renders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 09:20:33 +02:00
m038andClaude Sonnet 4.6 0b73cf5048 fix: wrap map init in DOMContentLoaded — map.js loads at bottom via Asset Manager
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 09:06:33 +02:00
m038andClaude Sonnet 4.6 10354a0760 refactor: remove CDN tags from feed-map, map, story templates
- feed-map.html.twig: removed MapLibre/toGeoJSON/maplibre-utils CDN tags; added {% do assets.addCss/addJs %} calls directly in partial
- map.html.twig: removed MapLibre/toGeoJSON/maplibre-utils CDN tags; added {% block map_assets %} with Asset Manager registrations
- story.html.twig: removed Scrollama CDN tag (now bundled in main.js as window.scrollama)

All map init and script logic remains unchanged. CDN requests eliminated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:39:22 +02:00
m038 632a3028fa refactor: trip.html.twig — remove CDN tags, deduplicate JS, use MapUtils.parseGpxFiles
- Remove PhotoSwipe CSS CDN link (now in css-compiled/main.css via Asset Manager)
- Remove MapLibre GL, toGeoJSON, maplibre-utils CDN/local script tags
- Add {% block map_assets %} to register map.css and map.js via Asset Manager
- Remove filter bar IIFE (now initFilterBar in main.js)
- Remove sort toggle IIFE (now initSortButton in main.js)
- Remove local haversineKm and parseGpxFiles functions (now in MapUtils via map.js)
- Update parseGpxFiles call to MapUtils.parseGpxFiles
- Update haversineKm call to MapUtils.haversineKm in Mode B stats
- Remove makePanelToggle helper + calls (now initPanelToggles in main.js)
- Remove back-to-top DOMContentLoaded block (now initBackToTop in main.js)
- Remove PhotoSwipe <script type="module"> block (now initPhotoSwipe + initPhotoStrip in main.js)
2026-06-22 23:35:58 +02:00
m038 53d873b4c7 feat: register assets via Asset Manager, remove Google Fonts, remove inline photo strip script 2026-06-22 23:31:30 +02:00
m038andClaude Sonnet 4.6 a640294e9b build: add map.js bundle — MapLibre GL, toGeoJSON, MapUtils
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:29:06 +02:00
m038 f8a6b9eb96 feat: add parseGpxFiles and export haversineKm from MapUtils 2026-06-22 23:22:48 +02:00
m038andClaude Sonnet 4.6 5923ba431a build: add main.js bundle — fonts, PhotoSwipe, UI utilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:19:19 +02:00
m038andClaude Sonnet 4.6 75aaa61640 build: add esbuild scaffolding and Docker build-assets target
package.json with esbuild ^0.21 and npm dependencies (maplibre-gl,
photoswipe, scrollama, fontsource). Placeholder src files and
compiled outputs committed. node_modules gitignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:11:25 +02:00
30 changed files with 2144 additions and 500 deletions
+1
View File
@@ -7,3 +7,4 @@
/pages/02.post/*ui-test*/
/config/plugins/git-sync.yaml
/config/security.yaml
/themes/intotheeast/node_modules/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+89
View File
@@ -330,9 +330,98 @@
});
}
/*
* Parse one or more GPX files and compute aggregate cycling statistics.
* urls: array of GPX file URL strings
* callback: called once with { distance, eleGain, eleLoss, highest, lowest, movingTime, avgSpeed }
* or { error: 'no files' } if urls is empty.
*
* distance/eleGain/eleLoss in raw units (km / metres).
* movingTime: "H:MM" string. avgSpeed: km/h number.
*/
function parseGpxFiles(urls, callback) {
var pending = urls.length;
var fileResults = new Array(urls.length);
if (pending === 0) { callback({ error: 'no files' }); return; }
urls.forEach(function (url, idx) {
fetch(url)
.then(function (r) { return r.text(); })
.then(function (text) {
var xml = new DOMParser().parseFromString(text, 'text/xml');
var pts = [];
xml.querySelectorAll('trkpt').forEach(function (pt) {
var eleEl = pt.querySelector('ele');
var timeEl = pt.querySelector('time');
pts.push({
lat: parseFloat(pt.getAttribute('lat')),
lon: parseFloat(pt.getAttribute('lon')),
ele: eleEl ? parseFloat(eleEl.textContent) : NaN,
time: timeEl ? timeEl.textContent : null
});
});
fileResults[idx] = pts;
if (--pending === 0) computeAndCallback();
})
.catch(function (err) {
console.warn('GPX load failed:', url, err);
fileResults[idx] = [];
if (--pending === 0) computeAndCallback();
});
});
function computeAndCallback() {
var totalDistance = 0, totalEleGain = 0, totalEleLoss = 0;
var globalHighest = NaN, globalLowest = NaN, totalMovingTime = 0;
fileResults.forEach(function (pts) {
if (!pts || pts.length < 2) return;
/* Include first point of each file in elevation range */
if (!isNaN(pts[0].ele)) {
if (isNaN(globalHighest) || pts[0].ele > globalHighest) globalHighest = pts[0].ele;
if (isNaN(globalLowest) || pts[0].ele < globalLowest) globalLowest = pts[0].ele;
}
for (var i = 1; i < pts.length; i++) {
var p0 = pts[i - 1], p1 = pts[i];
totalDistance += haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
var dEle = p1.ele - p0.ele;
if (dEle > 0) totalEleGain += dEle;
if (dEle < 0) totalEleLoss += (-dEle);
if (isNaN(globalHighest) || p1.ele > globalHighest) globalHighest = p1.ele;
if (isNaN(globalLowest) || p1.ele < globalLowest) globalLowest = p1.ele;
}
if (p0.time && p1.time) {
var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000;
if (dtHrs > 0 && (haversineKm(p0.lat, p0.lon, p1.lat, p1.lon) / dtHrs) >= 1) {
totalMovingTime += dtHrs;
}
}
}
});
var avgSpeed = totalMovingTime > 0 ? totalDistance / totalMovingTime : 0;
var movHours = Math.floor(totalMovingTime);
var movMins = Math.round((totalMovingTime - movHours) * 60);
if (movMins === 60) { movHours++; movMins = 0; }
callback({
distance: totalDistance,
eleGain: totalEleGain,
eleLoss: totalEleLoss,
highest: globalHighest,
lowest: globalLowest,
movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
avgSpeed: avgSpeed
});
}
}
global.MapUtils = {
MAP_STYLE: MAP_STYLE,
ACCENT: ACCENT,
haversineKm: haversineKm,
parseGpxFiles: parseGpxFiles,
addJourneyLine: addJourneyLine,
addJourneySegments: addJourneySegments,
buildJourneySegments: buildJourneySegments,
+253
View File
@@ -0,0 +1,253 @@
/* ── Fonts ───────────────────────────────────────────────── */
import '@fontsource-variable/dm-sans';
import '@fontsource/dm-serif-display/400.css';
import '@fontsource/dm-serif-display/400-italic.css';
/* ── PhotoSwipe ──────────────────────────────────────────── */
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
import 'photoswipe/style.css';
/* ── Scrollama (used by story.html.twig inline script) ────── */
import scrollama from 'scrollama';
window.scrollama = scrollama;
/* ── Photo strip: prev/next buttons + scroll-based dot sync ─ */
function initPhotoStrip() {
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
strip.setAttribute('role', 'region');
strip.setAttribute('aria-label', 'Photo strip');
strip.setAttribute('tabindex', '0');
var slideCount = parseInt(strip.dataset.slides, 10) || 1;
var dots = strip.nextElementSibling;
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
strip.addEventListener('scroll', function () {
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
}, { passive: true });
if (slideCount < 2) return;
var prev = document.createElement('button');
prev.className = 'strip-prev';
prev.setAttribute('aria-label', 'Previous photo');
prev.textContent = '';
prev.addEventListener('click', function () {
strip.scrollBy({ left: -strip.offsetWidth, behavior: 'smooth' });
});
var next = document.createElement('button');
next.className = 'strip-next';
next.setAttribute('aria-label', 'Next photo');
next.textContent = '';
next.addEventListener('click', function () {
strip.scrollBy({ left: strip.offsetWidth, behavior: 'smooth' });
});
var controls = document.createElement('div');
controls.className = 'strip-controls';
controls.appendChild(prev);
controls.appendChild(next);
var wrap = strip.closest('.journal-photo-wrap');
(wrap || dots).insertAdjacentElement('afterend', controls);
});
}
/* ── PhotoSwipe lightbox + IntersectionObserver dot sync ──── */
function initPhotoSwipe() {
if (!document.querySelector('.pswp-gallery')) return;
var lightbox = new PhotoSwipeLightbox({
gallery: '.pswp-gallery',
children: 'a.journal-photo-slide',
pswpModule: PhotoSwipe
});
lightbox.on('afterOpen', function () {
var pswp = lightbox.pswp;
var keyDir = 0;
var clearTimer = null;
function onKey(e) {
if (e.key === 'ArrowRight') keyDir = 1;
else if (e.key === 'ArrowLeft') keyDir = -1;
else keyDir = 0;
}
document.addEventListener('keydown', onKey, true);
pswp.on('change', function () {
if (!keyDir) return;
var dir = keyDir;
keyDir = 0;
var el = pswp.currSlide && pswp.currSlide.container;
if (!el) return;
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
el.offsetWidth; /* force reflow */
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
clearTimeout(clearTimer);
clearTimer = setTimeout(function () {
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
}, 400);
});
pswp.on('close', function () {
document.removeEventListener('keydown', onKey, true);
clearTimeout(clearTimer);
});
});
lightbox.init();
/* Per-strip: IntersectionObserver dot sync + expand button */
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
var strip = wrap.querySelector('.journal-photo-strip');
if (!strip) return;
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
var expandBtn = wrap.querySelector('.journal-photo-expand');
var article = wrap.closest('article');
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
var visibleIdx = 0;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
visibleIdx = slides.indexOf(e.target);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
});
}, { root: strip, threshold: 0.5 });
slides.forEach(function (s) { io.observe(s); });
if (expandBtn && slides.length) {
expandBtn.addEventListener('click', function () {
slides[visibleIdx].dispatchEvent(
new MouseEvent('click', { bubbles: true, cancelable: true })
);
});
}
});
}
/* Sort button
btnId: element id of the sort toggle button
containerSel: CSS selector for the list container
itemSel: CSS selector for sortable items within container
withLabel: true = button shows "↑ Oldest first" / "↓ Newest first"
false = button shows "↑" / "↓" only
*/
function initSortButton(btnId, containerSel, itemSel, withLabel) {
var btn = document.getElementById(btnId);
if (!btn) return;
var container = document.querySelector(containerSel);
if (!container) return;
var sentinel = container.querySelector('#feed-filter-empty');
var ascending = true;
btn.addEventListener('click', function () {
ascending = !ascending;
var items = Array.from(container.querySelectorAll(itemSel));
items.reverse().forEach(function (el) {
if (sentinel) container.insertBefore(el, sentinel);
else container.appendChild(el);
});
btn.textContent = withLabel
? (ascending ? '↑ Oldest first' : '↓ Newest first')
: (ascending ? '↑' : '↓');
btn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
btn.classList.toggle('is-active', !ascending);
});
}
/* ── Filter bar (trip page: All / Journal / Stories) ─────── */
function initFilterBar() {
var filterBtns = document.querySelectorAll('.trip-filter-btn');
if (!filterBtns.length) return;
var cards = document.querySelectorAll('[data-type]');
var filterEmpty = document.getElementById('feed-filter-empty');
filterBtns.forEach(function (btn) {
btn.addEventListener('click', function () {
filterBtns.forEach(function (b) {
b.classList.remove('is-active');
b.setAttribute('aria-pressed', 'false');
});
btn.classList.add('is-active');
btn.setAttribute('aria-pressed', 'true');
var filter = btn.getAttribute('data-filter');
var visible = 0;
cards.forEach(function (card) {
var show = filter === 'all' || card.getAttribute('data-type') === filter;
card.style.display = show ? '' : 'none';
if (show) visible++;
});
if (filterEmpty) {
if (visible === 0) {
filterEmpty.textContent = filter === 'story'
? 'No stories yet for this trip.'
: 'No entries yet.';
filterEmpty.style.display = '';
} else {
filterEmpty.style.display = 'none';
}
}
});
});
}
/* ── Back to top ─────────────────────────────────────────── */
function initBackToTop(btnId) {
var btn = document.getElementById(btnId);
if (!btn) return;
var threshold = window.innerHeight * 0.8;
var shown = false;
btn.addEventListener('click', function () {
history.pushState(null, '', window.location.pathname + window.location.search);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
window.addEventListener('scroll', function () {
var shouldShow = window.scrollY > threshold;
if (shouldShow !== shown) {
shown = shouldShow;
btn.classList.toggle('is-visible', shown);
}
}, { passive: true });
}
/* ── Panel toggles (trip stats / cycling panels) ─────────── */
function initPanelToggles() {
document.querySelectorAll('.trip-panel-toggle').forEach(function (toggle) {
var blockId = toggle.getAttribute('aria-controls');
var block = blockId ? document.getElementById(blockId) : null;
if (!block) return;
toggle.addEventListener('click', function () {
var isOpen = block.classList.contains('is-open');
block.classList.toggle('is-open', !isOpen);
toggle.classList.toggle('is-active', !isOpen);
toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
});
document.querySelectorAll('.trip-panel-close').forEach(function (btn) {
var toggleBtn = document.getElementById(btn.getAttribute('data-toggle'));
if (toggleBtn) btn.addEventListener('click', function () { toggleBtn.click(); });
});
}
/* ── Boot ────────────────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', function () {
initPhotoStrip();
initPhotoSwipe();
initFilterBar();
/* Sort buttons — each call is silent if its button/container isn't on this page */
initSortButton('trip-sort-toggle', '.feed', '[data-type]', false);
initSortButton('feed-sort-toggle', '.feed', '[data-type]', true);
initSortButton('feed-sort-toggle', '.stories-grid', '.story-card', true);
initBackToTop('story-totop');
initBackToTop('trip-totop');
initPanelToggles();
});
+9
View File
@@ -0,0 +1,9 @@
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import toGeoJSON from '@mapbox/togeojson';
/* maplibre-utils.js attaches MapUtils to window as a side effect */
import '../maplibre-utils.js';
window.maplibregl = maplibregl;
window.toGeoJSON = toGeoJSON;
+964
View File
@@ -0,0 +1,964 @@
{
"name": "app",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@fontsource-variable/dm-sans": "latest",
"@fontsource/dm-serif-display": "latest",
"@mapbox/togeojson": "^0.16.2",
"maplibre-gl": "^4",
"photoswipe": "^5",
"scrollama": "^3"
},
"devDependencies": {
"esbuild": "^0.21"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@fontsource-variable/dm-sans": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource-variable/dm-sans/-/dm-sans-5.2.8.tgz",
"integrity": "sha512-AxkvMTvNWgfrmlyjiV05vlHYJa+nRQCf1EfvIrQAPBpFJW0O9VTz7oAFr9S3lvbWdmnFoBk7yFqQL86u64nl2g==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/dm-serif-display": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/dm-serif-display/-/dm-serif-display-5.2.8.tgz",
"integrity": "sha512-GYSDSlGU6vyhv9a5MwaiVNf9HCuSVpK8hEFRyG4NNDHCDeHiX7YHDAcWsaoLKKcfXLgWG9YkBkk9T3SxM4rAjQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@mapbox/geojson-rewind": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
"license": "ISC",
"dependencies": {
"get-stream": "^6.0.1",
"minimist": "^1.2.6"
},
"bin": {
"geojson-rewind": "geojson-rewind"
}
},
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@mapbox/point-geometry": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
"license": "ISC"
},
"node_modules/@mapbox/tiny-sdf": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
"integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/togeojson": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@mapbox/togeojson/-/togeojson-0.16.2.tgz",
"integrity": "sha512-DcApudmw4g/grOrpM5gYPZfts6Kr8litBESN6n/27sDsjR2f+iJhx4BA0J2B+XrLlnHyJkKztYApe6oCUZpzFA==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"concat-stream": "~2.0.0",
"minimist": "1.2.8"
},
"bin": {
"togeojson": "togeojson"
}
},
"node_modules/@mapbox/unitbezier": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/vector-tile": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/point-geometry": "~0.1.0"
}
},
"node_modules/@mapbox/whoots-js": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
"license": "ISC",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "20.4.0",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz",
"integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==",
"license": "ISC",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
"@mapbox/unitbezier": "^0.0.1",
"json-stringify-pretty-compact": "^4.0.0",
"minimist": "^1.2.8",
"quickselect": "^2.0.0",
"rw": "^1.3.3",
"tinyqueue": "^3.0.0"
},
"bin": {
"gl-style-format": "dist/gl-style-format.mjs",
"gl-style-migrate": "dist/gl-style-migrate.mjs",
"gl-style-validate": "dist/gl-style-validate.mjs"
}
},
"node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
"license": "ISC"
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/geojson-vt": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/mapbox__point-geometry": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
"license": "MIT"
},
"node_modules/@types/mapbox__vector-tile": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*",
"@types/mapbox__point-geometry": "*",
"@types/pbf": "*"
}
},
"node_modules/@types/pbf": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
"license": "MIT"
},
"node_modules/@types/supercluster": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/earcut": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
"license": "ISC"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/geojson-vt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.3.tgz",
"integrity": "sha512-jR1MwkLaZGa8Zftct9ZFruyWFrdl9ZyD2OliXNy9Qq5bBPeg5wHVpBQF9p5GjnicSDQqvBVpysxTPKmWdsfWMA==",
"license": "ISC"
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gl-matrix": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
"license": "MIT"
},
"node_modules/global-prefix": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
"integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
"license": "MIT",
"dependencies": {
"ini": "^4.1.3",
"kind-of": "^6.0.3",
"which": "^4.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/isexe": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
"integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
"license": "MIT"
},
"node_modules/kdbush": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
"license": "ISC"
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/maplibre-gl": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz",
"integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==",
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^2.0.6",
"@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"@maplibre/maplibre-gl-style-spec": "^20.3.1",
"@types/geojson": "^7946.0.14",
"@types/geojson-vt": "3.2.5",
"@types/mapbox__point-geometry": "^0.1.4",
"@types/mapbox__vector-tile": "^1.3.4",
"@types/pbf": "^3.0.5",
"@types/supercluster": "^7.1.3",
"earcut": "^3.0.0",
"geojson-vt": "^4.0.2",
"gl-matrix": "^3.4.3",
"global-prefix": "^4.0.0",
"kdbush": "^4.0.2",
"murmurhash-js": "^1.0.0",
"pbf": "^3.3.0",
"potpack": "^2.0.0",
"quickselect": "^3.0.0",
"supercluster": "^8.0.1",
"tinyqueue": "^3.0.0",
"vt-pbf": "^3.1.3"
},
"engines": {
"node": ">=16.14.0",
"npm": ">=8.1.0"
},
"funding": {
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/murmurhash-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
"license": "MIT"
},
"node_modules/pbf": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
"license": "BSD-3-Clause",
"dependencies": {
"ieee754": "^1.1.12",
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
}
},
"node_modules/photoswipe": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz",
"integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==",
"license": "MIT",
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/potpack": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
"license": "ISC"
},
"node_modules/protocol-buffers-schema": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
"license": "MIT"
},
"node_modules/quickselect": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
"license": "ISC"
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/resolve-protobuf-schema": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
"license": "MIT",
"dependencies": {
"protocol-buffers-schema": "^3.3.1"
}
},
"node_modules/rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
"license": "BSD-3-Clause"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/scrollama": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/scrollama/-/scrollama-3.2.0.tgz",
"integrity": "sha512-PIPwB1kYBnbw/ezvPBJa5dCN5qEwokfpAkI3BmpZWAwcVID4nDf1qH6WV16A2fQaJmsKx0un5S/zhxN+PQeKDQ==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/supercluster": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
"license": "ISC",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/tinyqueue": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
"license": "ISC"
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
"license": "MIT",
"dependencies": {
"@mapbox/point-geometry": "0.1.0",
"@mapbox/vector-tile": "^1.3.1",
"pbf": "^3.2.1"
}
},
"node_modules/which": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"license": "ISC",
"dependencies": {
"isexe": "^3.1.1"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^16.13.0 || >=18.0.0"
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"private": true,
"scripts": {
"build": "esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; }"
},
"dependencies": {
"@fontsource-variable/dm-sans": "latest",
"@fontsource/dm-serif-display": "latest",
"@mapbox/togeojson": "^0.16.2",
"maplibre-gl": "^4",
"photoswipe": "^5",
"scrollama": "^3"
},
"devDependencies": {
"esbuild": "^0.21"
}
}
+5 -17
View File
@@ -1,5 +1,10 @@
{% extends 'default.html.twig' %}
{% block map_assets %}
{% do assets.addCss('theme://css-compiled/map.css') %}
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
{% endblock %}
{% block content %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
{% set journal_entries = page.collection() %}
@@ -126,21 +131,4 @@ document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
}
});
</script>
<script>
(function() {
var sortBtn = document.getElementById('feed-sort-toggle');
if (!sortBtn) return;
var feed = document.querySelector('.feed');
var ascending = true;
sortBtn.addEventListener('click', function() {
ascending = !ascending;
var entries = Array.from(feed.querySelectorAll('[data-type]'));
entries.reverse().forEach(function(el) { feed.appendChild(el); });
sortBtn.textContent = ascending ? '↑ Oldest first' : '↓ Newest first';
sortBtn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
sortBtn.classList.toggle('is-active', !ascending);
});
})();
</script>
{% endblock %}
@@ -0,0 +1,41 @@
{% macro cycling_panel() %}
<div id="trip-cycling-block" class="trip-cycling-block">
<div class="trip-panel-inner">
<div class="trip-cycling-header">
<span class="trip-cycling-icon">🚴</span>
<span class="trip-cycling-title">Cycling Stats</span>
</div>
<div class="trip-cycling-grid">
<div class="stat-block">
<span class="stat-value" id="cyc-distance">—</span>
<span class="stat-label">km distance</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-gain">—</span>
<span class="stat-label">m ↑ gain</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-loss">—</span>
<span class="stat-label">m ↓ loss</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-highest">—</span>
<span class="stat-label">m highest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-lowest">—</span>
<span class="stat-label">m lowest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-moving-time">—</span>
<span class="stat-label">moving time</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-avg-speed">—</span>
<span class="stat-label">km/h avg speed</span>
</div>
</div>
<button class="trip-panel-close" data-toggle="trip-cycling-toggle">↑ Close cycling</button>
</div>
</div>
{% endmacro %}
@@ -0,0 +1,19 @@
{%- macro format_date_range(start_date, end_date) -%}
{%- if end_date is not empty and end_date|date('Y-m-d') != start_date|date('Y-m-d') -%}
{%- set sd = start_date|date('d') -%}
{%- set sm = start_date|date('M') -%}
{%- set sy = start_date|date('Y') -%}
{%- set ed = end_date|date('d') -%}
{%- set em = end_date|date('M') -%}
{%- set ey = end_date|date('Y') -%}
{%- if sy == ey and sm == em -%}
{{- sd ~ ' ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey -}}
{%- elseif sy == ey -%}
{{- sd ~ ' ' ~ sm ~ ' ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey -}}
{%- else -%}
{{- sd ~ ' ' ~ sm ~ ' ' ~ sy ~ ' ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey -}}
{%- endif -%}
{%- else -%}
{{- start_date|date('d M Y') -}}
{%- endif %}
{%- endmacro -%}
@@ -0,0 +1,93 @@
{% macro stats_panel(journal_entries, page, journal_count, has_gpx) %}
{% set days_on_road = 0 %}
{% if page.header.date_end is not empty %}
{% set start_ts = page.header.date_start|date('U') %}
{% set end_ts = page.header.date_end|date('U') %}
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
{% else %}
{% set first_ts = null %}
{% for entry in journal_entries %}
{% set ts = entry.date|date('U') %}
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
{% endfor %}
{% if first_ts is not null %}
{% set diff_seconds = "now"|date('U') - first_ts %}
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
{% endif %}
{% endif %}
{% set seen_lower = [] %}
{% set country_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_country is not empty %}
{% set lower = entry.header.location_country|trim|lower %}
{% if lower not in seen_lower %}
{% set seen_lower = seen_lower|merge([lower]) %}
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set seen_city_lower = [] %}
{% set city_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_city is not empty %}
{% set lower = entry.header.location_city|trim|lower %}
{% if lower not in seen_city_lower %}
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set temp_min = null %}
{% set temp_max = null %}
{% for entry in journal_entries %}
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
{% set t = entry.header.weather_temp_c %}
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
{% endif %}
{% endfor %}
<div id="trip-stats-block" class="trip-stats-block">
<div class="trip-panel-inner">
<div class="trip-stats-grid">
<div class="stat-block">
<span class="stat-value">{{ days_on_road }}</span>
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ journal_count }}</span>
<span class="stat-label">{{ journal_count == 1 ? 'entry' : 'entries' }} posted</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ country_display|length }}</span>
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ city_display|length }}</span>
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value" id="stat-distance">—</span>
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
</div>
<div class="stat-block">
{% if temp_min is not null %}
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
{% else %}
<span class="stat-value">—</span>
{% endif %}
<span class="stat-label">°C range</span>
</div>
</div>
{% if country_display|length > 0 %}
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
{% endif %}
<p class="trip-stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
<button class="trip-panel-close" data-toggle="trip-stats-toggle">↑ Close stats</button>
</div>
</div>
{% endmacro %}
+7 -5
View File
@@ -1,5 +1,10 @@
{% extends 'partials/base.html.twig' %}
{% block map_assets %}
{% do assets.addCss('theme://css-compiled/map.css') %}
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
{% endblock %}
{% block content %}
{% set trip_page = page.parent() %}
{% set tracker_page = grav.pages.find(page.parent().route ~ '/dailies') %}
@@ -36,17 +41,13 @@
<div class="map-container" id="trip-map"></div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
<script>
var ENTRIES = {{ map_entries|json_encode|raw }};
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
var USE_GPX = {{ trip_page.header.use_gpx ?? true ? 'true' : 'false' }};
var AUTOCONNECT = "{{ trip_page.header.autoconnect ?? 'on' }}";
document.addEventListener('DOMContentLoaded', function() {
var map = new maplibregl.Map({
container: 'trip-map',
style: MapUtils.MAP_STYLE,
@@ -96,5 +97,6 @@ map.on('load', function () {
/* ── GPX tracks + journey segments ─────────────────────────── */
MapUtils.renderGpxJourney(map, USE_GPX ? GPX_URLS : [], ENTRIES, 'gpx', 'journey', { connectMode: AUTOCONNECT });
});
}); // DOMContentLoaded
</script>
{% endblock %}
@@ -4,11 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url('theme://css/tokens.css') }}">
<link rel="stylesheet" href="{{ url('theme://css/style.css') }}">
{% do assets.addCss('theme://css/tokens.css') %}
{% do assets.addCss('theme://css/style.css') %}
{% do assets.addCss('theme://css-compiled/main.css') %}
{% do assets.addJs('theme://js/main.js', {group: 'bottom'}) %}
{% block map_assets %}{% endblock %}
{{ assets.css()|raw }}
{{ assets.js()|raw }}
</head>
@@ -27,49 +27,5 @@
{% block content %}{% endblock %}
</main>
{{ assets.js('bottom')|raw }}
<script>
(function () {
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
strip.setAttribute('role', 'region');
strip.setAttribute('aria-label', 'Photo strip');
strip.setAttribute('tabindex', '0');
var slideCount = parseInt(strip.dataset.slides, 10) || 1;
var dots = strip.nextElementSibling;
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
strip.addEventListener('scroll', function () {
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
}, { passive: true });
if (slideCount < 2) return;
var prev = document.createElement('button');
prev.className = 'strip-prev';
prev.setAttribute('aria-label', 'Previous photo');
prev.textContent = '';
prev.addEventListener('click', function () {
strip.scrollBy({ left: -strip.offsetWidth, behavior: 'smooth' });
});
var next = document.createElement('button');
next.className = 'strip-next';
next.setAttribute('aria-label', 'Next photo');
next.textContent = '';
next.addEventListener('click', function () {
strip.scrollBy({ left: strip.offsetWidth, behavior: 'smooth' });
});
var controls = document.createElement('div');
controls.className = 'strip-controls';
controls.appendChild(prev);
controls.appendChild(next);
var wrap = strip.closest('.journal-photo-wrap');
(wrap || dots).insertAdjacentElement('afterend', controls);
});
})();
</script>
</body>
</html>
@@ -9,6 +9,8 @@
card_prefix — string: prefix for scroll-to card IDs ('entry-' or 'story-')
trip_page — Grav page: trip page for autoconnect setting (used when show_journey is true)
show_journey — bool: whether to draw the route connector line between markers
Callers must register map assets via {% block map_assets %} in their own template.
#}
{% if map_entries|length > 0 %}
<div class="feed-map-wrap">
@@ -25,17 +27,17 @@
{% endif %}
</div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
<script>
{% set js_suffix = map_id|replace({'-': '_'})|upper %}
var MAP_ENTRIES_{{ js_suffix }} = {{ map_entries|json_encode|raw }};
{% if show_journey %}
{% set _ac = trip_page ? (trip_page.header.autoconnect ?? 'on') : 'on' %}
{% endif %}
var MAP_ENTRIES_{{ js_suffix }} = {{ map_entries|json_encode|raw }};
{% if show_journey %}
var AUTOCONNECT_{{ js_suffix }} = "{{ _ac == 'intelligent_gpx' ? 'on' : _ac }}";
{% endif %}
document.addEventListener('DOMContentLoaded', function() {
var {{ map_var }} = new maplibregl.Map({
container: '{{ map_id }}',
style: MapUtils.MAP_STYLE,
@@ -100,8 +102,7 @@ var {{ map_var }} = new maplibregl.Map({
MapUtils.addJourneySegments({{ map_var }}, segments, '{{ map_id }}-journey');
{% endif %}
});
</script>
<script>
(function() {
var fsBtn = document.getElementById('{{ map_id }}-fullscreen');
var mapWrap = document.querySelector('.feed-map-wrap');
@@ -113,5 +114,6 @@ var {{ map_var }} = new maplibregl.Map({
setTimeout(function() { typeof {{ map_var }} !== 'undefined' && {{ map_var }}.resize(); }, 50);
});
})();
}); // DOMContentLoaded
</script>
{% endif %}
+7 -22
View File
@@ -1,6 +1,12 @@
{% extends 'partials/base.html.twig' %}
{% block map_assets %}
{% do assets.addCss('theme://css-compiled/map.css') %}
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
{% endblock %}
{% block content %}
{% import 'macros/date-range.html.twig' as dr_m %}
{% set stories = page.children.published().order('date', 'asc') %}
{# Collect stories that have coordinates for the mini-map #}
@@ -46,10 +52,7 @@
{% set hero = story.media[story.header.hero_image] %}
{% endif %}
{% set date_str = story.date|date('d M Y') %}
{% if story.header.end_date %}
{% set date_str = story.date|date('d M') ~ '' ~ story.header.end_date|date('d M Y') %}
{% endif %}
{% set date_str = dr_m.format_date_range(story.date, story.header.end_date ?? null) %}
<a class="story-card" id="story-{{ story.slug }}" href="{{ story.url }}">
{% if hero %}
@@ -74,22 +77,4 @@
<p class="stories-empty">No stories yet — check back soon.</p>
{% endif %}
</div>
<script>
(function() {
var sortBtn = document.getElementById('feed-sort-toggle');
if (!sortBtn) return;
var grid = document.querySelector('.stories-grid');
if (!grid) return;
var ascending = true;
sortBtn.addEventListener('click', function() {
ascending = !ascending;
var cards = Array.from(grid.querySelectorAll('.story-card'));
cards.reverse().forEach(function(el) { grid.appendChild(el); });
sortBtn.textContent = ascending ? '↑ Oldest first' : '↓ Newest first';
sortBtn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
sortBtn.classList.toggle('is-active', !ascending);
});
})();
</script>
{% endblock %}
+2 -17
View File
@@ -6,6 +6,7 @@
{% endblock %}
{% block content %}
{% import 'macros/date-range.html.twig' as dr_m %}
{% set hero_url = null %}
{% if page.header.hero_image %}
{% if page.media[page.header.hero_image] is defined %}
@@ -16,22 +17,7 @@
{% endif %}
{% endif %}
{% set date_str = page.date|date('d M Y') %}
{% if page.header.end_date and page.header.end_date|date('Y-m-d') != page.date|date('Y-m-d') %}
{% set sd = page.date|date('d') %}
{% set sm = page.date|date('M') %}
{% set sy = page.date|date('Y') %}
{% set ed = page.header.end_date|date('d') %}
{% set em = page.header.end_date|date('M') %}
{% set ey = page.header.end_date|date('Y') %}
{% if sy == ey and sm == em %}
{% set date_str = sd ~ ' ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
{% elseif sy == ey %}
{% set date_str = sd ~ ' ' ~ sm ~ ' ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
{% else %}
{% set date_str = sd ~ ' ' ~ sm ~ ' ' ~ sy ~ ' ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
{% endif %}
{% endif %}
{% set date_str = dr_m.format_date_range(page.date, page.header.end_date ?? null) %}
{% set location = page.header.location_name ?? '' %}
@@ -69,7 +55,6 @@
<button class="story-totop" id="story-totop" aria-label="Back to top">↑ Top</button>
<script src="https://cdn.jsdelivr.net/npm/scrollama@3/build/scrollama.min.js"></script>
<script>
/* ── Hero scroll effect ──────────────────────────────────── */
(function () {
+13 -384
View File
@@ -1,7 +1,12 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
{% import 'macros/stats.html.twig' as stats_m %}
{% import 'macros/cycling.html.twig' as cycling_m %}
{% block map_assets %}
{% do assets.addCss('theme://css-compiled/map.css') %}
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
{% endblock %}
{% set dailies_page = grav.pages.find(page.route ~ '/dailies') %}
{% set stories_page = grav.pages.find(page.route ~ '/stories') %}
{% set journal_entries = dailies_page ? dailies_page.children.published() : [] %}
@@ -19,59 +24,6 @@
{% set journal_count = journal_entries|length %}
{% set story_count = story_entries|length %}
{# Stats computation #}
{% set days_on_road = 0 %}
{% if page.header.date_end is not empty %}
{% set start_ts = page.header.date_start|date('U') %}
{% set end_ts = page.header.date_end|date('U') %}
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
{% else %}
{% set first_ts = null %}
{% for entry in journal_entries %}
{% set ts = entry.date|date('U') %}
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
{% endfor %}
{% if first_ts is not null %}
{% set diff_seconds = "now"|date('U') - first_ts %}
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
{% endif %}
{% endif %}
{% set seen_lower = [] %}
{% set country_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_country is not empty %}
{% set lower = entry.header.location_country|trim|lower %}
{% if lower not in seen_lower %}
{% set seen_lower = seen_lower|merge([lower]) %}
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set seen_city_lower = [] %}
{% set city_display = [] %}
{% for entry in journal_entries %}
{% if entry.header.location_city is not empty %}
{% set lower = entry.header.location_city|trim|lower %}
{% if lower not in seen_city_lower %}
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
{% endif %}
{% endif %}
{% endfor %}
{% set temp_min = null %}
{% set temp_max = null %}
{% for entry in journal_entries %}
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
{% set t = entry.header.weather_temp_c %}
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
{% endif %}
{% endfor %}
{% set gps_points = [] %}
{% for entry in journal_entries %}
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
@@ -144,86 +96,10 @@
</div>
</div>
<div id="trip-stats-block" class="trip-stats-block">
<div class="trip-panel-inner">
<div class="trip-stats-grid">
<div class="stat-block">
<span class="stat-value">{{ days_on_road }}</span>
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ journal_count }}</span>
<span class="stat-label">{{ journal_count == 1 ? 'entry' : 'entries' }} posted</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ country_display|length }}</span>
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value">{{ city_display|length }}</span>
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
</div>
<div class="stat-block">
<span class="stat-value" id="stat-distance">—</span>
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
</div>
<div class="stat-block">
{% if temp_min is not null %}
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
{% else %}
<span class="stat-value">—</span>
{% endif %}
<span class="stat-label">°C range</span>
</div>
</div>
{% if country_display|length > 0 %}
<p class="trip-stats-countries">{{ country_display|join(' · ') }}</p>
{% endif %}
<p class="trip-stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
<button class="trip-panel-close" data-toggle="trip-stats-toggle">↑ Close stats</button>
</div>
</div>
{{ stats_m.stats_panel(journal_entries, page, journal_count, has_gpx) }}
{% if has_gpx %}
<div id="trip-cycling-block" class="trip-cycling-block">
<div class="trip-panel-inner">
<div class="trip-cycling-header">
<span class="trip-cycling-icon">🚴</span>
<span class="trip-cycling-title">Cycling Stats</span>
</div>
<div class="trip-cycling-grid">
<div class="stat-block">
<span class="stat-value" id="cyc-distance">—</span>
<span class="stat-label">km distance</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-gain">—</span>
<span class="stat-label">m ↑ gain</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-ele-loss">—</span>
<span class="stat-label">m ↓ loss</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-highest">—</span>
<span class="stat-label">m highest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-lowest">—</span>
<span class="stat-label">m lowest</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-moving-time">—</span>
<span class="stat-label">moving time</span>
</div>
<div class="stat-block">
<span class="stat-value" id="cyc-avg-speed">—</span>
<span class="stat-label">km/h avg speed</span>
</div>
</div>
<button class="trip-panel-close" data-toggle="trip-cycling-toggle">↑ Close cycling</button>
</div>
</div>
{{ cycling_m.cycling_panel() }}
{% endif %}
<div class="feed">
@@ -244,16 +120,14 @@
</div>
</div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
<script>
var TRIP_ENTRIES = {{ map_entries|json_encode|raw }};
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
var USE_GPX = {{ page.header.use_gpx ?? true ? 'true' : 'false' }};
var AUTOCONNECT = "{{ page.header.autoconnect ?? 'on' }}";
document.addEventListener('DOMContentLoaded', function() {
var tripMap = new maplibregl.Map({
container: 'trip-map',
style: MapUtils.MAP_STYLE,
@@ -336,164 +210,17 @@ setTimeout(function () { tripMap.resize(); }, 100);
});
})();
(function() {
var filterBtns = document.querySelectorAll('.trip-filter-btn');
var cards = document.querySelectorAll('[data-type]');
var filterEmpty = document.getElementById('feed-filter-empty');
filterBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
filterBtns.forEach(function(b) { b.classList.remove('is-active'); b.setAttribute('aria-pressed', 'false'); });
btn.classList.add('is-active');
btn.setAttribute('aria-pressed', 'true');
var filter = btn.getAttribute('data-filter');
var visible = 0;
cards.forEach(function(card) {
var show = filter === 'all' || card.getAttribute('data-type') === filter;
card.style.display = show ? '' : 'none';
if (show) visible++;
});
if (filterEmpty) {
if (visible === 0) {
filterEmpty.textContent = filter === 'story'
? 'No stories yet for this trip.'
: 'No entries yet.';
filterEmpty.style.display = '';
} else {
filterEmpty.style.display = 'none';
}
}
});
});
})();
(function() {
var sortBtn = document.getElementById('trip-sort-toggle');
if (!sortBtn) return;
var feed = document.querySelector('.feed');
var emptyMsg = document.getElementById('feed-filter-empty');
var ascending = true;
sortBtn.addEventListener('click', function() {
ascending = !ascending;
var entries = Array.from(feed.querySelectorAll('[data-type]'));
entries.reverse().forEach(function(el) { feed.insertBefore(el, emptyMsg); });
sortBtn.textContent = ascending ? '↑' : '↓';
sortBtn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
sortBtn.classList.toggle('is-active', !ascending);
});
})();
var STATS_GPS = {{ gps_points|json_encode|raw }};
var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
function haversineKm(lat1, lng1, lat2, lng2) {
var R = 6371;
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLng = (lng2 - lng1) * Math.PI / 180;
var a = Math.sin(dLat/2)*Math.sin(dLat/2) +
Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*
Math.sin(dLng/2)*Math.sin(dLng/2);
return R * 2 * Math.asin(Math.sqrt(a));
}
function parseGpxFiles(urls, callback) {
var pending = urls.length;
var fileResults = new Array(urls.length);
if (pending === 0) { callback({ error: 'no files' }); return; }
urls.forEach(function(url, idx) {
fetch(url)
.then(function(r) { return r.text(); })
.then(function(text) {
var xml = new DOMParser().parseFromString(text, 'text/xml');
var trkpts = xml.querySelectorAll('trkpt');
var pts = [];
trkpts.forEach(function(pt) {
var eleEl = pt.querySelector('ele');
var timeEl = pt.querySelector('time');
pts.push({
lat: parseFloat(pt.getAttribute('lat')),
lon: parseFloat(pt.getAttribute('lon')),
ele: eleEl ? parseFloat(eleEl.textContent) : NaN,
time: timeEl ? timeEl.textContent : null
});
});
fileResults[idx] = pts;
pending--;
if (pending === 0) { computeAndCallback(); }
})
.catch(function(err) {
console.warn('GPX load failed:', url, err);
fileResults[idx] = [];
pending--;
if (pending === 0) { computeAndCallback(); }
});
});
function computeAndCallback() {
var totalDistance = 0, totalEleGain = 0, totalEleLoss = 0;
var globalHighest = NaN, globalLowest = NaN, totalMovingTime = 0;
fileResults.forEach(function(pts) {
if (!pts || pts.length < 2) return;
for (var i = 1; i < pts.length; i++) {
var p0 = pts[i-1], p1 = pts[i];
var d = haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
totalDistance += d;
if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
var dEle = p1.ele - p0.ele;
if (dEle > 0) totalEleGain += dEle;
if (dEle < 0) totalEleLoss += (-dEle);
if (isNaN(globalHighest) || p1.ele > globalHighest) globalHighest = p1.ele;
if (isNaN(globalLowest) || p1.ele < globalLowest) globalLowest = p1.ele;
}
if (p0.time && p1.time) {
var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000;
if (dtHrs > 0) {
var speed = d / dtHrs;
if (speed >= 1) totalMovingTime += dtHrs;
}
}
}
// include first point of each file in elevation range
if (pts.length > 0 && !isNaN(pts[0].ele)) {
if (isNaN(globalHighest) || pts[0].ele > globalHighest) globalHighest = pts[0].ele;
if (isNaN(globalLowest) || pts[0].ele < globalLowest) globalLowest = pts[0].ele;
}
});
var avgSpeed = totalMovingTime > 0 ? totalDistance / totalMovingTime : 0;
var movHours = Math.floor(totalMovingTime);
var movMins = Math.round((totalMovingTime - movHours) * 60);
if (movMins === 60) { movHours++; movMins = 0; }
callback({
distance: totalDistance,
eleGain: totalEleGain,
eleLoss: totalEleLoss,
highest: globalHighest,
lowest: globalLowest,
movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
avgSpeed: avgSpeed
});
}
}
(function() {
var distEl = document.getElementById('stat-distance');
if (HAS_GPX) {
parseGpxFiles(GPX_URLS, function(result) {
// Mode A: update distance stat
MapUtils.parseGpxFiles(GPX_URLS, function(result) {
if (distEl) {
distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—';
}
// Populate cycling panel
function setText(id, val) {
var el = document.getElementById(id);
if (el) el.textContent = val;
@@ -507,10 +234,9 @@ function parseGpxFiles(urls, callback) {
setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—');
});
} else {
// Mode B: haversine between entry points
var total = 0;
for (var i = 1; i < STATS_GPS.length; i++) {
total += haversineKm(
total += MapUtils.haversineKm(
parseFloat(STATS_GPS[i-1][0]), parseFloat(STATS_GPS[i-1][1]),
parseFloat(STATS_GPS[i][0]), parseFloat(STATS_GPS[i][1])
);
@@ -520,108 +246,11 @@ function parseGpxFiles(urls, callback) {
}
}
function makePanelToggle(toggleId, blockId) {
var toggle = document.getElementById(toggleId);
var block = document.getElementById(blockId);
if (!toggle || !block) return;
toggle.addEventListener('click', function() {
var isOpen = block.classList.contains('is-open');
block.classList.toggle('is-open', !isOpen);
toggle.classList.toggle('is-active', !isOpen);
toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
}
makePanelToggle('trip-stats-toggle', 'trip-stats-block');
makePanelToggle('trip-cycling-toggle', 'trip-cycling-block');
// Close buttons inside panels (mobile only via CSS)
document.querySelectorAll('.trip-panel-close').forEach(function(btn) {
var toggleBtn = document.getElementById(btn.getAttribute('data-toggle'));
if (toggleBtn) btn.addEventListener('click', function() { toggleBtn.click(); });
});
})();
/* ── Back to top ─────────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', function () {
var btn = document.getElementById('trip-totop');
if (!btn) return;
var threshold = window.innerHeight * 0.8;
var shown = false;
btn.addEventListener('click', function () {
history.pushState(null, '', window.location.pathname + window.location.search);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
window.addEventListener('scroll', function () {
var shouldShow = window.scrollY > threshold;
if (shouldShow !== shown) {
shown = shouldShow;
btn.classList.toggle('is-visible', shown);
}
}, { passive: true });
});
}); // DOMContentLoaded
</script>
<button class="story-totop" id="trip-totop" aria-label="Back to top">↑ Top</button>
<script type="module">
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe-lightbox.esm.min.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '.pswp-gallery',
children: 'a.journal-photo-slide',
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.esm.min.js')
});
lightbox.on('afterOpen', function () {
var pswp = lightbox.pswp;
var keyDir = 0;
var clearTimer = null;
function onKey(e) {
if (e.key === 'ArrowRight') keyDir = 1;
else if (e.key === 'ArrowLeft') keyDir = -1;
else keyDir = 0;
}
document.addEventListener('keydown', onKey, true);
pswp.on('change', function () {
if (!keyDir) return;
var dir = keyDir;
keyDir = 0;
var el = pswp.currSlide && pswp.currSlide.container;
if (!el) return;
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
el.offsetWidth;
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
clearTimeout(clearTimer);
clearTimer = setTimeout(function () { el.classList.remove('pswp-key-from-left', 'pswp-key-from-right'); }, 400);
});
pswp.on('close', function () {
document.removeEventListener('keydown', onKey, true);
clearTimeout(clearTimer);
});
});
lightbox.init();
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
var strip = wrap.querySelector('.journal-photo-strip');
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
var expandBtn = wrap.querySelector('.journal-photo-expand');
var article = wrap.closest('article');
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
var visibleIdx = 0;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
visibleIdx = slides.indexOf(e.target);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
});
}, { root: strip, threshold: 0.5 });
slides.forEach(function (s) { io.observe(s); });
if (expandBtn && slides.length) {
expandBtn.addEventListener('click', function () {
slides[visibleIdx].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
});
}
});
</script>
{% endblock %}