// openfootball worldcup-2026.json → public/data/fixtures.json // Normalizes the 104 matches into the app's Fixture shape: canonical team names, // ISO-UTC kickoffs, stage tags, and pretty knockout placeholder labels. import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { canonicalTeam } from '../src/lib/teams'; import type { Fixture, FixturesFile, Stage, TeamSlot } from '../src/lib/types'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const RAW = join(ROOT, 'data', 'raw', 'worldcup-2026.json'); const OUT_DIR = join(ROOT, 'public', 'data'); interface RawMatch { round: string; num?: number; date: string; time: string; team1: string; team2: string; group?: string; ground: string; } function stageOf(round: string): Stage { if (round.startsWith('Matchday')) return 'group'; if (round.startsWith('Round of 32')) return 'r32'; if (round.startsWith('Round of 16')) return 'r16'; if (round.startsWith('Quarter')) return 'qf'; if (round.startsWith('Semi')) return 'sf'; if (round.startsWith('Match for third')) return 'third'; if (round.startsWith('Final')) return 'final'; throw new Error(`Unknown round: ${round}`); } /** "2026-06-11" + "13:00 UTC-6" → ISO 8601 UTC. */ function toIsoUtc(date: string, time: string): string { const [y, mo, d] = date.split('-').map(Number); const m = time.match(/^(\d{1,2}):(\d{2})\s*UTC([+-]\d{1,2})$/); if (!m || y === undefined || mo === undefined || d === undefined) { throw new Error(`Bad date/time: ${date} ${time}`); } const hh = Number(m[1]); const mm = Number(m[2]); const offset = Number(m[3]); // local = UTC + offset, so UTC hour = hh - offset return new Date(Date.UTC(y, mo - 1, d, hh - offset, mm)).toISOString(); } /** Knockout placeholder code → readable label. */ function placeholderLabel(code: string): string { const pos = code.match(/^([123])([A-L])$/); if (pos) { const rank = pos[1] === '1' ? 'Winner' : pos[1] === '2' ? 'Runner-up' : '3rd'; return `${rank} Group ${pos[2]}`; } const third = code.match(/^3([A-L/]+)$/); if (third) return `3rd: ${third[1]}`; const wl = code.match(/^([WL])(\d{1,3})$/); if (wl) return `${wl[1] === 'W' ? 'Winner' : 'Loser'} of M${wl[2]}`; return code; } function slot(raw: string, isGroup: boolean): TeamSlot { if (isGroup) { const team = canonicalTeam(raw); return { team, placeholder: null, label: team }; } return { team: null, placeholder: raw, label: placeholderLabel(raw) }; } function main(): void { const raw = JSON.parse(readFileSync(RAW, 'utf8')) as { name: string; matches: RawMatch[] }; const matches = raw.matches; // groupRound: order each group's matches by kickoff, 2 matches per round. const groupOrder = new Map(); // `${group}#${num}` → 0-based index const byGroup = new Map(); const prelim = matches.map((m, i) => { const stage = stageOf(m.round); const num = m.num ?? i + 1; const iso = toIsoUtc(m.date, m.time); if (stage === 'group' && m.group) { const g = m.group.replace('Group ', ''); if (!byGroup.has(g)) byGroup.set(g, []); byGroup.get(g)!.push({ num, iso }); } return { m, stage, num, iso }; }); for (const [g, list] of byGroup) { list.sort((a, b) => a.iso.localeCompare(b.iso) || a.num - b.num); list.forEach((x, idx) => groupOrder.set(`${g}#${x.num}`, idx)); } const fixtures: Fixture[] = prelim.map(({ m, stage, num, iso }) => { const group = stage === 'group' && m.group ? m.group.replace('Group ', '') : null; const groupRound = group ? Math.floor((groupOrder.get(`${group}#${num}`) ?? 0) / 2) + 1 : null; return { num, stage, group, round: m.round, groupRound, kickoff: iso, venue: m.ground, home: slot(m.team1, stage === 'group'), away: slot(m.team2, stage === 'group'), status: 'scheduled', homeScore: null, awayScore: null, minute: null, } satisfies Fixture; }); fixtures.sort((a, b) => a.num - b.num); const out: FixturesFile = { season: raw.name, generatedAt: new Date().toISOString(), groups: [...new Set(fixtures.filter((f) => f.group).map((f) => f.group!))].sort(), venues: [...new Set(fixtures.map((f) => f.venue))].sort(), fixtures, }; mkdirSync(OUT_DIR, { recursive: true }); writeFileSync(join(OUT_DIR, 'fixtures.json'), JSON.stringify(out)); console.log( `fixtures → public/data/fixtures.json (${fixtures.length} matches, ${out.groups.length} groups, ${out.venues.length} venues)`, ); } main();