-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: add pagination feature in blog page #3595
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
base: master
Are you sure you want to change the base?
Changes from all commits
af0e6a1
1873828
1300a2a
7d649b3
20e3d3d
a06a8d2
458ca3e
66b9af3
454dd35
b514175
18b8a55
e1d505a
afa9a05
95fbac3
940becd
2fada41
1b375eb
e2d0a9e
63fb2f6
f79b659
07fbe20
90b093b
1b470f2
e655441
f9bd78e
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) { | ||
| 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,20 @@ | ||
| import React from 'react'; | ||
|
|
||
| /* eslint-disable max-len */ | ||
| /** | ||
| * @description Icons for Next button | ||
| */ | ||
| export default function IconNext() { | ||
|
Member
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. Why there is a need of having a separate component for these icons? Can't we use these images in SVG format?
Contributor
Author
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 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> | ||
| ); | ||
| } | ||
| 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,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> | ||
| ); | ||
| } |
| 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
Contributor
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 Prevent prop overrides: merge onClick, lock aria-current, and add type="button". As written, spreading -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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
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.
What is
There?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.
It takes a prop of items(all items) and returns the only item that has to be displayed on the current page
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.
@priyanshuxkumar Update the name of the type. Type name as
Tis way too generic.