/* ============================================================ Shared: Router, Nav, Footer, Page Header, Newsletter ============================================================ */ const { useState, useEffect, useRef, createContext, useContext } = React; /* --- Router ------------------------------------------------- */ const RouteCtx = createContext({ route: "home", go: () => {} }); function useRoute() { return useContext(RouteCtx); } function RouteProvider({ children }) { const getRoute = () => (window.location.hash.replace("#/", "") || "home"); const [route, setRoute] = useState(getRoute()); useEffect(() => { const handler = () => { setRoute(getRoute()); window.scrollTo({ top: 0, behavior: "instant" }); }; window.addEventListener("hashchange", handler); return () => window.removeEventListener("hashchange", handler); }, []); const go = (r) => { window.location.hash = "#/" + r; }; return {children}; } /* --- Utility Bar with EN/FR switch ------------------------- */ function UtilityBar() { const { lang, setLang, t } = useT(); return (
·
{t.utilNewsletter} {t.utilCareers} {t.utilMedia} {t.utilLogin}
); } function Logo() { const { t } = useT(); return ( {t.brandShort} {t.brandLine1} {t.brandLine2} ); } function Nav() { const { route } = useRoute(); const { t, lang, setLang } = useT(); const [open, setOpen] = useState(false); // Close drawer on route change useEffect(() => { setOpen(false); }, [route]); // Lock body scroll when drawer is open useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = open ? "hidden" : prev; return () => { document.body.style.overflow = prev; }; }, [open]); // Close on Escape useEffect(() => { const onKey = (e) => { if (e.key === "Escape") setOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); return ( <> {/* Mobile drawer */}
{t.brandShort} · Menu
{t.navCta}
Lang ·
); } /* --- Footer -------------------------------------------------- */ function Footer() { const { t } = useT(); return ( ); } /* --- Page Header (inner pages) ------------------------------ */ function PageHeader({ eyebrow, title, lede, meta, bgUrl }) { return (
{eyebrow}

{title}

{lede &&

{lede}

} {meta && (
{meta.map((m, i) => (
{m.label} {m.value}
))}
)}
); } /* --- Animated number (counts up when visible) --------------- */ function CountUp({ end, duration = 1600, prefix = "", suffix = "" }) { const { lang } = useT(); const ref = useRef(null); const [n, setN] = useState(0); const [started, setStarted] = useState(false); useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting && !started) setStarted(true); }); }, { threshold: 0.4 }); io.observe(ref.current); return () => io.disconnect(); }, [started]); useEffect(() => { if (!started) return; const t0 = performance.now(); let raf; const tick = (t) => { const k = Math.min(1, (t - t0) / duration); const eased = 1 - Math.pow(1 - k, 3); setN(Math.round(eased * end)); if (k < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [started, end, duration]); // French locale uses non-breaking space as thousand separator const locale = lang === "fr" ? "fr-CA" : "en-CA"; return {prefix}{n.toLocaleString(locale)}{suffix && {suffix}}; } /* --- Newsletter Signup form (with validation) -------------- */ function NewsletterForm() { const { t } = useT(); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [role, setRole] = useState(""); const [errs, setErrs] = useState({}); const [done, setDone] = useState(false); const validate = () => { const e = {}; if (!name.trim()) e.name = t.nlRequired; if (!email.trim()) e.email = t.nlRequired; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) e.email = t.nlInvalidEmail; setErrs(e); return Object.keys(e).length === 0; }; const submit = (ev) => { ev.preventDefault(); if (validate()) setDone(true); }; if (done) { return (
{t.nlSuccessTag}
{t.nlSuccessLine(name.split(" ")[0])}

{t.nlSuccessNote}

); } return (
setName(e.target.value)} className={errs.name ? "err" : ""} aria-label="Name" /> setEmail(e.target.value)} className={errs.email ? "err" : ""} aria-label="Email" />
setRole(e.target.value)} aria-label="Role" />
{errs.email || errs.name ? t.nlErrors : t.nlFinePrint}
); } function NewsletterBand() { const { t } = useT(); return (
{t.nlEyebrow}

{t.nlHeadline}

{t.nlLede}

); } Object.assign(window, { RouteProvider, useRoute, Nav, UtilityBar, Footer, Logo, PageHeader, CountUp, NewsletterForm, NewsletterBand, });