179 lines
8.4 KiB
TypeScript
179 lines
8.4 KiB
TypeScript
"use client";
|
|
|
|
import { ReactNode } from "react";
|
|
import { Button } from "./index";
|
|
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
|
|
|
|
interface Column<T> {
|
|
header: string;
|
|
accessor: keyof T | ((item: T) => ReactNode);
|
|
className?: string;
|
|
align?: 'left' | 'center' | 'right';
|
|
}
|
|
|
|
interface DataTableProps<T> {
|
|
columns: Column<T>[];
|
|
data: T[];
|
|
isLoading?: boolean;
|
|
emptyMessage?: string;
|
|
pagination?: {
|
|
currentPage: number;
|
|
totalPages: number;
|
|
onPageChange: (page: number) => void;
|
|
totalItems: number;
|
|
};
|
|
onRowClick?: (item: T) => void;
|
|
selectable?: boolean;
|
|
selectedIds?: (string | number)[];
|
|
onSelectionChange?: (ids: (string | number)[]) => void;
|
|
}
|
|
|
|
export default function DataTable<T extends { id: string | number }>({
|
|
columns,
|
|
data,
|
|
isLoading = false,
|
|
emptyMessage = "Nenhum resultado encontrado.",
|
|
pagination,
|
|
onRowClick,
|
|
selectable = false,
|
|
selectedIds = [],
|
|
onSelectionChange
|
|
}: DataTableProps<T>) {
|
|
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (!onSelectionChange) return;
|
|
if (e.target.checked) {
|
|
onSelectionChange(data.map(item => item.id));
|
|
} else {
|
|
onSelectionChange([]);
|
|
}
|
|
};
|
|
|
|
const handleSelectItem = (id: string | number) => {
|
|
if (!onSelectionChange) return;
|
|
if (selectedIds.includes(id)) {
|
|
onSelectionChange(selectedIds.filter(i => i !== id));
|
|
} else {
|
|
onSelectionChange([...selectedIds, id]);
|
|
}
|
|
};
|
|
|
|
const isAllSelected = data.length > 0 && selectedIds.length === data.length;
|
|
return (
|
|
<div className="w-full">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse">
|
|
<thead>
|
|
<tr className="bg-zinc-50/50 dark:bg-zinc-800/50 border-b border-zinc-200 dark:border-zinc-800">
|
|
{selectable && (
|
|
<th className="px-6 py-4 w-10 text-left">
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={isAllSelected}
|
|
onChange={handleSelectAll}
|
|
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
|
/>
|
|
</div>
|
|
</th>
|
|
)}
|
|
{columns.map((column, index) => (
|
|
<th
|
|
key={index}
|
|
className={`px-6 py-4 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider ${column.align === 'right' ? 'text-right' :
|
|
column.align === 'center' ? 'text-center' : 'text-left'
|
|
} ${column.className || ''}`}
|
|
>
|
|
{column.header}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
|
{isLoading ? (
|
|
Array.from({ length: 3 }).map((_, i) => (
|
|
<tr key={i} className="animate-pulse">
|
|
{columns.map((_, j) => (
|
|
<td key={j} className="px-6 py-4">
|
|
<div className="h-4 bg-zinc-100 dark:bg-zinc-800 rounded w-full"></div>
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))
|
|
) : data.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={columns.length + (selectable ? 1 : 0)} className="px-6 py-12 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
|
{emptyMessage}
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
data.map((item) => {
|
|
const isSelected = selectedIds.includes(item.id);
|
|
return (
|
|
<tr
|
|
key={item.id}
|
|
onClick={() => onRowClick?.(item)}
|
|
className={`transition-colors group ${isSelected ? 'bg-brand-50/30 dark:bg-brand-500/5' : ''} ${onRowClick ? 'cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/50' : 'hover:bg-zinc-50/50 dark:hover:bg-zinc-800/30'}`}
|
|
>
|
|
{selectable && (
|
|
<td className="px-6 py-4 w-10" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={() => handleSelectItem(item.id)}
|
|
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
|
/>
|
|
</div>
|
|
</td>
|
|
)}
|
|
{columns.map((column, index) => (
|
|
<td
|
|
key={index}
|
|
className={`px-6 py-4 text-sm text-zinc-600 dark:text-zinc-300 ${column.align === 'right' ? 'text-right' :
|
|
column.align === 'center' ? 'text-center' : 'text-left'
|
|
} ${column.className || ''}`}
|
|
>
|
|
{typeof column.accessor === 'function'
|
|
? column.accessor(item)
|
|
: (item[column.accessor] as ReactNode)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{pagination && (
|
|
<div className="p-4 bg-zinc-50/30 dark:bg-zinc-900/30 border-t border-zinc-200 dark:border-zinc-800 flex items-center justify-between">
|
|
<span className="text-xs text-zinc-500 italic">
|
|
Mostrando {data.length} de {pagination.totalItems} resultados
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={pagination.currentPage <= 1 || isLoading}
|
|
onClick={() => pagination.onPageChange(pagination.currentPage - 1)}
|
|
>
|
|
<ChevronLeftIcon className="w-4 h-4 mr-1" />
|
|
Anterior
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={pagination.currentPage >= pagination.totalPages || isLoading}
|
|
onClick={() => pagination.onPageChange(pagination.currentPage + 1)}
|
|
>
|
|
Próximo
|
|
<ChevronRightIcon className="w-4 h-4 ml-1" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|