feat: complete refactoring phase 1 with API, component, and business logic consolidation
Co-authored-by: cetinmustafa83 <30570791+cetinmustafa83@users.noreply.github.com>
This commit is contained in:
17 files changed
+3061
-1
No files matched your search
@@ -0,0 +1,198 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user