diff --git a/src/components/GroupTable.tsx b/src/components/GroupTable.tsx index a22ac2b..c02c9ba 100644 --- a/src/components/GroupTable.tsx +++ b/src/components/GroupTable.tsx @@ -1,3 +1,4 @@ +import { Link } from '@tanstack/react-router'; import { cn } from '@/lib/cn'; import { teamFlag } from '@/lib/teams'; import type { StandingRow } from '@/lib/types'; @@ -30,7 +31,7 @@ export function GroupTable({ group, rows }: { group: string; rows: StandingRow[] )} > - + {teamFlag(r.team)} {r.team} - + {r.played} diff --git a/src/components/MatchCard.tsx b/src/components/MatchCard.tsx index 8fa4c66..7a9161b 100644 --- a/src/components/MatchCard.tsx +++ b/src/components/MatchCard.tsx @@ -1,9 +1,28 @@ import { Link } from '@tanstack/react-router'; import { cn } from '@/lib/cn'; +import { teamFlag } from '@/lib/teams'; import type { Fixture } from '@/lib/types'; import { kickoffTime, relativeKickoff } from '@/lib/format'; +import { useTournamentStore } from '@/stores/tournamentStore'; import { TeamLabel } from './TeamLabel'; +/** The model's pre-match lean for scheduled matches — the "expectation" hint. */ +function ModelHint({ num }: { num: number }) { + const pred = useTournamentStore((s) => s.model?.matches.find((m) => m.num === num)); + if (!pred) return null; + const { home, draw, away } = pred.probs; + const drawLikely = draw >= home && draw >= away; + const homeFav = home >= away; + const side = drawLikely ? null : homeFav ? pred.home : pred.away; + const p = drawLikely ? draw : Math.max(home, away); + return ( +
+ model: {side ? <>{teamFlag(side)} {side} : 'draw '} + {Math.round(p * 100)}% +
+ ); +} + const STAGE_LABEL: Record = { group: 'Group', r32: 'Round of 32', @@ -62,6 +81,7 @@ export function MatchCard({ f }: { f: Fixture }) { + {f.status === 'scheduled' && }
{f.venue}
); diff --git a/src/components/OddsTable.tsx b/src/components/OddsTable.tsx index 32dea25..4ca85de 100644 --- a/src/components/OddsTable.tsx +++ b/src/components/OddsTable.tsx @@ -1,3 +1,4 @@ +import { Link } from '@tanstack/react-router'; import { teamFlag } from '@/lib/teams'; import { cn } from '@/lib/cn'; import type { TeamOdds } from '@/lib/types'; @@ -42,10 +43,10 @@ export function OddsTable({ odds }: { odds: TeamOdds[] }) { {odds.map((o) => ( - + {teamFlag(o.team)} {o.team} - + {cols.map((c) => ( diff --git a/src/features/bracket/BracketPage.tsx b/src/features/bracket/BracketPage.tsx index 3e87354..7adff9e 100644 --- a/src/features/bracket/BracketPage.tsx +++ b/src/features/bracket/BracketPage.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { Link } from '@tanstack/react-router'; import { PageHeader } from '@/components/ui/PageHeader'; import { useTournamentStore } from '@/stores/tournamentStore'; import { teamFlag } from '@/lib/teams'; @@ -51,7 +52,7 @@ function BracketMatch({ f, probs }: { f: Fixture; probs?: MatchProbs | undefined const decided = f.status === 'finished' && f.homeScore !== null && f.awayScore !== null; const showProbs = !!probs && !!f.home.team && !!f.away.team && f.status !== 'finished'; return ( -
+ f.awayScore!} />
f.homeScore!} /> @@ -59,7 +60,7 @@ function BracketMatch({ f, probs }: { f: Fixture; probs?: MatchProbs | undefined
M{f.num} · {f.status === 'finished' ? 'FT' : `${kickoffDay(f.kickoff)} ${kickoffTime(f.kickoff)}`}
-
+ ); } diff --git a/src/features/groups/GroupsPage.tsx b/src/features/groups/GroupsPage.tsx index 6391c88..f1e901e 100644 --- a/src/features/groups/GroupsPage.tsx +++ b/src/features/groups/GroupsPage.tsx @@ -1,3 +1,5 @@ +import { Link } from '@tanstack/react-router'; +import { Users } from 'lucide-react'; import { PageHeader } from '@/components/ui/PageHeader'; import { GroupTable } from '@/components/GroupTable'; import { useTournamentStore } from '@/stores/tournamentStore'; @@ -12,6 +14,11 @@ export function GroupsPage() { + All 48 teams + + } /> {groups.length ? (
diff --git a/src/features/predictions/PredictionsPage.tsx b/src/features/predictions/PredictionsPage.tsx index 592c72d..4a2797d 100644 --- a/src/features/predictions/PredictionsPage.tsx +++ b/src/features/predictions/PredictionsPage.tsx @@ -1,5 +1,6 @@ import { lazy, Suspense, useMemo } from 'react'; -import { TrendingUp, Trophy } from 'lucide-react'; +import { Link } from '@tanstack/react-router'; +import { Gauge, TrendingUp, Trophy } from 'lucide-react'; import { PageHeader } from '@/components/ui/PageHeader'; import { Card, CardBody, CardHeader } from '@/components/ui/Card'; import { OddsTable } from '@/components/OddsTable'; @@ -64,6 +65,11 @@ export function PredictionsPage() { + How & how accurate + + } />
diff --git a/src/features/team/TeamProfilePage.tsx b/src/features/team/TeamProfilePage.tsx index 2d0053e..4a1de9c 100644 --- a/src/features/team/TeamProfilePage.tsx +++ b/src/features/team/TeamProfilePage.tsx @@ -52,8 +52,8 @@ export function TeamProfilePage() { const s = profile.standing; return (
- - Groups + + All teams diff --git a/src/features/teams/TeamsPage.tsx b/src/features/teams/TeamsPage.tsx new file mode 100644 index 0000000..d600a96 --- /dev/null +++ b/src/features/teams/TeamsPage.tsx @@ -0,0 +1,51 @@ +import { useMemo } from 'react'; +import { Link } from '@tanstack/react-router'; +import { PageHeader } from '@/components/ui/PageHeader'; +import { teamFlag } from '@/lib/teams'; +import { useTournamentStore } from '@/stores/tournamentStore'; + +const pct = (x: number) => (x >= 0.995 ? '>99%' : x < 0.005 ? '—' : `${(x * 100).toFixed(x < 0.1 ? 1 : 0)}%`); + +export function TeamsPage() { + const model = useTournamentStore((s) => s.model); + const snapshot = useTournamentStore((s) => s.snapshot); + + const teams = useMemo(() => { + const groupOf = new Map(); + for (const [g, rows] of Object.entries(snapshot?.tables ?? {})) for (const r of rows) groupOf.set(r.team, g); + return (model?.odds ?? []).map((o) => ({ ...o, group: groupOf.get(o.team) ?? null })); + }, [model, snapshot]); + + return ( +
+ + {teams.length ? ( +
+ {teams.map((t, i) => ( + + {i + 1} + {teamFlag(t.team)} +
+
{t.team}
+
{t.group ? `Group ${t.group}` : ''}
+
+
+
{pct(t.champion)}
+
advance {pct(t.qualify)}
+
+ + ))} +
+ ) : ( +
+ {Array.from({ length: 9 }).map((_, i) =>
)} +
+ )} +
+ ); +} diff --git a/src/router.tsx b/src/router.tsx index 76aacae..6f677c1 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -9,6 +9,7 @@ import { StoryPage } from './features/story/StoryPage'; import { MatchPreviewPage } from './features/match/MatchPreviewPage'; import { TeamProfilePage } from './features/team/TeamProfilePage'; import { MethodologyPage } from './features/methodology/MethodologyPage'; +import { TeamsPage } from './features/teams/TeamsPage'; const rootRoute = createRootRoute({ component: RootLayout, @@ -23,8 +24,9 @@ const storyRoute = createRoute({ getParentRoute: () => rootRoute, path: '/story' const matchRoute = createRoute({ getParentRoute: () => rootRoute, path: '/match/$num', component: MatchPreviewPage }); const teamRoute = createRoute({ getParentRoute: () => rootRoute, path: '/team/$name', component: TeamProfilePage }); const methodologyRoute = createRoute({ getParentRoute: () => rootRoute, path: '/methodology', component: MethodologyPage }); +const teamsRoute = createRoute({ getParentRoute: () => rootRoute, path: '/teams', component: TeamsPage }); -const routeTree = rootRoute.addChildren([indexRoute, groupsRoute, bracketRoute, predictRoute, storyRoute, matchRoute, teamRoute, methodologyRoute]); +const routeTree = rootRoute.addChildren([indexRoute, groupsRoute, bracketRoute, predictRoute, storyRoute, matchRoute, teamRoute, methodologyRoute, teamsRoute]); export const router = createRouter({ routeTree, defaultPreload: 'intent' });