Skip to content

Commit

Permalink
refs #178 Add pagination providers and components
Browse files Browse the repository at this point in the history
  • Loading branch information
elboletaire authored and selankon committed Aug 20, 2024
1 parent 5b31eb3 commit 5abd316
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 0 deletions.
51 changes: 51 additions & 0 deletions packages/chakra-components/docs/04-Pagination.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: Pagination components
description: Vocdoni Chakra components pagination
---

## Pagination and RoutedPagination

You can easily add pagination to any method by using the included PaginationProvider (or RoutedPaginationProvider in
case you want to use it with react-router-dom)

```jsx
const MyRoutedPaginatedComponent = () => {
// retrieve your data
const query = useQuery()


return (
{/* specify the total pages and the expected path for the router provider */}
<RoutedPaginationProvider totalPages={query.data.totalPages} path='/my-paginated-route-path'>
{query.data.map((item) => (
<MyItem key={item.id} item={item} />
))}
{/* load the pagination itself */}
<Pagination />
</RoutedPaginationProvider>
)
}
```

The Pagination component will automatically handle the pagination state and will update the URL with the current page.

The `PaginationProvider` (non routed) uses an internal state to handle the current page, rather than taking it from the URL.

```jsx
const MyPaginatedComponent = () => {
// retrieve your data
const query = useQuery()


return (
{/* specify the total pages for the router provider */}
<PaginationProvider totalPages={query.data.totalPages}>
{query.data.map((item) => (
<MyItem key={item.id} item={item} />
))}
{/* load the pagination itself */}
<Pagination />
</PaginationProvider>
)
}
```
184 changes: 184 additions & 0 deletions packages/chakra-components/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { Button, ButtonGroup, ButtonGroupProps, ButtonProps, Input, InputProps } from '@chakra-ui/react'
import { usePagination, useRoutedPagination } from '@vocdoni/react-providers'
import { ReactElement, useMemo, useState } from 'react'
import { generatePath, Link as RouterLink, useLocation, useNavigate, useParams } from 'react-router-dom'

export type PaginationProps = ButtonGroupProps & {
maxButtons?: number | false
buttonProps?: ButtonProps
inputProps?: InputProps
}

const createButton = (page: number, currentPage: number, props: ButtonProps) => (
<Button key={page} isActive={currentPage === page} {...props}>
{page + 1}
</Button>
)

type EllipsisButtonProps = ButtonProps & {
gotoPage: (page: number) => void
inputProps?: InputProps
}

const EllipsisButton = ({ gotoPage, inputProps, ...rest }: EllipsisButtonProps) => {
const [ellipsisInput, setEllipsisInput] = useState(false)

if (ellipsisInput) {
return (
<Input
placeholder='Page #'
width='50px'
{...inputProps}
onKeyDown={(e) => {
if (e.target instanceof HTMLInputElement && e.key === 'Enter') {
const pageNumber = Number(e.target.value)
gotoPage(pageNumber)
setEllipsisInput(false)
}
}}
onBlur={() => setEllipsisInput(false)}
autoFocus
/>
)
}

return (
<Button
as='a'
href='#goto-page'
{...rest}
onClick={(e) => {
e.preventDefault()
setEllipsisInput(true)
}}
>
...
</Button>
)
}

const usePaginationPages = (
currentPage: number,
totalPages: number | undefined,
maxButtons: number | undefined | false,
gotoPage: (page: number) => void,
createPageButton: (i: number) => ReactElement,
inputProps?: InputProps,
buttonProps?: ButtonProps
) => {
return useMemo(() => {
if (totalPages === undefined) return []

let pages: ReactElement[] = []

// Create an array of all page buttons
for (let i = 0; i < totalPages; i++) {
pages.push(createPageButton(i))
}

if (!maxButtons || totalPages <= maxButtons) {
return pages
}

const startEllipsis = (
<EllipsisButton key='start-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} />
)
const endEllipsis = (
<EllipsisButton key='end-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} />
)

// Add ellipsis and slice the array accordingly
const sideButtons = 2 // First and last page
const availableButtons = maxButtons - sideButtons // Buttons we can distribute around the current page

if (currentPage <= availableButtons / 2) {
// Near the start
return [...pages.slice(0, availableButtons), endEllipsis, pages[totalPages - 1]]
} else if (currentPage >= totalPages - 1 - availableButtons / 2) {
// Near the end
return [pages[0], startEllipsis, ...pages.slice(totalPages - availableButtons, totalPages)]
} else {
// In the middle
const startPage = currentPage - Math.floor((availableButtons - 1) / 2)
const endPage = currentPage + Math.floor(availableButtons / 2)
return [pages[0], startEllipsis, ...pages.slice(startPage, endPage - 1), endEllipsis, pages[totalPages - 1]]
}
}, [currentPage, totalPages, maxButtons, gotoPage])
}

export const Pagination = ({ maxButtons = 10, buttonProps, inputProps, ...rest }: PaginationProps) => {
const { page, setPage, totalPages } = usePagination()

const pages = usePaginationPages(
page,
totalPages,
maxButtons ? Math.max(5, maxButtons) : false,
(page) => {
if (page >= 0 && totalPages && page < totalPages) {
setPage(page)
}
},
(i) => createButton(i, page, { onClick: () => setPage(i), ...buttonProps })
)

return (
<ButtonGroup {...rest}>
{totalPages === undefined ? (
<>
<Button key='previous' onClick={() => setPage(page - 1)} isDisabled={page === 0} {...buttonProps}>
Previous
</Button>
<Button key='next' onClick={() => setPage(page + 1)} {...buttonProps}>
Next
</Button>
</>
) : (
pages
)}
</ButtonGroup>
)
}

export const RoutedPagination = ({ maxButtons = 10, buttonProps, ...rest }: PaginationProps) => {
const { path, totalPages } = useRoutedPagination()
const { search } = useLocation()
const { page, ...extraParams }: { page?: number } = useParams()
const navigate = useNavigate()

const p = Number(page) || 1

const _generatePath = (page: number) => generatePath(path, { page, ...extraParams }) + search

const pages = usePaginationPages(
p,
totalPages,
maxButtons ? Math.max(5, maxButtons) : false,
(page) => {
if (page >= 0 && totalPages && page < totalPages) {
navigate(_generatePath(page))
}
},
(i) => (
<Button as={RouterLink} key={i} to={_generatePath(i + 1)} isActive={p - 1 === i} {...buttonProps}>
{i + 1}
</Button>
)
)

return (
<ButtonGroup {...rest}>
{totalPages === undefined ? (
<>
<Button key='previous' onClick={() => navigate(_generatePath(p - 1))} isDisabled={p === 1} {...buttonProps}>
Previous
</Button>
<Button key='next' onClick={() => navigate(_generatePath(p + 1))} {...buttonProps}>
Next
</Button>
</>
) : (
pages
)}
</ButtonGroup>
)
}
1 change: 1 addition & 0 deletions packages/react-providers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './client'
export * from './election'
export * from './i18n'
export * from './organization'
export * from './pagination'
export type { ErrorPayload, RecursivePartial } from './types'
export * from './utils'
50 changes: 50 additions & 0 deletions packages/react-providers/src/pagination/PaginationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createContext, PropsWithChildren, useContext, useState } from 'react'

export type PaginationContextProps = {
page: number
setPage: (page: number) => void
totalPages?: number
}

export type RoutedPaginationContextProps = Omit<PaginationContextProps, 'setPage' | 'page'> & {
path: string
}

const PaginationContext = createContext<PaginationContextProps | undefined>(undefined)
const RoutedPaginationContext = createContext<RoutedPaginationContextProps | undefined>(undefined)

export const usePagination = (): PaginationContextProps => {
const context = useContext(PaginationContext)
if (!context) {
throw new Error('usePagination must be used within a PaginationProvider')
}
return context
}

export const useRoutedPagination = (): RoutedPaginationContextProps => {
const context = useContext(RoutedPaginationContext)
if (!context) {
throw new Error('useRoutedPagination must be used within a RoutedPaginationProvider')
}
return context
}

export type PaginationProviderProps = Pick<PaginationContextProps, 'totalPages'>

export type RoutedPaginationProviderProps = PaginationProviderProps & {
path: string
}

export const RoutedPaginationProvider = ({
totalPages,
path,
...rest
}: PropsWithChildren<RoutedPaginationProviderProps>) => {
return <RoutedPaginationContext.Provider value={{ totalPages, path }} {...rest} />
}

export const PaginationProvider = ({ totalPages, ...rest }: PropsWithChildren<PaginationProviderProps>) => {
const [page, setPage] = useState<number>(0)

return <PaginationContext.Provider value={{ page, setPage, totalPages }} {...rest} />
}
1 change: 1 addition & 0 deletions packages/react-providers/src/pagination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PaginationProvider'

0 comments on commit 5abd316

Please sign in to comment.