@hed-hog/lms 0.0.365 → 0.0.366

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.
Files changed (113) hide show
  1. package/dist/class-group/class-group.controller.d.ts +1 -0
  2. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  3. package/dist/class-group/class-group.service.d.ts +1 -0
  4. package/dist/class-group/class-group.service.d.ts.map +1 -1
  5. package/dist/course/course-structure.controller.d.ts +4 -2
  6. package/dist/course/course-structure.controller.d.ts.map +1 -1
  7. package/dist/course/course-structure.controller.js +6 -3
  8. package/dist/course/course-structure.controller.js.map +1 -1
  9. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  10. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  11. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  12. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  13. package/dist/course/course-video-hls.service.d.ts +14 -0
  14. package/dist/course/course-video-hls.service.d.ts.map +1 -1
  15. package/dist/course/course-video-hls.service.js +25 -8
  16. package/dist/course/course-video-hls.service.js.map +1 -1
  17. package/dist/course/course.controller.d.ts +2 -0
  18. package/dist/course/course.controller.d.ts.map +1 -1
  19. package/dist/course/course.module.d.ts.map +1 -1
  20. package/dist/course/course.module.js +5 -0
  21. package/dist/course/course.module.js.map +1 -1
  22. package/dist/course/course.service.d.ts +2 -0
  23. package/dist/course/course.service.d.ts.map +1 -1
  24. package/dist/course/course.service.js +36 -2
  25. package/dist/course/course.service.js.map +1 -1
  26. package/dist/course/ffmpeg.util.d.ts +10 -0
  27. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  28. package/dist/course/ffmpeg.util.js +79 -0
  29. package/dist/course/ffmpeg.util.js.map +1 -0
  30. package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
  31. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  32. package/dist/course/lms-bulk-upload-automation.service.js +7 -3
  33. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  34. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  35. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  36. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  37. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  38. package/dist/lms.module.d.ts.map +1 -1
  39. package/dist/lms.module.js +10 -0
  40. package/dist/lms.module.js.map +1 -1
  41. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  42. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  43. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  44. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  45. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  46. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  47. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  48. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  49. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  50. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  51. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  52. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  53. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  54. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  55. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  56. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  57. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  58. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  59. package/dist/platforma/platforma-performance.service.js +500 -0
  60. package/dist/platforma/platforma-performance.service.js.map +1 -0
  61. package/dist/platforma/platforma-search.service.d.ts +21 -0
  62. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  63. package/dist/platforma/platforma-search.service.js +64 -0
  64. package/dist/platforma/platforma-search.service.js.map +1 -0
  65. package/dist/platforma/platforma.controller.d.ts +115 -1
  66. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  67. package/dist/platforma/platforma.controller.js +50 -2
  68. package/dist/platforma/platforma.controller.js.map +1 -1
  69. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  70. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  71. package/dist/realtime/lms-realtime.controller.js +31 -0
  72. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  73. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  74. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  75. package/dist/realtime/lms-realtime.service.js.map +1 -1
  76. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  77. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  78. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  83. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
  84. package/hedhog/frontend/app/courses/page.tsx.ejs +40 -9
  85. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  86. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  87. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  88. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  89. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  90. package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
  91. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  92. package/hedhog/frontend/messages/en.json +18 -0
  93. package/hedhog/frontend/messages/pt.json +21 -1
  94. package/hedhog/table/course_enrollment.yaml +3 -0
  95. package/hedhog/table/lesson_view_event.yaml +66 -0
  96. package/package.json +9 -8
  97. package/src/course/course-structure.controller.ts +3 -1
  98. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  99. package/src/course/course-video-hls.service.ts +30 -10
  100. package/src/course/course.module.ts +5 -0
  101. package/src/course/course.service.ts +46 -1
  102. package/src/course/ffmpeg.util.ts +65 -0
  103. package/src/course/lms-bulk-upload-automation.service.ts +4 -1
  104. package/src/lms.module.ts +10 -0
  105. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  106. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  107. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  108. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  109. package/src/platforma/platforma-performance.service.ts +606 -0
  110. package/src/platforma/platforma-search.service.ts +48 -0
  111. package/src/platforma/platforma.controller.ts +42 -0
  112. package/src/realtime/lms-realtime.controller.ts +27 -1
  113. package/src/realtime/lms-realtime.service.ts +2 -1
@@ -7,8 +7,17 @@ import {
7
7
  PageHeader,
8
8
  PaginationFooter,
9
9
  SearchBar,
10
- ViewModeToggle,
11
10
  } from '@/components/entity-list';
11
+ import {
12
+ Tooltip,
13
+ TooltipContent,
14
+ TooltipProvider,
15
+ TooltipTrigger,
16
+ } from '@/components/ui/tooltip';
17
+ import {
18
+ ToggleGroup,
19
+ ToggleGroupItem,
20
+ } from '@/components/ui/toggle-group';
12
21
  import { Badge } from '@/components/ui/badge';
13
22
  import { Button } from '@/components/ui/button';
14
23
  import { Card, CardContent } from '@/components/ui/card';
@@ -39,10 +48,13 @@ import { motion } from 'framer-motion';
39
48
  import {
40
49
  AlertTriangle,
41
50
  BarChart3,
51
+ CalendarDays,
42
52
  CalendarIcon,
43
53
  Clock,
44
54
  Eye,
55
+ LayoutGrid,
45
56
  Laptop,
57
+ List,
46
58
  Loader2,
47
59
  MapPin,
48
60
  Monitor,
@@ -59,6 +71,7 @@ import { useRouter } from 'next/navigation';
59
71
  import { useEffect, useMemo, useRef, useState } from 'react';
60
72
  import { toast } from 'sonner';
61
73
  import { ClassFormSheet } from '../_components/class-form-sheet';
74
+ import { ClassesCalendarView } from './_components/classes-calendar-view';
62
75
 
63
76
  // ── Types ─────────────────────────────────────────────────────────────────────
64
77
 
@@ -229,7 +242,7 @@ function mapApiClass(item: ApiClass): Turma {
229
242
  };
230
243
  }
231
244
 
232
- type ViewMode = 'cards' | 'list';
245
+ type ViewMode = 'cards' | 'list' | 'calendar';
233
246
 
234
247
  // ── Constants ─────────────────────────────────────────────────────────────────
235
248
 
@@ -269,7 +282,7 @@ const TIPO_ICON: Record<string, LucideIcon> = {
269
282
  hibrida: Laptop,
270
283
  };
271
284
 
272
- const PAGE_SIZES = [6, 12, 24];
285
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
273
286
 
274
287
  // ── Animations ────────────────────────────────────────────────────────────────
275
288
 
@@ -306,15 +319,49 @@ export default function TurmasPage() {
306
319
  const [viewMode, setViewMode] = usePersistedViewMode<ViewMode>({
307
320
  storageKey: 'lms:classes:view-mode',
308
321
  defaultValue: 'cards',
309
- allowedValues: ['cards', 'list'],
322
+ allowedValues: ['cards', 'list', 'calendar'],
310
323
  });
311
324
 
312
325
  // Pagination
313
326
  const [currentPage, setCurrentPage] = useState(1);
327
+
328
+ const { data: generalSettings } = useQuery<{
329
+ data: Array<{ slug: string; value: string }>;
330
+ }>({
331
+ queryKey: ['setting-group-general'],
332
+ queryFn: async () => {
333
+ const response = await request<{
334
+ data: Array<{ slug: string; value: string }>;
335
+ }>({
336
+ url: '/setting/group/general',
337
+ method: 'GET',
338
+ });
339
+ return response.data;
340
+ },
341
+ staleTime: 5 * 60 * 1000,
342
+ });
343
+
344
+ const pageSizeOptions = useMemo(() => {
345
+ const setting = generalSettings?.data?.find(
346
+ (s) => s.slug === 'pagination-page-sizes'
347
+ );
348
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
349
+ try {
350
+ const parsed = JSON.parse(setting.value) as string[];
351
+ const sizes = parsed
352
+ .map(Number)
353
+ .filter((n) => !isNaN(n) && n > 0)
354
+ .sort((a, b) => a - b);
355
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
356
+ } catch {
357
+ return DEFAULT_PAGE_SIZES;
358
+ }
359
+ }, [generalSettings]);
360
+
314
361
  const [pageSize, setPageSize] = usePersistedPageSize({
315
362
  storageKey: 'pagination:global:pageSize',
316
363
  defaultValue: 12,
317
- allowedValues: PAGE_SIZES,
364
+ allowedValues: pageSizeOptions,
318
365
  });
319
366
 
320
367
  const { data: coursesResponse } = useQuery<ApiCourseList>({
@@ -333,6 +380,9 @@ export default function TurmasPage() {
333
380
  },
334
381
  });
335
382
 
383
+ const effectivePage = viewMode === 'calendar' ? 1 : currentPage;
384
+ const effectivePageSize = viewMode === 'calendar' ? 500 : pageSize;
385
+
336
386
  const {
337
387
  data: classesResponse,
338
388
  refetch: refetchClasses,
@@ -341,8 +391,8 @@ export default function TurmasPage() {
341
391
  } = useQuery<ApiClassList>({
342
392
  queryKey: [
343
393
  'lms-classes-list',
344
- currentPage,
345
- pageSize,
394
+ effectivePage,
395
+ effectivePageSize,
346
396
  buscaDebounced,
347
397
  filtroStatusInput,
348
398
  filtroTipoInput,
@@ -359,8 +409,8 @@ export default function TurmasPage() {
359
409
  url: '/lms/classes',
360
410
  method: 'GET',
361
411
  params: {
362
- page: currentPage,
363
- pageSize,
412
+ page: effectivePage,
413
+ pageSize: effectivePageSize,
364
414
  ...(buscaDebounced ? { search: buscaDebounced } : {}),
365
415
  ...(filtroStatusInput !== 'todos'
366
416
  ? { status: toApiStatus(filtroStatusInput) }
@@ -742,13 +792,62 @@ export default function TurmasPage() {
742
792
  className: 'sm:w-56',
743
793
  },
744
794
  ]}
745
- afterSearchButton={
746
- <ViewModeToggle
747
- viewMode={viewMode}
748
- onViewModeChange={setViewMode}
749
- listLabel={t('viewMode.list')}
750
- cardsLabel={t('viewMode.cards')}
751
- />
795
+ actions={
796
+ <TooltipProvider>
797
+ <ToggleGroup
798
+ type="single"
799
+ value={viewMode}
800
+ onValueChange={(value) => {
801
+ if (
802
+ value === 'cards' ||
803
+ value === 'list' ||
804
+ value === 'calendar'
805
+ ) {
806
+ setViewMode(value);
807
+ }
808
+ }}
809
+ variant="outline"
810
+ size="sm"
811
+ className="shrink-0"
812
+ >
813
+ <Tooltip>
814
+ <TooltipTrigger asChild>
815
+ <ToggleGroupItem
816
+ value="cards"
817
+ aria-label={t('viewMode.cards')}
818
+ className="cursor-pointer"
819
+ >
820
+ <LayoutGrid className="h-4 w-4" />
821
+ </ToggleGroupItem>
822
+ </TooltipTrigger>
823
+ <TooltipContent>{t('viewMode.cards')}</TooltipContent>
824
+ </Tooltip>
825
+ <Tooltip>
826
+ <TooltipTrigger asChild>
827
+ <ToggleGroupItem
828
+ value="list"
829
+ aria-label={t('viewMode.list')}
830
+ className="cursor-pointer"
831
+ >
832
+ <List className="h-4 w-4" />
833
+ </ToggleGroupItem>
834
+ </TooltipTrigger>
835
+ <TooltipContent>{t('viewMode.list')}</TooltipContent>
836
+ </Tooltip>
837
+ <Tooltip>
838
+ <TooltipTrigger asChild>
839
+ <ToggleGroupItem
840
+ value="calendar"
841
+ aria-label={t('viewMode.calendar')}
842
+ className="cursor-pointer"
843
+ >
844
+ <CalendarDays className="h-4 w-4" />
845
+ </ToggleGroupItem>
846
+ </TooltipTrigger>
847
+ <TooltipContent>{t('viewMode.calendar')}</TooltipContent>
848
+ </Tooltip>
849
+ </ToggleGroup>
850
+ </TooltipProvider>
752
851
  }
753
852
  />
754
853
  <div className="flex items-center justify-between gap-3">
@@ -774,7 +873,13 @@ export default function TurmasPage() {
774
873
 
775
874
  {/* Classes list */}
776
875
  {loading ? (
777
- viewMode === 'cards' ? (
876
+ viewMode === 'calendar' ? (
877
+ <div className="grid grid-cols-7 gap-px">
878
+ {Array.from({ length: 42 }).map((_, i) => (
879
+ <div key={i} className="h-16 animate-pulse rounded bg-muted/30" />
880
+ ))}
881
+ </div>
882
+ ) : viewMode === 'cards' ? (
778
883
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
779
884
  {Array.from({ length: 6 }).map((_, i) => (
780
885
  <Card key={i} className="overflow-hidden border-border/70">
@@ -1018,7 +1123,7 @@ export default function TurmasPage() {
1018
1123
  );
1019
1124
  })}
1020
1125
  </motion.div>
1021
- ) : (
1126
+ ) : viewMode === 'list' ? (
1022
1127
  <div className="overflow-hidden rounded-xl border border-border/70">
1023
1128
  <Table>
1024
1129
  <TableHeader>
@@ -1146,10 +1251,12 @@ export default function TurmasPage() {
1146
1251
  </TableBody>
1147
1252
  </Table>
1148
1253
  </div>
1254
+ ) : (
1255
+ <ClassesCalendarView turmas={visibleTurmas} />
1149
1256
  )}
1150
1257
 
1151
1258
  {/* Pagination footer */}
1152
- {!loading && visibleTurmas.length > 0 && (
1259
+ {!loading && visibleTurmas.length > 0 && viewMode !== 'calendar' && (
1153
1260
  <div className="mt-6">
1154
1261
  <PaginationFooter
1155
1262
  currentPage={currentPage}
@@ -1160,7 +1267,7 @@ export default function TurmasPage() {
1160
1267
  setPageSize(value);
1161
1268
  setCurrentPage(1);
1162
1269
  }}
1163
- pageSizeOptions={PAGE_SIZES}
1270
+ pageSizeOptions={pageSizeOptions}
1164
1271
  />
1165
1272
  </div>
1166
1273
  )}
@@ -414,8 +414,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
414
414
  key: 'storage',
415
415
  label: 'Armazenamento',
416
416
  value: formatBytes(data.storage.totalBytes),
417
- toneClassName:
418
- 'border-cyan-500/15 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300',
417
+ toneClassName: 'border-border/60 bg-muted/30 text-muted-foreground',
419
418
  },
420
419
  {
421
420
  key: 'coverage',
@@ -424,22 +423,19 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
424
423
  totalLessons > 0
425
424
  ? `${Math.round((videoCount / totalLessons) * 100)}% das aulas`
426
425
  : '0% das aulas',
427
- toneClassName:
428
- 'border-teal-500/15 bg-teal-500/10 text-teal-700 dark:text-teal-300',
426
+ toneClassName: 'border-border/60 bg-muted/30 text-muted-foreground',
429
427
  },
430
428
  {
431
429
  key: 'images',
432
430
  label: 'Imagens extraídas',
433
431
  value: `${data.media.extractedImageCount}`,
434
- toneClassName:
435
- 'border-amber-500/15 bg-amber-500/10 text-amber-700 dark:text-amber-300',
432
+ toneClassName: 'border-border/60 bg-muted/30 text-muted-foreground',
436
433
  },
437
434
  {
438
435
  key: 'resources',
439
436
  label: 'Arquivos de apoio',
440
437
  value: `${data.resources.fileCount}`,
441
- toneClassName:
442
- 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300',
438
+ toneClassName: 'border-border/60 bg-muted/30 text-muted-foreground',
443
439
  },
444
440
  ];
445
441
 
@@ -569,7 +565,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
569
565
  <div className="rounded-2xl border border-border/70 bg-linear-to-br from-background via-background to-muted/30 px-2.5 py-2 shadow-[0_14px_34px_-28px_rgba(15,23,42,0.45)] backdrop-blur-sm sm:px-3 sm:py-2.5">
570
566
  <div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/50 pb-2">
571
567
  <div className="min-w-0">
572
- <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-700/80 dark:text-cyan-300/80">
568
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
573
569
  Course overview
574
570
  </div>
575
571
  <div className="mt-1 text-sm font-semibold text-foreground">
@@ -609,14 +605,14 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
609
605
  </div>
610
606
 
611
607
  <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
612
- <CardHeader className="border-b border-border/70 bg-linear-to-r from-cyan-500/12 via-sky-500/6 to-transparent pt-2.5 pb-1.5!">
608
+ <CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
613
609
  <div className="flex flex-wrap items-start justify-between gap-3">
614
610
  <div className="flex min-w-0 items-start gap-3">
615
611
  <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-cyan-500/10 text-cyan-600 ring-1 ring-inset ring-cyan-500/15 dark:text-cyan-400">
616
612
  <HardDrive className="size-4" />
617
613
  </div>
618
614
  <div className="min-w-0">
619
- <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-600/80 dark:text-cyan-400/80">
615
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
620
616
  Storage footprint
621
617
  </div>
622
618
  <CardTitle className="mt-1 text-sm font-semibold">
@@ -628,15 +624,15 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
628
624
  </CardDescription>
629
625
  </div>
630
626
  </div>
631
- <div className="rounded-full border border-cyan-500/15 bg-cyan-500/10 px-2.5 py-1 text-[10px] font-semibold text-cyan-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-cyan-500/15 hover:shadow-md dark:text-cyan-300">
627
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
632
628
  {formatBytes(data.storage.totalBytes)}
633
629
  </div>
634
630
  </div>
635
631
  </CardHeader>
636
632
  <CardContent className="grid gap-3 px-2.5 py-2.5 sm:px-3 xl:grid-cols-[minmax(0,240px)_minmax(0,1fr)]">
637
633
  <div className="grid gap-2">
638
- <div className="rounded-2xl border border-cyan-500/15 bg-linear-to-br from-cyan-500/12 via-sky-500/6 to-transparent px-3 py-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
639
- <div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-cyan-700/80 dark:text-cyan-300/80">
634
+ <div className="rounded-2xl border border-border/60 bg-muted/20 px-3 py-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
635
+ <div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
640
636
  Total armazenado
641
637
  </div>
642
638
  <div className="mt-2 text-2xl font-semibold tracking-tight text-foreground">
@@ -675,7 +671,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
675
671
  return (
676
672
  <div
677
673
  key={category.key}
678
- className="overflow-hidden rounded-2xl border border-border/60 bg-card/90 transition-all duration-200 hover:-translate-y-0.5 hover:border-cyan-500/20 hover:bg-background/95 hover:shadow-md"
674
+ className="overflow-hidden rounded-2xl border border-border/60 bg-card/90 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:bg-background/95 hover:shadow-md"
679
675
  >
680
676
  <div
681
677
  className={`h-1.5 w-full bg-linear-to-r ${category.accentClassName}`}
@@ -794,14 +790,14 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
794
790
 
795
791
  <div className="grid gap-3 xl:grid-cols-2">
796
792
  <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
797
- <CardHeader className="border-b border-border/70 bg-linear-to-r from-sky-500/12 via-blue-500/6 to-transparent pt-2.5 pb-1.5!">
793
+ <CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
798
794
  <div className="flex flex-wrap items-start justify-between gap-3">
799
795
  <div className="flex min-w-0 items-start gap-3">
800
796
  <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-sky-500/10 text-sky-600 ring-1 ring-inset ring-sky-500/15 dark:text-sky-400">
801
797
  <Layers className="size-4" />
802
798
  </div>
803
799
  <div className="min-w-0">
804
- <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-600/80 dark:text-sky-400/80">
800
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
805
801
  Lesson mix
806
802
  </div>
807
803
  <CardTitle className="mt-1 text-sm font-semibold">
@@ -812,7 +808,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
812
808
  </CardDescription>
813
809
  </div>
814
810
  </div>
815
- <div className="rounded-full border border-sky-500/15 bg-sky-500/10 px-2.5 py-1 text-[10px] font-semibold text-sky-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-sky-500/15 hover:shadow-md dark:text-sky-300">
811
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
816
812
  {totalLessons} aulas
817
813
  </div>
818
814
  </div>
@@ -886,7 +882,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
886
882
  return (
887
883
  <div
888
884
  key={item.type}
889
- className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-sky-500/20 hover:bg-background/95 hover:shadow-md"
885
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:bg-background/95 hover:shadow-md"
890
886
  >
891
887
  <div
892
888
  className="flex size-8 shrink-0 items-center justify-center rounded-lg"
@@ -926,16 +922,131 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
926
922
  </CardContent>
927
923
  </Card>
928
924
 
925
+ {/* Video pipeline */}
926
+ {videoCount > 0 && (
927
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
928
+ <CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
929
+ <div className="flex flex-wrap items-start justify-between gap-3">
930
+ <div className="flex min-w-0 items-start gap-3">
931
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-violet-500/10 text-violet-600 ring-1 ring-inset ring-violet-500/15 dark:text-violet-400">
932
+ <Film className="size-4" />
933
+ </div>
934
+ <div className="min-w-0">
935
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
936
+ Video pipeline
937
+ </div>
938
+ <CardTitle className="mt-1 text-sm font-semibold">
939
+ {t('videoPipeline.title')}
940
+ </CardTitle>
941
+ <CardDescription className="mt-0.5">
942
+ {t('videoPipeline.description')}
943
+ </CardDescription>
944
+ </div>
945
+ </div>
946
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
947
+ {videoCount} vídeos
948
+ </div>
949
+ </div>
950
+ </CardHeader>
951
+ <CardContent className="flex flex-col gap-3 px-2.5 py-2.5 sm:px-3">
952
+ {(
953
+ [
954
+ {
955
+ key: 'total',
956
+ label: t('videoPipeline.total'),
957
+ value: videoCount,
958
+ total: videoCount,
959
+ color: '#8b5cf6',
960
+ bgColor: 'bg-violet-500/10',
961
+ textColor: 'text-violet-600 dark:text-violet-400',
962
+ icon: Clapperboard,
963
+ },
964
+ {
965
+ key: 'withVideo',
966
+ label: t('videoPipeline.withVideo'),
967
+ value: data.videos.withVideo,
968
+ total: videoCount,
969
+ color: '#a855f7',
970
+ bgColor: 'bg-purple-500/10',
971
+ textColor: 'text-purple-600 dark:text-purple-400',
972
+ icon: Film,
973
+ },
974
+ {
975
+ key: 'withProcessedVideo',
976
+ label: t('videoPipeline.withProcessedVideo'),
977
+ value: data.videos.withProcessedVideo,
978
+ total: videoCount,
979
+ color: '#d946ef',
980
+ bgColor: 'bg-fuchsia-500/10',
981
+ textColor: 'text-fuchsia-600 dark:text-fuchsia-400',
982
+ icon: Sparkles,
983
+ },
984
+ ] as const
985
+ ).map((stat) => {
986
+ const pct =
987
+ stat.total > 0
988
+ ? Math.round((stat.value / stat.total) * 100)
989
+ : stat.key === 'total'
990
+ ? 100
991
+ : 0;
992
+ const Icon = stat.icon;
993
+ return (
994
+ <div
995
+ key={stat.key}
996
+ className="flex flex-col gap-2 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/60 hover:bg-background/70 hover:shadow-sm"
997
+ >
998
+ <div className="flex items-center gap-3">
999
+ <div
1000
+ className={`flex size-8 shrink-0 items-center justify-center rounded-lg ${stat.bgColor}`}
1001
+ >
1002
+ <Icon className={`size-4 ${stat.textColor}`} />
1003
+ </div>
1004
+ <div className="flex flex-1 items-center justify-between">
1005
+ <span className="text-sm font-medium text-foreground">
1006
+ {stat.label}
1007
+ </span>
1008
+ <div className="flex items-center gap-2">
1009
+ <span className="font-mono text-xs text-muted-foreground">
1010
+ {stat.value}/{stat.total}
1011
+ </span>
1012
+ <span
1013
+ className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
1014
+ style={{
1015
+ backgroundColor: `${stat.color}20`,
1016
+ color: stat.color,
1017
+ }}
1018
+ >
1019
+ {pct}%
1020
+ </span>
1021
+ </div>
1022
+ </div>
1023
+ </div>
1024
+ <div className="ml-11 h-2 w-full overflow-hidden rounded-full bg-muted">
1025
+ <div
1026
+ className="h-full rounded-full transition-all duration-700 ease-out"
1027
+ style={{
1028
+ width: `${pct}%`,
1029
+ backgroundColor: stat.color,
1030
+ }}
1031
+ />
1032
+ </div>
1033
+ </div>
1034
+ );
1035
+ })}
1036
+ </CardContent>
1037
+ </Card>
1038
+ )}
1039
+
929
1040
  {/* Video coverage */}
930
1041
  <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
931
- <CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/12 via-cyan-500/6 to-transparent pt-2.5 pb-1.5!">
1042
+ <CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
932
1043
  <div className="flex flex-wrap items-start justify-between gap-3">
933
1044
  <div className="flex min-w-0 items-start gap-3">
934
1045
  <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
935
1046
  <Clapperboard className="size-4" />
936
1047
  </div>
937
1048
  <div className="min-w-0">
938
- <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
1049
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
939
1050
  Video coverage
940
1051
  </div>
941
1052
  <CardTitle className="mt-1 text-sm font-semibold">
@@ -946,7 +1057,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
946
1057
  </CardDescription>
947
1058
  </div>
948
1059
  </div>
949
- <div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
1060
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
950
1061
  {videoCount} vídeos
951
1062
  </div>
952
1063
  </div>
@@ -1051,14 +1162,14 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
1051
1162
  <div className="grid gap-3 xl:grid-cols-2">
1052
1163
  {xpData.areas.length > 0 && (
1053
1164
  <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
1054
- <CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/10 via-cyan-500/5 to-transparent pt-2.5 pb-1.5!">
1165
+ <CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
1055
1166
  <div className="flex flex-wrap items-start justify-between gap-3">
1056
1167
  <div className="flex min-w-0 items-start gap-3">
1057
1168
  <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
1058
1169
  <Sparkles className="size-4" />
1059
1170
  </div>
1060
1171
  <div className="min-w-0">
1061
- <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
1172
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
1062
1173
  Macro areas
1063
1174
  </div>
1064
1175
  <CardTitle className="mt-1 text-sm font-semibold">
@@ -1069,7 +1180,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
1069
1180
  </CardDescription>
1070
1181
  </div>
1071
1182
  </div>
1072
- <div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
1183
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
1073
1184
  {xpData.areas.length} áreas
1074
1185
  </div>
1075
1186
  </div>
@@ -1078,7 +1189,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
1078
1189
  {xpData.areas.map((area, index) => (
1079
1190
  <div
1080
1191
  key={area.xpAreaId}
1081
- className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-teal-500/20 hover:bg-background/95 hover:shadow-md"
1192
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:bg-background/95 hover:shadow-md"
1082
1193
  >
1083
1194
  <div
1084
1195
  className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"
@@ -1109,14 +1220,14 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
1109
1220
 
1110
1221
  {xpData.skills.length > 0 && (
1111
1222
  <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
1112
- <CardHeader className="border-b border-border/70 bg-linear-to-r from-orange-500/10 via-amber-500/5 to-transparent pt-2.5 pb-1.5!">
1223
+ <CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
1113
1224
  <div className="flex flex-wrap items-start justify-between gap-3">
1114
1225
  <div className="flex min-w-0 items-start gap-3">
1115
1226
  <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-orange-500/10 text-orange-600 ring-1 ring-inset ring-orange-500/15 dark:text-orange-400">
1116
1227
  <Target className="size-4" />
1117
1228
  </div>
1118
1229
  <div className="min-w-0">
1119
- <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-orange-600/80 dark:text-orange-400/80">
1230
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
1120
1231
  Skills
1121
1232
  </div>
1122
1233
  <CardTitle className="mt-1 text-sm font-semibold">
@@ -1127,7 +1238,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
1127
1238
  </CardDescription>
1128
1239
  </div>
1129
1240
  </div>
1130
- <div className="rounded-full border border-orange-500/15 bg-orange-500/10 px-2.5 py-1 text-[10px] font-semibold text-orange-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-orange-500/15 hover:shadow-md dark:text-orange-300">
1241
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
1131
1242
  {xpData.skills.length} skills
1132
1243
  </div>
1133
1244
  </div>
@@ -1136,7 +1247,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
1136
1247
  {xpData.skills.map((skill, index) => (
1137
1248
  <div
1138
1249
  key={skill.xpSkillId}
1139
- className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-orange-500/20 hover:bg-background/95 hover:shadow-md"
1250
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:bg-background/95 hover:shadow-md"
1140
1251
  >
1141
1252
  <div
1142
1253
  className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"