// global.jsx — CDG Global · cómo están los pilotos de CDG en el Global Qualifier // La página carga al instante con data/global.js y, si existe el puente // gq-bridge.php, refresca tiempos/posiciones EN VIVO desde MultiGP al abrirse. const { useState, useEffect, useRef, useMemo } = React; // ---------- Directorio de pilotos (foto + país + IG) ---------- const GL_DIR = (typeof window !== "undefined" && window.CDG_PILOTS) ? window.CDG_PILOTS : {}; const _GL_NORM = Object.fromEntries(Object.entries(GL_DIR).map(([k, v]) => [k.toLowerCase().trim(), v])); function pilotMeta(name) { if (!name) return {}; return _GL_NORM[name.toLowerCase().trim()] || {}; } function flagUrl(code) { return (!code || code.length !== 2) ? "" : `https://flagcdn.com/${code.toLowerCase()}.svg`; } // ---------- Tiempo ---------- function parseTime(str) { if (str === null || str === undefined) return Infinity; const s = String(str); const m = s.match(/(\d+):(\d+(?:\.\d+)?)/); if (m) { const ms = parseInt(m[1]) * 60000 + parseFloat(m[2]) * 1000; return ms > 0 ? ms : Infinity; } const n = parseFloat(s); // segundos sueltos "39.4" return Number.isFinite(n) && n > 0 ? n * 1000 : Infinity; } function fmtDelta(ms) { const s = ms / 1000; return (s >= 10 ? s.toFixed(1) : s.toFixed(2)) + "s"; } // ---------- MultiGP profile link ---------- function mgpLink(p, cfg) { if (p.mgpUrl) return p.mgpUrl; return cfg.leaderboardUrl || "https://www.multigp.com/2026-global-qualifier-leaderboard/"; } // ---------- localStorage: mejores tiempos vistos la última visita ---------- const GL_STORE = "cdg_gq_best_v1"; function readBests() { try { return JSON.parse(localStorage.getItem(GL_STORE) || "{}") || {}; } catch (e) { return {}; } } function writeBests(obj) { try { localStorage.setItem(GL_STORE, JSON.stringify(obj)); } catch (e) {} } // ---------- UI helpers ---------- function PilotAvatar({ name, size }) { const meta = pilotMeta(name); const s = size || 88; if (meta.photo) return {name}; const initials = String(name || "?").split(/\s+/).map(w => w[0]).slice(0, 2).join("").toUpperCase(); return
{initials}
; } function IgGlyph({ size = 12 }) { return (); } // ---------- Tarjeta de piloto ---------- function PilotCard({ p, pos, cfg, status, animate, idx }) { const meta = pilotMeta(p.handle); const flag = flagUrl(meta.country || p.country); const ms = parseTime(p.gqTime); const hasTime = Number.isFinite(ms); const isCDG = (p.where || "").toUpperCase() === "CDG"; const chapter = p.chapter || meta.chapter || ""; return (
{isCDG ? "Marcó en CDG" : "Marcó afuera"}
#{p.globalRank ? p.globalRank : pos} {p.globalRank ? "Posición mundial" : "Provisional · entre CDG"}
{status && status.kind === "up" &&
▼ {fmtDelta(status.delta)}
} {status && status.kind === "new" &&
NUEVO
}
{flag && {meta.country}}{p.handle}
{meta.ig ? @{meta.ig} : @—} {!isCDG && chapter && (
Home chapter{chapter}
)}
Mejor tiempo GQ
{hasTime ? p.gqTime : "—"}
{cfg.region || "Región"} {p.regionalRank ? `#${p.regionalRank}` : "—"}
Ver en MultiGP
); } function Global() { const cfg = (window.CDG_GLOBAL || { pilots: [] }); const basePilots = cfg.pilots || []; // Snapshot de "mejores vistos" tomado UNA vez al abrir → para detectar mejoras. const snapshot = useRef(readBests()); const [pilots, setPilots] = useState(basePilots); const [source, setSource] = useState("local"); // local | live | cache const [updated, setUpdated] = useState(cfg.updated || ""); const [animate, setAnimate] = useState(false); useEffect(() => { const id = requestAnimationFrame(() => setAnimate(true)); return () => cancelAnimationFrame(id); }, []); // ---- Intento de refresco EN VIVO vía el puente PHP ---- useEffect(() => { // Le preguntamos al puente por TODOS los pilotos relevantes: // · el directorio (data/pilots.js) // · todos los que aparecen en el ranking 5" y Tiny (data/results.js) // · los de la base local (data/global.js) // Además, el puente añade solo a cualquiera cuyo home chapter sea CDG. const roster = (window.CDG_PILOTS && Object.keys(window.CDG_PILOTS)) || []; const rankHandles = []; (window.CDG_RESULTS || []).forEach(race => { [...(race.qualification || []), ...(race.bracket || [])].forEach(row => { const h = (row.handle || row.pilot || "").trim(); if (h) rankHandles.push(h); }); }); const handles = Array.from(new Set([ ...roster, ...rankHandles, ...basePilots.map(p => p.handle) ])).join(","); let cancelled = false; fetch(`gq-bridge.php?handles=${encodeURIComponent(handles)}`, { cache: "no-store" }) .then(r => r.ok ? r.json() : null) .then(json => { if (cancelled || !json || !json.ok || !json.pilots) return; const live = json.pilots; // { handleLower: {...} } const merged = basePilots.map(p => { const l = live[p.handle.toLowerCase()]; if (!l) return p; return { ...p, gqTime: l.gqTime || p.gqTime, globalRank: (l.globalRank != null) ? l.globalRank : p.globalRank, regionalRank: (l.regionalRank != null) ? l.regionalRank : p.regionalRank, where: p.where || l.where, chapter: l.chapter || p.chapter, mgpUrl: l.mgpUrl || p.mgpUrl, }; }); // Pilotos que el live trae pero no estaban en la base local (marcaron afuera) const known = new Set(basePilots.map(p => p.handle.toLowerCase())); Object.keys(live).forEach(k => { if (!known.has(k)) merged.push({ ...live[k], where: live[k].where || "Afuera" }); }); setPilots(merged); setSource(json.source === "cache" ? "cache" : "live"); if (json.updated) setUpdated(new Date(json.updated).toLocaleString("es-GT", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })); }) .catch(() => { /* sin puente: nos quedamos con data/global.js */ }); return () => { cancelled = true; }; }, []); // ---- Ordenar: por rank global; sin rank → por tiempo ---- const ordered = useMemo(() => { return [...pilots].map((p, i) => ({ p, i })).sort((a, b) => { const ra = a.p.globalRank ?? Infinity, rb = b.p.globalRank ?? Infinity; if (ra !== rb) return ra - rb; return parseTime(a.p.gqTime) - parseTime(b.p.gqTime); }).map(x => x.p); }, [pilots]); // ---- Detección de mejoras vs última visita + persistir nuevos bests ---- const statuses = useMemo(() => { const snap = snapshot.current; const map = {}; const nextBests = { ...snap }; pilots.forEach(p => { const key = p.handle.toLowerCase(); const ms = parseTime(p.gqTime); if (!Number.isFinite(ms)) return; const prev = snap[key]; if (prev == null) { map[key] = { kind: "new" }; nextBests[key] = ms; } else if (ms < prev - 1) { // mejoró (al menos 1ms) map[key] = { kind: "up", delta: prev - ms }; nextBests[key] = ms; } else { nextBests[key] = Math.min(prev, ms); } }); writeBests(nextBests); return map; }, [pilots]); const improvedCount = Object.values(statuses).filter(s => s.kind === "up").length; const withRank = ordered.filter(p => p.globalRank).length; // Mostramos a quien tenga POSICIÓN oficial O un TIEMPO GQ registrado. // (Así los pilotos de CDG siempre aparecen con su tiempo; cuando el puente // está en vivo, además les llega la posición mundial de la tabla.) const shown = ordered.filter(p => p.globalRank || Number.isFinite(parseTime(p.gqTime))); const srcLabel = source === "live" ? "En vivo desde MultiGP" : source === "cache" ? "En vivo (caché)" : "Archivo local"; return ( <>
// CDG EN EL MUNDO · GLOBAL QUALIFIER {cfg.season || 2026}

CDG
Global

Dónde están parados nuestros pilotos en el leaderboard mundial del MultiGP Global Qualifier. Sus tiempos en la pista oficial GQ — marcados en una fecha de CDG o afuera en el mundo — comparados contra todo el planeta.

{srcLabel} {updated && Actualizado · {updated}} {improvedCount > 0 && ▼ {improvedCount} {improvedCount === 1 ? "piloto mejoró" : "pilotos mejoraron"} desde tu última visita} Leaderboard oficial →
{shown.length === 0 ? (
Aún no hay pilotos de CDG en la tabla oficial del Global Qualifier.
) : (<>
{shown.map((p, i) => ( ))}
{shown.length} pilotos CDG en la tabla GQ · {withRank} con posición mundial oficial · Track: {cfg.trackName || "Global Qualifier"}
Cómo se ordena. Quien ya tiene posición oficial en la tabla del MultiGP Global Qualifier va primero (por número de posición); abajo, el resto de pilotos de CDG por su mejor tiempo GQ. Se actualiza directamente desde MultiGP. ▼ MEJORÓ marca a quien bajó su tiempo desde tu última visita.
)}
); } ReactDOM.createRoot(document.getElementById("gl-root")).render();