import clsx from "clsx"; import { ChevronLeft, ChevronRight } from "lucide-react"; import * as React from "react"; import styles from "./Pagination.module.css"; export function Pagination({ currentPage, pagesCount, nextPage, previousPage, setPage, compact, }: { currentPage: number; pagesCount: number; nextPage: () => void; previousPage: () => void; setPage: (page: number) => void; /** Renders only the current page number with prev/next arrows */ compact?: boolean; }) { const pages = getPageNumbers(currentPage, pagesCount); const [jumpToIndex, setJumpToIndex] = React.useState(null); return (
{compact ? ( {currentPage} ) : ( pages.map((page, index) => page.value === "..." ? ( setJumpToIndex(index)} onClose={() => setJumpToIndex(null)} onJump={(page) => { setPage(page); setJumpToIndex(null); }} /> ) : ( ), ) )}
); } function JumpToEllipsis({ isOpen, pagesCount, desktopOnly, mobileOnly, onOpen, onClose, onJump, }: { isOpen: boolean; pagesCount: number; desktopOnly?: boolean; mobileOnly?: boolean; onOpen: () => void; onClose: () => void; onJump: (page: number) => void; }) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const value = formData.get("page") as string; if (!value) { onClose(); return; } const pageNumber = Number(value); if (Number.isNaN(pageNumber) || pageNumber < 1 || pageNumber > pagesCount) { onClose(); return; } onJump(pageNumber); }; const handleBlur = () => { onClose(); }; if (isOpen) { return (
); } return ( ); } function PageButton({ page, currentPage, desktopOnly, setPage, }: { page: number; currentPage: number; desktopOnly?: boolean; setPage: (page: number) => void; }) { return ( ); } type PageItem = { value: number | "..."; desktopOnly?: boolean; mobileOnly?: boolean; }; export function getPageNumbers( currentPage: number, pagesCount: number, ): PageItem[] { if (pagesCount <= 5) { return Array.from({ length: pagesCount }, (_, i) => ({ value: i + 1 })); } const showAllOnDesktop = pagesCount <= 9; const { start: mobileStart, end: mobileEnd } = innerPageWindow( currentPage, pagesCount, 1, ); const { start: desktopStart, end: desktopEnd } = showAllOnDesktop ? { start: 2, end: pagesCount - 1 } : innerPageWindow(currentPage, pagesCount, 2); const isMobileVisible = (page: number) => page >= mobileStart && page <= mobileEnd; const isDesktopVisible = (page: number) => page >= desktopStart && page <= desktopEnd; const pages: PageItem[] = []; pages.push({ value: 1 }); const needsDesktopEllipsisStart = desktopStart > 2; const needsMobileEllipsisStart = mobileStart > 2; if (needsDesktopEllipsisStart && needsMobileEllipsisStart) { pages.push({ value: "..." }); } else if (needsMobileEllipsisStart) { pages.push({ value: "...", mobileOnly: true }); } else if (needsDesktopEllipsisStart) { pages.push({ value: "...", desktopOnly: true }); } for (let i = 2; i <= pagesCount - 1; i++) { if (isDesktopVisible(i)) { pages.push({ value: i, desktopOnly: !isMobileVisible(i) }); } } const needsDesktopEllipsisEnd = desktopEnd < pagesCount - 1; const needsMobileEllipsisEnd = mobileEnd < pagesCount - 1; if (needsDesktopEllipsisEnd && needsMobileEllipsisEnd) { pages.push({ value: "..." }); } else if (needsMobileEllipsisEnd) { pages.push({ value: "...", mobileOnly: true }); } else if (needsDesktopEllipsisEnd) { pages.push({ value: "...", desktopOnly: true }); } pages.push({ value: pagesCount }); return pages; } /** * Inclusive range of inner page numbers (excluding the always-shown first and * last page) to render around the current page. The window is nudged inward by * one when the current page is the very first or last page, so the edge view * shows a bridging number instead of a lonely jump like "1 2 … 8". * * When exactly one page would be left between the window and the always-shown * first or last page, the window is widened to include it: an ellipsis takes * the same space as a single page number, so "1 … 3" is never better than * "1 2 3". */ function innerPageWindow( currentPage: number, pagesCount: number, radius: number, ): { start: number; end: number } { const startNudge = currentPage === pagesCount ? 1 : 0; const endNudge = currentPage === 1 ? 1 : 0; let start = Math.max(2, currentPage - radius - startNudge); let end = Math.min(pagesCount - 1, currentPage + radius + endNudge); if (start === 3) start = 2; if (end === pagesCount - 2) end = pagesCount - 1; return { start, end }; }