-
-
Notifications
You must be signed in to change notification settings - Fork 765
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 in blog page #3638
base: master
Are you sure you want to change the base?
Changes from 4 commits
b90097d
a1d2be6
1ff9f2f
d599488
7162008
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,20 @@ | ||
import React from 'react'; | ||
|
||
/* eslint-disable max-len */ | ||
/** | ||
* @description Icons for Next button | ||
*/ | ||
export default function IconNext() { | ||
return ( | ||
<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> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import React from 'react'; | ||
|
||
/* eslint-disable max-len */ | ||
/** | ||
* @description Icons for Previous button in pagination | ||
*/ | ||
export default function IconPrevious() { | ||
return ( | ||
<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> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
|
||
import { ButtonIconPosition } from '@/types/components/buttons/ButtonPropsType'; | ||
|
||
import Button from '../buttons/Button'; | ||
import IconNext from '../icons/Next'; | ||
import IconPrevious from '../icons/Previous'; | ||
|
||
/** | ||
* Props for the BlogPagination component | ||
* @property {number} blogsPerPage - Number of blogs to display per page | ||
* @property {number} totalBlogs - Total number of blogs | ||
* @property {function} paginate - Callback function to handle page changes | ||
* @property {number} currentPage - Current active page number | ||
*/ | ||
interface BlogPaginationProps { | ||
// eslint-disable-next-line prettier/prettier | ||
|
||
blogsPerPage: number; | ||
totalBlogs: number; | ||
paginate: (pageNumber: number) => void; | ||
currentPage: number; | ||
} | ||
|
||
/** | ||
* A pagination component for blog posts that displays page numbers and navigation buttons | ||
* @param {BlogPaginationProps} props - The props for the component | ||
* @returns {JSX.Element} A navigation element with pagination controls | ||
*/ | ||
export default function BlogPagination({ | ||
blogsPerPage, | ||
totalBlogs, | ||
paginate, | ||
currentPage, | ||
}: BlogPaginationProps): JSX.Element { | ||
const totalPages: number = Math.ceil(totalBlogs / blogsPerPage); | ||
const pagesToShow: number = 6; | ||
const [pageNumbers, setPageNumbers] = useState<(number | string)[]>([]); | ||
|
||
const calculatePageNumbers = () => { | ||
const numbers: (number | string)[] = []; | ||
|
||
if (totalPages < 1) return []; | ||
if (totalPages <= pagesToShow) { | ||
for (let i = 1; i <= totalPages; i++) { | ||
numbers.push(i); | ||
} | ||
} else if (currentPage <= 2) { | ||
for (let i = 1; i <= 3; i++) { | ||
numbers.push(i); | ||
} | ||
numbers.push('...'); | ||
numbers.push(totalPages - 2); | ||
numbers.push(totalPages - 1); | ||
numbers.push(totalPages); | ||
} else if (currentPage >= totalPages - 1) { | ||
numbers.push(1); | ||
numbers.push(2); | ||
numbers.push(3); | ||
numbers.push('...'); | ||
for (let i = totalPages - 2; i <= totalPages; i++) { | ||
numbers.push(i); | ||
} | ||
} else { | ||
numbers.push(1); | ||
numbers.push('...'); | ||
numbers.push(currentPage - 1); | ||
numbers.push(currentPage); | ||
numbers.push(currentPage + 1); | ||
numbers.push('...'); | ||
numbers.push(totalPages); | ||
} | ||
|
||
return numbers; | ||
}; | ||
|
||
useEffect(() => { | ||
setPageNumbers(calculatePageNumbers()); | ||
}, [currentPage, totalBlogs]); | ||
|
||
return ( | ||
<nav | ||
aria-label="Blog pagination" | ||
className="mt-8 flex items-center justify-center gap-2 p-4" | ||
> | ||
{/* Previous button */} | ||
<Button | ||
className={`flex items-center gap-2 ${currentPage === 1 ? 'cursor-not-allowed opacity-50' : ''} size-[120px] rounded-l-md px-4 py-2`} | ||
aria-label="Previous page" | ||
bgClassName="bg-white" | ||
textClassName="text-[#212525] font-inter text-[14px] font-normal" | ||
text="Previous" | ||
disabled={currentPage === 1} | ||
onClick={() => paginate(currentPage - 1)} | ||
icon={<IconPrevious />} | ||
iconPosition={ButtonIconPosition.LEFT} | ||
/> | ||
{/* Page numbers */} | ||
<div className="flex w-[35vw] justify-center gap-3"> | ||
{pageNumbers.map((number, index) => ( | ||
<button | ||
key={index} | ||
className={`size-[40px] ${number === currentPage ? 'rounded border bg-[#6200EE] text-white' : 'text-[#6B6B6B]'}`} | ||
aria-label={`${typeof number === 'number' ? `Go to page ${number}` : 'More pages'}`} | ||
aria-current={number === currentPage ? 'page' : undefined} | ||
onClick={() => typeof number === 'number' && paginate(number)} | ||
disabled={number === '...'} | ||
> | ||
{number} | ||
</button> | ||
))} | ||
</div> | ||
{/* Next button */} | ||
<Button | ||
className={`flex items-center gap-2 ${currentPage === totalPages && 'cursor-not-allowed opacity-50'} h-[35px] w-[120px] rounded-l-md px-4 py-2`} | ||
bgClassName="bg-white" | ||
textClassName="text-[#212525] font-inter text-[14px] font-normal" | ||
text="Next" | ||
disabled={currentPage === totalPages} | ||
onClick={() => paginate(currentPage + 1)} | ||
icon={<IconNext />} | ||
iconPosition={ButtonIconPosition.RIGHT} | ||
/> | ||
</nav> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||
import { useRouter } from 'next/router'; | ||||||||||||||||||||||||||||
import React, { useContext, useEffect, useState } from 'react'; | ||||||||||||||||||||||||||||
import React, { useContext, useEffect, useState, useMemo } from 'react'; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
import Empty from '@/components/illustrations/Empty'; | ||||||||||||||||||||||||||||
import GenericLayout from '@/components/layout/GenericLayout'; | ||||||||||||||||||||||||||||
|
@@ -13,6 +13,7 @@ import BlogContext from '@/context/BlogContext'; | |||||||||||||||||||||||||||
import type { IBlogPost } from '@/types/post'; | ||||||||||||||||||||||||||||
import { HeadingLevel, HeadingTypeStyle } from '@/types/typography/Heading'; | ||||||||||||||||||||||||||||
import { ParagraphTypeStyle } from '@/types/typography/Paragraph'; | ||||||||||||||||||||||||||||
import BlogPagination from '@/components/navigation/BlogPagination'; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||
* @description The BlogIndexPage is the blog index page of the website. | ||||||||||||||||||||||||||||
|
@@ -35,7 +36,6 @@ export default function BlogIndexPage() { | |||||||||||||||||||||||||||
: [] | ||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||
const [isClient, setIsClient] = useState(false); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
const onFilter = (data: IBlogPost[]) => setPosts(data); | ||||||||||||||||||||||||||||
const toFilter = [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
|
@@ -50,14 +50,56 @@ export default function BlogIndexPage() { | |||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
]; | ||||||||||||||||||||||||||||
const clearFilters = () => { | ||||||||||||||||||||||||||||
router.push(`${router.pathname}`, undefined, { | ||||||||||||||||||||||||||||
shallow: true | ||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||
const { page } = router.query; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
router.push( | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
pathname: router.pathname, | ||||||||||||||||||||||||||||
query: { ...(page && { page }) }, | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
undefined, | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
shallow: true, | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||
const showClearFilters = Object.keys(router.query).length > 0; | ||||||||||||||||||||||||||||
const showClearFilters = Object.keys(router.query).length > 1; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
const description = 'Find the latest and greatest stories from our community'; | ||||||||||||||||||||||||||||
const image = '/img/social/blog.webp'; | ||||||||||||||||||||||||||||
const blogsPerPage = 9; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
const [currentPage, setCurrentPage] = useState<number>(1); | ||||||||||||||||||||||||||||
const currentPosts = useMemo(() => { | ||||||||||||||||||||||||||||
const indexOfLastPost = currentPage * blogsPerPage; | ||||||||||||||||||||||||||||
const indexOfFirstPost = indexOfLastPost - blogsPerPage; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return posts.slice(indexOfFirstPost, indexOfLastPost); | ||||||||||||||||||||||||||||
}, [currentPage, posts]); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
const paginate = (pageNumber: number) => { | ||||||||||||||||||||||||||||
setCurrentPage(pageNumber); | ||||||||||||||||||||||||||||
const { query } = router; | ||||||||||||||||||||||||||||
const newQuery = { | ||||||||||||||||||||||||||||
...query, | ||||||||||||||||||||||||||||
page: pageNumber, | ||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||
router.push( | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
pathname: router.pathname, | ||||||||||||||||||||||||||||
query: newQuery, | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
undefined, | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
shallow: true, | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
useEffect(()=>{ | ||||||||||||||||||||||||||||
const currentPageNumber = Math.max(1, Number.isNaN(parseInt(router.query.page as string, 10)) ? 1 : parseInt(router.query.page as string, 10)); | ||||||||||||||||||||||||||||
setCurrentPage(currentPageNumber); | ||||||||||||||||||||||||||||
},[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. Fix formatting in useEffect hook. The useEffect implementation has spacing and formatting issues. Apply these formatting fixes: - useEffect(()=>{
+ useEffect(() => {
const currentPageNumber = Math.max(
1,
Number.isNaN(parseInt(router.query.page as string, 10))
? 1
: parseInt(router.query.page as string, 10)
);
setCurrentPage(currentPageNumber);
- },[router.query.page]);
+ }, [router.query.page]); 📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 99-99: Replace (prettier/prettier) [error] 100-100: Expected blank line after variable declarations. (newline-after-var) [error] 100-100: Replace (prettier/prettier) [error] 101-101: Expected blank line before this statement. (padding-line-between-statements) [error] 101-101: Trailing spaces not allowed. (no-trailing-spaces) [error] 101-101: Delete (prettier/prettier) [error] 102-102: A space is required after ','. (comma-spacing) [error] 102-102: Insert (prettier/prettier) 🪛 GitHub Actions: PR testing - if Node project[error] 99-99: Replace [error] 100-100: Expected blank line after variable declarations. newline-after-var [error] 100-100: Replace [error] 101-101: Expected blank line before this statement. padding-line-between-statements [error] 101-101: Trailing spaces not allowed. no-trailing-spaces [error] 101-101: Delete [error] 102-102: A space is required after ','. comma-spacing [error] 102-102: Insert |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||
setIsClient(true); | ||||||||||||||||||||||||||||
|
@@ -114,24 +156,32 @@ export default function BlogIndexPage() { | |||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
<div> | ||||||||||||||||||||||||||||
{Object.keys(posts).length === 0 && ( | ||||||||||||||||||||||||||||
{(Object.keys(posts).length === 0 || | ||||||||||||||||||||||||||||
Object.keys(currentPosts).length === 0) && ( | ||||||||||||||||||||||||||||
<div className='mt-16 flex flex-col items-center justify-center'> | ||||||||||||||||||||||||||||
<Empty /> | ||||||||||||||||||||||||||||
<p className='mx-auto mt-3 max-w-2xl text-xl leading-7 text-gray-500'>No post matches your filter</p> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||
{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) => ( | ||||||||||||||||||||||||||||
{currentPosts.map((post, index) => ( | ||||||||||||||||||||||||||||
<BlogPostItem key={index} post={post} /> | ||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||
</ul> | ||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||
{Object.keys(posts).length > 0 && !isClient && ( | ||||||||||||||||||||||||||||
{Object.keys(currentPosts).length > 0 && !isClient && ( | ||||||||||||||||||||||||||||
<div className='h-screen w-full'> | ||||||||||||||||||||||||||||
<Loader loaderText='Loading Blogs' className='mx-auto my-60' pulsating /> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||
{/* Pagination component */} | ||||||||||||||||||||||||||||
<BlogPagination | ||||||||||||||||||||||||||||
blogsPerPage={blogsPerPage} | ||||||||||||||||||||||||||||
totalBlogs={posts.length} | ||||||||||||||||||||||||||||
paginate={paginate} | ||||||||||||||||||||||||||||
currentPage={currentPage} | ||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||
</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.
Fix formatting in paginate function.
The function logic is correct but contains linting errors with trailing commas and spacing.
Apply these formatting fixes:
📝 Committable suggestion
🧰 Tools
🪛 ESLint
[error] 83-86: Expected blank line after variable declarations.
(newline-after-var)
[error] 85-85: Delete
,
(prettier/prettier)
[error] 85-85: Unexpected trailing comma.
(comma-dangle)
[error] 87-96: Expected blank line before this statement.
(padding-line-between-statements)
[error] 90-90: Delete
,
(prettier/prettier)
[error] 90-90: Unexpected trailing comma.
(comma-dangle)
[error] 94-94: Delete
,
(prettier/prettier)
[error] 94-94: Unexpected trailing comma.
(comma-dangle)
[error] 95-95: Delete
,
(prettier/prettier)
[error] 95-95: Unexpected trailing comma.
(comma-dangle)
🪛 GitHub Actions: PR testing - if Node project
[error] 83-83: Expected blank line after variable declarations. newline-after-var
[error] 85-85: Delete
,
prettier/prettier[error] 85-85: Unexpected trailing comma. comma-dangle
[error] 87-87: Expected blank line before this statement. padding-line-between-statements
[error] 90-90: Delete
,
prettier/prettier[error] 90-90: Unexpected trailing comma. comma-dangle
[error] 94-94: Delete
,
prettier/prettier[error] 94-94: Unexpected trailing comma. comma-dangle
[error] 95-95: Delete
,
prettier/prettier[error] 95-95: Unexpected trailing comma. comma-dangle