2026-03-22 11:05:13 -07:00
|
|
|
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;
|
|
|
|
|
|
2026-03-22 20:41:44 -07:00
|
|
|
return data.toSorted((a, b) => {
|
2026-03-22 11:05:13 -07:00
|
|
|
// Use custom comparator if provided
|
|
|
|
|
if (comparators?.[key]) {
|
2026-03-22 20:41:44 -07:00
|
|
|
return comparators[key]?.(a, b, dir) ?? 0;
|
2026-03-22 11:05:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
}
|