import { useMemo } from "react"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table"; import { Badge } from "~/components/ui/badge"; import { TeamNameDisplay } from "~/components/ui/team-name-display"; import { PointsDisplay } from "~/components/standings/PointsDisplay"; import { useSortableData, type SortConfig, type SortDirection } from "~/hooks/useSortableData"; import { buildTiedRankChecker } from "~/lib/standings-display"; import { type TeamStanding } from "~/types/standings"; interface StandingsTableProps { standings: TeamStanding[]; leagueId: string; seasonId: string; } // Flat row shape used for sorting interface StandingRow extends TeamStanding { _sortPoints: number; } // Comparators are pure functions with no component state — define at module level const comparators = { // Rank: ties broken alphabetically by team name regardless of sort direction currentRank: (a: StandingRow, b: StandingRow, dir: SortDirection) => { const cmp = a.currentRank - b.currentRank; if (cmp !== 0) return dir === "asc" ? cmp : -cmp; return a.teamName.localeCompare(b.teamName); }, _sortPoints: (a: StandingRow, b: StandingRow, dir: SortDirection) => { const cmp = b._sortPoints - a._sortPoints; return dir === "asc" ? -cmp : cmp; }, sevenDayPointChange: (a: StandingRow, b: StandingRow, dir: SortDirection) => { const aVal = a.sevenDayPointChange ?? 0; const bVal = b.sevenDayPointChange ?? 0; const cmp = bVal - aVal; return dir === "asc" ? -cmp : cmp; }, teamName: (a: StandingRow, b: StandingRow, dir: SortDirection) => { const cmp = a.teamName.localeCompare(b.teamName); return dir === "asc" ? cmp : -cmp; }, participantsRemaining: (a: StandingRow, b: StandingRow, dir: SortDirection) => { const cmp = a.participantsRemaining - b.participantsRemaining; return dir === "asc" ? cmp : -cmp; }, }; /** * Display team standings with ranking, points, 7-day change, and sortable columns. */ export function StandingsTable({ standings, leagueId, seasonId }: StandingsTableProps) { const isTied = useMemo( () => buildTiedRankChecker(standings.map((s) => s.currentRank)), [standings] ); const rows: StandingRow[] = useMemo( () => standings.map((s) => ({ ...s, _sortPoints: s.actualPoints ?? s.totalPoints, })), [standings] ); const { sortedData, sortConfig, requestSort } = useSortableData( rows, "currentRank", "asc", comparators ); return (
requestSort("currentRank")} className="w-[110px]" /> requestSort("teamName")} />
Points
actual / projected
} sortKey="_sortPoints" sortConfig={sortConfig} onSort={() => requestSort("_sortPoints")} className="text-right" />
7-Day Change
pts / rank
} sortKey="sevenDayPointChange" sortConfig={sortConfig} onSort={() => requestSort("sevenDayPointChange")} className="text-right" /> requestSort("participantsRemaining")} className="text-right" />
{sortedData.length === 0 ? ( No standings data available ) : ( sortedData.map((standing) => (
{standing.rankChange !== 0 && ( )}
{standing.participantsRemaining > 0 ? ( {standing.participantsRemaining} remaining ) : ( Complete )}
)) )}
); } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- interface SortableHeadProps { label: React.ReactNode; sortKey: keyof StandingRow; sortConfig: SortConfig; onSort: () => void; className?: string; } function SortableHead({ label, sortKey, sortConfig, onSort, className }: SortableHeadProps) { const isActive = sortConfig.key === sortKey; const arrow = isActive ? (sortConfig.direction === "asc" ? " ↑" : " ↓") : " ↕"; return ( {label} {arrow} ); } function RankBadge({ rank, isTied }: { rank: number; isTied?: boolean }) { const t = isTied ? "T" : ""; if (rank === 1) { return ( 🏆 {t}1st ); } if (rank === 2) { return ( 🥈 {t}2nd ); } if (rank === 3) { return ( 🥉 {t}3rd ); } return ( {t}{rank} ); } function MovementIndicator({ change }: { change: number }) { if (change > 0) { return ( ↑{change} ); } if (change < 0) { return ( ↓{Math.abs(change)} ); } return null; } function SevenDayChange({ pointChange, rankChange, }: { pointChange?: number; rankChange: number; }) { const hasData = pointChange !== undefined; if (!hasData) { return ; } return (
= 0 ? "text-emerald-400" : "text-coral-accent"}`}> {pointChange >= 0 ? "+" : ""}{pointChange.toFixed(2)} pts {rankChange > 0 ? ( ↑{rankChange} rank ) : rankChange < 0 ? ( ↓{Math.abs(rankChange)} rank ) : ( — rank )}
); }