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:
@@ -124,11 +124,19 @@ export function buildServer() {
|
||||
const num = Number((req.params as { num: string }).num);
|
||||
if (!Number.isFinite(num)) return reply.code(400).send({ error: 'bad num' });
|
||||
interface FmShot { teamId?: number; playerName?: string; x?: number; y?: number; min?: number; expectedGoals?: number; expectedGoalsOnTarget?: number; eventType?: string; situation?: string }
|
||||
interface FmLineupPlayer {
|
||||
name?: string;
|
||||
shirtNumber?: string | number;
|
||||
verticalLayout?: { x?: number; y?: number };
|
||||
performance?: { rating?: number; substitutionEvents?: { time?: number; type?: string }[]; events?: { type?: string }[] };
|
||||
}
|
||||
interface FmLineupTeam { formation?: string; coach?: { name?: string }; starters?: FmLineupPlayer[]; subs?: FmLineupPlayer[] }
|
||||
interface FmDetails {
|
||||
homeTeamId?: number | null;
|
||||
shots?: FmShot[];
|
||||
momentum?: { main?: { data?: { minute: number; value: number }[] } } | null;
|
||||
matchFacts?: { playerOfTheMatch?: { name?: { fullName?: string }; rating?: { num?: string }; teamName?: string } } | null;
|
||||
lineup?: { homeTeam?: FmLineupTeam; awayTeam?: FmLineupTeam } | null;
|
||||
}
|
||||
const fm = getMatchProvider<FmDetails>(num, 'fotmob');
|
||||
const fifaInfo = getMatchProvider<{ attendance: number | null; officials: { Name?: { Description?: string }[]; TypeLocalized?: { Description?: string }[] }[]; stadium: string }>(num, 'fifa-info');
|
||||
@@ -155,10 +163,40 @@ export function buildServer() {
|
||||
{ home: 0, away: 0 },
|
||||
);
|
||||
const potmRaw = fm?.data.matchFacts?.playerOfTheMatch;
|
||||
|
||||
const subTime = (p: FmLineupPlayer, type: string): number | null => {
|
||||
const e = p.performance?.substitutionEvents?.find((s) => s.type === type);
|
||||
return typeof e?.time === 'number' ? e.time : null;
|
||||
};
|
||||
const trimPlayer = (p: FmLineupPlayer) => ({
|
||||
name: p.name ?? '',
|
||||
num: p.shirtNumber != null ? String(p.shirtNumber) : null,
|
||||
x: typeof p.verticalLayout?.x === 'number' ? p.verticalLayout.x : null,
|
||||
y: typeof p.verticalLayout?.y === 'number' ? p.verticalLayout.y : null,
|
||||
rating: typeof p.performance?.rating === 'number' ? p.performance.rating : null,
|
||||
goals: p.performance?.events?.filter((e) => e?.type === 'goal').length ?? 0,
|
||||
subOut: subTime(p, 'subOut'),
|
||||
subIn: subTime(p, 'subIn'),
|
||||
});
|
||||
// Only a complete, fully-positioned XI is worth drawing as a pitch.
|
||||
const trimTeam = (tm?: FmLineupTeam) => {
|
||||
const starters = (tm?.starters ?? []).map(trimPlayer);
|
||||
if (starters.length !== 11 || starters.some((p) => p.x == null || p.y == null || !p.name)) return null;
|
||||
return {
|
||||
formation: tm?.formation ?? null,
|
||||
coach: tm?.coach?.name ?? null,
|
||||
starters,
|
||||
subs: (tm?.subs ?? []).map(trimPlayer),
|
||||
};
|
||||
};
|
||||
const lineupHome = trimTeam(fm?.data.lineup?.homeTeam);
|
||||
const lineupAway = trimTeam(fm?.data.lineup?.awayTeam);
|
||||
|
||||
return {
|
||||
shots,
|
||||
xg: shots.length ? { home: +xg.home.toFixed(2), away: +xg.away.toFixed(2) } : null,
|
||||
momentum: fm?.data.momentum?.main?.data ?? [],
|
||||
lineup: lineupHome && lineupAway ? { home: lineupHome, away: lineupAway } : null,
|
||||
potm: potmRaw?.name?.fullName ? { name: potmRaw.name.fullName, rating: potmRaw.rating?.num ?? null } : null,
|
||||
info: fifaInfo
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user