From 4f111820ec0657c9537adb46a1f366519d881248 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 12 May 2026 10:39:25 -0700 Subject: [PATCH] Fix standings table bugs and polish across all three scoring patterns (#415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix standings table bugs and polish across all three scoring patterns - Fix isLastBeforePointsLine/PlayoffLine: was incorrectly suppressing the bottom border on the last row of a section when no divider followed (nextStanding === undefined case). Now correctly requires nextStanding to exist and exceed the cutoff rank. - Fix totalCols colSpan overcounting: hidden sm:table-cell columns (Drafted By / Mgr) don't occupy column slots on mobile, so counting them caused the divider rows to span one too many. Replaced with colSpan={100} (browser caps to actual column count). - Move pointsLinePushed mutation out of render in SeasonStandings and QualifyingPointsStandings: replaced let+mutation+array-push pattern with pre-computed firstOver8Idx and React.Fragment per row. - Replace array-returning .map() with keyed React.Fragment in both files. - Remove unused description prop from SeasonStandingsProps. - Use useId() for Switch id props in all three components to prevent id collisions when mounted multiple times. - Fix formatQP NaN fallback from "0" to "—". - Add comment noting canFinalize guards the admin-only finalize UI. - Drop dead description prop pass-through in SportSeasonDisplay. Fixes #408 Co-Authored-By: Claude Sonnet 4.6 * Fix RegularSeasonStandings tests broken by showStats toggle - STK/streak tests: click the Details switch before asserting on stats columns, which are now hidden behind the toggle by default - Ownership badge test: use getAllByText since the badge renders in both the mobile inline slot and the hidden-sm desktop Mgr cell - Division label test: remove expectation for inline per-row division labels, which were intentionally removed to fix mobile scroll Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../scoring/QualifyingPointsStandings.tsx | 255 +++++++-------- app/components/scoring/SeasonStandings.tsx | 302 ++++++++---------- app/components/scoring/SportSeasonDisplay.tsx | 2 +- .../sport-season/RegularSeasonStandings.tsx | 138 ++++---- .../__tests__/RegularSeasonStandings.test.tsx | 13 +- 5 files changed, 321 insertions(+), 389 deletions(-) diff --git a/app/components/scoring/QualifyingPointsStandings.tsx b/app/components/scoring/QualifyingPointsStandings.tsx index e1a1917..9a8a45e 100644 --- a/app/components/scoring/QualifyingPointsStandings.tsx +++ b/app/components/scoring/QualifyingPointsStandings.tsx @@ -1,3 +1,4 @@ +import { Fragment } from "react"; import { Form } from "react-router"; import { Button } from "~/components/ui/button"; import { TeamOwnerBadge } from "~/components/ui/team-owner-badge"; @@ -8,14 +9,6 @@ import { CardHeader, CardTitle, } from "~/components/ui/card"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "~/components/ui/table"; import { Trophy, CheckCircle2 } from "lucide-react"; interface TeamOwnership { @@ -53,8 +46,15 @@ interface QualifyingPointsStandingsProps { isFinalized: boolean; totalMajors?: number | null; majorsCompleted?: number; - canFinalize: boolean; // Whether all majors are complete + canFinalize: boolean; teamOwnerships?: TeamOwnership[]; + userParticipantIds?: string[]; +} + +function formatQP(raw: string): string { + const n = parseFloat(raw); + if (isNaN(n)) return "—"; + return n % 1 === 0 ? n.toString() : n.toFixed(1); } export function QualifyingPointsStandings({ @@ -65,23 +65,13 @@ export function QualifyingPointsStandings({ majorsCompleted = 0, canFinalize, teamOwnerships = [], + userParticipantIds = [], }: QualifyingPointsStandingsProps) { const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o])); - const pointsMap: Record = scoringRules - ? { - 1: scoringRules.pointsFor1st, - 2: scoringRules.pointsFor2nd, - 3: scoringRules.pointsFor3rd, - 4: scoringRules.pointsFor4th, - 5: scoringRules.pointsFor5th, - 6: scoringRules.pointsFor6th, - 7: scoringRules.pointsFor7th, - 8: scoringRules.pointsFor8th, - } - : {}; + const userParticipantSet = new Set(userParticipantIds); // Assign current ranks with tie handling - const rankedStandings: Array = []; + const rankedStandings: Array = []; let previousQP = -1; let previousRankStart = -1; @@ -97,32 +87,24 @@ export function QualifyingPointsStandings({ } rankedStandings.push({ ...standing, currentRank }); - previousRankStart = currentRank; previousQP = currentQP; } - // Pre-compute tied counts per rank to avoid O(N²) scanning per row const tiedCountByRank = new Map(); for (const s of rankedStandings) { tiedCountByRank.set(s.currentRank, (tiedCountByRank.get(s.currentRank) ?? 0) + 1); } - // Calculate EV for a rank by averaging points across all tied positions - const calculateEV = (currentRank: number): number => { - if (!scoringRules || currentRank > 8) return 0; - const tiedCount = tiedCountByRank.get(currentRank) ?? 1; - let total = 0; - for (let i = 0; i < tiedCount; i++) { - total += pointsMap[currentRank + i] ?? 0; - } - return total / tiedCount; - }; + const showOwnership = teamOwnerships.length > 0; + + // Index of first row with rank > 8; -1 when there is no cutoff. + const firstOver8Idx = rankedStandings.findIndex((s) => s.currentRank > 8); return ( - + - + Qualifying Points Standings @@ -130,20 +112,12 @@ export function QualifyingPointsStandings({ {isFinalized ? ( - Finalized - Fantasy points have been assigned + Finalized — fantasy points have been assigned ) : ( <> - Current standings based on {majorsCompleted} of {totalMajors || '?'} major tournaments completed. - {scoringRules ? ( - - Projected points account for ties — tied participants share the available points equally. - - ) : ( - - Projected points will be shown when linked to a fantasy season. - - )} + Current standings based on {majorsCompleted} of {totalMajors || "?"} major + tournaments completed. )} @@ -155,94 +129,98 @@ export function QualifyingPointsStandings({

) : ( <> - - - - Rank - Participant - {teamOwnerships.length > 0 && ( - Owner - )} - Total QP - Events - {scoringRules && !isFinalized && ( - Projected Points - )} - - - - {rankedStandings.map((standing) => { - const projectedPoints = calculateEV(standing.currentRank); - const isTop8 = standing.currentRank <= 8; +
+
+ + + + + + {showOwnership && ( + + )} + + + + {rankedStandings.map((standing, idx) => { + const ownership = showOwnership + ? ownershipMap.get(standing.participant.id) + : undefined; + const isTop8 = standing.currentRank <= 8; + const isTied = (tiedCountByRank.get(standing.currentRank) ?? 1) > 1; + const isOwned = userParticipantSet.has(standing.participant.id); + const showPointsLineBefore = firstOver8Idx !== -1 && idx === firstOver8Idx; + const isLastBeforePointsLine = firstOver8Idx !== -1 && idx === firstOver8Idx - 1; - const isTied = (tiedCountByRank.get(standing.currentRank) ?? 1) > 1; - - return ( - - -
- {standing.currentRank === 1 && !isTied && 🥇} - {standing.currentRank === 2 && !isTied && 🥈} - {standing.currentRank === 3 && !isTied && 🥉} - - {isTied && "T"} - {standing.currentRank} - {standing.currentRank === 1 - ? "st" - : standing.currentRank === 2 - ? "nd" - : standing.currentRank === 3 - ? "rd" - : "th"} - -
-
- - {standing.participant.name} - - {teamOwnerships.length > 0 && ( - - {(() => { - const ownership = ownershipMap.get(standing.participant.id); - return ownership ? ( - - ) : ( - Undrafted - ); - })()} - - )} - - - {parseFloat(standing.totalQualifyingPoints).toFixed(2)} QP - - - - {standing.eventsScored} - - {scoringRules && !isFinalized && ( - - {isTop8 ? ( - - {projectedPoints % 1 === 0 - ? projectedPoints - : projectedPoints.toFixed(1)}{" "} - pts + return ( + + {showPointsLineBefore && ( +
+ + + )} + + + + + {showOwnership && ( + )} - - )} - - ); - })} - -
#ParticipantTotal QPDrafted By
+
+
+ + Points Line + +
+
+
+ {isTied ? `T${standing.currentRank}` : standing.currentRank} + + + {standing.participant.name} - ) : ( - 0 pts + {ownership && ( +
+ +
+ )} +
+ + {formatQP(standing.totalQualifyingPoints)} QP + + + {ownership ? ( +
+ +
+ ) : ( + + )} +
+ + + ); + })} + + + + {/* canFinalize is controlled by the route loader; this section is only shown to league admins */} {!isFinalized && canFinalize && (
@@ -252,7 +230,7 @@ export function QualifyingPointsStandings({ All {totalMajors} major tournaments are complete. Finalizing will:

    -
  • Convert the top 8 participants to fantasy placements 1-8
  • +
  • Convert the top 8 participants to fantasy placements 1–8
  • Award fantasy points based on league scoring rules
  • Update all league standings
  • Lock the qualifying points (no further changes)
  • @@ -264,7 +242,11 @@ export function QualifyingPointsStandings({ type="submit" className="bg-amber-600 hover:bg-amber-700" onClick={(e) => { - if (!confirm("Are you sure you want to finalize qualifying points? This action cannot be undone.")) { + if ( + !confirm( + "Are you sure you want to finalize qualifying points? This action cannot be undone." + ) + ) { e.preventDefault(); } }} @@ -277,11 +259,12 @@ export function QualifyingPointsStandings({
)} - {!isFinalized && !canFinalize && ( + {!isFinalized && !canFinalize && scoringRules && (

- Not ready to finalize yet. Complete all {totalMajors} major tournaments before finalizing qualifying points. - ({majorsCompleted} of {totalMajors} completed) + Not ready to finalize yet. Complete all{" "} + {totalMajors} major tournaments before finalizing qualifying points. ( + {majorsCompleted} of {totalMajors} completed)

)} diff --git a/app/components/scoring/SeasonStandings.tsx b/app/components/scoring/SeasonStandings.tsx index 8af7da5..f68ed33 100644 --- a/app/components/scoring/SeasonStandings.tsx +++ b/app/components/scoring/SeasonStandings.tsx @@ -1,3 +1,4 @@ +import { Fragment, useId, useState } from "react"; import { Card, CardContent, @@ -5,23 +6,15 @@ import { CardHeader, CardTitle, } from "~/components/ui/card"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "~/components/ui/table"; -import { Badge } from "~/components/ui/badge"; -import { TrendingUp, TrendingDown, Minus, Flag, CheckCircle2, Star } from "lucide-react"; +import { Switch } from "~/components/ui/switch"; +import { TrendingUp, TrendingDown, Minus, Flag, CheckCircle2 } from "lucide-react"; import { TeamOwnerBadge } from "~/components/ui/team-owner-badge"; interface SeasonStanding { id: string; - championshipPoints: string; // Decimal as string + championshipPoints: string; position: number; - previousPosition?: number | null; // For showing movement + previousPosition?: number | null; participant: { id: string; name: string; @@ -37,31 +30,20 @@ interface TeamOwnership { interface SeasonStandingsProps { standings: SeasonStanding[]; - teamOwnerships?: TeamOwnership[]; // Which teams own which participants - userParticipantIds?: string[]; // Participants drafted by the current user + teamOwnerships?: TeamOwnership[]; + userParticipantIds?: string[]; showOwnership?: boolean; - isFinalized?: boolean; // Whether season is complete + isFinalized?: boolean; title?: string; - description?: string; } -/** - * SeasonStandings component - Displays F1-style championship standings - * - * Features: - * - Shows participants ranked by championship points - * - Positions auto-calculated from points (highest = 1st) - * - Optional ownership hints with team avatars - * - Movement indicators (position changes) - * - Handles ties (same points = same position) - */ function getMovementIndicator( currentPosition: number, previousPosition?: number | null ) { if (!previousPosition) return null; - const change = previousPosition - currentPosition; // Positive means moved up + const change = previousPosition - currentPosition; if (change > 0) { return ( @@ -86,12 +68,6 @@ function getMovementIndicator( } } -function getPositionBadge(position: number, isTied: boolean) { - const suffix = position === 1 ? "st" : position === 2 ? "nd" : position === 3 ? "rd" : "th"; - const positionText = isTied ? `T${position}` : `${position}${suffix}`; - return {positionText}; -} - export function SeasonStandings({ standings, teamOwnerships = [], @@ -99,48 +75,27 @@ export function SeasonStandings({ showOwnership = true, isFinalized = false, title = "Championship Standings", - description: _description, }: SeasonStandingsProps) { + const switchId = useId(); + const [showStats, setShowStats] = useState(false); + const userParticipantSet = new Set(userParticipantIds); - // Create ownership map for fast lookup const ownershipMap = new Map(); - teamOwnerships.forEach((ownership) => { - ownershipMap.set(ownership.participantId, ownership); - }); + teamOwnerships.forEach((o) => ownershipMap.set(o.participantId, o)); - // Get ownership info for a participant - const getOwnership = (participantId: string): TeamOwnership | null => { - if (!showOwnership) return null; - return ownershipMap.get(participantId) || null; - }; + const checkIfTied = (standing: SeasonStanding): boolean => + standings.some((o) => o.id !== standing.id && o.position === standing.position); - // Check if multiple participants share the same position - const checkIfTied = (standing: SeasonStanding): boolean => { - return standings.some( - (other) => - other.id !== standing.id && other.position === standing.position - ); - }; + const hasChangeColumn = standings.some((s) => s.previousPosition); + const sorted = [...standings].toSorted((a, b) => a.position - b.position); - // Count top 8 finishers (those who will get fantasy points) - const top8Count = standings.filter((s) => s.position <= 8).length; + // Index of first row with position > 8; -1 when there is no cutoff. + const firstOver8Idx = sorted.findIndex((s) => s.position > 8); return ( - + - + {title} @@ -148,17 +103,11 @@ export function SeasonStandings({ {isFinalized ? ( - Season complete - Fantasy points assigned to top 8 finishers + Season complete — fantasy points assigned to top 8 finishers ) : ( <> - Current championship standings. Positions calculated from points - (highest = 1st). - {top8Count > 0 && ( - - Top 8 finishers will receive fantasy points when season completes. - - )} + Current championship standings. Positions calculated from points (highest = 1st). )} @@ -173,104 +122,117 @@ export function SeasonStandings({
) : ( <> - - - - Pos - {standings.some((s) => s.previousPosition) && ( - Change - )} - Participant - Points - {showOwnership && ( - Drafted By - )} - - - - {standings - .toSorted((a, b) => a.position - b.position) - .map((standing) => { - const ownership = getOwnership(standing.participant.id); - const isTied = checkIfTied(standing); - const isTop8 = standing.position <= 8; - const isOwned = userParticipantSet.has(standing.participant.id); - - let rowClass = ""; - if (isOwned && isTop8) { - rowClass = "bg-electric/8 border-l-2 border-l-electric font-medium"; - } else if (isOwned && !isTop8) { - rowClass = "bg-muted/20 border-l-2 border-l-muted-foreground/40 opacity-80"; - } else if (isTop8) { - rowClass = standing.position <= 3 ? "bg-muted/30 font-medium" : ""; - } else { - rowClass = "opacity-60"; - } - - return ( - - - {getPositionBadge(standing.position, isTied)} - - {standings.some((s) => s.previousPosition) && ( - - {getMovementIndicator( - standing.position, - standing.previousPosition - )} - - )} - - {standing.participant.name} - {isOwned && ( - - )} - - - - {Math.round(parseFloat(standing.championshipPoints))} - - - {showOwnership && ( - - {ownership ? ( - - ) : ( - - - )} - - )} - - ); - })} - -
- - {!isFinalized && top8Count > 0 && ( -
-

- - {top8Count} - {" "} - participant{top8Count !== 1 ? "s" : ""} currently in top 8 will - receive fantasy points when season completes. -

+ {/* Details toggle only shown when position-change data is available */} + {hasChangeColumn && ( +
+ +
)} +
+ + + + + {hasChangeColumn && showStats && ( + + )} + + + {showOwnership && ( + + )} + + + + {sorted.map((standing, idx) => { + const ownership = showOwnership + ? ownershipMap.get(standing.participant.id) ?? null + : null; + const isTied = checkIfTied(standing); + const isOwned = userParticipantSet.has(standing.participant.id); + const isTop8 = standing.position <= 8; + const showPointsLineBefore = firstOver8Idx !== -1 && idx === firstOver8Idx; + const isLastBeforePointsLine = firstOver8Idx !== -1 && idx === firstOver8Idx - 1; + + return ( + + {showPointsLineBefore && ( + + + + )} + + + {hasChangeColumn && showStats && ( + + )} + + + {showOwnership && ( + + )} + + + ); + })} + +
#ChangeParticipantPointsDrafted By
+
+
+ + Points Line + +
+
+
+ {isTied ? `T${standing.position}` : standing.position} + + {getMovementIndicator(standing.position, standing.previousPosition)} + + + {standing.participant.name} + + {ownership && ( +
+ +
+ )} +
+ + {Math.round(parseFloat(standing.championshipPoints))} + + + {ownership ? ( +
+ +
+ ) : ( + + )} +
+
)} diff --git a/app/components/scoring/SportSeasonDisplay.tsx b/app/components/scoring/SportSeasonDisplay.tsx index 490194c..f8d1591 100644 --- a/app/components/scoring/SportSeasonDisplay.tsx +++ b/app/components/scoring/SportSeasonDisplay.tsx @@ -173,7 +173,6 @@ export function SportSeasonDisplay({ showOwnership={showOwnership} isFinalized={seasonIsFinalized} title={sportSeasonName} - description="Championship standings - positions calculated from points" /> ); @@ -188,6 +187,7 @@ export function SportSeasonDisplay({ majorsCompleted={majorsCompleted} canFinalize={canFinalize} teamOwnerships={showOwnership ? teamOwnerships : []} + userParticipantIds={userParticipantIds} /> ); diff --git a/app/components/sport-season/RegularSeasonStandings.tsx b/app/components/sport-season/RegularSeasonStandings.tsx index bc356bf..123475f 100644 --- a/app/components/sport-season/RegularSeasonStandings.tsx +++ b/app/components/sport-season/RegularSeasonStandings.tsx @@ -1,3 +1,4 @@ +import { useId, useState } from "react"; import { Card, CardContent, @@ -5,6 +6,7 @@ import { CardHeader, CardTitle, } from "~/components/ui/card"; +import { Switch } from "~/components/ui/switch"; import { BarChart3 } from "lucide-react"; import { TeamOwnerBadge } from "~/components/ui/team-owner-badge"; @@ -229,38 +231,31 @@ function StandingsTable({ userParticipantIds, showOtLosses, showSoccerTable = false, + showStats, }: { sections: TableSection[]; teamOwnerships: Record; userParticipantIds: string[]; showOtLosses: boolean; showSoccerTable?: boolean; + showStats: boolean; }) { 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; + // # Team [GP] W [D] L [GF GA GD PTS] [OTL PTS] [PCT] [GB] [L10] [STK] Mgr + // Stats columns (GP, PCT, GB, L10, STK) are toggled by showStats. return (
- +
- + - + {(!showSoccerTable && showStats) && } {showSoccerTable && } @@ -270,11 +265,11 @@ function StandingsTable({ {showSoccerTable && } {showOtLosses && } {showOtLosses && } - {!showSoccerTable && } - {!showSoccerTable && hasGB && } - {!showSoccerTable && hasLastTen && } - {!showSoccerTable && hasStreak && } - + {(!showSoccerTable && showStats) && } + {(!showSoccerTable && showStats && hasGB) && } + {(!showSoccerTable && showStats && hasLastTen) && } + {(!showSoccerTable && showStats && hasStreak) && } + @@ -286,7 +281,7 @@ function StandingsTable({ sectionRows.push( - - + - + {(!showSoccerTable && showStats) && ( + + )} {showSoccerTable && ( @@ -366,22 +373,22 @@ function StandingsTable({ {row.wins * 2 + (row.otLosses ?? 0)} )} - {!showSoccerTable && ( - )} - {!showSoccerTable && hasGB && ( - )} - {!showSoccerTable && hasLastTen && ( - )} - {!showSoccerTable && hasStreak && ( + {(!showSoccerTable && showStats && hasStreak) && ( ); - - if (showSubRow) { - sectionRows.push( - - - - ); - } }); return sectionRows; @@ -470,6 +441,9 @@ export function RegularSeasonStandings({ playoffSpots = 8, displayMode = "flat", }: Props) { + const switchId = useId(); + const [showStats, setShowStats] = useState(false); + if (standings.length === 0) return null; const lastSyncedAt = standings @@ -485,7 +459,7 @@ export function RegularSeasonStandings({ ? buildMlbSections(standings) : buildFlatSections(standings, playoffSpots); - const tableProps = { teamOwnerships, userParticipantIds, showOtLosses, showSoccerTable }; + const tableProps = { teamOwnerships, userParticipantIds, showOtLosses, showSoccerTable, showStats }; return ( @@ -503,6 +477,18 @@ export function RegularSeasonStandings({ + {!showSoccerTable && ( +
+ + +
+ )} {groups.map((group) => (
{group.conference && ( diff --git a/app/components/sport-season/__tests__/RegularSeasonStandings.test.tsx b/app/components/sport-season/__tests__/RegularSeasonStandings.test.tsx index 3911fd7..9e8cee1 100644 --- a/app/components/sport-season/__tests__/RegularSeasonStandings.test.tsx +++ b/app/components/sport-season/__tests__/RegularSeasonStandings.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { render, screen, fireEvent } from "@testing-library/react"; import { RegularSeasonStandings } from "../RegularSeasonStandings"; function makeRow(overrides: Partial<{ @@ -128,6 +128,7 @@ describe("RegularSeasonStandings", () => { userParticipantIds={NO_USER_IDS} /> ); + fireEvent.click(screen.getByRole("switch")); expect(screen.getByText("STK")).toBeInTheDocument(); }); @@ -184,7 +185,7 @@ describe("RegularSeasonStandings", () => { expect(screen.getByText("69")).toBeInTheDocument(); }); - it("renders conference headings and inline division labels when present", () => { + it("renders conference headings when standings have conference data", () => { render( { // Conference group headings are rendered as

expect(screen.getByText(/Eastern Conference/i)).toBeInTheDocument(); expect(screen.getByText(/Western Conference/i)).toBeInTheDocument(); - // Division is now shown as an inline label per row (not a section header) - expect(screen.getAllByText(/Atlantic/i).length).toBeGreaterThan(0); - expect(screen.getAllByText(/Pacific/i).length).toBeGreaterThan(0); }); it("shows ownership badge for drafted teams", () => { @@ -213,7 +211,8 @@ describe("RegularSeasonStandings", () => { userParticipantIds={NO_USER_IDS} /> ); - expect(screen.getByText("Alice")).toBeInTheDocument(); + // Owner name appears in both the mobile inline badge and the desktop Mgr column + expect(screen.getAllByText("Alice").length).toBeGreaterThan(0); }); it("does not show badge for undrafted teams", () => { @@ -249,6 +248,7 @@ describe("RegularSeasonStandings", () => { userParticipantIds={NO_USER_IDS} /> ); + fireEvent.click(screen.getByRole("switch")); const streakEl = screen.getByText("W5"); expect(streakEl.className).toContain("emerald"); }); @@ -261,6 +261,7 @@ describe("RegularSeasonStandings", () => { userParticipantIds={NO_USER_IDS} /> ); + fireEvent.click(screen.getByRole("switch")); const streakEl = screen.getByText("L3"); expect(streakEl.className).toContain("destructive"); });

## TeamGPGPWDLPTSOTLPTSPCTGBL10STKMgrPCTGBL10STKMgr
0 ? "pt-5" : "pt-2" }`} @@ -299,13 +294,20 @@ function StandingsTable({ section.rows.forEach((row, i) => { const rank = getRank(row, i, section.rankMode); + const nextRow = section.rows[i + 1]; + const nextRank = nextRow ? getRank(nextRow, i + 1, section.rankMode) : null; + const isLastBeforePlayoffLine = + section.playoffSpots > 0 && + rank <= section.playoffSpots && + nextRank !== null && + nextRank > section.playoffSpots; if (!playoffLineShown && section.playoffSpots > 0 && rank > section.playoffSpots) { playoffLineShown = true; sectionRows.push(
-
+
+
Playoff Line @@ -323,24 +325,29 @@ function StandingsTable({ sectionRows.push(
{rank}{rank} {row.participant.shortName ?? row.participant.name} - {section.showDivisionLabel && row.division && ( - - {row.division} - + {ownership && ( +
+ +
)}
- {row.gamesPlayed} - + {row.gamesPlayed} + {row.wins}{row.ties ?? 0} + {(!showSoccerTable && showStats) && ( + {formatWinPct(row.winPct, row.wins, row.gamesPlayed)} + {(!showSoccerTable && showStats && hasGB) && ( + {formatGB(row.gamesBack)} + {(!showSoccerTable && showStats && hasLastTen) && ( + {row.lastTen ?? "—"} {row.streak ? ( )} - + {ownership ? (
-
- - GP{" "} - {row.gamesPlayed} - - - PCT{" "} - {formatWinPct(row.winPct, row.wins, row.gamesPlayed)} - - {hasGB && ( - - GB{" "} - {formatGB(row.gamesBack)} - - )} - {hasLastTen && ( - - L10{" "} - {row.lastTen ?? "—"} - - )} -
-