feat: TaskPager with numbered buttons and ResizeObserver-aware width

This commit is contained in:
Roberto
2026-05-08 14:28:38 +02:00
parent 2e9ec31d83
commit ef04bec66f

View File

@@ -0,0 +1,100 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { cn } from '@/lib/utils';
interface Props {
total: number;
pageIndex: number;
pageSize: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
}
const PAGE_SIZES = [10, 25, 50, 100];
function buildWindow(current: number, last: number, max: number): Array<number | 'ellipsis'> {
if (last <= 0) return [0];
if (last < max) return Array.from({ length: last + 1 }, (_, i) => i);
const window: Array<number | 'ellipsis'> = [];
const halfMax = Math.floor((max - 2) / 2);
let start = Math.max(1, current - halfMax);
let end = Math.min(last - 1, current + halfMax);
if (current - halfMax < 1) end = Math.min(last - 1, max - 2);
if (current + halfMax > last - 1) start = Math.max(1, last - (max - 2));
window.push(0);
if (start > 1) window.push('ellipsis');
for (let i = start; i <= end; i++) window.push(i);
if (end < last - 1) window.push('ellipsis');
window.push(last);
return window;
}
export function TaskPager({ total, pageIndex, pageSize, onPageChange, onPageSizeChange }: Props) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const [maxButtons, setMaxButtons] = useState(7);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const ro = new ResizeObserver(([entry]) => {
const w = entry.contentRect.width;
setMaxButtons(w < 480 ? 3 : w < 640 ? 5 : 7);
});
ro.observe(el);
return () => ro.disconnect();
}, []);
const lastPage = Math.max(0, Math.ceil(total / pageSize) - 1);
const start = total === 0 ? 0 : pageIndex * pageSize + 1;
const end = Math.min(total, (pageIndex + 1) * pageSize);
const window = buildWindow(pageIndex, lastPage, maxButtons);
return (
<div
ref={containerRef}
className="rounded-lg border border-border/50 bg-card/65 backdrop-blur-xl shadow-sm flex items-center justify-between px-4 py-2 gap-3 flex-wrap"
>
<span className="text-xs text-muted-foreground">
<Trans
i18nKey="tasks.showingNofM"
values={{ start, end, total }}
components={{ b: <span className="font-medium text-foreground" /> }}
/>
</span>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{t('tasks.rowsPerPage')}</span>
<Select value={String(pageSize)} onValueChange={(v) => onPageSizeChange(Number(v))}>
<SelectTrigger className="h-7 w-[68px]"><SelectValue /></SelectTrigger>
<SelectContent>
{PAGE_SIZES.map((s) => <SelectItem key={s} value={String(s)}>{s}</SelectItem>)}
</SelectContent>
</Select>
<Button variant="ghost" size="icon" className="h-7 w-7" disabled={pageIndex === 0} onClick={() => onPageChange(pageIndex - 1)}>
<ChevronLeft className="h-4 w-4" />
</Button>
{window.map((p, i) =>
p === 'ellipsis' ? (
<span key={`e${i}`} className="px-1 text-muted-foreground"></span>
) : (
<Button
key={p}
variant={p === pageIndex ? 'default' : 'ghost'}
size="sm"
className={cn('h-7 min-w-7 px-2 text-xs')}
onClick={() => onPageChange(p)}
>
{p + 1}
</Button>
),
)}
<Button variant="ghost" size="icon" className="h-7 w-7" disabled={pageIndex >= lastPage} onClick={() => onPageChange(pageIndex + 1)}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
);
}