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>
This commit is contained in:
2026-06-11 13:04:31 +02:00
parent 4e4e75a1d8
commit 9a31e9f4db
18 changed files with 1461 additions and 21 deletions
+39
View File
@@ -0,0 +1,39 @@
import { teamFlag } from '@/lib/teams';
import { cn } from '@/lib/cn';
import type { TeamSlot } from '@/lib/types';
/** A team identity (flag + name) or, for an unresolved knockout slot, a muted
* placeholder label ("Runner-up Group A"). */
export function TeamLabel({
slot,
align = 'left',
className,
}: {
slot: TeamSlot;
align?: 'left' | 'right';
className?: string;
}) {
const right = align === 'right';
if (!slot.team) {
return (
<span
className={cn('truncate text-sm italic text-faint', right && 'text-right', className)}
title={slot.label}
>
{slot.label}
</span>
);
}
return (
<span
className={cn(
'flex min-w-0 items-center gap-2',
right && 'flex-row-reverse',
className,
)}
>
<span aria-hidden className="text-lg leading-none">{teamFlag(slot.team)}</span>
<span className="truncate font-medium text-ink">{slot.team}</span>
</span>
);
}