// Reusable components: Navbar, Hero, Sections, Footer, etc.
const { useState, useEffect, useRef } = React;
// ---- IntersectionObserver hook for scroll reveal ----
function useReveal() {
useEffect(() => {
const els = document.querySelectorAll(".reveal");
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add("in");
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: "0px 0px -60px 0px" });
els.forEach((el) => io.observe(el));
return () => io.disconnect();
});
}
// ---- Navbar ----
function Navbar({ t, lang, setLang, onCta }) {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const [active, setActive] = useState("hero");
useEffect(() => {
const onScroll = () => {
setScrolled(window.scrollY > 20);
const ids = ["historie", "carnaval", "lustrum", "over", "contact"];
let cur = "hero";
for (const id of ids) {
const el = document.getElementById(id);
if (el && el.getBoundingClientRect().top < 120) cur = id;
}
setActive(cur);
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const goTo = (id) => (e) => {
e.preventDefault();
setOpen(false);
const el = document.getElementById(id);
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 70;
window.scrollTo({ top, behavior: "smooth" });
}
};
return (
);
}
// ---- Hero ----
function Hero({ t, onPrimary, onSecondary }) {
return (
{t.hero.eyebrow}
{t.hero.title1} {t.hero.title2}
{t.hero.lead}
{t.hero.stats.map((s, i) => (
{s.num}
{s.label}
))}
);
}
// ---- Divider ----
function Divider() {
return (
✦
);
}
// ---- Historie / Timeline ----
function HistorieSection({ t }) {
return (
{t.historie.eyebrow}
{t.historie.lead}
{t.historie.timeline.map((it, i) => (
{it.date}
{it.title}
{it.body}
{it.glyph}
))}
);
}
// ---- Story (about origin) ----
function StorySection({ t }) {
return (
{t.story.eyebrow}
{t.story.title}
{t.story.paras[0]}
{t.story.paras[1]}
{t.story.quote}
{t.story.paras[2]}
);
}
// ---- Carnaval ----
function CarnavalSection({ t }) {
return (
{t.carnaval.eyebrow}
{t.carnaval.lead}
{t.carnaval.bullets.map((b, i) => (
-
{b.glyph}
{b.text}
))}
{t.carnaval.cardTitle}
{t.carnaval.cardLead}
);
}
function ConfettiBg() {
const colors = ["#8b1f1f", "#c89530", "#1e4a8c", "#6b1414", "#d9b04a"];
const pieces = Array.from({length: 18}, (_, i) => ({
left: (i * 5.5 + (i % 3) * 4) % 100,
delay: (i * 0.35) % 6,
color: colors[i % colors.length],
rot: (i * 23) % 360,
}));
return (
{pieces.map((p, i) => (
))}
);
}
// ---- Over Ons ----
function OverSection({ t }) {
return (
{t.over.eyebrow}
{t.over.lead}
{t.over.cards.map((c, i) => (
{c.label}
{c.name}
{c.num}{c.numSuffix}
{c.desc}
))}
);
}
function StichtingWrap({ t }) {
return (
{t.over.stichting.eyebrow}
{t.over.stichting.title}
{t.over.stichting.items.map((item, i) => (
- {item.label}
- {item.value}
))}
);
}
function BestuurSection({ t }) {
return (
{t.over.bestuur.eyebrow}
{t.over.bestuur.title}
{t.over.bestuur.members.map((m, i) => (
{m.name}
{m.role}
{m.emailUser}
[{t.over.bestuur.bij}]
{m.emailDomain}
))}
);
}
function RoundTable({ t }) {
const [seats, setSeats] = React.useState(56);
// shoulder width per seat for round-table circumference calc
const SHOULDER_CM = 70;
const radiusCm = (seats * SHOULDER_CM) / (2 * Math.PI);
const radiusM = (radiusCm / 100).toFixed(2);
const diameterM = ((radiusCm * 2) / 100).toFixed(2);
// dimensions of the SVG viewBox
const VB = 600;
const cx = VB / 2;
const cy = VB / 2;
// outer ring radius (visual)
const ringR = 260;
const innerR = ringR - 38;
return (
setSeats(parseInt(e.target.value, 10))}
className="rt-slider"
aria-label={t.over.tableSeatsLabel}
/>
{seats}{t.over.seats}
⌀ {diameterM}{t.over.diameter}
↔ {radiusM}{t.over.radius}
{t.over.formula}
{t.over.tableCenter}
);
}
// ---- Contact ----
function ContactSection({ t }) {
return (
);
}
// ---- Footer ----
function Footer({ t, onNav }) {
return (
);
}
// ---- Lustrum 2027 ----
function LustrumSection({ t }) {
const [now, setNow] = React.useState(() => Date.now());
React.useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
const target = new Date("2027-05-02T20:00:00").getTime();
const diff = Math.max(0, target - now);
const d = Math.floor(diff / (1000 * 60 * 60 * 24));
const h = Math.floor((diff / (1000 * 60 * 60)) % 24);
const m = Math.floor((diff / (1000 * 60)) % 60);
const s = Math.floor((diff / 1000) % 60);
const pad = (n) => String(n).padStart(2, "0");
return (
{t.lustrum.eyebrow}
{t.lustrum.lead}
{t.lustrum.bullets.map((b, i) => (
))}
MMXXVII
{t.lustrum.countdownLabel}
{pad(d)}
{t.lustrum.labels.days}
{pad(h)}
{t.lustrum.labels.hours}
{pad(m)}
{t.lustrum.labels.minutes}
{pad(s)}
{t.lustrum.labels.seconds}
II · V · MMXXVII
);
}
Object.assign(window, { useReveal, Navbar, Hero, HistorieSection, StorySection, CarnavalSection, OverSection, StichtingWrap, BestuurSection, ContactSection, Footer, Divider, LustrumSection });