@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,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
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { DragOverlay as DndDragOverlay } from '@dnd-kit/core';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { GripVertical, Layers, Video, FileText, HelpCircle } from 'lucide-react';
|
|
6
|
+
import type { FlatItem } from './types';
|
|
7
|
+
|
|
8
|
+
interface DragOverlayContentProps {
|
|
9
|
+
item: FlatItem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function DragOverlayContent({ item }: DragOverlayContentProps) {
|
|
13
|
+
const isSession = item.type === 'session';
|
|
14
|
+
const isLesson = item.type === 'lesson';
|
|
15
|
+
|
|
16
|
+
const lessonIconMap = {
|
|
17
|
+
video: <Video className="size-3.5 text-blue-500 shrink-0" />,
|
|
18
|
+
post: <FileText className="size-3.5 text-emerald-500 shrink-0" />,
|
|
19
|
+
questao: <HelpCircle className="size-3.5 text-amber-500 shrink-0" />,
|
|
20
|
+
exercicio: <Layers className="size-3.5 text-rose-500 shrink-0" />,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
'flex items-center gap-1.5 px-2 py-1.5 rounded-md text-sm',
|
|
27
|
+
'bg-card border shadow-lg ring-1 ring-primary/20',
|
|
28
|
+
'opacity-95 max-w-[260px]',
|
|
29
|
+
isLesson && 'pl-4'
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<GripVertical className="size-3.5 text-muted-foreground/60 shrink-0" />
|
|
33
|
+
|
|
34
|
+
{isSession && <Layers className="size-3.5 text-muted-foreground shrink-0" />}
|
|
35
|
+
{isLesson && item.type === 'lesson' && lessonIconMap[item.data.type]}
|
|
36
|
+
|
|
37
|
+
<span className="truncate font-medium text-xs">{item.data.title}</span>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TreeDragOverlayProps {
|
|
43
|
+
activeItem: FlatItem | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function TreeDragOverlay({ activeItem }: TreeDragOverlayProps) {
|
|
47
|
+
return (
|
|
48
|
+
<DndDragOverlay dropAnimation={{ duration: 150, easing: 'ease' }}>
|
|
49
|
+
{activeItem ? <DragOverlayContent item={activeItem} /> : null}
|
|
50
|
+
</DndDragOverlay>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Eye,
|
|
5
|
+
EyeOff,
|
|
6
|
+
FolderInput,
|
|
7
|
+
Layers,
|
|
8
|
+
Lock,
|
|
9
|
+
Save,
|
|
10
|
+
Video,
|
|
11
|
+
X,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { toast } from 'sonner';
|
|
15
|
+
|
|
16
|
+
import { Badge } from '@/components/ui/badge';
|
|
17
|
+
import { Button } from '@/components/ui/button';
|
|
18
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
19
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
20
|
+
import {
|
|
21
|
+
Select,
|
|
22
|
+
SelectContent,
|
|
23
|
+
SelectItem,
|
|
24
|
+
SelectTrigger,
|
|
25
|
+
SelectValue,
|
|
26
|
+
} from '@/components/ui/select';
|
|
27
|
+
import { Separator } from '@/components/ui/separator';
|
|
28
|
+
import { cn } from '@/lib/utils';
|
|
29
|
+
|
|
30
|
+
import { useStructureStore } from './store';
|
|
31
|
+
import type { LessonStatus, Visibility } from './types';
|
|
32
|
+
|
|
33
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const STATUS_LABELS: Record<LessonStatus, string> = {
|
|
36
|
+
preparada: 'Preparada',
|
|
37
|
+
gravada: 'Gravada',
|
|
38
|
+
editada: 'Editada',
|
|
39
|
+
finalizada: 'Finalizada',
|
|
40
|
+
publicada: 'Publicada',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function EditorBulk() {
|
|
46
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
47
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
48
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
49
|
+
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
50
|
+
|
|
51
|
+
const selectedArray = [...selectedIds];
|
|
52
|
+
const lessonIds = selectedArray.filter((id) =>
|
|
53
|
+
lessons.some((l) => l.id === id)
|
|
54
|
+
);
|
|
55
|
+
const sessionIds = selectedArray.filter((id) =>
|
|
56
|
+
sessions.some((s) => s.id === id)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const allLessons = lessonIds.length === selectedArray.length;
|
|
60
|
+
const allSessions = sessionIds.length === selectedArray.length;
|
|
61
|
+
|
|
62
|
+
// Local form state (mock — not wired to store actions yet)
|
|
63
|
+
const [status, setStatus] = useState<LessonStatus | ''>('');
|
|
64
|
+
const [visibility, setVisibility] = useState<Visibility | ''>('');
|
|
65
|
+
const [targetSession, setTargetSession] = useState<string>('');
|
|
66
|
+
|
|
67
|
+
function handleSave() {
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
if (status) lines.push(`status → ${STATUS_LABELS[status as LessonStatus]}`);
|
|
70
|
+
if (visibility) lines.push(`visibilidade → ${visibility}`);
|
|
71
|
+
if (targetSession)
|
|
72
|
+
lines.push(
|
|
73
|
+
`mover para sessão ${sessions.find((s) => s.id === targetSession)?.title ?? targetSession}`
|
|
74
|
+
);
|
|
75
|
+
toast.success(
|
|
76
|
+
lines.length
|
|
77
|
+
? `Edição em massa: ${lines.join(', ')} (mock)`
|
|
78
|
+
: 'Nenhuma alteração selecionada'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex flex-col h-full min-h-0">
|
|
84
|
+
{/* ── Header ───────────────────────────────────────────────────────── */}
|
|
85
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
|
|
86
|
+
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
|
87
|
+
{allLessons ? (
|
|
88
|
+
<Video className="size-4 text-primary" />
|
|
89
|
+
) : allSessions ? (
|
|
90
|
+
<Layers className="size-4 text-primary" />
|
|
91
|
+
) : (
|
|
92
|
+
<Layers className="size-4 text-primary" />
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex-1 min-w-0">
|
|
96
|
+
<p className="text-sm font-semibold">Edição em massa</p>
|
|
97
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
98
|
+
{selectedIds.size}{' '}
|
|
99
|
+
{allLessons ? 'aulas' : allSessions ? 'sessões' : 'itens'}{' '}
|
|
100
|
+
selecionados
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
<Button
|
|
104
|
+
variant="ghost"
|
|
105
|
+
size="icon"
|
|
106
|
+
className="size-7 shrink-0"
|
|
107
|
+
onClick={clearSelection}
|
|
108
|
+
title="Limpar seleção"
|
|
109
|
+
>
|
|
110
|
+
<X className="size-3.5" />
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* ── Body ─────────────────────────────────────────────────────────── */}
|
|
115
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
116
|
+
<div className="flex flex-col gap-3 p-3">
|
|
117
|
+
{/* Badges dos itens selecionados */}
|
|
118
|
+
<div className="flex flex-wrap gap-1">
|
|
119
|
+
{selectedArray.slice(0, 12).map((id) => {
|
|
120
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
121
|
+
const session = sessions.find((s) => s.id === id);
|
|
122
|
+
const label = lesson?.code ?? session?.code ?? id;
|
|
123
|
+
const isLesson = !!lesson;
|
|
124
|
+
return (
|
|
125
|
+
<Badge
|
|
126
|
+
key={id}
|
|
127
|
+
variant="secondary"
|
|
128
|
+
className={cn(
|
|
129
|
+
'text-[0.65rem] h-5',
|
|
130
|
+
isLesson
|
|
131
|
+
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
|
132
|
+
: 'bg-muted'
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{label}
|
|
136
|
+
</Badge>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
{selectedArray.length > 12 && (
|
|
140
|
+
<Badge variant="outline" className="text-[0.65rem] h-5">
|
|
141
|
+
+{selectedArray.length - 12} mais
|
|
142
|
+
</Badge>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Status — só mostra para aulas */}
|
|
147
|
+
{(allLessons || (!allSessions && lessonIds.length > 0)) && (
|
|
148
|
+
<Card>
|
|
149
|
+
<CardHeader className="px-3 pt-3 pb-2">
|
|
150
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
151
|
+
Status de produção
|
|
152
|
+
</CardTitle>
|
|
153
|
+
</CardHeader>
|
|
154
|
+
<CardContent className="px-3 pb-3">
|
|
155
|
+
<Select
|
|
156
|
+
value={status}
|
|
157
|
+
onValueChange={(v) => setStatus(v as LessonStatus)}
|
|
158
|
+
>
|
|
159
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
160
|
+
<SelectValue placeholder="Manter atual" />
|
|
161
|
+
</SelectTrigger>
|
|
162
|
+
<SelectContent>
|
|
163
|
+
<SelectItem value="preparada">Preparada</SelectItem>
|
|
164
|
+
<SelectItem value="gravada">Gravada</SelectItem>
|
|
165
|
+
<SelectItem value="editada">Editada</SelectItem>
|
|
166
|
+
<SelectItem value="finalizada">Finalizada</SelectItem>
|
|
167
|
+
<SelectItem value="publicada">Publicada</SelectItem>
|
|
168
|
+
</SelectContent>
|
|
169
|
+
</Select>
|
|
170
|
+
</CardContent>
|
|
171
|
+
</Card>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Visibilidade */}
|
|
175
|
+
<Card>
|
|
176
|
+
<CardHeader className="px-3 pt-3 pb-2">
|
|
177
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
178
|
+
Visibilidade
|
|
179
|
+
</CardTitle>
|
|
180
|
+
</CardHeader>
|
|
181
|
+
<CardContent className="px-3 pb-3">
|
|
182
|
+
<Select
|
|
183
|
+
value={visibility}
|
|
184
|
+
onValueChange={(v) => setVisibility(v as Visibility)}
|
|
185
|
+
>
|
|
186
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
187
|
+
<SelectValue placeholder="Manter atual" />
|
|
188
|
+
</SelectTrigger>
|
|
189
|
+
<SelectContent>
|
|
190
|
+
<SelectItem value="publico">
|
|
191
|
+
<span className="flex items-center gap-1.5">
|
|
192
|
+
<Eye className="size-3" /> Público
|
|
193
|
+
</span>
|
|
194
|
+
</SelectItem>
|
|
195
|
+
<SelectItem value="privado">
|
|
196
|
+
<span className="flex items-center gap-1.5">
|
|
197
|
+
<EyeOff className="size-3" /> Privado
|
|
198
|
+
</span>
|
|
199
|
+
</SelectItem>
|
|
200
|
+
<SelectItem value="restrito">
|
|
201
|
+
<span className="flex items-center gap-1.5">
|
|
202
|
+
<Lock className="size-3" /> Restrito
|
|
203
|
+
</span>
|
|
204
|
+
</SelectItem>
|
|
205
|
+
</SelectContent>
|
|
206
|
+
</Select>
|
|
207
|
+
</CardContent>
|
|
208
|
+
</Card>
|
|
209
|
+
|
|
210
|
+
{/* Mover para sessão — só para aulas */}
|
|
211
|
+
{(allLessons || lessonIds.length > 0) && (
|
|
212
|
+
<Card>
|
|
213
|
+
<CardHeader className="px-3 pt-3 pb-2">
|
|
214
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
215
|
+
<FolderInput className="size-3" />
|
|
216
|
+
Mover para sessão
|
|
217
|
+
</CardTitle>
|
|
218
|
+
</CardHeader>
|
|
219
|
+
<CardContent className="px-3 pb-3">
|
|
220
|
+
<Select value={targetSession} onValueChange={setTargetSession}>
|
|
221
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
222
|
+
<SelectValue placeholder="Selecionar sessão…" />
|
|
223
|
+
</SelectTrigger>
|
|
224
|
+
<SelectContent>
|
|
225
|
+
{sessions.map((s) => (
|
|
226
|
+
<SelectItem key={s.id} value={s.id}>
|
|
227
|
+
<span className="font-mono mr-1 text-muted-foreground">
|
|
228
|
+
{s.code}
|
|
229
|
+
</span>
|
|
230
|
+
{s.title}
|
|
231
|
+
</SelectItem>
|
|
232
|
+
))}
|
|
233
|
+
</SelectContent>
|
|
234
|
+
</Select>
|
|
235
|
+
</CardContent>
|
|
236
|
+
</Card>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<div className="rounded-md border border-amber-200 bg-amber-50/60 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2">
|
|
240
|
+
<p className="text-[0.65rem] text-amber-700 dark:text-amber-400">
|
|
241
|
+
As alterações em massa ainda não estão integradas com a API.
|
|
242
|
+
Clique em salvar para ver a pré-visualização.
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</ScrollArea>
|
|
247
|
+
|
|
248
|
+
{/* ── Footer ───────────────────────────────────────────────────────── */}
|
|
249
|
+
<div className="shrink-0 border-t bg-background">
|
|
250
|
+
<Separator />
|
|
251
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
252
|
+
<Button
|
|
253
|
+
type="button"
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="sm"
|
|
256
|
+
className="h-7 text-xs"
|
|
257
|
+
onClick={clearSelection}
|
|
258
|
+
>
|
|
259
|
+
<X className="size-3 mr-1" />
|
|
260
|
+
Limpar seleção
|
|
261
|
+
</Button>
|
|
262
|
+
<div className="flex-1" />
|
|
263
|
+
<Button
|
|
264
|
+
type="button"
|
|
265
|
+
size="sm"
|
|
266
|
+
className="h-7 text-xs"
|
|
267
|
+
onClick={handleSave}
|
|
268
|
+
>
|
|
269
|
+
<Save className="size-3 mr-1" />
|
|
270
|
+
Aplicar (mock)
|
|
271
|
+
</Button>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|