@hed-hog/lms 0.0.314 → 0.0.316

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 (67) hide show
  1. package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
  2. package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
  3. package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
  4. package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
  5. package/dist/enterprise/enterprise.controller.d.ts +3 -0
  6. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  7. package/dist/enterprise/enterprise.controller.js +14 -0
  8. package/dist/enterprise/enterprise.controller.js.map +1 -1
  9. package/dist/enterprise/enterprise.service.d.ts +3 -0
  10. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  11. package/dist/enterprise/enterprise.service.js +128 -1
  12. package/dist/enterprise/enterprise.service.js.map +1 -1
  13. package/dist/instructor/instructor.controller.d.ts +23 -0
  14. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  15. package/dist/instructor/instructor.controller.js +41 -0
  16. package/dist/instructor/instructor.controller.js.map +1 -1
  17. package/dist/instructor/instructor.service.d.ts +25 -0
  18. package/dist/instructor/instructor.service.d.ts.map +1 -1
  19. package/dist/instructor/instructor.service.js +126 -8
  20. package/dist/instructor/instructor.service.js.map +1 -1
  21. package/hedhog/data/menu.yaml +23 -7
  22. package/hedhog/data/role.yaml +17 -1
  23. package/hedhog/data/route.yaml +48 -0
  24. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
  25. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
  26. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
  27. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
  28. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
  29. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
  30. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
  31. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
  32. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
  33. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
  34. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
  35. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
  36. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
  37. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
  38. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
  39. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
  40. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
  42. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
  43. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
  44. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
  45. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
  51. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
  52. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
  53. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
  54. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
  55. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
  56. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
  57. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
  58. package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
  59. package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
  60. package/hedhog/query/add_route_role.sql +15 -0
  61. package/hedhog/table/enterprise_user.yaml +1 -1
  62. package/package.json +6 -6
  63. package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
  64. package/src/enterprise/enterprise.controller.ts +9 -1
  65. package/src/enterprise/enterprise.service.ts +147 -4
  66. package/src/instructor/instructor.controller.ts +36 -9
  67. package/src/instructor/instructor.service.ts +140 -10
@@ -1,314 +1,314 @@
1
- 'use client';
2
-
3
- import { Badge } from '@/components/ui/badge';
4
- import { Separator } from '@/components/ui/separator';
5
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
6
- import { cn } from '@/lib/utils';
7
- import {
8
- Clock,
9
- ExternalLink,
10
- FileText,
11
- HelpCircle,
12
- Layers,
13
- Paperclip,
14
- Video,
15
- type LucideIcon,
16
- } from 'lucide-react';
17
- import { useStructureStore } from './store';
18
- import type { LessonType } from './types';
19
-
20
- const LESSON_TYPE_CONFIG: Record<
21
- LessonType,
22
- { icon: LucideIcon; color: string; bg: string; label: string }
23
- > = {
24
- video: {
25
- icon: Video,
26
- color: 'text-blue-500',
27
- bg: 'bg-blue-500/10',
28
- label: 'Vídeo',
29
- },
30
- post: {
31
- icon: FileText,
32
- color: 'text-emerald-500',
33
- bg: 'bg-emerald-500/10',
34
- label: 'Post',
35
- },
36
- questao: {
37
- icon: HelpCircle,
38
- color: 'text-amber-500',
39
- bg: 'bg-amber-500/10',
40
- label: 'Quiz',
41
- },
42
- exercicio: {
43
- icon: Layers,
44
- color: 'text-rose-500',
45
- bg: 'bg-rose-500/10',
46
- label: 'Exercício',
47
- },
48
- };
49
-
50
- const VIDEO_PROVIDER_LABELS: Record<string, string> = {
51
- youtube: 'YouTube',
52
- vimeo: 'Vimeo',
53
- panda: 'Panda Video',
54
- bunny: 'Bunny.net',
55
- outros: 'Outros',
56
- };
57
-
58
- interface DetailLessonProps {
59
- lessonId: string;
60
- }
61
-
62
- export function DetailLesson({ lessonId }: DetailLessonProps) {
63
- const lesson = useStructureStore((s) =>
64
- s.lessons.find((l) => l.id === lessonId)
65
- );
66
-
67
- if (!lesson) return null;
68
-
69
- const cfg = LESSON_TYPE_CONFIG[lesson.type];
70
- const Icon = cfg.icon;
71
-
72
- return (
73
- <div className="flex flex-col h-full overflow-hidden">
74
- {/* Header */}
75
- <div className="flex items-center gap-3 px-4 py-4 border-b bg-muted/30 shrink-0">
76
- <div
77
- className={cn(
78
- 'flex size-10 items-center justify-center rounded-lg shrink-0',
79
- cfg.bg
80
- )}
81
- >
82
- <Icon className={cn('size-5', cfg.color)} />
83
- </div>
84
- <div className="min-w-0 flex-1">
85
- <h2 className="text-base font-semibold truncate">{lesson.title}</h2>
86
- <p className="text-xs text-muted-foreground">{lesson.code}</p>
87
- </div>
88
- <div className="flex items-center gap-1.5 shrink-0">
89
- <Badge variant="outline" className={cn('text-xs', cfg.color)}>
90
- {cfg.label}
91
- </Badge>
92
- <Badge variant="secondary" className="gap-1 text-xs">
93
- <Clock className="size-3" />
94
- {lesson.duration}min
95
- </Badge>
96
- </div>
97
- </div>
98
-
99
- {/* Tabs */}
100
- <Tabs defaultValue="dados" className="flex flex-col flex-1 min-h-0">
101
- <TabsList className="mx-4 mt-3 w-auto justify-start shrink-0">
102
- <TabsTrigger value="dados">Dados</TabsTrigger>
103
- <TabsTrigger value="transcription">Transcrição</TabsTrigger>
104
- <TabsTrigger value="resources">Recursos</TabsTrigger>
105
- </TabsList>
106
-
107
- {/* ── Dados ─────────────────────────────────────────────────────────── */}
108
- <TabsContent
109
- value="dados"
110
- className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
111
- >
112
- <div className="flex flex-col gap-4">
113
- {lesson.publicDescription && (
114
- <Section title="Descrição pública">
115
- <p className="text-sm leading-relaxed">
116
- {lesson.publicDescription}
117
- </p>
118
- </Section>
119
- )}
120
-
121
- {lesson.privateDescription && (
122
- <>
123
- <Separator />
124
- <Section title="Descrição interna">
125
- <p className="text-sm leading-relaxed text-muted-foreground">
126
- {lesson.privateDescription}
127
- </p>
128
- </Section>
129
- </>
130
- )}
131
-
132
- {/* Video */}
133
- {lesson.type === 'video' && (
134
- <>
135
- <Separator />
136
- <Section title="Configurações de vídeo">
137
- <div className="flex flex-col gap-2">
138
- {lesson.videoProvider && (
139
- <InfoRow
140
- label="Plataforma"
141
- value={
142
- VIDEO_PROVIDER_LABELS[lesson.videoProvider] ??
143
- lesson.videoProvider
144
- }
145
- />
146
- )}
147
- {lesson.videoUrl && (
148
- <div className="flex items-start gap-2">
149
- <span className="text-xs text-muted-foreground w-20 shrink-0 pt-0.5">
150
- URL
151
- </span>
152
- <a
153
- href={lesson.videoUrl}
154
- target="_blank"
155
- rel="noopener noreferrer"
156
- className="text-sm text-primary hover:underline flex items-center gap-1 truncate"
157
- >
158
- {lesson.videoUrl}
159
- <ExternalLink className="size-3 shrink-0" />
160
- </a>
161
- </div>
162
- )}
163
- {lesson.autoDuration !== undefined && (
164
- <InfoRow
165
- label="Duração auto"
166
- value={lesson.autoDuration ? 'Sim' : 'Não'}
167
- />
168
- )}
169
- </div>
170
- </Section>
171
- </>
172
- )}
173
-
174
- {/* Post */}
175
- {lesson.type === 'post' && lesson.postContent && (
176
- <>
177
- <Separator />
178
- <Section title="Conteúdo do post">
179
- <p className="text-sm leading-relaxed whitespace-pre-wrap">
180
- {lesson.postContent}
181
- </p>
182
- </Section>
183
- </>
184
- )}
185
-
186
- {/* Questão / exame vinculado */}
187
- {lesson.type === 'questao' && lesson.linkedExam && (
188
- <>
189
- <Separator />
190
- <Section title="Exame vinculado">
191
- <div className="flex items-center gap-2 text-sm">
192
- <HelpCircle className="size-3.5 text-amber-500 shrink-0" />
193
- <span>{lesson.linkedExam}</span>
194
- </div>
195
- </Section>
196
- </>
197
- )}
198
-
199
- {!lesson.publicDescription &&
200
- !lesson.privateDescription &&
201
- lesson.type === 'exercicio' && (
202
- <div className="flex flex-col items-center gap-2 py-8 text-center">
203
- <Layers className="size-8 text-muted-foreground/40" />
204
- <p className="text-sm text-muted-foreground">
205
- Sem informações adicionais nesta aula.
206
- </p>
207
- </div>
208
- )}
209
- </div>
210
- </TabsContent>
211
-
212
- {/* ── Transcrição ───────────────────────────────────────────────────── */}
213
- <TabsContent
214
- value="transcription"
215
- className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
216
- >
217
- {lesson.transcription ? (
218
- <div className="rounded-md border bg-muted/20 p-3">
219
- <p className="text-sm leading-relaxed whitespace-pre-wrap font-mono">
220
- {lesson.transcription}
221
- </p>
222
- </div>
223
- ) : (
224
- <EmptyState
225
- icon={<FileText className="size-8 text-muted-foreground/40" />}
226
- >
227
- Transcrição não disponível para esta aula.
228
- </EmptyState>
229
- )}
230
- </TabsContent>
231
-
232
- {/* ── Recursos ──────────────────────────────────────────────────────── */}
233
- <TabsContent
234
- value="resources"
235
- className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
236
- >
237
- {lesson.resources && lesson.resources.length > 0 ? (
238
- <div className="flex flex-col gap-2">
239
- {lesson.resources.map((res) => (
240
- <div
241
- key={res.id}
242
- className="flex items-center gap-2 rounded-md border px-3 py-2"
243
- >
244
- <Paperclip className="size-3.5 text-muted-foreground shrink-0" />
245
- <span className="flex-1 text-sm truncate">{res.name}</span>
246
- {res.size && (
247
- <span className="text-[0.6rem] text-muted-foreground shrink-0">
248
- {res.size}
249
- </span>
250
- )}
251
- <Badge
252
- variant={res.public ? 'default' : 'secondary'}
253
- className="text-[0.6rem] px-1.5 py-0 h-4 shrink-0"
254
- >
255
- {res.public ? 'Público' : 'Privado'}
256
- </Badge>
257
- </div>
258
- ))}
259
- </div>
260
- ) : (
261
- <EmptyState
262
- icon={<Paperclip className="size-8 text-muted-foreground/40" />}
263
- >
264
- Nenhum recurso anexado a esta aula.
265
- </EmptyState>
266
- )}
267
- </TabsContent>
268
- </Tabs>
269
- </div>
270
- );
271
- }
272
-
273
- // ── Local helpers ─────────────────────────────────────────────────────────────
274
-
275
- function Section({
276
- title,
277
- children,
278
- }: {
279
- title: string;
280
- children: React.ReactNode;
281
- }) {
282
- return (
283
- <div className="flex flex-col gap-1.5">
284
- <p className="text-xs font-medium text-muted-foreground">{title}</p>
285
- {children}
286
- </div>
287
- );
288
- }
289
-
290
- function InfoRow({ label, value }: { label: string; value: string }) {
291
- return (
292
- <div className="flex items-center gap-2">
293
- <span className="text-xs text-muted-foreground w-20 shrink-0">
294
- {label}
295
- </span>
296
- <span className="text-sm">{value}</span>
297
- </div>
298
- );
299
- }
300
-
301
- function EmptyState({
302
- icon,
303
- children,
304
- }: {
305
- icon: React.ReactNode;
306
- children: React.ReactNode;
307
- }) {
308
- return (
309
- <div className="flex flex-col items-center gap-2 py-12 text-center">
310
- {icon}
311
- <p className="text-sm text-muted-foreground">{children}</p>
312
- </div>
313
- );
314
- }
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Separator } from '@/components/ui/separator';
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
6
+ import { cn } from '@/lib/utils';
7
+ import {
8
+ Clock,
9
+ ExternalLink,
10
+ FileText,
11
+ HelpCircle,
12
+ Layers,
13
+ Paperclip,
14
+ Video,
15
+ type LucideIcon,
16
+ } from 'lucide-react';
17
+ import { useStructureStore } from './store';
18
+ import type { LessonType } from './types';
19
+
20
+ const LESSON_TYPE_CONFIG: Record<
21
+ LessonType,
22
+ { icon: LucideIcon; color: string; bg: string; label: string }
23
+ > = {
24
+ video: {
25
+ icon: Video,
26
+ color: 'text-blue-500',
27
+ bg: 'bg-blue-500/10',
28
+ label: 'Vídeo',
29
+ },
30
+ post: {
31
+ icon: FileText,
32
+ color: 'text-emerald-500',
33
+ bg: 'bg-emerald-500/10',
34
+ label: 'Post',
35
+ },
36
+ questao: {
37
+ icon: HelpCircle,
38
+ color: 'text-amber-500',
39
+ bg: 'bg-amber-500/10',
40
+ label: 'Quiz',
41
+ },
42
+ exercicio: {
43
+ icon: Layers,
44
+ color: 'text-rose-500',
45
+ bg: 'bg-rose-500/10',
46
+ label: 'Exercício',
47
+ },
48
+ };
49
+
50
+ const VIDEO_PROVIDER_LABELS: Record<string, string> = {
51
+ youtube: 'YouTube',
52
+ vimeo: 'Vimeo',
53
+ panda: 'Panda Video',
54
+ bunny: 'Bunny.net',
55
+ outros: 'Outros',
56
+ };
57
+
58
+ interface DetailLessonProps {
59
+ lessonId: string;
60
+ }
61
+
62
+ export function DetailLesson({ lessonId }: DetailLessonProps) {
63
+ const lesson = useStructureStore((s) =>
64
+ s.lessons.find((l) => l.id === lessonId)
65
+ );
66
+
67
+ if (!lesson) return null;
68
+
69
+ const cfg = LESSON_TYPE_CONFIG[lesson.type];
70
+ const Icon = cfg.icon;
71
+
72
+ return (
73
+ <div className="flex flex-col h-full overflow-hidden">
74
+ {/* Header */}
75
+ <div className="flex items-center gap-3 px-4 py-4 border-b bg-muted/30 shrink-0">
76
+ <div
77
+ className={cn(
78
+ 'flex size-10 items-center justify-center rounded-lg shrink-0',
79
+ cfg.bg
80
+ )}
81
+ >
82
+ <Icon className={cn('size-5', cfg.color)} />
83
+ </div>
84
+ <div className="min-w-0 flex-1">
85
+ <h2 className="text-base font-semibold truncate">{lesson.title}</h2>
86
+ <p className="text-xs text-muted-foreground">{lesson.code}</p>
87
+ </div>
88
+ <div className="flex items-center gap-1.5 shrink-0">
89
+ <Badge variant="outline" className={cn('text-xs', cfg.color)}>
90
+ {cfg.label}
91
+ </Badge>
92
+ <Badge variant="secondary" className="gap-1 text-xs">
93
+ <Clock className="size-3" />
94
+ {lesson.duration}min
95
+ </Badge>
96
+ </div>
97
+ </div>
98
+
99
+ {/* Tabs */}
100
+ <Tabs defaultValue="dados" className="flex flex-col flex-1 min-h-0">
101
+ <TabsList className="mx-4 mt-3 w-auto justify-start shrink-0">
102
+ <TabsTrigger value="dados">Dados</TabsTrigger>
103
+ <TabsTrigger value="transcription">Transcrição</TabsTrigger>
104
+ <TabsTrigger value="resources">Recursos</TabsTrigger>
105
+ </TabsList>
106
+
107
+ {/* ── Dados ─────────────────────────────────────────────────────────── */}
108
+ <TabsContent
109
+ value="dados"
110
+ className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
111
+ >
112
+ <div className="flex flex-col gap-4">
113
+ {lesson.publicDescription && (
114
+ <Section title="Descrição pública">
115
+ <p className="text-sm leading-relaxed">
116
+ {lesson.publicDescription}
117
+ </p>
118
+ </Section>
119
+ )}
120
+
121
+ {lesson.privateDescription && (
122
+ <>
123
+ <Separator />
124
+ <Section title="Descrição interna">
125
+ <p className="text-sm leading-relaxed text-muted-foreground">
126
+ {lesson.privateDescription}
127
+ </p>
128
+ </Section>
129
+ </>
130
+ )}
131
+
132
+ {/* Video */}
133
+ {lesson.type === 'video' && (
134
+ <>
135
+ <Separator />
136
+ <Section title="Configurações de vídeo">
137
+ <div className="flex flex-col gap-2">
138
+ {lesson.videoProvider && (
139
+ <InfoRow
140
+ label="Plataforma"
141
+ value={
142
+ VIDEO_PROVIDER_LABELS[lesson.videoProvider] ??
143
+ lesson.videoProvider
144
+ }
145
+ />
146
+ )}
147
+ {lesson.videoUrl && (
148
+ <div className="flex items-start gap-2">
149
+ <span className="text-xs text-muted-foreground w-20 shrink-0 pt-0.5">
150
+ URL
151
+ </span>
152
+ <a
153
+ href={lesson.videoUrl}
154
+ target="_blank"
155
+ rel="noopener noreferrer"
156
+ className="text-sm text-primary hover:underline flex items-center gap-1 truncate"
157
+ >
158
+ {lesson.videoUrl}
159
+ <ExternalLink className="size-3 shrink-0" />
160
+ </a>
161
+ </div>
162
+ )}
163
+ {lesson.autoDuration !== undefined && (
164
+ <InfoRow
165
+ label="Duração auto"
166
+ value={lesson.autoDuration ? 'Sim' : 'Não'}
167
+ />
168
+ )}
169
+ </div>
170
+ </Section>
171
+ </>
172
+ )}
173
+
174
+ {/* Post */}
175
+ {lesson.type === 'post' && lesson.postContent && (
176
+ <>
177
+ <Separator />
178
+ <Section title="Conteúdo do post">
179
+ <p className="text-sm leading-relaxed whitespace-pre-wrap">
180
+ {lesson.postContent}
181
+ </p>
182
+ </Section>
183
+ </>
184
+ )}
185
+
186
+ {/* Questão / exame vinculado */}
187
+ {lesson.type === 'questao' && lesson.linkedExam && (
188
+ <>
189
+ <Separator />
190
+ <Section title="Exame vinculado">
191
+ <div className="flex items-center gap-2 text-sm">
192
+ <HelpCircle className="size-3.5 text-amber-500 shrink-0" />
193
+ <span>{lesson.linkedExam}</span>
194
+ </div>
195
+ </Section>
196
+ </>
197
+ )}
198
+
199
+ {!lesson.publicDescription &&
200
+ !lesson.privateDescription &&
201
+ lesson.type === 'exercicio' && (
202
+ <div className="flex flex-col items-center gap-2 py-8 text-center">
203
+ <Layers className="size-8 text-muted-foreground/40" />
204
+ <p className="text-sm text-muted-foreground">
205
+ Sem informações adicionais nesta aula.
206
+ </p>
207
+ </div>
208
+ )}
209
+ </div>
210
+ </TabsContent>
211
+
212
+ {/* ── Transcrição ───────────────────────────────────────────────────── */}
213
+ <TabsContent
214
+ value="transcription"
215
+ className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
216
+ >
217
+ {lesson.transcription ? (
218
+ <div className="rounded-md border bg-muted/20 p-3">
219
+ <p className="text-sm leading-relaxed whitespace-pre-wrap font-mono">
220
+ {lesson.transcription}
221
+ </p>
222
+ </div>
223
+ ) : (
224
+ <EmptyState
225
+ icon={<FileText className="size-8 text-muted-foreground/40" />}
226
+ >
227
+ Transcrição não disponível para esta aula.
228
+ </EmptyState>
229
+ )}
230
+ </TabsContent>
231
+
232
+ {/* ── Recursos ──────────────────────────────────────────────────────── */}
233
+ <TabsContent
234
+ value="resources"
235
+ className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
236
+ >
237
+ {lesson.resources && lesson.resources.length > 0 ? (
238
+ <div className="flex flex-col gap-2">
239
+ {lesson.resources.map((res) => (
240
+ <div
241
+ key={res.id}
242
+ className="flex items-center gap-2 rounded-md border px-3 py-2"
243
+ >
244
+ <Paperclip className="size-3.5 text-muted-foreground shrink-0" />
245
+ <span className="flex-1 text-sm truncate">{res.name}</span>
246
+ {res.size && (
247
+ <span className="text-[0.6rem] text-muted-foreground shrink-0">
248
+ {res.size}
249
+ </span>
250
+ )}
251
+ <Badge
252
+ variant={res.public ? 'default' : 'secondary'}
253
+ className="text-[0.6rem] px-1.5 py-0 h-4 shrink-0"
254
+ >
255
+ {res.public ? 'Público' : 'Privado'}
256
+ </Badge>
257
+ </div>
258
+ ))}
259
+ </div>
260
+ ) : (
261
+ <EmptyState
262
+ icon={<Paperclip className="size-8 text-muted-foreground/40" />}
263
+ >
264
+ Nenhum recurso anexado a esta aula.
265
+ </EmptyState>
266
+ )}
267
+ </TabsContent>
268
+ </Tabs>
269
+ </div>
270
+ );
271
+ }
272
+
273
+ // ── Local helpers ─────────────────────────────────────────────────────────────
274
+
275
+ function Section({
276
+ title,
277
+ children,
278
+ }: {
279
+ title: string;
280
+ children: React.ReactNode;
281
+ }) {
282
+ return (
283
+ <div className="flex flex-col gap-1.5">
284
+ <p className="text-xs font-medium text-muted-foreground">{title}</p>
285
+ {children}
286
+ </div>
287
+ );
288
+ }
289
+
290
+ function InfoRow({ label, value }: { label: string; value: string }) {
291
+ return (
292
+ <div className="flex items-center gap-2">
293
+ <span className="text-xs text-muted-foreground w-20 shrink-0">
294
+ {label}
295
+ </span>
296
+ <span className="text-sm">{value}</span>
297
+ </div>
298
+ );
299
+ }
300
+
301
+ function EmptyState({
302
+ icon,
303
+ children,
304
+ }: {
305
+ icon: React.ReactNode;
306
+ children: React.ReactNode;
307
+ }) {
308
+ return (
309
+ <div className="flex flex-col items-center gap-2 py-12 text-center">
310
+ {icon}
311
+ <p className="text-sm text-muted-foreground">{children}</p>
312
+ </div>
313
+ );
314
+ }