mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-25 10:02:27 -04:00
167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
import {ChevronLeftIcon, ChevronRightIcon} from '@heroicons/react/24/solid'
|
|
import clsx from 'clsx'
|
|
import {range} from 'lodash'
|
|
import {usePathname, useRouter} from 'next/navigation'
|
|
import {useEffect} from 'react'
|
|
import {buttonClass} from 'web/components/buttons/button'
|
|
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
|
import {useDefinedSearchParams} from 'web/hooks/use-defined-search-params'
|
|
|
|
import {Row} from '../layout/row'
|
|
import {Spacer} from '../layout/spacer'
|
|
export const PAGE_ELLIPSES = '...'
|
|
|
|
export function PaginationNextPrev(props: {
|
|
className?: string
|
|
isStart: boolean
|
|
isEnd: boolean
|
|
isLoading: boolean
|
|
isComplete: boolean
|
|
getPrev: () => void
|
|
getNext: () => void
|
|
}) {
|
|
const {className, isStart, isEnd, isLoading, isComplete, getPrev, getNext} = props
|
|
return (
|
|
<Row
|
|
aria-label="Pagination"
|
|
className={clsx(className, 'flex-1 justify-between gap-2 sm:justify-end')}
|
|
>
|
|
<button className={buttonClass('lg', 'gray-outline')} onClick={getPrev} disabled={isStart}>
|
|
Previous
|
|
</button>
|
|
<button
|
|
className={buttonClass('lg', 'gray-outline')}
|
|
onClick={getNext}
|
|
disabled={isEnd && (isLoading || isComplete)}
|
|
>
|
|
{isEnd && isLoading ? <LoadingIndicator size="sm" /> : 'Next'}
|
|
</button>
|
|
</Row>
|
|
)
|
|
}
|
|
|
|
export function Pagination(props: {
|
|
page: number
|
|
pageSize: number
|
|
totalItems: number
|
|
setPage: (page: number) => void
|
|
className?: string
|
|
savePageToQuery?: boolean
|
|
}) {
|
|
const {page, pageSize, totalItems, setPage, className, savePageToQuery} = props
|
|
const router = useRouter()
|
|
const {searchParams, createQueryString} = useDefinedSearchParams()
|
|
const pathname = usePathname()
|
|
|
|
useEffect(() => {
|
|
if (!savePageToQuery) return
|
|
const p = searchParams.get('p')
|
|
if (p && page !== parseInt(p as string)) {
|
|
setPage(parseInt(p as string))
|
|
} else if (!p && page !== 0) {
|
|
setPage(0)
|
|
}
|
|
}, [searchParams])
|
|
|
|
const onClick = (page: number) => {
|
|
if (savePageToQuery) {
|
|
router.push(pathname + '?' + createQueryString('p', page.toString()))
|
|
} else setPage(page)
|
|
}
|
|
|
|
const maxPage = Math.ceil(totalItems / pageSize) - 1
|
|
|
|
if (maxPage <= 0) return <Spacer h={4} />
|
|
|
|
const pageNumbers = getPageNumbers(maxPage, page)
|
|
return (
|
|
<nav
|
|
className={clsx('flex w-full items-center bg-inherit pb-4 pt-2', className)}
|
|
aria-label="Pagination"
|
|
>
|
|
<Row className="mx-auto gap-4">
|
|
<PaginationArrow onClick={() => onClick(page - 1)} disabled={page <= 0} nextOrPrev="prev" />
|
|
<Row className="gap-2">
|
|
{pageNumbers.map((pageNumber, index) => (
|
|
<PageNumbers
|
|
key={pageNumber === PAGE_ELLIPSES ? `${pageNumber}-${index}` : pageNumber}
|
|
pageNumber={pageNumber}
|
|
setPage={onClick}
|
|
page={page}
|
|
/>
|
|
))}
|
|
</Row>
|
|
<PaginationArrow
|
|
onClick={() => onClick(page + 1)}
|
|
disabled={page >= maxPage}
|
|
nextOrPrev="next"
|
|
/>
|
|
</Row>
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
export function PaginationArrow(props: {
|
|
onClick: () => void
|
|
disabled: boolean
|
|
nextOrPrev: 'next' | 'prev'
|
|
}) {
|
|
const {onClick, disabled, nextOrPrev} = props
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
className={clsx(
|
|
'select-none rounded-lg transition-colors',
|
|
disabled
|
|
? 'text-ink-200 pointer-events-none'
|
|
: 'hover:bg-ink-100 text-primary-700 cursor-pointer',
|
|
)}
|
|
>
|
|
{nextOrPrev === 'prev' && <ChevronLeftIcon className="h-[24px] w-[24px]" />}
|
|
{nextOrPrev === 'next' && <ChevronRightIcon className="h-[24px] w-[24px]" />}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function PageNumbers(props: {
|
|
pageNumber: PageNumbers
|
|
setPage: (page: number) => void
|
|
page: number
|
|
}) {
|
|
const {pageNumber, setPage, page} = props
|
|
if (pageNumber === PAGE_ELLIPSES || typeof pageNumber === 'string') {
|
|
return <div className="text-ink-600 select-none">{PAGE_ELLIPSES}</div>
|
|
}
|
|
return (
|
|
<button
|
|
onClick={() => setPage(pageNumber)}
|
|
className={clsx(
|
|
'select-none rounded-lg px-2',
|
|
page === pageNumber ? 'bg-primary-100 text-primary-700' : 'text-ink-600 hover:bg-ink-100',
|
|
)}
|
|
>
|
|
{pageNumber + 1}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
type PageNumbers = number | string
|
|
|
|
function getPageNumbers(maxPage: number, page: number): Array<PageNumbers> {
|
|
if (maxPage <= 7) {
|
|
return range(0, maxPage + 1)
|
|
}
|
|
if (page < 4) {
|
|
return Array.from<unknown, PageNumbers>({length: 5}, (_, index) => index).concat([
|
|
PAGE_ELLIPSES,
|
|
maxPage,
|
|
])
|
|
}
|
|
if (page >= maxPage - 3) {
|
|
return [0, PAGE_ELLIPSES].concat(
|
|
Array.from<unknown, PageNumbers>({length: 5}, (_, index) => index + maxPage - 4),
|
|
)
|
|
}
|
|
return [0, PAGE_ELLIPSES, page - 1, page, page + 1, PAGE_ELLIPSES, maxPage]
|
|
}
|