/* Formula Destruction — shared React atoms. Exported to window. */ const { useState, useEffect, useRef } = React; /* ----------------------------------------------------------------- FLAGS Real flag SVGs from flagcdn.com, keyed by ISO 3166-1 alpha-2 code. Wrapped with rounded corners + shadow so they read crisply at any size. Falls back to a country-code chip for an empty/unknown code or a load failure (e.g. offline). */ function Flag({ code, w = 22 }) { const [failed, setFailed] = useState(false); const cc = (code || "").toLowerCase(); const h = Math.round(w * 0.68); const wrap = { width: w, height: h, borderRadius: 3, overflow: "hidden", flex: "none", boxShadow: "inset 0 0 0 1px rgba(255,255,255,.18), 0 1px 2px rgba(0,0,0,.4)", display: "inline-block", position: "relative", background: "#222", }; if (cc && !failed) { return {(code setFailed(true)} style={{ ...wrap, objectFit: "cover" }} />; } // fallback chip — empty code or image failed to load return {(code || "??").toUpperCase()}; } /* --------------------------------------------------------------- AVATAR Initials on a team-tinted gradient. Drop real photos in later. */ function Avatar({ driver, size = 44, ring = true }) { const FD = window.FD; // driver.team is the current-season team (null for drivers not racing now); // fall back to their most recent team, then to a neutral grey. let team = FD.teamById[driver.team]; if (!team && driver.hist) { for (const s of FD.seasons) { if (driver.hist[s.id]) { team = FD.teamById[driver.hist[s.id]]; break; } } } team = team || { color: "#555" }; const initials = (driver.code || driver.username.slice(0, 2)).toUpperCase(); const ringShadow = ring ? `inset 0 0 0 2px rgba(255,255,255,.14), 0 0 0 2px color-mix(in srgb, ${team.color} 55%, transparent)` : "inset 0 0 0 1px rgba(255,255,255,.12)"; if (driver.photoUrl) { return ( {driver.username} ); } return ( 2 ? 0.32 : 0.4), color: "#fff", letterSpacing: "-.02em", background: `radial-gradient(120% 120% at 30% 20%, ${team.color}, color-mix(in srgb, ${team.color} 30%, #0a0c10) 90%)`, boxShadow: ringShadow, }}>{initials} ); } /* ------------------------------------------------------------ TEAM LOGO A team's logo image when one is set, else a color-tinted chip showing the team code. `size` is the box edge in px. Used everywhere a team is shown prettily — cards, standings, hero blocks — and degrades gracefully if the image is missing or fails to load. */ function TeamLogo({ team, size = 28, radius }) { const [failed, setFailed] = useState(false); const t = team || {}; const color = t.color || "#555"; const r = radius == null ? Math.round(size * 0.28) : radius; const box = { width: size, height: size, borderRadius: r, flex: "none", overflow: "hidden", display: "inline-flex", alignItems: "center", justifyContent: "center", boxShadow: "inset 0 0 0 1px rgba(255,255,255,.12)", }; if (t.logoUrl && !failed) { // F1 marks are designed for light backgrounds (Ferrari, Sauber, Mercedes, // McLaren are dark-on-white). A clean white plate gives every logo contrast // and hides the baked-in white rectangles some sources ship. The logo fills // the padded plate by its longest side, so wide wordmarks aren't shrunk to // fit a square. return ( {t.name setFailed(true)} style={{ maxWidth: "100%", maxHeight: "100%", width: "auto", height: "auto", objectFit: "contain", display: "block" }} /> ); } const initials = (t.code || t.name || "??").slice(0, 3).toUpperCase(); return ( 2 ? 0.3 : 0.38), color: "#fff", letterSpacing: "-.02em", background: `radial-gradient(120% 120% at 30% 20%, ${color}, color-mix(in srgb, ${color} 30%, #0a0c10) 90%)` }}> {initials} ); } /* ------------------------------------------------------------- COUNTDOWN */ function useCountdown(target) { const [now, setNow] = useState(Date.now()); useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []); let diff = Math.max(0, new Date(target).getTime() - now); const d = Math.floor(diff / 864e5); diff -= d * 864e5; const h = Math.floor(diff / 36e5); diff -= h * 36e5; const m = Math.floor(diff / 6e4); diff -= m * 6e4; const s = Math.floor(diff / 1e3); return { d, h, m, s }; } /* --------------------------------------------------------- ANIMATED NUMBER */ function AnimatedNumber({ value, duration = 1100, className, style }) { const [n, setN] = useState(0); const ref = useRef(); useEffect(() => { const start = performance.now(); const from = 0; const to = value; cancelAnimationFrame(ref.current); const tick = (t) => { const p = Math.min(1, (t - start) / duration); const e = 1 - Math.pow(1 - p, 3); setN(Math.round(from + (to - from) * e)); if (p < 1) ref.current = requestAnimationFrame(tick); }; ref.current = requestAnimationFrame(tick); // safety: guarantee the final value even if rAF is throttled (hidden frame) const guard = setTimeout(() => setN(to), duration + 400); return () => { cancelAnimationFrame(ref.current); clearTimeout(guard); }; }, [value]); return {n.toLocaleString()}; } /* ---------------------------------------------------------------- LOGO Typographic lockup + simple angular mark (no fragile illustration). */ function Mark({ size = 26, color = "var(--accent)" }) { return ( {[1, 0.62, 0.30].map((op, i) => ( ))} ); } function Logo({ size = 22, stacked = false, color = "var(--ink)" }) { // Brand wordmark (orange "Formula Destruction" lockup). `size` keeps its // original meaning as the type height; the stacked two-line lockup renders // at ~1.6x that. `stacked`/`color` are accepted for call-site compatibility. return ( Formula Destruction ); } Object.assign(window, { Flag, Avatar, TeamLogo, useCountdown, AnimatedNumber, Mark, Logo });