@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,443 @@
1
+ 'use client';
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import {
5
+ CircleDot,
6
+ Clock,
7
+ Eye,
8
+ EyeOff,
9
+ Layers,
10
+ Loader2,
11
+ Lock,
12
+ Plus,
13
+ Save,
14
+ Trash2,
15
+ Undo2,
16
+ Video,
17
+ } from 'lucide-react';
18
+ import { useEffect, useMemo } from 'react';
19
+ import { useForm } from 'react-hook-form';
20
+ import { z } from 'zod';
21
+
22
+ import { Badge } from '@/components/ui/badge';
23
+ import { Button } from '@/components/ui/button';
24
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
25
+ import {
26
+ Form,
27
+ FormControl,
28
+ FormField,
29
+ FormItem,
30
+ FormLabel,
31
+ FormMessage,
32
+ } from '@/components/ui/form';
33
+ import { Input } from '@/components/ui/input';
34
+ import { ScrollArea } from '@/components/ui/scroll-area';
35
+ import {
36
+ Select,
37
+ SelectContent,
38
+ SelectItem,
39
+ SelectTrigger,
40
+ SelectValue,
41
+ } from '@/components/ui/select';
42
+ import { Separator } from '@/components/ui/separator';
43
+ import { Switch } from '@/components/ui/switch';
44
+ import { Textarea } from '@/components/ui/textarea';
45
+
46
+ import {
47
+ useCreateLessonMutation,
48
+ useDeleteSessionMutation,
49
+ useUpdateSessionMutation,
50
+ } from '../_data/use-course-structure-mutations';
51
+ import { useStructureStore } from './store';
52
+ import type { Visibility } from './types';
53
+
54
+ // ── Schema ────────────────────────────────────────────────────────────────────
55
+
56
+ const schema = z.object({
57
+ code: z.string().min(1, 'Código obrigatório'),
58
+ title: z.string().min(1, 'Título obrigatório'),
59
+ description: z.string(),
60
+ duration: z.coerce.number().min(0),
61
+ visibility: z.enum(['publico', 'privado', 'restrito'] as const),
62
+ published: z.boolean(),
63
+ });
64
+
65
+ type FormValues = z.infer<typeof schema>;
66
+
67
+ // ── Component ─────────────────────────────────────────────────────────────────
68
+
69
+ interface EditorSessionProps {
70
+ sessionId: string;
71
+ }
72
+
73
+ export function EditorSession({ sessionId }: EditorSessionProps) {
74
+ const session = useStructureStore((s) =>
75
+ s.sessions.find((ss) => ss.id === sessionId)
76
+ );
77
+ const allLessons = useStructureStore((s) => s.lessons);
78
+ const lessons = useMemo(
79
+ () => allLessons.filter((l) => l.sessionId === sessionId),
80
+ [allLessons, sessionId]
81
+ );
82
+ const showConfirm = useStructureStore((s) => s.showConfirm);
83
+
84
+ const updateSession = useUpdateSessionMutation();
85
+ const createLesson = useCreateLessonMutation();
86
+ const deleteSession = useDeleteSessionMutation();
87
+
88
+ const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
89
+ const hours = Math.floor(totalMinutes / 60);
90
+ const minutes = totalMinutes % 60;
91
+
92
+ const defaultValues: FormValues = {
93
+ code: session?.code ?? '',
94
+ title: session?.title ?? '',
95
+ description: session?.description ?? '',
96
+ duration: session?.duration ?? 0,
97
+ visibility: (session?.visibility ?? 'publico') as Visibility,
98
+ published: session?.published ?? true,
99
+ };
100
+
101
+ const form = useForm<FormValues>({
102
+ resolver: zodResolver(schema),
103
+ defaultValues,
104
+ });
105
+
106
+ const { isDirty } = form.formState;
107
+
108
+ useEffect(() => {
109
+ if (!session) return;
110
+ form.reset({
111
+ code: session.code,
112
+ title: session.title,
113
+ description: session.description ?? '',
114
+ duration: session.duration,
115
+ visibility: session.visibility ?? 'publico',
116
+ published: session.published ?? true,
117
+ });
118
+ }, [session?.id]); // eslint-disable-line react-hooks/exhaustive-deps
119
+
120
+ if (!session) return null;
121
+
122
+ function onSubmit(values: FormValues) {
123
+ updateSession.mutate({ sessionId, formValues: values });
124
+ form.reset(values);
125
+ }
126
+
127
+ function handleDelete() {
128
+ showConfirm({
129
+ title: `Excluir sessão "${session!.title}"?`,
130
+ description:
131
+ 'A sessão e todas as suas aulas serão excluídas permanentemente.',
132
+ onConfirm: () => deleteSession.mutate({ sessionId }),
133
+ });
134
+ }
135
+
136
+ return (
137
+ <Form {...form}>
138
+ <form
139
+ onSubmit={form.handleSubmit(onSubmit)}
140
+ className="flex flex-col h-full min-h-0"
141
+ >
142
+ {/* ── Header ───────────────────────────────────────────────────────── */}
143
+ <div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
144
+ <div className="flex size-9 items-center justify-center rounded-lg bg-muted shrink-0">
145
+ <Layers className="size-4 text-muted-foreground" />
146
+ </div>
147
+ <div className="flex-1 min-w-0">
148
+ <div className="flex items-center gap-1.5">
149
+ <span className="text-sm font-semibold truncate">Sessão</span>
150
+ {isDirty && (
151
+ <CircleDot className="size-3 text-amber-500 shrink-0" />
152
+ )}
153
+ </div>
154
+ <p className="text-[0.65rem] text-muted-foreground truncate">
155
+ {session.code}
156
+ </p>
157
+ </div>
158
+ <Badge
159
+ variant={session.published ? 'default' : 'secondary'}
160
+ className="shrink-0 text-xs"
161
+ >
162
+ {session.published ? 'Publicada' : 'Oculta'}
163
+ </Badge>
164
+ <Button
165
+ type="button"
166
+ variant="ghost"
167
+ size="icon"
168
+ className="size-7 text-destructive/60 hover:text-destructive shrink-0"
169
+ title="Excluir sessão"
170
+ aria-label="Excluir sessão"
171
+ disabled={deleteSession.isPending}
172
+ onClick={handleDelete}
173
+ >
174
+ {deleteSession.isPending ? (
175
+ <Loader2 className="size-3.5 animate-spin" />
176
+ ) : (
177
+ <Trash2 className="size-3.5" />
178
+ )}
179
+ </Button>
180
+ </div>
181
+
182
+ {/* ── Scrollable body ───────────────────────────────────────────────── */}
183
+ <ScrollArea className="flex-1 min-h-0">
184
+ <div className="flex flex-col gap-3 p-3">
185
+ {/* ── Stats ────────────────────────────────────────────────────── */}
186
+ <div className="grid grid-cols-2 gap-2">
187
+ <StatChip
188
+ icon={<Video className="size-3" />}
189
+ label="Aulas"
190
+ value={lessons.length}
191
+ />
192
+ <StatChip
193
+ icon={<Clock className="size-3" />}
194
+ label="Duração"
195
+ value={hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`}
196
+ />
197
+ </div>
198
+
199
+ {/* ── Dados ────────────────────────────────────────────────────── */}
200
+ <Card className="bg-muted/20 py-2 gap-2">
201
+ <CardHeader className="px-3 pt-2 pb-1">
202
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
203
+ Dados
204
+ </CardTitle>
205
+ </CardHeader>
206
+ <CardContent className="px-3 pb-2 flex flex-col gap-3">
207
+ <div className="grid grid-cols-2 gap-2">
208
+ <FormField
209
+ control={form.control}
210
+ name="code"
211
+ render={({ field }) => (
212
+ <FormItem>
213
+ <FormLabel className="text-xs">Código</FormLabel>
214
+ <FormControl>
215
+ <Input {...field} className="h-8 text-xs font-mono" />
216
+ </FormControl>
217
+ <FormMessage className="text-xs" />
218
+ </FormItem>
219
+ )}
220
+ />
221
+ <FormField
222
+ control={form.control}
223
+ name="duration"
224
+ render={({ field }) => (
225
+ <FormItem>
226
+ <FormLabel className="text-xs">Duração (min)</FormLabel>
227
+ <FormControl>
228
+ <Input
229
+ {...field}
230
+ type="number"
231
+ className="h-8 text-xs"
232
+ />
233
+ </FormControl>
234
+ <FormMessage className="text-xs" />
235
+ </FormItem>
236
+ )}
237
+ />
238
+ </div>
239
+
240
+ <FormField
241
+ control={form.control}
242
+ name="title"
243
+ render={({ field }) => (
244
+ <FormItem>
245
+ <FormLabel className="text-xs">Título</FormLabel>
246
+ <FormControl>
247
+ <Input {...field} className="h-8 text-sm" />
248
+ </FormControl>
249
+ <FormMessage className="text-xs" />
250
+ </FormItem>
251
+ )}
252
+ />
253
+
254
+ <FormField
255
+ control={form.control}
256
+ name="description"
257
+ render={({ field }) => (
258
+ <FormItem>
259
+ <FormLabel className="text-xs">Descrição</FormLabel>
260
+ <FormControl>
261
+ <Textarea
262
+ {...field}
263
+ rows={3}
264
+ className="text-sm resize-none"
265
+ placeholder="Descrição opcional da sessão…"
266
+ />
267
+ </FormControl>
268
+ <FormMessage className="text-xs" />
269
+ </FormItem>
270
+ )}
271
+ />
272
+ </CardContent>
273
+ </Card>
274
+
275
+ {/* ── Publicação ───────────────────────────────────────────────── */}
276
+ <Card className="bg-muted/20 py-2 gap-2">
277
+ <CardHeader className="px-3 pt-2 pb-1">
278
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
279
+ Publicação
280
+ </CardTitle>
281
+ </CardHeader>
282
+ <CardContent className="px-3 pb-2">
283
+ <div className="grid grid-cols-2 gap-2 items-end">
284
+ <FormField
285
+ control={form.control}
286
+ name="visibility"
287
+ render={({ field }) => (
288
+ <FormItem>
289
+ <FormLabel className="text-xs">Visibilidade</FormLabel>
290
+ <Select
291
+ value={field.value}
292
+ onValueChange={field.onChange}
293
+ >
294
+ <FormControl>
295
+ <SelectTrigger className="h-8 text-xs w-full">
296
+ <SelectValue />
297
+ </SelectTrigger>
298
+ </FormControl>
299
+ <SelectContent>
300
+ <SelectItem value="publico">
301
+ <span className="flex items-center gap-1.5">
302
+ <Eye className="size-3" /> Público
303
+ </span>
304
+ </SelectItem>
305
+ <SelectItem value="privado">
306
+ <span className="flex items-center gap-1.5">
307
+ <EyeOff className="size-3" /> Privado
308
+ </span>
309
+ </SelectItem>
310
+ <SelectItem value="restrito">
311
+ <span className="flex items-center gap-1.5">
312
+ <Lock className="size-3" /> Restrito
313
+ </span>
314
+ </SelectItem>
315
+ </SelectContent>
316
+ </Select>
317
+ <FormMessage className="text-xs" />
318
+ </FormItem>
319
+ )}
320
+ />
321
+
322
+ <FormField
323
+ control={form.control}
324
+ name="published"
325
+ render={({ field }) => (
326
+ <FormItem className="flex items-center justify-between gap-3 rounded-md border px-3 h-8">
327
+ <FormLabel className="text-xs cursor-pointer">
328
+ Publicada
329
+ </FormLabel>
330
+ <FormControl>
331
+ <Switch
332
+ checked={field.value}
333
+ onCheckedChange={field.onChange}
334
+ />
335
+ </FormControl>
336
+ </FormItem>
337
+ )}
338
+ />
339
+ </div>
340
+ </CardContent>
341
+ </Card>
342
+
343
+ {/* ── Aulas da sessão ───────────────────────────────────────────── */}
344
+ {lessons.length === 0 && (
345
+ <div className="flex flex-col items-center gap-2 py-6 px-2 rounded-lg border border-dashed text-center">
346
+ <Video className="size-5 text-muted-foreground/40" />
347
+ <p className="text-xs text-muted-foreground leading-relaxed">
348
+ Esta sessão ainda não tem aulas.
349
+ </p>
350
+ <Button
351
+ type="button"
352
+ variant="outline"
353
+ size="sm"
354
+ className="h-7 text-xs gap-1.5"
355
+ disabled={createLesson.isPending}
356
+ onClick={() => createLesson.mutate({ sessionId })}
357
+ >
358
+ {createLesson.isPending ? (
359
+ <Loader2 className="size-3 animate-spin" />
360
+ ) : (
361
+ <Plus className="size-3" />
362
+ )}
363
+ Adicionar aula
364
+ </Button>
365
+ </div>
366
+ )}
367
+ </div>
368
+ </ScrollArea>
369
+
370
+ {/* ── Footer ───────────────────────────────────────────────────────── */}
371
+ <div className="shrink-0 border-t bg-background">
372
+ <Separator />
373
+ <div className="flex items-center gap-2 px-3 py-2">
374
+ <Button
375
+ type="button"
376
+ variant="ghost"
377
+ size="sm"
378
+ className="h-7 text-xs"
379
+ disabled={!isDirty || updateSession.isPending}
380
+ onClick={() => form.reset()}
381
+ >
382
+ <Undo2 className="size-3 mr-1" />
383
+ Cancelar
384
+ </Button>
385
+ <div className="flex-1" />
386
+ <Button
387
+ type="button"
388
+ variant="outline"
389
+ size="sm"
390
+ className="h-7 text-xs"
391
+ disabled={createLesson.isPending}
392
+ onClick={() => createLesson.mutate({ sessionId })}
393
+ >
394
+ {createLesson.isPending ? (
395
+ <Loader2 className="size-3 mr-1 animate-spin" />
396
+ ) : (
397
+ <Plus className="size-3 mr-1" />
398
+ )}
399
+ Nova aula
400
+ </Button>
401
+ <Button
402
+ type="submit"
403
+ size="sm"
404
+ className="h-7 text-xs"
405
+ disabled={!isDirty || updateSession.isPending}
406
+ >
407
+ {updateSession.isPending ? (
408
+ <Loader2 className="size-3 mr-1 animate-spin" />
409
+ ) : (
410
+ <Save className="size-3 mr-1" />
411
+ )}
412
+ Salvar
413
+ </Button>
414
+ </div>
415
+ </div>
416
+ </form>
417
+ </Form>
418
+ );
419
+ }
420
+
421
+ // ── Helpers ───────────────────────────────────────────────────────────────────
422
+
423
+ function StatChip({
424
+ icon,
425
+ label,
426
+ value,
427
+ }: {
428
+ icon: React.ReactNode;
429
+ label: string;
430
+ value: string | number;
431
+ }) {
432
+ return (
433
+ <div className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2">
434
+ <span className="text-muted-foreground shrink-0">{icon}</span>
435
+ <div className="min-w-0">
436
+ <p className="text-[0.65rem] text-muted-foreground truncate">{label}</p>
437
+ <p className="text-sm font-semibold tabular-nums leading-none">
438
+ {value}
439
+ </p>
440
+ </div>
441
+ </div>
442
+ );
443
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import { getTextSegments } from './tree-helpers';
5
+
6
+ interface HighlightedTextProps {
7
+ text: string;
8
+ query: string;
9
+ className?: string;
10
+ }
11
+
12
+ /**
13
+ * Renders `text` with the first occurrence of `query` visually highlighted.
14
+ * Falls back to plain text when query is empty or no match is found.
15
+ */
16
+ export function HighlightedText({ text, query, className }: HighlightedTextProps) {
17
+ const segments = getTextSegments(text, query);
18
+
19
+ if (segments.length === 1 && !segments[0]?.highlight) {
20
+ return <span className={className}>{text}</span>;
21
+ }
22
+
23
+ return (
24
+ <span className={className}>
25
+ {segments.map((seg, i) =>
26
+ seg.highlight ? (
27
+ <mark
28
+ key={i}
29
+ className={cn(
30
+ 'bg-yellow-300/70 dark:bg-yellow-500/40 text-current rounded-[2px] px-[1px]'
31
+ )}
32
+ >
33
+ {seg.text}
34
+ </mark>
35
+ ) : (
36
+ <span key={i}>{seg.text}</span>
37
+ )
38
+ )}
39
+ </span>
40
+ );
41
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Mock Data — LMS Course Structure
3
+ *
4
+ * ⚠️ TEMPORARY — replace when integrating with the real API.
5
+ *
6
+ * TODO[API]: Remove this file entirely once `useCourseStructure` fetches data
7
+ * from GET /lms/courses/:id/structure. The Zustand store should then
8
+ * be seeded with the server response instead of these constants.
9
+ */
10
+
11
+ import type {
12
+ Course,
13
+ Lesson,
14
+ LessonStatus,
15
+ LessonType,
16
+ Session,
17
+ VideoProvider,
18
+ Visibility,
19
+ } from './types';
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Mock Data — LMS Course Structure
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ export const MOCK_COURSE: Course = {
26
+ id: 'course-1',
27
+ code: 'REACT-ADV',
28
+ title: 'React Avancado',
29
+ description:
30
+ 'Domine os conceitos avancados do React: hooks, patterns, performance e gerenciamento de estado moderno.',
31
+ slug: 'react-avancado',
32
+ published: true,
33
+ };
34
+
35
+ // Session config: title + how many lessons each gets
36
+ const SESSION_CONFIG: { title: string; lessonCount: number }[] = [
37
+ { title: 'Boas-vindas ao curso', lessonCount: 5 },
38
+ { title: 'Hooks Avancados useReducer e useContext', lessonCount: 32 },
39
+ { title: 'Patterns de Composicao', lessonCount: 31 },
40
+ { title: 'Gerenciamento de Estado com Zustand', lessonCount: 8 },
41
+ { title: 'Performance e Otimizacao', lessonCount: 30 },
42
+ { title: 'React Server Components', lessonCount: 10 },
43
+ { title: 'Data Fetching Moderno', lessonCount: 9 },
44
+ { title: 'Roteamento Avancado com Next.js', lessonCount: 7 },
45
+ { title: 'Formularios e Validacao com RHF', lessonCount: 11 },
46
+ { title: 'Autenticacao e Seguranca', lessonCount: 9 },
47
+ { title: 'Testing com React Testing Library', lessonCount: 12 },
48
+ { title: 'Acessibilidade a11y', lessonCount: 6 },
49
+ { title: 'Internacionalizacao i18n', lessonCount: 7 },
50
+ { title: 'Animacoes com Framer Motion', lessonCount: 8 },
51
+ { title: 'Arquitetura de Projetos Escalaveis', lessonCount: 10 },
52
+ { title: 'Design Systems e Storybook', lessonCount: 8 },
53
+ { title: 'Deploy CI/CD e DevOps', lessonCount: 7 },
54
+ { title: 'Monorepos e Micro-frontends', lessonCount: 6 },
55
+ { title: 'Debugging Avancado', lessonCount: 6 },
56
+ { title: 'Projeto Final App Completo', lessonCount: 5 },
57
+ ];
58
+
59
+ export const MOCK_SESSIONS: Session[] = SESSION_CONFIG.map((s, i) => ({
60
+ id: `s${i + 1}`,
61
+ code: `S${String(i + 1).padStart(2, '0')}`,
62
+ title: s.title,
63
+ duration: s.lessonCount * 12,
64
+ order: i,
65
+ }));
66
+
67
+ // Lesson title patterns
68
+ const TITLE_PATTERNS = [
69
+ 'Introducao e objetivos',
70
+ 'Conceitos fundamentais',
71
+ 'Configuracao do ambiente',
72
+ 'Pratica guiada passo a passo',
73
+ 'Exercicio: implementacao',
74
+ 'Quiz de revisao',
75
+ 'Caso de uso real',
76
+ 'Implementacao completa',
77
+ 'Debugging e troubleshooting',
78
+ 'Otimizacoes e boas praticas',
79
+ 'Desafio pratico',
80
+ 'Revisao do modulo',
81
+ 'Q e A e duvidas frequentes',
82
+ 'Proximos passos',
83
+ ];
84
+
85
+ const LESSON_STATUSES: LessonStatus[] = [
86
+ 'preparada',
87
+ 'gravada',
88
+ 'editada',
89
+ 'finalizada',
90
+ 'publicada',
91
+ ];
92
+
93
+ const VISIBILITIES: Visibility[] = [
94
+ 'publico',
95
+ 'publico',
96
+ 'privado',
97
+ 'restrito',
98
+ ];
99
+
100
+ const LESSON_TYPES: LessonType[] = [
101
+ 'video',
102
+ 'video',
103
+ 'video',
104
+ 'post',
105
+ 'video',
106
+ 'video',
107
+ 'questao',
108
+ 'exercicio',
109
+ ];
110
+ const PROVIDERS: VideoProvider[] = [
111
+ 'youtube',
112
+ 'vimeo',
113
+ 'bunny',
114
+ 'youtube',
115
+ 'youtube',
116
+ ];
117
+
118
+ let _lid = 0;
119
+
120
+ export const MOCK_LESSONS: Lesson[] = SESSION_CONFIG.flatMap((s, si) =>
121
+ Array.from({ length: s.lessonCount }, (_, li) => {
122
+ _lid += 1;
123
+ const type = LESSON_TYPES[_lid % LESSON_TYPES.length] as LessonType;
124
+ const isVideo = type === 'video';
125
+ const provider = PROVIDERS[_lid % PROVIDERS.length] as VideoProvider;
126
+ const baseTitle = TITLE_PATTERNS[li % TITLE_PATTERNS.length] as string;
127
+ const title =
128
+ li < TITLE_PATTERNS.length
129
+ ? baseTitle
130
+ : baseTitle +
131
+ ' parte ' +
132
+ String(Math.floor(li / TITLE_PATTERNS.length) + 1);
133
+
134
+ const hasResource = li % 4 === 0;
135
+
136
+ const status = LESSON_STATUSES[
137
+ _lid % LESSON_STATUSES.length
138
+ ] as LessonStatus;
139
+ const visibility = VISIBILITIES[_lid % VISIBILITIES.length] as Visibility;
140
+
141
+ const lesson: Lesson = {
142
+ id: `l${_lid}`,
143
+ code: `A${String(_lid).padStart(3, '0')}`,
144
+ title,
145
+ type,
146
+ status,
147
+ visibility,
148
+ duration: 8 + (_lid % 32),
149
+ publicDescription: `Aprenda ${s.title.toLowerCase()} de forma pratica nesta aula.`,
150
+ privateDescription: li % 7 === 0 ? 'Revisar antes de publicar.' : '',
151
+ sessionId: `s${si + 1}`,
152
+ order: li,
153
+ resources: hasResource
154
+ ? [
155
+ {
156
+ id: `r${_lid}`,
157
+ name: `material-${String(_lid).padStart(3, '0')}.pdf`,
158
+ size: `${1 + (_lid % 5)}.${_lid % 9} MB`,
159
+ type: 'application/pdf',
160
+ public: _lid % 2 === 0,
161
+ url: `https://www.w3.org/WAI/WCAG21/Techniques/pdf/PDF1.pdf`,
162
+ },
163
+ ]
164
+ : [],
165
+ };
166
+
167
+ if (isVideo) {
168
+ lesson.videoProvider = provider;
169
+ lesson.videoUrl = `https://example.com/video/${_lid}`;
170
+ lesson.autoDuration = _lid % 3 !== 0;
171
+ if (li === 0) {
172
+ lesson.transcription = `Transcricao completa da primeira aula de "${s.title}". Lorem ipsum dolor sit amet, consectetur adipiscing elit.`;
173
+ }
174
+ }
175
+ if (type === 'questao') {
176
+ lesson.linkedExam = `exam-${_lid}`;
177
+ }
178
+ if (type === 'post') {
179
+ lesson.postContent = `Conteudo detalhado sobre ${s.title.toLowerCase()}...`;
180
+ }
181
+
182
+ return lesson;
183
+ })
184
+ );