Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions frontend/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useId } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';

interface PaginationProps {
Expand All @@ -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({
Expand All @@ -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);
Expand Down Expand Up @@ -71,15 +76,14 @@ export function Pagination({
</p>

<div className="flex items-center gap-2">
<label htmlFor="items-per-page" className="text-sm text-gray-600 dark:text-gray-400">
<label htmlFor={itemsPerPageId} className="text-sm text-gray-600 dark:text-gray-400">
Items per page:
</label>
<select
id="items-per-page"
id={itemsPerPageId}
value={itemsPerPage}
onChange={(e) => {
onItemsPerPageChange(Number(e.target.value));
onPageChange(1); // Reset to first page when changing items per page
}}
className="bg-white dark:bg-[#262626] text-gray-900 dark:text-white px-3 py-1.5 rounded border border-gray-200 dark:border-[#404040] text-sm focus:outline-none focus:ring-2 focus:ring-[#556830] dark:focus:ring-[#8fb55e]"
>
Expand Down
136 changes: 0 additions & 136 deletions frontend/src/components/shared/Pagination.tsx

This file was deleted.

1 change: 0 additions & 1 deletion frontend/src/components/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ export { ConfirmDialog } from './ConfirmDialog';
export { FormModal } from './FormModal';
export { DataTable } from './DataTable';
export type { Column } from './DataTable';
export { Pagination } from './Pagination';
20 changes: 11 additions & 9 deletions frontend/src/components/student/ActivityHistoryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useUrlPagination } from '@/hooks/useUrlPagination';
import useSWR from 'swr';
import { Calendar } from 'lucide-react';
import {
Expand Down Expand Up @@ -53,8 +54,7 @@ export default function ActivityHistoryCard({
programId,
residentId
}: ActivityHistoryCardProps) {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const { page, perPage, setPage, setPerPage } = useUrlPagination(1, 20, 'activity');
const [filterQuery, setFilterQuery] = useState('');

const endpoint = residentId
Expand All @@ -67,9 +67,14 @@ export default function ActivityHistoryCard({
ServerResponseMany<ActivityHistoryResponse>
>(endpoint);

const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
setPage(1);
}, [filterQuery]);
}, [filterQuery, setPage]);

const heading = programId ? 'Program History' : 'Account Overview';
const entries = data?.data ?? [];
Expand Down Expand Up @@ -146,16 +151,13 @@ export default function ActivityHistoryCard({
})}
</div>
)}
{total >= perPage && (
{total > 0 && (
<Pagination
currentPage={page}
totalItems={total}
itemsPerPage={perPage}
onPageChange={setPage}
onItemsPerPageChange={(val) => {
setPerPage(val);
setPage(1);
}}
onItemsPerPageChange={setPerPage}
itemLabel="entries"
/>
)}
Expand Down
47 changes: 27 additions & 20 deletions frontend/src/hooks/useUrlPagination.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
8 changes: 4 additions & 4 deletions frontend/src/pages/ClassesPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -93,8 +94,7 @@ export default function ClassesPage() {
const [programSearch, setProgramSearch] = useState('');
const [attendanceClass, setAttendanceClass] = useState<Class | null>(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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const crossFacility = user ? canSwitchFacility(user) : false;

Expand Down Expand Up @@ -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;
Expand Down
32 changes: 19 additions & 13 deletions frontend/src/pages/ProgramsPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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) =>
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -989,17 +998,14 @@ export default function ProgramsPage() {
)}

{/* Pagination */}
{filtered.length > itemsPerPage && (
{filtered.length > 0 && (
<div className="mt-6">
<Pagination
currentPage={currentPage}
currentPage={page}
totalItems={filtered.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPage={perPage}
onPageChange={setPage}
onItemsPerPageChange={setPerPage}
itemLabel="items"
/>
</div>
Expand Down
Loading
Loading