-
-
Notifications
You must be signed in to change notification settings - Fork 755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add pagination feature in blog page #3595
base: master
Are you sure you want to change the base?
Changes from 1 commit
af0e6a1
1873828
1300a2a
7d649b3
20e3d3d
a06a8d2
458ca3e
66b9af3
454dd35
b514175
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T>(items: T[], itemsPerPage: number) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It takes a prop of items(all items) and returns the only item that has to be displayed on the current page |
||
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 | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please explain the else part of this logic? It seems to be very weird. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
return ( | ||
<div className='font-inter flex items-center justify-center gap-8'> | ||
{/* Previous button */} | ||
<button | ||
onClick={() => handlePageChange(currentPage - 1)} | ||
disabled={currentPage === 1} | ||
className={` | ||
font-normal flex h-[34px] items-center justify-center gap-2 rounded bg-white px-4 | ||
py-[7px] text-sm leading-[17px] tracking-[-0.01em] | ||
${currentPage === 1 ? 'cursor-not-allowed text-gray-300' : 'text-[#141717] hover:bg-gray-50'} | ||
`} | ||
> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have existing Button element, can't we use that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, i updated it |
||
<svg | ||
width='20' | ||
height='20' | ||
viewBox='0 0 24 24' | ||
fill='none' | ||
xmlns='http://www.w3.org/2000/svg' | ||
className='stroke-current' | ||
> | ||
<path d='M15 18L9 12L15 6' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' /> | ||
</svg> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add this SVG image as part of icons. Don't add the SVG code directly in the components. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, i have fixed it |
||
<span>Previous</span> | ||
</button> | ||
|
||
{/* Page numbers */} | ||
<div className='flex gap-2'> | ||
{getPageNumbers().map((page) => | ||
typeof page === 'number' ? ( | ||
<PaginationItem | ||
key={page} | ||
pageNumber={page} | ||
isActive={page === currentPage} | ||
onPageChange={handlePageChange} | ||
/> | ||
) : ( | ||
<span | ||
key={page} | ||
className='font-inter flex size-10 items-center justify-center text-sm font-semibold text-[#6B6B6B]' | ||
> | ||
... | ||
</span> | ||
) | ||
)} | ||
</div> | ||
|
||
{/* Next button */} | ||
<button | ||
onClick={() => handlePageChange(currentPage + 1)} | ||
disabled={currentPage === totalPages} | ||
className={` | ||
font-normal flex h-[34px] items-center justify-center gap-2 rounded bg-white px-4 | ||
py-[7px] text-sm leading-[17px] tracking-[-0.01em] | ||
${currentPage === totalPages ? 'cursor-not-allowed text-gray-300' : 'text-[#141717] hover:bg-gray-50'} | ||
`} | ||
> | ||
<span>Next</span> | ||
<svg | ||
width='20' | ||
height='20' | ||
viewBox='0 0 24 24' | ||
fill='none' | ||
xmlns='http://www.w3.org/2000/svg' | ||
className='stroke-current' | ||
> | ||
<path d='M9 6L15 12L9 18' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' /> | ||
</svg> | ||
</button> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<button | ||
onClick={() => onPageChange(pageNumber)} | ||
className={`font-inter font-normal relative flex size-10 items-center | ||
justify-center rounded-full text-sm leading-[26px] | ||
${isActive ? 'bg-[#6200EE] text-white' : 'bg-transparent text-[#141717] hover:bg-gray-50'} | ||
`} | ||
aria-current={isActive ? 'page' : undefined} | ||
> | ||
{pageNumber} | ||
</button> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling for invalid page numbers. When an invalid page number is provided in the URL, the code should handle it gracefully. useEffect(() => {
const pageFromQuery = parseInt(router.query.page as string, 10);
- if (!Number.isNaN(pageFromQuery) && pageFromQuery >= 1 && pageFromQuery !== currentPage) {
+ if (!Number.isNaN(pageFromQuery)) {
+ if (pageFromQuery >= 1 && pageFromQuery <= maxPage && pageFromQuery !== currentPage) {
setCurrentPage(pageFromQuery);
+ } else {
+ // Reset to page 1 for invalid page numbers
+ handlePageChange(1);
+ }
}
- }, [router.query.page]);
+ }, [router.query.page, maxPage]);
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the suggestion to add error handling for invalid page numbers. I implemented the proposed code, but I am encountering an issue where navigating to any page (e.g., ?page=2) redirects me back to ?page=1. It seems like the handlePageChange(1) function is being triggered unnecessarily, even when the pageFromQuery is valid. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @priyanshuxkumar I understand the issue. The problem occurs because we're validating against 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]); This should fix the issue by:
Let me know if this resolves the issue! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes its resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @priyanshuxkumar Great to hear that the pagination is working correctly now! 🎉 If you found this review helpful, would you consider giving us a shout-out on X? |
||
|
||
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 && ( | ||
<ul className='mx-auto mt-12 grid max-w-lg gap-5 lg:max-w-none lg:grid-cols-3'> | ||
{posts.map((post, index) => ( | ||
{currentItems.map((post, index) => ( | ||
<BlogPostItem key={index} post={post} /> | ||
))} | ||
</ul> | ||
)} | ||
{Object.keys(posts).length > 0 && !isClient && ( | ||
{Object.keys(currentItems).length > 0 && !isClient && ( | ||
<div className='h-screen w-full'> | ||
<Loader loaderText='Loading Blogs' className='mx-auto my-60' pulsating /> | ||
</div> | ||
)} | ||
{maxPage > 1 && ( | ||
<div className='mt-8 w-full'> | ||
<Pagination totalPages={maxPage} currentPage={currentPage} onPageChange={handlePageChange} /> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we handle filters in a better approach?
Think in an approach where we look for some specific variables from query params here, not all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
` const nonFilterableKeys = ['page'];
if (query && Object.keys(query).length >= 1) {
Object.keys(query).forEach((property) => {
if (nonFilterableKeys.includes(property)) {
return; // Skip non-filterable keys like 'page'
}
`
Dynamically handles filters while explicitly excluding non-filterable keys like 'page'