-
Notifications
You must be signed in to change notification settings - Fork 0
Add pagination to project and video listing pages #118
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: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| 'use client'; | ||
| import { FC, useState } from 'react'; | ||
| import { FC, useMemo, useState } from 'react'; | ||
| import Link from 'next/link'; | ||
| import { | ||
| Body1Strong, | ||
|
|
@@ -32,16 +32,32 @@ import { useQueryClient } from '@tanstack/react-query'; | |
| import FormAddProject from '../../../components/FormAddProject/FormAddProject'; | ||
| import { formatDate } from '@/src/helpers'; | ||
| import { MoreVertical20Regular } from '@fluentui/react-icons'; | ||
| import { Pagination } from '../../../components/Pagination'; | ||
|
|
||
| const ITEMS_PER_PAGE = 10; | ||
|
|
||
| const Projects: FC = () => { | ||
| const { data: projects, isFetching, isLoading } = useQueryGetProjects(); | ||
| const client = useQueryClient(); | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const [currentPage, setCurrentPage] = useState(1); | ||
|
||
| const [currentProject, setCurrentProject] = | ||
| useState<Partial<IProject> | null>(null); | ||
| const addProjectMutation = useMutationCreateProject(); | ||
| const updateProjectMutation = useMutationUpdateProject(); | ||
|
|
||
| const { paginatedProjects, totalPages } = useMemo(() => { | ||
| if (!projects || !projects.length) { | ||
| return { paginatedProjects: [], totalPages: 0 }; | ||
| } | ||
| const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; | ||
| const endIndex = startIndex + ITEMS_PER_PAGE; | ||
| return { | ||
| paginatedProjects: projects.slice(startIndex, endIndex), | ||
| totalPages: Math.ceil(projects.length / ITEMS_PER_PAGE), | ||
| }; | ||
| }, [projects, currentPage]); | ||
|
|
||
| const columns: TableColumnDefinition<IProject>[] = [ | ||
| createTableColumn<IProject>({ | ||
| columnId: 'name', | ||
|
|
@@ -158,25 +174,38 @@ const Projects: FC = () => { | |
| </Button> | ||
| </div> | ||
| </div> | ||
| {projects && projects.length && ( | ||
| <DataGrid className="w-100 flex" items={projects} columns={columns}> | ||
| <DataGridHeader> | ||
| <DataGridRow> | ||
| {({ renderHeaderCell }) => ( | ||
| <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell> | ||
| )} | ||
| </DataGridRow> | ||
| </DataGridHeader> | ||
| <DataGridBody<IVideo>> | ||
| {({ item, rowId }) => ( | ||
| <DataGridRow<IVideo> key={rowId}> | ||
| {({ renderCell }) => ( | ||
| <DataGridCell>{renderCell(item)}</DataGridCell> | ||
| {paginatedProjects && paginatedProjects.length && ( | ||
| <> | ||
| <DataGrid | ||
| className="w-100 flex" | ||
| items={paginatedProjects} | ||
| columns={columns} | ||
| > | ||
| <DataGridHeader> | ||
| <DataGridRow> | ||
| {({ renderHeaderCell }) => ( | ||
| <DataGridHeaderCell> | ||
| {renderHeaderCell()} | ||
| </DataGridHeaderCell> | ||
| )} | ||
| </DataGridRow> | ||
| )} | ||
| </DataGridBody> | ||
| </DataGrid> | ||
| </DataGridHeader> | ||
| <DataGridBody<IVideo>> | ||
| {({ item, rowId }) => ( | ||
| <DataGridRow<IVideo> key={rowId}> | ||
| {({ renderCell }) => ( | ||
| <DataGridCell>{renderCell(item)}</DataGridCell> | ||
| )} | ||
| </DataGridRow> | ||
| )} | ||
| </DataGridBody> | ||
| </DataGrid> | ||
| <Pagination | ||
| currentPage={currentPage} | ||
| totalPages={totalPages} | ||
| onPageChange={setCurrentPage} | ||
| /> | ||
| </> | ||
| )} | ||
| </div> | ||
| {isOpen && ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from '@fluentui/react-components'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||
| ChevronLeft20Regular, | ||||||||||||||||||||||||||||||||||||||||||||||
| ChevronRight20Regular, | ||||||||||||||||||||||||||||||||||||||||||||||
| } from '@fluentui/react-icons'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export interface PaginationProps { | ||||||||||||||||||||||||||||||||||||||||||||||
| currentPage: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| totalPages: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| onPageChange: (page: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export function Pagination({ | ||||||||||||||||||||||||||||||||||||||||||||||
| currentPage, | ||||||||||||||||||||||||||||||||||||||||||||||
| totalPages, | ||||||||||||||||||||||||||||||||||||||||||||||
| onPageChange, | ||||||||||||||||||||||||||||||||||||||||||||||
| }: PaginationProps) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const getPageNumbers = () => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const pages: (number | string)[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||
| const maxPagesToShow = 5; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (totalPages <= maxPagesToShow) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Show all pages if total is less than max | ||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 1; i <= totalPages; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||
| pages.push(i); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Always show first page | ||||||||||||||||||||||||||||||||||||||||||||||
| pages.push(1); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (currentPage > 3) { | ||||||||||||||||||||||||||||||||||||||||||||||
| pages.push('...'); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Show pages around current page | ||||||||||||||||||||||||||||||||||||||||||||||
| const startPage = Math.max(2, currentPage - 1); | ||||||||||||||||||||||||||||||||||||||||||||||
| const endPage = Math.min(totalPages - 1, currentPage + 1); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = startPage; i <= endPage; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||
| pages.push(i); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (currentPage < totalPages - 2) { | ||||||||||||||||||||||||||||||||||||||||||||||
| pages.push('...'); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Always show last page | ||||||||||||||||||||||||||||||||||||||||||||||
| pages.push(totalPages); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return pages; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (totalPages <= 1) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-center gap-2 py-4"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||
| appearance="subtle" | ||||||||||||||||||||||||||||||||||||||||||||||
| icon={<ChevronLeft20Regular />} | ||||||||||||||||||||||||||||||||||||||||||||||
| disabled={currentPage === 1} | ||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => onPageChange(currentPage - 1)} | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Previous page" | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| {getPageNumbers().map((page, index) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (page === '...') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <span key={`ellipsis-${index}`} className="px-2 text-gray-500"> | ||||||||||||||||||||||||||||||||||||||||||||||
| ... | ||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
+75
|
||||||||||||||||||||||||||||||||||||||||||||||
| {getPageNumbers().map((page, index) => { | |
| if (page === '...') { | |
| return ( | |
| <span key={`ellipsis-${index}`} className="px-2 text-gray-500"> | |
| ... | |
| </span> | |
| ); | |
| } | |
| {getPageNumbers().map((page, index, arr) => { | |
| if (page === '...') { | |
| // Determine if this is the first or second ellipsis | |
| const isFirstEllipsis = arr.indexOf('...') === index; | |
| const startPage = Math.max(2, currentPage - 1); | |
| const endPage = Math.min(totalPages - 1, currentPage + 1); | |
| const ellipsisKey = isFirstEllipsis | |
| ? `ellipsis-before-${startPage}` | |
| : `ellipsis-after-${endPage}`; | |
| return ( | |
| <span key={ellipsisKey} className="px-2 text-gray-500"> | |
| ... | |
| </span> | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './Pagination'; |
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.
The pagination state should be reset to page 1 when the
videosdata changes. Without this, if a user is on page 3 and the videos list is updated (e.g., via a refresh or new data), they might see an empty page if the new data has fewer pages.Consider adding a
useEffectto reset the page: