168 lines
4.5 KiB
TypeScript
168 lines
4.5 KiB
TypeScript
import * as React from 'react';
|
|
|
|
export interface TableCell {
|
|
value: string;
|
|
sortKey: string | number;
|
|
}
|
|
|
|
export interface TableData {
|
|
id: string;
|
|
cells: TableCell[];
|
|
}
|
|
|
|
export interface TableHeader {
|
|
label: string;
|
|
key: string;
|
|
align: 'left' | 'right';
|
|
}
|
|
|
|
interface SortableTableProps {
|
|
headers: TableHeader[];
|
|
data: TableData[];
|
|
defaultSortKey: string;
|
|
defaultSortOrder: 'asc' | 'desc';
|
|
}
|
|
|
|
type SortOrder = 'asc' | 'desc';
|
|
|
|
export const SortableTable: React.FC<SortableTableProps> = ({
|
|
headers,
|
|
data,
|
|
defaultSortKey,
|
|
defaultSortOrder,
|
|
}) => {
|
|
const [sortKey, setSortKey] = React.useState<string>(defaultSortKey);
|
|
const [sortOrder, setSortOrder] = React.useState<SortOrder>(defaultSortOrder);
|
|
|
|
const handleSort = (headerKey: string) => {
|
|
if (sortKey === headerKey) {
|
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
setSortKey(headerKey);
|
|
setSortOrder('desc');
|
|
}
|
|
};
|
|
|
|
const sortedData = React.useMemo(() => {
|
|
const headerIndex = headers.findIndex((h) => h.key === sortKey);
|
|
if (headerIndex === -1) return data;
|
|
|
|
return [...data].sort((a, b) => {
|
|
const aValue = a.cells[headerIndex]?.sortKey;
|
|
const bValue = b.cells[headerIndex]?.sortKey;
|
|
|
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
const comparison = aValue.localeCompare(bValue);
|
|
return sortOrder === 'asc' ? comparison : -comparison;
|
|
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
const comparison = aValue - bValue;
|
|
return sortOrder === 'asc' ? comparison : -comparison;
|
|
}
|
|
return 0;
|
|
});
|
|
}, [data, sortKey, sortOrder, headers]);
|
|
|
|
const gridStyle: React.CSSProperties = {
|
|
borderRadius: '0.5rem',
|
|
border: '1px solid #e2e8f0',
|
|
backgroundColor: 'white',
|
|
boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
overflow: 'hidden',
|
|
};
|
|
|
|
const containerStyle: React.CSSProperties = {
|
|
display: 'grid',
|
|
gridTemplateColumns: '1fr auto auto',
|
|
};
|
|
|
|
const headerStyle: React.CSSProperties = {
|
|
backgroundColor: '#f8fafc',
|
|
padding: '0.75rem 1rem',
|
|
fontSize: '0.875rem',
|
|
fontWeight: 500,
|
|
color: '#334155',
|
|
cursor: 'pointer',
|
|
userSelect: 'none',
|
|
};
|
|
|
|
const cellStyle: React.CSSProperties = {
|
|
padding: '0.25rem 1rem',
|
|
borderRight: '1px solid #e2e8f0',
|
|
};
|
|
|
|
const getSortIndicator = (headerKey: string) => {
|
|
if (sortKey !== headerKey) {
|
|
return (
|
|
<span
|
|
style={{ opacity: 0.5, marginLeft: '0.25rem', fontSize: '0.75rem' }}
|
|
>
|
|
▼
|
|
</span>
|
|
);
|
|
}
|
|
return (
|
|
<span style={{ marginLeft: '0.25rem', fontSize: '0.75rem' }}>
|
|
{sortOrder === 'asc' ? '▲' : '▼'}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={gridStyle}>
|
|
<div style={containerStyle}>
|
|
{/* Header Row */}
|
|
<div className="contents">
|
|
{headers.map((header, index) => (
|
|
<div
|
|
key={header.key}
|
|
style={{
|
|
...headerStyle,
|
|
textAlign: header.align,
|
|
...(index === headers.length - 1
|
|
? { borderRight: 'none' }
|
|
: {}),
|
|
}}
|
|
onClick={() => handleSort(header.key)}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.backgroundColor = '#f1f5f9';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.backgroundColor = '#f8fafc';
|
|
}}
|
|
>
|
|
{header.label}
|
|
{getSortIndicator(header.key)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Data Rows */}
|
|
{sortedData.map((row) => (
|
|
<div key={row.id} className="group contents">
|
|
{row.cells.map((cell, cellIndex) => (
|
|
<div
|
|
key={cellIndex}
|
|
style={{
|
|
...cellStyle,
|
|
textAlign: headers[cellIndex].align,
|
|
fontSize: '0.875rem',
|
|
fontWeight: cellIndex === 0 ? 500 : 400,
|
|
color: cellIndex === 0 ? '#0f172a' : '#64748b',
|
|
...(cellIndex === row.cells.length - 1
|
|
? { borderRight: 'none' }
|
|
: {}),
|
|
}}
|
|
className="transition-colors duration-150 group-hover:bg-slate-50"
|
|
>
|
|
{cell.value}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SortableTable;
|