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;
}
type SortColumn = "pick" | "points";
type SortColumn = "pick" | "points" | "sport" | "participant";
type SortDirection = "asc" | "desc";
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" />;
}
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(
picks: TeamScoreBreakdownProps["breakdown"]["picks"],
sortColumn: SortColumn,
sortDirection: SortDirection,
) {
const dir = sortDirection === "asc" ? 1 : -1;
return picks.slice().sort((a, b) => {
return picks.toSorted((a, b) => {
if (sortColumn === "pick") {
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
const pointsDiff = a.points - b.points;
if (pointsDiff !== 0) return pointsDiff * dir;
@ -142,16 +171,14 @@ export function TeamScoreBreakdown({
<TableHeader>
<TableRow>
<TableHead className="min-w-[90px]">
<button
onClick={() => handleSort("pick")}
aria-sort={sortColumn === "pick" ? (sortDirection === "asc" ? "ascending" : "descending") : "none"}
className="flex items-center hover:text-foreground cursor-pointer"
>
Pick #<SortIndicator column="pick" sortColumn={sortColumn} sortDirection={sortDirection} />
</button>
<SortableHeader column="pick" label="Pick #" sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort} />
</TableHead>
<TableHead>
<SortableHeader column="sport" label="Sport" sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort} />
</TableHead>
<TableHead>
<SortableHeader column="participant" label="Participant" sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort} />
</TableHead>
<TableHead>Sport</TableHead>
<TableHead>Participant</TableHead>
<TableHead className="text-center">Position</TableHead>
<TableHead className="text-right">
<button