diff --git a/components/buttons/Button.tsx b/components/buttons/Button.tsx index e19093238ebc..261f992f4a4f 100644 --- a/components/buttons/Button.tsx +++ b/components/buttons/Button.tsx @@ -8,7 +8,7 @@ type IButtonProps = { // eslint-disable-next-line prettier/prettier /** The text to be displayed on the button. */ - text: string; + text: string | React.ReactNode; /** The type of the button. Defaults to 'button'. */ type?: ButtonType; diff --git a/components/helpers/applyFilter.ts b/components/helpers/applyFilter.ts index a7f3f217d5eb..9386990f9d6f 100644 --- a/components/helpers/applyFilter.ts +++ b/components/helpers/applyFilter.ts @@ -133,10 +133,14 @@ export const onFilterApply = ( onFilter: (result: DataObject[], query: Filter) => void, query: Filter ): void => { + const nonFilterableKeys = ['page']; let result = inputData; if (query && Object.keys(query).length >= 1) { Object.keys(query).forEach((property) => { + if (nonFilterableKeys.includes(property)) { + return; // Skip non-filterable keys like 'page' + } const res = result.filter((e) => { if (!query[property] || e[property] === query[property]) { return e[property]; diff --git a/components/helpers/usePagination.ts b/components/helpers/usePagination.ts new file mode 100644 index 000000000000..567f0ca0b5d4 --- /dev/null +++ b/components/helpers/usePagination.ts @@ -0,0 +1,30 @@ +import { useMemo, useState } from 'react'; + +/** + * @description Custom hook for managing pagination logic + * @example const { currentPage, setCurrentPage, currentItems, maxPage } = usePagination(items, 10); + * @param {T[]} items - Array of items to paginate + * @param {number} itemsPerPage - Number of items per page + * @returns {object} + * @returns {number} currentPage - Current page number + * @returns {function} setCurrentPage - Function to update the current page + * @returns {T[]} currentItems - Items for the current page + * @returns {number} maxPage - Total number of pages + */ +export function usePagination(items: T[], itemsPerPage: number) { + const [currentPage, setCurrentPage] = useState(1); + const maxPage = Math.ceil(items.length / itemsPerPage); + + const currentItems = useMemo(() => { + const start = (currentPage - 1) * itemsPerPage; + + return items.slice(start, start + itemsPerPage); + }, [items, currentPage, itemsPerPage]); + + return { + currentPage, + setCurrentPage, + currentItems, + maxPage + }; +} diff --git a/components/icons/Next.tsx b/components/icons/Next.tsx new file mode 100644 index 000000000000..97e34cd26bc4 --- /dev/null +++ b/components/icons/Next.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +/* eslint-disable max-len */ +/** + * @description Icons for Next button + */ +export default function IconNext() { + return ( + + + + ); +} diff --git a/components/icons/Previous.tsx b/components/icons/Previous.tsx new file mode 100644 index 000000000000..3bf10d5e3b84 --- /dev/null +++ b/components/icons/Previous.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +/* eslint-disable max-len */ +/** + * @description Icons for Previous button in pagination + */ +export default function IconPrevious() { + return ( + + + + ); +} diff --git a/components/pagination/Pagination.tsx b/components/pagination/Pagination.tsx new file mode 100644 index 000000000000..99c1a80c9624 --- /dev/null +++ b/components/pagination/Pagination.tsx @@ -0,0 +1,117 @@ +import React from 'react'; + +import { ButtonIconPosition } from '@/types/components/buttons/ButtonPropsType'; + +import Button from '../buttons/Button'; +import IconNext from '../icons/Next'; +import IconPrevious from '../icons/Previous'; +import PaginationItem from './PaginationItem'; + +export interface PaginationProps { + // eslint-disable-next-line prettier/prettier + + /** Total number of pages */ + totalPages: number; + + /** Current active page */ + currentPage: number; + + /** Function to handle page changes */ + onPageChange: (page: number) => void; +} + +/** + * This is the Pagination component. It displays a list of page numbers that can be clicked to navigate. + */ +export default function Pagination({ totalPages, currentPage, onPageChange }: PaginationProps) { + const handlePageChange = (page: number) => { + if (page >= 1 && page <= totalPages) { + onPageChange(page); + } + }; + + /** + * @returns number of pages shows in Pagination. + */ + const getPageNumbers = (): (number | string)[] => { + if (totalPages <= 7) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + + const pages: (number | string)[] = [1]; + + if (currentPage > 3) { + pages.push('ellipsis1'); + } + + const start = Math.max(2, currentPage - 1); + const end = Math.min(totalPages - 1, currentPage + 1); + + for (let i = start; i <= end; i++) { + pages.push(i); + } + + pages.push(totalPages); + + return pages; + }; + + return ( + + ); +} diff --git a/components/pagination/PaginationItem.tsx b/components/pagination/PaginationItem.tsx new file mode 100644 index 000000000000..dd3c5a9e5de5 --- /dev/null +++ b/components/pagination/PaginationItem.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +export interface PaginationItemProps { + // eslint-disable-next-line prettier/prettier + + /** The page number to display */ + pageNumber: number; + + /** Whether this page is currently active */ + isActive: boolean; + + /** Function to handle page change */ + onPageChange: (page: number) => void; +} + +/** + * This is the PaginationItem component. It displays a single page number that can be clicked. + */ +export default function PaginationItem({ pageNumber, isActive, onPageChange }: PaginationItemProps) { + return ( + + ); +} diff --git a/pages/blog/index.tsx b/pages/blog/index.tsx index b6a13e507157..db4bf13c533e 100644 --- a/pages/blog/index.tsx +++ b/pages/blog/index.tsx @@ -1,11 +1,13 @@ import { useRouter } from 'next/router'; import React, { useContext, useEffect, useState } from 'react'; +import { usePagination } from '@/components/helpers/usePagination'; import Empty from '@/components/illustrations/Empty'; import GenericLayout from '@/components/layout/GenericLayout'; import Loader from '@/components/Loader'; import BlogPostItem from '@/components/navigation/BlogPostItem'; import Filter from '@/components/navigation/Filter'; +import Pagination from '@/components/pagination/Pagination'; import Heading from '@/components/typography/Heading'; import Paragraph from '@/components/typography/Paragraph'; import TextLink from '@/components/typography/TextLink'; @@ -34,6 +36,38 @@ export default function BlogIndexPage() { }) : [] ); + + const postsPerPage = 9; + const { currentPage, setCurrentPage, currentItems, maxPage } = usePagination(posts, postsPerPage); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + + const currentFilters = { ...router.query, page: page.toString() }; + + router.push( + { + pathname: router.pathname, + query: currentFilters + }, + undefined, + { shallow: true } + ); + }; + + useEffect(() => { + const pageFromQuery = parseInt(router.query.page as string, 10); + + if (!Number.isNaN(pageFromQuery) && maxPage > 0) { + if (pageFromQuery >= 1 && pageFromQuery <= maxPage && pageFromQuery !== currentPage) { + setCurrentPage(pageFromQuery); + } else if (pageFromQuery < 1 || pageFromQuery > maxPage) { + // Only reset to page 1 if the page number is actually invalid + handlePageChange(1); + } + } + }, [router.query.page, maxPage, currentPage]); + const [isClient, setIsClient] = useState(false); const onFilter = (data: IBlogPost[]) => setPosts(data); @@ -122,16 +156,21 @@ export default function BlogIndexPage() { )} {Object.keys(posts).length > 0 && isClient && ( )} - {Object.keys(posts).length > 0 && !isClient && ( + {Object.keys(currentItems).length > 0 && !isClient && (
)} + {maxPage > 1 && ( +
+ +
+ )}