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:
parent
6c3065f6f2
commit
08e93e955a
4 changed files with 118 additions and 26 deletions
|
|
@ -236,19 +236,31 @@ 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.
|
||||||
|
// 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 (
|
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={`${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>
|
<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 +270,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>
|
||||||
|
|
@ -311,7 +323,7 @@ function StandingsTable({
|
||||||
sectionRows.push(
|
sectionRows.push(
|
||||||
<tr
|
<tr
|
||||||
key={row.id}
|
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"
|
isUserTeam ? "bg-primary/5" : "hover:bg-muted/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -326,7 +338,7 @@ function StandingsTable({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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}
|
{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 +367,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 +413,42 @@ function StandingsTable({
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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;
|
return sectionRows;
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,49 @@ 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("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", () => {
|
it("shows soccer table columns when showSoccerTable=true", () => {
|
||||||
render(
|
render(
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ interface MlbTeamRecord {
|
||||||
leagueRank?: string; // rank within the 15-team AL or NL
|
leagueRank?: string; // rank within the 15-team AL or NL
|
||||||
gamesBack: string; // "-" for leader, "2.0" otherwise
|
gamesBack: string; // "-" for leader, "2.0" otherwise
|
||||||
wildCardGamesBack?: string;
|
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[] };
|
records?: { splitRecords?: MlbSplitRecord[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,10 +118,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"
|
// streakCode is the full string e.g. "W3"; streakNumber is the numeric part embedded within it
|
||||||
const streak = team.streak
|
const streak = team.streak?.streakCode;
|
||||||
? `${team.streak.streakCode}${team.streak.streakNumber}`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// Split records
|
// Split records
|
||||||
const splits = team.records?.splitRecords ?? [];
|
const splits = team.records?.splitRecords ?? [];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue