diff --git a/packages/chakra-components/docs/04-Pagination.mdx b/packages/chakra-components/docs/04-Pagination.mdx new file mode 100644 index 00000000..202988ac --- /dev/null +++ b/packages/chakra-components/docs/04-Pagination.mdx @@ -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 */} + + {query.data.map((item) => ( + + ))} + {/* load the pagination itself */} + + + ) +} +``` + +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 */} + + {query.data.map((item) => ( + + ))} + {/* load the pagination itself */} + + + ) +} +``` diff --git a/packages/chakra-components/src/components/Pagination.tsx b/packages/chakra-components/src/components/Pagination.tsx new file mode 100644 index 00000000..0bd3be9a --- /dev/null +++ b/packages/chakra-components/src/components/Pagination.tsx @@ -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) => ( + +) + +type EllipsisButtonProps = ButtonProps & { + gotoPage: (page: number) => void + inputProps?: InputProps +} + +const EllipsisButton = ({ gotoPage, inputProps, ...rest }: EllipsisButtonProps) => { + const [ellipsisInput, setEllipsisInput] = useState(false) + + if (ellipsisInput) { + return ( + { + if (e.target instanceof HTMLInputElement && e.key === 'Enter') { + const pageNumber = Number(e.target.value) + gotoPage(pageNumber) + setEllipsisInput(false) + } + }} + onBlur={() => setEllipsisInput(false)} + autoFocus + /> + ) + } + + return ( + + ) +} + +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 = ( + + ) + const endEllipsis = ( + + ) + + // 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 ( + + {totalPages === undefined ? ( + <> + + + + ) : ( + pages + )} + + ) +} + +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) => ( + + ) + ) + + return ( + + {totalPages === undefined ? ( + <> + + + + ) : ( + pages + )} + + ) +} diff --git a/packages/react-providers/src/index.ts b/packages/react-providers/src/index.ts index 5fc44f53..ed9a74f8 100644 --- a/packages/react-providers/src/index.ts +++ b/packages/react-providers/src/index.ts @@ -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' diff --git a/packages/react-providers/src/pagination/PaginationProvider.tsx b/packages/react-providers/src/pagination/PaginationProvider.tsx new file mode 100644 index 00000000..b17be5bf --- /dev/null +++ b/packages/react-providers/src/pagination/PaginationProvider.tsx @@ -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 & { + path: string +} + +const PaginationContext = createContext(undefined) +const RoutedPaginationContext = createContext(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 + +export type RoutedPaginationProviderProps = PaginationProviderProps & { + path: string +} + +export const RoutedPaginationProvider = ({ + totalPages, + path, + ...rest +}: PropsWithChildren) => { + return +} + +export const PaginationProvider = ({ totalPages, ...rest }: PropsWithChildren) => { + const [page, setPage] = useState(0) + + return +} diff --git a/packages/react-providers/src/pagination/index.ts b/packages/react-providers/src/pagination/index.ts new file mode 100644 index 00000000..24823042 --- /dev/null +++ b/packages/react-providers/src/pagination/index.ts @@ -0,0 +1 @@ +export * from './PaginationProvider'