brackt/app/components/standings/StandingsTable.tsx
Chris Parsons f17699e4c5 feat: Implement team breakdown pages with detailed score breakdown
- Created `TeamScoreBreakdown` component to display all drafted participants, their placements, and points.
- Added new route for team breakdown at `/leagues/:leagueId/standings/:seasonId/teams/:teamId`.
- Enhanced `getTeamScoreBreakdown` model to include `sportsSeasonId` for grouping.
- Updated `StandingsTable` to include clickable team name links.
- Added functionality to finalize brackets in playoff events, including error handling and recalculating standings.
- Implemented tests for `TeamScoreBreakdown` component to ensure proper rendering and functionality.
- Updated routing to support new team breakdown feature and ensure seamless navigation.
2025-11-14 20:01:21 -08:00

196 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Link } from "react-router";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table";
import { Badge } from "~/components/ui/badge";
import { type TeamStanding } from "~/types/standings";
interface StandingsTableProps {
standings: TeamStanding[];
leagueId: string;
seasonId: string;
showPlacementBreakdown?: boolean;
}
/**
* Display team standings with ranking, points, and placement breakdown
* Phase 4.1: Enhanced standings table with tiebreakers
* Phase 4.3: Added clickable links to team breakdown pages
*/
export function StandingsTable({
standings,
leagueId,
seasonId,
showPlacementBreakdown = true,
}: StandingsTableProps) {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[80px]">Rank</TableHead>
<TableHead>Team</TableHead>
<TableHead className="text-right">Points</TableHead>
{showPlacementBreakdown && (
<TableHead className="text-center">Placements</TableHead>
)}
<TableHead className="text-right">Remaining</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{standings.length === 0 ? (
<TableRow>
<TableCell colSpan={showPlacementBreakdown ? 5 : 4} className="text-center text-muted-foreground">
No standings data available
</TableCell>
</TableRow>
) : (
standings.map((standing) => (
<TableRow key={standing.teamId}>
<TableCell>
<div className="flex items-center gap-2">
<RankBadge rank={standing.currentRank} />
{standing.rankChange !== 0 && (
<MovementIndicator change={standing.rankChange} />
)}
</div>
</TableCell>
<TableCell className="font-medium">
<Link
to={`/leagues/${leagueId}/standings/${seasonId}/teams/${standing.teamId}`}
className="hover:underline hover:text-primary transition-colors"
>
{standing.teamName}
</Link>
</TableCell>
<TableCell className="text-right font-semibold">
{standing.totalPoints.toFixed(1)}
</TableCell>
{showPlacementBreakdown && (
<TableCell>
<PlacementBreakdown placements={standing.placementCounts} />
</TableCell>
)}
<TableCell className="text-right text-muted-foreground">
{standing.participantsRemaining > 0 ? (
<Badge variant="secondary">
{standing.participantsRemaining} remaining
</Badge>
) : (
<Badge variant="outline">Complete</Badge>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
}
/**
* Display rank badge with special styling for top 3
*/
function RankBadge({ rank }: { rank: number }) {
if (rank === 1) {
return (
<Badge className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold">
🏆 1st
</Badge>
);
}
if (rank === 2) {
return (
<Badge className="bg-gray-400 hover:bg-gray-500 text-white font-bold">
🥈 2nd
</Badge>
);
}
if (rank === 3) {
return (
<Badge className="bg-orange-600 hover:bg-orange-700 text-white font-bold">
🥉 3rd
</Badge>
);
}
return (
<Badge variant="outline" className="font-semibold">
{rank}
</Badge>
);
}
/**
* Show movement indicator (up/down arrow)
*/
function MovementIndicator({ change }: { change: number }) {
if (change > 0) {
return (
<span className="text-green-600 text-sm font-medium" title={`Up ${change} ${change === 1 ? 'place' : 'places'}`}>
{change}
</span>
);
}
if (change < 0) {
return (
<span className="text-red-600 text-sm font-medium" title={`Down ${Math.abs(change)} ${Math.abs(change) === 1 ? 'place' : 'places'}`}>
{Math.abs(change)}
</span>
);
}
return null;
}
/**
* Display placement breakdown showing counts for each placement (1st-8th)
*/
function PlacementBreakdown({
placements,
}: {
placements: {
first: number;
second: number;
third: number;
fourth: number;
fifth: number;
sixth: number;
seventh: number;
eighth: number;
};
}) {
const items = [
{ label: "1st", count: placements.first, color: "text-yellow-600" },
{ label: "2nd", count: placements.second, color: "text-gray-500" },
{ label: "3rd", count: placements.third, color: "text-orange-600" },
{ label: "4th", count: placements.fourth, color: "text-blue-600" },
{ label: "5th", count: placements.fifth, color: "text-purple-600" },
{ label: "6th", count: placements.sixth, color: "text-green-600" },
{ label: "7th", count: placements.seventh, color: "text-pink-600" },
{ label: "8th", count: placements.eighth, color: "text-indigo-600" },
];
// Only show placements that have counts > 0
const nonZero = items.filter((item) => item.count > 0);
if (nonZero.length === 0) {
return <span className="text-muted-foreground text-sm">None yet</span>;
}
return (
<div className="flex flex-wrap gap-2 justify-center">
{nonZero.map((item) => (
<span
key={item.label}
className={`text-sm font-medium ${item.color}`}
title={`${item.count} ${item.label} place ${item.count === 1 ? 'finish' : 'finishes'}`}
>
{item.label}×{item.count}
</span>
))}
</div>
);
}