Lineup pitch with live substitution tracking + polish pass

- LineupPitch: both XIs on a vertical pitch at FotMob's per-player
  formation coordinates — shirt numbers, live ratings, goal badges,
  formation + coach labels. Substitutes take over the outgoing player's
  slot (paired through ESPN's 'X replaces Y' commentary with a unique
  same-minute fallback — the sources romanize names differently), with
  a subbed-off strip below; the page's live refetch keeps it current
  through the match. Falls back to the list view (and hides predicted
  pre-kickoff XIs) when no confirmed lineup is captured.
- /api/match-rich now serves the trimmed FotMob lineup (only complete,
  fully-positioned XIs)
- Fix: vs-Market label box was too narrow — the probability bar covered
  the end of 'DraftKings'
- Timeline spine no longer runs past the kick-off/full-time pills

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 12:02:49 +02:00
parent 4faedd60c4
commit 09c543b009
8 changed files with 328 additions and 13 deletions
+23 -10
View File
@@ -11,6 +11,7 @@ import { XgRace } from '@/components/XgRace';
import { ShotMap } from '@/components/ShotMap';
import { MomentumStrip } from '@/components/MomentumStrip';
import { EventIcon, MatchTimeline } from '@/components/MatchTimeline';
import { LineupPitch } from '@/components/LineupPitch';
import { teamFlag } from '@/lib/teams';
import { fmt, useFormat, useT } from '@/lib/i18n';
import { goalEvents } from '@/lib/matchEvents';
@@ -418,16 +419,28 @@ export function MatchPreviewPage() {
{/* ── Lineups ── */}
{tab === 'lineups' && (
<div className="space-y-4">
<Card>
<CardHeader className="flex items-center gap-2"><Shirt size={16} className="text-accent" /><span className="font-display font-bold text-ink">{preview.lineups ? t.match.lineups : t.match.keyPlayers}</span></CardHeader>
<CardBody className="grid grid-cols-2 gap-6">
{preview.lineups ? (
<><LineupCol team={homeName} players={preview.lineups.home} /><LineupCol team={awayName} players={preview.lineups.away} /></>
) : (
<><ScorerList team={homeName} players={preview.keyPlayers.home} /><ScorerList team={awayName} players={preview.keyPlayers.away} /></>
)}
</CardBody>
</Card>
{/* FotMob serves predicted XIs before kickoff — only draw the pitch
once the match has started or ESPN confirms the lineups. */}
{rich?.lineup && (hasScore || preview.lineups) ? (
<Card>
<CardHeader className="flex items-center gap-2"><Shirt size={16} className="text-accent" /><span className="font-display font-bold text-ink">{t.match.lineups}</span></CardHeader>
<CardBody>
<LineupPitch lineup={rich.lineup} home={homeName} away={awayName} events={preview.events} />
<p className="mt-2 text-xs text-faint">{t.match.xgAttribution}</p>
</CardBody>
</Card>
) : (
<Card>
<CardHeader className="flex items-center gap-2"><Shirt size={16} className="text-accent" /><span className="font-display font-bold text-ink">{preview.lineups ? t.match.lineups : t.match.keyPlayers}</span></CardHeader>
<CardBody className="grid grid-cols-2 gap-6">
{preview.lineups ? (
<><LineupCol team={homeName} players={preview.lineups.home} /><LineupCol team={awayName} players={preview.lineups.away} /></>
) : (
<><ScorerList team={homeName} players={preview.keyPlayers.home} /><ScorerList team={awayName} players={preview.keyPlayers.away} /></>
)}
</CardBody>
</Card>
)}
<div className="grid items-start gap-4 md:grid-cols-2">
{(preview.form.home.length > 0 || preview.form.away.length > 0) && (
<Card>