Phase 0: scaffold cup26 — Vite+React+Fastify TS PWA, broadcast theme, app shell

Mirrors the TTRPG stack (strict TS, Tailwind 4, vite-plugin-pwa, esbuild server
bundle, multi-stage Dockerfile, Traefik compose). Floodlit-pitch design tokens,
UI primitives (Button/Card/Badge), TanStack Router shell with Live/Groups/
Bracket/Predict/Story routes (placeholders), dependency-free PWA icon generator.
Raw data staged: openfootball 2026 fixtures + martj42 international results CSV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:49:13 +02:00
commit 4e4e75a1d8
39 changed files with 53029 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
import { Link, Outlet } from '@tanstack/react-router';
import { Moon, Sun, Trophy } from 'lucide-react';
import { useUiStore } from '@/stores/uiStore';
import { cn } from '@/lib/cn';
const NAV = [
{ to: '/', label: 'Live', exact: true },
{ to: '/groups', label: 'Groups', exact: false },
{ to: '/bracket', label: 'Bracket', exact: false },
{ to: '/predict', label: 'Predict', exact: false },
{ to: '/story', label: 'Story', exact: false },
] as const;
function ThemeToggle() {
const theme = useUiStore((s) => s.theme);
const toggle = useUiStore((s) => s.toggleTheme);
return (
<button
type="button"
onClick={toggle}
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
className="grid h-9 w-9 place-items-center rounded-md text-muted hover:bg-elevated hover:text-ink transition-colors"
>
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
</button>
);
}
export function RootLayout() {
return (
<div className="flex min-h-full flex-col">
<header className="sticky top-0 z-30 border-b border-line bg-surface/85 backdrop-blur">
<div className="mx-auto flex h-14 max-w-6xl items-center gap-4 px-4">
<Link to="/" className="flex items-center gap-2 font-display text-lg font-extrabold tracking-tight text-ink">
<span className="grid h-7 w-7 place-items-center rounded-md bg-gradient-to-b from-accent-soft to-accent text-accent-ink">
<Trophy size={16} />
</span>
Cup<span className="text-accent">26</span>
</Link>
<nav className="flex items-center gap-0.5 overflow-x-auto">
{NAV.map((item) => (
<Link
key={item.to}
to={item.to}
{...(item.exact ? { activeOptions: { exact: true } } : {})}
className={cn(
'rounded-md px-3 py-1.5 text-sm font-medium text-muted transition-colors hover:bg-elevated hover:text-ink',
'[&.active]:bg-accent-glow [&.active]:text-accent',
)}
>
{item.label}
</Link>
))}
</nav>
<div className="ml-auto flex items-center gap-2">
<ThemeToggle />
</div>
</div>
</header>
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
<Outlet />
</main>
<footer className="border-t border-line py-6 text-center text-xs text-faint">
<p>
Cup26 · World Cup 2026 · data from openfootball, football-data.org &amp; StatsBomb open data ·
model-driven odds, not betting advice
</p>
</footer>
</div>
);
}