On viewports below 640px the pagination row at Dashboard > Usage > Details rendered the rows-per-page select plus the full numbered page list plus the prev/next chevrons in one unwrapped flex row with no horizontal padding. On a 390px phone the Rows: label was clipped on the left and the next chevron was clipped on the right. Add px-2 to the outer container so the pagination card has horizontal breathing room. Switch the inner controls group to flex-wrap with gap-2 on mobile (and gap-4 from sm: up) so the rows-per-page select can wrap below the chevrons. Hide the numbered page buttons, the 1 / last anchors, and the ... ellipses below sm: -- only the prev, current page indicator, and next chevrons render on mobile, which keeps every control reachable while fitting inside the card. Desktop layout (>= 640px) is unchanged. Refs decolua/9router#1146
151 lines
4.6 KiB
JavaScript
151 lines
4.6 KiB
JavaScript
"use client";
|
|
|
|
import { cn } from "@/shared/utils/cn";
|
|
import Button from "./Button";
|
|
|
|
export default function Pagination({
|
|
currentPage,
|
|
pageSize,
|
|
totalItems,
|
|
onPageChange,
|
|
onPageSizeChange,
|
|
className,
|
|
}) {
|
|
const totalPages = Math.ceil(totalItems / pageSize);
|
|
const startItem = totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0;
|
|
const endItem = Math.min(currentPage * pageSize, totalItems);
|
|
|
|
const getPageNumbers = () => {
|
|
const pages = [];
|
|
const showMax = 5;
|
|
|
|
let start = Math.max(1, currentPage - 2);
|
|
let end = Math.min(totalPages, start + showMax - 1);
|
|
|
|
if (end - start + 1 < showMax) {
|
|
start = Math.max(1, end - showMax + 1);
|
|
}
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i);
|
|
}
|
|
return pages;
|
|
};
|
|
|
|
const pageNumbers = getPageNumbers();
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col sm:flex-row items-center justify-between gap-4 py-4 px-2",
|
|
className
|
|
)}
|
|
>
|
|
{/* Info text */}
|
|
{totalItems > 0 && (
|
|
<div className="text-sm text-text-muted">
|
|
Showing <span className="font-medium text-text-main">{startItem}</span> to{" "}
|
|
<span className="font-medium text-text-main">{endItem}</span> of{" "}
|
|
<span className="font-medium text-text-main">{totalItems}</span> results
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-4">
|
|
{/* Page size selector */}
|
|
{onPageSizeChange && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-text-muted">Rows:</span>
|
|
<select
|
|
value={pageSize}
|
|
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
|
className={cn(
|
|
"h-9 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
|
|
"text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20",
|
|
"cursor-pointer"
|
|
)}
|
|
style={{ colorScheme: 'auto' }}
|
|
>
|
|
{[10, 20, 50].map((size) => (
|
|
<option key={size} value={size}>
|
|
{size}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(currentPage - 1)}
|
|
disabled={currentPage === 1}
|
|
className="w-9 px-0"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">chevron_left</span>
|
|
</Button>
|
|
|
|
{pageNumbers[0] > 1 && (
|
|
<>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onPageChange(1)}
|
|
className="w-9 px-0 hidden sm:inline-flex"
|
|
>
|
|
1
|
|
</Button>
|
|
{pageNumbers[0] > 2 && (
|
|
<span className="text-text-muted px-1 hidden sm:inline">...</span>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{pageNumbers.map((page) => (
|
|
<Button
|
|
key={page}
|
|
variant={currentPage === page ? "primary" : "ghost"}
|
|
size="sm"
|
|
onClick={() => onPageChange(page)}
|
|
className={cn(
|
|
"w-9 px-0",
|
|
currentPage === page ? "inline-flex" : "hidden sm:inline-flex"
|
|
)}
|
|
>
|
|
{page}
|
|
</Button>
|
|
))}
|
|
|
|
{pageNumbers[pageNumbers.length - 1] < totalPages && (
|
|
<>
|
|
{pageNumbers[pageNumbers.length - 1] < totalPages - 1 && (
|
|
<span className="text-text-muted px-1 hidden sm:inline">...</span>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onPageChange(totalPages)}
|
|
className="w-9 px-0 hidden sm:inline-flex"
|
|
>
|
|
{totalPages}
|
|
</Button>
|
|
</>
|
|
)}
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(currentPage + 1)}
|
|
disabled={currentPage === totalPages}
|
|
className="w-9 px-0"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">chevron_right</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|