Skip to content
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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components/helpers/applyFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
20 changes: 20 additions & 0 deletions components/icons/Next.tsx
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>
);
}
20 changes: 20 additions & 0 deletions components/icons/Previous.tsx
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>
);
}
126 changes: 126 additions & 0 deletions components/navigation/BlogPagination.tsx
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>
);
}
68 changes: 59 additions & 9 deletions pages/blog/index.tsx
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';
Expand All @@ -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.
Expand All @@ -35,7 +36,6 @@ export default function BlogIndexPage() {
: []
);
const [isClient, setIsClient] = useState(false);

const onFilter = (data: IBlogPost[]) => setPosts(data);
const toFilter = [
{
Expand All @@ -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,
},
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix formatting in paginate function.

The function logic is correct but contains linting errors with trailing commas and spacing.

Apply these formatting fixes:

  const paginate = (pageNumber: number) => {
    setCurrentPage(pageNumber);
    const { query } = router;
    const newQuery = {
      ...query,
-     page: pageNumber,
+     page: pageNumber
    };

    router.push(
      {
        pathname: router.pathname,
-       query: newQuery,
+       query: newQuery
      },
      undefined,
      {
-       shallow: true,
+       shallow: true
      }
    );
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const paginate = (pageNumber: number) => {
setCurrentPage(pageNumber);
const { query } = router;
const newQuery = {
...query,
page: pageNumber,
};
router.push(
{
pathname: router.pathname,
query: newQuery,
},
undefined,
{
shallow: true,
},
);
};
const paginate = (pageNumber: number) => {
setCurrentPage(pageNumber);
const { query } = router;
const newQuery = {
...query,
page: pageNumber
};
router.push(
{
pathname: router.pathname,
query: newQuery
},
undefined,
{
shallow: true
}
);
};
🧰 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


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]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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]);
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]);
🧰 Tools
🪛 ESLint

[error] 99-99: Replace => with ·=>·

(prettier/prettier)


[error] 100-100: Expected blank line after variable declarations.

(newline-after-var)


[error] 100-100: Replace 1,·Number.isNaN(parseInt(router.query.page·as·string,·10))·?·1·:·parseInt(router.query.page·as·string,·10) with ⏎······1,⏎······Number.isNaN(parseInt(router.query.page·as·string,·10))·?·1·:·parseInt(router.query.page·as·string,·10)⏎····

(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 => with ·=>· prettier/prettier


[error] 100-100: Expected blank line after variable declarations. newline-after-var


[error] 100-100: Replace 1,·Number.isNaN(parseInt(router.query.page·as·string,·10))·?·1·:·parseInt(router.query.page·as·string,·10) with ⏎······1,⏎······Number.isNaN(parseInt(router.query.page·as·string,·10))·?·1·:·parseInt(router.query.page·as·string,·10)⏎···· 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


useEffect(() => {
setIsClient(true);
Expand Down Expand Up @@ -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>
Expand Down
Loading