Adds live standings sync and display for bracket-based sports (NBA/NHL), so league members can see W/L tables and which teams their opponents drafted during the regular season — not just after the playoff bracket is set. - New `regular_season_standings` table with upsert-on-conflict sync - Standings sync service with NHL (api-web.nhle.com) and NBA (ESPN) adapters, externalId write-back for future syncs, and unmatched-team resolution UI - `RegularSeasonStandings` component: flat (NBA) + division/wild-card (NHL) modes, playoff line, TeamOwnerBadge, projected Brackt points (EV), mobile horizontal scroll - Admin "Sync Standings" card + "Resolve Unmatched" UI on sports season page - Admin manual standings edit hatch at /admin/sports-seasons/:id/regular-standings - Show standings above bracket until matches exist; below once bracket is set - `normalize-team-name` utility extracted to shared lib Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
import type { FetchedStandingsRecord, StandingsSyncAdapter } from "./types";
|
|
|
|
const NHL_STANDINGS_URL = "https://api-web.nhle.com/v1/standings/now";
|
|
|
|
interface NhlTeamRecord {
|
|
teamName: { default: string };
|
|
teamAbbrev: { default: string };
|
|
teamLogo?: string;
|
|
conferenceName: string;
|
|
divisionName: string;
|
|
gamesPlayed: number;
|
|
wins: number;
|
|
losses: number;
|
|
otLosses: number;
|
|
points: number;
|
|
pointPctg: number;
|
|
winPctg: number;
|
|
l10Wins: number;
|
|
l10Losses: number;
|
|
l10OtLosses: number;
|
|
streakCode: string; // "W" or "L"
|
|
streakCount: number;
|
|
homeWins: number;
|
|
homeLosses: number;
|
|
homeOtLosses: number;
|
|
roadWins: number;
|
|
roadLosses: number;
|
|
roadOtLosses: number;
|
|
conferenceSequence: number;
|
|
divisionSequence: number;
|
|
leagueSequence: number;
|
|
teamId?: number;
|
|
}
|
|
|
|
interface NhlStandingsResponse {
|
|
standings: NhlTeamRecord[];
|
|
}
|
|
|
|
export class NhlStandingsAdapter implements StandingsSyncAdapter {
|
|
async fetchStandings(): Promise<FetchedStandingsRecord[]> {
|
|
const response = await fetch(NHL_STANDINGS_URL);
|
|
if (!response.ok) {
|
|
throw new Error(`NHL standings API returned ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const json = (await response.json()) as NhlStandingsResponse;
|
|
|
|
if (!json.standings || !Array.isArray(json.standings)) {
|
|
throw new Error("Unexpected NHL standings API response shape");
|
|
}
|
|
|
|
return json.standings.map((team, index): FetchedStandingsRecord => {
|
|
const homeRecord = `${team.homeWins}-${team.homeLosses}-${team.homeOtLosses}`;
|
|
const awayRecord = `${team.roadWins}-${team.roadLosses}-${team.roadOtLosses}`;
|
|
const streak = `${team.streakCode}${team.streakCount}`;
|
|
const lastTen = `${team.l10Wins}-${team.l10Losses}-${team.l10OtLosses}`;
|
|
|
|
return {
|
|
teamName: team.teamName.default,
|
|
externalTeamId: team.teamAbbrev?.default ?? String(index),
|
|
wins: team.wins,
|
|
losses: team.losses,
|
|
otLosses: team.otLosses,
|
|
winPct: team.winPctg ?? 0,
|
|
gamesPlayed: team.gamesPlayed,
|
|
conference: team.conferenceName,
|
|
division: team.divisionName,
|
|
conferenceRank: team.conferenceSequence,
|
|
divisionRank: team.divisionSequence,
|
|
leagueRank: team.leagueSequence,
|
|
streak,
|
|
lastTen,
|
|
homeRecord,
|
|
awayRecord,
|
|
};
|
|
});
|
|
}
|
|
}
|