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:
v0andcetinmustafa83 committed 2026-07-31 13:46:13 +00:00
1 parent 36a6206c3b
commit 8b33280c20
17 files changed
+3061 -1

No files matched your search

+198
View File
@@ -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>
);
}