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
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1 +1,253 @@
|
||||
// placeholder — replaced in Task 2
|
||||
/* ── 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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user