updated table look

This commit is contained in:
Berkay 2025-06-20 16:14:50 +03:00
parent dda9b1bb36
commit 8022f5e725
7 changed files with 1083 additions and 422 deletions

View File

@ -1,11 +1,5 @@
'use client';
import { FC, useState } from 'react';
import {
useQuery,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { FC, useState, useEffect } from 'react';
import {
flexRender,
getCoreRowModel,
@ -15,14 +9,18 @@ import {
SortingState,
createColumnHelper,
ColumnDef,
Row,
Cell,
Header,
Table,
} from '@tanstack/react-table';
import * as z from "zod";
import { apiPostFetcher } from "@/lib/fetcher";
import { API_BASE_URL } from "@/config/config";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/mutual/ui/form";
import { Input } from "@/components/mutual/ui/input";
import { Button } from "@/components/mutual/ui/button";
interface DashboardPageProps {
searchParams: Record<string, any>;
activePageUrl?: string;
@ -39,340 +37,219 @@ interface DashboardPageProps {
updateOnline?: (data: any) => void;
}
// Wrap the component with QueryClientProvider
const queryClient = new QueryClient();
const formSchema = z.object({
page: z.number().min(1, "Page must be at least 1"),
size: z.number().min(1, "Size must be at least 1"),
orderField: z.array(z.string()),
orderType: z.array(z.string()),
query: z.any()
});
const DPage: FC<DashboardPageProps> = ({
searchParams,
userData,
userLoading,
onlineData,
activePageUrl
}) => {
// Sample data for the dashboard
const statCardsData = [
{
title: 'Users',
count: '36.5k',
icon: 'bx bx-user',
iconColor: 'text-blue-500',
percentage: '4.65%',
isPositive: true
},
{
title: 'Companies',
count: '4.5k',
icon: 'bx bx-building',
iconColor: 'text-yellow-500',
percentage: '1.25%',
isPositive: false
},
{
title: 'Blogs',
count: '12.5k',
icon: 'bx bxl-blogger',
iconColor: 'text-green-500',
percentage: '2.15%',
isPositive: true
},
{
title: 'Revenue',
count: '$35.5k',
icon: 'bx bx-dollar',
iconColor: 'text-pink-500',
percentage: '3.75%',
isPositive: true
}
];
type FormValues = z.infer<typeof formSchema>;
const userRolesColumns = [
{ header: 'Name', accessor: 'name' },
{ header: 'Email', accessor: 'email' },
{
header: 'Role', accessor: 'role',
cell: (value: string) => (
<span className={`py-1 px-2 rounded-md text-xs ${value === 'Admin' ? 'bg-blue-500/10 text-blue-500' : value === 'Editor' ? 'bg-yellow-500/10 text-yellow-500' : 'bg-emerald-500/10 text-emerald-500'}`}>
{value}
</span>
)
}
];
const DPage: FC<DashboardPageProps> = ({ searchParams, activePageUrl, userData, userLoading, userError, refreshUser, updateUser, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline }) => {
const userRolesData = [
{ name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ name: 'Jane Smith', email: 'jane@example.com', role: 'Editor' },
{ name: 'Robert Johnson', email: 'robert@example.com', role: 'Customer' },
{ name: 'Emily Davis', email: 'emily@example.com', role: 'Customer' },
{ name: 'Michael Brown', email: 'michael@example.com', role: 'Editor' }
];
const form = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { page: 1, size: 10, orderField: [], orderType: [], query: {} } });
const activitiesColumns = [
{ header: 'Name', accessor: 'name' },
{ header: 'Date', accessor: 'date' },
{ header: 'Time', accessor: 'time' }
];
const [tableData, setTableData] = useState<any[]>([]);
const [sorting, setSorting] = useState<SortingState>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: form.getValues().size });
const activitiesData = [
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' }
];
useEffect(() => { setPagination({ pageIndex: 0, pageSize: form.getValues().size }) }, [form.getValues().size]);
useEffect(() => { fetchTableData(form.getValues()) }, []);
const earningsData = [
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' },
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: false, status: 'Withdrawn' },
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' },
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: false, status: 'Withdrawn' },
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' }
];
return (
<div className='p-6'>
<QueryClientProvider client={queryClient}>
<TanStackTableExample />
</QueryClientProvider>
</div>
);
};
// TanStack Table Example component
type Person = {
id: number
firstName: string
lastName: string
age: number
visits: number
status: string
progress: number
}
const TanStackTableExample: FC = () => {
// Column definitions using columnHelper
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor('firstName', {
cell: (info) => info.getValue(),
header: () => 'First Name',
footer: (info) => info.column.id,
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
cell: (info) => info.getValue(),
header: () => 'Last Name',
footer: (info) => info.column.id,
}),
columnHelper.accessor('age', {
header: () => 'Age',
cell: (info) => info.renderValue(),
footer: (info) => info.column.id,
}),
columnHelper.accessor('visits', {
header: () => 'Visits',
footer: (info) => info.column.id,
}),
columnHelper.accessor('status', {
header: 'Status',
cell: (info) => (
<span className={`py-1 px-2 rounded-md text-xs ${info.getValue() === 'Active' ? 'bg-green-500/10 text-green-500' :
info.getValue() === 'Pending' ? 'bg-yellow-500/10 text-yellow-500' :
'bg-red-500/10 text-red-500'
}`}>
{info.getValue()}
</span>
),
footer: (info) => info.column.id,
}),
columnHelper.accessor('progress', {
header: 'Profile Progress',
cell: (info) => (
<div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${info.getValue()}%` }}
></div>
</div>
),
}),
] as ColumnDef<Person>[];
// Fetch data using React Query
const fetchPeople = async (): Promise<Person[]> => {
// In a real app, this would be an API call
// For this example, we'll return mock data
return [
{
id: 1,
firstName: 'John',
lastName: 'Doe',
age: 28,
visits: 10,
status: 'Active',
progress: 80,
},
{
id: 2,
firstName: 'Jane',
lastName: 'Smith',
age: 32,
visits: 5,
status: 'Pending',
progress: 45,
},
{
id: 3,
firstName: 'Robert',
lastName: 'Johnson',
age: 45,
visits: 20,
status: 'Inactive',
progress: 30,
},
{
id: 4,
firstName: 'Emily',
lastName: 'Davis',
age: 27,
visits: 15,
status: 'Active',
progress: 95,
},
{
id: 5,
firstName: 'Michael',
lastName: 'Brown',
age: 39,
visits: 8,
status: 'Pending',
progress: 60,
},
{
id: 6,
firstName: 'Sarah',
lastName: 'Wilson',
age: 34,
visits: 12,
status: 'Active',
progress: 75,
},
{
id: 7,
firstName: 'David',
lastName: 'Miller',
age: 41,
visits: 7,
status: 'Inactive',
progress: 25,
},
{
id: 8,
firstName: 'Jennifer',
lastName: 'Taylor',
age: 29,
visits: 18,
status: 'Active',
progress: 88,
},
{
id: 9,
firstName: 'James',
lastName: 'Anderson',
age: 36,
visits: 9,
status: 'Pending',
progress: 52,
},
{
id: 10,
firstName: 'Lisa',
lastName: 'Thomas',
age: 31,
visits: 14,
status: 'Active',
progress: 70,
},
{
id: 11,
firstName: 'Richard',
lastName: 'Jackson',
age: 47,
visits: 6,
status: 'Inactive',
progress: 15,
},
{
id: 12,
firstName: 'Mary',
lastName: 'White',
age: 25,
visits: 22,
status: 'Active',
progress: 92,
},
{
id: 13,
firstName: 'Thomas',
lastName: 'Harris',
age: 38,
visits: 11,
status: 'Pending',
progress: 48,
},
{
id: 14,
firstName: 'Patricia',
lastName: 'Martin',
age: 33,
visits: 16,
status: 'Active',
progress: 83,
},
{
id: 15,
firstName: 'Charles',
lastName: 'Thompson',
age: 42,
visits: 4,
status: 'Inactive',
progress: 22,
},
];
const fetchTableData = async (values: FormValues) => {
setIsLoading(true); setError(null);
try {
const result = await apiPostFetcher({ url: `${API_BASE_URL}/test`, body: values, isNoCache: true });
if (result?.data?.data) {
setTableData(result.data.data); setPagination({ pageIndex: values.page - 1, pageSize: values.size });
} else { setTableData([]); if (result?.data?.message) { setError(result.data.message) } }
} catch (error) {
console.error('Error fetching data:', error); setTableData([]); setError('Failed to fetch data');
} finally { setIsLoading(false) }
};
const { data = [], isLoading, error } = useQuery({
queryKey: ['people'],
queryFn: fetchPeople,
});
const handleSortingChange = (columnId: string) => {
const currentSort = sorting[0];
let newSorting: SortingState = [];
const [sorting, setSorting] = useState<SortingState>([]);
if (currentSort?.id === columnId) {
if (currentSort.desc) { newSorting = [] } else { newSorting = [{ id: columnId, desc: true }] }
} else { newSorting = [{ id: columnId, desc: false }] }
setSorting(newSorting);
const orderFields = newSorting.length > 0 ? [newSorting[0].id] : [];
const orderTypes = newSorting.length > 0 ? [newSorting[0].desc ? 'desc' : 'asc'] : [];
form.setValue('orderField', orderFields);
form.setValue('orderType', orderTypes);
fetchTableData({ ...form.getValues(), orderField: orderFields, orderType: orderTypes });
};
const columnHelper = createColumnHelper<any>();
const columns = [
columnHelper.accessor('uu_id', { cell: info => info.getValue(), header: () => <span>UUID</span>, footer: info => info.column.id }),
columnHelper.accessor('process_name', { cell: info => info.getValue(), header: () => <span>Name</span>, footer: info => info.column.id }),
columnHelper.accessor('bank_date', { header: () => <span>Bank Date</span>, cell: info => info.getValue(), footer: info => info.column.id }),
columnHelper.accessor('currency_value', { header: () => <span>Currency Value</span>, cell: info => String(info.getValue()), footer: info => info.column.id }),
] as ColumnDef<any>[];
const table = useReactTable({
data,
data: tableData,
columns,
state: {
sorting,
},
state: { sorting, pagination },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
pageCount: Math.ceil(tableData.length / pagination.pageSize) || 1,
});
if (isLoading) return <div className="text-center py-4">Loading data...</div>;
if (error) return <div className="text-center py-4 text-red-500">Error loading data</div>;
if (error) return <div className="text-center py-4 text-red-500">{error}</div>;
return (
<div className="bg-white rounded-lg shadow-md overflow-hidden">
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-20">
<div className="p-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-800">People Data</h2>
<p className="text-sm text-gray-500">TanStack React Query Table Example</p>
<h2 className="text-lg font-semibold text-gray-800">Data Table</h2>
<p className="text-sm text-gray-500">TanStack Table with API Data</p>
</div>
<div className="overflow-x-auto">
<div className="p-4 border-b border-gray-200">
<Form {...form}>
<form onSubmit={form.handleSubmit((values) => fetchTableData(values as FormValues))} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="page"
render={({ field }) => (
<FormItem>
<FormLabel>Page</FormLabel>
<FormControl>
<Input type="number" placeholder="Page" {...field} onChange={e => field.onChange(Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="size"
render={({ field }) => (
<FormItem>
<FormLabel>Size</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Size"
{...field}
onChange={e => {
const newSize = Number(e.target.value);
field.onChange(newSize);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="col-span-1 md:col-span-2 flex justify-end">
<Button type="submit" disabled={isLoading}>Fetch Data</Button>
</div>
</form>
</Form>
</div>
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<Button
onClick={() => {
const currentPage = form.getValues().page;
if (currentPage > 1) {
const newValues = { ...form.getValues(), page: currentPage - 1 };
form.setValue('page', currentPage - 1);
fetchTableData(newValues);
}
}}
disabled={form.getValues().page <= 1 || isLoading}
variant="outline"
size="sm"
>
Previous
</Button>
<Button
onClick={() => {
const currentPage = form.getValues().page;
const newValues = { ...form.getValues(), page: currentPage + 1 };
form.setValue('page', currentPage + 1);
fetchTableData(newValues);
}}
disabled={isLoading || tableData.length < form.getValues().size}
variant="outline"
size="sm"
className="ml-2"
>
Next
</Button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Page <span className="font-medium">{form.getValues().page}</span> ·
Size <span className="font-medium">{form.getValues().size}</span> ·
Total <span className="font-medium">{tableData.length}</span> items
</p>
</div>
<div className="flex items-center space-x-2">
<Button
onClick={() => {
form.setValue('page', 1);
fetchTableData({ ...form.getValues(), page: 1 });
}}
disabled={form.getValues().page <= 1 || isLoading}
variant="outline"
size="sm"
>
First
</Button>
<Button
onClick={() => {
const currentPage = form.getValues().page;
if (currentPage > 1) {
const newValues = { ...form.getValues(), page: currentPage - 1 };
form.setValue('page', currentPage - 1);
fetchTableData(newValues);
}
}}
disabled={form.getValues().page <= 1 || isLoading}
variant="outline"
size="sm"
>
Previous
</Button>
<Button
onClick={() => {
const currentPage = form.getValues().page;
const newValues = { ...form.getValues(), page: currentPage + 1 };
form.setValue('page', currentPage + 1);
fetchTableData(newValues);
}}
disabled={isLoading || tableData.length < form.getValues().size}
variant="outline"
size="sm"
>
Next
</Button>
</div>
</div>
</div>
<div className="overflow-x-auto ">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
@ -381,19 +258,11 @@ const TanStackTableExample: FC = () => {
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={header.column.getToggleSortingHandler()}
onClick={() => handleSortingChange(header.column.id)}
>
<div className="flex items-center space-x-1">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
<span>
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
</span>
{flexRender(header.column.columnDef.header, header.getContext())}
<span>{{ asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] ?? null} </span>
</div>
</th>
))}
@ -414,76 +283,6 @@ const TanStackTableExample: FC = () => {
</table>
</div>
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing{' '}
<span className="font-medium">{table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}</span>{' '}
to{' '}
<span className="font-medium">
{Math.min(
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
table.getRowCount()
)}
</span>{' '}
of <span className="font-medium">{table.getRowCount()}</span> results
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">First</span>
</button>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">Previous</span>
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">Next</span>
</button>
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">Last</span>
</button>
</nav>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,443 @@
'use client';
import React from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
flexRender,
createColumnHelper,
ColumnDef,
} from "@tanstack/react-table";
import { UseFormReturn } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/mutual/ui/form";
import { Button } from "@/components/mutual/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/mutual/ui/select";
import { API_BASE_URL } from "@/config/config";
import { useTableData } from "@/hooks/useTableData";
import { LanguageTypes } from "@/validations/mutual/language/validations";
import {
Translations,
TableHeaderProps,
LoadingSpinnerProps,
ErrorDisplayProps,
MobilePaginationControlsProps,
DashboardPageProps,
TableDataItem,
} from "@/validations/mutual/table/validations";
import LoadingContent from "@/components/mutual/loader/component";
interface DataTableProps {
table: ReturnType<typeof useReactTable>;
tableData: TableDataItem[];
isLoading: boolean;
handleSortingChange: (columnId: string) => void;
getSortingIcon: (columnId: string) => React.ReactNode;
flexRender: typeof flexRender;
t: Translations;
}
interface TableFormProps {
form: UseFormReturn<any>;
handleFormSubmit: (e: React.FormEvent) => void;
handleSelectChange: (value: string, field: { onChange: (value: number) => void }) => void;
renderPageOptions: () => { key: string | number; value: string; label: string }[];
pageSizeOptions: number[];
apiPagination: {
size: number;
page: number;
totalCount: number;
totalPages: number;
pageCount: number;
};
handleFirstPage: () => void;
handlePreviousPage: () => void;
handleNextPage: () => void;
isPreviousDisabled: () => boolean;
isNextDisabled: () => boolean;
t: Translations;
}
const translations: Record<LanguageTypes, Translations> = {
en: {
dataTable: 'Data Table',
tableWithApiData: 'Table with API Data',
loading: 'Loading...',
noDataAvailable: 'No data available',
page: 'Page',
size: 'Size',
total: 'Total',
items: 'items',
first: 'First',
previous: 'Previous',
next: 'Next',
selectPage: 'Select page',
selectSize: 'Select size'
},
tr: {
dataTable: 'Veri Tablosu',
tableWithApiData: 'API Verili Tablo',
loading: 'Yükleniyor...',
noDataAvailable: 'Veri bulunamadı',
page: 'Sayfa',
size: 'Boyut',
total: 'Toplam',
items: 'öğe',
first: 'İlk',
previous: 'Önceki',
next: 'Sonraki',
selectPage: 'Sayfa seç',
selectSize: 'Boyut seç'
}
};
const DataTable: React.FC<DataTableProps> = React.memo(({
table,
tableData,
isLoading,
handleSortingChange,
getSortingIcon,
flexRender,
t
}) => {
return (
<div className="overflow-x-auto relative">
{/* Semi-transparent loading overlay that preserves interactivity */}
{isLoading && (
<div className="absolute inset-0 bg-white bg-opacity-60 flex items-center justify-center z-10">
{/* We don't put anything here as we already have the loading indicator in the header */}
</div>
)}
<table className="min-w-full divide-y divide-gray-200">
<caption className="sr-only">{t.dataTable}</caption>
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => handleSortingChange(header.column.id)}
aria-sort={header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? 'descending' : 'ascending') : undefined}
scope="col"
>
<div className="flex items-center space-x-1">
{flexRender(header.column.columnDef.header, header.getContext())}
<span>{getSortingIcon(header.column.id)}</span>
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{tableData.length === 0 && !isLoading ? (
<tr>
<td colSpan={table.getAllColumns().length} className="px-6 py-4 text-center text-gray-500">
{t.noDataAvailable}
</td>
</tr>
) : (
table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
);
});
const TableForm: React.FC<TableFormProps> = ({
form,
handleFormSubmit,
handleSelectChange,
renderPageOptions,
pageSizeOptions,
apiPagination,
handleFirstPage,
handlePreviousPage,
handleNextPage,
isPreviousDisabled,
isNextDisabled,
t
}) => {
return (
<div className="p-4 border-b border-gray-200">
<Form {...form}>
<form onSubmit={handleFormSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="md:col-span-1">
<FormField
control={form.control}
name="page"
render={({ field }) => (
<FormItem>
<FormLabel>{t.page}</FormLabel>
<Select
value={field.value.toString()}
onValueChange={(value: string) => handleSelectChange(value, field)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t.selectPage} />
</SelectTrigger>
</FormControl>
<SelectContent>
{renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
<SelectItem key={option.key} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-1">
<FormField
control={form.control}
name="size"
render={({ field }) => (
<FormItem>
<FormLabel>{t.size}</FormLabel>
<Select
value={field.value.toString()}
onValueChange={(value: string) => handleSelectChange(value, field)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t.selectSize} />
</SelectTrigger>
</FormControl>
<SelectContent>
{pageSizeOptions.map((size: number) => (
<SelectItem key={size} value={size.toString()}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-1 flex items-end">
<p className="text-sm text-gray-700">
{t.page}: <span className="font-medium">{apiPagination.page}</span><span>{" / "}</span> <span className="font-medium">{apiPagination.totalPages}</span> ·
{t.size}: <span className="font-medium">{apiPagination.pageCount}</span><span>{" / "}</span> <span className="font-medium">{apiPagination.size}</span> ·
{t.total}: <span className="font-medium">{apiPagination.totalCount}</span> {t.items}
</p>
</div>
<div className="md:col-span-1 flex items-end justify-end space-x-2">
<Button
onClick={handleFirstPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to first page"
>{t.first}</Button>
<Button
onClick={handlePreviousPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to previous page"
>{t.previous}</Button>
<Button
onClick={handleNextPage}
disabled={isNextDisabled()}
variant="outline"
size="sm"
aria-label="Go to next page"
>{t.next}</Button>
{/* <Button type="submit" disabled={isLoading} size="sm">Fetch</Button> */}
</div>
</form>
</Form>
</div>
);
};
const MobilePaginationControls: React.FC<MobilePaginationControlsProps> = ({
handlePreviousPage,
handleNextPage,
isPreviousDisabled,
isNextDisabled,
t
}) => {
return (
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:hidden">
<div className="flex-1 flex justify-between">
<Button
onClick={handlePreviousPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to previous page"
>{t.previous}</Button>
<Button
onClick={handleNextPage}
disabled={isNextDisabled()}
variant="outline"
size="sm"
className="ml-2"
aria-label="Go to next page"
>{t.next}</Button>
</div>
</div>
);
};
const TableHeader: React.FC<TableHeaderProps> = ({ title, description, isLoading, error, t }) => {
return (
<div className="p-4 border-b border-gray-200">
<div className="flex justify-between items-center">
<div>
<h2 className="text-lg font-semibold text-gray-800">{title}</h2>
<p className="text-sm text-gray-500">{description}</p>
</div>
{/* {isLoading && <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />} */}
{error && <ErrorDisplay message={error} />}
</div>
</div>
);
};
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ message }) => {
return <div className="text-red-500">{message}</div>;
};
const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((props) => {
// Initialize translation with English as default
const language = props.onlineData?.lang as LanguageTypes || 'en';
const t = translations[language];
const {
form,
tableData,
sorting,
isLoading,
error,
pagination,
apiPagination,
setSorting,
handleSortingChange,
handleSelectChange,
handlePageChange,
handleFirstPage,
handlePreviousPage,
handleNextPage,
getSortingIcon,
handleFormSubmit,
// Disabled states
isPreviousDisabled,
isNextDisabled,
pageSizeOptions,
renderPageOptions,
} = useTableData({ apiUrl: `${API_BASE_URL}/test` });
const columnHelper = createColumnHelper<TableDataItem>();
const columns = React.useMemo(() => [
columnHelper.accessor('uu_id', {
cell: info => info.getValue(),
header: () => <span>UUID</span>,
footer: info => info.column.id
}),
columnHelper.accessor('process_name', {
cell: info => info.getValue(),
header: () => <span>Name</span>,
footer: info => info.column.id
}),
columnHelper.accessor('bank_date', {
header: () => <span>Bank Date</span>,
cell: info => info.getValue(),
footer: info => info.column.id
}),
columnHelper.accessor('currency_value', {
header: () => <span>Currency Value</span>,
cell: info => String(info.getValue()),
footer: info => info.column.id
}),
], [columnHelper]) as ColumnDef<TableDataItem>[];
const table = useReactTable({
data: tableData,
columns,
state: { sorting, pagination },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
pageCount: apiPagination.totalPages || 1,
});
return (
isLoading ? <LoadingContent height="h-48" size="w-36 h-36" plane="h-full w-full" /> :
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-20">
<div className="flex justify-between items-center p-4">
<TableHeader
title={t.dataTable}
description={t.tableWithApiData}
isLoading={isLoading}
error={error}
t={t}
/>
</div>
<TableForm
form={form}
handleFormSubmit={handleFormSubmit}
handleSelectChange={handleSelectChange}
renderPageOptions={renderPageOptions}
pageSizeOptions={pageSizeOptions}
apiPagination={apiPagination}
handleFirstPage={handleFirstPage}
handlePreviousPage={handlePreviousPage}
handleNextPage={handleNextPage}
isPreviousDisabled={isPreviousDisabled}
isNextDisabled={isNextDisabled}
t={t}
/>
{/* Mobile pagination controls - only visible on small screens */}
<MobilePaginationControls
handlePreviousPage={handlePreviousPage}
handleNextPage={handleNextPage}
isPreviousDisabled={isPreviousDisabled}
isNextDisabled={isNextDisabled}
t={t}
/>
<DataTable
table={table}
tableData={tableData}
isLoading={isLoading}
handleSortingChange={handleSortingChange}
getSortingIcon={getSortingIcon}
flexRender={flexRender}
t={t}
/>
</div>
);
});
export default TableCardComponentImproved;

View File

@ -67,6 +67,24 @@ async function initFirstSelection(firstSelection: any, userType: string) {
}
}
async function setLoginCookies(cookieStore: any, accessToken: string, redisKeyAccess: string, usersSelection: string) {
cookieStore.set({
name: "eys-zzz",
value: accessToken,
...cookieObject,
});
cookieStore.set({
name: "eys-yyy",
value: redisKeyAccess,
...cookieObject,
});
cookieStore.set({
name: "eys-sel",
value: usersSelection,
...cookieObject,
});
}
async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
const cookieStore = await cookies();
try {
@ -97,22 +115,7 @@ async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
const redisKeyAccess = await nextCrypto.encrypt(redisKey);
const usersSelection = await nextCrypto.encrypt(JSON.stringify({ selected: firstSelection, userType, redisKey }));
cookieStore.set({
name: "eys-zzz",
value: accessToken,
...cookieObject,
});
cookieStore.set({
name: "eys-yyy",
value: redisKeyAccess,
...cookieObject,
});
cookieStore.set({
name: "eys-sel",
value: usersSelection,
...cookieObject,
});
await setLoginCookies(cookieStore, accessToken, redisKeyAccess, usersSelection);
await initRedis(loginRespone, firstSelection, accessToken, redisKey);
try {

View File

@ -0,0 +1,338 @@
import { useState, useEffect, useRef } from "react";
import { UseFormReturn, Path, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { SortingState } from "@tanstack/react-table";
import { apiPostFetcher } from "@/lib/fetcher";
// Define interfaces for API responses
export interface ApiPagination {
size: number;
page: number;
allCount: number;
totalCount: number;
totalPages: number;
pageCount: number;
orderField: string[];
orderType: string[];
next: boolean;
back: boolean;
}
export interface FetcherDataResponse<T> {
success: boolean;
data: {
data: T[];
pagination?: ApiPagination;
message?: string;
} | null;
}
export interface TablePaginationState {
pageIndex: number;
pageSize: number;
}
// Define the default form schema using zod
export const defaultFormSchema = z.object({
page: z.number().min(1),
size: z.number().min(1),
orderField: z.array(z.string()),
orderType: z.array(z.string()),
query: z.record(z.any()),
});
// Default form values
export const defaultFormValues = {
page: 1,
size: 10,
orderField: [] as string[],
orderType: [] as string[],
query: {},
};
// Type for the form values
export type TableFormValues = z.infer<typeof defaultFormSchema>;
export interface UseTableDataProps {
apiUrl: string;
defaultPagination?: {
page: number;
size: number;
};
mapFormToRequestBody?: (values: TableFormValues) => any;
customFormSchema?: z.ZodType<any, any>;
customFormValues?: Record<string, any>;
pageField?: keyof TableFormValues;
sizeField?: keyof TableFormValues;
orderFieldField?: keyof TableFormValues;
orderTypeField?: keyof TableFormValues;
queryField?: keyof TableFormValues;
form?: UseFormReturn<TableFormValues>;
}
export function useTableData({
apiUrl,
defaultPagination = { page: 1, size: 10 },
mapFormToRequestBody,
customFormSchema = defaultFormSchema,
customFormValues = defaultFormValues,
pageField = "page" as keyof TableFormValues,
sizeField = "size" as keyof TableFormValues,
orderFieldField = "orderField" as keyof TableFormValues,
orderTypeField = "orderType" as keyof TableFormValues,
queryField = "query" as keyof TableFormValues,
form: externalForm,
}: UseTableDataProps) {
// Initialize the form with react-hook-form or use the provided one
const form =
externalForm ||
useForm<TableFormValues>({
resolver: zodResolver(customFormSchema),
defaultValues: customFormValues as any,
});
// Table data state
const [tableData, setTableData] = useState<any[]>([]);
const [sorting, setSorting] = useState<SortingState>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [inUse, setInUse] = useState<boolean>(false);
const inputTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Pagination states
const [pagination, setPagination] = useState<TablePaginationState>({
pageIndex: defaultPagination.page - 1,
pageSize: defaultPagination.size,
});
const [apiPagination, setApiPagination] = useState<ApiPagination>({
size: 10,
page: 1,
allCount: 0,
totalCount: 0,
totalPages: 1,
pageCount: 10,
orderField: [],
orderType: [],
next: false,
back: false,
});
console.log("apiPagination", apiPagination);
// Watch for form value changes
const page = form.watch(pageField as Path<TableFormValues>);
const size = form.watch(sizeField as Path<TableFormValues>);
const orderField = form.watch(orderFieldField as Path<TableFormValues>);
const orderType = form.watch(orderTypeField as Path<TableFormValues>);
const query = form.watch(queryField as Path<TableFormValues>);
// Update table pagination when size changes
useEffect(() => {
if (typeof size === "number") {
setPagination((prev) => ({ ...prev, pageSize: size }));
}
}, [size]);
// Fetch data when any pagination parameter changes
useEffect(() => {
fetchTableData();
}, [
page,
size,
orderField ? JSON.stringify(orderField) : null,
orderType ? JSON.stringify(orderType) : null,
query ? JSON.stringify(query) : null,
]);
const fetchTableData = async () => {
setIsLoading(true);
setError("");
try {
// Get current form values
const values = form.getValues();
// Prepare request body
const requestBody = mapFormToRequestBody
? mapFormToRequestBody(values)
: {
page: values[pageField],
size: values[sizeField],
orderField: values[orderFieldField],
orderType: values[orderTypeField],
query: values[queryField],
};
const response = await apiPostFetcher<any>({
url: apiUrl,
isNoCache: true,
body: requestBody,
});
if (response.success && response.data) {
const dataArray = Array.isArray(response.data.data)
? response.data.data
: [];
setTableData(dataArray);
setPagination({
pageIndex: Number(values[pageField]) - 1,
pageSize: Number(values[sizeField]),
});
if (response.data.pagination) {
setApiPagination(response.data.pagination);
}
} else {
setError("Failed to fetch data");
setTableData([]);
}
} catch (error) {
console.error("Error fetching data:", error);
setTableData([]);
setError("Failed to fetch data");
} finally {
setIsLoading(false);
}
};
const handleSortingChange = (columnId: string) => {
const sortingColumn = sorting.find((col) => col.id === columnId);
let newSorting: SortingState = [];
if (!sortingColumn) {
newSorting = [{ id: columnId, desc: false }];
} else if (!sortingColumn.desc) {
newSorting = [{ id: columnId, desc: true }];
}
setSorting(newSorting);
const orderFields = newSorting.length > 0 ? [newSorting[0].id] : [];
const orderTypes =
newSorting.length > 0 ? [newSorting[0].desc ? "desc" : "asc"] : [];
form.setValue(orderFieldField as Path<TableFormValues>, orderFields as any);
form.setValue(orderTypeField as Path<TableFormValues>, orderTypes as any);
fetchTableData();
};
// Dropdown options
const pageSizeOptions = [10, 20, 50, 100];
// Function to render page options for the dropdown
const renderPageOptions = () => {
return Array.from(
{ length: apiPagination.totalPages || 5 },
(_, i) => i + 1
).map((page) => ({
key: page,
value: page.toString(),
label: page.toString(),
}));
};
// UI event handlers
const handleSelectChange = (
value: string,
field: { onChange: (value: number) => void }
) => {
const numericValue = Number(value);
field.onChange(numericValue);
// Fetch data immediately when dropdown selection changes
fetchTableData();
};
const handleNumberInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
field: { onChange: (value: number) => void }
) => {
const value = Number(e.target.value);
field.onChange(value);
// Set inUse to true to indicate user is typing
setInUse(true);
// Clear any existing timeout
if (inputTimeoutRef.current) {
clearTimeout(inputTimeoutRef.current);
inputTimeoutRef.current = null;
}
// Set a new timeout
inputTimeoutRef.current = setTimeout(() => {
// After 1 second of inactivity, set inUse to false and fetch data
setInUse(false);
fetchTableData();
}, 1000);
};
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Immediate fetch without debounce when form is submitted
fetchTableData();
};
const handlePageChange = (newPage: number) => {
form.setValue(pageField as Path<TableFormValues>, newPage);
fetchTableData();
};
const handleFirstPage = () => {
form.setValue(pageField as Path<TableFormValues>, 1);
fetchTableData();
};
const handlePreviousPage = () => {
const currentPage = Number(form.getValues()[pageField]);
if (currentPage > 1) {
form.setValue(pageField as Path<TableFormValues>, currentPage - 1);
fetchTableData();
}
};
const handleNextPage = () => {
const currentPage = Number(form.getValues()[pageField]);
form.setValue(pageField as Path<TableFormValues>, currentPage + 1);
fetchTableData();
};
// Sorting indicator for UI
const getSortingIcon = (columnId: string) => {
const sortingColumn = sorting.find((col) => col.id === columnId);
if (!sortingColumn) return null;
return sortingColumn.desc ? " 🔽" : " 🔼";
};
// Check if previous button should be disabled
const isPreviousDisabled = () => {
return !apiPagination.back || page <= 1;
};
// Check if next button should be disabled
const isNextDisabled = () => {
return !apiPagination.next || page >= apiPagination.totalPages;
};
return {
form,
tableData,
sorting,
isLoading,
error,
pagination,
apiPagination,
setSorting,
handleSortingChange,
// UI handlers
handleSelectChange,
handlePageChange,
handleFirstPage,
handlePreviousPage,
handleNextPage,
getSortingIcon,
handleFormSubmit,
// Disabled states
isPreviousDisabled,
isNextDisabled,
// Input state
inUse,
// Dropdown options
pageSizeOptions,
renderPageOptions,
};
}

View File

@ -39,9 +39,9 @@ const ClientLayout: FC<ClientLayoutProps> = ({ activePageUrl, searchParams }) =>
<ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams}
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
<FooterComponent activePageUrl={activePageUrl} searchParams={searchParams}
{/* <FooterComponent activePageUrl={activePageUrl} searchParams={searchParams}
configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> */}
</div>
</ClientProviders>
);

View File

@ -1,8 +1,12 @@
// import { DashboardPage } from "@/components/custom/content/DashboardPage";
import { DPage } from "@/components/custom/content/DPage";
import TableCardComponentImproved from "@/components/custom/content/TableCardComponentImproved";
const pageIndexMulti: Record<string, Record<string, React.FC<any>>> = {
"/dashboard": { DashboardPage: DPage },
"/dashboard": { DashboardPage: TableCardComponentImproved },
"/build": { DashboardPage: DPage },
"/build/create": { DashboardPage: DPage },
"/build/update": { DashboardPage: DPage },
};
export { pageIndexMulti };

View File

@ -0,0 +1,74 @@
interface Translations {
dataTable: string;
tableWithApiData: string;
loading: string;
noDataAvailable: string;
page: string;
size: string;
total: string;
items: string;
first: string;
previous: string;
next: string;
selectPage: string;
selectSize: string;
}
interface TableHeaderProps {
title: string;
description: string;
isLoading: boolean;
error: string | null;
t: Translations;
}
interface LoadingSpinnerProps {
t: Translations;
}
interface ErrorDisplayProps {
message: string;
}
interface MobilePaginationControlsProps {
handlePreviousPage: () => void;
handleNextPage: () => void;
isPreviousDisabled: () => boolean;
isNextDisabled: () => boolean;
t: Translations;
}
interface DashboardPageProps {
searchParams: Record<string, any>;
activePageUrl?: string;
userData?: any;
userLoading?: boolean;
userError?: any;
refreshUser?: () => void;
updateUser?: (data: any) => void;
onlineData?: any;
onlineLoading?: boolean;
onlineError?: any;
refreshOnline?: () => void;
updateOnline?: (data: any) => void;
}
interface TableDataItem {
uu_id: string;
process_name: string;
bank_date: string;
currency_value: number | string;
[key: string]: any; // For any additional fields
}
export type {
Translations,
TableHeaderProps,
LoadingSpinnerProps,
ErrorDisplayProps,
MobilePaginationControlsProps,
DashboardPageProps,
TableDataItem,
};