Files
cup26/server/src/footballData.ts
T
NilsBriggen 9a31e9f4db Phase 1: live dashboard — fixtures, scores, group tables, bracket
- Shared domain model (types, team metadata/flags/aliases, FIFA-tiebreak standings)
- buildFixtures.ts: openfootball 2026 → normalized fixtures.json (104 matches,
  ISO-UTC kickoffs, pretty knockout placeholders)
- Fastify live layer: seed load, football-data.org poller + SofaScore enhancement
  (feature-flagged, fallback), dynamic live-window polling, WS snapshot push,
  REST /api/snapshot, dev /api/dev/score injection
- Client: tournament store (REST + WS + static-file fallback), MatchCard,
  TeamLabel, GroupTable, Live/Groups/Bracket pages, live connection indicator
- Verified: WS pushes a dev-injected result; standings recompute live in-browser

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:04:31 +02:00

84 lines
2.2 KiB
TypeScript

import type { LiveMatch } from './tournament';
import type { MatchStatus, Stage } from '../../src/lib/types';
// football-data.org v4 — the reliable (if delayed) free source.
// Free tier: World Cup is included, 10 req/min. We poll one endpoint (~1 req/min).
const BASE = 'https://api.football-data.org/v4';
const COMPETITION = 'WC';
interface FdTeam { name: string | null }
interface FdScore { home: number | null; away: number | null }
interface FdMatch {
status: string;
stage: string;
group: string | null;
utcDate: string;
minute?: number | null;
homeTeam: FdTeam;
awayTeam: FdTeam;
score: { fullTime: FdScore };
}
function mapStatus(s: string): MatchStatus | null {
switch (s) {
case 'SCHEDULED':
case 'TIMED':
return 'scheduled';
case 'IN_PLAY':
case 'PAUSED':
return 'live';
case 'FINISHED':
return 'finished';
default:
return null; // POSTPONED / SUSPENDED / CANCELLED — ignore
}
}
function mapStage(s: string): Stage | null {
switch (s) {
case 'GROUP_STAGE':
return 'group';
case 'LAST_32':
return 'r32';
case 'LAST_16':
return 'r16';
case 'QUARTER_FINALS':
return 'qf';
case 'SEMI_FINALS':
return 'sf';
case 'THIRD_PLACE':
return 'third';
case 'FINAL':
return 'final';
default:
return null;
}
}
export async function fetchFootballData(token: string): Promise<LiveMatch[]> {
const res = await fetch(`${BASE}/competitions/${COMPETITION}/matches`, {
headers: { 'X-Auth-Token': token },
signal: AbortSignal.timeout(12_000),
});
if (!res.ok) throw new Error(`football-data ${res.status}`);
const body = (await res.json()) as { matches?: FdMatch[] };
const out: LiveMatch[] = [];
for (const m of body.matches ?? []) {
const status = mapStatus(m.status);
const stage = mapStage(m.stage);
if (!status || !stage || !m.homeTeam.name || !m.awayTeam.name) continue;
out.push({
home: m.homeTeam.name,
away: m.awayTeam.name,
group: m.group ? m.group.replace('GROUP_', '') : null,
stage,
kickoff: m.utcDate,
homeScore: m.score.fullTime.home,
awayScore: m.score.fullTime.away,
status,
minute: m.minute ?? null,
});
}
return out;
}