// Reusable components for Midsummer Festival site const { useState, useEffect, useRef } = React; // ----- Icons ----- function Icon({ name, size = 24 }) { const stroke = "currentColor"; const sw = 1.5; const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke, strokeWidth: sw, strokeLinecap: "round", strokeLinejoin: "round" }; switch (name) { case "train": return ; case "bus": return ; case "shuttle": return ; case "car": return ; case "plane": return ; case "bike": return ; case "arrow": return ; case "plus": return ; case "play": return ; case "chevronLeft": return ; case "chevronRight": return ; default: return null; } } // ----- Sticky Nav ----- function Nav({ lang, setLang, copy, activeSection }) { const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 60); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); const items = [ { id: 'info', label: copy.nav.info }, { id: 'tickets', label: copy.nav.tickets }, { id: 'impressions', label: copy.nav.impressions }, { id: 'travel', label: copy.nav.travel }, { id: 'faq', label: copy.nav.faq }, ]; return ( ); } // ----- Countdown ----- function Countdown({ labels }) { const target = new Date('2026-06-20T12:00:00+02:00').getTime(); const calc = () => { const diff = Math.max(0, target - Date.now()); const d = Math.floor(diff / 86400000); const h = Math.floor((diff % 86400000) / 3600000); const m = Math.floor((diff % 3600000) / 60000); const s = Math.floor((diff % 60000) / 1000); return { d, h, m, s }; }; const [t, setT] = useState(calc()); useEffect(() => { const i = setInterval(() => setT(calc()), 1000); return () => clearInterval(i); }, []); const pad = (n) => String(n).padStart(2, '0'); return (
{pad(t.d)}{labels[0]}
{pad(t.h)}{labels[1]}
{pad(t.m)}{labels[2]}
{pad(t.s)}{labels[3]}
); } // ----- Reveal on scroll ----- function Reveal({ children, delay = 0, as = 'div', ...rest }) { const ref = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setVisible(true); obs.disconnect(); } }, { threshold: 0.12 }); obs.observe(el); return () => obs.disconnect(); }, []); const Tag = as; return {children}; } // ----- Ticker ----- function Ticker({ items }) { const content = ( {items.map((it, i) => ( {it} ))} ); return (
{content}{content}
); } // ----- Section header ----- function SectionHead({ num, eyebrow, heading }) { return (
{eyebrow}
{heading}
); } Object.assign(window, { Icon, Nav, Countdown, Reveal, Ticker, SectionHead });