Skip to content

Commit

Permalink
fix(RV-394): Add pagination (#477)
Browse files Browse the repository at this point in the history
* fix(RV-394): Add pagination

* Update TemplatesIndex.tsx
  • Loading branch information
knguyenrise8 authored Dec 10, 2024
1 parent bb3edb8 commit a932583
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 2 deletions.
16 changes: 16 additions & 0 deletions frontend/src/components/TemplatesIndex/TemplatesIndex.scss
Original file line number Diff line number Diff line change
@@ -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;
}
51 changes: 49 additions & 2 deletions frontend/src/components/TemplatesIndex/TemplatesIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TemplateIndexProps> = () => {
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({
Expand Down Expand Up @@ -137,7 +150,7 @@ export const TemplatesIndex: FC<TemplateIndexProps> = () => {
</>
);
}

return (
<>
<div className="bg-white padding-2 border-gray-5 border-1px">
Expand Down Expand Up @@ -170,10 +183,44 @@ export const TemplatesIndex: FC<TemplateIndexProps> = () => {
<h2>Saved Templates</h2>
<SortableTable
columns={templateColumns}
data={templates}
data={currentItems}
formatters={templateColumnFormatters}
columnNames={templateColumnNames}
/>
<div className="display-flex flex-row width-full pagination-container">
<p className="pagination-text">
Showing {Math.min(currentPage * 10, templates.length)} of {templates.length} templates
</p>
<div className="flex items-center justify-center space-x-2 pagination-button-group">
<Button
onClick={previousPage}
disabled={!hasPreviousPage}
type="button"
>
Previous
</Button>

{getPageNumbers().map(pageNum => (
<Button
key={pageNum}
onClick={() => goToPage(pageNum)}
type="button"
outline={pageNum !== currentPage}
>
{pageNum}
</Button>
))}

<Button
onClick={nextPage}
disabled={!hasNextPage}
type="button"
>
Next
</Button>
</div>
</div>

</div>
</div>
</>
Expand Down
87 changes: 87 additions & 0 deletions frontend/src/hooks/use-pagination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useState, useMemo } from "react";


const usePagination = <T>(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;
98 changes: 98 additions & 0 deletions frontend/src/hooks/use-pagination/use-pagination.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit a932583

Please sign in to comment.