@hed-hog/lms 0.0.354 → 0.0.357
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/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +15 -7
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +12 -0
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.service.d.ts +22 -0
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +147 -18
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.service.d.ts +4 -2
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +61 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +3 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +15 -0
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.d.ts +1 -0
- package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.js +5 -0
- package/dist/course/dto/create-course-structure-session.dto.js.map +1 -1
- package/dist/enterprise/training/training-student.controller.d.ts +0 -95
- package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.controller.js +1 -34
- package/dist/enterprise/training/training-student.controller.js.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +63 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +320 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +2 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +287 -0
- package/dist/platforma/platforma.controller.d.ts.map +1 -0
- package/dist/platforma/platforma.controller.js +147 -0
- package/dist/platforma/platforma.controller.js.map +1 -0
- package/hedhog/data/route.yaml +75 -9
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -13
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +17 -6
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +14 -7
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +92 -53
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +25 -60
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +11 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +22 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +79 -64
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +31 -14
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -4
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +28 -24
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +20 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +41 -6
- package/hedhog/frontend/app/courses/page.tsx.ejs +59 -15
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/paths/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +1 -1
- package/hedhog/frontend/messages/en.json +7 -2
- package/hedhog/frontend/messages/pt.json +7 -2
- package/hedhog/table/course_lesson.yaml +2 -2
- package/hedhog/table/course_module.yaml +3 -0
- package/package.json +9 -9
- package/src/course/course-audio-transcription.service.ts +21 -8
- package/src/course/course-structure.service.ts +204 -3
- package/src/course/course.service.ts +67 -1
- package/src/course/dto/create-course-structure-lesson.dto.ts +17 -0
- package/src/course/dto/create-course-structure-session.dto.ts +13 -1
- package/src/enterprise/training/training-student.controller.ts +3 -27
- package/src/enterprise/training/training-student.service.ts +350 -2
- package/src/lms.module.ts +2 -0
- package/src/platforma/platforma.controller.ts +92 -0
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
EyeOff,
|
|
9
9
|
Layers,
|
|
10
10
|
Loader2,
|
|
11
|
-
Lock,
|
|
12
11
|
Plus,
|
|
13
12
|
Save,
|
|
14
13
|
Trash2,
|
|
@@ -19,7 +18,6 @@ import { useEffect, useMemo } from 'react';
|
|
|
19
18
|
import { useForm } from 'react-hook-form';
|
|
20
19
|
import { z } from 'zod';
|
|
21
20
|
|
|
22
|
-
import { Badge } from '@/components/ui/badge';
|
|
23
21
|
import { Button } from '@/components/ui/button';
|
|
24
22
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
25
23
|
import {
|
|
@@ -32,16 +30,14 @@ import {
|
|
|
32
30
|
} from '@/components/ui/form';
|
|
33
31
|
import { Input } from '@/components/ui/input';
|
|
34
32
|
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
33
|
import { Separator } from '@/components/ui/separator';
|
|
43
34
|
import { Switch } from '@/components/ui/switch';
|
|
44
35
|
import { Textarea } from '@/components/ui/textarea';
|
|
36
|
+
import {
|
|
37
|
+
Tooltip,
|
|
38
|
+
TooltipContent,
|
|
39
|
+
TooltipTrigger,
|
|
40
|
+
} from '@/components/ui/tooltip';
|
|
45
41
|
|
|
46
42
|
import {
|
|
47
43
|
useCreateLessonMutation,
|
|
@@ -50,7 +46,6 @@ import {
|
|
|
50
46
|
} from '../_data/use-course-structure-mutations';
|
|
51
47
|
import { IconActionTooltip } from './icon-action-tooltip';
|
|
52
48
|
import { useStructureStore } from './store';
|
|
53
|
-
import type { Visibility } from './types';
|
|
54
49
|
|
|
55
50
|
// ── Schema ────────────────────────────────────────────────────────────────────
|
|
56
51
|
|
|
@@ -59,7 +54,6 @@ const schema = z.object({
|
|
|
59
54
|
title: z.string().min(1, 'Título obrigatório'),
|
|
60
55
|
description: z.string(),
|
|
61
56
|
duration: z.coerce.number().min(0),
|
|
62
|
-
visibility: z.enum(['publico', 'privado', 'restrito'] as const),
|
|
63
57
|
published: z.boolean(),
|
|
64
58
|
});
|
|
65
59
|
|
|
@@ -95,8 +89,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
95
89
|
title: session?.title ?? '',
|
|
96
90
|
description: session?.description ?? '',
|
|
97
91
|
duration: session?.duration ?? 0,
|
|
98
|
-
|
|
99
|
-
published: session?.published ?? true,
|
|
92
|
+
published: session?.published ?? false,
|
|
100
93
|
};
|
|
101
94
|
|
|
102
95
|
const form = useForm<FormValues>({
|
|
@@ -113,8 +106,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
113
106
|
title: session.title,
|
|
114
107
|
description: session.description ?? '',
|
|
115
108
|
duration: session.duration,
|
|
116
|
-
|
|
117
|
-
published: session.published ?? true,
|
|
109
|
+
published: session.published ?? false,
|
|
118
110
|
});
|
|
119
111
|
}, [session?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
120
112
|
|
|
@@ -156,12 +148,23 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
156
148
|
{session.code}
|
|
157
149
|
</p>
|
|
158
150
|
</div>
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
151
|
+
<Tooltip>
|
|
152
|
+
<TooltipTrigger asChild>
|
|
153
|
+
<span
|
|
154
|
+
className="inline-flex shrink-0 items-center"
|
|
155
|
+
aria-label={session.published ? 'Publicada' : 'Rascunho'}
|
|
156
|
+
>
|
|
157
|
+
{session.published ? (
|
|
158
|
+
<Eye className="size-4 text-emerald-600 dark:text-emerald-400" />
|
|
159
|
+
) : (
|
|
160
|
+
<EyeOff className="size-4 text-muted-foreground" />
|
|
161
|
+
)}
|
|
162
|
+
</span>
|
|
163
|
+
</TooltipTrigger>
|
|
164
|
+
<TooltipContent>
|
|
165
|
+
{session.published ? 'Publicada' : 'Rascunho'}
|
|
166
|
+
</TooltipContent>
|
|
167
|
+
</Tooltip>
|
|
165
168
|
<IconActionTooltip
|
|
166
169
|
label="Excluir sessão"
|
|
167
170
|
asWrapper={deleteSession.isPending}
|
|
@@ -285,45 +288,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
285
288
|
</CardTitle>
|
|
286
289
|
</CardHeader>
|
|
287
290
|
<CardContent className="px-3 pb-2">
|
|
288
|
-
<div className="grid grid-cols-1 gap-2 items-end
|
|
289
|
-
<FormField
|
|
290
|
-
control={form.control}
|
|
291
|
-
name="visibility"
|
|
292
|
-
render={({ field }) => (
|
|
293
|
-
<FormItem>
|
|
294
|
-
<FormLabel className="text-xs">Visibilidade</FormLabel>
|
|
295
|
-
<Select
|
|
296
|
-
value={field.value}
|
|
297
|
-
onValueChange={field.onChange}
|
|
298
|
-
>
|
|
299
|
-
<FormControl>
|
|
300
|
-
<SelectTrigger className="h-8 text-xs w-full">
|
|
301
|
-
<SelectValue />
|
|
302
|
-
</SelectTrigger>
|
|
303
|
-
</FormControl>
|
|
304
|
-
<SelectContent>
|
|
305
|
-
<SelectItem value="publico">
|
|
306
|
-
<span className="flex items-center gap-1.5">
|
|
307
|
-
<Eye className="size-3" /> Público
|
|
308
|
-
</span>
|
|
309
|
-
</SelectItem>
|
|
310
|
-
<SelectItem value="privado">
|
|
311
|
-
<span className="flex items-center gap-1.5">
|
|
312
|
-
<EyeOff className="size-3" /> Privado
|
|
313
|
-
</span>
|
|
314
|
-
</SelectItem>
|
|
315
|
-
<SelectItem value="restrito">
|
|
316
|
-
<span className="flex items-center gap-1.5">
|
|
317
|
-
<Lock className="size-3" /> Restrito
|
|
318
|
-
</span>
|
|
319
|
-
</SelectItem>
|
|
320
|
-
</SelectContent>
|
|
321
|
-
</Select>
|
|
322
|
-
<FormMessage className="text-xs" />
|
|
323
|
-
</FormItem>
|
|
324
|
-
)}
|
|
325
|
-
/>
|
|
326
|
-
|
|
291
|
+
<div className="grid grid-cols-1 gap-2 items-end">
|
|
327
292
|
<FormField
|
|
328
293
|
control={form.control}
|
|
329
294
|
name="published"
|
|
@@ -72,6 +72,8 @@ interface StructureState {
|
|
|
72
72
|
open: boolean;
|
|
73
73
|
title: string;
|
|
74
74
|
description: string;
|
|
75
|
+
confirmText: string;
|
|
76
|
+
destructive: boolean;
|
|
75
77
|
onConfirm: (() => void) | null;
|
|
76
78
|
};
|
|
77
79
|
|
|
@@ -122,6 +124,7 @@ interface StructureState {
|
|
|
122
124
|
data: Partial<
|
|
123
125
|
LessonFormValues & {
|
|
124
126
|
transcription?: string;
|
|
127
|
+
videoConversionJobId?: number;
|
|
125
128
|
resources?: Lesson['resources'];
|
|
126
129
|
}
|
|
127
130
|
>
|
|
@@ -183,6 +186,8 @@ interface StructureState {
|
|
|
183
186
|
showConfirm: (opts: {
|
|
184
187
|
title: string;
|
|
185
188
|
description: string;
|
|
189
|
+
confirmText?: string;
|
|
190
|
+
destructive?: boolean;
|
|
186
191
|
onConfirm: () => void;
|
|
187
192
|
}) => void;
|
|
188
193
|
closeConfirm: () => void;
|
|
@@ -282,6 +287,8 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
282
287
|
open: false,
|
|
283
288
|
title: '',
|
|
284
289
|
description: '',
|
|
290
|
+
confirmText: '',
|
|
291
|
+
destructive: true,
|
|
285
292
|
onConfirm: null as (() => void) | null,
|
|
286
293
|
},
|
|
287
294
|
sessionPickerDialog: {
|
|
@@ -380,6 +387,7 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
380
387
|
title: `Nova Sessão ${code}`,
|
|
381
388
|
duration: 30,
|
|
382
389
|
order: sessions.length,
|
|
390
|
+
published: false,
|
|
383
391
|
};
|
|
384
392
|
set((s) => {
|
|
385
393
|
const nextExpandedIds = new Set([...s.expandedIds, newSession.id]);
|
|
@@ -433,6 +441,7 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
433
441
|
duration: 10,
|
|
434
442
|
sessionId,
|
|
435
443
|
order: sessionLessons.length,
|
|
444
|
+
published: false,
|
|
436
445
|
resources: [],
|
|
437
446
|
};
|
|
438
447
|
set((s) => {
|
|
@@ -919,6 +928,8 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
919
928
|
open: true,
|
|
920
929
|
title: opts.title,
|
|
921
930
|
description: opts.description,
|
|
931
|
+
confirmText: opts.confirmText ?? '',
|
|
932
|
+
destructive: opts.destructive ?? true,
|
|
922
933
|
onConfirm: opts.onConfirm,
|
|
923
934
|
},
|
|
924
935
|
}),
|
|
@@ -328,6 +328,7 @@ function LessonMenu({ data }: { data: Lesson }) {
|
|
|
328
328
|
|
|
329
329
|
const hasLessonClipboard = copiedType === 'lesson' && copiedIds.length > 0;
|
|
330
330
|
const hasOtherSessions = sessions.some((ss) => ss.id !== data.sessionId);
|
|
331
|
+
const session = sessions.find((ss) => ss.id === data.sessionId);
|
|
331
332
|
|
|
332
333
|
// Ids to move when "Mover" is triggered
|
|
333
334
|
const idsToMove = multiLessons ? selectedLessonIds : [data.id];
|
|
@@ -373,6 +374,27 @@ function LessonMenu({ data }: { data: Lesson }) {
|
|
|
373
374
|
Copiar aula
|
|
374
375
|
</ContextMenuItem>
|
|
375
376
|
|
|
377
|
+
<ContextMenuItem
|
|
378
|
+
onSelect={async () => {
|
|
379
|
+
if (!session) {
|
|
380
|
+
toast.error('Sessão da aula não encontrada');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const lessonCopyCode = `${session.code}_${data.code}_${data.title}`;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await navigator.clipboard.writeText(lessonCopyCode);
|
|
388
|
+
toast.success('Código da aula copiado');
|
|
389
|
+
} catch {
|
|
390
|
+
toast.error('Não foi possível copiar o código da aula');
|
|
391
|
+
}
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
395
|
+
Copiar código da aula
|
|
396
|
+
</ContextMenuItem>
|
|
397
|
+
|
|
376
398
|
{multiLessons && (
|
|
377
399
|
<ContextMenuItem
|
|
378
400
|
onSelect={() => {
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
Film,
|
|
17
17
|
HelpCircle,
|
|
18
18
|
Loader2,
|
|
19
|
-
Lock,
|
|
20
19
|
Paperclip,
|
|
21
20
|
ScrollText,
|
|
22
21
|
Video,
|
|
@@ -32,7 +31,7 @@ import {
|
|
|
32
31
|
} from '../_data/services/course-structure.service';
|
|
33
32
|
import { HighlightedText } from './highlighted-text';
|
|
34
33
|
import { useStructureStore } from './store';
|
|
35
|
-
import type { Lesson, LessonStatus, LessonType
|
|
34
|
+
import type { Lesson, LessonStatus, LessonType } from './types';
|
|
36
35
|
import {
|
|
37
36
|
getLessonAttachedResourceCount,
|
|
38
37
|
getLessonUploadedVideoCount,
|
|
@@ -63,15 +62,6 @@ const STATUS_CONFIG: Record<LessonStatus, { label: string; dot: string }> = {
|
|
|
63
62
|
publicada: { label: 'Publicada', dot: 'bg-emerald-500' },
|
|
64
63
|
};
|
|
65
64
|
|
|
66
|
-
const VISIBILITY_CONFIG: Record<
|
|
67
|
-
Visibility,
|
|
68
|
-
{ icon: LucideIcon; color: string; label: string }
|
|
69
|
-
> = {
|
|
70
|
-
publico: { icon: Eye, color: 'text-emerald-500', label: 'Público' },
|
|
71
|
-
privado: { icon: EyeOff, color: 'text-muted-foreground', label: 'Privado' },
|
|
72
|
-
restrito: { icon: Lock, color: 'text-amber-500', label: 'Restrito' },
|
|
73
|
-
};
|
|
74
|
-
|
|
75
65
|
// ── Props ─────────────────────────────────────────────────────────────────────
|
|
76
66
|
|
|
77
67
|
interface TreeRowLessonProps {
|
|
@@ -98,9 +88,7 @@ export function TreeRowLesson({
|
|
|
98
88
|
const { request } = useApp();
|
|
99
89
|
|
|
100
90
|
const statusCfg = data.status ? STATUS_CONFIG[data.status] : null;
|
|
101
|
-
const
|
|
102
|
-
? VISIBILITY_CONFIG[data.visibility]
|
|
103
|
-
: null;
|
|
91
|
+
const isPublished = data.published === true;
|
|
104
92
|
|
|
105
93
|
const {
|
|
106
94
|
showStatusDot,
|
|
@@ -171,7 +159,6 @@ export function TreeRowLesson({
|
|
|
171
159
|
publicDescription: data.publicDescription,
|
|
172
160
|
privateDescription: data.privateDescription,
|
|
173
161
|
status: data.status,
|
|
174
|
-
visibility: data.visibility,
|
|
175
162
|
videoProvider: data.videoProvider,
|
|
176
163
|
videoUrl: data.videoUrl,
|
|
177
164
|
autoDuration: data.autoDuration,
|
|
@@ -215,24 +202,32 @@ export function TreeRowLesson({
|
|
|
215
202
|
data.type !== 'video' ? (
|
|
216
203
|
<Icon className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
|
|
217
204
|
) : videoJobVisualState === 'waiting' ? (
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
205
|
+
<Tooltip>
|
|
206
|
+
<TooltipTrigger asChild>
|
|
207
|
+
<span
|
|
208
|
+
aria-label="Conversão aguardando processamento"
|
|
209
|
+
className="inline-flex items-center"
|
|
210
|
+
>
|
|
211
|
+
<Clock3 className="size-3.5 shrink-0 text-amber-500" aria-hidden />
|
|
212
|
+
</span>
|
|
213
|
+
</TooltipTrigger>
|
|
214
|
+
<TooltipContent>Conversão aguardando processamento</TooltipContent>
|
|
215
|
+
</Tooltip>
|
|
225
216
|
) : videoJobVisualState === 'processing' ? (
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
217
|
+
<Tooltip>
|
|
218
|
+
<TooltipTrigger asChild>
|
|
219
|
+
<span
|
|
220
|
+
aria-label="Conversão em processamento"
|
|
221
|
+
className="inline-flex items-center"
|
|
222
|
+
>
|
|
223
|
+
<Loader2
|
|
224
|
+
className="size-3.5 shrink-0 animate-spin text-blue-500"
|
|
225
|
+
aria-hidden
|
|
226
|
+
/>
|
|
227
|
+
</span>
|
|
228
|
+
</TooltipTrigger>
|
|
229
|
+
<TooltipContent>Conversão em processamento</TooltipContent>
|
|
230
|
+
</Tooltip>
|
|
236
231
|
) : (
|
|
237
232
|
<Video className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
|
|
238
233
|
);
|
|
@@ -246,7 +241,7 @@ export function TreeRowLesson({
|
|
|
246
241
|
}}
|
|
247
242
|
role="treeitem"
|
|
248
243
|
aria-selected={isActive}
|
|
249
|
-
|
|
244
|
+
aria-label={`${cfg.label} · ${data.duration}min`}
|
|
250
245
|
className={cn(
|
|
251
246
|
'flex items-center gap-1.5 pl-7 pr-2 rounded-md cursor-pointer select-none text-sm h-full group',
|
|
252
247
|
'transition-colors duration-100',
|
|
@@ -290,39 +285,55 @@ export function TreeRowLesson({
|
|
|
290
285
|
{durationLabel}
|
|
291
286
|
</span>
|
|
292
287
|
{/* Visibility icon */}
|
|
293
|
-
{showVisibility &&
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const VisIcon = visibilityCfg.icon;
|
|
297
|
-
return (
|
|
288
|
+
{showVisibility && (
|
|
289
|
+
<Tooltip>
|
|
290
|
+
<TooltipTrigger asChild>
|
|
298
291
|
<span
|
|
299
|
-
|
|
300
|
-
aria-label={t('visibility', { value: visibilityCfg.label })}
|
|
292
|
+
aria-label={isPublished ? t('published') : t('draft')}
|
|
301
293
|
className="inline-flex items-center"
|
|
302
294
|
>
|
|
303
|
-
|
|
304
|
-
className=
|
|
305
|
-
|
|
295
|
+
{isPublished ? (
|
|
296
|
+
<Eye className="size-3 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
297
|
+
) : (
|
|
298
|
+
<EyeOff className="size-3 shrink-0 text-muted-foreground" />
|
|
299
|
+
)}
|
|
306
300
|
</span>
|
|
307
|
-
|
|
308
|
-
|
|
301
|
+
</TooltipTrigger>
|
|
302
|
+
<TooltipContent>
|
|
303
|
+
{isPublished ? t('published') : t('draft')}
|
|
304
|
+
</TooltipContent>
|
|
305
|
+
</Tooltip>
|
|
306
|
+
)}
|
|
309
307
|
{/* Status dot */}
|
|
310
308
|
{showStatusDot && statusCfg && (
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
<Tooltip>
|
|
310
|
+
<TooltipTrigger asChild>
|
|
311
|
+
<span
|
|
312
|
+
className={cn(
|
|
313
|
+
'size-1.5 rounded-full shrink-0',
|
|
314
|
+
statusCfg.dot
|
|
315
|
+
)}
|
|
316
|
+
aria-label={t('status', { value: statusCfg.label })}
|
|
317
|
+
/>
|
|
318
|
+
</TooltipTrigger>
|
|
319
|
+
<TooltipContent>
|
|
320
|
+
{t('status', { value: statusCfg.label })}
|
|
321
|
+
</TooltipContent>
|
|
322
|
+
</Tooltip>
|
|
316
323
|
)}
|
|
317
324
|
{/* Video linked indicator */}
|
|
318
325
|
{showVideoIndicator && data.type === 'video' && data.videoUrl && (
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
+
<Tooltip>
|
|
327
|
+
<TooltipTrigger asChild>
|
|
328
|
+
<span
|
|
329
|
+
aria-label={t('videoLinked')}
|
|
330
|
+
className="inline-flex items-center"
|
|
331
|
+
>
|
|
332
|
+
<Film className="size-3 shrink-0 text-violet-500" />
|
|
333
|
+
</span>
|
|
334
|
+
</TooltipTrigger>
|
|
335
|
+
<TooltipContent>{t('videoLinked')}</TooltipContent>
|
|
336
|
+
</Tooltip>
|
|
326
337
|
)}
|
|
327
338
|
{/* Uploaded videos indicator */}
|
|
328
339
|
{showVideoIndicator && uploadedVideoCount > 0 && (
|
|
@@ -364,13 +375,17 @@ export function TreeRowLesson({
|
|
|
364
375
|
{showTranscriptionIndicator &&
|
|
365
376
|
data.type === 'video' &&
|
|
366
377
|
data.transcription && (
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
<Tooltip>
|
|
379
|
+
<TooltipTrigger asChild>
|
|
380
|
+
<span
|
|
381
|
+
aria-label={t('hasTranscription')}
|
|
382
|
+
className="inline-flex items-center"
|
|
383
|
+
>
|
|
384
|
+
<ScrollText className="size-3 shrink-0 text-emerald-500" />
|
|
385
|
+
</span>
|
|
386
|
+
</TooltipTrigger>
|
|
387
|
+
<TooltipContent>{t('hasTranscription')}</TooltipContent>
|
|
388
|
+
</Tooltip>
|
|
374
389
|
)}
|
|
375
390
|
</div>
|
|
376
391
|
)}
|
|
@@ -10,6 +10,8 @@ import { cn } from '@/lib/utils';
|
|
|
10
10
|
import {
|
|
11
11
|
ChevronDown,
|
|
12
12
|
ChevronRight,
|
|
13
|
+
Eye,
|
|
14
|
+
EyeOff,
|
|
13
15
|
Layers,
|
|
14
16
|
Paperclip,
|
|
15
17
|
Video,
|
|
@@ -54,7 +56,7 @@ export function TreeRowSession({
|
|
|
54
56
|
const cancelRename = useStructureStore((s) => s.cancelRename);
|
|
55
57
|
const startRename = useStructureStore((s) => s.startRename);
|
|
56
58
|
const renameItem = useStructureStore((s) => s.renameItem);
|
|
57
|
-
const { showCode } = useTreeDisplaySettings();
|
|
59
|
+
const { showCode, showVisibility } = useTreeDisplaySettings();
|
|
58
60
|
|
|
59
61
|
const updateSession = useUpdateSessionMutation();
|
|
60
62
|
|
|
@@ -79,8 +81,7 @@ export function TreeRowSession({
|
|
|
79
81
|
duration: data.duration,
|
|
80
82
|
code: data.code,
|
|
81
83
|
description: '',
|
|
82
|
-
|
|
83
|
-
published: data.published ?? false,
|
|
84
|
+
published: data.published,
|
|
84
85
|
},
|
|
85
86
|
});
|
|
86
87
|
}
|
|
@@ -96,6 +97,7 @@ export function TreeRowSession({
|
|
|
96
97
|
|
|
97
98
|
const durationH = Math.floor((data.duration ?? 0) / 60);
|
|
98
99
|
const durationM = (data.duration ?? 0) % 60;
|
|
100
|
+
const isPublished = data.published === true;
|
|
99
101
|
const durationLabel =
|
|
100
102
|
durationH > 0
|
|
101
103
|
? `${durationH}h${durationM > 0 ? ` ${durationM}m` : ''}`
|
|
@@ -138,7 +140,12 @@ export function TreeRowSession({
|
|
|
138
140
|
|
|
139
141
|
{/* Session icon */}
|
|
140
142
|
<Layers
|
|
141
|
-
className=
|
|
143
|
+
className={cn(
|
|
144
|
+
'size-3.5 shrink-0 transition-colors',
|
|
145
|
+
isPublished
|
|
146
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
147
|
+
: 'text-muted-foreground/70'
|
|
148
|
+
)}
|
|
142
149
|
aria-hidden
|
|
143
150
|
/>
|
|
144
151
|
|
|
@@ -162,6 +169,26 @@ export function TreeRowSession({
|
|
|
162
169
|
{/* Meta badges (hidden during rename) */}
|
|
163
170
|
{!isRenaming && (
|
|
164
171
|
<div className="flex items-center gap-1 shrink-0">
|
|
172
|
+
{showVisibility && (
|
|
173
|
+
<Tooltip>
|
|
174
|
+
<TooltipTrigger asChild>
|
|
175
|
+
<span
|
|
176
|
+
className="inline-flex items-center"
|
|
177
|
+
aria-label={isPublished ? t('published') : t('draft')}
|
|
178
|
+
>
|
|
179
|
+
{isPublished ? (
|
|
180
|
+
<Eye className="size-3 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
181
|
+
) : (
|
|
182
|
+
<EyeOff className="size-3 shrink-0 text-muted-foreground" />
|
|
183
|
+
)}
|
|
184
|
+
</span>
|
|
185
|
+
</TooltipTrigger>
|
|
186
|
+
<TooltipContent>
|
|
187
|
+
{isPublished ? t('published') : t('draft')}
|
|
188
|
+
</TooltipContent>
|
|
189
|
+
</Tooltip>
|
|
190
|
+
)}
|
|
191
|
+
|
|
165
192
|
{showCode && (
|
|
166
193
|
<span className="inline-flex items-center rounded border border-border/60 bg-muted/60 px-1 text-[0.6rem] font-mono text-muted-foreground leading-4">
|
|
167
194
|
{data.code}
|
|
@@ -207,16 +234,6 @@ export function TreeRowSession({
|
|
|
207
234
|
</TooltipContent>
|
|
208
235
|
</Tooltip>
|
|
209
236
|
)}
|
|
210
|
-
{/* Published status dot */}
|
|
211
|
-
<span
|
|
212
|
-
className={cn(
|
|
213
|
-
'size-1.5 rounded-full shrink-0',
|
|
214
|
-
data.published === false
|
|
215
|
-
? 'bg-muted-foreground/30'
|
|
216
|
-
: 'bg-emerald-500'
|
|
217
|
-
)}
|
|
218
|
-
title={data.published === false ? t('draft') : t('published')}
|
|
219
|
-
/>
|
|
220
237
|
</div>
|
|
221
238
|
)}
|
|
222
239
|
</div>
|
|
@@ -35,8 +35,7 @@ export interface Session {
|
|
|
35
35
|
description?: string;
|
|
36
36
|
duration: number;
|
|
37
37
|
order: number;
|
|
38
|
-
|
|
39
|
-
published?: boolean;
|
|
38
|
+
published: boolean;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
export interface Resource {
|
|
@@ -95,6 +94,7 @@ export interface Lesson {
|
|
|
95
94
|
order: number;
|
|
96
95
|
status?: LessonStatus;
|
|
97
96
|
visibility?: Visibility;
|
|
97
|
+
published?: boolean;
|
|
98
98
|
// Video
|
|
99
99
|
videoProvider?: VideoProvider;
|
|
100
100
|
videoUrl?: string;
|
|
@@ -127,7 +127,6 @@ export type SessionFormValues = {
|
|
|
127
127
|
title: string;
|
|
128
128
|
description: string;
|
|
129
129
|
duration: number;
|
|
130
|
-
visibility: Visibility;
|
|
131
130
|
published: boolean;
|
|
132
131
|
};
|
|
133
132
|
|
|
@@ -139,7 +138,7 @@ export type LessonFormValues = {
|
|
|
139
138
|
type: LessonType;
|
|
140
139
|
duration: number;
|
|
141
140
|
status: LessonStatus;
|
|
142
|
-
|
|
141
|
+
published: boolean;
|
|
143
142
|
videoProvider?: VideoProvider;
|
|
144
143
|
videoUrl?: string;
|
|
145
144
|
autoDuration?: boolean;
|