@hed-hog/lms 0.0.306 → 0.0.310

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 (120) hide show
  1. package/dist/course/course-structure.controller.d.ts +60 -0
  2. package/dist/course/course-structure.controller.d.ts.map +1 -1
  3. package/dist/course/course-structure.controller.js +79 -0
  4. package/dist/course/course-structure.controller.js.map +1 -1
  5. package/dist/course/course-structure.service.d.ts +61 -1
  6. package/dist/course/course-structure.service.d.ts.map +1 -1
  7. package/dist/course/course-structure.service.js +326 -1
  8. package/dist/course/course-structure.service.js.map +1 -1
  9. package/dist/course/course.controller.d.ts +52 -4
  10. package/dist/course/course.controller.d.ts.map +1 -1
  11. package/dist/course/course.service.d.ts +52 -5
  12. package/dist/course/course.service.d.ts.map +1 -1
  13. package/dist/course/course.service.js +78 -57
  14. package/dist/course/course.service.js.map +1 -1
  15. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  16. package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
  17. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  18. package/dist/course/dto/create-course.dto.d.ts +1 -1
  19. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  20. package/dist/course/dto/create-course.dto.js +4 -1
  21. package/dist/course/dto/create-course.dto.js.map +1 -1
  22. package/dist/course/dto/move-lesson.dto.d.ts +10 -0
  23. package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
  24. package/dist/course/dto/move-lesson.dto.js +28 -0
  25. package/dist/course/dto/move-lesson.dto.js.map +1 -0
  26. package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
  27. package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
  28. package/dist/course/dto/paste-lessons.dto.js +24 -0
  29. package/dist/course/dto/paste-lessons.dto.js.map +1 -0
  30. package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
  31. package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
  32. package/dist/course/dto/reorder-lessons.dto.js +24 -0
  33. package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
  34. package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
  35. package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
  36. package/dist/course/dto/reorder-sessions.dto.js +24 -0
  37. package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
  38. package/dist/training/training.controller.js +1 -1
  39. package/dist/training/training.controller.js.map +1 -1
  40. package/hedhog/data/image_type.yaml +20 -0
  41. package/hedhog/data/menu.yaml +2 -2
  42. package/hedhog/data/route.yaml +60 -6
  43. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
  44. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
  45. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
  46. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +10 -1
  47. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -3
  48. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +32 -0
  49. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
  50. package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
  51. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
  52. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
  53. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
  54. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
  55. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
  58. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
  59. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
  60. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
  61. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  62. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  69. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  70. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
  71. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
  93. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
  94. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
  95. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
  97. package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
  98. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
  99. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
  100. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
  101. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
  102. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
  103. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
  104. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
  105. package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
  106. package/hedhog/frontend/messages/en.json +91 -11
  107. package/hedhog/frontend/messages/pt.json +91 -11
  108. package/hedhog/table/course.yaml +1 -1
  109. package/hedhog/table/image_type.yaml +14 -0
  110. package/package.json +7 -7
  111. package/src/course/course-structure.controller.ts +63 -0
  112. package/src/course/course-structure.service.ts +390 -3
  113. package/src/course/course.service.ts +59 -27
  114. package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
  115. package/src/course/dto/create-course.dto.ts +4 -1
  116. package/src/course/dto/move-lesson.dto.ts +17 -0
  117. package/src/course/dto/paste-lessons.dto.ts +9 -0
  118. package/src/course/dto/reorder-lessons.dto.ts +10 -0
  119. package/src/course/dto/reorder-sessions.dto.ts +10 -0
  120. package/src/training/training.controller.ts +1 -1
@@ -0,0 +1,174 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Separator } from '@/components/ui/separator';
5
+ import { cn } from '@/lib/utils';
6
+ import {
7
+ Clock,
8
+ FileText,
9
+ HelpCircle,
10
+ Layers,
11
+ Video,
12
+ type LucideIcon,
13
+ } from 'lucide-react';
14
+ import { useStructureStore } from './store';
15
+ import type { Lesson, LessonType } from './types';
16
+
17
+ const LESSON_TYPE_CONFIG: Record<
18
+ LessonType,
19
+ { icon: LucideIcon; color: string; label: string; bg: string }
20
+ > = {
21
+ video: { icon: Video, color: 'text-blue-500', label: 'Vídeo', bg: 'bg-blue-500/10' },
22
+ post: { icon: FileText, color: 'text-emerald-500', label: 'Post', bg: 'bg-emerald-500/10' },
23
+ questao: { icon: HelpCircle, color: 'text-amber-500', label: 'Quiz', bg: 'bg-amber-500/10' },
24
+ exercicio: { icon: Layers, color: 'text-rose-500', label: 'Exercício', bg: 'bg-rose-500/10' },
25
+ };
26
+
27
+ interface DetailSessionProps {
28
+ sessionId: string;
29
+ }
30
+
31
+ export function DetailSession({ sessionId }: DetailSessionProps) {
32
+ const session = useStructureStore((s) => s.sessions.find((ss) => ss.id === sessionId));
33
+ const lessons = useStructureStore((s) => s.lessons.filter((l) => l.sessionId === sessionId).sort((a, b) => a.order - b.order));
34
+ const selectItem = useStructureStore((s) => s.selectItem);
35
+
36
+ if (!session) return null;
37
+
38
+ const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
39
+ const hours = Math.floor(totalMinutes / 60);
40
+ const minutes = totalMinutes % 60;
41
+
42
+ const typeCounts = lessons.reduce<Partial<Record<LessonType, number>>>(
43
+ (acc, l) => ({ ...acc, [l.type]: (acc[l.type] ?? 0) + 1 }),
44
+ {}
45
+ );
46
+
47
+ return (
48
+ <div className="flex flex-col overflow-y-auto h-full">
49
+ {/* Header */}
50
+ <div className="flex items-center gap-3 px-4 py-4 border-b bg-muted/30 shrink-0">
51
+ <div className="flex size-10 items-center justify-center rounded-lg bg-muted shrink-0">
52
+ <Layers className="size-5 text-muted-foreground" />
53
+ </div>
54
+ <div className="min-w-0 flex-1">
55
+ <h2 className="text-base font-semibold truncate">{session.title}</h2>
56
+ <p className="text-xs text-muted-foreground">{session.code}</p>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="flex flex-col gap-5 p-4">
61
+ {/* Stats */}
62
+ <div className="grid grid-cols-2 gap-3">
63
+ <StatCard label="Aulas" value={lessons.length} />
64
+ <StatCard
65
+ label="Duração"
66
+ value={hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`}
67
+ />
68
+ </div>
69
+
70
+ {/* Type breakdown */}
71
+ {Object.entries(typeCounts).length > 0 && (
72
+ <>
73
+ <Separator />
74
+ <div>
75
+ <p className="text-xs font-medium text-muted-foreground mb-2">
76
+ Tipos de aula
77
+ </p>
78
+ <div className="flex flex-wrap gap-2">
79
+ {(Object.entries(typeCounts) as [LessonType, number][]).map(
80
+ ([type, count]) => {
81
+ const cfg = LESSON_TYPE_CONFIG[type];
82
+ const Icon = cfg.icon;
83
+ return (
84
+ <div
85
+ key={type}
86
+ className={cn(
87
+ 'flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs',
88
+ cfg.bg
89
+ )}
90
+ >
91
+ <Icon className={cn('size-3', cfg.color)} />
92
+ <span className={cfg.color}>{cfg.label}</span>
93
+ <span className="font-semibold">{count}</span>
94
+ </div>
95
+ );
96
+ }
97
+ )}
98
+ </div>
99
+ </div>
100
+ </>
101
+ )}
102
+
103
+ {/* Lesson list */}
104
+ {lessons.length > 0 && (
105
+ <>
106
+ <Separator />
107
+ <div>
108
+ <p className="text-xs font-medium text-muted-foreground mb-2">
109
+ Aulas
110
+ </p>
111
+ <div className="flex flex-col gap-0.5">
112
+ {lessons.map((lesson) => (
113
+ <LessonListItem
114
+ key={lesson.id}
115
+ lesson={lesson}
116
+ onSelect={() => selectItem(lesson.id, 'lesson')}
117
+ />
118
+ ))}
119
+ </div>
120
+ </div>
121
+ </>
122
+ )}
123
+
124
+ {lessons.length === 0 && (
125
+ <div className="flex flex-col items-center gap-2 py-8 text-center">
126
+ <Layers className="size-8 text-muted-foreground/40" />
127
+ <p className="text-sm text-muted-foreground">
128
+ Esta sessão ainda não tem aulas.
129
+ </p>
130
+ </div>
131
+ )}
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ function LessonListItem({
138
+ lesson,
139
+ onSelect,
140
+ }: {
141
+ lesson: Lesson;
142
+ onSelect: () => void;
143
+ }) {
144
+ const cfg = LESSON_TYPE_CONFIG[lesson.type];
145
+ const Icon = cfg.icon;
146
+
147
+ return (
148
+ <button
149
+ onClick={onSelect}
150
+ className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-muted/60 cursor-pointer transition-colors text-left w-full"
151
+ >
152
+ <Icon className={cn('size-3.5 shrink-0', cfg.color)} />
153
+ <span className="text-sm flex-1 truncate">{lesson.title}</span>
154
+ <div className="flex items-center gap-1 shrink-0">
155
+ <Clock className="size-3 text-muted-foreground" />
156
+ <span className="text-[0.6rem] text-muted-foreground">
157
+ {lesson.duration}min
158
+ </span>
159
+ </div>
160
+ <Badge variant="outline" className="text-[0.55rem] px-1 py-0 h-4 shrink-0">
161
+ {lesson.code}
162
+ </Badge>
163
+ </button>
164
+ );
165
+ }
166
+
167
+ function StatCard({ label, value }: { label: string; value: string | number }) {
168
+ return (
169
+ <div className="flex flex-col items-center rounded-lg border bg-muted/30 py-3 gap-0.5">
170
+ <span className="text-lg font-bold tabular-nums">{value}</span>
171
+ <span className="text-[0.65rem] text-muted-foreground">{label}</span>
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import type { DraggableAttributes } from '@dnd-kit/core';
5
+ import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
6
+ import { GripVertical } from 'lucide-react';
7
+
8
+ interface DragHandleProps {
9
+ listeners?: SyntheticListenerMap;
10
+ attributes?: DraggableAttributes;
11
+ disabled?: boolean;
12
+ className?: string;
13
+ }
14
+
15
+ /**
16
+ * Grip handle for drag-and-drop.
17
+ * Attach the dnd-kit `listeners` and `attributes` to this element so that
18
+ * clicking elsewhere (expand button, context menu, etc.) does NOT start a drag.
19
+ */
20
+ export function DragHandle({
21
+ listeners,
22
+ attributes,
23
+ disabled,
24
+ className,
25
+ }: DragHandleProps) {
26
+ if (disabled) {
27
+ return (
28
+ <span
29
+ className={cn(
30
+ 'shrink-0 size-5 flex items-center justify-center opacity-20 cursor-not-allowed',
31
+ className
32
+ )}
33
+ title="Limpe a busca para reordenar"
34
+ >
35
+ <GripVertical className="size-3.5" />
36
+ </span>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <span
42
+ {...listeners}
43
+ {...attributes}
44
+ className={cn(
45
+ 'shrink-0 size-5 flex items-center justify-center rounded',
46
+ 'text-muted-foreground/40 hover:text-muted-foreground',
47
+ 'cursor-grab active:cursor-grabbing',
48
+ 'transition-colors touch-none',
49
+ className
50
+ )}
51
+ title="Arrastar para reordenar"
52
+ // Prevent click events from bubbling to the row selection handler
53
+ onClick={(e) => e.stopPropagation()}
54
+ >
55
+ <GripVertical className="size-3.5" />
56
+ </span>
57
+ );
58
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { DragOverlay as DndDragOverlay } from '@dnd-kit/core';
4
+ import { cn } from '@/lib/utils';
5
+ import { GripVertical, Layers, Video, FileText, HelpCircle } from 'lucide-react';
6
+ import type { FlatItem } from './types';
7
+
8
+ interface DragOverlayContentProps {
9
+ item: FlatItem;
10
+ }
11
+
12
+ function DragOverlayContent({ item }: DragOverlayContentProps) {
13
+ const isSession = item.type === 'session';
14
+ const isLesson = item.type === 'lesson';
15
+
16
+ const lessonIconMap = {
17
+ video: <Video className="size-3.5 text-blue-500 shrink-0" />,
18
+ post: <FileText className="size-3.5 text-emerald-500 shrink-0" />,
19
+ questao: <HelpCircle className="size-3.5 text-amber-500 shrink-0" />,
20
+ exercicio: <Layers className="size-3.5 text-rose-500 shrink-0" />,
21
+ } as const;
22
+
23
+ return (
24
+ <div
25
+ className={cn(
26
+ 'flex items-center gap-1.5 px-2 py-1.5 rounded-md text-sm',
27
+ 'bg-card border shadow-lg ring-1 ring-primary/20',
28
+ 'opacity-95 max-w-[260px]',
29
+ isLesson && 'pl-4'
30
+ )}
31
+ >
32
+ <GripVertical className="size-3.5 text-muted-foreground/60 shrink-0" />
33
+
34
+ {isSession && <Layers className="size-3.5 text-muted-foreground shrink-0" />}
35
+ {isLesson && item.type === 'lesson' && lessonIconMap[item.data.type]}
36
+
37
+ <span className="truncate font-medium text-xs">{item.data.title}</span>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ interface TreeDragOverlayProps {
43
+ activeItem: FlatItem | null;
44
+ }
45
+
46
+ export function TreeDragOverlay({ activeItem }: TreeDragOverlayProps) {
47
+ return (
48
+ <DndDragOverlay dropAnimation={{ duration: 150, easing: 'ease' }}>
49
+ {activeItem ? <DragOverlayContent item={activeItem} /> : null}
50
+ </DndDragOverlay>
51
+ );
52
+ }
@@ -0,0 +1,276 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Eye,
5
+ EyeOff,
6
+ FolderInput,
7
+ Layers,
8
+ Lock,
9
+ Save,
10
+ Video,
11
+ X,
12
+ } from 'lucide-react';
13
+ import { useState } from 'react';
14
+ import { toast } from 'sonner';
15
+
16
+ import { Badge } from '@/components/ui/badge';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
19
+ import { ScrollArea } from '@/components/ui/scroll-area';
20
+ import {
21
+ Select,
22
+ SelectContent,
23
+ SelectItem,
24
+ SelectTrigger,
25
+ SelectValue,
26
+ } from '@/components/ui/select';
27
+ import { Separator } from '@/components/ui/separator';
28
+ import { cn } from '@/lib/utils';
29
+
30
+ import { useStructureStore } from './store';
31
+ import type { LessonStatus, Visibility } from './types';
32
+
33
+ // ── Config ────────────────────────────────────────────────────────────────────
34
+
35
+ const STATUS_LABELS: Record<LessonStatus, string> = {
36
+ preparada: 'Preparada',
37
+ gravada: 'Gravada',
38
+ editada: 'Editada',
39
+ finalizada: 'Finalizada',
40
+ publicada: 'Publicada',
41
+ };
42
+
43
+ // ── Component ─────────────────────────────────────────────────────────────────
44
+
45
+ export function EditorBulk() {
46
+ const selectedIds = useStructureStore((s) => s.selectedIds);
47
+ const sessions = useStructureStore((s) => s.sessions);
48
+ const lessons = useStructureStore((s) => s.lessons);
49
+ const clearSelection = useStructureStore((s) => s.clearSelection);
50
+
51
+ const selectedArray = [...selectedIds];
52
+ const lessonIds = selectedArray.filter((id) =>
53
+ lessons.some((l) => l.id === id)
54
+ );
55
+ const sessionIds = selectedArray.filter((id) =>
56
+ sessions.some((s) => s.id === id)
57
+ );
58
+
59
+ const allLessons = lessonIds.length === selectedArray.length;
60
+ const allSessions = sessionIds.length === selectedArray.length;
61
+
62
+ // Local form state (mock — not wired to store actions yet)
63
+ const [status, setStatus] = useState<LessonStatus | ''>('');
64
+ const [visibility, setVisibility] = useState<Visibility | ''>('');
65
+ const [targetSession, setTargetSession] = useState<string>('');
66
+
67
+ function handleSave() {
68
+ const lines: string[] = [];
69
+ if (status) lines.push(`status → ${STATUS_LABELS[status as LessonStatus]}`);
70
+ if (visibility) lines.push(`visibilidade → ${visibility}`);
71
+ if (targetSession)
72
+ lines.push(
73
+ `mover para sessão ${sessions.find((s) => s.id === targetSession)?.title ?? targetSession}`
74
+ );
75
+ toast.success(
76
+ lines.length
77
+ ? `Edição em massa: ${lines.join(', ')} (mock)`
78
+ : 'Nenhuma alteração selecionada'
79
+ );
80
+ }
81
+
82
+ return (
83
+ <div className="flex flex-col h-full min-h-0">
84
+ {/* ── Header ───────────────────────────────────────────────────────── */}
85
+ <div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
86
+ <div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 shrink-0">
87
+ {allLessons ? (
88
+ <Video className="size-4 text-primary" />
89
+ ) : allSessions ? (
90
+ <Layers className="size-4 text-primary" />
91
+ ) : (
92
+ <Layers className="size-4 text-primary" />
93
+ )}
94
+ </div>
95
+ <div className="flex-1 min-w-0">
96
+ <p className="text-sm font-semibold">Edição em massa</p>
97
+ <p className="text-[0.65rem] text-muted-foreground">
98
+ {selectedIds.size}{' '}
99
+ {allLessons ? 'aulas' : allSessions ? 'sessões' : 'itens'}{' '}
100
+ selecionados
101
+ </p>
102
+ </div>
103
+ <Button
104
+ variant="ghost"
105
+ size="icon"
106
+ className="size-7 shrink-0"
107
+ onClick={clearSelection}
108
+ title="Limpar seleção"
109
+ >
110
+ <X className="size-3.5" />
111
+ </Button>
112
+ </div>
113
+
114
+ {/* ── Body ─────────────────────────────────────────────────────────── */}
115
+ <ScrollArea className="flex-1 min-h-0">
116
+ <div className="flex flex-col gap-3 p-3">
117
+ {/* Badges dos itens selecionados */}
118
+ <div className="flex flex-wrap gap-1">
119
+ {selectedArray.slice(0, 12).map((id) => {
120
+ const lesson = lessons.find((l) => l.id === id);
121
+ const session = sessions.find((s) => s.id === id);
122
+ const label = lesson?.code ?? session?.code ?? id;
123
+ const isLesson = !!lesson;
124
+ return (
125
+ <Badge
126
+ key={id}
127
+ variant="secondary"
128
+ className={cn(
129
+ 'text-[0.65rem] h-5',
130
+ isLesson
131
+ ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
132
+ : 'bg-muted'
133
+ )}
134
+ >
135
+ {label}
136
+ </Badge>
137
+ );
138
+ })}
139
+ {selectedArray.length > 12 && (
140
+ <Badge variant="outline" className="text-[0.65rem] h-5">
141
+ +{selectedArray.length - 12} mais
142
+ </Badge>
143
+ )}
144
+ </div>
145
+
146
+ {/* Status — só mostra para aulas */}
147
+ {(allLessons || (!allSessions && lessonIds.length > 0)) && (
148
+ <Card>
149
+ <CardHeader className="px-3 pt-3 pb-2">
150
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
151
+ Status de produção
152
+ </CardTitle>
153
+ </CardHeader>
154
+ <CardContent className="px-3 pb-3">
155
+ <Select
156
+ value={status}
157
+ onValueChange={(v) => setStatus(v as LessonStatus)}
158
+ >
159
+ <SelectTrigger className="h-8 text-xs w-full">
160
+ <SelectValue placeholder="Manter atual" />
161
+ </SelectTrigger>
162
+ <SelectContent>
163
+ <SelectItem value="preparada">Preparada</SelectItem>
164
+ <SelectItem value="gravada">Gravada</SelectItem>
165
+ <SelectItem value="editada">Editada</SelectItem>
166
+ <SelectItem value="finalizada">Finalizada</SelectItem>
167
+ <SelectItem value="publicada">Publicada</SelectItem>
168
+ </SelectContent>
169
+ </Select>
170
+ </CardContent>
171
+ </Card>
172
+ )}
173
+
174
+ {/* Visibilidade */}
175
+ <Card>
176
+ <CardHeader className="px-3 pt-3 pb-2">
177
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
178
+ Visibilidade
179
+ </CardTitle>
180
+ </CardHeader>
181
+ <CardContent className="px-3 pb-3">
182
+ <Select
183
+ value={visibility}
184
+ onValueChange={(v) => setVisibility(v as Visibility)}
185
+ >
186
+ <SelectTrigger className="h-8 text-xs w-full">
187
+ <SelectValue placeholder="Manter atual" />
188
+ </SelectTrigger>
189
+ <SelectContent>
190
+ <SelectItem value="publico">
191
+ <span className="flex items-center gap-1.5">
192
+ <Eye className="size-3" /> Público
193
+ </span>
194
+ </SelectItem>
195
+ <SelectItem value="privado">
196
+ <span className="flex items-center gap-1.5">
197
+ <EyeOff className="size-3" /> Privado
198
+ </span>
199
+ </SelectItem>
200
+ <SelectItem value="restrito">
201
+ <span className="flex items-center gap-1.5">
202
+ <Lock className="size-3" /> Restrito
203
+ </span>
204
+ </SelectItem>
205
+ </SelectContent>
206
+ </Select>
207
+ </CardContent>
208
+ </Card>
209
+
210
+ {/* Mover para sessão — só para aulas */}
211
+ {(allLessons || lessonIds.length > 0) && (
212
+ <Card>
213
+ <CardHeader className="px-3 pt-3 pb-2">
214
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
215
+ <FolderInput className="size-3" />
216
+ Mover para sessão
217
+ </CardTitle>
218
+ </CardHeader>
219
+ <CardContent className="px-3 pb-3">
220
+ <Select value={targetSession} onValueChange={setTargetSession}>
221
+ <SelectTrigger className="h-8 text-xs w-full">
222
+ <SelectValue placeholder="Selecionar sessão…" />
223
+ </SelectTrigger>
224
+ <SelectContent>
225
+ {sessions.map((s) => (
226
+ <SelectItem key={s.id} value={s.id}>
227
+ <span className="font-mono mr-1 text-muted-foreground">
228
+ {s.code}
229
+ </span>
230
+ {s.title}
231
+ </SelectItem>
232
+ ))}
233
+ </SelectContent>
234
+ </Select>
235
+ </CardContent>
236
+ </Card>
237
+ )}
238
+
239
+ <div className="rounded-md border border-amber-200 bg-amber-50/60 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2">
240
+ <p className="text-[0.65rem] text-amber-700 dark:text-amber-400">
241
+ As alterações em massa ainda não estão integradas com a API.
242
+ Clique em salvar para ver a pré-visualização.
243
+ </p>
244
+ </div>
245
+ </div>
246
+ </ScrollArea>
247
+
248
+ {/* ── Footer ───────────────────────────────────────────────────────── */}
249
+ <div className="shrink-0 border-t bg-background">
250
+ <Separator />
251
+ <div className="flex items-center gap-2 px-3 py-2">
252
+ <Button
253
+ type="button"
254
+ variant="ghost"
255
+ size="sm"
256
+ className="h-7 text-xs"
257
+ onClick={clearSelection}
258
+ >
259
+ <X className="size-3 mr-1" />
260
+ Limpar seleção
261
+ </Button>
262
+ <div className="flex-1" />
263
+ <Button
264
+ type="button"
265
+ size="sm"
266
+ className="h-7 text-xs"
267
+ onClick={handleSave}
268
+ >
269
+ <Save className="size-3 mr-1" />
270
+ Aplicar (mock)
271
+ </Button>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ );
276
+ }