/* 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
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 (
);
}
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 (
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 (
);
}
Object.assign(window, { Flag, Avatar, TeamLogo, useCountdown, AnimatedNumber, Mark, Logo });