@hed-hog/lms 0.0.328 → 0.0.330
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.
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +22 -16
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +18 -8
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +7 -5
- package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +5 -9
- package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +5 -9
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +15 -14
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +66 -29
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +4 -2
- package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -34
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/classes/page.tsx.ejs +23 -15
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +9 -7
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +3 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +4 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +24 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +21 -19
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +7 -5
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +18 -16
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +13 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +14 -9
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +42 -25
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +3 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +10 -8
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +22 -20
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +21 -19
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +34 -36
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +3 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +7 -5
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +106 -54
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +79 -59
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +92 -26
- package/hedhog/frontend/app/instructors/page.tsx.ejs +4 -2
- package/hedhog/frontend/messages/en.json +619 -13
- package/hedhog/frontend/messages/pt.json +619 -13
- package/package.json +7 -7
- package/src/instructor/instructor.service.ts +22 -19
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +0 -591
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +0 -109
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +0 -60
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +0 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +0 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +0 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +0 -174
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +0 -185
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +0 -277
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +0 -207
|
@@ -1,314 +0,0 @@
|
|
|
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,174 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
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
|
-
name: 'React Avancado',
|
|
29
|
-
title: 'React Avancado',
|
|
30
|
-
description:
|
|
31
|
-
'Domine os conceitos avancados do React: hooks, patterns, performance e gerenciamento de estado moderno.',
|
|
32
|
-
slug: 'react-avancado',
|
|
33
|
-
published: true,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Session config: title + how many lessons each gets
|
|
37
|
-
const SESSION_CONFIG: { title: string; lessonCount: number }[] = [
|
|
38
|
-
{ title: 'Boas-vindas ao curso', lessonCount: 5 },
|
|
39
|
-
{ title: 'Hooks Avancados useReducer e useContext', lessonCount: 32 },
|
|
40
|
-
{ title: 'Patterns de Composicao', lessonCount: 31 },
|
|
41
|
-
{ title: 'Gerenciamento de Estado com Zustand', lessonCount: 8 },
|
|
42
|
-
{ title: 'Performance e Otimizacao', lessonCount: 30 },
|
|
43
|
-
{ title: 'React Server Components', lessonCount: 10 },
|
|
44
|
-
{ title: 'Data Fetching Moderno', lessonCount: 9 },
|
|
45
|
-
{ title: 'Roteamento Avancado com Next.js', lessonCount: 7 },
|
|
46
|
-
{ title: 'Formularios e Validacao com RHF', lessonCount: 11 },
|
|
47
|
-
{ title: 'Autenticacao e Seguranca', lessonCount: 9 },
|
|
48
|
-
{ title: 'Testing com React Testing Library', lessonCount: 12 },
|
|
49
|
-
{ title: 'Acessibilidade a11y', lessonCount: 6 },
|
|
50
|
-
{ title: 'Internacionalizacao i18n', lessonCount: 7 },
|
|
51
|
-
{ title: 'Animacoes com Framer Motion', lessonCount: 8 },
|
|
52
|
-
{ title: 'Arquitetura de Projetos Escalaveis', lessonCount: 10 },
|
|
53
|
-
{ title: 'Design Systems e Storybook', lessonCount: 8 },
|
|
54
|
-
{ title: 'Deploy CI/CD e DevOps', lessonCount: 7 },
|
|
55
|
-
{ title: 'Monorepos e Micro-frontends', lessonCount: 6 },
|
|
56
|
-
{ title: 'Debugging Avancado', lessonCount: 6 },
|
|
57
|
-
{ title: 'Projeto Final App Completo', lessonCount: 5 },
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
export const MOCK_SESSIONS: Session[] = SESSION_CONFIG.map((s, i) => ({
|
|
61
|
-
id: `s${i + 1}`,
|
|
62
|
-
code: `S${String(i + 1).padStart(2, '0')}`,
|
|
63
|
-
title: s.title,
|
|
64
|
-
duration: s.lessonCount * 12,
|
|
65
|
-
order: i,
|
|
66
|
-
}));
|
|
67
|
-
|
|
68
|
-
// Lesson title patterns
|
|
69
|
-
const TITLE_PATTERNS = [
|
|
70
|
-
'Introducao e objetivos',
|
|
71
|
-
'Conceitos fundamentais',
|
|
72
|
-
'Configuracao do ambiente',
|
|
73
|
-
'Pratica guiada passo a passo',
|
|
74
|
-
'Exercicio: implementacao',
|
|
75
|
-
'Quiz de revisao',
|
|
76
|
-
'Caso de uso real',
|
|
77
|
-
'Implementacao completa',
|
|
78
|
-
'Debugging e troubleshooting',
|
|
79
|
-
'Otimizacoes e boas praticas',
|
|
80
|
-
'Desafio pratico',
|
|
81
|
-
'Revisao do modulo',
|
|
82
|
-
'Q e A e duvidas frequentes',
|
|
83
|
-
'Proximos passos',
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
const LESSON_STATUSES: LessonStatus[] = [
|
|
87
|
-
'preparada',
|
|
88
|
-
'gravada',
|
|
89
|
-
'editada',
|
|
90
|
-
'finalizada',
|
|
91
|
-
'publicada',
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
const VISIBILITIES: Visibility[] = [
|
|
95
|
-
'publico',
|
|
96
|
-
'publico',
|
|
97
|
-
'privado',
|
|
98
|
-
'restrito',
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
const LESSON_TYPES: LessonType[] = [
|
|
102
|
-
'video',
|
|
103
|
-
'video',
|
|
104
|
-
'video',
|
|
105
|
-
'post',
|
|
106
|
-
'video',
|
|
107
|
-
'video',
|
|
108
|
-
'questao',
|
|
109
|
-
'exercicio',
|
|
110
|
-
];
|
|
111
|
-
const PROVIDERS: VideoProvider[] = [
|
|
112
|
-
'youtube',
|
|
113
|
-
'vimeo',
|
|
114
|
-
'bunny',
|
|
115
|
-
'youtube',
|
|
116
|
-
'youtube',
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
let _lid = 0;
|
|
120
|
-
|
|
121
|
-
export const MOCK_LESSONS: Lesson[] = SESSION_CONFIG.flatMap((s, si) =>
|
|
122
|
-
Array.from({ length: s.lessonCount }, (_, li) => {
|
|
123
|
-
_lid += 1;
|
|
124
|
-
const type = LESSON_TYPES[_lid % LESSON_TYPES.length] as LessonType;
|
|
125
|
-
const isVideo = type === 'video';
|
|
126
|
-
const provider = PROVIDERS[_lid % PROVIDERS.length] as VideoProvider;
|
|
127
|
-
const baseTitle = TITLE_PATTERNS[li % TITLE_PATTERNS.length] as string;
|
|
128
|
-
const title =
|
|
129
|
-
li < TITLE_PATTERNS.length
|
|
130
|
-
? baseTitle
|
|
131
|
-
: baseTitle +
|
|
132
|
-
' parte ' +
|
|
133
|
-
String(Math.floor(li / TITLE_PATTERNS.length) + 1);
|
|
134
|
-
|
|
135
|
-
const hasResource = li % 4 === 0;
|
|
136
|
-
|
|
137
|
-
const status = LESSON_STATUSES[
|
|
138
|
-
_lid % LESSON_STATUSES.length
|
|
139
|
-
] as LessonStatus;
|
|
140
|
-
const visibility = VISIBILITIES[_lid % VISIBILITIES.length] as Visibility;
|
|
141
|
-
|
|
142
|
-
const lesson: Lesson = {
|
|
143
|
-
id: `l${_lid}`,
|
|
144
|
-
code: `A${String(_lid).padStart(3, '0')}`,
|
|
145
|
-
title,
|
|
146
|
-
type,
|
|
147
|
-
status,
|
|
148
|
-
visibility,
|
|
149
|
-
duration: 8 + (_lid % 32),
|
|
150
|
-
publicDescription: `Aprenda ${s.title.toLowerCase()} de forma pratica nesta aula.`,
|
|
151
|
-
privateDescription: li % 7 === 0 ? 'Revisar antes de publicar.' : '',
|
|
152
|
-
sessionId: `s${si + 1}`,
|
|
153
|
-
order: li,
|
|
154
|
-
resources: hasResource
|
|
155
|
-
? [
|
|
156
|
-
{
|
|
157
|
-
id: `r${_lid}`,
|
|
158
|
-
name: `material-${String(_lid).padStart(3, '0')}.pdf`,
|
|
159
|
-
size: `${1 + (_lid % 5)}.${_lid % 9} MB`,
|
|
160
|
-
type: 'application/pdf',
|
|
161
|
-
public: _lid % 2 === 0,
|
|
162
|
-
url: `https://www.w3.org/WAI/WCAG21/Techniques/pdf/PDF1.pdf`,
|
|
163
|
-
},
|
|
164
|
-
]
|
|
165
|
-
: [],
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
if (isVideo) {
|
|
169
|
-
lesson.videoProvider = provider;
|
|
170
|
-
lesson.videoUrl = `https://example.com/video/${_lid}`;
|
|
171
|
-
lesson.autoDuration = _lid % 3 !== 0;
|
|
172
|
-
if (li === 0) {
|
|
173
|
-
lesson.transcription = `Transcricao completa da primeira aula de "${s.title}". Lorem ipsum dolor sit amet, consectetur adipiscing elit.`;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (type === 'questao') {
|
|
177
|
-
lesson.linkedExam = `exam-${_lid}`;
|
|
178
|
-
}
|
|
179
|
-
if (type === 'post') {
|
|
180
|
-
lesson.postContent = `Conteudo detalhado sobre ${s.title.toLowerCase()}...`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return lesson;
|
|
184
|
-
})
|
|
185
|
-
);
|