Co-authored-by: cetinmustafa83 <30570791+cetinmustafa83@users.noreply.github.com>
199 lines
6.1 KiB
TypeScript
199 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { Input } from '@/components/ui/input';
|
|
import { ChevronDown, MoreHorizontal, Search } from 'lucide-react';
|
|
|
|
export interface DataTableColumn<T> {
|
|
key: keyof T;
|
|
label: string;
|
|
sortable?: boolean;
|
|
filterable?: boolean;
|
|
render?: (value: any, row: T) => React.ReactNode;
|
|
width?: string;
|
|
}
|
|
|
|
export interface DataTableAction<T> {
|
|
label: string;
|
|
onClick: (row: T) => void;
|
|
variant?: 'default' | 'destructive';
|
|
}
|
|
|
|
export interface DataTableProps<T> {
|
|
columns: DataTableColumn<T>[];
|
|
data: T[];
|
|
actions?: DataTableAction<T>[];
|
|
onRowClick?: (row: T) => void;
|
|
searchableFields?: (keyof T)[];
|
|
isLoading?: boolean;
|
|
rowKey?: keyof T;
|
|
}
|
|
|
|
export function DataTable<T extends Record<string, any>>({
|
|
columns,
|
|
data,
|
|
actions,
|
|
onRowClick,
|
|
searchableFields = [],
|
|
isLoading = false,
|
|
rowKey = 'id' as keyof T,
|
|
}: DataTableProps<T>) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [sortConfig, setSortConfig] = useState<{
|
|
key: keyof T;
|
|
direction: 'asc' | 'desc';
|
|
} | null>(null);
|
|
|
|
// Filter data based on search
|
|
const filteredData = useMemo(() => {
|
|
if (!searchTerm || searchableFields.length === 0) return data;
|
|
|
|
return data.filter((row) =>
|
|
searchableFields.some((field) => {
|
|
const value = row[field];
|
|
return value?.toString().toLowerCase().includes(searchTerm.toLowerCase());
|
|
})
|
|
);
|
|
}, [data, searchTerm, searchableFields]);
|
|
|
|
// Sort data
|
|
const sortedData = useMemo(() => {
|
|
if (!sortConfig) return filteredData;
|
|
|
|
return [...filteredData].sort((a, b) => {
|
|
const aVal = a[sortConfig.key];
|
|
const bVal = b[sortConfig.key];
|
|
|
|
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
|
|
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
}, [filteredData, sortConfig]);
|
|
|
|
const handleSort = (key: keyof T) => {
|
|
setSortConfig((prev) =>
|
|
prev?.key === key && prev.direction === 'asc'
|
|
? { key, direction: 'desc' }
|
|
: { key, direction: 'asc' }
|
|
);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return <div className="text-center py-8 text-gray-500">Loading...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{searchableFields.length > 0 && (
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
|
|
<Input
|
|
placeholder="Search..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-gray-50">
|
|
{columns.map((col) => (
|
|
<TableHead
|
|
key={String(col.key)}
|
|
style={{ width: col.width }}
|
|
className="font-semibold text-gray-900"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span>{col.label}</span>
|
|
{col.sortable && (
|
|
<button
|
|
onClick={() => handleSort(col.key)}
|
|
className="hover:bg-gray-100 p-1 rounded"
|
|
>
|
|
<ChevronDown
|
|
className="h-4 w-4 text-gray-400"
|
|
style={{
|
|
transform:
|
|
sortConfig?.key === col.key && sortConfig.direction === 'desc'
|
|
? 'rotate(180deg)'
|
|
: 'rotate(0deg)',
|
|
}}
|
|
/>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</TableHead>
|
|
))}
|
|
{actions && <TableHead className="w-12"></TableHead>}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{sortedData.map((row, idx) => (
|
|
<TableRow
|
|
key={String(row[rowKey]) || idx}
|
|
onClick={() => onRowClick?.(row)}
|
|
className="cursor-pointer hover:bg-gray-50"
|
|
>
|
|
{columns.map((col) => (
|
|
<TableCell key={String(col.key)} className="text-gray-700">
|
|
{col.render ? col.render(row[col.key], row) : row[col.key]}
|
|
</TableCell>
|
|
))}
|
|
{actions && (
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{actions.map((action) => (
|
|
<DropdownMenuItem
|
|
key={action.label}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
action.onClick(row);
|
|
}}
|
|
className={
|
|
action.variant === 'destructive' ? 'text-red-600' : ''
|
|
}
|
|
>
|
|
{action.label}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{sortedData.length === 0 && (
|
|
<div className="text-center py-8 text-gray-500">No data found</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|