import { renderHeader, renderFooter, api, esc, formatDate, fromIso, toIso, formatTime } from './common.js'; renderHeader('home', { overlay: true }); renderFooter(); const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)'); // ---------- Scrollen: Flagge zoomt heran ---------- // Eine einzige rAF-Schleife, die nur läuft, wenn gescrollt wurde. const stage = document.getElementById('hero-stage'); const hero = stage.parentElement; let ticking = false; function frame() { ticking = false; if (reduceMotion.matches) { stage.style.setProperty('--p', 0); return; } const range = Math.max(1, hero.offsetHeight - innerHeight); const p = Math.min(1, Math.max(0, scrollY / range)); stage.style.setProperty('--p', p.toFixed(4)); } function requestFrame() { if (!ticking) { ticking = true; requestAnimationFrame(frame); } } addEventListener('scroll', requestFrame, { passive: true }); addEventListener('resize', requestFrame); reduceMotion.addEventListener('change', requestFrame); frame(); // ---------- Abschnitte gleiten beim ersten Sichtbarwerden herein ---------- // Nur was beim Laden unterhalb des Bildschirms liegt, wird erst versteckt – sichtbarer Inhalt bleibt sichtbar. if (!reduceMotion.matches && 'IntersectionObserver' in window) { const observer = new IntersectionObserver((entries) => { for (const entry of entries) { if (!entry.isIntersecting) continue; entry.target.classList.add('reveal-in'); entry.target.classList.remove('reveal-pending'); observer.unobserve(entry.target); } }, { rootMargin: '0px 0px -10% 0px' }); for (const el of document.querySelectorAll('.reveal')) { if (el.getBoundingClientRect().top > innerHeight) { el.classList.add('reveal-pending'); observer.observe(el); } } } // ---------- Nächster Einsatz + nächste Termine ---------- function daysUntil(iso) { return Math.round((fromIso(iso) - fromIso(toIso(new Date()))) / 86400000); } function renderNext(events) { const next = events.find((e) => e.isGame) || events[0]; const title = document.getElementById('next-title'); const when = document.getElementById('next-when'); const box = document.getElementById('next-countdown'); if (!next) { title.textContent = 'Noch nichts geplant'; when.textContent = 'Der nächste Spieltag steht noch nicht im Kalender.'; return; } title.textContent = next.title; when.textContent = [formatDate(next.date, true), formatTime(next)].filter(Boolean).join(' · '); const days = daysUntil(next.date); const label = next.attendeeCount === 1 ? '1 Zusage' : `${next.attendeeCount} Zusagen`; box.innerHTML = days <= 0 ? `Heute${label}` : `${days} ${days === 1 ? 'Tag' : 'Tage'}bis zum Spiel · ${label}`; box.hidden = false; } const box = document.getElementById('upcoming'); try { const events = await api('GET', '/upcoming'); renderNext(events); box.innerHTML = events.length ? `
Noch keine Termine eingetragen.
'; } catch { box.innerHTML = 'Termine konnten nicht geladen werden.
'; document.getElementById('next-title').textContent = 'Termine nicht erreichbar'; } // ---------- Pile of Shame: wer dieses Jahr noch nicht spielen war ---------- const shame = document.getElementById('shame'); try { const { year, entries } = await api('GET', '/stats'); const names = entries.filter((s) => s.total === 0).map((s) => s.name).sort((a, b) => a.localeCompare(b, 'de')); shame.innerHTML = names.length ? `Waren ${year} noch kein einziges Mal spielen:
Leer, alle waren ${year} schon spielen.
`; } catch { shame.innerHTML = 'Konnte nicht geladen werden.
'; } // ---------- Patches (neueste zuerst) ---------- try { const patches = await api('GET', '/patches'); if (patches.length) { document.getElementById('patch-rail').innerHTML = patches.slice(0, 12).map((p) => `