/* CrossCurrent Racing — shared site shell. Exports nav / footer / hero / discord beacon / band helpers + site data onto window so every page (each its own Babel scope) can use them. */ (function () { const D = window.CrossCurrentRacingDesignSystem_c4f6ec; const { CurrentStripe, Button, TextLink, Tag } = D; const { useState, useEffect, useRef } = React; const LOGO = "assets/logos/crosscurrent-logo.jpg"; const DISCORD = "https://discord.gg/RUucJh26u7"; const GUILD_ID = "1478477879926067335"; /* ---- Global navigation (real hyperlinks between pages) ---- */ const PAGES = [ { label: "Home", href: "index.html" }, { label: "About", href: "about.html" }, { label: "Drivers", href: "drivers.html" }, { label: "Races", href: "races.html" }, { label: "Results", href: "results.html" }, { label: "Gallery", href: "gallery.html" }, { label: "Apps", href: "apps.html" }, { label: "Contact", href: "contact.html" }, ]; /* ---- Shared Discord auth (real OAuth2 via /api/auth/*, see HANDOFF.md seam 1) ---- Identity/permissions live server-side (signed httpOnly session cookie + a live Discord role check on every /api/auth/me call, so a role change in Discord takes effect immediately without forcing a re-login). The client just mirrors whatever the backend reports; there is nothing to trust on this side. */ const SIGNED_OUT_AUTH = { authenticated: false, name: null, isMember: false, isManager: false, isAdmin: false, canManageRaces: false }; let authFetchPromise = null; // de-dupe: pages that call useAuth() more than once share one request function fetchAuth() { if (!authFetchPromise) { authFetchPromise = fetch("/api/auth/me", { credentials: "include" }) .then((r) => (r.ok ? r.json() : SIGNED_OUT_AUTH)) .catch(() => SIGNED_OUT_AUTH) .finally(() => { authFetchPromise = null; }); } return authFetchPromise; } function useAuth() { const [state, setState] = useState(SIGNED_OUT_AUTH); useEffect(() => { let alive = true; const load = () => fetchAuth().then((d) => { if (alive) setState(d); }); load(); window.addEventListener("cc-auth-change", load); return () => { alive = false; window.removeEventListener("cc-auth-change", load); }; }, []); return { auth: state.authenticated ? { name: state.name } : null, isMember: state.isMember, isManager: state.isManager, isAdmin: state.isAdmin, canManageRaces: state.canManageRaces, signIn: () => { window.location.href = "/api/auth/discord/login?returnTo=" + encodeURIComponent(location.pathname); }, signOut: () => { fetch("/api/auth/logout", { method: "POST", credentials: "include" }) .finally(() => window.dispatchEvent(new CustomEvent("cc-auth-change"))); }, }; } /* ---- Shared data store (races/results/drivers/tracks/stats, see HANDOFF.md seam 2). Reads go through /api/data/all so all visitors see the same admin-edited content; falls back to the static data.js constants if the backend is briefly unreachable, so the site still renders something. ---- */ function liveFallback() { const D = window.CCData || {}; return { roster: D.ROSTER || [], calendar: D.CALENDAR || [], results: D.RESULTS || [], tracks: D.TRACKS || [], stats: { racesEntered: "0", enduranceKm: "0" }, }; } let liveDataPromise = null; function fetchLiveData() { if (!liveDataPromise) { liveDataPromise = fetch("/api/data/all") .then((r) => (r.ok ? r.json() : null)) .catch(() => null) .finally(() => { liveDataPromise = null; }); } return liveDataPromise; } function useLiveData() { const [state, setState] = useState(liveFallback); const [loading, setLoading] = useState(true); const load = () => { setLoading(true); fetchLiveData().then((d) => { if (d) setState(d); setLoading(false); }); }; useEffect(() => { load(); }, []); return { ...state, loading, refresh: load }; } function navAuthBtn(primary) { return { font: "var(--type-body-sm)", fontSize: "12px", letterSpacing: "1.5px", textTransform: "uppercase", cursor: "pointer", padding: "8px 14px", border: "1px solid " + (primary ? "var(--cc-red)" : "var(--cc-hairline)"), background: primary ? "var(--cc-red)" : "transparent", color: primary ? "#fff" : "var(--cc-body)", }; } function AuthModal({ onClose, onSignIn }) { const mbtn = { font: "var(--type-body-sm)", fontWeight: 700, fontSize: "13px", letterSpacing: "1.5px", textTransform: "uppercase", cursor: "pointer", padding: "14px 18px", border: "none", background: "var(--cc-red)", color: "#fff", textAlign: "left", }; return (
e.stopPropagation()} style={{ position: "relative", width: "min(440px,100%)", background: "var(--cc-surface-card)", border: "1px solid var(--cc-hairline-strong)", padding: "var(--space-xl)", display: "flex", flexDirection: "column", gap: "var(--space-md)", overflow: "hidden" }}>
CrossCurrent · Members

Sign in with Discord

We check your role in the CrossCurrent Discord to unlock members-only tools.

); } function SiteNav({ active }) { const [hover, setHover] = useState(null); const { auth, signIn, signOut, isAdmin, canManageRaces } = useAuth(); const [modal, setModal] = useState(false); const items = canManageRaces ? [...PAGES, { label: "Admin", href: "admin.html", admin: true }] : PAGES; return (
CROSSCURRENT
{auth ? (
{auth.name}
) : ( )}
{modal && setModal(false)} onSignIn={signIn} />}
); } /* ---- Content band + head ---- */ function Band({ children, style }) { return (
{children}
); } function BandHead({ eyebrow, title, link, href = "#" }) { return (
{eyebrow && {eyebrow}}

{title}

{link && {link}}
); } /* ---- Per-page hero (distinct treatment via props, one structure) ---- */ function PageHero({ eyebrow, title, subtitle, align = "left", accent = "var(--cc-red)", meta, children, minHeight = 380 }) { const centered = align === "center"; return (
{/* carbon telemetry field */}
{eyebrow && {eyebrow}}

{title}

{subtitle &&

{subtitle}

} {meta &&
{meta}
} {children}
); } /* ---- Discord "team radio" beacon — on-theme comms channel ---- */ function DiscordBeacon({ compact = false, memberCount = 128 }) { const [online, setOnline] = useState(null); useEffect(() => { let alive = true; const load = () => fetch(`https://discord.com/api/guilds/${GUILD_ID}/widget.json`) .then((r) => (r.ok ? r.json() : null)) .then((d) => { if (alive && d && typeof d.presence_count === "number") setOnline(d.presence_count); }) .catch(() => {}); load(); const id = setInterval(load, 60000); return () => { alive = false; clearInterval(id); }; }, []); const count = online != null ? online : memberCount; return (
Team radio · live
{count} online · on the paddock channel
{/* waveform */}
Open the comms Setups, stint planning, banter and other games — all in our Discord.
); } function SiteFooter() { const columns = [ { title: "Racing", links: [["Upcoming races", "races.html"], ["Race results", "results.html"], ["The grid", "drivers.html"]] }, { title: "Team", links: [["About us", "about.html"], ["Gallery", "gallery.html"], ["Team apps", "apps.html"]] }, { title: "Community", links: [["Discord", DISCORD], ["Contact", "contact.html"]] }, ]; return (
CrossCurrent Racing

Different currents, one racing line. A global sim-racing community with roots in the UK and US.

{columns.map((col) => (
{col.title}
{col.links.map(([l, href]) => ( {l} ))}
))}
© {new Date().getFullYear()} CrossCurrent Racing. A global sim-racing community. Not affiliated with any vehicle manufacturer. United Kingdom · United States
); } /* Country flag (real flag image via ISO code; falls back to code text) */ function isoFor(country) { const map = (window.CCData && window.CCData.COUNTRY_ISO) || {}; return map[country] || map[(country || "").toUpperCase()] || null; } function flagUrl(country) { const iso = isoFor(country); return iso ? `https://flagcdn.com/${iso}.svg` : null; } function FlagChip({ country, size = 18 }) { const url = flagUrl(country); const iso = isoFor(country); return ( {url && { e.currentTarget.style.display = "none"; }} style={{ width: `${size}px`, height: `${Math.round(size * 0.7)}px`, objectFit: "cover", border: "1px solid var(--cc-hairline)", display: "block" }} />} {iso ? iso.toUpperCase() : country} ); } /* ---- Inline admin ribbon (shown on manageable pages when signed in as admin) ---- */ function AdminRibbon({ title, note, actions }) { return (
{title || "Admin mode"} {note && {note}}
{actions &&
{actions}
}
); } /* ---- Local-timezone formatting for race sessions ---- */ function localTZAbbr() { try { const parts = new Intl.DateTimeFormat("en-GB", { timeZoneName: "short" }).formatToParts(new Date()); const p = parts.find((x) => x.type === "timeZoneName"); return p ? p.value : ""; } catch (e) { return ""; } } function fmtSessionLocal(iso) { const d = new Date(iso); return { day: d.toLocaleDateString([], { weekday: "short", day: "2-digit", month: "short" }), time: d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), }; } window.CCSite = { LOGO, DISCORD, PAGES, SiteNav, SiteFooter, Band, BandHead, PageHero, DiscordBeacon, FlagChip, useAuth, AuthModal, AdminRibbon, fmtSessionLocal, localTZAbbr, flagUrl, useLiveData, }; })();