Files
aggios.app/front-end-agency/components/ui/DataTable.tsx
2025-12-29 17:23:59 -03:00

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>
);
}