Add sortable Sport and Participant columns to team breakdown table (#208)

- Sort by sport (secondary: participant) or participant name
- Extract SortableHeader component to reduce header button repetition
- Use toSorted() instead of slice().sort() (oxlint fix)
- Use localeCompare with sensitivity: 'base' for case-insensitive string sorts

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-22 20:30:17 -07:00 committed by GitHub
parent 194f9087a3
commit b672c50d78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -39,7 +39,7 @@ interface TeamScoreBreakdownProps {
} | null; } | null;
} }
type SortColumn = "pick" | "points"; type SortColumn = "pick" | "points" | "sport" | "participant";
type SortDirection = "asc" | "desc"; type SortDirection = "asc" | "desc";
function SortIndicator({ column, sortColumn, sortDirection }: { column: SortColumn; sortColumn: SortColumn; sortDirection: SortDirection }) { function SortIndicator({ column, sortColumn, sortDirection }: { column: SortColumn; sortColumn: SortColumn; sortDirection: SortDirection }) {
@ -49,16 +49,45 @@ function SortIndicator({ column, sortColumn, sortDirection }: { column: SortColu
: <ArrowDown className="ml-1 h-3 w-3" />; : <ArrowDown className="ml-1 h-3 w-3" />;
} }
function SortableHeader({ column, label, sortColumn, sortDirection, onSort, className }: {
column: SortColumn;
label: string;
sortColumn: SortColumn;
sortDirection: SortDirection;
onSort: (column: SortColumn) => void;
className?: string;
}) {
return (
<button
onClick={() => onSort(column)}
aria-sort={sortColumn === column ? (sortDirection === "asc" ? "ascending" : "descending") : "none"}
className={`flex items-center hover:text-foreground cursor-pointer ${className ?? ""}`}
>
{label}<SortIndicator column={column} sortColumn={sortColumn} sortDirection={sortDirection} />
</button>
);
}
const cmp = (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: "base" });
function sortPicks( function sortPicks(
picks: TeamScoreBreakdownProps["breakdown"]["picks"], picks: TeamScoreBreakdownProps["breakdown"]["picks"],
sortColumn: SortColumn, sortColumn: SortColumn,
sortDirection: SortDirection, sortDirection: SortDirection,
) { ) {
const dir = sortDirection === "asc" ? 1 : -1; const dir = sortDirection === "asc" ? 1 : -1;
return picks.slice().sort((a, b) => { return picks.toSorted((a, b) => {
if (sortColumn === "pick") { if (sortColumn === "pick") {
return (a.pickNumber - b.pickNumber) * dir; return (a.pickNumber - b.pickNumber) * dir;
} }
if (sortColumn === "sport") {
const sportCmp = cmp(a.participant.sport, b.participant.sport) * dir;
if (sportCmp !== 0) return sportCmp;
return cmp(a.participant.name, b.participant.name) * dir;
}
if (sortColumn === "participant") {
return cmp(a.participant.name, b.participant.name) * dir;
}
// points: sort by actual first, then projected // points: sort by actual first, then projected
const pointsDiff = a.points - b.points; const pointsDiff = a.points - b.points;
if (pointsDiff !== 0) return pointsDiff * dir; if (pointsDiff !== 0) return pointsDiff * dir;
@ -142,16 +171,14 @@ export function TeamScoreBreakdown({
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="min-w-[90px]"> <TableHead className="min-w-[90px]">
<button <SortableHeader column="pick" label="Pick #" sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort} />
onClick={() => handleSort("pick")} </TableHead>
aria-sort={sortColumn === "pick" ? (sortDirection === "asc" ? "ascending" : "descending") : "none"} <TableHead>
className="flex items-center hover:text-foreground cursor-pointer" <SortableHeader column="sport" label="Sport" sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort} />
> </TableHead>
Pick #<SortIndicator column="pick" sortColumn={sortColumn} sortDirection={sortDirection} /> <TableHead>
</button> <SortableHeader column="participant" label="Participant" sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort} />
</TableHead> </TableHead>
<TableHead>Sport</TableHead>
<TableHead>Participant</TableHead>
<TableHead className="text-center">Position</TableHead> <TableHead className="text-center">Position</TableHead>
<TableHead className="text-right"> <TableHead className="text-right">
<button <button