E3WSKRJTPPRD6ZEEURTO77N6GTHYJY7ISWQL2LN2VCCXA4YRH2CAC
4FBIL6IZUDNCXTM6EUHTEOJRHVI4LIIX4BU2IXPXKR362GKIAJMQC
M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC
HM75N4NTZ4BBSSDC7TUSYOQ4SIF3G6KPZA5QRYCVCVRSKQVTJAXAC
PVQBFR72OCQGYF2G2KDWNKBHWJ24N6D653X6KARBGUSYBIHIXPRQC
4M3EBLTLSS2BRCM42ZP7WVD4YMRRLGV2P2XF47IAV5XHHJD52HTQC
RLH37YB4D7O42IFM2T7GJG4AVVAURWBZ7AOTHAWR7YJZRG3JOPLQC
ROQGXQWL2V363K3W7TVVYKIAX4N4IWRERN5BJ7NYJRRVB6OMIJ4QC
LYPSC7BOH6T45FCPRHSCXILAJSJ74D5WSQTUIKPWD5ECXOYGUY5AC
import React from "react";
import Button from "./button";
import { cn } from "@/utils";
export interface PaginationProps {
/**
* Current page number (1-based)
*/
currentPage: number;
/**
* Total number of pages
*/
totalPages: number;
/**
* Callback when page changes
*/
onPageChange: (page: number) => void;
/**
* Additional CSS class names
*/
className?: string;
/**
* Show empty pagination (for loading states)
*/
disabled?: boolean;
}
/**
* Shared pagination component that provides consistent navigation across the application
*/
export function Pagination({
currentPage,
totalPages,
onPageChange,
className,
disabled = false,
}: PaginationProps) {
// Don't render pagination if there's only one page or we're in disabled state
if (totalPages <= 1 || disabled) {
return null;
}
// Function to handle page navigation
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
onPageChange(newPage);
}
};
// Generate the page buttons
const renderPageButtons = () => {
const pages: React.ReactNode[] = [];
// Always show first page
if (currentPage > 3) {
pages.push(
<Button
key="page-1"
variant={currentPage === 1 ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
>
1
</Button>
);
// Add ellipsis if not showing page 2
if (currentPage > 4) {
pages.push(
<span key="ellipsis1" className="px-1">…</span>
);
}
}
// Show current page and surrounding pages
const startPage = Math.max(1, currentPage - 1);
const endPage = Math.min(totalPages, currentPage + 1);
for (let i = startPage; i <= endPage; i++) {
if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're handled separately
pages.push(
<Button
key={`page-${i}`}
variant={currentPage === i ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(i)}
>
{i}
</Button>
);
}
// Add intermediate points for large page counts
const checkpoints = [10, 20, 30, 40];
if (totalPages > 5) {
for (const checkpoint of checkpoints) {
if (checkpoint > currentPage + 2 && checkpoint < totalPages - 2) {
// Only insert checkpoint if it's not close to what we already display
if (!pages.some(p => React.isValidElement(p) && p.key === `page-${checkpoint}`)) {
pages.push(
<Button
key={`page-${checkpoint}`}
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(checkpoint)}
>
{checkpoint}
</Button>
);
// Insert only one checkpoint button
break;
}
}
}
}
// Add ellipsis if needed
if (currentPage < totalPages - 3) {
pages.push(
<span key="ellipsis2" className="px-1">…</span>
);
}
// Always show last page
if (totalPages > 1) {
pages.push(
<Button
key={`page-${totalPages}`}
variant={currentPage === totalPages ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(totalPages)}
>
{totalPages}
</Button>
);
}
return pages;
};
return (
<div className={cn("flex justify-center items-center mt-6", className)}>
<nav className="flex items-center gap-1" aria-label="Pagination">
{/* First page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
aria-label="First page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="11 17 6 12 11 7"></polyline>
<polyline points="18 17 13 12 18 7"></polyline>
</svg>
</Button>
{/* Previous page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Previous page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</Button>
{/* Page number buttons */}
{renderPageButtons()}
{/* Next page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="Next page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</Button>
{/* Last page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
aria-label="Last page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="13 17 18 12 13 7"></polyline>
<polyline points="6 17 11 12 6 7"></polyline>
</svg>
</Button>
</nav>
</div>
);
}
<div className="flex justify-center items-center mt-6">
<nav className="flex items-center gap-1" aria-label="Pagination">
{/* First page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
aria-label="First page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="11 17 6 12 11 7"></polyline>
<polyline points="18 17 13 12 18 7"></polyline>
</svg>
</Button>
{/* Previous page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Previous page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</Button>
{/* Page number buttons */}
{(() => {
const totalPages = pagination.totalPages;
const current = currentPage;
const pages = [];
// Always show first page
if (current > 3) {
pages.push(
<Button
key="page-1"
variant={current === 1 ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
>
1
</Button>
);
// Add ellipsis if not showing page 2
if (current > 4) {
pages.push(
<span key="ellipsis1" className="px-1">…</span>
);
}
}
// Show current page and surrounding pages
const startPage = Math.max(1, current - 1);
const endPage = Math.min(totalPages, current + 1);
for (let i = startPage; i <= endPage; i++) {
if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're handled separately
pages.push(
<Button
key={`page-${i}`}
variant={current === i ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(i)}
>
{i}
</Button>
);
}
// Add intermediate points
const checkpoints = [10, 20, 30, 40];
if (totalPages > 5) {
for (const checkpoint of checkpoints) {
if (checkpoint > current + 2 && checkpoint < totalPages - 2) {
// Only insert checkpoint if it's not close to what we already display
if (!pages.some(p => p.key === `page-${checkpoint}`)) {
pages.push(
<Button
key={`page-${checkpoint}`}
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(checkpoint)}
>
{checkpoint}
</Button>
);
// Insert only one checkpoint button
break;
}
}
}
}
// Add ellipsis if needed
if (current < totalPages - 3) {
pages.push(
<span key="ellipsis2" className="px-1">…</span>
);
}
// Always show last page
if (totalPages > 1) {
pages.push(
<Button
key={`page-${totalPages}`}
variant={current === totalPages ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(totalPages)}
>
{totalPages}
</Button>
);
}
return pages;
})()}
{/* Next page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === pagination.totalPages}
aria-label="Next page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</Button>
{/* Last page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(pagination.totalPages)}
disabled={currentPage === pagination.totalPages}
aria-label="Last page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="13 17 18 12 13 7"></polyline>
<polyline points="6 17 11 12 6 7"></polyline>
</svg>
</Button>
</nav>
</div>
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}
onPageChange={handlePageChange}
/>
<div className="flex justify-center items-center mt-6">
<nav className="flex items-center gap-1" aria-label="Pagination">
{/* First page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
aria-label="First page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="11 17 6 12 11 7"></polyline>
<polyline points="18 17 13 12 18 7"></polyline>
</svg>
</Button>
{/* Previous page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Previous page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</Button>
{/* Page number buttons */}
{(() => {
const totalPagesCount = totalPages;
const current = currentPage;
const pages = [];
// Always show first page
if (current > 3) {
pages.push(
<Button
key="page-1"
variant={current === 1 ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
>
1
</Button>
);
// Add ellipsis if not showing page 2
if (current > 4) {
pages.push(
<span key="ellipsis1" className="px-1">…</span>
);
}
}
// Show current page and surrounding pages
const startPage = Math.max(1, current - 1);
const endPage = Math.min(totalPagesCount, current + 1);
for (let i = startPage; i <= endPage; i++) {
if (i === 1 || i === totalPagesCount) continue; // Skip first and last pages as they're handled separately
pages.push(
<Button
key={`page-${i}`}
variant={current === i ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(i)}
>
{i}
</Button>
);
}
// Add ellipsis if needed
if (current < totalPagesCount - 3) {
pages.push(
<span key="ellipsis2" className="px-1">…</span>
);
}
// Always show last page
if (totalPagesCount > 1) {
pages.push(
<Button
key={`page-${totalPagesCount}`}
variant={current === totalPagesCount ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(totalPagesCount)}
>
{totalPagesCount}
</Button>
);
}
return pages;
})()}
{/* Next page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="Next page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</Button>
{/* Last page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
aria-label="Last page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="13 17 18 12 13 7"></polyline>
<polyline points="6 17 11 12 6 7"></polyline>
</svg>
</Button>
</nav>
</div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
<div className="flex justify-center items-center mt-6">
<nav className="flex items-center gap-1" aria-label="Pagination">
{/* First page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
aria-label="First page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="11 17 6 12 11 7"></polyline>
<polyline points="18 17 13 12 18 7"></polyline>
</svg>
</Button>
{/* Previous page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Previous page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</Button>
{/* Page number buttons */}
{(() => {
const totalPages = pagination.totalPages;
const current = currentPage;
const pages = [];
// Always show first page
if (current > 3) {
pages.push(
<Button
key="page-1"
variant={current === 1 ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(1)}
>
1
</Button>
);
// Add ellipsis if not showing page 2
if (current > 4) {
pages.push(
<span key="ellipsis1" className="px-1">…</span>
);
}
}
// Show current page and surrounding pages
const startPage = Math.max(1, current - 1);
const endPage = Math.min(totalPages, current + 1);
for (let i = startPage; i <= endPage; i++) {
if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're handled separately
pages.push(
<Button
key={`page-${i}`}
variant={current === i ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(i)}
>
{i}
</Button>
);
}
// Add intermediate points
const checkpoints = [10, 20, 30, 40];
if (totalPages > 5) {
for (const checkpoint of checkpoints) {
if (checkpoint > current + 2 && checkpoint < totalPages - 2) {
// Only insert checkpoint if it's not close to what we already display
if (!pages.some(p => p.key === `page-${checkpoint}`)) {
pages.push(
<Button
key={`page-${checkpoint}`}
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(checkpoint)}
>
{checkpoint}
</Button>
);
// Insert only one checkpoint button
break;
}
}
}
}
// Add ellipsis if needed
if (current < totalPages - 3) {
pages.push(
<span key="ellipsis2" className="px-1">…</span>
);
}
// Always show last page
if (totalPages > 1) {
pages.push(
<Button
key={`page-${totalPages}`}
variant={current === totalPages ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(totalPages)}
>
{totalPages}
</Button>
);
}
return pages;
})()}
{/* Next page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === pagination.totalPages}
aria-label="Next page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</Button>
{/* Last page button */}
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-md"
onClick={() => handlePageChange(pagination.totalPages)}
disabled={currentPage === pagination.totalPages}
aria-label="Last page"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="13 17 18 12 13 7"></polyline>
<polyline points="6 17 11 12 6 7"></polyline>
</svg>
</Button>
</nav>
</div>
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}
onPageChange={handlePageChange}
/>