"use client"; import { Fragment, useEffect, useMemo, useRef, useState } from "react"; import clsx from "clsx"; import { ChevronUp, ArrowDownUp, ArrowDownWideNarrow, ArrowDownNarrowWide, } from "lucide-react"; /* ================= TYPES ================= */ interface ColumnType { title: string | React.ReactNode; render: (row: T, index: number, path?: number[]) => React.ReactNode; className?: string; checkBox?: boolean; fixedLeft?: boolean; fixedRight?: boolean; maxWidth?: number | string; sortTable?: string; } interface PropsTable { data: T[]; column: ColumnType[]; fixedHeader?: boolean; activeHeader?: boolean; handleCheckedAll?: (e: React.ChangeEvent) => void; isCheckedAll?: boolean; handleCheckedRow?: (e: React.ChangeEvent, row: T) => void; handleIsCheckedRow?: (row: T) => boolean; rowKey: (row: T) => React.Key; getChildren?: (row: T) => T[] | undefined; useIndexPathAsKey?: boolean; } /* ================= COMPONENT ================= */ export default function Table({ data, column, fixedHeader, activeHeader, handleCheckedAll, isCheckedAll, handleCheckedRow, handleIsCheckedRow, rowKey, getChildren, useIndexPathAsKey, }: PropsTable) { const tableRef = useRef(null); const thRefs = useRef<(HTMLTableCellElement | null)[]>([]); const [expandedRows, setExpandedRows] = useState([]); const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" | null; }>({ key: "", direction: null }); /* ================= SORT ================= */ const handleSort = (key: string) => { setSortConfig((prev) => { if (prev.key === key) { const next = prev.direction === "asc" ? "desc" : prev.direction === "desc" ? null : "asc"; return { key, direction: next }; } return { key, direction: "asc" }; }); }; const sortedData = useMemo(() => { if (!sortConfig.key || !sortConfig.direction) return data; const copy = [...data]; copy.sort((a: any, b: any) => { const aVal = a?.[sortConfig.key]; const bVal = b?.[sortConfig.key]; if (typeof aVal === "number" && typeof bVal === "number") { return sortConfig.direction === "asc" ? aVal - bVal : bVal - aVal; } if (typeof aVal === "string" && typeof bVal === "string") { return sortConfig.direction === "asc" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); } return 0; }); return copy; }, [data, sortConfig]); /* ================= EXPAND ================= */ const toggleExpand = (key: React.Key) => { setExpandedRows((prev) => prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key], ); }; /* ================= ROWS ================= */ const renderRows = (rows: T[], path: number[] = []): React.ReactNode => rows.map((row, i) => { const currentPath = [...path, i]; const keyPath = useIndexPathAsKey ? currentPath.join(".") : rowKey(row); const children = getChildren?.(row); const expanded = expandedRows.includes(keyPath); return ( {getChildren && ( {!!children?.length && ( )} )} {column.map((col, idx) => (
{col.checkBox && ( handleCheckedRow?.(e, row)} checked={handleIsCheckedRow?.(row) ?? false} /> )} {col.render(row, i, currentPath)}
))} {expanded && children && renderRows(children, currentPath)}
); }); /* ================= UI ================= */ return (
table>thead]:sticky [&>table>thead]:top-0", activeHeader && "[&>table>thead]:bg-[#F4F7FA]", )} > {getChildren && ); })} {renderRows(sortedData)}
} {column.map((col, i) => { const isActive = col.sortTable === sortConfig.key; return ( { thRefs.current[i] = el; }} onClick={() => col.sortTable && handleSort(col.sortTable)} className={clsx( "px-4 py-3 text-left text-[15px] font-semibold whitespace-nowrap", col.sortTable && "cursor-pointer", isActive && "text-blue-600", col.fixedLeft && "sticky left-0 bg-white z-10", col.fixedRight && "sticky right-0 bg-white z-10", )} >
{col.checkBox && ( )} {col.title} {col.sortTable && (isActive ? ( sortConfig.direction === "asc" ? ( ) : sortConfig.direction === "desc" ? ( ) : ( ) ) : ( ))}
); }