);
}
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.