// ranking.jsx — Página dedicada del RANKING (podio + movimientos + tabla completa)
// Autónoma: re-implementa los mismos cálculos que el index para no depender de app.jsx
const { useState, useEffect, useRef } = React;
// ---------- Directorio de pilotos ----------
const RK_DIR = (typeof window !== "undefined" && window.CDG_PILOTS) ? window.CDG_PILOTS : {};
const _RK_NORM = Object.fromEntries(Object.entries(RK_DIR).map(([k, v]) => [k.toLowerCase().trim(), v]));
function pilotMeta(name) { if (!name) return {}; return _RK_NORM[name.toLowerCase().trim()] || {}; }
function flagUrl(code) { return (!code || code.length !== 2) ? "" : `https://flagcdn.com/${code.toLowerCase()}.svg`; }
function pilotHref(name, year, cat) { return `piloto.html?name=${encodeURIComponent(name)}&year=${year}&cat=${encodeURIComponent(cat)}`; }
function parseTime(str) {
if (!str || typeof str !== "string") return Infinity;
const m = str.match(/(\d+):(\d+(?:\.\d+)?)/);
if (!m) return Infinity;
const ms = parseInt(m[1]) * 60000 + parseFloat(m[2]) * 1000;
return ms > 0 ? ms : Infinity;
}
function availableYears() {
const ys = new Set((window.CDG_RESULTS || []).map(r => r.year).filter(Boolean));
const arr = [...ys].sort((a, b) => b - a);
if (arr.length === 0) arr.push(new Date().getFullYear());
return arr;
}
// ---------- Cálculo de standings ----------
function rankRaces(data) {
const map = new Map();
const accumulate = (rows, isQual) => {
rows.forEach(row => {
const key = (row.handle || row.pilot).toUpperCase();
const cur = map.get(key) || { name: row.pilot, handle: row.handle || "", points: 0, races: new Set(), bestQual: null, bestBracket: null, qualPts: 0, bracketPts: 0, podiums: 0, wins: 0 };
const pts = Number(row.points || 0);
cur.points += pts;
if (isQual) cur.qualPts += pts; else cur.bracketPts += pts;
if (row.pos === 1 || row.pos === 2 || row.pos === 3) cur.podiums += 1;
if (!isQual && row.pos === 1) cur.wins += 1;
if (row.time) {
const t = parseTime(row.time);
if (Number.isFinite(t)) {
if (isQual && (cur.bestQual === null || t < cur.bestQual.ms)) cur.bestQual = { ms: t, str: row.time };
if (!isQual && (cur.bestBracket === null || t < cur.bestBracket.ms)) cur.bestBracket = { ms: t, str: row.time };
}
}
map.set(key, cur);
});
};
data.forEach(race => {
accumulate(race.qualification || [], true);
accumulate(race.bracket || [], false);
[...(race.qualification || []), ...(race.bracket || [])].forEach(row => {
const key = (row.handle || row.pilot).toUpperCase();
const cur = map.get(key); if (cur) cur.races.add(race.id);
});
});
return [...map.values()].map(p => ({ ...p, racesCount: p.races.size }))
.sort((a, b) => b.points - a.points || (a.bestQual?.ms ?? Infinity) - (b.bestQual?.ms ?? Infinity));
}
function computeRanking(year, category) {
return rankRaces((window.CDG_RESULTS || []).filter(r => r.year === year && r.category === category));
}
// ---------- Movimientos vs fecha anterior ----------
const MONTHS_ES = { ENE:0, FEB:1, MAR:2, ABR:3, MAY:4, JUN:5, JUL:6, AGO:7, SEP:8, OCT:9, NOV:10, DIC:11 };
function raceSortKey(race) {
const d = race.date || ""; let mo = 0, day = 1;
const mm = d.match(/\b(ENE|FEB|MAR|ABR|MAY|JUN|JUL|AGO|SEP|OCT|NOV|DIC)\b/i);
if (mm) mo = MONTHS_ES[mm[1].toUpperCase()] || 0;
const dm = d.match(/\d+/); if (dm) day = parseInt(dm[0], 10);
return (race.year || 0) * 10000 + mo * 100 + day;
}
function rankingMoves(year, category) {
const data = (window.CDG_RESULTS || []).filter(r => r.year === year && r.category === category);
if (data.length < 2) return { moves: {}, lastDate: null };
const sorted = [...data].sort((a, b) => raceSortKey(a) - raceSortKey(b));
const maxKey = Math.max(...sorted.map(raceSortKey));
const prevRaces = sorted.filter(r => raceSortKey(r) < maxKey);
const lastRace = sorted.find(r => raceSortKey(r) === maxKey);
if (prevRaces.length === 0) return { moves: {}, lastDate: lastRace?.date || null };
const cur = rankRaces(sorted), prev = rankRaces(prevRaces);
const prevPos = {}; prev.forEach((p, i) => { prevPos[(p.handle || p.name).toUpperCase()] = i; });
const moves = {};
cur.forEach((p, i) => {
const key = (p.handle || p.name).toUpperCase(); const pp = prevPos[key];
if (pp === undefined) moves[key] = { move: "new", amt: 0 };
else if (pp > i) moves[key] = { move: "up", amt: pp - i };
else if (pp < i) moves[key] = { move: "down", amt: i - pp };
else moves[key] = { move: "same", amt: 0 };
});
return { moves, lastDate: lastRace?.date || null };
}
// ---------- UI helpers ----------
function PilotAvatar({ name, size, cls }) {
const meta = pilotMeta(name); const s = size || 54;
if (meta.photo) return ;
const initials = String(name || "?").split(/\s+/).map(w => w[0]).slice(0, 2).join("").toUpperCase();
return
La pelea por el campeonato del 502, fecha a fecha. Puntos de clasificación + bracket, con los movimientos de cada piloto respecto a la fecha anterior.