@hed-hog/lms 0.0.314 → 0.0.315
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/class-group/class-group.controller.d.ts +2 -2
- package/dist/class-group/class-group.service.d.ts +2 -2
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +3 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +14 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +128 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +23 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +41 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +126 -8
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/data/menu.yaml +23 -7
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
- package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
- package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
- package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
- package/hedhog/table/enterprise_user.yaml +1 -1
- package/package.json +8 -8
- package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
- package/src/enterprise/enterprise.controller.ts +9 -1
- package/src/enterprise/enterprise.service.ts +147 -4
- package/src/instructor/instructor.controller.ts +36 -9
- package/src/instructor/instructor.service.ts +140 -10
|
@@ -1,271 +1,271 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Input } from '@/components/ui/input';
|
|
4
|
-
import { cn } from '@/lib/utils';
|
|
5
|
-
import {
|
|
6
|
-
ClipboardList,
|
|
7
|
-
Eye,
|
|
8
|
-
EyeOff,
|
|
9
|
-
FileText,
|
|
10
|
-
Film,
|
|
11
|
-
HelpCircle,
|
|
12
|
-
Lock,
|
|
13
|
-
Paperclip,
|
|
14
|
-
ScrollText,
|
|
15
|
-
Video,
|
|
16
|
-
type LucideIcon,
|
|
17
|
-
} from 'lucide-react';
|
|
18
|
-
import { useEffect, useRef } from 'react';
|
|
19
|
-
import { useUpdateLessonMutation } from '../_data/use-course-structure-mutations';
|
|
20
|
-
import { HighlightedText } from './highlighted-text';
|
|
21
|
-
import { useStructureStore } from './store';
|
|
22
|
-
import type { Lesson, LessonStatus, LessonType, Visibility } from './types';
|
|
23
|
-
import { useTreeDisplaySettings } from './use-tree-display-settings';
|
|
24
|
-
|
|
25
|
-
// ── Type configs ──────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
const LESSON_TYPE_CONFIG: Record<
|
|
28
|
-
LessonType,
|
|
29
|
-
{ icon: LucideIcon; color: string; label: string }
|
|
30
|
-
> = {
|
|
31
|
-
video: { icon: Video, color: 'text-violet-500', label: 'Vídeo' },
|
|
32
|
-
post: { icon: FileText, color: 'text-emerald-500', label: 'Post' },
|
|
33
|
-
questao: { icon: HelpCircle, color: 'text-amber-500', label: 'Quiz' },
|
|
34
|
-
exercicio: {
|
|
35
|
-
icon: ClipboardList,
|
|
36
|
-
color: 'text-rose-500',
|
|
37
|
-
label: 'Exercício',
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const STATUS_CONFIG: Record<LessonStatus, { label: string; dot: string }> = {
|
|
42
|
-
preparada: { label: 'Preparada', dot: 'bg-slate-400' },
|
|
43
|
-
gravada: { label: 'Gravada', dot: 'bg-sky-500' },
|
|
44
|
-
editada: { label: 'Editada', dot: 'bg-violet-500' },
|
|
45
|
-
finalizada: { label: 'Finalizada', dot: 'bg-amber-500' },
|
|
46
|
-
publicada: { label: 'Publicada', dot: 'bg-emerald-500' },
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const VISIBILITY_CONFIG: Record<
|
|
50
|
-
Visibility,
|
|
51
|
-
{ icon: LucideIcon; color: string; label: string }
|
|
52
|
-
> = {
|
|
53
|
-
publico: { icon: Eye, color: 'text-emerald-500', label: 'Público' },
|
|
54
|
-
privado: { icon: EyeOff, color: 'text-muted-foreground', label: 'Privado' },
|
|
55
|
-
restrito: { icon: Lock, color: 'text-amber-500', label: 'Restrito' },
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// ── Props ─────────────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
interface TreeRowLessonProps {
|
|
61
|
-
data: Lesson;
|
|
62
|
-
isSelected: boolean;
|
|
63
|
-
isActive: boolean;
|
|
64
|
-
query: string;
|
|
65
|
-
isMatched: boolean;
|
|
66
|
-
onClick: (e: React.MouseEvent) => void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Component ─────────────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
export function TreeRowLesson({
|
|
72
|
-
data,
|
|
73
|
-
isSelected,
|
|
74
|
-
isActive,
|
|
75
|
-
query,
|
|
76
|
-
onClick,
|
|
77
|
-
}: TreeRowLessonProps) {
|
|
78
|
-
const cfg = LESSON_TYPE_CONFIG[data.type];
|
|
79
|
-
const Icon = cfg.icon;
|
|
80
|
-
|
|
81
|
-
const statusCfg = data.status ? STATUS_CONFIG[data.status] : null;
|
|
82
|
-
const visibilityCfg = data.visibility
|
|
83
|
-
? VISIBILITY_CONFIG[data.visibility]
|
|
84
|
-
: null;
|
|
85
|
-
|
|
86
|
-
const {
|
|
87
|
-
showStatusDot,
|
|
88
|
-
showVisibility,
|
|
89
|
-
showCode,
|
|
90
|
-
showVideoIndicator,
|
|
91
|
-
showResourcesIndicator,
|
|
92
|
-
showTranscriptionIndicator,
|
|
93
|
-
} = useTreeDisplaySettings();
|
|
94
|
-
|
|
95
|
-
const inlineRenamingId = useStructureStore((s) => s.inlineRenamingId);
|
|
96
|
-
const cancelRename = useStructureStore((s) => s.cancelRename);
|
|
97
|
-
const renameItem = useStructureStore((s) => s.renameItem);
|
|
98
|
-
const startRename = useStructureStore((s) => s.startRename);
|
|
99
|
-
|
|
100
|
-
const updateLesson = useUpdateLessonMutation();
|
|
101
|
-
|
|
102
|
-
const isRenaming = inlineRenamingId === data.id;
|
|
103
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (isRenaming) {
|
|
107
|
-
inputRef.current?.focus();
|
|
108
|
-
inputRef.current?.select();
|
|
109
|
-
}
|
|
110
|
-
}, [isRenaming]);
|
|
111
|
-
|
|
112
|
-
function commitAndPersist(newTitle: string) {
|
|
113
|
-
const trimmed = newTitle.trim() || data.title;
|
|
114
|
-
renameItem(data.id, trimmed);
|
|
115
|
-
cancelRename();
|
|
116
|
-
updateLesson.mutate({
|
|
117
|
-
lessonId: data.id,
|
|
118
|
-
sessionId: data.sessionId,
|
|
119
|
-
formValues: {
|
|
120
|
-
title: trimmed,
|
|
121
|
-
type: data.type,
|
|
122
|
-
duration: data.duration,
|
|
123
|
-
publicDescription: data.publicDescription,
|
|
124
|
-
privateDescription: data.privateDescription,
|
|
125
|
-
status: data.status,
|
|
126
|
-
visibility: data.visibility,
|
|
127
|
-
videoProvider: data.videoProvider,
|
|
128
|
-
videoUrl: data.videoUrl,
|
|
129
|
-
autoDuration: data.autoDuration,
|
|
130
|
-
transcription: data.transcription,
|
|
131
|
-
postContent: data.postContent,
|
|
132
|
-
linkedExam: data.linkedExam,
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
138
|
-
e.stopPropagation();
|
|
139
|
-
if (e.key === 'Enter') {
|
|
140
|
-
commitAndPersist(inputRef.current?.value ?? data.title);
|
|
141
|
-
} else if (e.key === 'Escape') {
|
|
142
|
-
cancelRename();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const durationH = Math.floor(data.duration / 60);
|
|
147
|
-
const durationM = data.duration % 60;
|
|
148
|
-
const durationLabel =
|
|
149
|
-
durationH > 0
|
|
150
|
-
? `${durationH}h${durationM > 0 ? `${durationM}m` : ''}`
|
|
151
|
-
: `${durationM}m`;
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<div
|
|
155
|
-
onClick={isRenaming ? undefined : onClick}
|
|
156
|
-
onDoubleClick={(e) => {
|
|
157
|
-
e.stopPropagation();
|
|
158
|
-
startRename(data.id);
|
|
159
|
-
}}
|
|
160
|
-
role="treeitem"
|
|
161
|
-
aria-selected={isActive}
|
|
162
|
-
title={`${cfg.label} · ${data.duration}min`}
|
|
163
|
-
className={cn(
|
|
164
|
-
'flex items-center gap-1.5 pl-7 pr-2 rounded-md cursor-pointer select-none text-sm h-full group',
|
|
165
|
-
'transition-colors duration-100',
|
|
166
|
-
isActive &&
|
|
167
|
-
'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
|
|
168
|
-
isSelected && !isActive && 'bg-accent/60',
|
|
169
|
-
!isActive && !isSelected && 'hover:bg-muted/60'
|
|
170
|
-
)}
|
|
171
|
-
>
|
|
172
|
-
{/* Type icon */}
|
|
173
|
-
<Icon className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
|
|
174
|
-
|
|
175
|
-
{/* Inline rename or title */}
|
|
176
|
-
{isRenaming ? (
|
|
177
|
-
<Input
|
|
178
|
-
ref={inputRef}
|
|
179
|
-
defaultValue={data.title}
|
|
180
|
-
className="h-5 text-xs px-1 py-0 flex-1 min-w-0"
|
|
181
|
-
onClick={(e) => e.stopPropagation()}
|
|
182
|
-
onBlur={(e) => commitAndPersist(e.target.value)}
|
|
183
|
-
onKeyDown={handleKeyDown}
|
|
184
|
-
aria-label="Renomear aula"
|
|
185
|
-
/>
|
|
186
|
-
) : (
|
|
187
|
-
<span className="truncate flex-1 text-xs leading-tight">
|
|
188
|
-
<HighlightedText text={data.title} query={query} />
|
|
189
|
-
</span>
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{/* Meta (hidden during rename) */}
|
|
193
|
-
{!isRenaming && (
|
|
194
|
-
<div className="flex items-center gap-1 shrink-0">
|
|
195
|
-
{/* Code */}
|
|
196
|
-
{showCode && (
|
|
197
|
-
<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">
|
|
198
|
-
{data.code}
|
|
199
|
-
</span>
|
|
200
|
-
)}
|
|
201
|
-
{/* Duration */}
|
|
202
|
-
<span className="text-[0.6rem] text-muted-foreground tabular-nums">
|
|
203
|
-
{durationLabel}
|
|
204
|
-
</span>
|
|
205
|
-
{/* Visibility icon */}
|
|
206
|
-
{showVisibility &&
|
|
207
|
-
visibilityCfg &&
|
|
208
|
-
(() => {
|
|
209
|
-
const VisIcon = visibilityCfg.icon;
|
|
210
|
-
return (
|
|
211
|
-
<span
|
|
212
|
-
title={`Visibilidade: ${visibilityCfg.label}`}
|
|
213
|
-
aria-label={`Visibilidade: ${visibilityCfg.label}`}
|
|
214
|
-
className="inline-flex items-center"
|
|
215
|
-
>
|
|
216
|
-
<VisIcon
|
|
217
|
-
className={cn('size-3 shrink-0', visibilityCfg.color)}
|
|
218
|
-
/>
|
|
219
|
-
</span>
|
|
220
|
-
);
|
|
221
|
-
})()}
|
|
222
|
-
{/* Status dot */}
|
|
223
|
-
{showStatusDot && statusCfg && (
|
|
224
|
-
<span
|
|
225
|
-
className={cn('size-1.5 rounded-full shrink-0', statusCfg.dot)}
|
|
226
|
-
title={`Status: ${statusCfg.label}`}
|
|
227
|
-
aria-label={`Status: ${statusCfg.label}`}
|
|
228
|
-
/>
|
|
229
|
-
)}
|
|
230
|
-
{/* Video linked indicator */}
|
|
231
|
-
{showVideoIndicator && data.type === 'video' && data.videoUrl && (
|
|
232
|
-
<span
|
|
233
|
-
title="Vídeo vinculado"
|
|
234
|
-
aria-label="Vídeo vinculado"
|
|
235
|
-
className="inline-flex items-center"
|
|
236
|
-
>
|
|
237
|
-
<Film className="size-3 shrink-0 text-violet-500" />
|
|
238
|
-
</span>
|
|
239
|
-
)}
|
|
240
|
-
{/* Resources indicator */}
|
|
241
|
-
{showResourcesIndicator && data.resources.length > 0 && (
|
|
242
|
-
<span
|
|
243
|
-
title={`${data.resources.length} recurso${data.resources.length > 1 ? 's' : ''} para download`}
|
|
244
|
-
aria-label={`${data.resources.length} recursos`}
|
|
245
|
-
className="inline-flex items-center gap-0.5"
|
|
246
|
-
>
|
|
247
|
-
<Paperclip className="size-3 shrink-0 text-sky-500" />
|
|
248
|
-
{data.resources.length > 1 && (
|
|
249
|
-
<span className="text-[0.55rem] text-sky-500 tabular-nums leading-none">
|
|
250
|
-
{data.resources.length}
|
|
251
|
-
</span>
|
|
252
|
-
)}
|
|
253
|
-
</span>
|
|
254
|
-
)}
|
|
255
|
-
{/* Transcription indicator */}
|
|
256
|
-
{showTranscriptionIndicator &&
|
|
257
|
-
data.type === 'video' &&
|
|
258
|
-
data.transcription && (
|
|
259
|
-
<span
|
|
260
|
-
title="Possui transcrição"
|
|
261
|
-
aria-label="Possui transcrição"
|
|
262
|
-
className="inline-flex items-center"
|
|
263
|
-
>
|
|
264
|
-
<ScrollText className="size-3 shrink-0 text-emerald-500" />
|
|
265
|
-
</span>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Input } from '@/components/ui/input';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import {
|
|
6
|
+
ClipboardList,
|
|
7
|
+
Eye,
|
|
8
|
+
EyeOff,
|
|
9
|
+
FileText,
|
|
10
|
+
Film,
|
|
11
|
+
HelpCircle,
|
|
12
|
+
Lock,
|
|
13
|
+
Paperclip,
|
|
14
|
+
ScrollText,
|
|
15
|
+
Video,
|
|
16
|
+
type LucideIcon,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { useEffect, useRef } from 'react';
|
|
19
|
+
import { useUpdateLessonMutation } from '../_data/use-course-structure-mutations';
|
|
20
|
+
import { HighlightedText } from './highlighted-text';
|
|
21
|
+
import { useStructureStore } from './store';
|
|
22
|
+
import type { Lesson, LessonStatus, LessonType, Visibility } from './types';
|
|
23
|
+
import { useTreeDisplaySettings } from './use-tree-display-settings';
|
|
24
|
+
|
|
25
|
+
// ── Type configs ──────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const LESSON_TYPE_CONFIG: Record<
|
|
28
|
+
LessonType,
|
|
29
|
+
{ icon: LucideIcon; color: string; label: string }
|
|
30
|
+
> = {
|
|
31
|
+
video: { icon: Video, color: 'text-violet-500', label: 'Vídeo' },
|
|
32
|
+
post: { icon: FileText, color: 'text-emerald-500', label: 'Post' },
|
|
33
|
+
questao: { icon: HelpCircle, color: 'text-amber-500', label: 'Quiz' },
|
|
34
|
+
exercicio: {
|
|
35
|
+
icon: ClipboardList,
|
|
36
|
+
color: 'text-rose-500',
|
|
37
|
+
label: 'Exercício',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const STATUS_CONFIG: Record<LessonStatus, { label: string; dot: string }> = {
|
|
42
|
+
preparada: { label: 'Preparada', dot: 'bg-slate-400' },
|
|
43
|
+
gravada: { label: 'Gravada', dot: 'bg-sky-500' },
|
|
44
|
+
editada: { label: 'Editada', dot: 'bg-violet-500' },
|
|
45
|
+
finalizada: { label: 'Finalizada', dot: 'bg-amber-500' },
|
|
46
|
+
publicada: { label: 'Publicada', dot: 'bg-emerald-500' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const VISIBILITY_CONFIG: Record<
|
|
50
|
+
Visibility,
|
|
51
|
+
{ icon: LucideIcon; color: string; label: string }
|
|
52
|
+
> = {
|
|
53
|
+
publico: { icon: Eye, color: 'text-emerald-500', label: 'Público' },
|
|
54
|
+
privado: { icon: EyeOff, color: 'text-muted-foreground', label: 'Privado' },
|
|
55
|
+
restrito: { icon: Lock, color: 'text-amber-500', label: 'Restrito' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ── Props ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
interface TreeRowLessonProps {
|
|
61
|
+
data: Lesson;
|
|
62
|
+
isSelected: boolean;
|
|
63
|
+
isActive: boolean;
|
|
64
|
+
query: string;
|
|
65
|
+
isMatched: boolean;
|
|
66
|
+
onClick: (e: React.MouseEvent) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export function TreeRowLesson({
|
|
72
|
+
data,
|
|
73
|
+
isSelected,
|
|
74
|
+
isActive,
|
|
75
|
+
query,
|
|
76
|
+
onClick,
|
|
77
|
+
}: TreeRowLessonProps) {
|
|
78
|
+
const cfg = LESSON_TYPE_CONFIG[data.type];
|
|
79
|
+
const Icon = cfg.icon;
|
|
80
|
+
|
|
81
|
+
const statusCfg = data.status ? STATUS_CONFIG[data.status] : null;
|
|
82
|
+
const visibilityCfg = data.visibility
|
|
83
|
+
? VISIBILITY_CONFIG[data.visibility]
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
showStatusDot,
|
|
88
|
+
showVisibility,
|
|
89
|
+
showCode,
|
|
90
|
+
showVideoIndicator,
|
|
91
|
+
showResourcesIndicator,
|
|
92
|
+
showTranscriptionIndicator,
|
|
93
|
+
} = useTreeDisplaySettings();
|
|
94
|
+
|
|
95
|
+
const inlineRenamingId = useStructureStore((s) => s.inlineRenamingId);
|
|
96
|
+
const cancelRename = useStructureStore((s) => s.cancelRename);
|
|
97
|
+
const renameItem = useStructureStore((s) => s.renameItem);
|
|
98
|
+
const startRename = useStructureStore((s) => s.startRename);
|
|
99
|
+
|
|
100
|
+
const updateLesson = useUpdateLessonMutation();
|
|
101
|
+
|
|
102
|
+
const isRenaming = inlineRenamingId === data.id;
|
|
103
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (isRenaming) {
|
|
107
|
+
inputRef.current?.focus();
|
|
108
|
+
inputRef.current?.select();
|
|
109
|
+
}
|
|
110
|
+
}, [isRenaming]);
|
|
111
|
+
|
|
112
|
+
function commitAndPersist(newTitle: string) {
|
|
113
|
+
const trimmed = newTitle.trim() || data.title;
|
|
114
|
+
renameItem(data.id, trimmed);
|
|
115
|
+
cancelRename();
|
|
116
|
+
updateLesson.mutate({
|
|
117
|
+
lessonId: data.id,
|
|
118
|
+
sessionId: data.sessionId,
|
|
119
|
+
formValues: {
|
|
120
|
+
title: trimmed,
|
|
121
|
+
type: data.type,
|
|
122
|
+
duration: data.duration,
|
|
123
|
+
publicDescription: data.publicDescription,
|
|
124
|
+
privateDescription: data.privateDescription,
|
|
125
|
+
status: data.status,
|
|
126
|
+
visibility: data.visibility,
|
|
127
|
+
videoProvider: data.videoProvider,
|
|
128
|
+
videoUrl: data.videoUrl,
|
|
129
|
+
autoDuration: data.autoDuration,
|
|
130
|
+
transcription: data.transcription,
|
|
131
|
+
postContent: data.postContent,
|
|
132
|
+
linkedExam: data.linkedExam,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
if (e.key === 'Enter') {
|
|
140
|
+
commitAndPersist(inputRef.current?.value ?? data.title);
|
|
141
|
+
} else if (e.key === 'Escape') {
|
|
142
|
+
cancelRename();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const durationH = Math.floor(data.duration / 60);
|
|
147
|
+
const durationM = data.duration % 60;
|
|
148
|
+
const durationLabel =
|
|
149
|
+
durationH > 0
|
|
150
|
+
? `${durationH}h${durationM > 0 ? `${durationM}m` : ''}`
|
|
151
|
+
: `${durationM}m`;
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div
|
|
155
|
+
onClick={isRenaming ? undefined : onClick}
|
|
156
|
+
onDoubleClick={(e) => {
|
|
157
|
+
e.stopPropagation();
|
|
158
|
+
startRename(data.id);
|
|
159
|
+
}}
|
|
160
|
+
role="treeitem"
|
|
161
|
+
aria-selected={isActive}
|
|
162
|
+
title={`${cfg.label} · ${data.duration}min`}
|
|
163
|
+
className={cn(
|
|
164
|
+
'flex items-center gap-1.5 pl-7 pr-2 rounded-md cursor-pointer select-none text-sm h-full group',
|
|
165
|
+
'transition-colors duration-100',
|
|
166
|
+
isActive &&
|
|
167
|
+
'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
|
|
168
|
+
isSelected && !isActive && 'bg-accent/60',
|
|
169
|
+
!isActive && !isSelected && 'hover:bg-muted/60'
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{/* Type icon */}
|
|
173
|
+
<Icon className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
|
|
174
|
+
|
|
175
|
+
{/* Inline rename or title */}
|
|
176
|
+
{isRenaming ? (
|
|
177
|
+
<Input
|
|
178
|
+
ref={inputRef}
|
|
179
|
+
defaultValue={data.title}
|
|
180
|
+
className="h-5 text-xs px-1 py-0 flex-1 min-w-0"
|
|
181
|
+
onClick={(e) => e.stopPropagation()}
|
|
182
|
+
onBlur={(e) => commitAndPersist(e.target.value)}
|
|
183
|
+
onKeyDown={handleKeyDown}
|
|
184
|
+
aria-label="Renomear aula"
|
|
185
|
+
/>
|
|
186
|
+
) : (
|
|
187
|
+
<span className="truncate flex-1 text-xs leading-tight">
|
|
188
|
+
<HighlightedText text={data.title} query={query} />
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Meta (hidden during rename) */}
|
|
193
|
+
{!isRenaming && (
|
|
194
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
195
|
+
{/* Code */}
|
|
196
|
+
{showCode && (
|
|
197
|
+
<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">
|
|
198
|
+
{data.code}
|
|
199
|
+
</span>
|
|
200
|
+
)}
|
|
201
|
+
{/* Duration */}
|
|
202
|
+
<span className="text-[0.6rem] text-muted-foreground tabular-nums">
|
|
203
|
+
{durationLabel}
|
|
204
|
+
</span>
|
|
205
|
+
{/* Visibility icon */}
|
|
206
|
+
{showVisibility &&
|
|
207
|
+
visibilityCfg &&
|
|
208
|
+
(() => {
|
|
209
|
+
const VisIcon = visibilityCfg.icon;
|
|
210
|
+
return (
|
|
211
|
+
<span
|
|
212
|
+
title={`Visibilidade: ${visibilityCfg.label}`}
|
|
213
|
+
aria-label={`Visibilidade: ${visibilityCfg.label}`}
|
|
214
|
+
className="inline-flex items-center"
|
|
215
|
+
>
|
|
216
|
+
<VisIcon
|
|
217
|
+
className={cn('size-3 shrink-0', visibilityCfg.color)}
|
|
218
|
+
/>
|
|
219
|
+
</span>
|
|
220
|
+
);
|
|
221
|
+
})()}
|
|
222
|
+
{/* Status dot */}
|
|
223
|
+
{showStatusDot && statusCfg && (
|
|
224
|
+
<span
|
|
225
|
+
className={cn('size-1.5 rounded-full shrink-0', statusCfg.dot)}
|
|
226
|
+
title={`Status: ${statusCfg.label}`}
|
|
227
|
+
aria-label={`Status: ${statusCfg.label}`}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
{/* Video linked indicator */}
|
|
231
|
+
{showVideoIndicator && data.type === 'video' && data.videoUrl && (
|
|
232
|
+
<span
|
|
233
|
+
title="Vídeo vinculado"
|
|
234
|
+
aria-label="Vídeo vinculado"
|
|
235
|
+
className="inline-flex items-center"
|
|
236
|
+
>
|
|
237
|
+
<Film className="size-3 shrink-0 text-violet-500" />
|
|
238
|
+
</span>
|
|
239
|
+
)}
|
|
240
|
+
{/* Resources indicator */}
|
|
241
|
+
{showResourcesIndicator && data.resources.length > 0 && (
|
|
242
|
+
<span
|
|
243
|
+
title={`${data.resources.length} recurso${data.resources.length > 1 ? 's' : ''} para download`}
|
|
244
|
+
aria-label={`${data.resources.length} recursos`}
|
|
245
|
+
className="inline-flex items-center gap-0.5"
|
|
246
|
+
>
|
|
247
|
+
<Paperclip className="size-3 shrink-0 text-sky-500" />
|
|
248
|
+
{data.resources.length > 1 && (
|
|
249
|
+
<span className="text-[0.55rem] text-sky-500 tabular-nums leading-none">
|
|
250
|
+
{data.resources.length}
|
|
251
|
+
</span>
|
|
252
|
+
)}
|
|
253
|
+
</span>
|
|
254
|
+
)}
|
|
255
|
+
{/* Transcription indicator */}
|
|
256
|
+
{showTranscriptionIndicator &&
|
|
257
|
+
data.type === 'video' &&
|
|
258
|
+
data.transcription && (
|
|
259
|
+
<span
|
|
260
|
+
title="Possui transcrição"
|
|
261
|
+
aria-label="Possui transcrição"
|
|
262
|
+
className="inline-flex items-center"
|
|
263
|
+
>
|
|
264
|
+
<ScrollText className="size-3 shrink-0 text-emerald-500" />
|
|
265
|
+
</span>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|