107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
|
|
export const TABLE_PAGE_SIZE = 10;
|
|
|
|
export interface TablePagerProps {
|
|
page: number;
|
|
totalPages: number;
|
|
total: number;
|
|
pageSize: number;
|
|
onPageChange: (page: number) => void;
|
|
}
|
|
|
|
export function useClientPagination<T>(items: T[], pageSize: number = TABLE_PAGE_SIZE) {
|
|
const [page, setPage] = useState(1);
|
|
const total = items.length;
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
const currentPage = Math.min(Math.max(1, page), totalPages);
|
|
|
|
useEffect(() => {
|
|
if (page !== currentPage) setPage(currentPage);
|
|
}, [page, currentPage]);
|
|
|
|
const pagedItems = useMemo(() => {
|
|
const start = (currentPage - 1) * pageSize;
|
|
return items.slice(start, start + pageSize);
|
|
}, [items, currentPage, pageSize]);
|
|
|
|
return {
|
|
items: pagedItems,
|
|
page: currentPage,
|
|
totalPages,
|
|
total,
|
|
pageSize,
|
|
onPageChange: setPage,
|
|
};
|
|
}
|
|
|
|
function pageWindow(current: number, totalPages: number) {
|
|
if (totalPages <= 5) return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
if (current <= 3) return [1, 2, 3, 4, 5];
|
|
if (current >= totalPages - 2) return [totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
|
|
return [current - 2, current - 1, current, current + 1, current + 2];
|
|
}
|
|
|
|
export function TablePagination({
|
|
page,
|
|
totalPages,
|
|
total,
|
|
pageSize,
|
|
onPageChange,
|
|
}: TablePagerProps & { items?: unknown }) {
|
|
if (total === 0) return null;
|
|
|
|
const from = (page - 1) * pageSize + 1;
|
|
const to = Math.min(page * pageSize, total);
|
|
const pages = pageWindow(page, totalPages);
|
|
|
|
const btnClass =
|
|
'inline-flex items-center justify-center h-8 min-w-8 px-2.5 crm-radius-control border border-border bg-card text-[12px] font-medium text-foreground hover:bg-muted cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-card';
|
|
|
|
return (
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 px-3 sm:px-4 py-3 border-t border-border bg-card">
|
|
<p className="text-[12px] text-muted-foreground">
|
|
Showing <span className="font-semibold text-foreground">{from}</span> to{' '}
|
|
<span className="font-semibold text-foreground">{to}</span> of{' '}
|
|
<span className="font-semibold text-foreground">{total}</span>
|
|
</p>
|
|
<div className="flex items-center gap-1 overflow-x-auto max-w-full">
|
|
<button
|
|
type="button"
|
|
aria-label="Previous page"
|
|
disabled={page <= 1}
|
|
onClick={() => onPageChange(page - 1)}
|
|
className={btnClass}
|
|
>
|
|
<ChevronLeft className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">Previous</span>
|
|
</button>
|
|
{pages.map((n) => (
|
|
<button
|
|
key={n}
|
|
type="button"
|
|
aria-label={`Page ${n}`}
|
|
aria-current={n === page ? 'page' : undefined}
|
|
onClick={() => onPageChange(n)}
|
|
className={`${btnClass} ${n === page ? 'bg-primary text-white border-primary hover:bg-primary/90 hover:text-white' : ''}`}
|
|
>
|
|
{n}
|
|
</button>
|
|
))}
|
|
<button
|
|
type="button"
|
|
aria-label="Next page"
|
|
disabled={page >= totalPages}
|
|
onClick={() => onPageChange(page + 1)}
|
|
className={btnClass}
|
|
>
|
|
<span className="hidden sm:inline">Next</span>
|
|
<ChevronRight className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|