diff --git a/pages/ManageCourses.tsx b/pages/ManageCourses.tsx index 6d51956..db28756 100644 --- a/pages/ManageCourses.tsx +++ b/pages/ManageCourses.tsx @@ -40,6 +40,22 @@ import { getManagedCourses, saveCourse, deleteCourse, duplicateCourse, getCommen import { getAttachmentIcon, getAttachmentBadge } from '../components/MaterialCard'; import { BunnyVideo, courseApi, instructorApi, InstructorAnalytics } from '../services/api'; +const formatVideoDuration = (seconds: number | null | undefined) => { + if (!seconds || seconds < 1) return 'Processando'; + const totalSeconds = Math.round(seconds); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const remainingSeconds = totalSeconds % 60; + if (hours) return `${hours}h ${String(minutes).padStart(2, '0')}min`; + if (minutes) return `${minutes}min`; + return `${remainingSeconds}s`; +}; + +const totalVideoDuration = (lessons: Lesson[]) => { + const seconds = lessons.reduce((total, lesson) => total + (lesson.media?.[0]?.durationSeconds || 0), 0); + return seconds ? formatVideoDuration(seconds) : 'Em processamento'; +}; + // 1. Create/Edit Course Modal const CourseModal: React.FC<{ isOpen: boolean; @@ -72,7 +88,6 @@ const CourseModal: React.FC<{ // New Lesson State const [newLessonTitle, setNewLessonTitle] = useState(''); - const [newLessonDuration, setNewLessonDuration] = useState(''); const [newLessonMedia, setNewLessonMedia] = useState(null); const [newLessonIsFree, setNewLessonIsFree] = useState(false); const [bunnyConfigured, setBunnyConfigured] = useState(null); @@ -179,10 +194,6 @@ const CourseModal: React.FC<{ const uploaded = await instructorApi.uploadBunnyVideo(created.data.id, file); const media = toBunnyMedia(uploaded.data); setNewLessonMedia(media); - if (!newLessonDuration.trim() && uploaded.data.durationSeconds) { - const minutes = Math.max(1, Math.round(uploaded.data.durationSeconds / 60)); - setNewLessonDuration(`${minutes}m`); - } setBunnyUploadState('uploaded'); setBunnyUploadMessage(uploaded.data.status === 'ready' ? 'Vídeo pronto para ser adicionado à aula.' @@ -257,7 +268,7 @@ const CourseModal: React.FC<{ const refreshed = toBunnyMedia(response.data); setLessons((current) => current.map((item) => item.id === lessonId ? { ...item, - duration: refreshed.durationSeconds ? `${Math.max(1, Math.round(refreshed.durationSeconds / 60))}m` : item.duration, + duration: formatVideoDuration(refreshed.durationSeconds), media: [refreshed], } : item)); } catch (error) { @@ -290,7 +301,7 @@ const CourseModal: React.FC<{ if (!update) return lesson; return { ...lesson, - duration: update.media.durationSeconds ? `${Math.max(1, Math.round(update.media.durationSeconds / 60))}m` : lesson.duration, + duration: formatVideoDuration(update.media.durationSeconds), media: [update.media], }; })); @@ -312,7 +323,7 @@ const CourseModal: React.FC<{ const newLesson: Lesson = { id: `lesson-${Date.now()}`, title: newLessonTitle, - duration: newLessonDuration || '15m', + duration: formatVideoDuration(newLessonMedia.durationSeconds), videoUrl: newLessonMedia.playbackUrl || '', mediaProvider: newLessonMedia.provider, media: [newLessonMedia], @@ -323,7 +334,6 @@ const CourseModal: React.FC<{ setLessons([...lessons, newLesson]); setNewLessonTitle(''); - setNewLessonDuration(''); setNewLessonMedia(null); setNewLessonIsFree(false); setBunnyUploadState('idle'); @@ -406,7 +416,7 @@ const CourseModal: React.FC<{ setIsSubmitting(true); - const durationStr = `${lessons.length} Aulas`; + const durationStr = totalVideoDuration(lessons); try { await onSave({ @@ -785,20 +795,13 @@ const CourseModal: React.FC<{ Adicionar aula -
+
setNewLessonTitle(e.target.value)} - placeholder="Ex.: Aula 01 — Configuração inicial do Pixel" - className="flex-1 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-orange-500/50" - /> - setNewLessonDuration(e.target.value)} - placeholder="Ex.: 25 min" - className="w-28 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-orange-500/50" + placeholder="Título da aula" + className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-orange-500/50" />
diff --git a/server/src/routes/media.ts b/server/src/routes/media.ts index 644ea61..2f99864 100644 --- a/server/src/routes/media.ts +++ b/server/src/routes/media.ts @@ -40,6 +40,14 @@ async function persistBunnyStatus(video: { id: string; status: string; durationS where provider = 'bunny' and external_id = $1`, [video.id, video.status, video.durationSeconds], ); + if (video.durationSeconds !== null) { + await pool.query( + `update lessons + set duration_seconds = $2 + where id = (select lesson_id from lesson_media where provider = 'bunny' and external_id = $1)`, + [video.id, video.durationSeconds], + ); + } } export const mediaRoutes: FastifyPluginAsync = async (app) => { diff --git a/services/db.ts b/services/db.ts index b63044a..c36fd28 100644 --- a/services/db.ts +++ b/services/db.ts @@ -64,6 +64,12 @@ const toDuration = (seconds: number | null) => { return minutes >= 60 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${minutes}m`; }; +const toCourseDuration = (lessons: ApiCourse['lessons']) => { + const seconds = lessons.reduce((total, lesson) => total + (lesson.media[0]?.durationSeconds ?? lesson.durationSeconds ?? 0), 0); + if (seconds) return toDuration(seconds); + return lessons.length ? 'Processando' : '—'; +}; + const toCourse = (course: ApiCourse): Course => ({ id: course.id, title: course.title, @@ -77,7 +83,7 @@ const toCourse = (course: ApiCourse): Course => ({ status: course.status === 'draft' ? 'draft' : 'published', progress: course.progress, lastActivity: course.lastActivity || undefined, - duration: `${course.lessons.length} Aulas`, + duration: toCourseDuration(course.lessons), lessons: course.lessons.map((lesson): Lesson => { const media: LessonMedia[] = lesson.media.map((item) => ({ id: item.id, @@ -92,7 +98,7 @@ const toCourse = (course: ApiCourse): Course => ({ id: lesson.id, title: lesson.title, description: lesson.description, - duration: toDuration(lesson.durationSeconds), + duration: toDuration(media[0]?.durationSeconds ?? lesson.durationSeconds), isFree: lesson.accessLevel === 'public', videoUrl: media[0]?.playbackUrl || media[0]?.embedUrl || '', mediaProvider: media[0]?.provider, @@ -139,7 +145,7 @@ const toCoursePayload = (course: Course) => ({ id: /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(lesson.id) ? lesson.id : undefined, title: lesson.title, description: lesson.description || '', - durationSeconds: toSeconds(lesson.duration), + durationSeconds: lesson.media?.[0]?.durationSeconds ?? toSeconds(lesson.duration), accessLevel: lesson.isFree ? 'public' as const : 'enrolled' as const, media: lesson.media?.length ? lesson.media.map((media) => ({