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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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