diff --git a/src/assets/build/chevron-left-outline.icon.tsx b/src/assets/build/chevron-left-outline.icon.tsx new file mode 100644 index 00000000..e391eb32 --- /dev/null +++ b/src/assets/build/chevron-left-outline.icon.tsx @@ -0,0 +1,18 @@ +export const ChevronLeftOutlineIcon = (props: any) => ( + +) diff --git a/src/assets/build/chevron-right-outline.icon.tsx b/src/assets/build/chevron-right-outline.icon.tsx new file mode 100644 index 00000000..a2fd8e2b --- /dev/null +++ b/src/assets/build/chevron-right-outline.icon.tsx @@ -0,0 +1,18 @@ +export const ChevronRightOutlineIcon = (props: any) => ( + +) diff --git a/src/assets/index.ts b/src/assets/index.ts index 0fe6b52a..3710dd13 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -11,5 +11,7 @@ export * from './build/folder.icon' export * from './build/circle-outline.icon' export * from './build/angle-down.icon' export * from './build/calendar.icon' +export * from './build/chevron-left-outline.icon' +export * from './build/chevron-right-outline.icon' export * from './build/arrow-left.icon' export * from './build/arrow-right.icon' diff --git a/src/components/Pagination/index.tsx b/src/components/Pagination/index.tsx new file mode 100644 index 00000000..4177aca5 --- /dev/null +++ b/src/components/Pagination/index.tsx @@ -0,0 +1,183 @@ +import React, { useState, useEffect } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' +import { ChevronLeftOutlineIcon, ChevronRightOutlineIcon } from '@/assets' + +interface PaginationProps { + currentPage: number + setCurrentPage: React.Dispatch> + numberOfItemsPerPage: number + totalNumberOfFilteredItems: number +} + +const PaginationVariants = cva([ + '[&>*]:flex', + '[&>*]:items-center', + '[&>*]:justify-center', + 'w-fit' +]) + +const PageNumberStyles = cva([ + 'min-w-40', + 'max-w-40', + 'h-10', + 'px-4', + 'py-2', + 'rounded-sm', + 'text-foreground-muted', + 'active:bg-accent', + 'active:text-primary-50', + 'dark:hover:bg-background-dark', + 'dark:active:bg-primary-300', + 'dark:active:text-grey-900' +]) + +const PageNumberActiveStyles = cva([ + 'bg-accent', + 'text-primary-50', + 'hover:bg-accent', + 'hover:text-primary-50', + 'dark:bg-primary-300', + 'dark:text-grey-900', + 'dark:hover:bg-primary-300', + 'dark:hover:text-grey-900' +]) + +interface PaginationProps + extends React.ComponentPropsWithoutRef<'div'>, + VariantProps {} + +export const Pagination = React.forwardRef( + ( + { + className, + currentPage, + setCurrentPage, + numberOfItemsPerPage, + totalNumberOfFilteredItems, + ...props + }, + refs + ) => { + const currentRowsLength = totalNumberOfFilteredItems ?? 0 + + const totalPages = React.useMemo(() => { + return Math.ceil( + currentRowsLength > 0 ? currentRowsLength / numberOfItemsPerPage : 0 + ) + }, [currentRowsLength, numberOfItemsPerPage]) + + const allPages = [...Array(totalPages + 1).keys()].slice(1) + const [pageNumbers, setPageNumbers] = useState(allPages) + const [showLeftDots, setShowLeftDots] = useState(false) + const [showRightDots, setShowRightDots] = useState(false) + const noOfSiblings = 1 + const noOfPagesShown = noOfSiblings * 2 + 5 + + useEffect(() => { + if (totalPages <= noOfPagesShown) { + setPageNumbers(allPages) + setShowRightDots(false) + setShowLeftDots(false) + } else if (currentPage <= noOfPagesShown - 3) { + const pages = allPages.slice(0, noOfPagesShown - 2) + pages.push(totalPages) + setPageNumbers(pages) + setShowRightDots(true) + setShowLeftDots(false) + } else if ( + noOfPagesShown - 3 < currentPage && + currentPage < totalPages - 3 + ) { + const pages = [1] + const start = Math.max(2, currentPage - noOfSiblings) + const end = Math.min(totalPages - 1, currentPage + noOfSiblings) + for (let i = start; i <= end; i++) { + pages.push(i) + } + pages.push(totalPages) + setPageNumbers(pages) + setShowRightDots(true) + setShowLeftDots(true) + } else if (currentPage >= totalPages - 3) { + const pages = allPages.slice(-noOfPagesShown + 2) + pages.unshift(1) + setPageNumbers(pages) + setShowRightDots(false) + setShowLeftDots(true) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage, totalPages]) + + const goToNextPage = () => { + if (currentPage !== totalPages) setCurrentPage(currentPage + 1) + } + + const goToPrevPage = () => { + if (currentPage !== 1) setCurrentPage(currentPage - 1) + } + return ( + + ) + } +) diff --git a/src/components/index.ts b/src/components/index.ts index 1d2f36f6..f5661989 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,6 +8,7 @@ export * from './Tooltip' export * from './Input' export * from './Switch' export * from './Chip' +export * from './Pagination' export * from './Modal' export * from './WebsiteFooter' export * from './Select' diff --git a/stories/Pagination/Docs.mdx b/stories/Pagination/Docs.mdx new file mode 100644 index 00000000..07476a28 --- /dev/null +++ b/stories/Pagination/Docs.mdx @@ -0,0 +1,222 @@ +import { Canvas, Meta } from '@storybook/blocks' + +import * as PaginationStories from './Pagination.stories' + + + +# Pagination + +The Pagination component enables the user to select a specific page from a range of pages. + + + +## Pagination Code + +```ts +import React, { useState, useEffect } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' +import { ChevronLeftOutlineIcon, ChevronRightOutlineIcon } from '@/assets' + +interface PaginationProps { + currentPage: number + setCurrentPage: React.Dispatch> + numberOfItemsPerPage: number + totalNumberOfFilteredItems: number +} + +const PaginationVariants = cva([ + '[&>*]:flex', + '[&>*]:items-center', + '[&>*]:justify-center', + 'w-fit' +]) + +const PageNumberStyles = cva([ + 'min-w-40', + 'max-w-40', + 'h-10', + 'px-4', + 'py-2', + 'rounded-sm', + 'text-foreground-muted', + 'active:bg-accent', + 'active:text-primary-50', + 'dark:hover:bg-background-dark', + 'dark:active:bg-primary-300', + 'dark:active:text-grey-900' +]) + +const PageNumberActiveStyles = cva([ + 'bg-accent', + 'text-primary-50', + 'hover:bg-accent', + 'hover:text-primary-50', + 'dark:bg-primary-300', + 'dark:text-grey-900', + 'dark:hover:bg-primary-300', + 'dark:hover:text-grey-900' +]) + +interface PaginationProps + extends React.ComponentPropsWithoutRef<'div'>, + VariantProps {} + +export const Pagination = React.forwardRef( + ( + { + className, + currentPage, + setCurrentPage, + numberOfItemsPerPage, + totalNumberOfFilteredItems, + ...props + }, + refs + ) => { + const currentRowsLength = totalNumberOfFilteredItems ?? 0 + + const totalPages = React.useMemo(() => { + return Math.ceil( + currentRowsLength > 0 ? currentRowsLength / numberOfItemsPerPage : 0 + ) + }, [currentRowsLength, numberOfItemsPerPage]) + + const allPages = [...Array(totalPages + 1).keys()].slice(1) + const [pageNumbers, setPageNumbers] = useState(allPages) + const [showLeftDots, setShowLeftDots] = useState(false) + const [showRightDots, setShowRightDots] = useState(false) + const noOfSiblings = 1 + const noOfPagesShown = noOfSiblings * 2 + 5 + + useEffect(() => { + if (totalPages <= noOfPagesShown) { + setPageNumbers(allPages) + setShowRightDots(false) + setShowLeftDots(false) + } else if (currentPage <= noOfPagesShown - 3) { + const pages = allPages.slice(0, noOfPagesShown - 2) + pages.push(totalPages) + setPageNumbers(pages) + setShowRightDots(true) + setShowLeftDots(false) + } else if ( + noOfPagesShown - 3 < currentPage && + currentPage < totalPages - 3 + ) { + const pages = [1] + const start = Math.max(2, currentPage - noOfSiblings) + const end = Math.min(totalPages - 1, currentPage + noOfSiblings) + for (let i = start; i <= end; i++) { + pages.push(i) + } + pages.push(totalPages) + setPageNumbers(pages) + setShowRightDots(true) + setShowLeftDots(true) + } else if (currentPage >= totalPages - 3) { + const pages = allPages.slice(-noOfPagesShown + 2) + pages.unshift(1) + setPageNumbers(pages) + setShowRightDots(false) + setShowLeftDots(true) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage, totalPages]) + + const goToNextPage = () => { + if (currentPage !== totalPages) setCurrentPage(currentPage + 1) + } + + const goToPrevPage = () => { + if (currentPage !== 1) setCurrentPage(currentPage - 1) + } + return ( + + ) + } +) +``` + +## Example Usage + +```ts +import { Pagination } from '@/components/Pagination' +import { useState } from 'react' + +export const PaginationDemo = () => { + const [currentPage, setCurrentPage] = useState(1) + const [numberOfItemsPerPage, setNumberOfItemsPerPage] = useState(10) + const [totalNumberOfFilteredItems, setTotalNumberOfFilteredItems] = + useState(100) + + return ( + + ) +} +``` diff --git a/stories/Pagination/Pagination.example.tsx b/stories/Pagination/Pagination.example.tsx new file mode 100644 index 00000000..b0391c13 --- /dev/null +++ b/stories/Pagination/Pagination.example.tsx @@ -0,0 +1,18 @@ +import { Pagination } from '@/components/Pagination' +import { useState } from 'react' + +export const PaginationDemo = () => { + const [currentPage, setCurrentPage] = useState(1) + const [numberOfItemsPerPage, setNumberOfItemsPerPage] = useState(10) + const [totalNumberOfFilteredItems, setTotalNumberOfFilteredItems] = + useState(100) + + return ( + + ) +} diff --git a/stories/Pagination/Pagination.stories.ts b/stories/Pagination/Pagination.stories.ts new file mode 100644 index 00000000..4f5cc5d0 --- /dev/null +++ b/stories/Pagination/Pagination.stories.ts @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { PaginationDemo } from './Pagination.example' + +const meta = { + title: 'Components/Pagination', + component: PaginationDemo +} as Meta + +export default meta +type Story = StoryObj + +export const Pagination: Story = {}