@hed-hog/lms 0.0.350 → 0.0.351
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/certificate/certificate.controller.d.ts +2 -2
- package/dist/certificate/certificate.controller.d.ts.map +1 -1
- package/dist/certificate/certificate.controller.js +8 -6
- package/dist/certificate/certificate.controller.js.map +1 -1
- package/dist/certificate/certificate.service.d.ts +5 -2
- package/dist/certificate/certificate.service.d.ts.map +1 -1
- package/dist/certificate/certificate.service.js +70 -6
- package/dist/certificate/certificate.service.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +24 -10
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +23 -2
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +16 -8
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +61 -30
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +37 -0
- package/dist/course/course-video-conversion.service.d.ts.map +1 -0
- package/dist/course/course-video-conversion.service.js +308 -0
- package/dist/course/course-video-conversion.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +17 -0
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +23 -0
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +15 -2
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +15 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +103 -49
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +5 -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 +16 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -0
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +9 -0
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +3 -3
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +0 -1
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -3
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +9 -2
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +3 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts +6 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts.map +1 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js +33 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js.map +1 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts +6 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts.map +1 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js +33 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts +38 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.js +89 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts +26 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js +160 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.d.ts +3 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.js +26 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.d.ts +45 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.js +117 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.js.map +1 -0
- package/hedhog/data/menu.yaml +17 -0
- package/hedhog/data/route.yaml +133 -0
- package/hedhog/data/video_resolution_profile.yaml +7 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +269 -324
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +124 -70
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +7 -4
- package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +34 -4
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +28 -3
- package/hedhog/frontend/app/achievements/page.tsx.ejs +9 -3
- package/hedhog/frontend/app/bitcodes/page.tsx.ejs +9 -3
- package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +7 -3
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +29 -8
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +14 -0
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +194 -9
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +15 -5
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +9 -5
- package/hedhog/frontend/app/classes/page.tsx.ejs +73 -47
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +19 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +24 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +28 -16
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +11 -6
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +7 -4
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +24 -87
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +892 -411
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1004 -293
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +11 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +62 -52
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +86 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +112 -89
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +8 -4
- package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +10 -4
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +23 -9
- package/hedhog/frontend/app/exams/page.tsx.ejs +14 -6
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +9 -3
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +190 -17
- package/hedhog/frontend/app/layout.tsx.ejs +5 -1
- package/hedhog/frontend/app/paths/page.tsx.ejs +13 -5
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/training/page.tsx.ejs +13 -5
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +607 -0
- package/hedhog/frontend/messages/en.json +250 -9
- package/hedhog/frontend/messages/pt.json +250 -9
- package/hedhog/table/course.yaml +4 -0
- package/hedhog/table/course_lesson_file.yaml +8 -0
- package/hedhog/table/course_video_resolution_profile.yaml +22 -0
- package/hedhog/table/video_resolution_profile.yaml +18 -0
- package/package.json +7 -6
- package/src/certificate/certificate.controller.ts +19 -14
- package/src/certificate/certificate.service.ts +106 -11
- package/src/course/course-structure.controller.ts +24 -2
- package/src/course/course-structure.service.ts +21 -4
- package/src/course/course-video-conversion.service.ts +415 -0
- package/src/course/course.controller.ts +18 -0
- package/src/course/course.module.ts +15 -2
- package/src/course/course.service.ts +72 -2
- package/src/course/dto/create-course-structure-lesson.dto.ts +13 -2
- package/src/course/dto/create-course.dto.ts +8 -0
- package/src/enterprise/enterprise.controller.ts +0 -1
- package/src/evaluation/evaluation.service.ts +9 -2
- package/src/index.ts +1 -0
- package/src/lms.module.ts +3 -0
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +16 -0
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +16 -0
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +62 -0
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +128 -0
- package/src/video-resolution-profile/video-resolution-profile.module.ts +13 -0
- package/src/video-resolution-profile/video-resolution-profile.service.ts +117 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
FileText,
|
|
16
16
|
Layers,
|
|
17
17
|
Loader2,
|
|
18
|
+
Pencil,
|
|
18
19
|
Plus,
|
|
19
20
|
Save,
|
|
20
21
|
Undo2,
|
|
@@ -30,6 +31,7 @@ import { toast } from 'sonner';
|
|
|
30
31
|
import { z } from 'zod';
|
|
31
32
|
|
|
32
33
|
import { createDefaultTemplate } from '@/app/(app)/(libraries)/lms/_lib/editor/types';
|
|
34
|
+
import { FfmpegParamsEditor } from '@/components/ffmpeg-params-editor';
|
|
33
35
|
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
34
36
|
import { Badge } from '@/components/ui/badge';
|
|
35
37
|
import { Button } from '@/components/ui/button';
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
DialogHeader,
|
|
43
45
|
DialogTitle,
|
|
44
46
|
} from '@/components/ui/dialog';
|
|
47
|
+
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
45
48
|
import {
|
|
46
49
|
Form,
|
|
47
50
|
FormControl,
|
|
@@ -51,17 +54,24 @@ import {
|
|
|
51
54
|
FormMessage,
|
|
52
55
|
} from '@/components/ui/form';
|
|
53
56
|
import { Input } from '@/components/ui/input';
|
|
54
|
-
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
55
57
|
import { Separator } from '@/components/ui/separator';
|
|
56
58
|
import {
|
|
57
59
|
Sheet,
|
|
58
|
-
SheetContent,
|
|
59
60
|
SheetDescription,
|
|
60
61
|
SheetFooter,
|
|
61
62
|
SheetHeader,
|
|
62
63
|
SheetTitle,
|
|
63
64
|
} from '@/components/ui/sheet';
|
|
65
|
+
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
66
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
64
67
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
68
|
+
import {
|
|
69
|
+
Tooltip,
|
|
70
|
+
TooltipContent,
|
|
71
|
+
TooltipProvider,
|
|
72
|
+
TooltipTrigger,
|
|
73
|
+
} from '@/components/ui/tooltip';
|
|
74
|
+
import { useIsMobile } from '@/hooks/use-mobile';
|
|
65
75
|
import { cn } from '@/lib/utils';
|
|
66
76
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
67
77
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
@@ -95,13 +105,14 @@ type ApiCourseDetail = {
|
|
|
95
105
|
id: number;
|
|
96
106
|
name: string;
|
|
97
107
|
slug: string;
|
|
108
|
+
code?: string | null;
|
|
98
109
|
title: string;
|
|
99
110
|
description: string;
|
|
100
111
|
primaryColor?: string | null;
|
|
101
112
|
secondaryColor?: string | null;
|
|
102
113
|
level: 'beginner' | 'intermediate' | 'advanced';
|
|
103
114
|
status: 'draft' | 'published' | 'archived';
|
|
104
|
-
offeringType
|
|
115
|
+
offeringType?: 'scheduled' | 'on_demand' | 'blended' | null;
|
|
105
116
|
categories: string[];
|
|
106
117
|
isFeatured: boolean;
|
|
107
118
|
hasCertificate: boolean;
|
|
@@ -138,6 +149,13 @@ type ApiCourseDetail = {
|
|
|
138
149
|
operationsProjectName?: string | null;
|
|
139
150
|
};
|
|
140
151
|
|
|
152
|
+
type VideoProfileOption = {
|
|
153
|
+
id: number;
|
|
154
|
+
name: string;
|
|
155
|
+
ffmpeg_params: string;
|
|
156
|
+
status: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
141
159
|
type ApiCategory = { id: number; slug: string; name: string };
|
|
142
160
|
type ApiCategoryDetail = {
|
|
143
161
|
id: number;
|
|
@@ -246,12 +264,23 @@ function toApiStatus(status: CourseEditFormValues['status']) {
|
|
|
246
264
|
}
|
|
247
265
|
|
|
248
266
|
function toPtOfferingType(
|
|
249
|
-
value
|
|
267
|
+
value?: ApiCourseDetail['offeringType'] | string | null
|
|
250
268
|
): CourseEditFormValues['tipoOferta'] {
|
|
251
269
|
const n = normalizeEnumValue(value);
|
|
252
270
|
if (n === 'scheduled' || n === 'agendado') return 'agendado';
|
|
253
271
|
if (n === 'blended' || n === 'hibrido') return 'hibrido';
|
|
254
|
-
|
|
272
|
+
if (
|
|
273
|
+
n === 'on_demand' ||
|
|
274
|
+
n === 'ondemand' ||
|
|
275
|
+
n === 'on-demand' ||
|
|
276
|
+
n === 'sob_demanda' ||
|
|
277
|
+
n === 'sob demanda'
|
|
278
|
+
) {
|
|
279
|
+
return 'sob_demanda';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Safe fallback: keep video profiles tab hidden when backend sends invalid data.
|
|
283
|
+
return 'agendado';
|
|
255
284
|
}
|
|
256
285
|
|
|
257
286
|
function toApiOfferingType(value: CourseEditFormValues['tipoOferta']) {
|
|
@@ -309,7 +338,7 @@ function mapApiCourseResourceToLocal(
|
|
|
309
338
|
): CourseResourceItem {
|
|
310
339
|
return {
|
|
311
340
|
id: String(item.id),
|
|
312
|
-
fileId: item.fileId ??
|
|
341
|
+
fileId: item.fileId ?? undefined,
|
|
313
342
|
name: item.nome,
|
|
314
343
|
size: '',
|
|
315
344
|
type: item.tipo ?? 'file',
|
|
@@ -326,10 +355,12 @@ function buildSchema(t: (key: string) => string) {
|
|
|
326
355
|
.trim()
|
|
327
356
|
.min(2, t('validation.codeMin'))
|
|
328
357
|
.max(32, t('validation.codeMax'))
|
|
329
|
-
.regex(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
)
|
|
358
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, t('validation.codeFormat')),
|
|
359
|
+
code: z
|
|
360
|
+
.string()
|
|
361
|
+
.regex(/^[A-Z0-9]{2,}$/, t('validation.editionCodeFormat'))
|
|
362
|
+
.or(z.literal(''))
|
|
363
|
+
.optional(),
|
|
333
364
|
nomeInterno: z.string().trim().min(3, t('validation.internalNameMin')),
|
|
334
365
|
tituloComercial: z.string().trim().min(3, t('validation.titleMin')),
|
|
335
366
|
descricaoPublica: z.string().trim(),
|
|
@@ -367,6 +398,7 @@ export function EditorCourse() {
|
|
|
367
398
|
const { request, currentLocaleCode, locales } = useApp();
|
|
368
399
|
const t = useTranslations('lms.CursoEditPage');
|
|
369
400
|
const router = useRouter();
|
|
401
|
+
const isMobile = useIsMobile();
|
|
370
402
|
const queryClient = useQueryClient();
|
|
371
403
|
const createSessionMutation = useCreateSessionMutation();
|
|
372
404
|
|
|
@@ -414,6 +446,18 @@ export function EditorCourse() {
|
|
|
414
446
|
const [isUploadingResources, setIsUploadingResources] = useState(false);
|
|
415
447
|
const [isSavingResources, setIsSavingResources] = useState(false);
|
|
416
448
|
const resourcesInputRef = useRef<HTMLInputElement>(null);
|
|
449
|
+
const [linkedProfileIds, setLinkedProfileIds] = useState<number[]>([]);
|
|
450
|
+
const [videoProfilePickerResetKey, setVideoProfilePickerResetKey] =
|
|
451
|
+
useState(0);
|
|
452
|
+
const [videoProfileEditSheetOpen, setVideoProfileEditSheetOpen] =
|
|
453
|
+
useState(false);
|
|
454
|
+
const [editingVideoProfileId, setEditingVideoProfileId] = useState<
|
|
455
|
+
number | null
|
|
456
|
+
>(null);
|
|
457
|
+
const [editingVideoProfileName, setEditingVideoProfileName] = useState('');
|
|
458
|
+
const [editingVideoProfileParams, setEditingVideoProfileParams] =
|
|
459
|
+
useState('');
|
|
460
|
+
const [savingVideoProfileEdit, setSavingVideoProfileEdit] = useState(false);
|
|
417
461
|
|
|
418
462
|
// ── Queries ─────────────────────────────────────────────────────────────────
|
|
419
463
|
const { data: apiCourse, refetch: refetchCourse } = useQuery<ApiCourseDetail>(
|
|
@@ -525,6 +569,22 @@ export function EditorCourse() {
|
|
|
525
569
|
initialData: { data: [] },
|
|
526
570
|
});
|
|
527
571
|
|
|
572
|
+
const {
|
|
573
|
+
data: allVideoProfiles = [],
|
|
574
|
+
refetch: refetchVideoProfiles,
|
|
575
|
+
isFetching: isFetchingVideoProfiles,
|
|
576
|
+
} = useQuery<VideoProfileOption[]>({
|
|
577
|
+
queryKey: ['lms-video-resolution-profiles-all'],
|
|
578
|
+
queryFn: async () => {
|
|
579
|
+
const response = await request<VideoProfileOption[]>({
|
|
580
|
+
url: '/lms/video-resolution-profiles/all',
|
|
581
|
+
method: 'GET',
|
|
582
|
+
});
|
|
583
|
+
return response.data;
|
|
584
|
+
},
|
|
585
|
+
initialData: [],
|
|
586
|
+
});
|
|
587
|
+
|
|
528
588
|
// ── Save mutation ───────────────────────────────────────────────────────────
|
|
529
589
|
const { mutate: saveCourse, isPending: saving } = useMutation({
|
|
530
590
|
mutationFn: async (data: CourseEditFormValues) => {
|
|
@@ -534,6 +594,7 @@ export function EditorCourse() {
|
|
|
534
594
|
data: {
|
|
535
595
|
name: data.nomeInterno.trim(),
|
|
536
596
|
slug: data.slug.trim().toLowerCase(),
|
|
597
|
+
code: data.code?.trim() || undefined,
|
|
537
598
|
title: data.tituloComercial.trim(),
|
|
538
599
|
description: data.descricaoPublica.trim(),
|
|
539
600
|
requirements: data.preRequisitos?.trim() || undefined,
|
|
@@ -560,6 +621,15 @@ export function EditorCourse() {
|
|
|
560
621
|
return data;
|
|
561
622
|
},
|
|
562
623
|
onSuccess: (data) => {
|
|
624
|
+
if (courseId) {
|
|
625
|
+
void request({
|
|
626
|
+
url: `/lms/courses/${courseId}/video-resolution-profiles/sync`,
|
|
627
|
+
method: 'POST',
|
|
628
|
+
data: { profileIds: linkedProfileIds },
|
|
629
|
+
}).catch(() => {
|
|
630
|
+
toast.error('Erro ao sincronizar perfis de vídeo.');
|
|
631
|
+
});
|
|
632
|
+
}
|
|
563
633
|
setPersistedCertificateModel(data.modeloCertificado || '');
|
|
564
634
|
updateCourseInStore({
|
|
565
635
|
title: data.tituloComercial,
|
|
@@ -586,6 +656,7 @@ export function EditorCourse() {
|
|
|
586
656
|
resolver: zodResolver(schema),
|
|
587
657
|
defaultValues: {
|
|
588
658
|
slug: '',
|
|
659
|
+
code: '',
|
|
589
660
|
nomeInterno: '',
|
|
590
661
|
tituloComercial: '',
|
|
591
662
|
descricaoPublica: '',
|
|
@@ -716,15 +787,27 @@ export function EditorCourse() {
|
|
|
716
787
|
);
|
|
717
788
|
}, [certificateTemplateData, createdTemplateOptions]);
|
|
718
789
|
|
|
790
|
+
const availableVideoProfiles = useMemo(
|
|
791
|
+
() =>
|
|
792
|
+
allVideoProfiles.filter(
|
|
793
|
+
(profile) => !linkedProfileIds.includes(profile.id)
|
|
794
|
+
),
|
|
795
|
+
[allVideoProfiles, linkedProfileIds]
|
|
796
|
+
);
|
|
797
|
+
|
|
719
798
|
// ── Structural stats ─────────────────────────────────────────────────────────
|
|
720
799
|
const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
|
|
721
800
|
const hours = Math.floor(totalMinutes / 60);
|
|
722
801
|
const minutes = totalMinutes % 60;
|
|
802
|
+
const durationLabel = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
723
803
|
const publishedCount = lessons.filter(
|
|
724
804
|
(l) => l.visibility === 'publico' || l.status === 'publicada'
|
|
725
805
|
).length;
|
|
726
806
|
|
|
727
807
|
const formValues = form.watch();
|
|
808
|
+
const showVideoTab =
|
|
809
|
+
formValues.tipoOferta === 'sob_demanda' ||
|
|
810
|
+
formValues.tipoOferta === 'hibrido';
|
|
728
811
|
const descriptionText = String(formValues.descricaoPublica ?? '')
|
|
729
812
|
.replace(/<[^>]+>/g, '')
|
|
730
813
|
.trim();
|
|
@@ -789,6 +872,7 @@ export function EditorCourse() {
|
|
|
789
872
|
apiCourse.certificateModel ?? persistedCertificateModel ?? '';
|
|
790
873
|
form.reset({
|
|
791
874
|
slug: apiCourse.slug,
|
|
875
|
+
code: apiCourse.code ?? '',
|
|
792
876
|
nomeInterno: apiCourse.name,
|
|
793
877
|
tituloComercial: apiCourse.title,
|
|
794
878
|
descricaoPublica: apiCourse.description ?? '',
|
|
@@ -855,6 +939,32 @@ export function EditorCourse() {
|
|
|
855
939
|
};
|
|
856
940
|
}, [courseId, request]);
|
|
857
941
|
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
if (!showVideoTab && activeTab === 'videos') {
|
|
944
|
+
setActiveTab('estrutura');
|
|
945
|
+
}
|
|
946
|
+
}, [showVideoTab, activeTab]);
|
|
947
|
+
|
|
948
|
+
useEffect(() => {
|
|
949
|
+
if (!courseId) return;
|
|
950
|
+
let active = true;
|
|
951
|
+
void request<VideoProfileOption[]>({
|
|
952
|
+
url: `/lms/courses/${courseId}/video-resolution-profiles`,
|
|
953
|
+
method: 'GET',
|
|
954
|
+
})
|
|
955
|
+
.then((res) => {
|
|
956
|
+
if (!active) return;
|
|
957
|
+
setLinkedProfileIds((res.data ?? []).map((p) => p.id));
|
|
958
|
+
})
|
|
959
|
+
.catch(() => {
|
|
960
|
+
if (!active) return;
|
|
961
|
+
setLinkedProfileIds([]);
|
|
962
|
+
});
|
|
963
|
+
return () => {
|
|
964
|
+
active = false;
|
|
965
|
+
};
|
|
966
|
+
}, [courseId, request]);
|
|
967
|
+
|
|
858
968
|
async function syncCourseResources(nextResources: CourseResourceItem[]) {
|
|
859
969
|
setIsSavingResources(true);
|
|
860
970
|
try {
|
|
@@ -1140,6 +1250,38 @@ export function EditorCourse() {
|
|
|
1140
1250
|
setInstructorEditSheetOpen(true);
|
|
1141
1251
|
}
|
|
1142
1252
|
|
|
1253
|
+
function handleEditVideoProfile(profileId: number) {
|
|
1254
|
+
const profile = allVideoProfiles.find((p) => p.id === profileId);
|
|
1255
|
+
if (!profile) return;
|
|
1256
|
+
setEditingVideoProfileId(profileId);
|
|
1257
|
+
setEditingVideoProfileName(profile.name);
|
|
1258
|
+
setEditingVideoProfileParams(profile.ffmpeg_params);
|
|
1259
|
+
setVideoProfileEditSheetOpen(true);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async function handleSaveVideoProfileEdit() {
|
|
1263
|
+
if (!editingVideoProfileId) return;
|
|
1264
|
+
try {
|
|
1265
|
+
setSavingVideoProfileEdit(true);
|
|
1266
|
+
await request({
|
|
1267
|
+
url: `/lms/video-resolution-profiles/${editingVideoProfileId}`,
|
|
1268
|
+
method: 'PATCH',
|
|
1269
|
+
data: {
|
|
1270
|
+
name: editingVideoProfileName,
|
|
1271
|
+
ffmpeg_params: editingVideoProfileParams,
|
|
1272
|
+
},
|
|
1273
|
+
});
|
|
1274
|
+
await refetchVideoProfiles();
|
|
1275
|
+
setVideoProfileEditSheetOpen(false);
|
|
1276
|
+
setEditingVideoProfileId(null);
|
|
1277
|
+
toast.success('Perfil de vídeo atualizado com sucesso.');
|
|
1278
|
+
} catch {
|
|
1279
|
+
toast.error('Erro ao atualizar o perfil de vídeo.');
|
|
1280
|
+
} finally {
|
|
1281
|
+
setSavingVideoProfileEdit(false);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1143
1285
|
async function handleEditCategory(categorySlug: string) {
|
|
1144
1286
|
const category = (categoryListData?.data ?? []).find(
|
|
1145
1287
|
(item) => item.slug === categorySlug
|
|
@@ -1245,6 +1387,33 @@ export function EditorCourse() {
|
|
|
1245
1387
|
}
|
|
1246
1388
|
|
|
1247
1389
|
function onSubmit(data: CourseEditFormValues) {
|
|
1390
|
+
if (data.status === 'ativo') {
|
|
1391
|
+
const requiredProfileIds = new Set(linkedProfileIds);
|
|
1392
|
+
const invalidLesson = lessons.find((lesson) => {
|
|
1393
|
+
if (lesson.type !== 'video' || lesson.videoProvider !== 'file_storage') {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
if (requiredProfileIds.size === 0) {
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const resourceTypes = new Set(lesson.resources.map((res) => res.type));
|
|
1402
|
+
return [...requiredProfileIds].some(
|
|
1403
|
+
(profileId) => !resourceTypes.has(`video_profile:${profileId}`)
|
|
1404
|
+
);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
if (invalidLesson) {
|
|
1408
|
+
toast.error(
|
|
1409
|
+
linkedProfileIds.length === 0
|
|
1410
|
+
? 'Configure ao menos um perfil de vídeo antes de publicar o curso.'
|
|
1411
|
+
: `A aula "${invalidLesson.title}" precisa de um vídeo para cada perfil antes da publicação.`
|
|
1412
|
+
);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1248
1417
|
saveCourse(data);
|
|
1249
1418
|
}
|
|
1250
1419
|
|
|
@@ -1276,21 +1445,123 @@ export function EditorCourse() {
|
|
|
1276
1445
|
{course.slug}
|
|
1277
1446
|
</p>
|
|
1278
1447
|
</div>
|
|
1279
|
-
<
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1448
|
+
<div className="flex min-w-0 max-w-[56%] flex-wrap justify-end gap-1 sm:max-w-none sm:flex-nowrap sm:items-center sm:gap-1.5 sm:shrink-0">
|
|
1449
|
+
<TooltipProvider>
|
|
1450
|
+
<Tooltip>
|
|
1451
|
+
<TooltipTrigger asChild>
|
|
1452
|
+
<Badge
|
|
1453
|
+
variant="outline"
|
|
1454
|
+
className={cn(
|
|
1455
|
+
'h-5 gap-1 font-normal bg-sky-500/15 text-sky-700 dark:text-sky-400 border-sky-500/30 cursor-help',
|
|
1456
|
+
isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
|
|
1457
|
+
)}
|
|
1458
|
+
>
|
|
1459
|
+
<Layers className="size-3" />
|
|
1460
|
+
{sessions.length}
|
|
1461
|
+
{!isMobile && (
|
|
1462
|
+
<> {sessions.length === 1 ? 'sessão' : 'sessões'}</>
|
|
1463
|
+
)}
|
|
1464
|
+
</Badge>
|
|
1465
|
+
</TooltipTrigger>
|
|
1466
|
+
<TooltipContent side="bottom">
|
|
1467
|
+
Total de sessões cadastradas neste curso.
|
|
1468
|
+
</TooltipContent>
|
|
1469
|
+
</Tooltip>
|
|
1470
|
+
|
|
1471
|
+
<Tooltip>
|
|
1472
|
+
<TooltipTrigger asChild>
|
|
1473
|
+
<Badge
|
|
1474
|
+
variant="outline"
|
|
1475
|
+
className={cn(
|
|
1476
|
+
'h-5 gap-1 font-normal bg-violet-500/15 text-violet-700 dark:text-violet-400 border-violet-500/30 cursor-help',
|
|
1477
|
+
isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
|
|
1478
|
+
)}
|
|
1479
|
+
>
|
|
1480
|
+
<Video className="size-3" />
|
|
1481
|
+
{lessons.length}
|
|
1482
|
+
{!isMobile && (
|
|
1483
|
+
<> {lessons.length === 1 ? 'aula' : 'aulas'}</>
|
|
1484
|
+
)}
|
|
1485
|
+
</Badge>
|
|
1486
|
+
</TooltipTrigger>
|
|
1487
|
+
<TooltipContent side="bottom">
|
|
1488
|
+
Quantidade total de aulas do curso.
|
|
1489
|
+
</TooltipContent>
|
|
1490
|
+
</Tooltip>
|
|
1491
|
+
|
|
1492
|
+
<Tooltip>
|
|
1493
|
+
<TooltipTrigger asChild>
|
|
1494
|
+
<Badge
|
|
1495
|
+
variant="outline"
|
|
1496
|
+
className={cn(
|
|
1497
|
+
'h-5 gap-1 font-normal bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30 cursor-help',
|
|
1498
|
+
isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
|
|
1499
|
+
)}
|
|
1500
|
+
>
|
|
1501
|
+
<Clock className="size-3" />
|
|
1502
|
+
{durationLabel}
|
|
1503
|
+
</Badge>
|
|
1504
|
+
</TooltipTrigger>
|
|
1505
|
+
<TooltipContent side="bottom">
|
|
1506
|
+
Duração somada de todas as aulas.
|
|
1507
|
+
</TooltipContent>
|
|
1508
|
+
</Tooltip>
|
|
1509
|
+
|
|
1510
|
+
<Tooltip>
|
|
1511
|
+
<TooltipTrigger asChild>
|
|
1512
|
+
<Badge
|
|
1513
|
+
className={cn(
|
|
1514
|
+
'h-5 gap-1 font-normal bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-0 hover:bg-emerald-500/20 cursor-help',
|
|
1515
|
+
isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
|
|
1516
|
+
)}
|
|
1517
|
+
>
|
|
1518
|
+
<span className="size-1.5 rounded-full bg-emerald-500 inline-block" />
|
|
1519
|
+
{publishedCount}
|
|
1520
|
+
{!isMobile && ' publicadas'}
|
|
1521
|
+
</Badge>
|
|
1522
|
+
</TooltipTrigger>
|
|
1523
|
+
<TooltipContent side="bottom">
|
|
1524
|
+
Quantidade de aulas visiveis para os alunos.
|
|
1525
|
+
</TooltipContent>
|
|
1526
|
+
</Tooltip>
|
|
1527
|
+
|
|
1528
|
+
<Tooltip>
|
|
1529
|
+
<TooltipTrigger asChild>
|
|
1530
|
+
<Badge
|
|
1531
|
+
variant="outline"
|
|
1532
|
+
className={cn(
|
|
1533
|
+
'text-xs border-0 cursor-help',
|
|
1534
|
+
course.published
|
|
1535
|
+
? 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-400 hover:bg-cyan-500/20'
|
|
1536
|
+
: 'bg-slate-500/15 text-slate-700 dark:text-slate-400 hover:bg-slate-500/20'
|
|
1537
|
+
)}
|
|
1538
|
+
>
|
|
1539
|
+
{course.published ? 'Publicado' : 'Rascunho'}
|
|
1540
|
+
</Badge>
|
|
1541
|
+
</TooltipTrigger>
|
|
1542
|
+
<TooltipContent side="bottom">
|
|
1543
|
+
Status atual do curso.
|
|
1544
|
+
</TooltipContent>
|
|
1545
|
+
</Tooltip>
|
|
1546
|
+
</TooltipProvider>
|
|
1547
|
+
</div>
|
|
1285
1548
|
</div>
|
|
1286
1549
|
|
|
1287
1550
|
{/* ── Tabs ─────────────────────────────────────────────────────────── */}
|
|
1288
1551
|
<Tabs
|
|
1289
1552
|
value={activeTab}
|
|
1290
1553
|
onValueChange={setActiveTab}
|
|
1291
|
-
className="flex flex-
|
|
1554
|
+
className="flex flex-1 min-h-0 min-w-0 flex-col"
|
|
1292
1555
|
>
|
|
1293
|
-
<TabsList
|
|
1556
|
+
<TabsList
|
|
1557
|
+
className={cn(
|
|
1558
|
+
'mx-3 mt-3 h-auto shrink-0 w-[calc(100%-1.5rem)] rounded-lg bg-muted/80 p-1',
|
|
1559
|
+
'flex items-center gap-1 overflow-x-auto overflow-y-hidden whitespace-nowrap',
|
|
1560
|
+
isMobile
|
|
1561
|
+
? 'touch-pan-x snap-x snap-mandatory [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted-foreground/35'
|
|
1562
|
+
: '[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden'
|
|
1563
|
+
)}
|
|
1564
|
+
>
|
|
1294
1565
|
{(
|
|
1295
1566
|
[
|
|
1296
1567
|
'estrutura',
|
|
@@ -1298,13 +1569,19 @@ export function EditorCourse() {
|
|
|
1298
1569
|
'midia',
|
|
1299
1570
|
'recursos',
|
|
1300
1571
|
'extra',
|
|
1572
|
+
...(showVideoTab ? ['videos'] : []),
|
|
1301
1573
|
'publicacao',
|
|
1302
1574
|
] as const
|
|
1303
1575
|
).map((tab) => (
|
|
1304
1576
|
<TabsTrigger
|
|
1305
1577
|
key={tab}
|
|
1306
1578
|
value={tab}
|
|
1307
|
-
className=
|
|
1579
|
+
className={cn(
|
|
1580
|
+
'shrink-0 font-medium',
|
|
1581
|
+
isMobile
|
|
1582
|
+
? 'h-7 snap-start px-2.5 text-[11px]'
|
|
1583
|
+
: 'h-8 px-3 text-xs'
|
|
1584
|
+
)}
|
|
1308
1585
|
>
|
|
1309
1586
|
{tab === 'estrutura'
|
|
1310
1587
|
? t('structureEditor.tabs.structure')
|
|
@@ -1316,349 +1593,498 @@ export function EditorCourse() {
|
|
|
1316
1593
|
? t('structureEditor.tabs.resources')
|
|
1317
1594
|
: tab === 'extra'
|
|
1318
1595
|
? t('structureEditor.tabs.extra')
|
|
1319
|
-
:
|
|
1596
|
+
: tab === 'videos'
|
|
1597
|
+
? t('structureEditor.tabs.videoProfiles')
|
|
1598
|
+
: t('structureEditor.tabs.publish')}
|
|
1320
1599
|
</TabsTrigger>
|
|
1321
1600
|
))}
|
|
1322
1601
|
</TabsList>
|
|
1323
1602
|
|
|
1324
|
-
<div className="flex-1 min-h-0 overflow-hidden">
|
|
1325
|
-
<
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
<
|
|
1334
|
-
<
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
<
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
/>
|
|
1358
|
-
<StatChip
|
|
1359
|
-
icon={
|
|
1360
|
-
<CheckCircle2 className="size-3 text-emerald-500" />
|
|
1361
|
-
}
|
|
1362
|
-
label="Publicadas"
|
|
1363
|
-
value={publishedCount}
|
|
1364
|
-
/>
|
|
1365
|
-
</div>
|
|
1366
|
-
</CardContent>
|
|
1367
|
-
</Card>
|
|
1368
|
-
|
|
1369
|
-
{/* Dados principais */}
|
|
1370
|
-
<Card className="bg-muted/20 py-2 gap-2">
|
|
1371
|
-
<CardHeader className="px-3 pt-2 pb-0">
|
|
1372
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1373
|
-
Dados principais
|
|
1374
|
-
</CardTitle>
|
|
1375
|
-
</CardHeader>
|
|
1376
|
-
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
1603
|
+
<div className="flex-1 min-h-0 min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain">
|
|
1604
|
+
<div className="flex min-w-0 flex-col gap-3 p-2 sm:p-3">
|
|
1605
|
+
{/* ── Tab: Estrutura ──────────────────────────────────────── */}
|
|
1606
|
+
<TabsContent
|
|
1607
|
+
value="estrutura"
|
|
1608
|
+
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
1609
|
+
>
|
|
1610
|
+
{/* Dados principais */}
|
|
1611
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
1612
|
+
<CardHeader className="px-3 pt-2 pb-0">
|
|
1613
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1614
|
+
{t('structureEditor.structureTab.mainInfo.title')}
|
|
1615
|
+
</CardTitle>
|
|
1616
|
+
</CardHeader>
|
|
1617
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
1618
|
+
<FormField
|
|
1619
|
+
control={form.control}
|
|
1620
|
+
name="tituloComercial"
|
|
1621
|
+
render={({ field }) => (
|
|
1622
|
+
<FormItem>
|
|
1623
|
+
<FormLabel className="text-xs">
|
|
1624
|
+
{t(
|
|
1625
|
+
'structureEditor.structureTab.mainInfo.fields.title'
|
|
1626
|
+
)}
|
|
1627
|
+
</FormLabel>
|
|
1628
|
+
<FormControl>
|
|
1629
|
+
<Input {...field} className="h-8 text-sm" />
|
|
1630
|
+
</FormControl>
|
|
1631
|
+
<FormMessage className="text-xs" />
|
|
1632
|
+
</FormItem>
|
|
1633
|
+
)}
|
|
1634
|
+
/>
|
|
1635
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
1377
1636
|
<FormField
|
|
1378
1637
|
control={form.control}
|
|
1379
|
-
name="
|
|
1638
|
+
name="nomeInterno"
|
|
1380
1639
|
render={({ field }) => (
|
|
1381
1640
|
<FormItem>
|
|
1382
|
-
<FormLabel className="text-xs">
|
|
1641
|
+
<FormLabel className="text-xs">
|
|
1642
|
+
{t(
|
|
1643
|
+
'structureEditor.structureTab.mainInfo.fields.internalName'
|
|
1644
|
+
)}
|
|
1645
|
+
</FormLabel>
|
|
1383
1646
|
<FormControl>
|
|
1384
|
-
<Input {...field} className="h-8 text-
|
|
1647
|
+
<Input {...field} className="h-8 text-xs" />
|
|
1385
1648
|
</FormControl>
|
|
1386
1649
|
<FormMessage className="text-xs" />
|
|
1387
1650
|
</FormItem>
|
|
1388
1651
|
)}
|
|
1389
1652
|
/>
|
|
1390
|
-
<div className="grid grid-cols-2 gap-2">
|
|
1391
|
-
<FormField
|
|
1392
|
-
control={form.control}
|
|
1393
|
-
name="nomeInterno"
|
|
1394
|
-
render={({ field }) => (
|
|
1395
|
-
<FormItem>
|
|
1396
|
-
<FormLabel className="text-xs">
|
|
1397
|
-
Nome Interno
|
|
1398
|
-
</FormLabel>
|
|
1399
|
-
<FormControl>
|
|
1400
|
-
<Input {...field} className="h-8 text-xs" />
|
|
1401
|
-
</FormControl>
|
|
1402
|
-
<FormMessage className="text-xs" />
|
|
1403
|
-
</FormItem>
|
|
1404
|
-
)}
|
|
1405
|
-
/>
|
|
1406
|
-
<FormField
|
|
1407
|
-
control={form.control}
|
|
1408
|
-
name="slug"
|
|
1409
|
-
render={({ field }) => (
|
|
1410
|
-
<FormItem>
|
|
1411
|
-
<FormLabel className="text-xs">Slug</FormLabel>
|
|
1412
|
-
<FormControl>
|
|
1413
|
-
<Input
|
|
1414
|
-
{...field}
|
|
1415
|
-
className="h-8 text-xs font-mono"
|
|
1416
|
-
onChange={(e) =>
|
|
1417
|
-
field.onChange(e.target.value.toLowerCase())
|
|
1418
|
-
}
|
|
1419
|
-
/>
|
|
1420
|
-
</FormControl>
|
|
1421
|
-
<FormMessage className="text-xs" />
|
|
1422
|
-
</FormItem>
|
|
1423
|
-
)}
|
|
1424
|
-
/>
|
|
1425
|
-
</div>
|
|
1426
|
-
</CardContent>
|
|
1427
|
-
</Card>
|
|
1428
|
-
|
|
1429
|
-
{/* Descrição */}
|
|
1430
|
-
<Card className="bg-muted/20 py-2 gap-2">
|
|
1431
|
-
<CardHeader className="px-3 pt-2 pb-0">
|
|
1432
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1433
|
-
Descrição
|
|
1434
|
-
</CardTitle>
|
|
1435
|
-
</CardHeader>
|
|
1436
|
-
<CardContent className="px-3 pb-2">
|
|
1437
1653
|
<FormField
|
|
1438
1654
|
control={form.control}
|
|
1439
|
-
name="
|
|
1655
|
+
name="slug"
|
|
1440
1656
|
render={({ field }) => (
|
|
1441
1657
|
<FormItem>
|
|
1658
|
+
<FormLabel className="text-xs">
|
|
1659
|
+
{t(
|
|
1660
|
+
'structureEditor.structureTab.mainInfo.fields.slug'
|
|
1661
|
+
)}
|
|
1662
|
+
</FormLabel>
|
|
1442
1663
|
<FormControl>
|
|
1443
|
-
<
|
|
1444
|
-
|
|
1445
|
-
|
|
1664
|
+
<Input
|
|
1665
|
+
{...field}
|
|
1666
|
+
className="h-8 text-xs font-mono"
|
|
1667
|
+
onChange={(e) =>
|
|
1668
|
+
field.onChange(e.target.value.toLowerCase())
|
|
1669
|
+
}
|
|
1446
1670
|
/>
|
|
1447
1671
|
</FormControl>
|
|
1448
1672
|
<FormMessage className="text-xs" />
|
|
1449
1673
|
</FormItem>
|
|
1450
1674
|
)}
|
|
1451
1675
|
/>
|
|
1452
|
-
</
|
|
1453
|
-
</
|
|
1454
|
-
</
|
|
1455
|
-
|
|
1456
|
-
{/*
|
|
1457
|
-
<
|
|
1458
|
-
<
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1676
|
+
</div>
|
|
1677
|
+
</CardContent>
|
|
1678
|
+
</Card>
|
|
1679
|
+
|
|
1680
|
+
{/* Descrição */}
|
|
1681
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
1682
|
+
<CardHeader className="px-3 pt-2 pb-0">
|
|
1683
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1684
|
+
{t('structureEditor.structureTab.description.title')}
|
|
1685
|
+
</CardTitle>
|
|
1686
|
+
</CardHeader>
|
|
1687
|
+
<CardContent className="px-3 pb-2">
|
|
1688
|
+
<FormField
|
|
1689
|
+
control={form.control}
|
|
1690
|
+
name="descricaoPublica"
|
|
1691
|
+
render={({ field }) => (
|
|
1692
|
+
<FormItem>
|
|
1693
|
+
<FormControl>
|
|
1694
|
+
<RichTextEditor
|
|
1695
|
+
value={field.value}
|
|
1696
|
+
onChange={field.onChange}
|
|
1697
|
+
/>
|
|
1698
|
+
</FormControl>
|
|
1699
|
+
<FormMessage className="text-xs" />
|
|
1700
|
+
</FormItem>
|
|
1701
|
+
)}
|
|
1702
|
+
/>
|
|
1703
|
+
</CardContent>
|
|
1704
|
+
</Card>
|
|
1705
|
+
</TabsContent>
|
|
1706
|
+
|
|
1707
|
+
{/* ── Tab: Sobre ──────────────────────────────────────────── */}
|
|
1708
|
+
<TabsContent
|
|
1709
|
+
value="sobre"
|
|
1710
|
+
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
1711
|
+
>
|
|
1712
|
+
<CourseClassificationCard
|
|
1713
|
+
form={form}
|
|
1714
|
+
t={t}
|
|
1715
|
+
levels={NIVEIS}
|
|
1716
|
+
statuses={STATUS_OPTIONS}
|
|
1717
|
+
offeringTypes={OFFERING_TYPE_OPTIONS}
|
|
1718
|
+
/>
|
|
1719
|
+
<CourseRelationsCard
|
|
1720
|
+
form={form}
|
|
1721
|
+
t={t}
|
|
1722
|
+
categoryOptions={categoryOptions}
|
|
1723
|
+
projectOptions={projectOptions}
|
|
1724
|
+
instructorOptions={instructorOptions}
|
|
1725
|
+
onCreateCategory={handleCreateCategory}
|
|
1726
|
+
onCreateInstructor={handleCreateInstructor}
|
|
1727
|
+
onEditCategory={handleEditCategory}
|
|
1728
|
+
onEditInstructor={handleEditInstructor}
|
|
1729
|
+
/>
|
|
1730
|
+
<CourseContentCard form={form} />
|
|
1731
|
+
</TabsContent>
|
|
1732
|
+
|
|
1733
|
+
{/* ── Tab: Mídia ──────────────────────────────────────────── */}
|
|
1734
|
+
<TabsContent value="midia" className="mt-0 min-w-0">
|
|
1735
|
+
<CourseMediaCard
|
|
1736
|
+
logoPreview={logoPreview}
|
|
1737
|
+
bannerPreview={bannerPreview}
|
|
1738
|
+
uploadingLogo={uploadingLogo}
|
|
1739
|
+
uploadingBanner={uploadingBanner}
|
|
1740
|
+
onLogoSelect={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
1741
|
+
handleFileSelect(e, setLogoPreview, 'logo')
|
|
1742
|
+
}
|
|
1743
|
+
onBannerSelect={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
1744
|
+
handleFileSelect(e, setBannerPreview, 'banner')
|
|
1745
|
+
}
|
|
1746
|
+
logoFile={
|
|
1747
|
+
apiCourse?.logoFileId
|
|
1748
|
+
? {
|
|
1749
|
+
id: apiCourse.logoFileId,
|
|
1750
|
+
name:
|
|
1751
|
+
apiCourse.logoFilename ||
|
|
1752
|
+
`#${apiCourse.logoFileId}`,
|
|
1753
|
+
}
|
|
1754
|
+
: undefined
|
|
1755
|
+
}
|
|
1756
|
+
bannerFile={
|
|
1757
|
+
apiCourse?.bannerFileId
|
|
1758
|
+
? {
|
|
1759
|
+
id: apiCourse.bannerFileId,
|
|
1760
|
+
name:
|
|
1761
|
+
apiCourse.bannerFilename ||
|
|
1762
|
+
`#${apiCourse.bannerFileId}`,
|
|
1763
|
+
}
|
|
1764
|
+
: undefined
|
|
1765
|
+
}
|
|
1766
|
+
onOpenLogoFile={() => openUploadedFile(apiCourse?.logoFileId)}
|
|
1767
|
+
onOpenBannerFile={() =>
|
|
1768
|
+
openUploadedFile(apiCourse?.bannerFileId)
|
|
1769
|
+
}
|
|
1770
|
+
onRemoveLogoFile={
|
|
1771
|
+
apiCourse?.logoFileId
|
|
1772
|
+
? () => handleRemoveFile('logo')
|
|
1773
|
+
: undefined
|
|
1774
|
+
}
|
|
1775
|
+
onRemoveBannerFile={
|
|
1776
|
+
apiCourse?.bannerFileId
|
|
1777
|
+
? () => handleRemoveFile('banner')
|
|
1778
|
+
: undefined
|
|
1779
|
+
}
|
|
1780
|
+
logoImageType={apiCourse?.logoImageType}
|
|
1781
|
+
bannerImageType={apiCourse?.bannerImageType}
|
|
1782
|
+
t={t}
|
|
1783
|
+
/>
|
|
1784
|
+
</TabsContent>
|
|
1785
|
+
|
|
1786
|
+
{/* ── Tab: Recursos ─────────────────────────────────────── */}
|
|
1787
|
+
<TabsContent
|
|
1788
|
+
value="recursos"
|
|
1789
|
+
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
1790
|
+
>
|
|
1791
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
1792
|
+
<CardHeader className="px-3 pt-2 pb-0">
|
|
1793
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1794
|
+
{t('structureEditor.resources.title')}
|
|
1795
|
+
</CardTitle>
|
|
1796
|
+
</CardHeader>
|
|
1797
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
1798
|
+
<div
|
|
1799
|
+
role="button"
|
|
1800
|
+
tabIndex={0}
|
|
1801
|
+
aria-label={t(
|
|
1802
|
+
'structureEditor.resources.dropzoneAriaLabel'
|
|
1803
|
+
)}
|
|
1804
|
+
aria-disabled={isUploadingResources || isSavingResources}
|
|
1805
|
+
onDragOver={(event) => {
|
|
1806
|
+
event.preventDefault();
|
|
1807
|
+
if (!isUploadingResources && !isSavingResources)
|
|
1808
|
+
setResourcesDragOver(true);
|
|
1809
|
+
}}
|
|
1810
|
+
onDragLeave={() => setResourcesDragOver(false)}
|
|
1811
|
+
onDrop={(event) => {
|
|
1812
|
+
event.preventDefault();
|
|
1813
|
+
setResourcesDragOver(false);
|
|
1814
|
+
if (!isUploadingResources && !isSavingResources) {
|
|
1815
|
+
void handleCourseResourceFiles(
|
|
1816
|
+
Array.from(event.dataTransfer.files)
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
}}
|
|
1820
|
+
onClick={() => {
|
|
1821
|
+
if (!isUploadingResources && !isSavingResources) {
|
|
1822
|
+
resourcesInputRef.current?.click();
|
|
1823
|
+
}
|
|
1824
|
+
}}
|
|
1825
|
+
onKeyDown={(event) => {
|
|
1826
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1827
|
+
event.preventDefault();
|
|
1828
|
+
if (!isUploadingResources && !isSavingResources) {
|
|
1829
|
+
resourcesInputRef.current?.click();
|
|
1499
1830
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1831
|
+
}
|
|
1832
|
+
}}
|
|
1833
|
+
className={cn(
|
|
1834
|
+
'flex flex-col items-center justify-center gap-2 py-7 rounded-lg border-2 border-dashed transition-colors select-none',
|
|
1835
|
+
isUploadingResources || isSavingResources
|
|
1836
|
+
? 'cursor-wait border-border opacity-60'
|
|
1837
|
+
: 'cursor-pointer',
|
|
1838
|
+
!isUploadingResources &&
|
|
1839
|
+
!isSavingResources &&
|
|
1840
|
+
resourcesDragOver
|
|
1841
|
+
? 'border-primary/70 bg-primary/5'
|
|
1842
|
+
: !isUploadingResources && !isSavingResources
|
|
1843
|
+
? 'border-border hover:border-primary/40 hover:bg-muted/30'
|
|
1844
|
+
: ''
|
|
1845
|
+
)}
|
|
1846
|
+
>
|
|
1847
|
+
{isUploadingResources || isSavingResources ? (
|
|
1848
|
+
<Loader2 className="size-7 animate-spin text-muted-foreground/50" />
|
|
1849
|
+
) : (
|
|
1850
|
+
<UploadCloud
|
|
1851
|
+
className={cn(
|
|
1852
|
+
'size-7 transition-colors',
|
|
1853
|
+
resourcesDragOver
|
|
1854
|
+
? 'text-primary'
|
|
1855
|
+
: 'text-muted-foreground/50'
|
|
1856
|
+
)}
|
|
1857
|
+
/>
|
|
1858
|
+
)}
|
|
1859
|
+
<div className="text-center">
|
|
1860
|
+
<p className="text-xs font-medium">
|
|
1861
|
+
{isUploadingResources
|
|
1862
|
+
? t('structureEditor.resources.uploading')
|
|
1863
|
+
: isSavingResources
|
|
1864
|
+
? t('structureEditor.resources.saving')
|
|
1865
|
+
: t('structureEditor.resources.idle')}
|
|
1866
|
+
</p>
|
|
1867
|
+
{!isUploadingResources && !isSavingResources && (
|
|
1868
|
+
<p className="text-xs text-muted-foreground">
|
|
1869
|
+
{t('structureEditor.resources.helper')}
|
|
1870
|
+
</p>
|
|
1871
|
+
)}
|
|
1872
|
+
</div>
|
|
1873
|
+
<input
|
|
1874
|
+
ref={resourcesInputRef}
|
|
1875
|
+
type="file"
|
|
1876
|
+
multiple
|
|
1877
|
+
className="hidden"
|
|
1878
|
+
onChange={(event) => {
|
|
1879
|
+
if (event.target.files) {
|
|
1880
|
+
void handleCourseResourceFiles(
|
|
1881
|
+
Array.from(event.target.files)
|
|
1882
|
+
);
|
|
1883
|
+
event.target.value = '';
|
|
1509
1884
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
openUploadedFile(apiCourse?.logoFileId)
|
|
1514
|
-
}
|
|
1515
|
-
onOpenBannerFile={() =>
|
|
1516
|
-
openUploadedFile(apiCourse?.bannerFileId)
|
|
1517
|
-
}
|
|
1518
|
-
onRemoveLogoFile={
|
|
1519
|
-
apiCourse?.logoFileId
|
|
1520
|
-
? () => handleRemoveFile('logo')
|
|
1521
|
-
: undefined
|
|
1522
|
-
}
|
|
1523
|
-
onRemoveBannerFile={
|
|
1524
|
-
apiCourse?.bannerFileId
|
|
1525
|
-
? () => handleRemoveFile('banner')
|
|
1526
|
-
: undefined
|
|
1527
|
-
}
|
|
1528
|
-
logoImageType={apiCourse?.logoImageType}
|
|
1529
|
-
bannerImageType={apiCourse?.bannerImageType}
|
|
1530
|
-
t={t}
|
|
1531
|
-
/>
|
|
1532
|
-
</TabsContent>
|
|
1885
|
+
}}
|
|
1886
|
+
/>
|
|
1887
|
+
</div>
|
|
1533
1888
|
|
|
1534
|
-
|
|
1889
|
+
{courseResources.length === 0 ? (
|
|
1890
|
+
<p className="text-center text-xs text-muted-foreground py-1">
|
|
1891
|
+
{t('structureEditor.resources.empty')}
|
|
1892
|
+
</p>
|
|
1893
|
+
) : (
|
|
1894
|
+
<div className="flex flex-col gap-1">
|
|
1895
|
+
{courseResources.map((item) => {
|
|
1896
|
+
const ItemIcon = getResourceIcon(item.type);
|
|
1897
|
+
return (
|
|
1898
|
+
<div
|
|
1899
|
+
key={item.id}
|
|
1900
|
+
className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
|
|
1901
|
+
>
|
|
1902
|
+
<ItemIcon
|
|
1903
|
+
className={cn(
|
|
1904
|
+
'size-3.5 shrink-0',
|
|
1905
|
+
getResourceIconColor(item.type)
|
|
1906
|
+
)}
|
|
1907
|
+
/>
|
|
1908
|
+
<div className="flex-1 min-w-0">
|
|
1909
|
+
<p className="text-xs truncate font-medium">
|
|
1910
|
+
{item.name}
|
|
1911
|
+
</p>
|
|
1912
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1913
|
+
{item.size ||
|
|
1914
|
+
t('structureEditor.resources.attachedFile')}
|
|
1915
|
+
</p>
|
|
1916
|
+
</div>
|
|
1917
|
+
<Button
|
|
1918
|
+
type="button"
|
|
1919
|
+
variant="ghost"
|
|
1920
|
+
size="icon"
|
|
1921
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
1922
|
+
onClick={() =>
|
|
1923
|
+
void handleDownloadCourseResource(item)
|
|
1924
|
+
}
|
|
1925
|
+
aria-label={t(
|
|
1926
|
+
'structureEditor.resources.downloadAria',
|
|
1927
|
+
{ name: item.name }
|
|
1928
|
+
)}
|
|
1929
|
+
>
|
|
1930
|
+
<Download className="size-3" />
|
|
1931
|
+
</Button>
|
|
1932
|
+
<Button
|
|
1933
|
+
type="button"
|
|
1934
|
+
variant="ghost"
|
|
1935
|
+
size="icon"
|
|
1936
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
|
1937
|
+
onClick={() =>
|
|
1938
|
+
void handleRemoveCourseResource(item)
|
|
1939
|
+
}
|
|
1940
|
+
aria-label={t(
|
|
1941
|
+
'structureEditor.resources.removeAria',
|
|
1942
|
+
{ name: item.name }
|
|
1943
|
+
)}
|
|
1944
|
+
>
|
|
1945
|
+
<X className="size-3" />
|
|
1946
|
+
</Button>
|
|
1947
|
+
</div>
|
|
1948
|
+
);
|
|
1949
|
+
})}
|
|
1950
|
+
</div>
|
|
1951
|
+
)}
|
|
1952
|
+
</CardContent>
|
|
1953
|
+
</Card>
|
|
1954
|
+
</TabsContent>
|
|
1955
|
+
|
|
1956
|
+
{/* ── Tab: Extra ──────────────────────────────────────────── */}
|
|
1957
|
+
<TabsContent
|
|
1958
|
+
value="extra"
|
|
1959
|
+
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
1960
|
+
>
|
|
1961
|
+
<CourseCertificateCard
|
|
1962
|
+
form={form}
|
|
1963
|
+
t={t}
|
|
1964
|
+
options={certificateOptions}
|
|
1965
|
+
onCreateTemplate={handleCreateCertificateTemplate}
|
|
1966
|
+
/>
|
|
1967
|
+
<CourseFlagsCard form={form} t={t} />
|
|
1968
|
+
<CourseDangerZoneCard
|
|
1969
|
+
t={t}
|
|
1970
|
+
onDelete={() => setDeleteDialogOpen(true)}
|
|
1971
|
+
/>
|
|
1972
|
+
</TabsContent>
|
|
1973
|
+
|
|
1974
|
+
{/* ── Tab: Vídeos (on_demand / blended only) ─────────────── */}
|
|
1975
|
+
{showVideoTab && (
|
|
1535
1976
|
<TabsContent
|
|
1536
|
-
value="
|
|
1537
|
-
className="mt-0 flex flex-col gap-3"
|
|
1977
|
+
value="videos"
|
|
1978
|
+
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
1538
1979
|
>
|
|
1539
1980
|
<Card className="bg-muted/20 py-2 gap-2">
|
|
1540
1981
|
<CardHeader className="px-3 pt-2 pb-0">
|
|
1541
1982
|
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1542
|
-
|
|
1983
|
+
{t('structureEditor.videoProfiles.title')}
|
|
1543
1984
|
</CardTitle>
|
|
1544
1985
|
</CardHeader>
|
|
1545
|
-
<CardContent className="px-3 pb-2 flex flex-col gap-
|
|
1546
|
-
<
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
if (
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
onDrop={(event) => {
|
|
1560
|
-
event.preventDefault();
|
|
1561
|
-
setResourcesDragOver(false);
|
|
1562
|
-
if (!isUploadingResources && !isSavingResources) {
|
|
1563
|
-
void handleCourseResourceFiles(
|
|
1564
|
-
Array.from(event.dataTransfer.files)
|
|
1986
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
1987
|
+
<EntityPicker
|
|
1988
|
+
key={videoProfilePickerResetKey}
|
|
1989
|
+
placeholder={t(
|
|
1990
|
+
'structureEditor.videoProfiles.placeholder'
|
|
1991
|
+
)}
|
|
1992
|
+
options={availableVideoProfiles}
|
|
1993
|
+
getOptionValue={(opt) => opt.id}
|
|
1994
|
+
getOptionLabel={(opt) => opt.name}
|
|
1995
|
+
onChange={(_val, opt) => {
|
|
1996
|
+
if (opt && !linkedProfileIds.includes(opt.id)) {
|
|
1997
|
+
setLinkedProfileIds((prev) => [...prev, opt.id]);
|
|
1998
|
+
setVideoProfilePickerResetKey(
|
|
1999
|
+
(current) => current + 1
|
|
1565
2000
|
);
|
|
1566
2001
|
}
|
|
1567
2002
|
}}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2003
|
+
showCreateButton
|
|
2004
|
+
createTitle={t(
|
|
2005
|
+
'structureEditor.videoProfiles.createTitle'
|
|
2006
|
+
)}
|
|
2007
|
+
createFields={[
|
|
2008
|
+
{
|
|
2009
|
+
name: 'name',
|
|
2010
|
+
label: t(
|
|
2011
|
+
'structureEditor.videoProfiles.createFields.name.label'
|
|
2012
|
+
),
|
|
2013
|
+
placeholder: t(
|
|
2014
|
+
'structureEditor.videoProfiles.createFields.name.placeholder'
|
|
2015
|
+
),
|
|
2016
|
+
required: true,
|
|
2017
|
+
},
|
|
2018
|
+
{
|
|
2019
|
+
name: 'ffmpeg_params',
|
|
2020
|
+
label: t(
|
|
2021
|
+
'structureEditor.videoProfiles.createFields.ffmpegParams.label'
|
|
2022
|
+
),
|
|
2023
|
+
placeholder: t(
|
|
2024
|
+
'structureEditor.videoProfiles.createFields.ffmpegParams.placeholder'
|
|
2025
|
+
),
|
|
2026
|
+
required: true,
|
|
2027
|
+
},
|
|
2028
|
+
]}
|
|
2029
|
+
onCreate={async (values) => {
|
|
2030
|
+
try {
|
|
2031
|
+
const response = await request<VideoProfileOption>({
|
|
2032
|
+
url: '/lms/video-resolution-profiles',
|
|
2033
|
+
method: 'POST',
|
|
2034
|
+
data: {
|
|
2035
|
+
name: values.name,
|
|
2036
|
+
ffmpeg_params: values.ffmpeg_params,
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
await refetchVideoProfiles();
|
|
2040
|
+
return response.data;
|
|
2041
|
+
} catch {
|
|
2042
|
+
toast.error(
|
|
2043
|
+
t('structureEditor.videoProfiles.createError')
|
|
2044
|
+
);
|
|
2045
|
+
return null;
|
|
1579
2046
|
}
|
|
1580
2047
|
}}
|
|
1581
|
-
|
|
1582
|
-
'flex flex-col items-center justify-center gap-2 py-7 rounded-lg border-2 border-dashed transition-colors select-none',
|
|
1583
|
-
isUploadingResources || isSavingResources
|
|
1584
|
-
? 'cursor-wait border-border opacity-60'
|
|
1585
|
-
: 'cursor-pointer',
|
|
1586
|
-
!isUploadingResources &&
|
|
1587
|
-
!isSavingResources &&
|
|
1588
|
-
resourcesDragOver
|
|
1589
|
-
? 'border-primary/70 bg-primary/5'
|
|
1590
|
-
: !isUploadingResources && !isSavingResources
|
|
1591
|
-
? 'border-border hover:border-primary/40 hover:bg-muted/30'
|
|
1592
|
-
: ''
|
|
1593
|
-
)}
|
|
1594
|
-
>
|
|
1595
|
-
{isUploadingResources || isSavingResources ? (
|
|
1596
|
-
<Loader2 className="size-7 animate-spin text-muted-foreground/50" />
|
|
1597
|
-
) : (
|
|
1598
|
-
<UploadCloud
|
|
1599
|
-
className={cn(
|
|
1600
|
-
'size-7 transition-colors',
|
|
1601
|
-
resourcesDragOver
|
|
1602
|
-
? 'text-primary'
|
|
1603
|
-
: 'text-muted-foreground/50'
|
|
1604
|
-
)}
|
|
1605
|
-
/>
|
|
1606
|
-
)}
|
|
1607
|
-
<div className="text-center">
|
|
1608
|
-
<p className="text-xs font-medium">
|
|
1609
|
-
{isUploadingResources
|
|
1610
|
-
? 'Enviando arquivos...'
|
|
1611
|
-
: isSavingResources
|
|
1612
|
-
? 'Salvando recursos...'
|
|
1613
|
-
: 'Arraste arquivos para adicionar'}
|
|
1614
|
-
</p>
|
|
1615
|
-
{!isUploadingResources && !isSavingResources && (
|
|
1616
|
-
<p className="text-xs text-muted-foreground">
|
|
1617
|
-
Clique para selecionar arquivos
|
|
1618
|
-
</p>
|
|
1619
|
-
)}
|
|
1620
|
-
</div>
|
|
1621
|
-
<input
|
|
1622
|
-
ref={resourcesInputRef}
|
|
1623
|
-
type="file"
|
|
1624
|
-
multiple
|
|
1625
|
-
className="hidden"
|
|
1626
|
-
onChange={(event) => {
|
|
1627
|
-
if (event.target.files) {
|
|
1628
|
-
void handleCourseResourceFiles(
|
|
1629
|
-
Array.from(event.target.files)
|
|
1630
|
-
);
|
|
1631
|
-
event.target.value = '';
|
|
1632
|
-
}
|
|
1633
|
-
}}
|
|
1634
|
-
/>
|
|
1635
|
-
</div>
|
|
2048
|
+
/>
|
|
1636
2049
|
|
|
1637
|
-
{
|
|
2050
|
+
{isFetchingVideoProfiles ? (
|
|
2051
|
+
<div className="flex flex-col gap-1">
|
|
2052
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
2053
|
+
<div
|
|
2054
|
+
key={`video-profile-skeleton-${index}`}
|
|
2055
|
+
className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
|
|
2056
|
+
>
|
|
2057
|
+
<Skeleton className="size-3.5 rounded-full shrink-0" />
|
|
2058
|
+
<Skeleton className="h-3 w-full max-w-56" />
|
|
2059
|
+
</div>
|
|
2060
|
+
))}
|
|
2061
|
+
</div>
|
|
2062
|
+
) : linkedProfileIds.length === 0 ? (
|
|
1638
2063
|
<p className="text-center text-xs text-muted-foreground py-1">
|
|
1639
|
-
|
|
2064
|
+
{t('structureEditor.videoProfiles.empty')}
|
|
1640
2065
|
</p>
|
|
1641
2066
|
) : (
|
|
1642
2067
|
<div className="flex flex-col gap-1">
|
|
1643
|
-
{
|
|
1644
|
-
const
|
|
2068
|
+
{linkedProfileIds.map((profileId) => {
|
|
2069
|
+
const profile = allVideoProfiles.find(
|
|
2070
|
+
(p) => p.id === profileId
|
|
2071
|
+
);
|
|
1645
2072
|
return (
|
|
1646
2073
|
<div
|
|
1647
|
-
key={
|
|
2074
|
+
key={profileId}
|
|
1648
2075
|
className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
|
|
1649
2076
|
>
|
|
1650
|
-
<
|
|
1651
|
-
className={cn(
|
|
1652
|
-
'size-3.5 shrink-0',
|
|
1653
|
-
getResourceIconColor(item.type)
|
|
1654
|
-
)}
|
|
1655
|
-
/>
|
|
2077
|
+
<Video className="size-3.5 shrink-0 text-violet-500" />
|
|
1656
2078
|
<div className="flex-1 min-w-0">
|
|
1657
|
-
<p className="text-xs
|
|
1658
|
-
{
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
2079
|
+
<p className="text-xs font-medium truncate">
|
|
2080
|
+
{profile?.name?.trim()
|
|
2081
|
+
? profile.name
|
|
2082
|
+
: t(
|
|
2083
|
+
'structureEditor.videoProfiles.fallbackName',
|
|
2084
|
+
{
|
|
2085
|
+
id: profileId,
|
|
2086
|
+
}
|
|
2087
|
+
)}
|
|
1662
2088
|
</p>
|
|
1663
2089
|
</div>
|
|
1664
2090
|
<Button
|
|
@@ -1667,11 +2093,13 @@ export function EditorCourse() {
|
|
|
1667
2093
|
size="icon"
|
|
1668
2094
|
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
1669
2095
|
onClick={() =>
|
|
1670
|
-
|
|
2096
|
+
handleEditVideoProfile(profileId)
|
|
1671
2097
|
}
|
|
1672
|
-
aria-label={
|
|
2098
|
+
aria-label={t(
|
|
2099
|
+
'structureEditor.videoProfiles.editAria'
|
|
2100
|
+
)}
|
|
1673
2101
|
>
|
|
1674
|
-
<
|
|
2102
|
+
<Pencil className="size-3" />
|
|
1675
2103
|
</Button>
|
|
1676
2104
|
<Button
|
|
1677
2105
|
type="button"
|
|
@@ -1679,9 +2107,13 @@ export function EditorCourse() {
|
|
|
1679
2107
|
size="icon"
|
|
1680
2108
|
className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
|
1681
2109
|
onClick={() =>
|
|
1682
|
-
|
|
2110
|
+
setLinkedProfileIds((prev) =>
|
|
2111
|
+
prev.filter((id) => id !== profileId)
|
|
2112
|
+
)
|
|
1683
2113
|
}
|
|
1684
|
-
aria-label={
|
|
2114
|
+
aria-label={t(
|
|
2115
|
+
'structureEditor.videoProfiles.removeAria'
|
|
2116
|
+
)}
|
|
1685
2117
|
>
|
|
1686
2118
|
<X className="size-3" />
|
|
1687
2119
|
</Button>
|
|
@@ -1693,98 +2125,79 @@ export function EditorCourse() {
|
|
|
1693
2125
|
</CardContent>
|
|
1694
2126
|
</Card>
|
|
1695
2127
|
</TabsContent>
|
|
2128
|
+
)}
|
|
1696
2129
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
<CheckCircle2 className="size-4 shrink-0 text-emerald-500" />
|
|
1731
|
-
) : (
|
|
1732
|
-
<AlertTriangle className="size-4 shrink-0 text-amber-500" />
|
|
1733
|
-
)}
|
|
1734
|
-
<div className="flex-1 min-w-0">
|
|
1735
|
-
<p className="text-xs font-medium truncate">
|
|
1736
|
-
{item.label}
|
|
2130
|
+
{/* ── Tab: Publicação ─────────────────────────────────────── */}
|
|
2131
|
+
<TabsContent
|
|
2132
|
+
value="publicacao"
|
|
2133
|
+
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
2134
|
+
>
|
|
2135
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
2136
|
+
<CardHeader className="px-3 pt-2 pb-0">
|
|
2137
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
2138
|
+
{t('structureEditor.publishChecklist.title')}
|
|
2139
|
+
</CardTitle>
|
|
2140
|
+
</CardHeader>
|
|
2141
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
2142
|
+
{checklistItems.map((item) => (
|
|
2143
|
+
<div
|
|
2144
|
+
key={item.id}
|
|
2145
|
+
className="flex items-center gap-2 rounded-md border bg-background/80 px-2.5 py-2"
|
|
2146
|
+
>
|
|
2147
|
+
{item.done ? (
|
|
2148
|
+
<CheckCircle2 className="size-4 shrink-0 text-emerald-500" />
|
|
2149
|
+
) : (
|
|
2150
|
+
<AlertTriangle className="size-4 shrink-0 text-amber-500" />
|
|
2151
|
+
)}
|
|
2152
|
+
<div className="flex-1 min-w-0">
|
|
2153
|
+
<p className="text-xs font-medium truncate">
|
|
2154
|
+
{item.label}
|
|
2155
|
+
</p>
|
|
2156
|
+
{!item.done && (
|
|
2157
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
2158
|
+
{item.required
|
|
2159
|
+
? t('structureEditor.publishChecklist.required')
|
|
2160
|
+
: t(
|
|
2161
|
+
'structureEditor.publishChecklist.recommended'
|
|
2162
|
+
)}
|
|
1737
2163
|
</p>
|
|
1738
|
-
|
|
1739
|
-
<p className="text-[0.65rem] text-muted-foreground">
|
|
1740
|
-
{item.required
|
|
1741
|
-
? t(
|
|
1742
|
-
'structureEditor.publishChecklist.required'
|
|
1743
|
-
)
|
|
1744
|
-
: t(
|
|
1745
|
-
'structureEditor.publishChecklist.recommended'
|
|
1746
|
-
)}
|
|
1747
|
-
</p>
|
|
1748
|
-
)}
|
|
1749
|
-
</div>
|
|
1750
|
-
<Badge
|
|
1751
|
-
variant={item.done ? 'default' : 'secondary'}
|
|
1752
|
-
className="text-[0.65rem]"
|
|
1753
|
-
>
|
|
1754
|
-
{item.done
|
|
1755
|
-
? t('structureEditor.publishChecklist.done')
|
|
1756
|
-
: t('structureEditor.publishChecklist.missing')}
|
|
1757
|
-
</Badge>
|
|
2164
|
+
)}
|
|
1758
2165
|
</div>
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
</
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2166
|
+
<Badge
|
|
2167
|
+
variant={item.done ? 'default' : 'secondary'}
|
|
2168
|
+
className="text-[0.65rem]"
|
|
2169
|
+
>
|
|
2170
|
+
{item.done
|
|
2171
|
+
? t('structureEditor.publishChecklist.done')
|
|
2172
|
+
: t('structureEditor.publishChecklist.missing')}
|
|
2173
|
+
</Badge>
|
|
2174
|
+
</div>
|
|
2175
|
+
))}
|
|
2176
|
+
</CardContent>
|
|
2177
|
+
</Card>
|
|
2178
|
+
|
|
2179
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
2180
|
+
<CardHeader className="px-3 pt-2 pb-0">
|
|
2181
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
2182
|
+
{t('structureEditor.publishChecklist.statusTitle')}
|
|
2183
|
+
</CardTitle>
|
|
2184
|
+
</CardHeader>
|
|
2185
|
+
<CardContent className="px-3 pb-2 flex items-center justify-between gap-2">
|
|
2186
|
+
<p className="text-xs text-muted-foreground">
|
|
2187
|
+
{t('structureEditor.publishChecklist.statusProgress', {
|
|
2188
|
+
completed: completedRequired,
|
|
2189
|
+
total: requiredChecklist.length,
|
|
2190
|
+
})}
|
|
2191
|
+
</p>
|
|
2192
|
+
<Badge variant={isReadyToPublish ? 'default' : 'secondary'}>
|
|
2193
|
+
{isReadyToPublish
|
|
2194
|
+
? t('structureEditor.publishChecklist.ready')
|
|
2195
|
+
: t('structureEditor.publishChecklist.notReady')}
|
|
2196
|
+
</Badge>
|
|
2197
|
+
</CardContent>
|
|
2198
|
+
</Card>
|
|
2199
|
+
</TabsContent>
|
|
2200
|
+
</div>
|
|
1788
2201
|
</div>
|
|
1789
2202
|
</Tabs>
|
|
1790
2203
|
|
|
@@ -1801,7 +2214,7 @@ export function EditorCourse() {
|
|
|
1801
2214
|
onClick={() => form.reset()}
|
|
1802
2215
|
>
|
|
1803
2216
|
<Undo2 className="size-3 mr-1" />
|
|
1804
|
-
|
|
2217
|
+
{t('structureEditor.footer.cancel')}
|
|
1805
2218
|
</Button>
|
|
1806
2219
|
<div className="flex-1" />
|
|
1807
2220
|
<Button
|
|
@@ -1813,7 +2226,7 @@ export function EditorCourse() {
|
|
|
1813
2226
|
onClick={() => createSessionMutation.mutate()}
|
|
1814
2227
|
>
|
|
1815
2228
|
<Plus className="size-3 mr-1" />
|
|
1816
|
-
|
|
2229
|
+
{t('structureEditor.footer.newSession')}
|
|
1817
2230
|
</Button>
|
|
1818
2231
|
<Button
|
|
1819
2232
|
type="submit"
|
|
@@ -1826,7 +2239,7 @@ export function EditorCourse() {
|
|
|
1826
2239
|
) : (
|
|
1827
2240
|
<Save className="size-3 mr-1" />
|
|
1828
2241
|
)}
|
|
1829
|
-
|
|
2242
|
+
{t('structureEditor.footer.save')}
|
|
1830
2243
|
</Button>
|
|
1831
2244
|
</div>
|
|
1832
2245
|
</div>
|
|
@@ -1859,6 +2272,68 @@ export function EditorCourse() {
|
|
|
1859
2272
|
}}
|
|
1860
2273
|
/>
|
|
1861
2274
|
|
|
2275
|
+
<Sheet
|
|
2276
|
+
open={videoProfileEditSheetOpen}
|
|
2277
|
+
onOpenChange={(open) => {
|
|
2278
|
+
setVideoProfileEditSheetOpen(open);
|
|
2279
|
+
if (!open) setEditingVideoProfileId(null);
|
|
2280
|
+
}}
|
|
2281
|
+
>
|
|
2282
|
+
<ResizableSheetContent
|
|
2283
|
+
sheetId="lms-course-structure-video-profile-edit-sheet"
|
|
2284
|
+
defaultWidth={560}
|
|
2285
|
+
minWidth={420}
|
|
2286
|
+
maxWidth={920}
|
|
2287
|
+
className="sm:max-w-lg"
|
|
2288
|
+
>
|
|
2289
|
+
<SheetHeader>
|
|
2290
|
+
<SheetTitle>
|
|
2291
|
+
{t('structureEditor.videoProfiles.sheet.title')}
|
|
2292
|
+
</SheetTitle>
|
|
2293
|
+
<SheetDescription>
|
|
2294
|
+
{t('structureEditor.videoProfiles.sheet.description')}
|
|
2295
|
+
</SheetDescription>
|
|
2296
|
+
</SheetHeader>
|
|
2297
|
+
<div className="space-y-3 px-4 pb-4 pt-2">
|
|
2298
|
+
<div className="space-y-1.5">
|
|
2299
|
+
<FormLabel>
|
|
2300
|
+
{t('structureEditor.videoProfiles.createFields.name.label')}
|
|
2301
|
+
</FormLabel>
|
|
2302
|
+
<Input
|
|
2303
|
+
value={editingVideoProfileName}
|
|
2304
|
+
onChange={(e) => setEditingVideoProfileName(e.target.value)}
|
|
2305
|
+
placeholder={t(
|
|
2306
|
+
'structureEditor.videoProfiles.createFields.name.placeholder'
|
|
2307
|
+
)}
|
|
2308
|
+
/>
|
|
2309
|
+
</div>
|
|
2310
|
+
<FfmpegParamsEditor
|
|
2311
|
+
value={editingVideoProfileParams}
|
|
2312
|
+
onChange={setEditingVideoProfileParams}
|
|
2313
|
+
/>
|
|
2314
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
2315
|
+
<Button
|
|
2316
|
+
type="button"
|
|
2317
|
+
variant="outline"
|
|
2318
|
+
onClick={() => setVideoProfileEditSheetOpen(false)}
|
|
2319
|
+
disabled={savingVideoProfileEdit}
|
|
2320
|
+
>
|
|
2321
|
+
{t('structureEditor.videoProfiles.sheet.actions.cancel')}
|
|
2322
|
+
</Button>
|
|
2323
|
+
<Button
|
|
2324
|
+
type="button"
|
|
2325
|
+
disabled={savingVideoProfileEdit}
|
|
2326
|
+
onClick={() => void handleSaveVideoProfileEdit()}
|
|
2327
|
+
>
|
|
2328
|
+
{savingVideoProfileEdit
|
|
2329
|
+
? t('structureEditor.videoProfiles.sheet.actions.saving')
|
|
2330
|
+
: t('structureEditor.videoProfiles.sheet.actions.save')}
|
|
2331
|
+
</Button>
|
|
2332
|
+
</div>
|
|
2333
|
+
</div>
|
|
2334
|
+
</ResizableSheetContent>
|
|
2335
|
+
</Sheet>
|
|
2336
|
+
|
|
1862
2337
|
<Sheet
|
|
1863
2338
|
open={categoryEditSheetOpen}
|
|
1864
2339
|
onOpenChange={(open) => {
|
|
@@ -1875,7 +2350,13 @@ export function EditorCourse() {
|
|
|
1875
2350
|
}
|
|
1876
2351
|
}}
|
|
1877
2352
|
>
|
|
1878
|
-
<
|
|
2353
|
+
<ResizableSheetContent
|
|
2354
|
+
sheetId="lms-course-structure-category-edit-sheet"
|
|
2355
|
+
defaultWidth={560}
|
|
2356
|
+
minWidth={420}
|
|
2357
|
+
maxWidth={920}
|
|
2358
|
+
className="sm:max-w-lg"
|
|
2359
|
+
>
|
|
1879
2360
|
<SheetHeader>
|
|
1880
2361
|
<SheetTitle>Editar categoria</SheetTitle>
|
|
1881
2362
|
<SheetDescription>
|
|
@@ -1925,7 +2406,7 @@ export function EditorCourse() {
|
|
|
1925
2406
|
Salvar
|
|
1926
2407
|
</Button>
|
|
1927
2408
|
</SheetFooter>
|
|
1928
|
-
</
|
|
2409
|
+
</ResizableSheetContent>
|
|
1929
2410
|
</Sheet>
|
|
1930
2411
|
|
|
1931
2412
|
{/* ── Delete dialog ────────────────────────────────────────────────── */}
|