@hed-hog/lms 0.0.279 → 0.0.286

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import { EmptyState, Page, PageHeader } from '@/components/entity-list';
4
4
  import { Badge } from '@/components/ui/badge';
5
5
  import { Button } from '@/components/ui/button';
6
6
  import { Card, CardContent } from '@/components/ui/card';
@@ -61,7 +61,7 @@ import {
61
61
  } from '@dnd-kit/sortable';
62
62
  import { CSS } from '@dnd-kit/utilities';
63
63
  import { zodResolver } from '@hookform/resolvers/zod';
64
- import { AnimatePresence, motion } from 'framer-motion';
64
+ import { AnimatePresence, motion, type Variants } from 'framer-motion';
65
65
  import {
66
66
  AlertTriangle,
67
67
  Archive,
@@ -579,7 +579,7 @@ const initialAulas: Aula[] = [
579
579
  // ── Animations ──────────────────────────────────────────────────────────────
580
580
 
581
581
  const stagger = { hidden: {}, show: { transition: { staggerChildren: 0.04 } } };
582
- const fadeUp = {
582
+ const fadeUp: Variants = {
583
583
  hidden: { opacity: 0, y: 12 },
584
584
  show: { opacity: 1, y: 0, transition: { duration: 0.3, ease: 'easeOut' } },
585
585
  };
@@ -1310,23 +1310,16 @@ export default function EstruturaPage({
1310
1310
 
1311
1311
  {/* Empty state */}
1312
1312
  {sessoes.length === 0 && (
1313
- <motion.div
1314
- variants={fadeUp}
1315
- className="mt-8 flex flex-col items-center gap-4 py-16 text-center"
1316
- >
1317
- <div className="flex size-16 items-center justify-center rounded-full bg-muted">
1318
- <Layers className="size-8 text-muted-foreground" />
1319
- </div>
1320
- <div>
1321
- <h3 className="text-lg font-semibold">{t('empty.title')}</h3>
1322
- <p className="text-sm text-muted-foreground">
1323
- {t('empty.description')}
1324
- </p>
1325
- </div>
1326
- <Button className="gap-2" onClick={openCreateSessao}>
1327
- <Plus className="size-4" />
1328
- {t('empty.action')}
1329
- </Button>
1313
+ <motion.div variants={fadeUp} className="mt-8">
1314
+ <EmptyState
1315
+ icon={<Layers className="h-12 w-12" />}
1316
+ title={t('empty.title')}
1317
+ description={t('empty.description')}
1318
+ actionLabel={t('empty.action')}
1319
+ actionIcon={<Plus className="size-4" />}
1320
+ onAction={openCreateSessao}
1321
+ className="py-16"
1322
+ />
1330
1323
  </motion.div>
1331
1324
  )}
1332
1325
  </motion.div>
@@ -2429,20 +2422,15 @@ function SortableSessao({
2429
2422
  >
2430
2423
  <div className="flex flex-col gap-0 p-2">
2431
2424
  {aulas.length === 0 ? (
2432
- <div className="flex flex-col items-center gap-2 py-6 text-center">
2433
- <p className="text-xs text-muted-foreground">
2434
- {t('session.noLessons')}
2435
- </p>
2436
- <Button
2437
- variant="outline"
2438
- size="sm"
2439
- className="gap-1.5 text-xs"
2440
- onClick={onCreateAula}
2441
- >
2442
- <Plus className="size-3" />
2443
- {t('session.addLesson')}
2444
- </Button>
2445
- </div>
2425
+ <EmptyState
2426
+ icon={<Layers className="h-10 w-10" />}
2427
+ title={t('session.noLessons')}
2428
+ description={t('session.noLessonsDescription')}
2429
+ actionLabel={t('session.addLesson')}
2430
+ actionIcon={<Plus className="size-3.5" />}
2431
+ onAction={onCreateAula}
2432
+ className="py-8"
2433
+ />
2446
2434
  ) : (
2447
2435
  aulas.map((aula) => (
2448
2436
  <SortableAula
@@ -1,6 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ } from '@/components/entity-list';
4
9
  import { Badge } from '@/components/ui/badge';
5
10
  import { Button } from '@/components/ui/button';
6
11
  import { Card, CardContent } from '@/components/ui/card';
@@ -54,10 +59,6 @@ import {
54
59
  Award,
55
60
  BarChart3,
56
61
  BookOpen,
57
- ChevronLeft,
58
- ChevronRight,
59
- ChevronsLeft,
60
- ChevronsRight,
61
62
  Eye,
62
63
  FileCheck,
63
64
  GraduationCap,
@@ -748,7 +749,7 @@ export default function CursosPage() {
748
749
  />
749
750
 
750
751
  {/* ── KPI Cards ────────────────────────────────────────────────────── */}
751
- <div className="mb-2 grid grid-cols-2 gap-4 lg:grid-cols-4">
752
+ <div className="mb-6 grid grid-cols-2 gap-4 lg:grid-cols-4">
752
753
  {loading
753
754
  ? Array.from({ length: 4 }).map((_, i) => (
754
755
  <Card key={i}>
@@ -790,7 +791,7 @@ export default function CursosPage() {
790
791
  </div>
791
792
 
792
793
  {/* ── Search bar ───────────────────────────────────────────────────── */}
793
- <form onSubmit={handleSearch} className="mb-2 mt-0">
794
+ <form onSubmit={handleSearch} className="mb-6 mt-0">
794
795
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
795
796
  {/* Search input — grows to fill space */}
796
797
  <div className="relative flex-1">
@@ -962,17 +963,14 @@ export default function CursosPage() {
962
963
  ))}
963
964
  </div>
964
965
  ) : filteredCursos.length === 0 ? (
965
- <div className="flex flex-col items-center justify-center py-20 text-center">
966
- <BookOpen className="mb-4 size-12 text-muted-foreground/40" />
967
- <p className="text-lg font-medium">{t('table.empty.title')}</p>
968
- <p className="mt-1 text-sm text-muted-foreground">
969
- {t('table.empty.description')}
970
- </p>
971
- <Button className="mt-6 gap-2" onClick={openCreateSheet}>
972
- <Plus className="size-4" />
973
- {t('actions.createCourse')}
974
- </Button>
975
- </div>
966
+ <EmptyState
967
+ icon={<BookOpen className="h-12 w-12" />}
968
+ title={t('table.empty.title')}
969
+ description={t('table.empty.description')}
970
+ actionLabel={t('actions.createCourse')}
971
+ onAction={openCreateSheet}
972
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
973
+ />
976
974
  ) : (
977
975
  <motion.div
978
976
  className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
@@ -1093,7 +1091,7 @@ export default function CursosPage() {
1093
1091
  )}
1094
1092
  </Badge>
1095
1093
  {curso.destaque && (
1096
- <span className="inline-flex items-center gap-1 rounded-full bg-gradient-to-r from-amber-50 to-orange-50 px-2.5 py-0.5 text-[11px] font-medium text-amber-700 border border-amber-200/60 shadow-sm">
1094
+ <span className="inline-flex items-center gap-1 rounded-full border border-amber-200/60 bg-linear-to-r from-amber-50 to-orange-50 px-2.5 py-0.5 text-[11px] font-medium text-amber-700 shadow-sm">
1097
1095
  <Star className="size-3 fill-amber-400 text-amber-400" />{' '}
1098
1096
  {t('form.flags.featured.label')}
1099
1097
  </span>
@@ -1152,92 +1150,19 @@ export default function CursosPage() {
1152
1150
 
1153
1151
  {/* ── Pagination footer ─────────────────────────────────────────────── */}
1154
1152
  {!loading && filteredCursos.length > 0 && (
1155
- <div className="mt-6 flex flex-col items-center justify-between gap-4 sm:flex-row">
1156
- {/* Total count */}
1157
- <p className="text-sm text-muted-foreground">
1158
- {filteredCursos.length}{' '}
1159
- {filteredCursos.length === 1
1160
- ? t('pagination.course')
1161
- : t('pagination.courses')}{' '}
1162
- {filteredCursos.length === 1
1163
- ? t('pagination.found')
1164
- : t('pagination.foundPlural')}
1165
- </p>
1166
-
1167
- {/* Center: page nav */}
1168
- <div className="flex items-center gap-1">
1169
- <Button
1170
- variant="outline"
1171
- size="icon"
1172
- className="size-8"
1173
- onClick={() => setCurrentPage(1)}
1174
- disabled={safePage === 1}
1175
- aria-label={t('pagination.firstPage')}
1176
- >
1177
- <ChevronsLeft className="size-4" />
1178
- </Button>
1179
- <Button
1180
- variant="outline"
1181
- size="icon"
1182
- className="size-8"
1183
- onClick={() => setCurrentPage((p) => p - 1)}
1184
- disabled={safePage === 1}
1185
- aria-label={t('pagination.previousPage')}
1186
- >
1187
- <ChevronLeft className="size-4" />
1188
- </Button>
1189
- <span className="px-3 text-sm">
1190
- {t('pagination.page')}{' '}
1191
- <span className="font-semibold">{safePage}</span>{' '}
1192
- {t('pagination.of')}{' '}
1193
- <span className="font-semibold">{totalPages}</span>
1194
- </span>
1195
- <Button
1196
- variant="outline"
1197
- size="icon"
1198
- className="size-8"
1199
- onClick={() => setCurrentPage((p) => p + 1)}
1200
- disabled={safePage === totalPages}
1201
- aria-label={t('pagination.nextPage')}
1202
- >
1203
- <ChevronRight className="size-4" />
1204
- </Button>
1205
- <Button
1206
- variant="outline"
1207
- size="icon"
1208
- className="size-8"
1209
- onClick={() => setCurrentPage(totalPages)}
1210
- disabled={safePage === totalPages}
1211
- aria-label={t('pagination.lastPage')}
1212
- >
1213
- <ChevronsRight className="size-4" />
1214
- </Button>
1215
- </div>
1216
-
1217
- {/* Items per page */}
1218
- <div className="flex items-center gap-2 text-sm">
1219
- <span className="text-muted-foreground">
1220
- {t('pagination.itemsPerPage')}
1221
- </span>
1222
- <Select
1223
- value={String(pageSize)}
1224
- onValueChange={(v) => {
1225
- setPageSize(Number(v));
1226
- setCurrentPage(1);
1227
- }}
1228
- >
1229
- <SelectTrigger className="h-8 w-16 text-sm">
1230
- <SelectValue />
1231
- </SelectTrigger>
1232
- <SelectContent>
1233
- {PAGE_SIZES.map((s) => (
1234
- <SelectItem key={s} value={String(s)}>
1235
- {s}
1236
- </SelectItem>
1237
- ))}
1238
- </SelectContent>
1239
- </Select>
1240
- </div>
1153
+ <div className="mt-6">
1154
+ <PaginationFooter
1155
+ currentPage={safePage}
1156
+ pageSize={pageSize}
1157
+ totalItems={filteredCursos.length}
1158
+ onPageChange={setCurrentPage}
1159
+ onPageSizeChange={(nextSize) => {
1160
+ setPageSize(nextSize);
1161
+ setCurrentPage(1);
1162
+ }}
1163
+ pageSizeOptions={PAGE_SIZES}
1164
+ selectedCount={selectedIds.size}
1165
+ />
1241
1166
  </div>
1242
1167
  )}
1243
1168
 
@@ -1409,7 +1334,7 @@ export default function CursosPage() {
1409
1334
  {CATEGORIAS.map((cat) => (
1410
1335
  <label
1411
1336
  key={cat}
1412
- className="flex cursor-pointer items-center gap-2 rounded-md border p-2.5 text-sm hover:bg-muted has-[:checked]:border-foreground has-[:checked]:bg-muted"
1337
+ className="flex cursor-pointer items-center gap-2 rounded-md border p-2.5 text-sm hover:bg-muted has-checked:border-foreground has-checked:bg-muted"
1413
1338
  >
1414
1339
  <Checkbox
1415
1340
  checked={field.value.includes(cat)}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import { EmptyState, Page, PageHeader } from '@/components/entity-list';
4
4
  import { Badge } from '@/components/ui/badge';
5
5
  import { Button } from '@/components/ui/button';
6
6
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -265,6 +265,29 @@ export default function TentativaPage() {
265
265
  const answeredCount = Object.keys(answers).length;
266
266
  const progressPercent = (answeredCount / examQuestions.length) * 100;
267
267
 
268
+ if (!currentQuestion) {
269
+ return (
270
+ <Page>
271
+ <PageHeader
272
+ title={EXAM_TITLE}
273
+ breadcrumbs={[
274
+ { label: t('breadcrumbs.home'), href: '/' },
275
+ { label: t('breadcrumbs.exams'), href: '/lms/exams' },
276
+ { label: t('breadcrumbs.current') },
277
+ ]}
278
+ />
279
+ <Card>
280
+ <CardContent className="p-6 text-center">
281
+ <p className="text-base font-semibold">{t('empty.title')}</p>
282
+ <p className="mt-2 text-sm text-muted-foreground">
283
+ {t('empty.description')}
284
+ </p>
285
+ </CardContent>
286
+ </Card>
287
+ </Page>
288
+ );
289
+ }
290
+
268
291
  function formatTime(seconds: number) {
269
292
  const h = Math.floor(seconds / 3600);
270
293
  const m = Math.floor((seconds % 3600) / 60);
@@ -738,8 +761,7 @@ export default function TentativaPage() {
738
761
  </Button>
739
762
  <Button
740
763
  variant="ghost"
741
- className={`shrink-0 ${flagged.has(currentQuestion.id) ? 'text-primary' : 'text-muted-foreground'}`}
742
- className="lg:hidden"
764
+ className={`shrink-0 lg:hidden ${flagged.has(currentQuestion.id) ? 'text-primary' : 'text-muted-foreground'}`}
743
765
  onClick={() => setShowNav(!showNav)}
744
766
  >
745
767
  {currentIndex + 1}/{examQuestions.length}
@@ -1,6 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ } from '@/components/entity-list';
4
9
  import { Badge } from '@/components/ui/badge';
5
10
  import { Button } from '@/components/ui/button';
6
11
  import { Card, CardContent } from '@/components/ui/card';
@@ -167,6 +172,8 @@ const initialQuestoes: Questao[] = [
167
172
  },
168
173
  ];
169
174
 
175
+ const PAGE_SIZE_OPTIONS = [6, 12, 24];
176
+
170
177
  function SortableAlternativa({
171
178
  alt,
172
179
  index,
@@ -435,6 +442,8 @@ export default function QuestoesPage() {
435
442
  const [loading, setLoading] = useState(true);
436
443
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
437
444
  const [questoes, setQuestoes] = useState<Questao[]>(initialQuestoes);
445
+ const [currentPage, setCurrentPage] = useState(1);
446
+ const [pageSize, setPageSize] = useState(6);
438
447
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
439
448
  const [sheetOpen, setSheetOpen] = useState(false);
440
449
  const [editingQuestao, setEditingQuestao] = useState<Questao | null>(null);
@@ -463,6 +472,13 @@ export default function QuestoesPage() {
463
472
  return () => clearTimeout(timer);
464
473
  }, []);
465
474
 
475
+ const totalPages = Math.max(1, Math.ceil(questoes.length / pageSize));
476
+ const safePage = Math.min(Math.max(currentPage, 1), totalPages);
477
+ const paginatedQuestoes = questoes.slice(
478
+ (safePage - 1) * pageSize,
479
+ safePage * pageSize
480
+ );
481
+
466
482
  function toggleExpand(qId: string) {
467
483
  setExpandedIds((prev) => {
468
484
  const next = new Set(prev);
@@ -730,20 +746,14 @@ export default function QuestoesPage() {
730
746
  ))}
731
747
  </div>
732
748
  ) : questoes.length === 0 ? (
733
- <Card>
734
- <CardContent className="flex flex-col items-center gap-3 py-16">
735
- <div className="flex size-12 items-center justify-center rounded-full bg-muted">
736
- <FileCheck className="size-6 text-muted-foreground" />
737
- </div>
738
- <p className="font-medium">{t('empty.title')}</p>
739
- <p className="text-sm text-muted-foreground">
740
- {t('empty.description')}
741
- </p>
742
- <Button onClick={openCreateSheet} className="mt-2 gap-2">
743
- <Plus className="size-4" /> {t('empty.action')}
744
- </Button>
745
- </CardContent>
746
- </Card>
749
+ <EmptyState
750
+ icon={<FileCheck className="h-12 w-12" />}
751
+ title={t('empty.title')}
752
+ description={t('empty.description')}
753
+ actionLabel={t('empty.action')}
754
+ onAction={openCreateSheet}
755
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
756
+ />
747
757
  ) : (
748
758
  <DndContext
749
759
  sensors={sensors}
@@ -756,13 +766,13 @@ export default function QuestoesPage() {
756
766
  >
757
767
  <div className="flex flex-col gap-3">
758
768
  <AnimatePresence>
759
- {questoes.map((questao, qIndex) => {
769
+ {paginatedQuestoes.map((questao, qIndex) => {
760
770
  const isExpanded = expandedIds.has(questao.id);
761
771
  return (
762
772
  <SortableQuestao
763
773
  key={questao.id}
764
774
  questao={questao}
765
- qIndex={qIndex}
775
+ qIndex={(safePage - 1) * pageSize + qIndex}
766
776
  isExpanded={isExpanded}
767
777
  onToggleExpand={() => toggleExpand(questao.id)}
768
778
  onEdit={() => openEditSheet(questao)}
@@ -779,6 +789,22 @@ export default function QuestoesPage() {
779
789
  </SortableContext>
780
790
  </DndContext>
781
791
  )}
792
+
793
+ {!loading && questoes.length > 0 && (
794
+ <div className="mt-6">
795
+ <PaginationFooter
796
+ currentPage={safePage}
797
+ pageSize={pageSize}
798
+ totalItems={questoes.length}
799
+ onPageChange={setCurrentPage}
800
+ onPageSizeChange={(nextSize) => {
801
+ setPageSize(nextSize);
802
+ setCurrentPage(1);
803
+ }}
804
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
805
+ />
806
+ </div>
807
+ )}
782
808
  </motion.div>
783
809
  </div>
784
810
 
@@ -1,6 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ } from '@/components/entity-list';
4
9
  import { Badge } from '@/components/ui/badge';
5
10
  import { Button } from '@/components/ui/button';
6
11
  import { Card, CardContent } from '@/components/ui/card';
@@ -50,10 +55,6 @@ import {
50
55
  BarChart3,
51
56
  BookOpen,
52
57
  CheckCircle2,
53
- ChevronLeft,
54
- ChevronRight,
55
- ChevronsLeft,
56
- ChevronsRight,
57
58
  FileCheck,
58
59
  FileQuestion,
59
60
  GraduationCap,
@@ -585,7 +586,7 @@ export default function ExamesPage() {
585
586
  </div>
586
587
 
587
588
  {/* Search bar */}
588
- <form onSubmit={handleSearch} className="-mt-4 mb-2">
589
+ <form onSubmit={handleSearch} className="mb-6 mt-0">
589
590
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
590
591
  <div className="relative flex-1">
591
592
  <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
@@ -651,17 +652,14 @@ export default function ExamesPage() {
651
652
  ))}
652
653
  </div>
653
654
  ) : filteredExames.length === 0 ? (
654
- <div className="flex flex-col items-center justify-center py-20 text-center">
655
- <FileCheck className="mb-4 size-12 text-muted-foreground/40" />
656
- <p className="text-lg font-medium">{t('empty.title')}</p>
657
- <p className="mt-1 text-sm text-muted-foreground">
658
- {t('empty.description')}
659
- </p>
660
- <Button className="mt-6 gap-2" onClick={openCreateSheet}>
661
- <Plus className="size-4" />
662
- {t('empty.action')}
663
- </Button>
664
- </div>
655
+ <EmptyState
656
+ icon={<FileCheck className="h-12 w-12" />}
657
+ title={t('empty.title')}
658
+ description={t('empty.description')}
659
+ actionLabel={t('empty.action')}
660
+ onAction={openCreateSheet}
661
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
662
+ />
665
663
  ) : (
666
664
  <motion.div
667
665
  className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
@@ -853,87 +851,18 @@ export default function ExamesPage() {
853
851
 
854
852
  {/* Pagination footer */}
855
853
  {!loading && filteredExames.length > 0 && (
856
- <div className="mt-6 flex flex-col items-center justify-between gap-4 sm:flex-row">
857
- <p className="text-sm text-muted-foreground">
858
- {filteredExames.length}{' '}
859
- {filteredExames.length !== 1
860
- ? t('pagination.examsPlural')
861
- : t('pagination.exams')}{' '}
862
- {filteredExames.length !== 1
863
- ? t('pagination.foundPlural')
864
- : t('pagination.found')}
865
- </p>
866
- <div className="flex items-center gap-1">
867
- <Button
868
- variant="outline"
869
- size="icon"
870
- className="size-8"
871
- onClick={() => setCurrentPage(1)}
872
- disabled={safePage === 1}
873
- aria-label={t('pagination.firstPage')}
874
- >
875
- <ChevronsLeft className="size-4" />
876
- </Button>
877
- <Button
878
- variant="outline"
879
- size="icon"
880
- className="size-8"
881
- onClick={() => setCurrentPage((p) => p - 1)}
882
- disabled={safePage === 1}
883
- aria-label={t('pagination.previousPage')}
884
- >
885
- <ChevronLeft className="size-4" />
886
- </Button>
887
- <span className="px-3 text-sm">
888
- {t('pagination.page')}{' '}
889
- <span className="font-semibold">{safePage}</span>{' '}
890
- {t('pagination.of')}{' '}
891
- <span className="font-semibold">{totalPages}</span>
892
- </span>
893
- <Button
894
- variant="outline"
895
- size="icon"
896
- className="size-8"
897
- onClick={() => setCurrentPage((p) => p + 1)}
898
- disabled={safePage === totalPages}
899
- aria-label={t('pagination.nextPage')}
900
- >
901
- <ChevronRight className="size-4" />
902
- </Button>
903
- <Button
904
- variant="outline"
905
- size="icon"
906
- className="size-8"
907
- onClick={() => setCurrentPage(totalPages)}
908
- disabled={safePage === totalPages}
909
- aria-label={t('pagination.lastPage')}
910
- >
911
- <ChevronsRight className="size-4" />
912
- </Button>
913
- </div>
914
- <div className="flex items-center gap-2 text-sm">
915
- <span className="text-muted-foreground">
916
- {t('pagination.itemsPerPage')}
917
- </span>
918
- <Select
919
- value={String(pageSize)}
920
- onValueChange={(v) => {
921
- setPageSize(Number(v));
922
- setCurrentPage(1);
923
- }}
924
- >
925
- <SelectTrigger className="h-8 w-16 text-sm">
926
- <SelectValue />
927
- </SelectTrigger>
928
- <SelectContent>
929
- {PAGE_SIZES.map((s) => (
930
- <SelectItem key={s} value={String(s)}>
931
- {s}
932
- </SelectItem>
933
- ))}
934
- </SelectContent>
935
- </Select>
936
- </div>
854
+ <div className="mt-6">
855
+ <PaginationFooter
856
+ currentPage={safePage}
857
+ pageSize={pageSize}
858
+ totalItems={filteredExames.length}
859
+ onPageChange={setCurrentPage}
860
+ onPageSizeChange={(nextSize) => {
861
+ setPageSize(nextSize);
862
+ setCurrentPage(1);
863
+ }}
864
+ pageSizeOptions={PAGE_SIZES}
865
+ />
937
866
  </div>
938
867
  )}
939
868
 
@@ -31,7 +31,7 @@ import {
31
31
  startOfWeek,
32
32
  } from 'date-fns';
33
33
  import { ptBR } from 'date-fns/locale/pt-BR';
34
- import { motion } from 'framer-motion';
34
+ import { motion, type Variants } from 'framer-motion';
35
35
  import {
36
36
  Award,
37
37
  BarChart3,
@@ -163,7 +163,7 @@ function genCalendarEvents() {
163
163
  if (dayOfWeek === 0 || dayOfWeek === 6) continue;
164
164
  const numAulas = dayOfWeek % 2 === 0 ? 3 : 2;
165
165
  for (let a = 0; a < numAulas; a++) {
166
- const turma = turmas[(d + 14 + a) % turmas.length];
166
+ const turma = turmas[(d + 14 + a) % turmas.length]!;
167
167
  const horaInicio = 8 + a * 3;
168
168
  events.push({
169
169
  title: turma.nome,
@@ -394,7 +394,7 @@ const stagger = {
394
394
  show: { transition: { staggerChildren: 0.08 } },
395
395
  };
396
396
 
397
- const fadeUp = {
397
+ const fadeUp: Variants = {
398
398
  hidden: { opacity: 0, y: 16 },
399
399
  show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: 'easeOut' } },
400
400
  };