@hed-hog/lms 0.0.314 → 0.0.316
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/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 +17 -1
- package/hedhog/data/route.yaml +48 -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/query/add_route_role.sql +15 -0
- package/hedhog/table/enterprise_user.yaml +1 -1
- package/package.json +6 -6
- 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,62 +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
|
-
});
|
|
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
|
+
});
|
|
@@ -1,174 +1,174 @@
|
|
|
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
|
-
);
|
|
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
174
|
}
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { cn } from '@/lib/utils';
|
|
4
|
-
import type { DraggableAttributes } from '@dnd-kit/core';
|
|
5
|
-
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
|
6
|
-
import { GripVertical } from 'lucide-react';
|
|
7
|
-
|
|
8
|
-
interface DragHandleProps {
|
|
9
|
-
listeners?: SyntheticListenerMap;
|
|
10
|
-
attributes?: DraggableAttributes;
|
|
11
|
-
disabled?: boolean;
|
|
12
|
-
className?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Grip handle for drag-and-drop.
|
|
17
|
-
* Attach the dnd-kit `listeners` and `attributes` to this element so that
|
|
18
|
-
* clicking elsewhere (expand button, context menu, etc.) does NOT start a drag.
|
|
19
|
-
*/
|
|
20
|
-
export function DragHandle({
|
|
21
|
-
listeners,
|
|
22
|
-
attributes,
|
|
23
|
-
disabled,
|
|
24
|
-
className,
|
|
25
|
-
}: DragHandleProps) {
|
|
26
|
-
if (disabled) {
|
|
27
|
-
return (
|
|
28
|
-
<span
|
|
29
|
-
className={cn(
|
|
30
|
-
'shrink-0 size-5 flex items-center justify-center opacity-20 cursor-not-allowed',
|
|
31
|
-
className
|
|
32
|
-
)}
|
|
33
|
-
title="Limpe a busca para reordenar"
|
|
34
|
-
>
|
|
35
|
-
<GripVertical className="size-3.5" />
|
|
36
|
-
</span>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<span
|
|
42
|
-
{...listeners}
|
|
43
|
-
{...attributes}
|
|
44
|
-
className={cn(
|
|
45
|
-
'shrink-0 size-5 flex items-center justify-center rounded',
|
|
46
|
-
'text-muted-foreground/40 hover:text-muted-foreground',
|
|
47
|
-
'cursor-grab active:cursor-grabbing',
|
|
48
|
-
'transition-colors touch-none',
|
|
49
|
-
className
|
|
50
|
-
)}
|
|
51
|
-
title="Arrastar para reordenar"
|
|
52
|
-
// Prevent click events from bubbling to the row selection handler
|
|
53
|
-
onClick={(e) => e.stopPropagation()}
|
|
54
|
-
>
|
|
55
|
-
<GripVertical className="size-3.5" />
|
|
56
|
-
</span>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import type { DraggableAttributes } from '@dnd-kit/core';
|
|
5
|
+
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
|
6
|
+
import { GripVertical } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
interface DragHandleProps {
|
|
9
|
+
listeners?: SyntheticListenerMap;
|
|
10
|
+
attributes?: DraggableAttributes;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Grip handle for drag-and-drop.
|
|
17
|
+
* Attach the dnd-kit `listeners` and `attributes` to this element so that
|
|
18
|
+
* clicking elsewhere (expand button, context menu, etc.) does NOT start a drag.
|
|
19
|
+
*/
|
|
20
|
+
export function DragHandle({
|
|
21
|
+
listeners,
|
|
22
|
+
attributes,
|
|
23
|
+
disabled,
|
|
24
|
+
className,
|
|
25
|
+
}: DragHandleProps) {
|
|
26
|
+
if (disabled) {
|
|
27
|
+
return (
|
|
28
|
+
<span
|
|
29
|
+
className={cn(
|
|
30
|
+
'shrink-0 size-5 flex items-center justify-center opacity-20 cursor-not-allowed',
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
title="Limpe a busca para reordenar"
|
|
34
|
+
>
|
|
35
|
+
<GripVertical className="size-3.5" />
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
{...listeners}
|
|
43
|
+
{...attributes}
|
|
44
|
+
className={cn(
|
|
45
|
+
'shrink-0 size-5 flex items-center justify-center rounded',
|
|
46
|
+
'text-muted-foreground/40 hover:text-muted-foreground',
|
|
47
|
+
'cursor-grab active:cursor-grabbing',
|
|
48
|
+
'transition-colors touch-none',
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
title="Arrastar para reordenar"
|
|
52
|
+
// Prevent click events from bubbling to the row selection handler
|
|
53
|
+
onClick={(e) => e.stopPropagation()}
|
|
54
|
+
>
|
|
55
|
+
<GripVertical className="size-3.5" />
|
|
56
|
+
</span>
|
|
57
|
+
);
|
|
58
|
+
}
|