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:
2026-06-22 23:19:19 +02:00
parent 75aaa61640
commit 5923ba431a
13 changed files with 274 additions and 2 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+253 -1
View File
@@ -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();
});