| import { useState, useEffect, useCallback } from 'react' |
| import { useTranslation } from 'react-i18next' |
| import Button from './Button' |
| import Input from './Input' |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './Select' |
| import { cn } from '@/lib/utils' |
| import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon } from 'lucide-react' |
|
|
| export type PaginationControlsProps = { |
| currentPage: number |
| totalPages: number |
| pageSize: number |
| totalCount: number |
| onPageChange: (page: number) => void |
| onPageSizeChange: (pageSize: number) => void |
| isLoading?: boolean |
| compact?: boolean |
| className?: string |
| } |
|
|
| const PAGE_SIZE_OPTIONS = [ |
| { value: 10, label: '10' }, |
| { value: 20, label: '20' }, |
| { value: 50, label: '50' }, |
| { value: 100, label: '100' }, |
| { value: 200, label: '200' } |
| ] |
|
|
| export default function PaginationControls({ |
| currentPage, |
| totalPages, |
| pageSize, |
| totalCount, |
| onPageChange, |
| onPageSizeChange, |
| isLoading = false, |
| compact = false, |
| className |
| }: PaginationControlsProps) { |
| const { t } = useTranslation() |
| const [inputPage, setInputPage] = useState(currentPage.toString()) |
|
|
| |
| useEffect(() => { |
| setInputPage(currentPage.toString()) |
| }, [currentPage]) |
|
|
| |
| const handlePageInputChange = useCallback((value: string) => { |
| setInputPage(value) |
| }, []) |
|
|
| |
| const handlePageInputSubmit = useCallback(() => { |
| const pageNum = parseInt(inputPage, 10) |
| if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) { |
| onPageChange(pageNum) |
| } else { |
| |
| setInputPage(currentPage.toString()) |
| } |
| }, [inputPage, totalPages, onPageChange, currentPage]) |
|
|
| |
| const handlePageInputKeyPress = useCallback((e: React.KeyboardEvent) => { |
| if (e.key === 'Enter') { |
| handlePageInputSubmit() |
| } |
| }, [handlePageInputSubmit]) |
|
|
| |
| const handlePageSizeChange = useCallback((value: string) => { |
| const newPageSize = parseInt(value, 10) |
| if (!isNaN(newPageSize)) { |
| onPageSizeChange(newPageSize) |
| } |
| }, [onPageSizeChange]) |
|
|
| |
| const goToFirstPage = useCallback(() => { |
| if (currentPage > 1 && !isLoading) { |
| onPageChange(1) |
| } |
| }, [currentPage, onPageChange, isLoading]) |
|
|
| const goToPrevPage = useCallback(() => { |
| if (currentPage > 1 && !isLoading) { |
| onPageChange(currentPage - 1) |
| } |
| }, [currentPage, onPageChange, isLoading]) |
|
|
| const goToNextPage = useCallback(() => { |
| if (currentPage < totalPages && !isLoading) { |
| onPageChange(currentPage + 1) |
| } |
| }, [currentPage, totalPages, onPageChange, isLoading]) |
|
|
| const goToLastPage = useCallback(() => { |
| if (currentPage < totalPages && !isLoading) { |
| onPageChange(totalPages) |
| } |
| }, [currentPage, totalPages, onPageChange, isLoading]) |
|
|
| if (totalPages <= 1) { |
| return null |
| } |
|
|
| if (compact) { |
| return ( |
| <div className={cn('flex items-center gap-2', className)}> |
| <div className="flex items-center gap-1"> |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={goToPrevPage} |
| disabled={currentPage <= 1 || isLoading} |
| className="h-8 w-8 p-0" |
| > |
| <ChevronLeftIcon className="h-4 w-4" /> |
| </Button> |
| |
| <div className="flex items-center gap-1"> |
| <Input |
| type="text" |
| value={inputPage} |
| onChange={(e) => handlePageInputChange(e.target.value)} |
| onBlur={handlePageInputSubmit} |
| onKeyPress={handlePageInputKeyPress} |
| disabled={isLoading} |
| className="h-8 w-12 text-center text-sm" |
| /> |
| <span className="text-sm text-gray-500">/ {totalPages}</span> |
| </div> |
| |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={goToNextPage} |
| disabled={currentPage >= totalPages || isLoading} |
| className="h-8 w-8 p-0" |
| > |
| <ChevronRightIcon className="h-4 w-4" /> |
| </Button> |
| </div> |
| |
| <Select |
| value={pageSize.toString()} |
| onValueChange={handlePageSizeChange} |
| disabled={isLoading} |
| > |
| <SelectTrigger className="h-8 w-16"> |
| <SelectValue /> |
| </SelectTrigger> |
| <SelectContent> |
| {PAGE_SIZE_OPTIONS.map((option) => ( |
| <SelectItem key={option.value} value={option.value.toString()}> |
| {option.label} |
| </SelectItem> |
| ))} |
| </SelectContent> |
| </Select> |
| </div> |
| ) |
| } |
|
|
| return ( |
| <div className={cn('flex items-center justify-between gap-4', className)}> |
| <div className="text-sm text-gray-500"> |
| {t('pagination.showing', { |
| start: Math.min((currentPage - 1) * pageSize + 1, totalCount), |
| end: Math.min(currentPage * pageSize, totalCount), |
| total: totalCount |
| })} |
| </div> |
| |
| <div className="flex items-center gap-2"> |
| <div className="flex items-center gap-1"> |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={goToFirstPage} |
| disabled={currentPage <= 1 || isLoading} |
| className="h-8 w-8 p-0" |
| tooltip={t('pagination.firstPage')} |
| > |
| <ChevronsLeftIcon className="h-4 w-4" /> |
| </Button> |
| |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={goToPrevPage} |
| disabled={currentPage <= 1 || isLoading} |
| className="h-8 w-8 p-0" |
| tooltip={t('pagination.prevPage')} |
| > |
| <ChevronLeftIcon className="h-4 w-4" /> |
| </Button> |
| |
| <div className="flex items-center gap-1"> |
| <span className="text-sm">{t('pagination.page')}</span> |
| <Input |
| type="text" |
| value={inputPage} |
| onChange={(e) => handlePageInputChange(e.target.value)} |
| onBlur={handlePageInputSubmit} |
| onKeyPress={handlePageInputKeyPress} |
| disabled={isLoading} |
| className="h-8 w-16 text-center text-sm" |
| /> |
| <span className="text-sm">/ {totalPages}</span> |
| </div> |
| |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={goToNextPage} |
| disabled={currentPage >= totalPages || isLoading} |
| className="h-8 w-8 p-0" |
| tooltip={t('pagination.nextPage')} |
| > |
| <ChevronRightIcon className="h-4 w-4" /> |
| </Button> |
| |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={goToLastPage} |
| disabled={currentPage >= totalPages || isLoading} |
| className="h-8 w-8 p-0" |
| tooltip={t('pagination.lastPage')} |
| > |
| <ChevronsRightIcon className="h-4 w-4" /> |
| </Button> |
| </div> |
| |
| <div className="flex items-center gap-2"> |
| <span className="text-sm">{t('pagination.pageSize')}</span> |
| <Select |
| value={pageSize.toString()} |
| onValueChange={handlePageSizeChange} |
| disabled={isLoading} |
| > |
| <SelectTrigger className="h-8 w-16"> |
| <SelectValue /> |
| </SelectTrigger> |
| <SelectContent> |
| {PAGE_SIZE_OPTIONS.map((option) => ( |
| <SelectItem key={option.value} value={option.value.toString()}> |
| {option.label} |
| </SelectItem> |
| ))} |
| </SelectContent> |
| </Select> |
| </div> |
| </div> |
| </div> |
| ) |
| } |
|
|