112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
'use client'
|
|
|
|
type PaginationProps = {
|
|
currentPage: number
|
|
totalPages: number
|
|
onPageChange: (page: number) => void
|
|
}
|
|
|
|
function getDisplayedPages(currentPage: number, totalPages: number): (number | '...')[] {
|
|
const delta = 1
|
|
const range: (number | '...')[] = []
|
|
|
|
const left = Math.max(2, currentPage - delta)
|
|
const right = Math.min(totalPages - 1, currentPage + delta)
|
|
|
|
range.push(1)
|
|
|
|
if (left > 2) range.push('...')
|
|
for (let i = left; i <= right; i++) range.push(i)
|
|
if (right < totalPages - 1) range.push('...')
|
|
|
|
if (totalPages > 1) range.push(totalPages)
|
|
|
|
return range
|
|
}
|
|
|
|
export default function Pagination({
|
|
currentPage,
|
|
totalPages,
|
|
onPageChange,
|
|
}: PaginationProps) {
|
|
if (totalPages <= 1) return null
|
|
|
|
const pages = getDisplayedPages(currentPage, totalPages)
|
|
|
|
return (
|
|
<nav className="flex items-center gap-x-1 mt-2" aria-label="Pagination">
|
|
{/* Prev Button */}
|
|
<button
|
|
type="button"
|
|
onClick={() => onPageChange(currentPage - 1)}
|
|
disabled={currentPage === 1}
|
|
className="min-h-9.5 min-w-9.5 py-2 px-2.5 inline-flex justify-center items-center gap-x-2 text-sm rounded-lg border border-transparent text-gray-800 hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
|
|
aria-label="Previous"
|
|
>
|
|
<svg
|
|
className="shrink-0 size-3.5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M15 18l-6-6 6-6" />
|
|
</svg>
|
|
<span className="sr-only">Previous</span>
|
|
</button>
|
|
|
|
{/* Page Numbers */}
|
|
<div className="flex items-center gap-x-1">
|
|
{pages.map((page, index) =>
|
|
page === '...' ? (
|
|
<span
|
|
key={`ellipsis-${index}`}
|
|
className="min-h-9.5 min-w-9.5 flex justify-center items-center text-gray-500 dark:text-neutral-500 text-sm px-2"
|
|
>
|
|
...
|
|
</span>
|
|
) : (
|
|
<button
|
|
key={page}
|
|
type="button"
|
|
onClick={() => onPageChange(page)}
|
|
aria-current={page === currentPage ? 'page' : undefined}
|
|
className={`min-h-9.5 min-w-9.5 flex justify-center items-center py-2 px-3 text-sm rounded-lg border
|
|
${
|
|
page === currentPage
|
|
? 'border-gray-200 text-gray-800 dark:border-neutral-700 dark:text-white bg-gray-100 dark:bg-white/10'
|
|
: 'border-transparent text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-white/10'
|
|
}
|
|
focus:outline-hidden focus:bg-gray-100 dark:focus:bg-white/10`}
|
|
>
|
|
{page}
|
|
</button>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{/* Next Button */}
|
|
<button
|
|
type="button"
|
|
onClick={() => onPageChange(currentPage + 1)}
|
|
disabled={currentPage === totalPages}
|
|
className="min-h-9.5 min-w-9.5 py-2 px-2.5 inline-flex justify-center items-center gap-x-2 text-sm rounded-lg border border-transparent text-gray-800 hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
|
|
aria-label="Next"
|
|
>
|
|
<span className="sr-only">Next</span>
|
|
<svg
|
|
className="shrink-0 size-3.5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M9 18l6-6-6-6" />
|
|
</svg>
|
|
</button>
|
|
</nav>
|
|
)
|
|
}
|