Spaces:
Sleeping
Sleeping
| import React from 'react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { CustomDropdown } from './CustomDropdown'; | |
| import { | |
| ChevronLeft, | |
| ChevronRight, | |
| ChevronsLeft, | |
| ChevronsRight | |
| } from 'lucide-react'; | |
| interface PaginationProps { | |
| currentPage: number; | |
| totalPages: number; | |
| totalItems: number; | |
| itemsPerPage: number; | |
| onPageChange: (page: number) => void; | |
| onItemsPerPageChange: (itemsPerPage: number) => void; | |
| className?: string; | |
| } | |
| const ITEMS_PER_PAGE_OPTIONS = [10, 25, 50, 100]; | |
| const Pagination: React.FC<PaginationProps> = ({ | |
| currentPage, | |
| totalPages, | |
| totalItems, | |
| itemsPerPage, | |
| onPageChange, | |
| onItemsPerPageChange, | |
| className = '' | |
| }) => { | |
| const startItem = (currentPage - 1) * itemsPerPage + 1; | |
| const endItem = Math.min(currentPage * itemsPerPage, totalItems); | |
| const handlePageChange = (page: number) => { | |
| if (page >= 1 && page <= totalPages) { | |
| onPageChange(page); | |
| } | |
| }; | |
| // Generate page numbers to show | |
| const getPageNumbers = () => { | |
| const pages: (number | string)[] = []; | |
| const maxVisiblePages = 5; | |
| if (totalPages <= maxVisiblePages) { | |
| // Show all pages if total is small | |
| for (let i = 1; i <= totalPages; i++) { | |
| pages.push(i); | |
| } | |
| } else { | |
| // Show first page | |
| pages.push(1); | |
| if (currentPage > 3) { | |
| pages.push('...'); | |
| } | |
| // Show pages around current page | |
| const start = Math.max(2, currentPage - 1); | |
| const end = Math.min(totalPages - 1, currentPage + 1); | |
| for (let i = start; i <= end; i++) { | |
| pages.push(i); | |
| } | |
| if (currentPage < totalPages - 2) { | |
| pages.push('...'); | |
| } | |
| // Show last page | |
| if (totalPages > 1) { | |
| pages.push(totalPages); | |
| } | |
| } | |
| return pages; | |
| }; | |
| if (totalItems === 0) { | |
| return null; | |
| } | |
| return ( | |
| <nav | |
| className={`flex flex-col sm:flex-row items-center justify-between gap-4 ${className}`} | |
| role="navigation" | |
| aria-label="Pagination Navigation" | |
| > | |
| {/* Items per page selector */} | |
| <div className="flex items-center gap-2 text-sm"> | |
| <label htmlFor="items-per-page" className="text-muted-foreground">Show</label> | |
| <CustomDropdown | |
| options={ITEMS_PER_PAGE_OPTIONS.map(option => ({ | |
| value: option.toString(), | |
| label: option.toString() | |
| }))} | |
| value={itemsPerPage.toString()} | |
| onChange={(value) => onItemsPerPageChange(parseInt(value))} | |
| className="w-20" | |
| triggerClassName="h-8" | |
| aria-label={`Items per page, currently ${itemsPerPage}`} | |
| /> | |
| <span className="text-muted-foreground">per page</span> | |
| </div> | |
| {/* Page info */} | |
| <div className="text-sm text-muted-foreground" aria-live="polite" aria-atomic="true"> | |
| Showing {startItem} to {endItem} of {totalItems} items | |
| </div> | |
| {/* Page navigation */} | |
| <div className="flex items-center gap-1" role="group" aria-label="Pagination controls"> | |
| {/* First page */} | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => handlePageChange(1)} | |
| disabled={currentPage === 1} | |
| className="h-8 w-8 p-0" | |
| aria-label="Go to first page" | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| if (currentPage !== 1) { | |
| handlePageChange(1); | |
| } | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| > | |
| <ChevronsLeft className="h-4 w-4" aria-hidden="true" /> | |
| <span className="sr-only">First page</span> | |
| </Button> | |
| {/* Previous page */} | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => handlePageChange(currentPage - 1)} | |
| disabled={currentPage === 1} | |
| className="h-8 w-8 p-0" | |
| aria-label="Go to previous page" | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| if (currentPage > 1) { | |
| handlePageChange(currentPage - 1); | |
| } | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| > | |
| <ChevronLeft className="h-4 w-4" aria-hidden="true" /> | |
| <span className="sr-only">Previous page</span> | |
| </Button> | |
| {/* Page numbers */} | |
| {getPageNumbers().map((page, index) => ( | |
| <React.Fragment key={`page-${index}`}> | |
| {typeof page === 'number' ? ( | |
| <Button | |
| variant={page === currentPage ? 'default' : 'outline'} | |
| size="sm" | |
| onClick={() => handlePageChange(page)} | |
| className="h-8 w-8 p-0" | |
| aria-label={`Go to page ${page}`} | |
| aria-current={page === currentPage ? 'page' : undefined} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| handlePageChange(page); | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| > | |
| {page} | |
| </Button> | |
| ) : ( | |
| <span className="px-2 text-muted-foreground" aria-hidden="true">...</span> | |
| )} | |
| </React.Fragment> | |
| ))} | |
| {/* Next page */} | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => handlePageChange(currentPage + 1)} | |
| disabled={currentPage === totalPages} | |
| className="h-8 w-8 p-0" | |
| aria-label="Go to next page" | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| if (currentPage < totalPages) { | |
| handlePageChange(currentPage + 1); | |
| } | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| > | |
| <ChevronRight className="h-4 w-4" aria-hidden="true" /> | |
| <span className="sr-only">Next page</span> | |
| </Button> | |
| {/* Last page */} | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => handlePageChange(totalPages)} | |
| disabled={currentPage === totalPages} | |
| className="h-8 w-8 p-0" | |
| aria-label="Go to last page" | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| if (currentPage !== totalPages) { | |
| handlePageChange(totalPages); | |
| } | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| > | |
| <ChevronsRight className="h-4 w-4" aria-hidden="true" /> | |
| <span className="sr-only">Last page</span> | |
| </Button> | |
| </div> | |
| </nav> | |
| ); | |
| }; | |
| export default Pagination; |