brackt/app/components/standings/StandingsTable.tsx

184 lines
5.4 KiB
TypeScript
Raw Normal View History

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[];
showPlacementBreakdown?: boolean;
}
/**
* Display team standings with ranking, points, and placement breakdown
* Phase 4.1: Enhanced standings table with tiebreakers
*/
export function StandingsTable({
standings,
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">{standing.teamName}</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>
);
}