// ── The Roasted Bean — Editorial Motion ──────────────────────────────────── // // Motion decisions (Jakub primary · Jhey secondary · Emil selective): // • Hero: one orchestrated entrance on load — opacity + translateY + blur // 700ms, 120–160ms stagger (Jakub's enter recipe) // • Menu cards: staggered cascade via CSS --card-index (Jhey moment) // 500ms per card, 40ms delay increment // • Scroll reveals: editorial subtlety — 8px translateY, no blur (avoid slop) // • Buttons: scale(0.97) on :active (Emil's tactile feedback) // • Nav / category filter / newsletter: no JS animation (Emil's frequency gate) // — these use CSS transitions only // • About image: scale(1.03) on hover (CSS transition, no JS) // • prefers-reduced-motion: all animations collapsed to instant (0.01ms) // • No parallax, no custom cursor, no preloader, no springs document.addEventListener('DOMContentLoaded', async () => { triggerHeroEntrance(); await loadCafeData(); setupNavigation(); setupNewsletter(); initScrollReveal(); initMenuCardReveal(); setupSmoothScroll(); }); // ── Hero Entrance ─────────────────────────────────────────────────────────── // One orchestrated moment — rare interaction (first visit only) // Jakub's enter recipe: opacity + translateY + blur materialization function triggerHeroEntrance() { const hero = document.querySelector('.hero'); if (!hero) return; // Small delay so the browser has painted the initial state requestAnimationFrame(() => { requestAnimationFrame(() => { hero.classList.add('hero-entered'); }); }); } // ── Load Data ─────────────────────────────────────────────────────────────── let menuItems = []; let menuCategories = []; async function loadCafeData() { try { const res = await fetch('/api/menu'); const data = await res.json(); menuItems = data.items; menuCategories = data.categories; populateHero(data.info); buildCategories(data.categories); buildMenu(data.items, data.categories); buildVisit(data.info); // Re-run menu card observer after DOM update initMenuCardReveal(); } catch (err) { console.error('Error loading cafe data:', err); } } function populateHero(info) { if (info.name) { document.getElementById('cafeName').textContent = info.name; document.title = `${info.name} — Specialty Coffee Roasters`; } if (info.address) { const el = document.getElementById('cafeAddressSmall'); if (el) el.textContent = info.address; } if (info.phone) { const el = document.getElementById('cafePhoneSmall'); if (el) el.textContent = info.phone; } } // ── Categories ────────────────────────────────────────────────────────────── // Rebuilds the grid from filtered data — no display:none, no empty cells let activeCategory = 'all'; function buildCategories(categories) { const container = document.getElementById('categoryFilters'); if (!container) return; container.innerHTML = categories .map(cat => ` `).join(''); container.addEventListener('click', (e) => { const btn = e.target.closest('.cat-btn'); if (!btn) return; document.querySelectorAll('.cat-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); activeCategory = btn.dataset.category; rebuildGrid(); }); } function rebuildGrid() { const grid = document.getElementById('menuGrid'); if (!grid) return; const filtered = activeCategory === 'all' ? menuItems : menuItems.filter(item => item.category === activeCategory); if (filtered.length === 0) { grid.innerHTML = '
'; return; } buildMenu(filtered, menuCategories); initMenuCardReveal(); } function filterCards() { // Kept for API compatibility — rebuildGrid handles filtering now rebuildGrid(); } // ── Menu Cards ────────────────────────────────────────────────────────────── function buildMenu(items, categories) { const grid = document.getElementById('menuGrid'); if (!grid) return; const catMeta = {}; categories.forEach(c => { catMeta[c.id] = { name: c.name, icon: c.icon }; }); grid.innerHTML = items.map((item, i) => { const cat = catMeta[item.category] || { name: item.category, icon: '/img/icons/default.svg' }; const tagClass = !item.available ? ' soldout' : ''; const tagText = !item.available ? 'Sold Out' : cat.name; // Thumbnail: SVG fallback always present; image overlays on top const thumbSvg = svgPlaceholder(item.category); const thumbImg = (item.image && item.image.startsWith('http')) ? `