/* ── 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(); }); }); } /* ── Trip stats / cycling computation (trip + home-active) ─── config: { gpxUrls: [], gpsPoints: [[lat,lng],...], hasGpx: bool } No-op if #stat-distance is absent (page rendered no stats panel). No-GPX fallback: if gpsPoints.length < 2, write '—' and return (no '~0'). */ function initTripStats(config) { var distEl = document.getElementById('stat-distance'); if (!distEl) return; var gpxUrls = config.gpxUrls || []; var gpsPoints = config.gpsPoints || []; if (config.hasGpx) { MapUtils.parseGpxFiles(gpxUrls, function (result) { distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—'; function setText(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; } setText('cyc-distance', result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—'); setText('cyc-ele-gain', !isNaN(result.eleGain) ? Math.round(result.eleGain) : '—'); setText('cyc-ele-loss', !isNaN(result.eleLoss) ? Math.round(result.eleLoss) : '—'); setText('cyc-highest', !isNaN(result.highest) ? Math.round(result.highest) : '—'); setText('cyc-lowest', !isNaN(result.lowest) ? Math.round(result.lowest) : '—'); setText('cyc-moving-time', result.movingTime || '—'); setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—'); }); } else { if (gpsPoints.length < 2) { distEl.textContent = '—'; return; } var total = 0; for (var i = 1; i < gpsPoints.length; i++) { total += MapUtils.haversineKm( parseFloat(gpsPoints[i-1][0]), parseFloat(gpsPoints[i-1][1]), parseFloat(gpsPoints[i][0]), parseFloat(gpsPoints[i][1]) ); } distEl.textContent = '~' + Math.round(total).toLocaleString(); } } window.initTripStats = initTripStats; /* ── 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(); });