Fix MLB streak doubling bug, hide empty standings columns, improve mobile layout (#413)

* 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

* Address code review: cleanup mlb streak type, fix soccer GP on mobile, hoist showSubRow

- mlb.ts: drop redundant `?? undefined` after optional chain; document that
  streakCode is the full string (e.g. "W3") and streakNumber is unused
- RegularSeasonStandings: hoist hasSecondaryStats → showSubRow to component
  level (it only depends on a prop, not on individual row data)
- Remove non-functional `truncate`/`min-w-0` from team name cell — truncation
  requires table-layout:fixed which we don't use; team names size naturally
- Soccer GP was hidden on mobile with no sub-row to surface it; GP now shows
  inline for soccer on all viewports, hidden only for non-soccer (which has
  the secondary sub-row)
- Add comment explaining totalCols counts hidden-on-mobile columns for colSpan
- Add missing test for GB column hiding when no rows have gamesBack data

https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-05-12 08:14:34 -07:00 committed by GitHub
parent 6c3065f6f2
commit 08e93e955a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 118 additions and 26 deletions

View file

@ -236,19 +236,31 @@ 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.
// GP and PCT are always counted even when hidden on mobile; colSpan must
// reflect all rendered columns regardless of CSS visibility.
const totalCols = showSoccerTable
? 11
: 5 + 1 /* GP */ + (showOtLosses ? 2 : 0) + 1 /* PCT */ + (hasGB ? 1 : 0) + (hasLastTen ? 1 : 0) + (hasStreak ? 1 : 0);
// Non-soccer rows get a secondary sub-row on mobile showing GP/PCT/GB/L10.
// Soccer tables show GP inline (no sub-row) since they already show many columns.
const showSubRow = !showSoccerTable;
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={`${showSubRow ? "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 +270,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>
@ -311,7 +323,7 @@ function StandingsTable({
sectionRows.push(
<tr
key={row.id}
className={`border-b border-border/50 last:border-0 ${
className={`${showSubRow ? "" : "border-b border-border/50 last:border-0"} ${
isUserTeam ? "bg-primary/5" : "hover:bg-muted/30"
}`}
>
@ -326,7 +338,7 @@ function StandingsTable({
</span>
)}
</td>
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground">
<td className={`${showSubRow ? "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 +367,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 +413,42 @@ function StandingsTable({
</td>
</tr>
);
if (showSubRow) {
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,49 @@ 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("hides GB column when no rows have gamesBack data", () => {
render(
<RegularSeasonStandings
standings={[makeRow({ gamesBack: null })]}
teamOwnerships={NO_OWNERSHIPS}
userParticipantIds={NO_USER_IDS}
/>
);
expect(screen.queryByText("GB")).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

@ -27,7 +27,10 @@ interface MlbTeamRecord {
leagueRank?: string; // rank within the 15-team AL or NL
gamesBack: string; // "-" for leader, "2.0" otherwise
wildCardGamesBack?: string;
streak?: { streakCode: string; streakNumber: number };
streak?: {
streakCode: string; // full streak string e.g. "W3" or "L5" — NOT just the letter
streakNumber: number; // numeric part; redundant with streakCode, not used
};
records?: { splitRecords?: MlbSplitRecord[] };
}
@ -115,10 +118,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;
// streakCode is the full string e.g. "W3"; streakNumber is the numeric part embedded within it
const streak = team.streak?.streakCode;
// Split records
const splits = team.records?.splitRecords ?? [];