Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
af0e6a1
add pagination feature on blog page
pryxnsu Jan 21, 2025
1873828
Add error handling for invalid page numbers
pryxnsu Jan 21, 2025
1300a2a
Merge branch 'master' into feat-pagination
pryxnsu Jan 22, 2025
7d649b3
Merge branch 'master' into feat-pagination
pryxnsu Jan 22, 2025
20e3d3d
add Next and Previos icon fix(filters): exclude non-filterable keys d…
pryxnsu Jan 26, 2025
a06a8d2
Merge branch 'asyncapi:master' into feat-pagination
pryxnsu Jan 26, 2025
458ca3e
Merge branch 'feat-pagination' of https://github.com/priyanshuxkumar/…
pryxnsu Jan 26, 2025
66b9af3
fix: IconPrevious typo and Improve accessibility and clean up style
pryxnsu Jan 26, 2025
454dd35
Fix CSS class concatenation
pryxnsu Jan 26, 2025
b514175
Merge branch 'master' into feat-pagination
pryxnsu Jan 27, 2025
18b8a55
fix pagination: mobile view
pryxnsu Feb 5, 2025
e1d505a
Merge branch 'master' into feat-pagination
pryxnsu Feb 5, 2025
afa9a05
Merge branch 'master' into feat-pagination
pryxnsu Feb 18, 2025
95fbac3
Merge branch 'master' into feat-pagination
pryxnsu Mar 6, 2025
940becd
Merge branch 'master' into feat-pagination
sambhavgupta0705 Apr 6, 2025
2fada41
Merge branch 'asyncapi:master' into feat-pagination
pryxnsu May 24, 2025
1b375eb
Merge branch 'asyncapi:master' into feat-pagination
pryxnsu Aug 26, 2025
e2d0a9e
fix: button text type
pryxnsu May 24, 2025
63fb2f6
feat: refactor pagination logic and add filter application
pryxnsu Aug 26, 2025
f79b659
fix: line length in pagination Button
pryxnsu Aug 26, 2025
07fbe20
Merge branch 'master' into feat-pagination
sambhavgupta0705 Sep 7, 2025
90b093b
refactor: update getPageNumbefunction logic and handle edge cases
pryxnsu Sep 7, 2025
1b470f2
refactor: allow custom button attributes on PaginationItem
pryxnsu Sep 8, 2025
e655441
Merge branch 'master' into feat-pagination
anshgoyalevil Dec 21, 2025
f9bd78e
Merge branch 'master' into feat-pagination
pryxnsu Dec 23, 2025
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
8 changes: 4 additions & 4 deletions components/buttons/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ export default function Button({
data-testid='Button-main'
>
{icon && iconPosition === ButtonIconPosition.LEFT && (
<span className='mr-2 inline-block' data-testid='Button-icon-left'>
<span className='inline-block' data-testid='Button-icon-left'>
{icon}
</span>
)}
<span className='inline-block'>{text}</span>
{icon && iconPosition === ButtonIconPosition.RIGHT && (
<span className='ml-2 inline-block' data-testid='Button-icon-right'>
<span className='inline-block' data-testid='Button-icon-right'>
{icon}
</span>
)}
Expand All @@ -98,9 +98,9 @@ export default function Button({
className={buttonSize === ButtonSize.SMALL ? smallButtonClasses : classNames}
data-testid='Button-link'
>
{icon && iconPosition === ButtonIconPosition.LEFT && <span className='mr-2 inline-block'>{icon}</span>}
{icon && iconPosition === ButtonIconPosition.LEFT && <span className='inline-block'>{icon}</span>}
<span className='inline-block'>{text}</span>
{icon && iconPosition === ButtonIconPosition.RIGHT && <span className='ml-2 inline-block'>{icon}</span>}
{icon && iconPosition === ButtonIconPosition.RIGHT && <span className='inline-block'>{icon}</span>}
</Link>
);
}
30 changes: 30 additions & 0 deletions components/helpers/usePagination.ts
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) {
Copy link
Member

@akshatnema akshatnema Jan 25, 2025

Choose a reason for hiding this comment

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

What is T here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

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

@priyanshuxkumar Update the name of the type. Type name as T is way too generic.

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
};
}
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() {
Copy link
Member

Choose a reason for hiding this comment

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

Why there is a need of having a separate component for these icons? Can't we use these images in SVG format?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can, but components with SVG images already exist. I did the same

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>
);
}
4 changes: 3 additions & 1 deletion components/navigation/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export default function Filter({ data, onFilter, checks, className }: FilterProp
}, [route]);

useEffect(() => {
onFilterApply(data, onFilter, routeQuery);
const filterableQuery = Object.fromEntries(Object.entries(routeQuery).filter(([key]) => key !== 'page'));

onFilterApply(data, onFilter, filterableQuery);
}, [routeQuery]);

return checks.map((check) => {
Expand Down
118 changes: 118 additions & 0 deletions components/pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 getPageNumber = (): (number | 'start-ellipsis' | 'end-ellipsis')[] => {
if (totalPages <= 6) {
return Array.from({ length: Math.max(0, totalPages) }, (_, i) => i + 1);
}

const pages: (number | 'start-ellipsis' | 'end-ellipsis')[] = [1];

const left = Math.max(2, currentPage - 1);
const right = Math.min(totalPages - 1, currentPage + 1);

if (left > 2) pages.push('start-ellipsis');
for (let i = left; i <= right; i++) pages.push(i);
if (right < totalPages - 1) pages.push('end-ellipsis');

pages.push(totalPages);

return pages;
};

return (
<nav
role='navigation'
aria-label='Pagination'
className='font-inter flex min-w-[326px] items-center justify-center md:gap-8'
>
{/* Previous button */}
<Button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
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
? 'hover:bg-gray-white cursor-not-allowed text-gray-300'
: 'text-[#141717] hover:bg-gray-50'
}`}
text=''
icon={<IconPrevious />}
iconPosition={ButtonIconPosition.LEFT}
aria-label='Go to previous page'
/>

{/* Page numbers */}
<div className='flex gap-2' role='list'>
{getPageNumber().map((page) =>
typeof page === 'number' ? (
<PaginationItem
key={page}
pageNumber={page}
isActive={page === currentPage}
onPageChange={handlePageChange}
aria-label={`Go to page ${page}`}
/>
) : (
<span
key={page}
className='font-inter flex size-10 items-center justify-center text-sm font-semibold text-[#6B6B6B]'
aria-hidden='true'
>
...
</span>
)
)}
</div>

{/* Next button */}
<Button
onClick={() => handlePageChange(currentPage + 1)}
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
? 'hover:bg-gray-white cursor-not-allowed text-gray-300'
: 'text-[#141717] hover:bg-gray-50'
}`}
text=''
icon={<IconNext />}
aria-label='Go to next page'
/>
</nav>
);
}
38 changes: 38 additions & 0 deletions components/pagination/PaginationItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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,
...buttonProps
}: PaginationItemProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
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}
{...buttonProps}
>
{pageNumber}
</button>
Comment on lines +19 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prevent prop overrides: merge onClick, lock aria-current, and add type="button".

As written, spreading buttonProps after your props allows consumers to override onClick (breaking pagination) and aria-current. Also, without type="button", this can submit a surrounding form. Merge handlers, place the spread first, and set type="button".

-export default function PaginationItem({
-  pageNumber,
-  isActive,
-  onPageChange,
-  ...buttonProps
-}: PaginationItemProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
-  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}
-      {...buttonProps}
-    >
-      {pageNumber}
-    </button>
-  );
+export default function PaginationItem(
+  {
+    pageNumber,
+    isActive,
+    onPageChange,
+    onClick,
+    className,
+    ...rest
+  }: PaginationItemProps & React.ButtonHTMLAttributes<HTMLButtonElement>
+) {
+  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
+    onClick?.(e);
+    if (!e.defaultPrevented) onPageChange(pageNumber);
+  };
+  return (
+    <button
+      {...rest}
+      type="button"
+      onClick={handleClick}
+      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'} ${className ?? ''}`}
+      aria-current={isActive ? 'page' : undefined}
+    >
+      {pageNumber}
+    </button>
+  );
📝 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
export default function PaginationItem({
pageNumber,
isActive,
onPageChange,
...buttonProps
}: PaginationItemProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
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}
{...buttonProps}
>
{pageNumber}
</button>
export default function PaginationItem(
{
pageNumber,
isActive,
onPageChange,
onClick,
className,
...rest
}: PaginationItemProps & React.ButtonHTMLAttributes<HTMLButtonElement>
) {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(e);
if (!e.defaultPrevented) onPageChange(pageNumber);
};
return (
<button
{...rest}
type="button"
onClick={handleClick}
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'
} ${className ?? ''}`}
aria-current={isActive ? 'page' : undefined}
>
{pageNumber}
</button>
);
}
🤖 Prompt for AI Agents
components/pagination/PaginationItem.tsx lines 19-36: spread of buttonProps
currently comes after component props allowing consumers to override onClick and
aria-current, and missing type attribute can submit parent forms; fix by
spreading buttonProps first, add type="button", and create a merged onClick
handler that calls the component's onPageChange(pageNumber) then calls
buttonProps.onClick if present (preserve event propagation and prevent default
handling only if callers rely on it), and set aria-current explicitly based on
isActive after the spread so it cannot be overridden.

);
}
43 changes: 41 additions & 2 deletions pages/blog/index.tsx
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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -122,16 +156,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>
Expand Down