Skip to content

Commit 3f10b80

Browse files
committed
feat: add pagintaion to class
1 parent dec5d98 commit 3f10b80

11 files changed

Lines changed: 931 additions & 160 deletions

File tree

frontend-v2/src/components/BulkCancelClassesModal.tsx

Lines changed: 500 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { ChevronLeft, ChevronRight } from 'lucide-react';
2+
3+
interface PaginationProps {
4+
currentPage: number;
5+
totalItems: number;
6+
itemsPerPage: number;
7+
onPageChange: (page: number) => void;
8+
onItemsPerPageChange: (itemsPerPage: number) => void;
9+
itemLabel?: string;
10+
}
11+
12+
function getPageNumbers(currentPage: number, totalPages: number) {
13+
const pages: (number | string)[] = [];
14+
15+
if (totalPages <= 7) {
16+
for (let i = 1; i <= totalPages; i++) pages.push(i);
17+
return pages;
18+
}
19+
20+
pages.push(1);
21+
if (currentPage > 3) pages.push('...');
22+
23+
const start = Math.max(2, currentPage - 1);
24+
const end = Math.min(totalPages - 1, currentPage + 1);
25+
for (let i = start; i <= end; i++) pages.push(i);
26+
27+
if (currentPage < totalPages - 2) pages.push('...');
28+
pages.push(totalPages);
29+
30+
return pages;
31+
}
32+
33+
export function Pagination({
34+
currentPage,
35+
totalItems,
36+
itemsPerPage,
37+
onPageChange,
38+
onItemsPerPageChange,
39+
itemLabel = 'items'
40+
}: PaginationProps) {
41+
const totalPages = Math.ceil(totalItems / itemsPerPage);
42+
const startItem = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
43+
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
44+
45+
if (totalItems === 0) return null;
46+
47+
return (
48+
<div className="bg-white dark:bg-[#171717] border-t border-gray-200 dark:border-[#262626] px-6 py-4 rounded-b-lg">
49+
<div className="flex items-center justify-between">
50+
<div className="flex items-center gap-6">
51+
<p className="text-sm text-gray-600 dark:text-gray-400">
52+
Showing{' '}
53+
<span className="font-medium text-gray-900 dark:text-white">
54+
{startItem}-{endItem}
55+
</span>{' '}
56+
of{' '}
57+
<span className="font-medium text-gray-900 dark:text-white">
58+
{totalItems}
59+
</span>{' '}
60+
{itemLabel}
61+
</p>
62+
<div className="flex items-center gap-2">
63+
<label
64+
htmlFor="items-per-page"
65+
className="text-sm text-gray-600 dark:text-gray-400"
66+
>
67+
Items per page:
68+
</label>
69+
<select
70+
id="items-per-page"
71+
value={itemsPerPage}
72+
onChange={(e) => {
73+
onItemsPerPageChange(Number(e.target.value));
74+
onPageChange(1);
75+
}}
76+
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]"
77+
>
78+
<option value={10}>10</option>
79+
<option value={20}>20</option>
80+
<option value={40}>40</option>
81+
<option value={80}>80</option>
82+
</select>
83+
</div>
84+
</div>
85+
86+
{totalPages > 1 && (
87+
<div className="flex items-center gap-2">
88+
<button
89+
onClick={() => onPageChange(currentPage - 1)}
90+
disabled={currentPage === 1}
91+
className="p-2 rounded-lg border border-gray-200 dark:border-[#404040] text-gray-600 dark:text-gray-400 hover:bg-[#E2E7EA] dark:hover:bg-[#262626] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
92+
aria-label="Previous page"
93+
>
94+
<ChevronLeft className="size-5" />
95+
</button>
96+
97+
<div className="flex items-center gap-1">
98+
{getPageNumbers(currentPage, totalPages).map(
99+
(page, index) => {
100+
if (page === '...') {
101+
return (
102+
<span
103+
key={`ellipsis-${index}`}
104+
className="px-3 py-2 text-gray-600 dark:text-gray-400"
105+
>
106+
...
107+
</span>
108+
);
109+
}
110+
const num = page as number;
111+
return (
112+
<button
113+
key={num}
114+
onClick={() => onPageChange(num)}
115+
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
116+
num === currentPage
117+
? 'bg-[#556830] text-white'
118+
: 'text-gray-600 dark:text-gray-400 hover:bg-[#E2E7EA] dark:hover:bg-[#262626]'
119+
}`}
120+
>
121+
{num}
122+
</button>
123+
);
124+
}
125+
)}
126+
</div>
127+
128+
<button
129+
onClick={() => onPageChange(currentPage + 1)}
130+
disabled={currentPage === totalPages}
131+
className="p-2 rounded-lg border border-gray-200 dark:border-[#404040] text-gray-600 dark:text-gray-400 hover:bg-[#E2E7EA] dark:hover:bg-[#262626] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
132+
aria-label="Next page"
133+
>
134+
<ChevronRight className="size-5" />
135+
</button>
136+
</div>
137+
)}
138+
</div>
139+
</div>
140+
);
141+
}

frontend-v2/src/components/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { ConfirmDialog } from './ConfirmDialog';
66
export { FormModal } from './FormModal';
77
export { DataTable } from './DataTable';
88
export type { Column } from './DataTable';
9+
export { Pagination } from './Pagination';

frontend-v2/src/layouts/AuthenticatedLayout.tsx

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -63,42 +63,43 @@ export default function AuthenticatedLayout() {
6363
if (!user) return null;
6464

6565
return (
66-
<div className="h-screen bg-background flex overflow-hidden">
67-
<div className="hidden md:flex">
68-
<Sidebar
69-
collapsed={sidebarCollapsed}
70-
onToggleCollapse={() =>
71-
setSidebarCollapsed(!sidebarCollapsed)
72-
}
73-
/>
74-
</div>
75-
76-
<div className="flex-1 flex flex-col min-w-0">
77-
<div className="flex items-center">
78-
<div className="md:hidden px-2">
79-
<MobileNav />
80-
</div>
81-
<div className="flex-1">
82-
<TopNav
83-
facilities={facilities}
84-
onToggleHelpCenter={() =>
85-
setHelpCenterOpen(!helpCenterOpen)
86-
}
87-
/>
88-
</div>
66+
<>
67+
<div className="h-screen bg-background flex overflow-hidden">
68+
<div className="hidden md:flex">
69+
<Sidebar
70+
collapsed={sidebarCollapsed}
71+
onToggleCollapse={() =>
72+
setSidebarCollapsed(!sidebarCollapsed)
73+
}
74+
/>
8975
</div>
9076

91-
<TitleManager />
92-
<UnlockEdTour />
93-
<Toaster />
77+
<div className="flex-1 flex flex-col min-w-0">
78+
<div className="flex items-center shrink-0">
79+
<div className="md:hidden px-2">
80+
<MobileNav />
81+
</div>
82+
<div className="flex-1">
83+
<TopNav
84+
facilities={facilities}
85+
onToggleHelpCenter={() =>
86+
setHelpCenterOpen(!helpCenterOpen)
87+
}
88+
/>
89+
</div>
90+
</div>
91+
<TitleManager />
9492

95-
<div className="flex-1 overflow-y-auto px-6 py-4 bg-[#E2E7EA] dark:bg-[#0a0a0a]">
96-
{breadcrumbItems.length > 0 && (
97-
<Breadcrumbs items={breadcrumbItems} />
98-
)}
99-
<Outlet />
93+
<div className="flex-1 overflow-y-auto px-6 py-4 bg-[#E2E7EA] dark:bg-[#0a0a0a]">
94+
{breadcrumbItems.length > 0 && (
95+
<Breadcrumbs items={breadcrumbItems} />
96+
)}
97+
<Outlet />
98+
</div>
10099
</div>
101100
</div>
102-
</div>
101+
<UnlockEdTour />
102+
<Toaster />
103+
</>
103104
);
104105
}

frontend-v2/src/pages/ClassesPage.tsx

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@ import {
3838
Search,
3939
Plus,
4040
Calendar,
41+
CalendarOff,
4142
Clock,
4243
Filter,
4344
MapPin
4445
} from 'lucide-react';
46+
import { Pagination } from '@/components/shared';
4547
import { TakeAttendanceModal } from './class-detail/TakeAttendanceModal';
48+
import { BulkCancelClassesModal } from '@/components/BulkCancelClassesModal';
4649

47-
const STATUS_OPTIONS: Array<{ label: string; value: string }> = [
50+
const STATUS_OPTIONS: { label: string; value: string }[] = [
4851
{ label: 'All Status', value: 'all' },
4952
{ label: 'Active', value: SelectedClassStatus.Active },
5053
{ label: 'Scheduled', value: SelectedClassStatus.Scheduled },
@@ -89,10 +92,14 @@ export default function ClassesPage() {
8992
const [showCreateModal, setShowCreateModal] = useState(false);
9093
const [programSearch, setProgramSearch] = useState('');
9194
const [attendanceClass, setAttendanceClass] = useState<Class | null>(null);
95+
const [showBulkCancel, setShowBulkCancel] = useState(false);
96+
const [currentPage, setCurrentPage] = useState(1);
97+
const [itemsPerPage, setItemsPerPage] = useState(20);
9298

93-
const { data: classesResp } = useSWR<ServerResponseMany<Class>>(
94-
'/api/program-classes?per_page=100'
95-
);
99+
const { data: classesResp, mutate: mutateClasses } =
100+
useSWR<ServerResponseMany<Class>>(
101+
'/api/program-classes?per_page=100'
102+
);
96103
const { data: programsResp } = useSWR<ServerResponseMany<Program>>(
97104
showCreateModal ? '/api/programs' : null
98105
);
@@ -185,6 +192,15 @@ export default function ClassesPage() {
185192
statusFilter
186193
]);
187194

195+
useEffect(() => {
196+
setCurrentPage(1);
197+
}, [searchQuery, todayOnly, attendanceConcerns, programFilter, statusFilter]);
198+
199+
const paginatedClasses = useMemo(() => {
200+
const start = (currentPage - 1) * itemsPerPage;
201+
return filteredClasses.slice(start, start + itemsPerPage);
202+
}, [filteredClasses, currentPage, itemsPerPage]);
203+
188204
return (
189205
<div className="-mx-6 -mt-4 -mb-4 min-h-[calc(100vh-4rem)] bg-[#E2E7EA]">
190206
<div className="max-w-7xl mx-auto px-6 py-8">
@@ -198,13 +214,23 @@ export default function ClassesPage() {
198214
Manage and monitor all classes at your facility
199215
</p>
200216
</div>
201-
<Button
202-
className="bg-[#F1B51C] text-[#203622] hover:bg-[#d9a419] gap-2"
203-
onClick={() => setShowCreateModal(true)}
204-
>
205-
<Plus className="size-5" />
206-
Create New Class
207-
</Button>
217+
<div className="flex gap-2">
218+
<Button
219+
variant="outline"
220+
className="border-red-300 text-red-700 hover:bg-red-50 hover:border-red-400 gap-2"
221+
onClick={() => setShowBulkCancel(true)}
222+
>
223+
<CalendarOff className="size-4" />
224+
Cancel Classes by Instructor
225+
</Button>
226+
<Button
227+
className="bg-[#F1B51C] text-[#203622] hover:bg-[#d9a419] gap-2"
228+
onClick={() => setShowCreateModal(true)}
229+
>
230+
<Plus className="size-5" />
231+
Create New Class
232+
</Button>
233+
</div>
208234
</div>
209235
</div>
210236

@@ -292,11 +318,6 @@ export default function ClassesPage() {
292318
</div>
293319
</div>
294320

295-
<div className="mb-4 text-sm text-gray-600">
296-
Showing {filteredClasses.length}{' '}
297-
{filteredClasses.length === 1 ? 'class' : 'classes'}
298-
</div>
299-
300321
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
301322
<table className="w-full">
302323
<thead className="bg-[#E2E7EA] border-b border-gray-200">
@@ -322,7 +343,7 @@ export default function ClassesPage() {
322343
</tr>
323344
</thead>
324345
<tbody className="divide-y divide-gray-200">
325-
{filteredClasses.length === 0 ? (
346+
{paginatedClasses.length === 0 ? (
326347
<tr>
327348
<td
328349
colSpan={6}
@@ -332,7 +353,7 @@ export default function ClassesPage() {
332353
</td>
333354
</tr>
334355
) : (
335-
filteredClasses.map((cls) => (
356+
paginatedClasses.map((cls) => (
336357
<ClassRow
337358
key={cls.id}
338359
cls={cls}
@@ -349,6 +370,14 @@ export default function ClassesPage() {
349370
)}
350371
</tbody>
351372
</table>
373+
<Pagination
374+
totalItems={filteredClasses.length}
375+
itemsPerPage={itemsPerPage}
376+
currentPage={currentPage}
377+
onPageChange={setCurrentPage}
378+
onItemsPerPageChange={setItemsPerPage}
379+
itemLabel="classes"
380+
/>
352381
</div>
353382
</div>
354383

@@ -413,6 +442,12 @@ export default function ClassesPage() {
413442
className={attendanceClass.name}
414443
/>
415444
)}
445+
446+
<BulkCancelClassesModal
447+
open={showBulkCancel}
448+
onClose={() => setShowBulkCancel(false)}
449+
mutate={mutateClasses}
450+
/>
416451
</div>
417452
);
418453
}

frontend-v2/src/pages/class-detail/RosterTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ export function RosterTab({ classId, classStatus }: RosterTabProps) {
387387
<Button
388388
size="sm"
389389
className="bg-[#556830] hover:bg-[#203622]"
390-
onClick={handleBulkGraduate}
390+
onClick={() => { void handleBulkGraduate(); }}
391391
>
392392
<CheckCircle className="size-4 mr-2" />
393393
Graduate Selected

0 commit comments

Comments
 (0)