import { useEffect, useMemo, useState } from 'react'; import { Link, useParams } from '@tanstack/react-router'; import { ArrowLeft, Bell, BellRing, CalendarPlus, Route, Shield, Star, Stethoscope } from 'lucide-react'; import { EloHistoryChart } from '@/components/EloHistoryChart'; import { pushSupported, serverPushKey, subscribeTeam, unsubscribePush } from '@/lib/push'; import { useFavoriteStore } from '@/stores/favoriteStore'; import { useTournamentStore } from '@/stores/tournamentStore'; import { Card, CardBody, CardHeader } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { teamFlag } from '@/lib/teams'; import { fmt, useFormat, useT } from '@/lib/i18n'; import { hostAdvantage } from '@/lib/model/hosts'; import { knockoutAdvanceProb, predictMatch, type RatingsModel } from '@/lib/model/predict'; import { modelBracket, projectedSeeds, teamPath } from '@/lib/whatif'; import type { Fixture, TeamProfile } from '@/lib/types'; const pct = (x: number | null) => (x == null ? '—' : `${(x * 100).toFixed(1)}%`); /** The model's most likely knockout run for this team, opponent by opponent. */ function RoadToFinal({ team }: { team: string }) { const t = useT(); const snapshot = useTournamentStore((s) => s.snapshot); const model = useTournamentStore((s) => s.model); const [ratings, setRatings] = useState(null); useEffect(() => { let alive = true; fetch('/data/ratings.json') .then((r) => (r.ok ? r.json() : Promise.reject())) .then((d: RatingsModel) => alive && setRatings(d)) .catch(() => {}); return () => { alive = false; }; }, []); const path = useMemo(() => { if (!snapshot || !model || !ratings) return null; const fixtures = snapshot.fixtures; const seeds = projectedSeeds(fixtures, model.odds); // Every team gets a road: if the model doesn't project this side out of // the group, slot them hypothetically as their group's runner-up. if (!Object.values(seeds).includes(team)) { const g = fixtures.find((f) => f.stage === 'group' && (f.home.team === team || f.away.team === team))?.group; if (!g) return null; seeds[`2${g}`] = team; } const advFor = (f: Fixture, home: string, away: string): number | null => { const adv = hostAdvantage(home, away, f.venue, ratings.params.homeAdvElo); const p = predictMatch(home, away, ratings, adv); return knockoutAdvanceProb(p.probs, p.eloHome, p.eloAway); }; return teamPath(fixtures, seeds, modelBracket(fixtures, seeds, advFor), team); }, [snapshot, model, ratings, team]); const odds = model?.odds.find((o) => o.team === team); if (!path?.length || !odds) return null; const reach: Partial> = { r32: odds.qualify, r16: odds.reachR16, qf: odds.reachQF, sf: odds.reachSF, final: odds.reachFinal, }; return ( {t.team.road.title}
    {path.map((p) => (
  • {t.stage[p.stage]} {t.common.vs} {p.opponent ? ( <> {teamFlag(p.opponent)} {p.opponent} ) : ( {t.team.road.tbd} )} {pct(reach[p.stage] ?? null)}
  • ))}
{fmt(t.team.road.winItAll, { pct: pct(odds.champion) })}

{t.team.road.note}

); } function FixtureRow({ f, team }: { f: Fixture; team: string }) { const { t, kickoffDay, kickoffTime } = useFormat(); const isHome = f.home.team === team; const opp = isHome ? f.away : f.home; const done = f.status === 'finished' || f.status === 'live'; const us = isHome ? f.homeScore : f.awayScore; const them = isHome ? f.awayScore : f.homeScore; const res = done && us != null && them != null ? (us > them ? 'win' : us < them ? 'loss' : 'draw') : null; return ( {f.group ? fmt(t.team.groupShort, { group: f.group }) : f.stage !== 'group' ? t.team.stageShort[f.stage] : f.round} {t.common.vs} {opp.team ? teamFlag(opp.team) : } {opp.label} {isHome ? t.team.homeShort : t.team.awayShort} {done && us != null ? {us}–{them} : `${kickoffDay(f.kickoff)} ${kickoffTime(f.kickoff)}`} {res && {t.team.resultShort[res]}} ); } function FavoriteStar({ team }: { team: string }) { const t = useT(); const favorite = useFavoriteStore((s) => s.favorite); const toggle = useFavoriteStore((s) => s.toggleFavorite); const active = favorite === team; return ( ); } const PUSH_TEAM_KEY = 'cup26:push-team'; /** Goal/kickoff alerts for this team — shown only when the server has push * enabled and the browser supports it. One followed team per device. */ function NotifyBell({ team }: { team: string }) { const t = useT(); const [avail, setAvail] = useState(false); const [denied, setDenied] = useState(false); const [subTeam, setSubTeam] = useState(() => { try { return localStorage.getItem(PUSH_TEAM_KEY); } catch { return null; } }); useEffect(() => { let alive = true; void serverPushKey().then((k) => alive && setAvail(Boolean(k) && pushSupported())); return () => { alive = false; }; }, []); if (!avail) return null; const active = subTeam === team; const toggle = async (): Promise => { if (active) { await unsubscribePush(); try { localStorage.removeItem(PUSH_TEAM_KEY); } catch { /* ok */ } setSubTeam(null); return; } const res = await subscribeTeam(team); if (res === 'subscribed') { try { localStorage.setItem(PUSH_TEAM_KEY, team); } catch { /* ok */ } setSubTeam(team); setDenied(false); } else if (res === 'denied') { setDenied(true); } }; return ( {denied && {t.team.notifyDenied}} ); } export function TeamProfilePage() { const { name } = useParams({ strict: false }) as { name: string }; const t = useT(); const [profile, setProfile] = useState(null); const [failed, setFailed] = useState(false); useEffect(() => { let alive = true; setProfile(null); setFailed(false); fetch(`/api/team/${encodeURIComponent(name)}`) .then((r) => (r.ok ? r.json() : Promise.reject())) .then((d: TeamProfile) => alive && setProfile(d)) .catch(() => alive && setFailed(true)); return () => { alive = false; }; }, [name]); if (failed) return
{t.team.unknown} {t.team.allTeams}
; if (!profile) return
; const s = profile.standing; return (
{t.team.allTeams} {teamFlag(profile.team)}

{profile.team}

{profile.group && {fmt(t.common.group, { group: profile.group })}} {fmt(t.team.eloBadge, { rating: profile.elo })} {fmt(t.team.titleOddsBadge, { pct: pct(profile.championOdds) })} {fmt(t.team.advanceOddsBadge, { pct: pct(profile.qualifyOdds) })} {t.team.addToCalendar}
{s && (
{t.team.groupStanding}
{fmt(t.team.standingLine, { rank: s.rank, points: s.points, won: s.won, drawn: s.drawn, lost: s.lost, gd: s.gd > 0 ? `+${s.gd}` : s.gd })}
)}
{t.team.eloHistory}
{t.team.fixtures} {profile.fixtures.map((f) => )}
{profile.injuries.length > 0 && ( {t.team.injuriesTitle}
    {profile.injuries.map((i) => (
  • {i.player} {i.reason ?? i.status ?? ''}
  • ))}
)} {profile.discipline.length > 0 && ( {t.team.discipline}
    {profile.discipline.map((p) => (
  • {p.player} {Array.from({ length: p.yellows }).map((_, i) => )} {Array.from({ length: p.reds }).map((_, i) => )} {p.suspendedNext && ( {p.suspendedFor != null ? fmt(t.team.suspChipFor, { num: p.suspendedFor }) : t.team.suspChipNext} )} {!p.suspendedNext && p.pending === 1 && ( {t.team.atRiskChip} )}
  • ))}
)} {t.team.allTimeTopScorers} {profile.keyPlayers.length ? (
    {profile.keyPlayers.map((p, i) => (
  1. {i + 1} {p.name} {p.goals} {p.pens ? {fmt(t.common.pensShort, { n: p.pens })} : null}
  2. ))}
) :

{t.common.noData}

}
); }