* Redesign standings page with sortable table, 7-day change, and chart repositioned - Move point progression chart below the standings table - Replace separate Points/Projected/Placement columns with: - Single stacked "actual / projected" Points column (shared PointsDisplay component) - "7-Day Change" column showing points gained + rank change over past 7 days - Remove Placement breakdown column - Sort ties alphabetically by team name (rank sort only) - All columns are sortable via new reusable useSortableData hook - Add sevenDayPointChange to TeamStandingWithChange type and model https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7 * Code review fixes: module-level comparators, correct type on SortableHead, handle negative point change, remove redundant spread - Move comparators object to module level (closes over nothing, no useMemo needed) - Use SortConfig<StandingRow> on SortableHead instead of inline duplicate type - SevenDayChange: handle negative pointChange with correct sign and color - Remove redundant sevenDayPointChange explicit assignment (already in ...standing spread) https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7 * Update StandingsTable tests to match redesigned component - Remove showPlacementBreakdown prop (no longer exists on component) - Replace Placement Breakdown test suite with 7-Day Change column tests - Update header assertion: "Placements" → "7-Day Change" - Add tests for positive/negative point change display and rank change indicators - Remove accessibility test for placement title attributes https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7 * Fix movement indicator arrow count assertion to exclude sort header arrows queryAllByText(/↑|↓/) was matching the active sort column's ↑ indicator. Narrow to /[↑↓]\d/ so only movement indicators (↑1, ↓1) are counted. https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7 --------- Co-authored-by: Claude <noreply@anthropic.com>
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
import { useState, useMemo } from "react";
|
|
|
|
export type SortDirection = "asc" | "desc";
|
|
|
|
export interface SortConfig<T> {
|
|
key: keyof T | null;
|
|
direction: SortDirection;
|
|
}
|
|
|
|
type Comparators<T> = Partial<Record<keyof T, (a: T, b: T, direction: SortDirection) => number>>;
|
|
|
|
/**
|
|
* Generic hook for client-side sortable table data.
|
|
*
|
|
* @param data - The array of items to sort
|
|
* @param defaultKey - The column key to sort by initially (null = no sort)
|
|
* @param defaultDirection - Initial sort direction
|
|
* @param comparators - Optional per-key custom comparators. Receives both items and the
|
|
* current direction so the comparator can handle ascending/descending internally
|
|
* (return positive to sort a after b).
|
|
*/
|
|
export function useSortableData<T>(
|
|
data: T[],
|
|
defaultKey: keyof T | null = null,
|
|
defaultDirection: SortDirection = "asc",
|
|
comparators?: Comparators<T>
|
|
): {
|
|
sortedData: T[];
|
|
sortConfig: SortConfig<T>;
|
|
requestSort: (key: keyof T) => void;
|
|
} {
|
|
const [sortConfig, setSortConfig] = useState<SortConfig<T>>({
|
|
key: defaultKey,
|
|
direction: defaultDirection,
|
|
});
|
|
|
|
const sortedData = useMemo(() => {
|
|
if (!sortConfig.key) return data;
|
|
|
|
const key = sortConfig.key;
|
|
const dir = sortConfig.direction;
|
|
|
|
return [...data].sort((a, b) => {
|
|
// Use custom comparator if provided
|
|
if (comparators?.[key]) {
|
|
return comparators[key]!(a, b, dir);
|
|
}
|
|
|
|
const aVal = a[key];
|
|
const bVal = b[key];
|
|
|
|
if (aVal === null || aVal === undefined) return 1;
|
|
if (bVal === null || bVal === undefined) return -1;
|
|
|
|
let cmp = 0;
|
|
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
cmp = aVal.localeCompare(bVal);
|
|
} else {
|
|
cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
}
|
|
|
|
return dir === "asc" ? cmp : -cmp;
|
|
});
|
|
}, [data, sortConfig, comparators]);
|
|
|
|
const requestSort = (key: keyof T) => {
|
|
setSortConfig((prev) => ({
|
|
key,
|
|
direction: prev.key === key && prev.direction === "asc" ? "desc" : "asc",
|
|
}));
|
|
};
|
|
|
|
return { sortedData, sortConfig, requestSort };
|
|
}
|