Skip to content

Commit

Permalink
Improved and simplified pagination logic
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed Jul 18, 2024
1 parent ad61d5d commit f66c067
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 59 deletions.
39 changes: 14 additions & 25 deletions src/components/elements/paged-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const PagedList = ({
// Use the GET param for page, but make sure that it is between 1 and the last page. If it's a string or a number
// outside the range, fix the value, so it works as expected.
const {count: currentPage, setCount: setPage} = useCounter(
Math.min(totalPages, pageKey ? Math.max(1, parseInt(searchParams.get(pageKey) || "")) : 1)
Math.min(totalPages, Math.max(1, parseInt((pageKey && searchParams.get(pageKey)) || "1")))
)
const {value: focusOnElement, setTrue: enableFocusElement, setFalse: disableFocusElement} = useBoolean(false)

Expand All @@ -69,18 +69,18 @@ const PagedList = ({
(page: number, doNotFocusOnResults?: boolean) => {
runAction(page - 1)
.then(response => {
if (!response) return

// Set the rendering to the response from the server. If the response has a suspense boundary, it will have a
// fallback prop. Then we only want to render the list of children within the suspense.
setItems(response.props.fallback ? response.props.children.props.children : response.props.children)

// When loading a page during the initial page load, we don't want to focus on anything. But when a user changes
// pages, we want to focus on the first element.
if (!doNotFocusOnResults) enableFocusElement()
setPage(page)
if (response) {
// Set the rendering to the response from the server. If the response has a suspense boundary, it will have a
// fallback prop. Then we only want to render the list of children within the suspense.
setItems(response.props.fallback ? response.props.children.props.children : response.props.children)

// When loading a page during the initial page load, we don't want to focus on anything. But when a user changes
// pages, we want to focus on the first element.
if (!doNotFocusOnResults) enableFocusElement()
setPage(page)
}
})
.catch(() => console.error("An error occurred fetching more results."))
.catch(() => console.warn("An error occurred fetching more results."))
},
[enableFocusElement, setPage, runAction]
)
Expand All @@ -97,17 +97,14 @@ const PagedList = ({
// Use search params to retain any other parameters.
const params = new URLSearchParams(searchParams.toString())
params.delete(pageKey)

if (currentPage > 1) params.set(pageKey, `${currentPage}`)

router.replace(`?${params.toString()}`, {scroll: false})
}, [loadPage, router, currentPage, pageKey, searchParams])

useEffect(() => {
if (currentPage > 1 && !ref.current) {
ref.current = true
goToPage(currentPage, true)
}
if (currentPage > 1 && !ref.current) goToPage(currentPage, true)
ref.current = true
}, [currentPage, goToPage])

const paginationButtons = usePagination(totalPages * items.length, currentPage, items.length, pagerSiblingCount)
Expand Down Expand Up @@ -161,7 +158,6 @@ const PaginationButton = ({
currentPage,
total,
onPageClick,
pagerSiblingCount,
disabled,
}: {
page: number | string
Expand All @@ -186,13 +182,6 @@ const PaginationButton = ({
onPageClick(page as number)
}

// Conditionally render left arrow and right arrow based on currentPage
if (page === 1 && currentPage >= pagerSiblingCount + 3) return null
if (page === "leftArrow" && currentPage < pagerSiblingCount + 3) return null

if (page === total && currentPage <= total - (pagerSiblingCount + 3)) return null
if (page === "rightArrow" && currentPage > total - (pagerSiblingCount + 3)) return null

const isCurrent = page === currentPage
return (
<li className="m-0 flex items-center">
Expand Down
59 changes: 25 additions & 34 deletions src/lib/hooks/usePagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,60 +11,51 @@ import {useMemo} from "react"
* How many items per page.
* @param siblingCount
* How many page buttons to display left and right of the current page.
*
* @return
* Page numbers.
*/
const usePagination = (totalCount: number, currentPage = 1, pageSize = 5, siblingCount = 2): (number | string)[] => {
return useMemo(() => {
const totalPageCount = Math.ceil(totalCount / pageSize)

// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2 * DOTS + 2 * ARROWS
const totalPageNumbers = siblingCount + 7
// Total pages to display are twice the siblings plus the current page: sibling, current, sibling
const totalPageNumbers = siblingCount * 2 + 1

// Arrow constants
// Arrow constants.
const leftArrow = "leftArrow"
const rightArrow = "rightArrow"

// Case 1: If the number of pages is less than the page numbers we want to show in our paginationComponent, we
// return the range [1..totalPageCount]
// If the total displayed pages is less than or equal to the total pages, we just display all pages.
if (totalPageNumbers >= totalPageCount) {
return [leftArrow, ...range(1, totalPageCount), rightArrow]
return range(1, totalPageCount)
}

// Calculate left and right sibling index and make sure they are within range 1 and totalPageCount
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1)
const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPageCount)

// We do not show dots just when there is just one page number to be inserted between the extremes of sibling and the page limits i.e 1 and totalPageCount. Hence we are using leftSiblingIndex > 2 and rightSiblingIndex < totalPageCount - 2
const shouldShowLeftDots = leftSiblingIndex > 2
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2
// Start from page 1 or the siblingCount pages away from the current page.
const leftStart = Math.max(1, Math.max(1, currentPage) - siblingCount)
const leftRange = range(leftStart, currentPage - 1)

const firstPageIndex = 1
const lastPageIndex = totalPageCount
// End with the next siblingCount pages after the current page, or the last pages in the list.
const rightRange = range(currentPage + 1, Math.min(totalPageCount, currentPage + siblingCount))

// Case 2: No left dots to show, but rights dots to be shown
if (!shouldShowLeftDots && shouldShowRightDots) {
const leftItemCount = 3 + 2 * siblingCount
const leftRange = range(1, leftItemCount)
// If there are more pages past the siblings.
const shouldShowLeftDots = leftStart > 1
// If there are more pages after the siblings.
const shouldShowRightDots = !!rightRange.length && rightRange[rightRange.length - 1] !== totalPageCount

return [leftArrow, ...leftRange, 0, totalPageCount, rightArrow]
// More pages exists before and after the displayed pages.
if (shouldShowLeftDots && shouldShowRightDots) {
return [leftArrow, 0, ...leftRange, currentPage, ...rightRange, 0, rightArrow]
}

// Case 3: No right dots to show, but left dots to be shown
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = 3 + 2 * siblingCount
const rightRange = range(totalPageCount - rightItemCount + 1, totalPageCount)

return [leftArrow, firstPageIndex, 0, ...rightRange, rightArrow]
// More pages only to the left of the displayed pages.
if (shouldShowLeftDots) {
return [leftArrow, 0, ...leftRange, currentPage, ...rightRange]
}

// Case 4: Both left and right dots to be shown
if (shouldShowLeftDots && shouldShowRightDots) {
const middleRange = range(leftSiblingIndex, rightSiblingIndex)
return [leftArrow, firstPageIndex, 0, ...middleRange, 0, lastPageIndex, rightArrow]
// More pages to the right of the displayed pages.
if (shouldShowRightDots) {
return [...leftRange, currentPage, ...rightRange, 0, rightArrow]
}
return []

return [...leftRange, currentPage, ...rightRange]
}, [totalCount, pageSize, siblingCount, currentPage])
}

Expand Down

0 comments on commit f66c067

Please sign in to comment.