Files
menu/public/app.js
T
tanshu 9cb4021229 Serve the customer-facing menus
Landing page, Section pages and archived sets rendered per request from the
store, with the approved design: hand-written CSS, self-hosted Archivo, a
copper pour line in the Section rail, an admin-only Thumbnail and a text page
index. Media is keyed by Page id so URLs survive publishing, and a Draft's
media needs a session. Includes the scrypt admin auth and the login throttle
the media rule needs.
2026-09-22 21:18:49 +05:30

121 lines
3.8 KiB
JavaScript

/* Progressive enhancement only: the pour line, the page counter, and
tap-to-zoom. Without this file every menu is still readable and every
Page opens as a plain link. */
(function () {
'use strict';
var reducesMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var pages = document.querySelector('[data-pages]');
var fill = document.querySelector('[data-pour]');
var counter = document.querySelector('[data-counter]');
var counterCurrent = counter && counter.querySelector('[data-counter-current]');
var figures = pages ? Array.prototype.slice.call(pages.querySelectorAll('[data-page]')) : [];
var currentPage = 1;
function updatePour() {
if (!pages || !fill) return;
var rect = pages.getBoundingClientRect();
var travel = rect.height - window.innerHeight;
if (travel <= 0) {
fill.style.width = '100%';
return;
}
var progress = Math.min(Math.max(-rect.top, 0), travel) / travel;
fill.style.width = (progress * 100).toFixed(1) + '%';
}
function updateCounter() {
if (!counter || figures.length === 0) return;
var middle = window.innerHeight / 2;
var best = 1;
var bestDistance = Infinity;
figures.forEach(function (figure, index) {
var rect = figure.getBoundingClientRect();
var distance = Math.abs(rect.top + rect.height / 2 - middle);
if (distance < bestDistance) {
bestDistance = distance;
best = index + 1;
}
});
if (best !== currentPage) {
currentPage = best;
if (counterCurrent) counterCurrent.textContent = String(best);
}
}
function onScroll() {
updatePour();
updateCounter();
}
if (counter) counter.hidden = false;
if (figures.length > 0) {
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
onScroll();
}
if (counter) {
counter.addEventListener('click', function () {
var fold = document.querySelector('.fold--index');
if (!fold) return;
fold.open = true;
fold.scrollIntoView({ behavior: reducesMotion ? 'auto' : 'smooth', block: 'center' });
var first = fold.querySelector('a');
if (first) first.focus({ preventScroll: true });
});
}
var overlay = null;
function closeOverlay() {
if (!overlay) return;
overlay.remove();
overlay = null;
document.body.style.overflow = '';
document.removeEventListener('keydown', onKeydown);
}
function onKeydown(event) {
if (event.key === 'Escape') closeOverlay();
}
function openOverlay(href, alt) {
overlay = document.createElement('div');
overlay.className = 'zoom';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', alt);
var close = document.createElement('button');
close.type = 'button';
close.className = 'zoom__close';
close.textContent = 'Close';
var image = document.createElement('img');
image.src = href;
image.alt = alt;
overlay.appendChild(close);
overlay.appendChild(image);
overlay.addEventListener('click', function (event) {
if (event.target === overlay || event.target === close) closeOverlay();
});
document.addEventListener('keydown', onKeydown);
document.body.appendChild(overlay);
document.body.style.overflow = 'hidden';
close.focus();
}
Array.prototype.forEach.call(document.querySelectorAll('[data-zoom]'), function (link) {
link.addEventListener('click', function (event) {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
return;
}
var image = link.querySelector('img');
event.preventDefault();
openOverlay(link.getAttribute('href'), (image && image.alt) || 'Menu page');
});
});
})();