From e15ff6cc47d9f8e9e3fa4722632f471bc4dbfaed Mon Sep 17 00:00:00 2001 From: Namiiikaze Date: Sat, 25 Jul 2026 05:47:05 -0700 Subject: [PATCH] Add paginated notification search navigation with numbered pages and active-page highlighting --- .../src/pages/NotificationSearchPage.test.tsx | 47 ++++++++++++++ .../src/pages/NotificationSearchPage.tsx | 62 ++++++++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 dashboard/src/pages/NotificationSearchPage.test.tsx diff --git a/dashboard/src/pages/NotificationSearchPage.test.tsx b/dashboard/src/pages/NotificationSearchPage.test.tsx new file mode 100644 index 0000000..5afe0ea --- /dev/null +++ b/dashboard/src/pages/NotificationSearchPage.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { NotificationSearchPage } from './NotificationSearchPage'; +import * as eventsApi from '../services/eventsApi'; + +jest.mock('../services/eventsApi', () => ({ + searchNotifications: jest.fn(), +})); + +describe('NotificationSearchPage pagination', () => { + beforeEach(() => { + jest.clearAllMocks(); + (eventsApi.searchNotifications as jest.Mock).mockResolvedValue({ + results: Array.from({ length: 20 }, (_, index) => ({ + id: index + 1, + source: 'scheduled' as const, + eventId: `event-${index + 1}`, + txHash: null, + contractAddress: null, + notificationType: 'discord', + targetRecipient: 'recipient@example.com', + status: 'PENDING', + createdAt: new Date().toISOString(), + payload: null, + })), + total: 60, + limit: 20, + offset: 0, + itemCount: 20, + totalPages: 3, + }); + }); + + it('highlights the active page and navigates between pages', async () => { + render(); + + const searchInput = screen.getByLabelText(/free-text search/i); + await userEvent.type(searchInput, 'hello'); + + const pageTwo = await screen.findByRole('button', { name: 'Page 2' }); + expect(pageTwo).toHaveAttribute('aria-current', 'page'); + + await userEvent.click(pageTwo); + expect(screen.getByRole('button', { name: 'Page 2' })).toHaveAttribute('aria-current', 'page'); + expect(screen.getByText(/Page 2 of 3/i)).toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/pages/NotificationSearchPage.tsx b/dashboard/src/pages/NotificationSearchPage.tsx index 28a59ab..1fce5c2 100644 --- a/dashboard/src/pages/NotificationSearchPage.tsx +++ b/dashboard/src/pages/NotificationSearchPage.tsx @@ -100,6 +100,28 @@ export function NotificationSearchPage() { const totalPages = response ? response.totalPages : 0; + const getVisiblePages = () => { + if (totalPages <= 1) return []; + + const pages: number[] = []; + const maxVisible = 5; + let startPage = Math.max(1, page - Math.floor(maxVisible / 2)); + let endPage = startPage + maxVisible - 1; + + if (endPage > totalPages) { + endPage = totalPages; + startPage = Math.max(1, endPage - maxVisible + 1); + } + + for (let pageNumber = startPage; pageNumber <= endPage; pageNumber += 1) { + pages.push(pageNumber); + } + + return pages; + }; + + const visiblePages = getVisiblePages(); + return (
@@ -238,6 +260,15 @@ export function NotificationSearchPage() { {totalPages > 1 && ( )}