import { MouseEvent, useCallback, useEffect, useState } from 'react'; import { PaginationProps } from '@theme/components/types'; import { twMerge } from 'tailwind-merge'; import clsx from 'clsx'; import { Link, Button, LoaderSpinner } from '@theme/components'; import usePagination from '@akinon/next/hooks/use-pagination'; import { useLocalization } from '@akinon/next/hooks'; import { useRouter } from '@akinon/next/hooks'; import { useInView } from 'react-intersection-observer'; const parseSafeUrl = (url: string) => { try { return new URL(url, window.location.origin); } catch { return null; } }; export const Pagination = (props: PaginationProps) => { const { t } = useLocalization(); const router = useRouter(); const { total, limit, currentPage, numberOfPages, containerClassName, prevClassName, pageClassName, nextClassName, moreButtonClassName, threshold = 1, type = 'list', onPageChange, direction, render, isLoading, customStyles } = props; const pagination = usePagination(total, limit, currentPage, numberOfPages); const { total: paginationTotal, limit: paginationLimit, page, pageList, prev, next, last, setTotal, setLimit } = pagination; const [paginationItems, setPaginationItems] = useState([]); const showNext = currentPage * paginationLimit < paginationTotal; const { ref, inView } = useInView({ threshold: 0.75 }); const [prevPage, setPrevPage] = useState(page); const [nextPage, setNextPage] = useState(page); const createListItems = useCallback(() => { setPaginationItems([]); const delta = 2; const startPage = Math.max(Number(page) - delta, 1); const endPage = Math.min(Number(page) + delta, numberOfPages); setPaginationItems((prev) => [ ...prev, { page: pageList[0].page, url: pageList[0].url } ]); if (delta < startPage) { setPaginationItems((prev) => [...prev, { page: '...', url: '#' }]); } for (let i = startPage; i <= endPage; i++) { if (i === 1) continue; setPaginationItems((prev) => [ ...prev, { page: pageList[i - 1]?.page, url: pageList[i - 1]?.url } ]); } if (endPage < numberOfPages - threshold) { setPaginationItems((prev) => [...prev, { page: '...', url: '#' }]); } if (page < numberOfPages - delta) { setPaginationItems((prev) => [ ...prev, { page: pageList[pageList.length - threshold].page, url: pageList[pageList.length - threshold].url } ]); } }, [numberOfPages, page, pageList, threshold]); const handleClick = (e: MouseEvent, url: string) => { e.preventDefault(); const newUrl = parseSafeUrl(url); if (!newUrl) { return; } const page = newUrl.searchParams.get('page'); if (page === '1') { newUrl.searchParams.delete('page'); } router.push(newUrl.pathname + newUrl.search, undefined); }; const handlePageChange = () => { let changingPage; if (direction === 'prev') { changingPage = Number(type !== 'list' ? prevPage : page) - 1; setPrevPage(changingPage); } else { changingPage = Number(type !== 'list' ? nextPage : page) + 1; setNextPage(changingPage); } // Navigate to new page using router if onPageChange is not provided if (onPageChange) { onPageChange(changingPage); } else { const currentUrl = parseSafeUrl(window.location.href); if (!currentUrl) { return; } currentUrl.searchParams.set('page', String(changingPage)); if (changingPage === 1) { currentUrl.searchParams.delete('page'); } router.push(currentUrl.pathname + currentUrl.search, undefined); } }; useEffect(() => { if (type === 'infinite' && page === 1) { setPrevPage(1); setNextPage(1); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [page]); useEffect(() => { if (inView) { handlePageChange(); } }, [inView]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (type === 'list') { createListItems(); } }, [page]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (total && total !== paginationTotal) { setTotal(total); } }, [total, paginationTotal, setTotal]); useEffect(() => { if (limit && limit !== paginationLimit) { setLimit(limit); } }, [limit, paginationLimit, setLimit]); if (render) { return <>{render(pagination)}; } // Custom styles for pagination container const containerStyles: React.CSSProperties = { marginTop: customStyles?.marginTop || '32px', marginBottom: customStyles?.marginBottom || '16px', gap: customStyles?.pageGap || '8px' }; // Custom styles for page links (button-like) const pageStyles: React.CSSProperties = { fontSize: customStyles?.pageFontSize || '14px', color: customStyles?.pageColor || '#9ca3af', backgroundColor: customStyles?.pageBgColor || 'transparent', paddingLeft: customStyles?.pagePaddingX || '8px', paddingRight: customStyles?.pagePaddingX || '8px', paddingTop: customStyles?.pagePaddingY || '4px', paddingBottom: customStyles?.pagePaddingY || '4px', borderRadius: customStyles?.pageBorderRadius || '0px', borderWidth: customStyles?.pageBorderWidth || '0px', borderStyle: 'solid', borderColor: customStyles?.pageBorderColor || '#e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'center' }; const activePageStyles: React.CSSProperties = { fontSize: customStyles?.pageFontSize || '14px', color: customStyles?.pageActiveColor || '#1a1a1a', fontWeight: customStyles?.pageActiveFontWeight || '600', backgroundColor: customStyles?.pageActiveBgColor || 'transparent', paddingLeft: customStyles?.pagePaddingX || '8px', paddingRight: customStyles?.pagePaddingX || '8px', paddingTop: customStyles?.pagePaddingY || '4px', paddingBottom: customStyles?.pagePaddingY || '4px', borderRadius: customStyles?.pageBorderRadius || '0px', borderWidth: customStyles?.pageBorderWidth || '0px', borderStyle: 'solid', borderColor: customStyles?.pageActiveBorderColor || '#1a1a1a', display: 'flex', alignItems: 'center', justifyContent: 'center' }; const arrowStyles: React.CSSProperties = { color: customStyles?.arrowColor || '#1a1a1a' }; // Custom styles for more button const buttonStyles: React.CSSProperties = { backgroundColor: customStyles?.buttonBgColor || '#000000', color: customStyles?.buttonTextColor || '#ffffff', borderRadius: customStyles?.buttonBorderRadius || '4px', paddingLeft: customStyles?.buttonPaddingX || '20px', paddingRight: customStyles?.buttonPaddingX || '20px', paddingTop: customStyles?.buttonPaddingY || '12px', paddingBottom: customStyles?.buttonPaddingY || '12px' }; return direction === 'prev' && type !== 'list' ? ( <>
) : ( <> {type === 'more' && (
)} {type === 'infinite' && Number(nextPage) !== last && (
)} {(type === 'infinite' || type === 'more') && Number(nextPage) === last && !isLoading && (

{t('category.pagination.shown_items')}

)} {type === 'list' && ( )} ); };