From af0e6a1f314d780198e88c491b9cccc73cdee610 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Wed, 22 Jan 2025 00:40:09 +0530 Subject: [PATCH 1/6] add pagination feature on blog page --- components/helpers/applyFilter.ts | 3 + components/helpers/usePagination.ts | 30 ++++++ components/pagination/Pagination.tsx | 122 +++++++++++++++++++++++ components/pagination/PaginationItem.tsx | 32 ++++++ pages/blog/index.tsx | 38 ++++++- 5 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 components/helpers/usePagination.ts create mode 100644 components/pagination/Pagination.tsx create mode 100644 components/pagination/PaginationItem.tsx diff --git a/components/helpers/applyFilter.ts b/components/helpers/applyFilter.ts index a7f3f217d5eb..c2772cce63e1 100644 --- a/components/helpers/applyFilter.ts +++ b/components/helpers/applyFilter.ts @@ -137,6 +137,9 @@ export const onFilterApply = ( if (query && Object.keys(query).length >= 1) { Object.keys(query).forEach((property) => { + if (property === 'page') { + return; + } 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/pagination/Pagination.tsx b/components/pagination/Pagination.tsx new file mode 100644 index 000000000000..862beade7665 --- /dev/null +++ b/components/pagination/Pagination.tsx @@ -0,0 +1,122 @@ +import React from 'react'; + +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) return; + onPageChange(page); + }; + + const getPageNumbers = () => { + const pages = []; + + if (totalPages <= 7) { + for (let i = 1; i <= totalPages; i++) pages.push(i); + } else { + pages.push(1); + if (currentPage > 3) { + pages.push('ellipsis1'); + } + + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + pages.push(i); + } + + if (currentPage < totalPages - 2) { + pages.push('ellipsis2'); + } + + pages.push(totalPages); + } + + return pages; + }; + + return ( +
+ {/* Previous button */} + + + {/* Page numbers */} +
+ {getPageNumbers().map((page) => + typeof page === 'number' ? ( + + ) : ( + + ... + + ) + )} +
+ + {/* Next button */} + +
+ ); +} 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 37958cec4308..906c440eae21 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,33 @@ 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) && pageFromQuery >= 1 && pageFromQuery !== currentPage) { + setCurrentPage(pageFromQuery); + } + }, [router.query.page]); + const [isClient, setIsClient] = useState(false); const onFilter = (data: IBlogPost[]) => setPosts(data); @@ -122,16 +151,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 && ( +
+ +
+ )} From 18738286bd9d239e174dc70fa8e655f7b86263f2 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Wed, 22 Jan 2025 01:53:18 +0530 Subject: [PATCH 2/6] Add error handling for invalid page numbers --- pages/blog/index.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pages/blog/index.tsx b/pages/blog/index.tsx index 906c440eae21..ad8042eda2da 100644 --- a/pages/blog/index.tsx +++ b/pages/blog/index.tsx @@ -58,10 +58,15 @@ export default function BlogIndexPage() { useEffect(() => { const pageFromQuery = parseInt(router.query.page as string, 10); - if (!Number.isNaN(pageFromQuery) && pageFromQuery >= 1 && pageFromQuery !== currentPage) { - setCurrentPage(pageFromQuery); + 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]); + }, [router.query.page, maxPage, currentPage]); const [isClient, setIsClient] = useState(false); From 20e3d3dd144e230f5bddc437e7779c24e6cfc3d7 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Sun, 26 Jan 2025 11:20:29 +0530 Subject: [PATCH 3/6] add Next and Previos icon fix(filters): exclude non-filterable keys dynamically and update the function to render page number on pagination component --- components/helpers/applyFilter.ts | 5 +- components/icons/Next.tsx | 20 +++++ components/icons/Previous.tsx | 20 +++++ components/pagination/Pagination.tsx | 108 ++++++++++++--------------- 4 files changed, 90 insertions(+), 63 deletions(-) create mode 100644 components/icons/Next.tsx create mode 100644 components/icons/Previous.tsx diff --git a/components/helpers/applyFilter.ts b/components/helpers/applyFilter.ts index c2772cce63e1..9386990f9d6f 100644 --- a/components/helpers/applyFilter.ts +++ b/components/helpers/applyFilter.ts @@ -133,12 +133,13 @@ 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 (property === 'page') { - return; + if (nonFilterableKeys.includes(property)) { + return; // Skip non-filterable keys like 'page' } const res = result.filter((e) => { if (!query[property] || e[property] === query[property]) { 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..4b78a6c4d535 --- /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 IconPrevios() { + return ( + + + + ); +} diff --git a/components/pagination/Pagination.tsx b/components/pagination/Pagination.tsx index 862beade7665..dde0f92a1bdc 100644 --- a/components/pagination/Pagination.tsx +++ b/components/pagination/Pagination.tsx @@ -1,5 +1,10 @@ import React from 'react'; +import { ButtonIconPosition } from '@/types/components/buttons/ButtonPropsType'; + +import Button from '../buttons/Button'; +import IconNext from '../icons/Next'; +import IconPrevios from '../icons/Previous'; import PaginationItem from './PaginationItem'; export interface PaginationProps { @@ -20,59 +25,53 @@ export interface PaginationProps { */ export default function Pagination({ totalPages, currentPage, onPageChange }: PaginationProps) { const handlePageChange = (page: number) => { - if (page < 1 || page > totalPages) return; - onPageChange(page); + if (page >= 1 && page <= totalPages) { + onPageChange(page); + } }; - const getPageNumbers = () => { - const pages = []; - + /** + * @returns number of pages shows in Pagination. + */ + const getPageNumbers = (): (number | string)[] => { if (totalPages <= 7) { - for (let i = 1; i <= totalPages; i++) pages.push(i); - } else { - pages.push(1); - if (currentPage > 3) { - pages.push('ellipsis1'); - } - - for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { - pages.push(i); - } - - if (currentPage < totalPages - 2) { - pages.push('ellipsis2'); - } - - pages.push(totalPages); + 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); + } + + if (currentPage < totalPages - 2) { + pages.push('ellipsis2'); + } + + pages.push(totalPages); + return pages; }; return (
{/* Previous button */} - + className={`font-normal flex h-[34px] items-center justify-center rounded bg-white px-3 py-[7px] text-sm leading-[17px] tracking-[-0.01em] ${ + currentPage === 1 ? 'cursor-not-allowed text-gray-300' : 'text-[#141717] hover:bg-gray-50' + }`} + text='Previous' + icon={} + iconPosition={ButtonIconPosition.LEFT} + /> {/* Page numbers */}
@@ -96,27 +95,14 @@ export default function Pagination({ totalPages, currentPage, onPageChange }: Pa
{/* Next button */} - + className={`font-normal flex h-[34px] items-center justify-center rounded bg-white px-3 py-[7px] text-sm leading-[17px] tracking-[-0.01em] ${ + currentPage === totalPages ? 'cursor-not-allowed text-gray-300' : 'text-[#141717] hover:bg-gray-50' + }`} + text='Next' + icon={} + />
); } From 66b9af36758bae37f33595087d162ee4e20deaea Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Sun, 26 Jan 2025 12:02:51 +0530 Subject: [PATCH 4/6] fix: IconPrevious typo and Improve accessibility and clean up style --- components/icons/Previous.tsx | 2 +- components/pagination/Pagination.tsx | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/components/icons/Previous.tsx b/components/icons/Previous.tsx index 4b78a6c4d535..3bf10d5e3b84 100644 --- a/components/icons/Previous.tsx +++ b/components/icons/Previous.tsx @@ -4,7 +4,7 @@ import React from 'react'; /** * @description Icons for Previous button in pagination */ -export default function IconPrevios() { +export default function IconPrevious() { return ( +
+
{getPageNumbers().map((page) => typeof page === 'number' ? ( ) : ( @@ -97,12 +102,16 @@ export default function Pagination({ totalPages, currentPage, onPageChange }: Pa {/* Next button */}
+ ); } From 454dd357a768529cef48faf3f5f3c81f286bcb89 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Sun, 26 Jan 2025 12:11:09 +0530 Subject: [PATCH 5/6] Fix CSS class concatenation --- components/pagination/Pagination.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/pagination/Pagination.tsx b/components/pagination/Pagination.tsx index 6f876f287a12..03d2416b3d2b 100644 --- a/components/pagination/Pagination.tsx +++ b/components/pagination/Pagination.tsx @@ -105,7 +105,7 @@ export default function Pagination({ totalPages, currentPage, onPageChange }: Pa disabled={currentPage === totalPages} className={`font-normal flex h-[34px] items-center justify-center rounded bg-white px-3 py-[7px] text-sm leading-[17px] tracking-[-0.01em] ${ currentPage === totalPages - ? 'cursor-not-allowedtext-gray-300 hover:bg-gray-white text-gray-300' + ? 'hover:bg-gray-white cursor-not-allowed text-gray-300' : 'text-[#141717] hover:bg-gray-50' }`} text='Next' From 18b8a55049b1a33949b828b929cff93505ad1677 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Thu, 6 Feb 2025 01:28:13 +0530 Subject: [PATCH 6/6] fix pagination: mobile view --- components/buttons/Button.tsx | 2 +- components/pagination/Pagination.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) 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/pagination/Pagination.tsx b/components/pagination/Pagination.tsx index 03d2416b3d2b..99c1a80c9624 100644 --- a/components/pagination/Pagination.tsx +++ b/components/pagination/Pagination.tsx @@ -51,17 +51,17 @@ export default function Pagination({ totalPages, currentPage, onPageChange }: Pa pages.push(i); } - if (currentPage < totalPages - 2) { - pages.push('ellipsis2'); - } - pages.push(totalPages); return pages; }; return ( -