Fix MLB streak doubling bug, hide empty standings columns, improve mobile layout

- mlb.ts: use streakCode alone (the API already returns the full string like
  "W3"); appending streakNumber was doubling the digit, causing "L33" display
- Update mlb.test.ts mocks to match real API format (streakCode "W3" not "W")
- RegularSeasonStandings: conditionally hide GB/L10/STK columns when no rows
  have data, so pre-season or stats-free sports don't show blank columns
- Mobile two-row layout: GP/PCT/GB/L10 move to a secondary sub-row (sm:hidden)
  so all data stays visible without horizontal scroll on small screens; STK and
  W/L remain on the primary row; reduce min-w from 740px to 360px

https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF
This commit is contained in:
Claude 2026-05-12 09:22:18 +00:00
parent 6c3065f6f2
commit cee9469f48
No known key found for this signature in database
4 changed files with 103 additions and 27 deletions

View file

@ -236,19 +236,25 @@ function StandingsTable({
showOtLosses: boolean; showOtLosses: boolean;
showSoccerTable?: boolean; showSoccerTable?: boolean;
}) { }) {
// # Team GP W L [OTL] [PTS] PCT GB L10 STK Mgr const allRows = sections.flatMap((s) => s.rows);
// OTL and PTS are both shown for hockey (showOtLosses = true). const hasStreak = allRows.some((r) => r.streak != null);
// Soccer table mode shows D/GF/GA/GD/Pts instead of PCT/GB/form columns. const hasLastTen = allRows.some((r) => r.lastTen != null);
const totalCols = showSoccerTable ? 11 : 10 + (showOtLosses ? 2 : 0); const hasGB = allRows.some((r) => r.gamesBack != null);
// # Team GP W [D] L [GF GA GD PTS] [OTL PTS] [PCT] [GB] [L10] [STK] Mgr
// Soccer: 11 fixed columns. Non-soccer: 5 base + GP + optional cols.
const totalCols = showSoccerTable
? 11
: 5 + 1 /* GP */ + (showOtLosses ? 2 : 0) + 1 /* PCT */ + (hasGB ? 1 : 0) + (hasLastTen ? 1 : 0) + (hasStreak ? 1 : 0);
return ( return (
<div className="overflow-x-auto -mx-6 px-6"> <div className="overflow-x-auto -mx-6 px-6">
<table className="w-full min-w-[740px] text-sm"> <table className="w-full min-w-[360px] text-sm">
<thead> <thead>
<tr className="text-xs text-muted-foreground uppercase tracking-wide border-b"> <tr className="text-xs text-muted-foreground uppercase tracking-wide border-b">
<th className="text-left py-1.5 pr-2 w-6">#</th> <th className="text-left py-1.5 pr-2 w-6">#</th>
<th className="text-left py-1.5">Team</th> <th className="text-left py-1.5">Team</th>
<th className="text-right py-1.5 px-2 w-10">GP</th> <th className="hidden sm:table-cell text-right py-1.5 px-2 w-10">GP</th>
<th className="text-right py-1.5 px-2 w-8">W</th> <th className="text-right py-1.5 px-2 w-8">W</th>
{showSoccerTable && <th className="text-right py-1.5 px-2 w-8">D</th>} {showSoccerTable && <th className="text-right py-1.5 px-2 w-8">D</th>}
<th className="text-right py-1.5 px-2 w-8">L</th> <th className="text-right py-1.5 px-2 w-8">L</th>
@ -258,10 +264,10 @@ function StandingsTable({
{showSoccerTable && <th className="text-right py-1.5 px-2 w-10">PTS</th>} {showSoccerTable && <th className="text-right py-1.5 px-2 w-10">PTS</th>}
{showOtLosses && <th className="text-right py-1.5 px-2 w-10">OTL</th>} {showOtLosses && <th className="text-right py-1.5 px-2 w-10">OTL</th>}
{showOtLosses && <th className="text-right py-1.5 px-2 w-10">PTS</th>} {showOtLosses && <th className="text-right py-1.5 px-2 w-10">PTS</th>}
{!showSoccerTable && <th className="text-right py-1.5 px-2 w-12">PCT</th>} {!showSoccerTable && <th className="hidden sm:table-cell text-right py-1.5 px-2 w-12">PCT</th>}
{!showSoccerTable && <th className="text-right py-1.5 px-2 w-10">GB</th>} {!showSoccerTable && hasGB && <th className="hidden sm:table-cell text-right py-1.5 px-2 w-10">GB</th>}
{!showSoccerTable && <th className="text-right py-1.5 px-2 w-12">L10</th>} {!showSoccerTable && hasLastTen && <th className="hidden sm:table-cell text-right py-1.5 px-2 w-12">L10</th>}
{!showSoccerTable && <th className="text-right py-1.5 px-2 w-12">STK</th>} {!showSoccerTable && hasStreak && <th className="text-right py-1.5 px-2 w-12">STK</th>}
<th className="text-right py-1.5 pl-4 w-40">Mgr</th> <th className="text-right py-1.5 pl-4 w-40">Mgr</th>
</tr> </tr>
</thead> </thead>
@ -308,16 +314,19 @@ function StandingsTable({
const isUserTeam = userParticipantIds.includes(row.participantId); const isUserTeam = userParticipantIds.includes(row.participantId);
const ownership = teamOwnerships[row.participantId]; const ownership = teamOwnerships[row.participantId];
// Non-soccer rows get a secondary sub-row on mobile showing GP/PCT/GB/L10
const hasSecondaryStats = !showSoccerTable;
sectionRows.push( sectionRows.push(
<tr <tr
key={row.id} key={row.id}
className={`border-b border-border/50 last:border-0 ${ className={`${hasSecondaryStats ? "" : "border-b border-border/50 last:border-0"} ${
isUserTeam ? "bg-primary/5" : "hover:bg-muted/30" isUserTeam ? "bg-primary/5" : "hover:bg-muted/30"
}`} }`}
> >
<td className="py-2 pr-2 text-muted-foreground tabular-nums">{rank}</td> <td className="py-2 pr-2 text-muted-foreground tabular-nums">{rank}</td>
<td className="py-2"> <td className="py-2 min-w-0">
<span className={`font-medium ${isUserTeam ? "text-primary" : ""}`}> <span className={`font-medium truncate ${isUserTeam ? "text-primary" : ""}`}>
{row.participant.shortName ?? row.participant.name} {row.participant.shortName ?? row.participant.name}
</span> </span>
{section.showDivisionLabel && row.division && ( {section.showDivisionLabel && row.division && (
@ -326,7 +335,7 @@ function StandingsTable({
</span> </span>
)} )}
</td> </td>
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground"> <td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
{row.gamesPlayed} {row.gamesPlayed}
</td> </td>
<td className="py-2 px-2 text-right tabular-nums font-medium">{row.wins}</td> <td className="py-2 px-2 text-right tabular-nums font-medium">{row.wins}</td>
@ -355,21 +364,21 @@ function StandingsTable({
</td> </td>
)} )}
{!showSoccerTable && ( {!showSoccerTable && (
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground"> <td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
{formatWinPct(row.winPct, row.wins, row.gamesPlayed)} {formatWinPct(row.winPct, row.wins, row.gamesPlayed)}
</td> </td>
)} )}
{!showSoccerTable && ( {!showSoccerTable && hasGB && (
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground"> <td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
{formatGB(row.gamesBack)} {formatGB(row.gamesBack)}
</td> </td>
)} )}
{!showSoccerTable && ( {!showSoccerTable && hasLastTen && (
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground"> <td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
{row.lastTen ?? "—"} {row.lastTen ?? "—"}
</td> </td>
)} )}
{!showSoccerTable && ( {!showSoccerTable && hasStreak && (
<td className="py-2 px-2 text-right tabular-nums"> <td className="py-2 px-2 text-right tabular-nums">
{row.streak ? ( {row.streak ? (
<span <span
@ -401,6 +410,43 @@ function StandingsTable({
</td> </td>
</tr> </tr>
); );
// Secondary stats sub-row for mobile (hidden on sm+)
if (hasSecondaryStats) {
sectionRows.push(
<tr
key={`${row.id}-sub`}
className={`sm:hidden border-b border-border/50 last:border-0 ${
isUserTeam ? "bg-primary/5" : "hover:bg-muted/30"
}`}
>
<td colSpan={totalCols} className="pb-2 pt-0 pr-2">
<div className="flex flex-wrap gap-x-4 gap-y-0.5 text-xs text-muted-foreground pl-4">
<span>
<span className="uppercase tracking-wide">GP</span>{" "}
<span className="tabular-nums text-foreground">{row.gamesPlayed}</span>
</span>
<span>
<span className="uppercase tracking-wide">PCT</span>{" "}
<span className="tabular-nums text-foreground">{formatWinPct(row.winPct, row.wins, row.gamesPlayed)}</span>
</span>
{hasGB && (
<span>
<span className="uppercase tracking-wide">GB</span>{" "}
<span className="tabular-nums text-foreground">{formatGB(row.gamesBack)}</span>
</span>
)}
{hasLastTen && (
<span>
<span className="uppercase tracking-wide">L10</span>{" "}
<span className="tabular-nums text-foreground">{row.lastTen ?? "—"}</span>
</span>
)}
</div>
</td>
</tr>
);
}
}); });
return sectionRows; return sectionRows;

View file

@ -109,6 +109,38 @@ describe("RegularSeasonStandings", () => {
expect(screen.queryByText("OTL")).not.toBeInTheDocument(); expect(screen.queryByText("OTL")).not.toBeInTheDocument();
}); });
it("hides STK column when no rows have streak data", () => {
render(
<RegularSeasonStandings
standings={[makeRow({ streak: null }), makeRow({ id: "row-2", participantId: "p-2", streak: null, participant: { id: "p-2", name: "Toronto Raptors" } })]}
teamOwnerships={NO_OWNERSHIPS}
userParticipantIds={NO_USER_IDS}
/>
);
expect(screen.queryByText("STK")).not.toBeInTheDocument();
});
it("shows STK column when at least one row has streak data", () => {
render(
<RegularSeasonStandings
standings={[makeRow({ streak: "W3" }), makeRow({ id: "row-2", participantId: "p-2", streak: null, participant: { id: "p-2", name: "Toronto Raptors" } })]}
teamOwnerships={NO_OWNERSHIPS}
userParticipantIds={NO_USER_IDS}
/>
);
expect(screen.getByText("STK")).toBeInTheDocument();
});
it("hides L10 column when no rows have lastTen data", () => {
render(
<RegularSeasonStandings
standings={[makeRow({ lastTen: null })]}
teamOwnerships={NO_OWNERSHIPS}
userParticipantIds={NO_USER_IDS}
/>
);
expect(screen.queryByText("L10")).not.toBeInTheDocument();
});
it("shows soccer table columns when showSoccerTable=true", () => { it("shows soccer table columns when showSoccerTable=true", () => {
render( render(

View file

@ -20,7 +20,7 @@ const SAMPLE_MLB_RESPONSE = {
divisionRank: "1", divisionRank: "1",
leagueRank: "1", leagueRank: "1",
gamesBack: "-", gamesBack: "-",
streak: { streakCode: "W", streakNumber: 3 }, streak: { streakCode: "W3", streakNumber: 3 },
records: { records: {
splitRecords: [ splitRecords: [
{ type: "home", wins: 24, losses: 15 }, { type: "home", wins: 24, losses: 15 },
@ -43,7 +43,7 @@ const SAMPLE_MLB_RESPONSE = {
divisionRank: "2", divisionRank: "2",
leagueRank: "4", leagueRank: "4",
gamesBack: "5.0", gamesBack: "5.0",
streak: { streakCode: "L", streakNumber: 2 }, streak: { streakCode: "L2", streakNumber: 2 },
records: { records: {
splitRecords: [ splitRecords: [
{ type: "home", wins: 22, losses: 18 }, { type: "home", wins: 22, losses: 18 },
@ -71,7 +71,7 @@ const SAMPLE_MLB_RESPONSE = {
divisionRank: "1", divisionRank: "1",
leagueRank: "1", leagueRank: "1",
gamesBack: "-", gamesBack: "-",
streak: { streakCode: "W", streakNumber: 1 }, streak: { streakCode: "W1", streakNumber: 1 },
records: { records: {
splitRecords: [ splitRecords: [
{ type: "home", wins: 22, losses: 17 }, { type: "home", wins: 22, losses: 17 },

View file

@ -115,10 +115,8 @@ export class MlbStandingsAdapter implements StandingsSyncAdapter {
// Conference rank: use the per-league rank computed above // Conference rank: use the per-league rank computed above
const conferenceRank = conferenceRankMap.get(team.team.id) ?? null; const conferenceRank = conferenceRankMap.get(team.team.id) ?? null;
// Streak: e.g. "W3" or "L2" // Streak: streakCode from the MLB API already contains the full string e.g. "W3" or "L2"
const streak = team.streak const streak = team.streak?.streakCode ?? undefined;
? `${team.streak.streakCode}${team.streak.streakNumber}`
: undefined;
// Split records // Split records
const splits = team.records?.splitRecords ?? []; const splits = team.records?.splitRecords ?? [];