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
+1
View File
@@ -129,6 +129,7 @@ export const de: Dict = {
penalty: "Elfmeter",
assist: "Vorlage · {name}",
noGoalsYet: "Noch keine Tore.",
subbedOff: "Ausgewechselt",
chips: {
att: "Zusch.",
ref: "SR",
+1
View File
@@ -128,6 +128,7 @@ export const en = {
penalty: "Penalty",
assist: "Assist · {name}",
noGoalsYet: "No goals yet.",
subbedOff: "Subbed off",
chips: {
att: "Att",
ref: "Ref",
+24
View File
@@ -181,11 +181,35 @@ export interface RichShot {
situation: string;
}
/** One player in a FotMob lineup. x/y are normalized vertical-pitch
* coordinates (0..1 across · 0..1 own-goal-line → halfway); bench players
* have no coordinates. subIn/subOut are substitution minutes. */
export interface PitchPlayer {
name: string;
num: string | null;
x: number | null;
y: number | null;
rating: number | null;
goals: number;
subOut: number | null;
subIn: number | null;
}
export interface LineupTeam {
formation: string | null;
coach: string | null;
starters: PitchPlayer[];
subs: PitchPlayer[];
}
export interface MatchLineup { home: LineupTeam; away: LineupTeam }
export interface MatchRich {
shots: RichShot[];
xg: { home: number; away: number } | null;
/** Attack-momentum curve; positive values = home pressure. */
momentum: { minute: number; value: number }[];
lineup: MatchLineup | null;
potm: { name: string; rating: string | null } | null;
info: { attendance: number | null; stadium: string; referee: string | null } | null;
weather: { tempC: number | null; precipProbPct: number | null; windKmh: number | null; elevationM: number; tz: string } | null;