mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
407 lines
11 KiB
TypeScript
407 lines
11 KiB
TypeScript
import React from 'react';
|
|
import { clsx } from 'clsx';
|
|
|
|
export interface TableProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Table container using divs for better responsiveness and control
|
|
*/
|
|
export const Table = React.forwardRef<HTMLDivElement, TableProps>(
|
|
({ className, children, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx('w-full border rounded-lg overflow-hidden', 'bg-background', className)}
|
|
role="table"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
Table.displayName = 'Table';
|
|
|
|
export interface TableHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Table header container
|
|
*/
|
|
export const TableHeader = React.forwardRef<HTMLDivElement, TableHeaderProps>(
|
|
({ className, children, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
'bg-muted/50 border-b',
|
|
'grid gap-4 px-4 py-3',
|
|
'text-xs font-medium text-muted-foreground uppercase tracking-wider',
|
|
className
|
|
)}
|
|
role="rowgroup"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
TableHeader.displayName = 'TableHeader';
|
|
|
|
export interface TableRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
selected?: boolean;
|
|
interactive?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Table row using div
|
|
*/
|
|
export const TableRow = React.forwardRef<HTMLDivElement, TableRowProps>(
|
|
({ className, children, selected, interactive = false, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
'grid gap-4 px-4 py-3 border-b border-border last:border-b-0',
|
|
'bg-background',
|
|
'transition-colors',
|
|
{
|
|
'bg-primary/5 border-primary/20': selected,
|
|
'hover:bg-muted/50 cursor-pointer': interactive && !selected,
|
|
'hover:bg-primary/10': interactive && selected,
|
|
},
|
|
className
|
|
)}
|
|
role="row"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
TableRow.displayName = 'TableRow';
|
|
|
|
export interface TableHeadProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
sortable?: boolean;
|
|
sorted?: 'asc' | 'desc' | null;
|
|
onSort?: () => void;
|
|
width?: string | number;
|
|
minWidth?: string | number;
|
|
}
|
|
|
|
/**
|
|
* Table header cell
|
|
*/
|
|
export const TableHead = React.forwardRef<HTMLDivElement, TableHeadProps>(
|
|
(
|
|
{ className, children, sortable = false, sorted = null, onSort, width, minWidth, ...props },
|
|
ref
|
|
) => {
|
|
const style: React.CSSProperties = {};
|
|
if (width) style.width = typeof width === 'number' ? `${width}px` : width;
|
|
if (minWidth) style.minWidth = typeof minWidth === 'number' ? `${minWidth}px` : minWidth;
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
'flex items-center gap-2',
|
|
{
|
|
'cursor-pointer select-none hover:text-foreground': sortable,
|
|
},
|
|
className
|
|
)}
|
|
style={style}
|
|
role="columnheader"
|
|
onClick={sortable ? onSort : undefined}
|
|
{...props}
|
|
>
|
|
{children}
|
|
{sortable && (
|
|
<span className="text-muted-foreground/50">
|
|
{sorted === 'asc' ? '↑' : sorted === 'desc' ? '↓' : '⇅'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
TableHead.displayName = 'TableHead';
|
|
|
|
export interface TableCellProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
width?: string | number;
|
|
minWidth?: string | number;
|
|
align?: 'left' | 'center' | 'right';
|
|
}
|
|
|
|
/**
|
|
* Table cell
|
|
*/
|
|
export const TableCell = React.forwardRef<HTMLDivElement, TableCellProps>(
|
|
({ className, children, width, minWidth, align = 'left', ...props }, ref) => {
|
|
const style: React.CSSProperties = {};
|
|
if (width) style.width = typeof width === 'number' ? `${width}px` : width;
|
|
if (minWidth) style.minWidth = typeof minWidth === 'number' ? `${minWidth}px` : minWidth;
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
'flex items-center',
|
|
{
|
|
'justify-start': align === 'left',
|
|
'justify-center': align === 'center',
|
|
'justify-end': align === 'right',
|
|
},
|
|
className
|
|
)}
|
|
style={style}
|
|
role="cell"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
TableCell.displayName = 'TableCell';
|
|
|
|
export interface TableBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Table body container
|
|
*/
|
|
export const TableBody = React.forwardRef<HTMLDivElement, TableBodyProps>(
|
|
({ className, children, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx('divide-y divide-border', className)}
|
|
role="rowgroup"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
TableBody.displayName = 'TableBody';
|
|
|
|
export interface TableEmptyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
message?: string;
|
|
description?: string;
|
|
action?: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Empty state for table
|
|
*/
|
|
export const TableEmpty = React.forwardRef<HTMLDivElement, TableEmptyProps>(
|
|
({ className, message = 'No data available', description, action, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
'flex flex-col items-center justify-center py-12 px-4 text-center',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<p className="text-sm font-medium text-foreground mb-1">{message}</p>
|
|
{description && <p className="text-sm text-muted-foreground mb-4">{description}</p>}
|
|
{action && <div className="mt-2">{action}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
TableEmpty.displayName = 'TableEmpty';
|
|
|
|
/**
|
|
* Responsive table wrapper that switches to card view on mobile
|
|
*/
|
|
export interface ResponsiveTableProps<T> {
|
|
data: T[];
|
|
columns: Array<{
|
|
key: string;
|
|
header: string;
|
|
render: (item: T, index: number) => React.ReactNode;
|
|
width?: string | number;
|
|
minWidth?: string | number;
|
|
sortable?: boolean;
|
|
align?: 'left' | 'center' | 'right';
|
|
}>;
|
|
onSort?: (key: string, direction: 'asc' | 'desc') => void;
|
|
sortedColumn?: string | null;
|
|
sortDirection?: 'asc' | 'desc' | null;
|
|
emptyMessage?: string;
|
|
emptyDescription?: string;
|
|
emptyAction?: React.ReactNode;
|
|
selectedRows?: Set<string>;
|
|
onRowSelect?: (id: string) => void;
|
|
getRowId?: (item: T) => string;
|
|
renderMobileCard?: (item: T, index: number) => React.ReactNode;
|
|
isLoading?: boolean;
|
|
loadingRows?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function ResponsiveTable<T>({
|
|
data,
|
|
columns,
|
|
onSort,
|
|
sortedColumn = null,
|
|
sortDirection = null,
|
|
emptyMessage = 'No data available',
|
|
emptyDescription,
|
|
emptyAction,
|
|
selectedRows,
|
|
onRowSelect,
|
|
getRowId,
|
|
renderMobileCard,
|
|
isLoading = false,
|
|
loadingRows = 5,
|
|
className,
|
|
}: ResponsiveTableProps<T>) {
|
|
const handleSort = (key: string) => {
|
|
if (!onSort) return;
|
|
const newDirection = sortedColumn === key && sortDirection === 'asc' ? 'desc' : 'asc';
|
|
onSort(key, newDirection);
|
|
};
|
|
|
|
const gridTemplateColumns = columns
|
|
.map((col) => {
|
|
if (col.width) {
|
|
return typeof col.width === 'number' ? `${col.width}px` : col.width;
|
|
}
|
|
return '1fr';
|
|
})
|
|
.join(' ');
|
|
|
|
if (isLoading) {
|
|
// Loading skeleton
|
|
return (
|
|
<Table className={className}>
|
|
<TableHeader style={{ gridTemplateColumns }} className="grid">
|
|
{columns.map((column) => (
|
|
<TableHead key={column.key} width={column.width} minWidth={column.minWidth}>
|
|
<div className="h-4 w-20 bg-muted animate-pulse rounded" />
|
|
</TableHead>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{Array.from({ length: loadingRows }).map((_, index) => (
|
|
<TableRow key={`loading-${index}`} style={{ gridTemplateColumns }} className="grid">
|
|
{columns.map((column) => (
|
|
<TableCell key={column.key}>
|
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
);
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
return (
|
|
<Table className={className}>
|
|
<TableEmpty message={emptyMessage} description={emptyDescription} action={emptyAction} />
|
|
</Table>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop Table View */}
|
|
<div className="hidden md:block">
|
|
<Table className={className}>
|
|
<TableHeader style={{ gridTemplateColumns }} className="grid">
|
|
{columns.map((column) => (
|
|
<TableHead
|
|
key={column.key}
|
|
sortable={column.sortable}
|
|
sorted={sortedColumn === column.key ? sortDirection : null}
|
|
onSort={column.sortable ? () => handleSort(column.key) : undefined}
|
|
width={column.width}
|
|
minWidth={column.minWidth}
|
|
align={column.align}
|
|
>
|
|
{column.header}
|
|
</TableHead>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((item, index) => {
|
|
const rowId = getRowId ? getRowId(item) : String(index);
|
|
const isSelected = selectedRows?.has(rowId) ?? false;
|
|
|
|
return (
|
|
<TableRow
|
|
key={rowId}
|
|
selected={isSelected}
|
|
interactive={!!onRowSelect}
|
|
onClick={onRowSelect ? () => onRowSelect(rowId) : undefined}
|
|
style={{ gridTemplateColumns }}
|
|
className="grid"
|
|
>
|
|
{columns.map((column) => (
|
|
<TableCell
|
|
key={column.key}
|
|
width={column.width}
|
|
minWidth={column.minWidth}
|
|
align={column.align}
|
|
>
|
|
{column.render(item, index)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Mobile Card View */}
|
|
<div className="md:hidden space-y-4">
|
|
{data.map((item, index) => {
|
|
if (renderMobileCard) {
|
|
return (
|
|
<React.Fragment key={getRowId ? getRowId(item) : index}>
|
|
{renderMobileCard(item, index)}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
// Default mobile card rendering
|
|
return (
|
|
<div
|
|
key={getRowId ? getRowId(item) : index}
|
|
className="border rounded-lg p-4 space-y-3 bg-card"
|
|
>
|
|
{columns.map((column) => (
|
|
<div key={column.key} className="flex flex-col">
|
|
<span className="text-xs font-medium text-muted-foreground mb-1">
|
|
{column.header}
|
|
</span>
|
|
<div className="text-sm text-foreground">{column.render(item, index)}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|