// components/teacher/TeacherEditCourseForm.tsx "use client"; import { updateCourse, createModule, createLesson, reorderModules, reorderLessons, updateModuleTitle, } from "@/app/(protected)/teacher/actions"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { toast } from "sonner"; import { getClientLocale } from "@/lib/i18n/clientLocale"; import { getLessonContentTypeLabel, parseLessonDescriptionMeta, type LessonContentType, } from "@/lib/courses/lessonContent"; import { Prisma } from "@prisma/client"; function parseLearningOutcomes(val: Prisma.JsonValue | null | undefined): string[] { if (val == null) return []; if (Array.isArray(val)) return val.filter((x): x is string => typeof x === "string"); return []; } type CourseData = { id: string; slug: string; title: Prisma.JsonValue; description: Prisma.JsonValue; level: string; status: string; price: number; learningOutcomes?: Prisma.JsonValue | null; modules: { id: string; title: Prisma.JsonValue; lessons: { id: string; title: Prisma.JsonValue; description: Prisma.JsonValue | null }[]; }[]; }; const selectableLessonTypes: LessonContentType[] = [ "VIDEO", "LECTURE", "ACTIVITY", "QUIZ", "FINAL_EXAM", ]; function getLessonTypeBadgeClass(type: LessonContentType): string { if (type === "LECTURE") return "border-indigo-200 bg-indigo-50 text-indigo-700"; if (type === "ACTIVITY") return "border-rose-200 bg-rose-50 text-rose-700"; if (type === "QUIZ" || type === "FINAL_EXAM") return "border-amber-200 bg-amber-50 text-amber-700"; return "border-sky-200 bg-sky-50 text-sky-700"; } export default function TeacherEditCourseForm({ course }: { course: CourseData }) { const router = useRouter(); const [loading, setLoading] = useState(false); const [optimisticModules, setOptimisticModules] = useState(course.modules); const [editingModuleId, setEditingModuleId] = useState(null); const [editingTitle, setEditingTitle] = useState(""); const [newLessonTypeByModule, setNewLessonTypeByModule] = useState>({}); const [learningOutcomes, setLearningOutcomes] = useState(() => parseLearningOutcomes(course.learningOutcomes) ); useEffect(() => { setOptimisticModules(course.modules); setNewLessonTypeByModule( course.modules.reduce>((acc, module) => { acc[module.id] = acc[module.id] ?? "VIDEO"; return acc; }, {}), ); }, [course.modules]); useEffect(() => { setLearningOutcomes(parseLearningOutcomes(course.learningOutcomes)); }, [course.learningOutcomes]); const showSavedToast = () => { const isSpanish = getClientLocale() === "es"; toast.success(isSpanish ? "Cambios guardados" : "Changes saved", { description: isSpanish ? "Puedes seguir editando o volver al panel de cursos." : "You can keep editing or go back to the courses dashboard.", action: { label: isSpanish ? "Volver a cursos" : "Back to courses", onClick: () => router.push("/teacher"), }, cancel: { label: isSpanish ? "Seguir editando" : "Keep editing", onClick: () => {}, }, duration: 7000, }); }; // Helper for JSON/String fields const getStr = (val: Prisma.JsonValue) => { if (typeof val === "string") return val; if (val && typeof val === "object" && !Array.isArray(val)) { const v = val as Record; if (typeof v.en === "string") return v.en; if (typeof v.es === "string") return v.es; return ""; } return ""; }; // 1. SAVE COURSE SETTINGS async function handleSubmit(event: React.FormEvent) { event.preventDefault(); setLoading(true); const form = event.currentTarget; const formData = new FormData(form); formData.set("learningOutcomes", JSON.stringify(learningOutcomes)); const res = await updateCourse(course.id, course.slug, formData); if (res.success) { showSavedToast(); router.refresh(); } else { toast.error("Error al guardar"); } setLoading(false); } // 2. CREATE NEW MODULE const handleAddModule = async () => { setLoading(true); // Block UI while working const res = await createModule(course.id); if (res.success) { const isSpanish = getClientLocale() === "es"; toast.success(isSpanish ? "Módulo creado" : "Module created", { description: isSpanish ? "Puedes seguir editando o volver al panel de cursos." : "You can keep editing or go back to the courses dashboard.", action: { label: isSpanish ? "Volver a cursos" : "Back to courses", onClick: () => router.push("/teacher"), }, cancel: { label: isSpanish ? "Seguir editando" : "Keep editing", onClick: () => {}, }, duration: 7000, }); router.refresh(); } else { toast.error("Error al crear módulo"); } setLoading(false); }; // 3. CREATE NEW LESSON const handleAddLesson = async (moduleId: string) => { const selectedType = newLessonTypeByModule[moduleId] ?? "VIDEO"; setLoading(true); const res = await createLesson(moduleId, selectedType); if (res.success && res.lessonId) { toast.success(`Lección creada (${getLessonContentTypeLabel(selectedType)})`); // Redirect immediately to the video upload page router.push(`/teacher/courses/${course.slug}/lessons/${res.lessonId}`); } else { toast.error("Error al crear lección"); setLoading(false); // Only stop loading if we failed (otherwise we are redirecting) } }; const startEditingModuleTitle = (moduleId: string, currentTitle: string) => { setEditingModuleId(moduleId); setEditingTitle(currentTitle || ""); }; const cancelEditingModuleTitle = () => { setEditingModuleId(null); setEditingTitle(""); }; const saveModuleTitle = async () => { if (!editingModuleId || !editingTitle.trim()) { cancelEditingModuleTitle(); return; } setLoading(true); const res = await updateModuleTitle(editingModuleId, editingTitle.trim()); if (res.success) { toast.success("Título actualizado"); cancelEditingModuleTitle(); router.push(`/teacher/courses/${course.slug}/edit`); } else { toast.error(res.error ?? "Error al guardar"); } setLoading(false); }; // 4. REORDER MODULES (optimistic) const handleReorderModule = async (moduleIndex: number, direction: "up" | "down") => { const swapWith = direction === "up" ? moduleIndex - 1 : moduleIndex + 1; if (swapWith < 0 || swapWith >= optimisticModules.length) return; const next = [...optimisticModules]; [next[moduleIndex], next[swapWith]] = [next[swapWith], next[moduleIndex]]; setOptimisticModules(next); const res = await reorderModules(optimisticModules[moduleIndex].id, direction); if (res.success) { router.refresh(); } else { setOptimisticModules(course.modules); toast.error(res.error ?? "Error al reordenar"); } }; // 5. REORDER LESSONS (optimistic) const handleReorderLesson = async ( moduleIndex: number, lessonIndex: number, direction: "up" | "down" ) => { const lessons = optimisticModules[moduleIndex].lessons; const swapWith = direction === "up" ? lessonIndex - 1 : lessonIndex + 1; if (swapWith < 0 || swapWith >= lessons.length) return; const nextModules = [...optimisticModules]; const nextLessons = [...lessons]; [nextLessons[lessonIndex], nextLessons[swapWith]] = [ nextLessons[swapWith], nextLessons[lessonIndex], ]; nextModules[moduleIndex] = { ...nextModules[moduleIndex], lessons: nextLessons }; setOptimisticModules(nextModules); const res = await reorderLessons(lessons[lessonIndex].id, direction); if (res.success) { router.refresh(); } else { setOptimisticModules(course.modules); toast.error(res.error ?? "Error al reordenar"); } }; return (
{/* Header */}

Editar Curso

👁️ Ver Vista Previa
{/* LEFT COLUMN: Main Info */}
{/* Title */}
{/* Description */}