Fix standings table bugs and polish across all three scoring patterns (#415)
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c5366fe9cc
commit
4f111820ec
5 changed files with 321 additions and 389 deletions
|
|
@ -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<number, number> = 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<typeof standings[0] & { currentRank: number }> = [];
|
||||
const rankedStandings: Array<QPStanding & { currentRank: number }> = [];
|
||||
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<number, number>();
|
||||
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 (
|
||||
<Card className={isFinalized ? "border-emerald-500/30" : "border-amber-accent/30"}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className={isFinalized ? "text-emerald-400" : "text-amber-accent"}>
|
||||
<CardTitle>
|
||||
<Trophy className="inline mr-2 h-5 w-5" />
|
||||
Qualifying Points Standings
|
||||
</CardTitle>
|
||||
|
|
@ -130,20 +112,12 @@ export function QualifyingPointsStandings({
|
|||
{isFinalized ? (
|
||||
<span className="text-emerald-400 font-semibold">
|
||||
<CheckCircle2 className="inline h-4 w-4 mr-1" />
|
||||
Finalized - Fantasy points have been assigned
|
||||
Finalized — fantasy points have been assigned
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
Current standings based on {majorsCompleted} of {totalMajors || '?'} major tournaments completed.
|
||||
{scoringRules ? (
|
||||
<span className="block mt-1">
|
||||
Projected points account for ties — tied participants share the available points equally.
|
||||
</span>
|
||||
) : (
|
||||
<span className="block mt-1 text-muted-foreground">
|
||||
Projected points will be shown when linked to a fantasy season.
|
||||
</span>
|
||||
)}
|
||||
Current standings based on {majorsCompleted} of {totalMajors || "?"} major
|
||||
tournaments completed.
|
||||
</>
|
||||
)}
|
||||
</CardDescription>
|
||||
|
|
@ -155,94 +129,98 @@ export function QualifyingPointsStandings({
|
|||
</p>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">Rank</TableHead>
|
||||
<TableHead>Participant</TableHead>
|
||||
{teamOwnerships.length > 0 && (
|
||||
<TableHead className="w-40">Owner</TableHead>
|
||||
)}
|
||||
<TableHead className="w-28 text-right">Total QP</TableHead>
|
||||
<TableHead className="w-24 text-right">Events</TableHead>
|
||||
{scoringRules && !isFinalized && (
|
||||
<TableHead className="w-32 text-right">Projected Points</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rankedStandings.map((standing) => {
|
||||
const projectedPoints = calculateEV(standing.currentRank);
|
||||
const isTop8 = standing.currentRank <= 8;
|
||||
<div className="overflow-x-auto -mx-6 px-6">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs text-muted-foreground uppercase tracking-wide border-b">
|
||||
<th className="text-left py-1.5 pl-2 pr-2 w-10">#</th>
|
||||
<th className="text-left py-1.5">Participant</th>
|
||||
<th className="text-right py-1.5 px-2 w-24">Total QP</th>
|
||||
{showOwnership && (
|
||||
<th className="hidden sm:table-cell text-right py-1.5 pl-4 w-40">Drafted By</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<TableRow key={standing.id} className={!isTop8 ? "opacity-60" : ""}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{standing.currentRank === 1 && !isTied && <span className="text-xl">🥇</span>}
|
||||
{standing.currentRank === 2 && !isTied && <span className="text-xl">🥈</span>}
|
||||
{standing.currentRank === 3 && !isTied && <span className="text-xl">🥉</span>}
|
||||
<span className="font-semibold">
|
||||
{isTied && "T"}
|
||||
{standing.currentRank}
|
||||
{standing.currentRank === 1
|
||||
? "st"
|
||||
: standing.currentRank === 2
|
||||
? "nd"
|
||||
: standing.currentRank === 3
|
||||
? "rd"
|
||||
: "th"}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{standing.participant.name}
|
||||
</TableCell>
|
||||
{teamOwnerships.length > 0 && (
|
||||
<TableCell>
|
||||
{(() => {
|
||||
const ownership = ownershipMap.get(standing.participant.id);
|
||||
return ownership ? (
|
||||
<TeamOwnerBadge
|
||||
teamName={ownership.teamName}
|
||||
ownerName={ownership.ownerName}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">Undrafted</span>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-right">
|
||||
<span className="font-semibold text-amber-accent">
|
||||
{parseFloat(standing.totalQualifyingPoints).toFixed(2)} QP
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{standing.eventsScored}
|
||||
</TableCell>
|
||||
{scoringRules && !isFinalized && (
|
||||
<TableCell className="text-right">
|
||||
{isTop8 ? (
|
||||
<span className="font-semibold text-emerald-400">
|
||||
{projectedPoints % 1 === 0
|
||||
? projectedPoints
|
||||
: projectedPoints.toFixed(1)}{" "}
|
||||
pts
|
||||
return (
|
||||
<Fragment key={standing.id}>
|
||||
{showPointsLineBefore && (
|
||||
<tr>
|
||||
<td colSpan={100} className="py-0.5">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex-1 border-t border-dashed border-amber-500/40" />
|
||||
<span className="text-[10px] text-amber-600/70 dark:text-amber-500/70 uppercase tracking-wide whitespace-nowrap font-medium px-1">
|
||||
Points Line
|
||||
</span>
|
||||
<div className="flex-1 border-t border-dashed border-amber-500/40" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr
|
||||
className={`${isLastBeforePointsLine ? "" : "border-b border-border/50 last:border-0"} ${
|
||||
isOwned
|
||||
? "bg-primary/5"
|
||||
: isTop8
|
||||
? "hover:bg-muted/30"
|
||||
: "opacity-60 hover:bg-muted/30"
|
||||
}`}
|
||||
>
|
||||
<td className="py-2 pl-2 pr-2 text-muted-foreground tabular-nums">
|
||||
{isTied ? `T${standing.currentRank}` : standing.currentRank}
|
||||
</td>
|
||||
<td className="py-2 font-medium">
|
||||
<span className={isOwned ? "text-primary" : ""}>
|
||||
{standing.participant.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">0 pts</span>
|
||||
{ownership && (
|
||||
<div className="sm:hidden mt-0.5">
|
||||
<TeamOwnerBadge
|
||||
teamName={ownership.teamName}
|
||||
ownerName={ownership.ownerName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-2 text-right tabular-nums">
|
||||
<span className={`font-semibold ${isTop8 ? "text-amber-accent" : "text-muted-foreground"}`}>
|
||||
{formatQP(standing.totalQualifyingPoints)} QP
|
||||
</span>
|
||||
</td>
|
||||
{showOwnership && (
|
||||
<td className="hidden sm:table-cell py-2 pl-4 text-right">
|
||||
{ownership ? (
|
||||
<div className="flex justify-end">
|
||||
<TeamOwnerBadge
|
||||
teamName={ownership.teamName}
|
||||
ownerName={ownership.ownerName}
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</tr>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* canFinalize is controlled by the route loader; this section is only shown to league admins */}
|
||||
{!isFinalized && canFinalize && (
|
||||
<div className="mt-6 p-4 border-t">
|
||||
<div className="space-y-3">
|
||||
|
|
@ -252,7 +230,7 @@ export function QualifyingPointsStandings({
|
|||
All {totalMajors} major tournaments are complete. Finalizing will:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-muted-foreground mt-2 space-y-1">
|
||||
<li>Convert the top 8 participants to fantasy placements 1-8</li>
|
||||
<li>Convert the top 8 participants to fantasy placements 1–8</li>
|
||||
<li>Award fantasy points based on league scoring rules</li>
|
||||
<li>Update all league standings</li>
|
||||
<li>Lock the qualifying points (no further changes)</li>
|
||||
|
|
@ -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({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!isFinalized && !canFinalize && (
|
||||
{!isFinalized && !canFinalize && scoringRules && (
|
||||
<div className="mt-6 p-4 border-t">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-semibold">Not ready to finalize yet.</span> Complete all {totalMajors} major tournaments before finalizing qualifying points.
|
||||
({majorsCompleted} of {totalMajors} completed)
|
||||
<span className="font-semibold">Not ready to finalize yet.</span> Complete all{" "}
|
||||
{totalMajors} major tournaments before finalizing qualifying points. (
|
||||
{majorsCompleted} of {totalMajors} completed)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 <span className="font-medium">{positionText}</span>;
|
||||
}
|
||||
|
||||
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<string, TeamOwnership>();
|
||||
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 (
|
||||
<Card
|
||||
className={
|
||||
isFinalized
|
||||
? "border-emerald-500/30"
|
||||
: "border-electric/30"
|
||||
}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle
|
||||
className={
|
||||
isFinalized
|
||||
? "text-emerald-400"
|
||||
: "text-electric"
|
||||
}
|
||||
>
|
||||
<CardTitle>
|
||||
<Flag className="inline mr-2 h-5 w-5" />
|
||||
{title}
|
||||
</CardTitle>
|
||||
|
|
@ -148,17 +103,11 @@ export function SeasonStandings({
|
|||
{isFinalized ? (
|
||||
<span className="text-emerald-400 font-semibold">
|
||||
<CheckCircle2 className="inline h-4 w-4 mr-1" />
|
||||
Season complete - Fantasy points assigned to top 8 finishers
|
||||
Season complete — fantasy points assigned to top 8 finishers
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
Current championship standings. Positions calculated from points
|
||||
(highest = 1st).
|
||||
{top8Count > 0 && (
|
||||
<span className="block mt-1">
|
||||
Top 8 finishers will receive fantasy points when season completes.
|
||||
</span>
|
||||
)}
|
||||
Current championship standings. Positions calculated from points (highest = 1st).
|
||||
</>
|
||||
)}
|
||||
</CardDescription>
|
||||
|
|
@ -173,104 +122,117 @@ export function SeasonStandings({
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">Pos</TableHead>
|
||||
{standings.some((s) => s.previousPosition) && (
|
||||
<TableHead className="w-20">Change</TableHead>
|
||||
)}
|
||||
<TableHead>Participant</TableHead>
|
||||
<TableHead className="text-right w-20">Points</TableHead>
|
||||
{showOwnership && (
|
||||
<TableHead className="w-40 pl-6">Drafted By</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{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 (
|
||||
<TableRow
|
||||
key={standing.id}
|
||||
className={rowClass}
|
||||
>
|
||||
<TableCell>
|
||||
{getPositionBadge(standing.position, isTied)}
|
||||
</TableCell>
|
||||
{standings.some((s) => s.previousPosition) && (
|
||||
<TableCell>
|
||||
{getMovementIndicator(
|
||||
standing.position,
|
||||
standing.previousPosition
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="font-medium">
|
||||
<span>{standing.participant.name}</span>
|
||||
{isOwned && (
|
||||
<Star
|
||||
className={`inline ml-1.5 h-3 w-3 fill-current ${isTop8 ? "text-electric" : "text-muted-foreground"}`}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
isTop8
|
||||
? "text-electric"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{Math.round(parseFloat(standing.championshipPoints))}
|
||||
</span>
|
||||
</TableCell>
|
||||
{showOwnership && (
|
||||
<TableCell className="pl-6">
|
||||
{ownership ? (
|
||||
<TeamOwnerBadge
|
||||
teamName={ownership.teamName}
|
||||
ownerName={ownership.ownerName}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{!isFinalized && top8Count > 0 && (
|
||||
<div className="mt-4 p-3 border-t">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{top8Count}
|
||||
</Badge>{" "}
|
||||
participant{top8Count !== 1 ? "s" : ""} currently in top 8 will
|
||||
receive fantasy points when season completes.
|
||||
</p>
|
||||
{/* Details toggle only shown when position-change data is available */}
|
||||
{hasChangeColumn && (
|
||||
<div className="flex items-center justify-end gap-2 mb-2">
|
||||
<label className="text-xs text-muted-foreground cursor-pointer" htmlFor={switchId}>
|
||||
Details
|
||||
</label>
|
||||
<Switch
|
||||
id={switchId}
|
||||
checked={showStats}
|
||||
onCheckedChange={setShowStats}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto -mx-6 px-6">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs text-muted-foreground uppercase tracking-wide border-b">
|
||||
<th className="text-left py-1.5 pl-2 pr-2 w-10">#</th>
|
||||
{hasChangeColumn && showStats && (
|
||||
<th className="text-left py-1.5 w-16">Change</th>
|
||||
)}
|
||||
<th className="text-left py-1.5">Participant</th>
|
||||
<th className="text-right py-1.5 px-2 w-20">Points</th>
|
||||
{showOwnership && (
|
||||
<th className="hidden sm:table-cell text-right py-1.5 pl-4 w-40">Drafted By</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<Fragment key={standing.id}>
|
||||
{showPointsLineBefore && (
|
||||
<tr>
|
||||
<td colSpan={100} className="py-0.5">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex-1 border-t border-dashed border-amber-500/40" />
|
||||
<span className="text-[10px] text-amber-600/70 dark:text-amber-500/70 uppercase tracking-wide whitespace-nowrap font-medium px-1">
|
||||
Points Line
|
||||
</span>
|
||||
<div className="flex-1 border-t border-dashed border-amber-500/40" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr
|
||||
className={`${isLastBeforePointsLine ? "" : "border-b border-border/50 last:border-0"} ${
|
||||
isOwned ? "bg-primary/5" : isTop8 ? "hover:bg-muted/30" : "opacity-60 hover:bg-muted/30"
|
||||
}`}
|
||||
>
|
||||
<td className="py-2 pl-2 pr-2 text-muted-foreground tabular-nums">
|
||||
{isTied ? `T${standing.position}` : standing.position}
|
||||
</td>
|
||||
{hasChangeColumn && showStats && (
|
||||
<td className="py-2">
|
||||
{getMovementIndicator(standing.position, standing.previousPosition)}
|
||||
</td>
|
||||
)}
|
||||
<td className="py-2 font-medium">
|
||||
<span className={isOwned ? "text-primary" : ""}>
|
||||
{standing.participant.name}
|
||||
</span>
|
||||
{ownership && (
|
||||
<div className="sm:hidden mt-0.5">
|
||||
<TeamOwnerBadge
|
||||
teamName={ownership.teamName}
|
||||
ownerName={ownership.ownerName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-2 text-right tabular-nums">
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
isTop8 ? "text-electric" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{Math.round(parseFloat(standing.championshipPoints))}
|
||||
</span>
|
||||
</td>
|
||||
{showOwnership && (
|
||||
<td className="hidden sm:table-cell py-2 pl-4 text-right">
|
||||
{ownership ? (
|
||||
<div className="flex justify-end">
|
||||
<TeamOwnerBadge
|
||||
teamName={ownership.teamName}
|
||||
ownerName={ownership.ownerName}
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, TeamOwnership>;
|
||||
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 (
|
||||
<div className="overflow-x-auto -mx-6 px-6">
|
||||
<table className="w-full min-w-[360px] text-sm">
|
||||
<table className="w-full 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 pl-2 pr-2 w-8">#</th>
|
||||
<th className="text-left py-1.5">Team</th>
|
||||
<th className={`${showSubRow ? "hidden sm:table-cell " : ""}text-right py-1.5 px-2 w-10`}>GP</th>
|
||||
{(!showSoccerTable && showStats) && <th className="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>
|
||||
|
|
@ -270,11 +265,11 @@ 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="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>
|
||||
{(!showSoccerTable && showStats) && <th className="text-right py-1.5 px-2 w-12">PCT</th>}
|
||||
{(!showSoccerTable && showStats && hasGB) && <th className="text-right py-1.5 px-2 w-10">GB</th>}
|
||||
{(!showSoccerTable && showStats && hasLastTen) && <th className="text-right py-1.5 px-2 w-12">L10</th>}
|
||||
{(!showSoccerTable && showStats && hasStreak) && <th className="text-right py-1.5 px-2 w-12">STK</th>}
|
||||
<th className="hidden sm:table-cell text-right py-1.5 pl-4 w-40">Mgr</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -286,7 +281,7 @@ function StandingsTable({
|
|||
sectionRows.push(
|
||||
<tr key={`h-${section.heading}`}>
|
||||
<td
|
||||
colSpan={totalCols}
|
||||
colSpan={100}
|
||||
className={`text-xs font-medium text-muted-foreground/60 uppercase tracking-wider pb-1 ${
|
||||
sIdx > 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(
|
||||
<tr key={`pl-${sIdx}`}>
|
||||
<td colSpan={totalCols} className="py-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<td colSpan={100} className="py-0.5">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex-1 border-t border-dashed border-amber-500/40" />
|
||||
<span className="text-[10px] text-amber-600/70 dark:text-amber-500/70 uppercase tracking-wide whitespace-nowrap font-medium px-1">
|
||||
Playoff Line
|
||||
|
|
@ -323,24 +325,29 @@ function StandingsTable({
|
|||
sectionRows.push(
|
||||
<tr
|
||||
key={row.id}
|
||||
className={`${showSubRow ? "" : "border-b border-border/50 last:border-0"} ${
|
||||
className={`${isLastBeforePlayoffLine ? "" : "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 pl-2 pr-2 text-muted-foreground tabular-nums">{rank}</td>
|
||||
<td className="py-2">
|
||||
<span className={`font-medium ${isUserTeam ? "text-primary" : ""}`}>
|
||||
{row.participant.shortName ?? row.participant.name}
|
||||
</span>
|
||||
{section.showDivisionLabel && row.division && (
|
||||
<span className="ml-1.5 text-[10px] text-muted-foreground/50 uppercase tracking-wide">
|
||||
{row.division}
|
||||
</span>
|
||||
{ownership && (
|
||||
<div className="sm:hidden mt-0.5">
|
||||
<TeamOwnerBadge
|
||||
teamName={ownership.teamName}
|
||||
ownerName={ownership.ownerName || undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className={`${showSubRow ? "hidden sm:table-cell " : ""}py-2 px-2 text-right tabular-nums text-muted-foreground`}>
|
||||
{row.gamesPlayed}
|
||||
</td>
|
||||
{(!showSoccerTable && showStats) && (
|
||||
<td className="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>
|
||||
{showSoccerTable && (
|
||||
<td className="py-2 px-2 text-right tabular-nums">{row.ties ?? 0}</td>
|
||||
|
|
@ -366,22 +373,22 @@ function StandingsTable({
|
|||
{row.wins * 2 + (row.otLosses ?? 0)}
|
||||
</td>
|
||||
)}
|
||||
{!showSoccerTable && (
|
||||
<td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
|
||||
{(!showSoccerTable && showStats) && (
|
||||
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground">
|
||||
{formatWinPct(row.winPct, row.wins, row.gamesPlayed)}
|
||||
</td>
|
||||
)}
|
||||
{!showSoccerTable && hasGB && (
|
||||
<td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
|
||||
{(!showSoccerTable && showStats && hasGB) && (
|
||||
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground">
|
||||
{formatGB(row.gamesBack)}
|
||||
</td>
|
||||
)}
|
||||
{!showSoccerTable && hasLastTen && (
|
||||
<td className="hidden sm:table-cell py-2 px-2 text-right tabular-nums text-muted-foreground">
|
||||
{(!showSoccerTable && showStats && hasLastTen) && (
|
||||
<td className="py-2 px-2 text-right tabular-nums text-muted-foreground">
|
||||
{row.lastTen ?? "—"}
|
||||
</td>
|
||||
)}
|
||||
{!showSoccerTable && hasStreak && (
|
||||
{(!showSoccerTable && showStats && hasStreak) && (
|
||||
<td className="py-2 px-2 text-right tabular-nums">
|
||||
{row.streak ? (
|
||||
<span
|
||||
|
|
@ -398,7 +405,7 @@ function StandingsTable({
|
|||
)}
|
||||
</td>
|
||||
)}
|
||||
<td className="py-2 pl-4 text-right">
|
||||
<td className="hidden sm:table-cell py-2 pl-4 text-right">
|
||||
{ownership ? (
|
||||
<div className="flex justify-end">
|
||||
<TeamOwnerBadge
|
||||
|
|
@ -413,42 +420,6 @@ 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;
|
||||
|
|
@ -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 (
|
||||
<Card>
|
||||
|
|
@ -503,6 +477,18 @@ export function RegularSeasonStandings({
|
|||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!showSoccerTable && (
|
||||
<div className="flex items-center justify-end gap-2 mb-2">
|
||||
<label className="text-xs text-muted-foreground cursor-pointer" htmlFor={switchId}>
|
||||
Details
|
||||
</label>
|
||||
<Switch
|
||||
id={switchId}
|
||||
checked={showStats}
|
||||
onCheckedChange={setShowStats}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<div key={group.conference} className="mb-6 last:mb-0">
|
||||
{group.conference && (
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<RegularSeasonStandings
|
||||
standings={[
|
||||
|
|
@ -198,9 +199,6 @@ describe("RegularSeasonStandings", () => {
|
|||
// Conference group headings are rendered as <h3>
|
||||
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");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue