2026-06-29 07:55:47 +02:00
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import clsx from 'clsx';
|
2026-07-23 00:47:23 +02:00
|
|
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
2026-06-29 07:55:47 +02:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
|
|
|
|
|
|
export interface PaginationProps {
|
|
|
|
|
|
currentPage: number;
|
|
|
|
|
|
totalPages: number;
|
|
|
|
|
|
total: number;
|
|
|
|
|
|
pageSize: number;
|
|
|
|
|
|
onPageChange: (page: number) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Pagination({ currentPage, totalPages, total, pageSize, onPageChange }: PaginationProps) {
|
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
|
|
|
|
|
|
const end = Math.min(currentPage * pageSize, total);
|
|
|
|
|
|
|
|
|
|
|
|
const pages: number[] = [];
|
|
|
|
|
|
const maxVisible = 5;
|
|
|
|
|
|
const halfVisible = Math.floor(maxVisible / 2);
|
|
|
|
|
|
let startPage = Math.max(1, currentPage - halfVisible);
|
|
|
|
|
|
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
|
|
|
|
|
|
if (endPage - startPage < maxVisible - 1) {
|
|
|
|
|
|
startPage = Math.max(1, endPage - maxVisible + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
|
|
|
|
pages.push(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (totalPages <= 1) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-08-27 09:14:21 +02:00
|
|
|
|
<nav className="flex items-center justify-between px-3 py-1.5 border-t border-secondary-200 bg-white" aria-label="Pagination">
|
2026-07-16 09:40:59 +02:00
|
|
|
|
<div className="text-xs text-secondary-500">
|
|
|
|
|
|
<span>{start}–{end}</span> <span>{t('table.of')}</span> <span>{total}</span>
|
2026-06-29 07:55:47 +02:00
|
|
|
|
</div>
|
2026-07-16 09:40:59 +02:00
|
|
|
|
<div className="flex items-center gap-0.5">
|
2026-06-29 07:55:47 +02:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onPageChange(currentPage - 1)}
|
|
|
|
|
|
disabled={currentPage <= 1}
|
2026-07-16 09:40:59 +02:00
|
|
|
|
className="px-1.5 py-0.5 text-xs rounded border border-secondary-300 hover:bg-secondary-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-500"
|
2026-06-29 07:55:47 +02:00
|
|
|
|
aria-label={t('pagination.previous')}
|
|
|
|
|
|
>
|
2026-07-23 00:47:23 +02:00
|
|
|
|
<ChevronLeft className="h-3 w-3" aria-hidden="true" />
|
2026-06-29 07:55:47 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
{startPage > 1 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onPageChange(1)}
|
2026-07-16 09:40:59 +02:00
|
|
|
|
className="px-1.5 py-0.5 text-xs rounded border border-secondary-300 hover:bg-secondary-50 focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-500"
|
2026-06-29 07:55:47 +02:00
|
|
|
|
aria-label={t('pagination.first')}
|
|
|
|
|
|
>
|
|
|
|
|
|
1
|
|
|
|
|
|
</button>
|
2026-07-16 09:40:59 +02:00
|
|
|
|
{startPage > 2 && <span className="px-0.5 text-secondary-400" aria-hidden="true">…</span>}
|
2026-06-29 07:55:47 +02:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{pages.map((page) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={page}
|
|
|
|
|
|
onClick={() => onPageChange(page)}
|
|
|
|
|
|
className={clsx(
|
2026-07-16 09:40:59 +02:00
|
|
|
|
'px-1.5 py-0.5 text-xs rounded border focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-500',
|
2026-06-29 07:55:47 +02:00
|
|
|
|
page === currentPage
|
|
|
|
|
|
? 'border-primary-500 bg-primary-50 text-primary-700 font-semibold'
|
|
|
|
|
|
: 'border-secondary-300 hover:bg-secondary-50'
|
|
|
|
|
|
)}
|
|
|
|
|
|
aria-label={t('pagination.page', { page })}
|
|
|
|
|
|
aria-current={page === currentPage ? 'page' : undefined}
|
|
|
|
|
|
>
|
|
|
|
|
|
{page}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{endPage < totalPages && (
|
|
|
|
|
|
<>
|
2026-07-16 09:40:59 +02:00
|
|
|
|
{endPage < totalPages - 1 && <span className="px-0.5 text-secondary-400" aria-hidden="true">…</span>}
|
2026-06-29 07:55:47 +02:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onPageChange(totalPages)}
|
2026-07-16 09:40:59 +02:00
|
|
|
|
className="px-1.5 py-0.5 text-xs rounded border border-secondary-300 hover:bg-secondary-50 focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-500"
|
2026-06-29 07:55:47 +02:00
|
|
|
|
aria-label={t('pagination.last')}
|
|
|
|
|
|
>
|
|
|
|
|
|
{totalPages}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onPageChange(currentPage + 1)}
|
|
|
|
|
|
disabled={currentPage >= totalPages}
|
2026-07-16 09:40:59 +02:00
|
|
|
|
className="px-1.5 py-0.5 text-xs rounded border border-secondary-300 hover:bg-secondary-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-500"
|
2026-06-29 07:55:47 +02:00
|
|
|
|
aria-label={t('pagination.next')}
|
|
|
|
|
|
>
|
2026-07-23 00:47:23 +02:00
|
|
|
|
<ChevronRight className="h-3 w-3" aria-hidden="true" />
|
2026-06-29 07:55:47 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|