feat: otimização de performance e ajustes finais

This commit is contained in:
Idrissa Banora
2026-05-18 10:49:32 +00:00
commit 52a7c4f9cf
579 changed files with 156489 additions and 0 deletions
@@ -0,0 +1,225 @@
import { useState } from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface Column<T> {
key: string;
header: string;
cell: (item: T) => React.ReactNode;
sortable?: boolean;
className?: string;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
searchPlaceholder?: string;
searchKey?: keyof T;
onRowClick?: (item: T) => void;
isLoading?: boolean;
emptyMessage?: string;
pageSize?: number;
}
export function DataTable<T extends { id: string }>({
data,
columns,
searchPlaceholder = 'Pesquisar...',
searchKey,
onRowClick,
isLoading = false,
emptyMessage = 'Nenhum registro encontrado',
pageSize: initialPageSize = 10,
}: DataTableProps<T>) {
const [search, setSearch] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(initialPageSize);
const filteredData = searchKey
? data.filter((item) =>
String(item[searchKey])
.toLowerCase()
.includes(search.toLowerCase())
)
: data;
const totalPages = Math.ceil(filteredData.length / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const paginatedData = filteredData.slice(startIndex, startIndex + pageSize);
const handlePageChange = (page: number) => {
setCurrentPage(Math.min(Math.max(1, page), totalPages));
};
if (isLoading) {
return (
<div className="gov-card">
<div className="p-8 text-center">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full mx-auto" />
<p className="mt-4 text-muted-foreground">Carregando dados...</p>
</div>
</div>
);
}
return (
<div className="gov-card animate-fade-in">
{/* Search and filters */}
<div className="p-4 border-b border-border">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="relative w-full sm:w-80">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={searchPlaceholder}
value={search}
onChange={(e) => {
setSearch(e.target.value);
setCurrentPage(1);
}}
className="pl-9"
/>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{filteredData.length} registros</span>
</div>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
{columns.map((column) => (
<TableHead
key={column.key}
className={cn('font-semibold text-foreground', column.className)}
>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{paginatedData.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-32 text-center text-muted-foreground"
>
{emptyMessage}
</TableCell>
</TableRow>
) : (
paginatedData.map((item) => (
<TableRow
key={item.id}
className={cn(
'table-row-hover',
onRowClick && 'cursor-pointer'
)}
onClick={() => onRowClick?.(item)}
>
{columns.map((column) => (
<TableCell key={column.key} className={column.className}>
{column.cell(item)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
{filteredData.length > 0 && (
<div className="p-4 border-t border-border">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Linhas por página:</span>
<Select
value={String(pageSize)}
onValueChange={(value) => {
setPageSize(Number(value));
setCurrentPage(1);
}}
>
<SelectTrigger className="w-16 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages}
</span>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
)}
</div>
);
}