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
+57
View File
@@ -0,0 +1,57 @@
import { cn } from '@/lib/cn';
import { teamFlag } from '@/lib/teams';
import type { StandingRow } from '@/lib/types';
/** One group's standings. Top 2 qualify directly; 3rd may advance as a best-third. */
export function GroupTable({ group, rows }: { group: string; rows: StandingRow[] }) {
return (
<div className="overflow-hidden rounded-xl border border-line bg-panel">
<div className="flex items-center justify-between border-b border-line px-3 py-2">
<span className="font-display text-sm font-bold text-ink">Group {group}</span>
<span className="smallcaps">Pld · Pts</span>
</div>
<table className="w-full border-collapse text-sm">
<thead>
<tr className="text-[11px] uppercase tracking-wide text-faint">
<th className="px-3 py-1.5 text-left font-semibold">Team</th>
<th className="w-8 py-1.5 text-center font-semibold">P</th>
<th className="w-8 py-1.5 text-center font-semibold">GD</th>
<th className="w-9 py-1.5 text-center font-semibold">Pts</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr
key={r.team}
className={cn(
'border-t border-line/60',
r.rank <= 2 && 'bg-accent-glow',
r.rank === 3 && 'bg-gold/5',
)}
>
<td className="px-3 py-1.5">
<span className="flex items-center gap-2">
<span
className={cn(
'w-4 text-center text-[11px] font-bold tabular-nums',
r.rank <= 2 ? 'text-accent' : r.rank === 3 ? 'text-gold' : 'text-faint',
)}
>
{r.rank}
</span>
<span aria-hidden className="text-base leading-none">{teamFlag(r.team)}</span>
<span className="truncate text-ink">{r.team}</span>
</span>
</td>
<td className="py-1.5 text-center tabular-nums text-muted">{r.played}</td>
<td className="py-1.5 text-center tabular-nums text-muted">
{r.gd > 0 ? `+${r.gd}` : r.gd}
</td>
<td className="py-1.5 text-center font-bold tabular-nums text-ink">{r.points}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}