From a932583789d364bb5dcab5f2e8c2357b4f68b6fb Mon Sep 17 00:00:00 2001 From: knguyenrise8 <159168836+knguyenrise8@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:58:40 -0600 Subject: [PATCH] fix(RV-394): Add pagination (#477) * fix(RV-394): Add pagination * Update TemplatesIndex.tsx --- .../TemplatesIndex/TemplatesIndex.scss | 16 +++ .../TemplatesIndex/TemplatesIndex.tsx | 51 +++++++++- frontend/src/hooks/use-pagination/index.ts | 87 ++++++++++++++++ .../use-pagination/use-pagination.test.ts | 98 +++++++++++++++++++ 4 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/TemplatesIndex/TemplatesIndex.scss create mode 100644 frontend/src/hooks/use-pagination/index.ts create mode 100644 frontend/src/hooks/use-pagination/use-pagination.test.ts diff --git a/frontend/src/components/TemplatesIndex/TemplatesIndex.scss b/frontend/src/components/TemplatesIndex/TemplatesIndex.scss new file mode 100644 index 00000000..caca7b7a --- /dev/null +++ b/frontend/src/components/TemplatesIndex/TemplatesIndex.scss @@ -0,0 +1,16 @@ +.pagination-text { + color: #71767A; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-left: 40px; +} + +.pagination-container { + justify-content: space-between; +} + +.pagination-button-group { + margin-right: 40px; +} \ No newline at end of file diff --git a/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx b/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx index 558d1cd1..21155a96 100644 --- a/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx +++ b/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx @@ -5,11 +5,24 @@ import { useNavigate } from "react-router-dom"; import extractImage from "../../assets/extract_image.svg"; import { useQuery } from "@tanstack/react-query"; import { TemplateAPI } from "../../types/templates.ts"; +import usePagination from "../../hooks/use-pagination/index.ts"; + +import './TemplatesIndex.scss' type TemplateIndexProps = unknown; export const TemplatesIndex: FC = () => { const [templates, setTemplates] = useState([]); + const { + currentItems, + currentPage, + nextPage, + previousPage, + goToPage, + getPageNumbers, + hasNextPage, + hasPreviousPage + } = usePagination(templates, 10, 1); const navigate = useNavigate(); // TODO: Pagination and sorting will be added later const templateQuery = useQuery({ @@ -137,7 +150,7 @@ export const TemplatesIndex: FC = () => { ); } - + return ( <>
@@ -170,10 +183,44 @@ export const TemplatesIndex: FC = () => {

Saved Templates

+
+

+ Showing {Math.min(currentPage * 10, templates.length)} of {templates.length} templates +

+
+ + + {getPageNumbers().map(pageNum => ( + + ))} + + +
+
+
diff --git a/frontend/src/hooks/use-pagination/index.ts b/frontend/src/hooks/use-pagination/index.ts new file mode 100644 index 00000000..d118ba89 --- /dev/null +++ b/frontend/src/hooks/use-pagination/index.ts @@ -0,0 +1,87 @@ +import { useState, useMemo } from "react"; + + +const usePagination = (items: T[] = [], itemsPerPage = 10, initialPage = 1) => { + const [currentPage, setCurrentPage] = useState(initialPage); + + // Calculate total number of pages + const totalPages = useMemo(() => + Math.ceil(items.length / itemsPerPage), + [items.length, itemsPerPage] + ); + + // Ensure current page stays within bounds + useMemo(() => { + if (currentPage > totalPages) { + setCurrentPage(totalPages || 1); + } + }, [currentPage, totalPages]); + + // Get current page items + const currentItems = useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return items.slice(startIndex, endIndex); + }, [items, currentPage, itemsPerPage]); + + // Navigation functions + const goToPage = (pageNumber: number) => { + const page = Math.max(1, Math.min(pageNumber, totalPages)); + setCurrentPage(page); + }; + + const nextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(prev => prev + 1); + } + }; + + const previousPage = () => { + if (currentPage > 1) { + setCurrentPage(prev => prev - 1); + } + }; + + const firstPage = () => { + setCurrentPage(1); + }; + + const lastPage = () => { + setCurrentPage(totalPages); + }; + + // Generate page numbers for pagination display + const getPageNumbers = (maxVisible = 5) => { + const pages = []; + let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); + const endPage = Math.min(totalPages, startPage + maxVisible - 1); + + // Adjust start page if end page is maxed out + if (endPage - startPage + 1 < maxVisible) { + startPage = Math.max(1, endPage - maxVisible + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; + }; + + return { + currentPage, + currentItems, + totalPages, + itemsPerPage, + goToPage, + nextPage, + previousPage, + firstPage, + lastPage, + getPageNumbers, + hasNextPage: currentPage < totalPages, + hasPreviousPage: currentPage > 1 + }; + }; + +export default usePagination; \ No newline at end of file diff --git a/frontend/src/hooks/use-pagination/use-pagination.test.ts b/frontend/src/hooks/use-pagination/use-pagination.test.ts new file mode 100644 index 00000000..d2073894 --- /dev/null +++ b/frontend/src/hooks/use-pagination/use-pagination.test.ts @@ -0,0 +1,98 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import usePagination from './index'; + +describe('usePagination hook', () => { + const items: number[] = Array.from({ length: 50 }, (_, i) => i + 1); // Sample items [1, 2, ..., 50] + + it('should initialize with the correct state', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + expect(result.current.currentPage).toBe(1); + expect(result.current.totalPages).toBe(5); + expect(result.current.currentItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('should navigate to the next page', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + act(() => { + result.current.nextPage(); + }); + + expect(result.current.currentPage).toBe(2); + expect(result.current.currentItems).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + }); + + it('should navigate to the previous page', () => { + const { result } = renderHook(() => usePagination(items, 10, 2)); + + act(() => { + result.current.previousPage(); + }); + + expect(result.current.currentPage).toBe(1); + expect(result.current.currentItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('should navigate to the first page', () => { + const { result } = renderHook(() => usePagination(items, 10, 3)); + + act(() => { + result.current.firstPage(); + }); + + expect(result.current.currentPage).toBe(1); + expect(result.current.currentItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('should navigate to the last page', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + act(() => { + result.current.lastPage(); + }); + + expect(result.current.currentPage).toBe(5); + expect(result.current.currentItems).toEqual([41, 42, 43, 44, 45, 46, 47, 48, 49, 50]); + }); + + it('should navigate to a specific page', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + act(() => { + result.current.goToPage(3); + }); + + expect(result.current.currentPage).toBe(3); + expect(result.current.currentItems).toEqual([21, 22, 23, 24, 25, 26, 27, 28, 29, 30]); + }); + + it('should generate correct page numbers', () => { + const { result } = renderHook(() => usePagination(items, 10, 3)); + + const pageNumbers = result.current.getPageNumbers(5); + expect(pageNumbers).toEqual([1, 2, 3, 4, 5]); + }); + + it('should handle edge cases for page numbers', () => { + const { result } = renderHook(() => usePagination(items, 10, 5)); + + const pageNumbers = result.current.getPageNumbers(5); + expect(pageNumbers).toEqual([1, 2, 3, 4, 5]); + }); + + it('should handle hasNextPage and hasPreviousPage correctly', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + expect(result.current.hasNextPage).toBe(true); + expect(result.current.hasPreviousPage).toBe(false); + + act(() => { + result.current.goToPage(5); + }); + + expect(result.current.hasNextPage).toBe(false); + expect(result.current.hasPreviousPage).toBe(true); + }); +});