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;
showSoccerTable?: boolean;
}) {
// # Team GP W L [OTL] [PTS] PCT GB L10 STK Mgr
// OTL and PTS are both shown for hockey (showOtLosses = true).
// Soccer table mode shows D/GF/GA/GD/Pts instead of PCT/GB/form columns.
const totalCols = showSoccerTable ? 11 : 10 + (showOtLosses ? 2 : 0);
const allRows = sections.flatMap((s) => s.rows);
const hasStreak = allRows.some((r) => r.streak != null);
const hasLastTen = allRows.some((r) => r.lastTen != null);
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 (
<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>
<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">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>
{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>
@ -258,10 +264,10 @@ function StandingsTable({
{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">PTS</th>}
{!showSoccerTable && <th className="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 && <th className="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 && <th className="hidden sm:table-cell text-right py-1.5 px-2 w-12">PCT</th>}
{!showSoccerTable && hasGB && <th className="hidden sm:table-cell text-right py-1.5 px-2 w-10">GB</th>}
{!showSoccerTable && hasLastTen && <th className="hidden sm:table-cell text-right py-1.5 px-2 w-12">L10</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>
</tr>
</thead>
@ -308,16 +314,19 @@ function StandingsTable({
const isUserTeam = userParticipantIds.includes(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(
<tr
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"
}`}
>
<td className="py-2 pr-2 text-muted-foreground tabular-nums">{rank}</td>
<td className="py-2">
<span className={`font-medium ${isUserTeam ? "text-primary" : ""}`}>
<td className="py-2 min-w-0">
<span className={`font-medium truncate ${isUserTeam ? "text-primary" : ""}`}>
{row.participant.shortName ?? row.participant.name}
</span>
{section.showDivisionLabel && row.division && (
@ -326,7 +335,7 @@ function StandingsTable({
</span>
)}
</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}
</td>
<td className="py-2 px-2 text-right tabular-nums font-medium">{row.wins}</td>
@ -355,21 +364,21 @@ function StandingsTable({
</td>
)}
{!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)}
</td>
)}
{!showSoccerTable && (
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground">
{!showSoccerTable && hasGB && (
<td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
{formatGB(row.gamesBack)}
</td>
)}
{!showSoccerTable && (
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground">
{!showSoccerTable && hasLastTen && (
<td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
{row.lastTen ?? "—"}
</td>
)}
{!showSoccerTable && (
{!showSoccerTable && hasStreak && (
<td className="py-2 px-2 text-right tabular-nums">
{row.streak ? (
<span
@ -401,6 +410,43 @@ function StandingsTable({
</td>
</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;

View file

@ -109,6 +109,38 @@ describe("RegularSeasonStandings", () => {
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", () => {
render(

View file

@ -20,7 +20,7 @@ const SAMPLE_MLB_RESPONSE = {
divisionRank: "1",
leagueRank: "1",
gamesBack: "-",
streak: { streakCode: "W", streakNumber: 3 },
streak: { streakCode: "W3", streakNumber: 3 },
records: {
splitRecords: [
{ type: "home", wins: 24, losses: 15 },
@ -43,7 +43,7 @@ const SAMPLE_MLB_RESPONSE = {
divisionRank: "2",
leagueRank: "4",
gamesBack: "5.0",
streak: { streakCode: "L", streakNumber: 2 },
streak: { streakCode: "L2", streakNumber: 2 },
records: {
splitRecords: [
{ type: "home", wins: 22, losses: 18 },
@ -71,7 +71,7 @@ const SAMPLE_MLB_RESPONSE = {
divisionRank: "1",
leagueRank: "1",
gamesBack: "-",
streak: { streakCode: "W", streakNumber: 1 },
streak: { streakCode: "W1", streakNumber: 1 },
records: {
splitRecords: [
{ 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
const conferenceRank = conferenceRankMap.get(team.team.id) ?? null;
// Streak: e.g. "W3" or "L2"
const streak = team.streak
? `${team.streak.streakCode}${team.streak.streakNumber}`
: undefined;
// Streak: streakCode from the MLB API already contains the full string e.g. "W3" or "L2"
const streak = team.streak?.streakCode ?? undefined;
// Split records
const splits = team.records?.splitRecords ?? [];