diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx
index 9a6e12d13..ef2aa9d6f 100644
--- a/frontend/src/components/Pagination.tsx
+++ b/frontend/src/components/Pagination.tsx
@@ -1,3 +1,4 @@
+import { useId } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
interface PaginationProps {
@@ -6,7 +7,8 @@ interface PaginationProps {
itemsPerPage: number;
onPageChange: (page: number) => void;
onItemsPerPageChange: (itemsPerPage: number) => void;
- itemLabel?: string; // e.g., "programs", "classes", "residents"
+ itemLabel?: string;
+ instanceId?: string;
}
export function Pagination({
@@ -15,8 +17,11 @@ export function Pagination({
itemsPerPage,
onPageChange,
onItemsPerPageChange,
- itemLabel = 'items'
+ itemLabel = 'items',
+ instanceId
}: PaginationProps) {
+ const generatedId = useId();
+ const itemsPerPageId = `${instanceId ?? `pagination-${generatedId}`}-items-per-page`;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startItem = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
@@ -71,15 +76,14 @@ export function Pagination({
-
)}
- {total >= perPage && (
+ {total > 0 && (
{
- setPerPage(val);
- setPage(1);
- }}
+ onItemsPerPageChange={setPerPage}
itemLabel="entries"
/>
)}
diff --git a/frontend/src/hooks/useUrlPagination.ts b/frontend/src/hooks/useUrlPagination.ts
index 984db6c61..ef2dad34a 100644
--- a/frontend/src/hooks/useUrlPagination.ts
+++ b/frontend/src/hooks/useUrlPagination.ts
@@ -1,29 +1,36 @@
+import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
-export function useUrlPagination(defaultPage = 1, defaultPerPage = 20) {
+export function useUrlPagination(defaultPage = 1, defaultPerPage = 20, prefix = '') {
const [searchParams, setSearchParams] = useSearchParams();
- const page = parseInt(
- searchParams.get('page') ?? defaultPage.toString(),
- 10
- );
- const perPage = parseInt(
- searchParams.get('per_page') ?? defaultPerPage.toString(),
- 10
- );
+ const pageKey = prefix ? `${prefix}_page` : 'page';
+ const perPageKey = prefix ? `${prefix}_per_page` : 'per_page';
+
+ const parsedPage = parseInt(searchParams.get(pageKey) ?? '', 10);
+ const page = Number.isFinite(parsedPage) && parsedPage >= 1 ? parsedPage : defaultPage;
- const setPage = (newPage: number, options?: { replace?: boolean }) => {
- const params = new URLSearchParams(searchParams);
- params.set('page', newPage.toString());
- setSearchParams(params, { replace: options?.replace ?? false });
- };
+ const parsedPerPage = parseInt(searchParams.get(perPageKey) ?? '', 10);
+ const perPage = Number.isFinite(parsedPerPage) && parsedPerPage >= 1 ? parsedPerPage : defaultPerPage;
- const setPerPage = (newPerPage: number) => {
- const params = new URLSearchParams(searchParams);
- params.set('per_page', newPerPage.toString());
- params.set('page', '1');
- setSearchParams(params, { replace: false });
- };
+ const setPage = useCallback(
+ (newPage: number, options?: { replace?: boolean }) => {
+ const params = new URLSearchParams(searchParams);
+ params.set(pageKey, newPage.toString());
+ setSearchParams(params, { replace: options?.replace ?? false });
+ },
+ [searchParams, setSearchParams, pageKey]
+ );
+
+ const setPerPage = useCallback(
+ (newPerPage: number) => {
+ const params = new URLSearchParams(searchParams);
+ params.set(perPageKey, newPerPage.toString());
+ params.set(pageKey, '1');
+ setSearchParams(params, { replace: false });
+ },
+ [searchParams, setSearchParams, pageKey, perPageKey]
+ );
return { page, perPage, setPage, setPerPage };
}
diff --git a/frontend/src/pages/ClassesPage.tsx b/frontend/src/pages/ClassesPage.tsx
index 59b252b0e..c868aec67 100644
--- a/frontend/src/pages/ClassesPage.tsx
+++ b/frontend/src/pages/ClassesPage.tsx
@@ -1,4 +1,5 @@
import { useState, useMemo, useEffect } from 'react';
+import { useUrlPagination } from '@/hooks/useUrlPagination';
import { useNavigate, Link } from 'react-router-dom';
import useSWR from 'swr';
import { useAuth, canSwitchFacility } from '@/auth/useAuth';
@@ -46,7 +47,7 @@ import {
MapPin,
Users
} from 'lucide-react';
-import { Pagination } from '@/components/shared';
+import { Pagination } from '@/components/Pagination';
import { TakeAttendanceModal } from './class-detail/TakeAttendanceModal';
import { BulkCancelClassesModal } from '@/components/BulkCancelClassesModal';
@@ -93,8 +94,7 @@ export default function ClassesPage() {
const [programSearch, setProgramSearch] = useState('');
const [attendanceClass, setAttendanceClass] = useState(null);
const [showBulkCancel, setShowBulkCancel] = useState(false);
- const [currentPage, setCurrentPage] = useState(1);
- const [itemsPerPage, setItemsPerPage] = useState(20);
+ const { page: currentPage, perPage: itemsPerPage, setPage: setCurrentPage, setPerPage: setItemsPerPage } = useUrlPagination();
const crossFacility = user ? canSwitchFacility(user) : false;
@@ -216,7 +216,7 @@ export default function ClassesPage() {
useEffect(() => {
setCurrentPage(1);
- }, [searchQuery, todayOnly, attendanceConcerns, facilityFilter, programFilter, statusFilter]);
+ }, [searchQuery, todayOnly, attendanceConcerns, facilityFilter, programFilter, statusFilter, setCurrentPage]);
const paginatedClasses = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
diff --git a/frontend/src/pages/ProgramsPage.tsx b/frontend/src/pages/ProgramsPage.tsx
index 451981738..5b75ef8b7 100644
--- a/frontend/src/pages/ProgramsPage.tsx
+++ b/frontend/src/pages/ProgramsPage.tsx
@@ -1,4 +1,5 @@
-import { useState, useMemo } from 'react';
+import { useState, useMemo, useEffect, useRef } from 'react';
+import { useUrlPagination } from '@/hooks/useUrlPagination';
import { useNavigate } from 'react-router-dom';
import useSWR from 'swr';
import { useAuth, canSwitchFacility } from '@/auth/useAuth';
@@ -175,8 +176,16 @@ export default function ProgramsPage() {
}
};
- const [currentPage, setCurrentPage] = useState(1);
- const [itemsPerPage, setItemsPerPage] = useState(10);
+ const { page, perPage, setPage, setPerPage } = useUrlPagination(1, 10);
+
+ const isFirstRender = useRef(true);
+ useEffect(() => {
+ if (isFirstRender.current) {
+ isFirstRender.current = false;
+ return;
+ }
+ setPage(1);
+ }, [search, sort, selectedTypes, selectedStatuses, setPage]);
const toggleTypeFilter = (type: ProgramType) => {
setSelectedTypes((prev) =>
@@ -286,8 +295,8 @@ export default function ProgramsPage() {
}, [resp?.data, search, sort, selectedTypes, selectedStatuses]);
const paginatedPrograms = filtered.slice(
- (currentPage - 1) * itemsPerPage,
- currentPage * itemsPerPage
+ (page - 1) * perPage,
+ page * perPage
);
const stats = useMemo(() => {
@@ -989,17 +998,14 @@ export default function ProgramsPage() {
)}
{/* Pagination */}
- {filtered.length > itemsPerPage && (
+ {filtered.length > 0 && (
{
- setItemsPerPage(val);
- setCurrentPage(1);
- }}
+ itemsPerPage={perPage}
+ onPageChange={setPage}
+ onItemsPerPageChange={setPerPage}
itemLabel="items"
/>
diff --git a/frontend/src/pages/admin/AdminManagement.tsx b/frontend/src/pages/admin/AdminManagement.tsx
index 56dfd7380..289f4c3a4 100644
--- a/frontend/src/pages/admin/AdminManagement.tsx
+++ b/frontend/src/pages/admin/AdminManagement.tsx
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
+import { useUrlPagination } from '@/hooks/useUrlPagination';
import useSWR from 'swr';
import {
Search,
@@ -162,8 +163,7 @@ export default function AdminManagement() {
const [sortDirection, setSortDirection] = useState('asc');
const [facilityFilter, setFacilityFilter] = useState('all');
- const [currentPage, setCurrentPage] = useState(1);
- const [itemsPerPage, setItemsPerPage] = useState(20);
+ const { page, perPage, setPage, setPerPage } = useUrlPagination();
const [selectedAdmins, setSelectedAdmins] = useState