@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.
- package/dist/course/course-structure.controller.d.ts +60 -0
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +79 -0
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +61 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +326 -1
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +52 -4
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.service.d.ts +52 -5
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +78 -57
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -1
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +4 -1
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/move-lesson.dto.d.ts +10 -0
- package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
- package/dist/course/dto/move-lesson.dto.js +28 -0
- package/dist/course/dto/move-lesson.dto.js.map +1 -0
- package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
- package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/paste-lessons.dto.js +24 -0
- package/dist/course/dto/paste-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.js +24 -0
- package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.js +24 -0
- package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
- package/dist/training/training.controller.js +1 -1
- package/dist/training/training.controller.js.map +1 -1
- package/hedhog/data/image_type.yaml +20 -0
- package/hedhog/data/menu.yaml +2 -2
- package/hedhog/data/route.yaml +60 -6
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
- package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +10 -1
- package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -3
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +32 -0
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
- package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
- package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
- package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
- package/hedhog/frontend/messages/en.json +91 -11
- package/hedhog/frontend/messages/pt.json +91 -11
- package/hedhog/table/course.yaml +1 -1
- package/hedhog/table/image_type.yaml +14 -0
- package/package.json +7 -7
- package/src/course/course-structure.controller.ts +63 -0
- package/src/course/course-structure.service.ts +390 -3
- package/src/course/course.service.ts +59 -27
- package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
- package/src/course/dto/create-course.dto.ts +4 -1
- package/src/course/dto/move-lesson.dto.ts +17 -0
- package/src/course/dto/paste-lessons.dto.ts +9 -0
- package/src/course/dto/reorder-lessons.dto.ts +10 -0
- package/src/course/dto/reorder-sessions.dto.ts +10 -0
- package/src/training/training.controller.ts +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
4
|
+
import { SearchX } from 'lucide-react';
|
|
5
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useStructureStore } from './store';
|
|
8
|
+
import { buildLessonCountMap, buildVisibleItems } from './tree-helpers';
|
|
9
|
+
import { TreeRow } from './tree-row';
|
|
10
|
+
import type { FlatItem } from './types';
|
|
11
|
+
|
|
12
|
+
// Row height estimates per node type (px)
|
|
13
|
+
const ROW_HEIGHT: Record<FlatItem['type'], number> = {
|
|
14
|
+
course: 34,
|
|
15
|
+
session: 32,
|
|
16
|
+
lesson: 28,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function CourseTree() {
|
|
20
|
+
const course = useStructureStore((s) => s.course);
|
|
21
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
22
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
23
|
+
const expandedIds = useStructureStore((s) => s.expandedIds);
|
|
24
|
+
const filterQuery = useStructureStore((s) => s.filterQuery);
|
|
25
|
+
const activeItemId = useStructureStore((s) => s.activeItemId);
|
|
26
|
+
const activeItemType = useStructureStore((s) => s.activeItemType);
|
|
27
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
28
|
+
|
|
29
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
// ── Derived state ───────────────────────────────────────────────────────────
|
|
32
|
+
const { items, matchedIds, expandedBySearch, resultCount } = useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
buildVisibleItems(course, sessions, lessons, expandedIds, filterQuery),
|
|
35
|
+
[course, sessions, lessons, expandedIds, filterQuery]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const lessonCountMap = useMemo(() => buildLessonCountMap(lessons), [lessons]);
|
|
39
|
+
|
|
40
|
+
const estimateSize = useCallback(
|
|
41
|
+
(index: number) => ROW_HEIGHT[items[index]?.type ?? 'lesson'] ?? 32,
|
|
42
|
+
[items]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// ── Virtualizer ─────────────────────────────────────────────────────────────
|
|
46
|
+
// eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual returns non-memoizable functions; this is expected and safe here
|
|
47
|
+
const virtualizer = useVirtualizer({
|
|
48
|
+
count: items.length,
|
|
49
|
+
getScrollElement: () => scrollRef.current,
|
|
50
|
+
estimateSize,
|
|
51
|
+
overscan: 8,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const isSearchActive = filterQuery.trim().length > 0;
|
|
55
|
+
const isEmpty = items.length === 0;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex flex-col h-full min-h-0">
|
|
59
|
+
{/* Result count bar */}
|
|
60
|
+
{isSearchActive && !isEmpty && (
|
|
61
|
+
<div className="px-3 py-1 shrink-0 border-b">
|
|
62
|
+
<span className="text-[0.65rem] text-muted-foreground">
|
|
63
|
+
{resultCount === 1 ? '1 resultado' : `${resultCount} resultados`}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Empty state */}
|
|
69
|
+
{isEmpty && (
|
|
70
|
+
<div className="flex flex-col items-center justify-center gap-2 py-12 px-4 text-center">
|
|
71
|
+
<SearchX className="size-8 text-muted-foreground/40" />
|
|
72
|
+
<p className="text-sm text-muted-foreground">
|
|
73
|
+
{isSearchActive
|
|
74
|
+
? `Nenhum resultado para "${filterQuery.trim()}"`
|
|
75
|
+
: 'Nenhum item no curso.'}
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Virtualised tree */}
|
|
81
|
+
{!isEmpty && (
|
|
82
|
+
<div
|
|
83
|
+
ref={scrollRef}
|
|
84
|
+
className="overflow-y-auto flex-1 min-h-0"
|
|
85
|
+
style={{ contain: 'strict' }}
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
style={{
|
|
89
|
+
height: virtualizer.getTotalSize(),
|
|
90
|
+
width: '100%',
|
|
91
|
+
position: 'relative',
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{virtualizer.getVirtualItems().map((virtualRow) => {
|
|
95
|
+
const item = items[virtualRow.index];
|
|
96
|
+
if (!item) return null;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
key={virtualRow.key}
|
|
101
|
+
data-index={virtualRow.index}
|
|
102
|
+
style={{
|
|
103
|
+
position: 'absolute',
|
|
104
|
+
top: 0,
|
|
105
|
+
left: 0,
|
|
106
|
+
width: '100%',
|
|
107
|
+
height: `${virtualRow.size}px`,
|
|
108
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
109
|
+
padding: '1px 4px',
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<TreeRow
|
|
113
|
+
item={item}
|
|
114
|
+
isActive={
|
|
115
|
+
activeItemId === item.id && activeItemType === item.type
|
|
116
|
+
}
|
|
117
|
+
isSelected={selectedIds.has(`${item.type}:${item.id}`)}
|
|
118
|
+
query={filterQuery}
|
|
119
|
+
isMatched={matchedIds.has(item.id)}
|
|
120
|
+
isEffectivelyExpanded={
|
|
121
|
+
expandedIds.has(item.id) || expandedBySearch.has(item.id)
|
|
122
|
+
}
|
|
123
|
+
lessonCountMap={lessonCountMap}
|
|
124
|
+
visibleItems={items}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Separator } from '@/components/ui/separator';
|
|
5
|
+
import { BookOpen, Globe, Hash, Tag } from 'lucide-react';
|
|
6
|
+
import { useStructureStore } from './store';
|
|
7
|
+
|
|
8
|
+
export function DetailCourse() {
|
|
9
|
+
const course = useStructureStore((s) => s.course);
|
|
10
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
11
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
12
|
+
|
|
13
|
+
const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
|
|
14
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
15
|
+
const minutes = totalMinutes % 60;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-col overflow-y-auto h-full">
|
|
19
|
+
{/* Header */}
|
|
20
|
+
<div className="flex items-center gap-3 px-4 py-4 border-b bg-muted/30 shrink-0">
|
|
21
|
+
<div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
|
22
|
+
<BookOpen className="size-5 text-primary" />
|
|
23
|
+
</div>
|
|
24
|
+
<div className="min-w-0 flex-1">
|
|
25
|
+
<h2 className="text-base font-semibold truncate">{course.title}</h2>
|
|
26
|
+
<p className="text-xs text-muted-foreground">{course.slug}</p>
|
|
27
|
+
</div>
|
|
28
|
+
<Badge
|
|
29
|
+
variant={course.published ? 'default' : 'secondary'}
|
|
30
|
+
className="shrink-0"
|
|
31
|
+
>
|
|
32
|
+
{course.published ? 'Publicado' : 'Rascunho'}
|
|
33
|
+
</Badge>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="flex flex-col gap-5 p-4">
|
|
37
|
+
{/* Stats */}
|
|
38
|
+
<div className="grid grid-cols-3 gap-3">
|
|
39
|
+
<StatCard label="Sessões" value={sessions.length} />
|
|
40
|
+
<StatCard label="Aulas" value={lessons.length} />
|
|
41
|
+
<StatCard
|
|
42
|
+
label="Duração"
|
|
43
|
+
value={hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<Separator />
|
|
48
|
+
|
|
49
|
+
{/* Info */}
|
|
50
|
+
<div className="flex flex-col gap-2.5">
|
|
51
|
+
<InfoRow
|
|
52
|
+
icon={<Hash className="size-3.5" />}
|
|
53
|
+
label="Slug"
|
|
54
|
+
value={course.slug}
|
|
55
|
+
/>
|
|
56
|
+
<InfoRow
|
|
57
|
+
icon={<Tag className="size-3.5" />}
|
|
58
|
+
label="Slug"
|
|
59
|
+
value={course.slug}
|
|
60
|
+
/>
|
|
61
|
+
<InfoRow
|
|
62
|
+
icon={<Globe className="size-3.5" />}
|
|
63
|
+
label="Status"
|
|
64
|
+
value={course.published ? 'Publicado' : 'Rascunho'}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{course.description && (
|
|
69
|
+
<>
|
|
70
|
+
<Separator />
|
|
71
|
+
<div>
|
|
72
|
+
<p className="text-xs font-medium text-muted-foreground mb-1.5">
|
|
73
|
+
Descrição
|
|
74
|
+
</p>
|
|
75
|
+
<p className="text-sm leading-relaxed text-foreground/90">
|
|
76
|
+
{course.description}
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function StatCard({ label, value }: { label: string; value: string | number }) {
|
|
87
|
+
return (
|
|
88
|
+
<div className="flex flex-col items-center rounded-lg border bg-muted/30 py-3 gap-0.5">
|
|
89
|
+
<span className="text-lg font-bold tabular-nums">{value}</span>
|
|
90
|
+
<span className="text-[0.65rem] text-muted-foreground">{label}</span>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function InfoRow({
|
|
96
|
+
icon,
|
|
97
|
+
label,
|
|
98
|
+
value,
|
|
99
|
+
}: {
|
|
100
|
+
icon: React.ReactNode;
|
|
101
|
+
label: string;
|
|
102
|
+
value: string;
|
|
103
|
+
}) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex items-center gap-2">
|
|
106
|
+
<span className="text-muted-foreground shrink-0">{icon}</span>
|
|
107
|
+
<span className="text-xs text-muted-foreground w-14 shrink-0">
|
|
108
|
+
{label}
|
|
109
|
+
</span>
|
|
110
|
+
<span className="text-sm truncate">{value}</span>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Separator } from '@/components/ui/separator';
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
import {
|
|
8
|
+
Clock,
|
|
9
|
+
ExternalLink,
|
|
10
|
+
FileText,
|
|
11
|
+
HelpCircle,
|
|
12
|
+
Layers,
|
|
13
|
+
Paperclip,
|
|
14
|
+
Video,
|
|
15
|
+
type LucideIcon,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
import { useStructureStore } from './store';
|
|
18
|
+
import type { LessonType } from './types';
|
|
19
|
+
|
|
20
|
+
const LESSON_TYPE_CONFIG: Record<
|
|
21
|
+
LessonType,
|
|
22
|
+
{ icon: LucideIcon; color: string; bg: string; label: string }
|
|
23
|
+
> = {
|
|
24
|
+
video: {
|
|
25
|
+
icon: Video,
|
|
26
|
+
color: 'text-blue-500',
|
|
27
|
+
bg: 'bg-blue-500/10',
|
|
28
|
+
label: 'Vídeo',
|
|
29
|
+
},
|
|
30
|
+
post: {
|
|
31
|
+
icon: FileText,
|
|
32
|
+
color: 'text-emerald-500',
|
|
33
|
+
bg: 'bg-emerald-500/10',
|
|
34
|
+
label: 'Post',
|
|
35
|
+
},
|
|
36
|
+
questao: {
|
|
37
|
+
icon: HelpCircle,
|
|
38
|
+
color: 'text-amber-500',
|
|
39
|
+
bg: 'bg-amber-500/10',
|
|
40
|
+
label: 'Quiz',
|
|
41
|
+
},
|
|
42
|
+
exercicio: {
|
|
43
|
+
icon: Layers,
|
|
44
|
+
color: 'text-rose-500',
|
|
45
|
+
bg: 'bg-rose-500/10',
|
|
46
|
+
label: 'Exercício',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const VIDEO_PROVIDER_LABELS: Record<string, string> = {
|
|
51
|
+
youtube: 'YouTube',
|
|
52
|
+
vimeo: 'Vimeo',
|
|
53
|
+
panda: 'Panda Video',
|
|
54
|
+
bunny: 'Bunny.net',
|
|
55
|
+
outros: 'Outros',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
interface DetailLessonProps {
|
|
59
|
+
lessonId: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
63
|
+
const lesson = useStructureStore((s) =>
|
|
64
|
+
s.lessons.find((l) => l.id === lessonId)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (!lesson) return null;
|
|
68
|
+
|
|
69
|
+
const cfg = LESSON_TYPE_CONFIG[lesson.type];
|
|
70
|
+
const Icon = cfg.icon;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex flex-col h-full overflow-hidden">
|
|
74
|
+
{/* Header */}
|
|
75
|
+
<div className="flex items-center gap-3 px-4 py-4 border-b bg-muted/30 shrink-0">
|
|
76
|
+
<div
|
|
77
|
+
className={cn(
|
|
78
|
+
'flex size-10 items-center justify-center rounded-lg shrink-0',
|
|
79
|
+
cfg.bg
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
<Icon className={cn('size-5', cfg.color)} />
|
|
83
|
+
</div>
|
|
84
|
+
<div className="min-w-0 flex-1">
|
|
85
|
+
<h2 className="text-base font-semibold truncate">{lesson.title}</h2>
|
|
86
|
+
<p className="text-xs text-muted-foreground">{lesson.code}</p>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
89
|
+
<Badge variant="outline" className={cn('text-xs', cfg.color)}>
|
|
90
|
+
{cfg.label}
|
|
91
|
+
</Badge>
|
|
92
|
+
<Badge variant="secondary" className="gap-1 text-xs">
|
|
93
|
+
<Clock className="size-3" />
|
|
94
|
+
{lesson.duration}min
|
|
95
|
+
</Badge>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Tabs */}
|
|
100
|
+
<Tabs defaultValue="dados" className="flex flex-col flex-1 min-h-0">
|
|
101
|
+
<TabsList className="mx-4 mt-3 w-auto justify-start shrink-0">
|
|
102
|
+
<TabsTrigger value="dados">Dados</TabsTrigger>
|
|
103
|
+
<TabsTrigger value="transcription">Transcrição</TabsTrigger>
|
|
104
|
+
<TabsTrigger value="resources">Recursos</TabsTrigger>
|
|
105
|
+
</TabsList>
|
|
106
|
+
|
|
107
|
+
{/* ── Dados ─────────────────────────────────────────────────────────── */}
|
|
108
|
+
<TabsContent
|
|
109
|
+
value="dados"
|
|
110
|
+
className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
|
|
111
|
+
>
|
|
112
|
+
<div className="flex flex-col gap-4">
|
|
113
|
+
{lesson.publicDescription && (
|
|
114
|
+
<Section title="Descrição pública">
|
|
115
|
+
<p className="text-sm leading-relaxed">
|
|
116
|
+
{lesson.publicDescription}
|
|
117
|
+
</p>
|
|
118
|
+
</Section>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{lesson.privateDescription && (
|
|
122
|
+
<>
|
|
123
|
+
<Separator />
|
|
124
|
+
<Section title="Descrição interna">
|
|
125
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
126
|
+
{lesson.privateDescription}
|
|
127
|
+
</p>
|
|
128
|
+
</Section>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Video */}
|
|
133
|
+
{lesson.type === 'video' && (
|
|
134
|
+
<>
|
|
135
|
+
<Separator />
|
|
136
|
+
<Section title="Configurações de vídeo">
|
|
137
|
+
<div className="flex flex-col gap-2">
|
|
138
|
+
{lesson.videoProvider && (
|
|
139
|
+
<InfoRow
|
|
140
|
+
label="Plataforma"
|
|
141
|
+
value={
|
|
142
|
+
VIDEO_PROVIDER_LABELS[lesson.videoProvider] ??
|
|
143
|
+
lesson.videoProvider
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
{lesson.videoUrl && (
|
|
148
|
+
<div className="flex items-start gap-2">
|
|
149
|
+
<span className="text-xs text-muted-foreground w-20 shrink-0 pt-0.5">
|
|
150
|
+
URL
|
|
151
|
+
</span>
|
|
152
|
+
<a
|
|
153
|
+
href={lesson.videoUrl}
|
|
154
|
+
target="_blank"
|
|
155
|
+
rel="noopener noreferrer"
|
|
156
|
+
className="text-sm text-primary hover:underline flex items-center gap-1 truncate"
|
|
157
|
+
>
|
|
158
|
+
{lesson.videoUrl}
|
|
159
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
160
|
+
</a>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
{lesson.autoDuration !== undefined && (
|
|
164
|
+
<InfoRow
|
|
165
|
+
label="Duração auto"
|
|
166
|
+
value={lesson.autoDuration ? 'Sim' : 'Não'}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</Section>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Post */}
|
|
175
|
+
{lesson.type === 'post' && lesson.postContent && (
|
|
176
|
+
<>
|
|
177
|
+
<Separator />
|
|
178
|
+
<Section title="Conteúdo do post">
|
|
179
|
+
<p className="text-sm leading-relaxed whitespace-pre-wrap">
|
|
180
|
+
{lesson.postContent}
|
|
181
|
+
</p>
|
|
182
|
+
</Section>
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{/* Questão / exame vinculado */}
|
|
187
|
+
{lesson.type === 'questao' && lesson.linkedExam && (
|
|
188
|
+
<>
|
|
189
|
+
<Separator />
|
|
190
|
+
<Section title="Exame vinculado">
|
|
191
|
+
<div className="flex items-center gap-2 text-sm">
|
|
192
|
+
<HelpCircle className="size-3.5 text-amber-500 shrink-0" />
|
|
193
|
+
<span>{lesson.linkedExam}</span>
|
|
194
|
+
</div>
|
|
195
|
+
</Section>
|
|
196
|
+
</>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{!lesson.publicDescription &&
|
|
200
|
+
!lesson.privateDescription &&
|
|
201
|
+
lesson.type === 'exercicio' && (
|
|
202
|
+
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
203
|
+
<Layers className="size-8 text-muted-foreground/40" />
|
|
204
|
+
<p className="text-sm text-muted-foreground">
|
|
205
|
+
Sem informações adicionais nesta aula.
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</TabsContent>
|
|
211
|
+
|
|
212
|
+
{/* ── Transcrição ───────────────────────────────────────────────────── */}
|
|
213
|
+
<TabsContent
|
|
214
|
+
value="transcription"
|
|
215
|
+
className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
|
|
216
|
+
>
|
|
217
|
+
{lesson.transcription ? (
|
|
218
|
+
<div className="rounded-md border bg-muted/20 p-3">
|
|
219
|
+
<p className="text-sm leading-relaxed whitespace-pre-wrap font-mono">
|
|
220
|
+
{lesson.transcription}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
) : (
|
|
224
|
+
<EmptyState
|
|
225
|
+
icon={<FileText className="size-8 text-muted-foreground/40" />}
|
|
226
|
+
>
|
|
227
|
+
Transcrição não disponível para esta aula.
|
|
228
|
+
</EmptyState>
|
|
229
|
+
)}
|
|
230
|
+
</TabsContent>
|
|
231
|
+
|
|
232
|
+
{/* ── Recursos ──────────────────────────────────────────────────────── */}
|
|
233
|
+
<TabsContent
|
|
234
|
+
value="resources"
|
|
235
|
+
className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
|
|
236
|
+
>
|
|
237
|
+
{lesson.resources && lesson.resources.length > 0 ? (
|
|
238
|
+
<div className="flex flex-col gap-2">
|
|
239
|
+
{lesson.resources.map((res) => (
|
|
240
|
+
<div
|
|
241
|
+
key={res.id}
|
|
242
|
+
className="flex items-center gap-2 rounded-md border px-3 py-2"
|
|
243
|
+
>
|
|
244
|
+
<Paperclip className="size-3.5 text-muted-foreground shrink-0" />
|
|
245
|
+
<span className="flex-1 text-sm truncate">{res.name}</span>
|
|
246
|
+
{res.size && (
|
|
247
|
+
<span className="text-[0.6rem] text-muted-foreground shrink-0">
|
|
248
|
+
{res.size}
|
|
249
|
+
</span>
|
|
250
|
+
)}
|
|
251
|
+
<Badge
|
|
252
|
+
variant={res.public ? 'default' : 'secondary'}
|
|
253
|
+
className="text-[0.6rem] px-1.5 py-0 h-4 shrink-0"
|
|
254
|
+
>
|
|
255
|
+
{res.public ? 'Público' : 'Privado'}
|
|
256
|
+
</Badge>
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
) : (
|
|
261
|
+
<EmptyState
|
|
262
|
+
icon={<Paperclip className="size-8 text-muted-foreground/40" />}
|
|
263
|
+
>
|
|
264
|
+
Nenhum recurso anexado a esta aula.
|
|
265
|
+
</EmptyState>
|
|
266
|
+
)}
|
|
267
|
+
</TabsContent>
|
|
268
|
+
</Tabs>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Local helpers ─────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
function Section({
|
|
276
|
+
title,
|
|
277
|
+
children,
|
|
278
|
+
}: {
|
|
279
|
+
title: string;
|
|
280
|
+
children: React.ReactNode;
|
|
281
|
+
}) {
|
|
282
|
+
return (
|
|
283
|
+
<div className="flex flex-col gap-1.5">
|
|
284
|
+
<p className="text-xs font-medium text-muted-foreground">{title}</p>
|
|
285
|
+
{children}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function InfoRow({ label, value }: { label: string; value: string }) {
|
|
291
|
+
return (
|
|
292
|
+
<div className="flex items-center gap-2">
|
|
293
|
+
<span className="text-xs text-muted-foreground w-20 shrink-0">
|
|
294
|
+
{label}
|
|
295
|
+
</span>
|
|
296
|
+
<span className="text-sm">{value}</span>
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function EmptyState({
|
|
302
|
+
icon,
|
|
303
|
+
children,
|
|
304
|
+
}: {
|
|
305
|
+
icon: React.ReactNode;
|
|
306
|
+
children: React.ReactNode;
|
|
307
|
+
}) {
|
|
308
|
+
return (
|
|
309
|
+
<div className="flex flex-col items-center gap-2 py-12 text-center">
|
|
310
|
+
{icon}
|
|
311
|
+
<p className="text-sm text-muted-foreground">{children}</p>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { BookOpen, Keyboard, MousePointerClick } from 'lucide-react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
5
|
+
import { memo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { EditorBulk } from './editor-bulk';
|
|
8
|
+
import { EditorCourse } from './editor-course';
|
|
9
|
+
import { EditorLesson } from './editor-lesson';
|
|
10
|
+
import { EditorSession } from './editor-session';
|
|
11
|
+
import { useStructureStore } from './store';
|
|
12
|
+
|
|
13
|
+
export const DetailPanel = memo(function DetailPanel() {
|
|
14
|
+
const t = useTranslations('lms.CoursesPage.StructurePage');
|
|
15
|
+
const { activeItemId, activeItemType, selectedIds } = useStructureStore();
|
|
16
|
+
|
|
17
|
+
// Multi-select: show bulk editor when more than one item is selected
|
|
18
|
+
if (selectedIds.size > 1) {
|
|
19
|
+
return <EditorBulk />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!activeItemId || !activeItemType) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground p-8">
|
|
25
|
+
<div className="flex size-16 items-center justify-center rounded-2xl bg-muted/60">
|
|
26
|
+
<BookOpen className="size-7 opacity-40" />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="text-center space-y-1 max-w-[220px]">
|
|
29
|
+
<p className="text-sm font-medium text-foreground/70">
|
|
30
|
+
Nenhum item selecionado
|
|
31
|
+
</p>
|
|
32
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
33
|
+
{t('detailPanel.emptyHint')}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="flex flex-col items-center gap-2 mt-2">
|
|
37
|
+
<div className="flex items-center gap-1.5 text-[0.65rem] text-muted-foreground">
|
|
38
|
+
<MousePointerClick className="size-3" />
|
|
39
|
+
Clique para selecionar
|
|
40
|
+
</div>
|
|
41
|
+
<div className="flex items-center gap-1.5 text-[0.65rem] text-muted-foreground">
|
|
42
|
+
<Keyboard className="size-3" />↑ ↓ para navegar
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (activeItemType === 'course') {
|
|
50
|
+
return <EditorCourse />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (activeItemType === 'session') {
|
|
54
|
+
return <EditorSession sessionId={activeItemId} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (activeItemType === 'lesson') {
|
|
58
|
+
return <EditorLesson lessonId={activeItemId} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
});
|