import { useCallback, useEffect, useState } from "react"; import { ArrowLeft, Video, Calendar, Mic, Layers, Edit2, Trash2, X, } from "lucide-react"; import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; import { toast } from "sonner"; import { deleteTopLevelModuleLesson, getModuleLessons, getTopLevelCourseModules, updateTopLevelModuleLesson, } from "../../api/courses.api"; import type { TopLevelModuleLessonItem } from "../../types/course.types"; import { Button } from "../../components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Textarea } from "../../components/ui/textarea"; import { resolveThumbnailForPreview } from "../../lib/videoPreview"; import { cn } from "../../lib/utils"; import { LessonMediaUploadField } from "./components/LessonMediaUploadField"; import { VideoCard } from "./components/VideoCard"; const LESSON_THUMB_GRADIENTS = [ "from-[#CBD5E1] to-[#94A3B8]", "from-[#DBEAFE] to-[#93C5FD]", "from-[#FEF3C7] to-[#FCD34D]", "from-[#FCE7F3] to-[#F9A8D4]", ] as const; const MOCK_PRACTICES = [ { id: "p1", title: "Describe a Photo", level: "IELTS", variations: 12, status: "Draft", }, { id: "p2", title: "Describe a Photo", level: "IELTS", variations: 12, status: "Draft", }, { id: "p3", title: "Describe a Photo", level: "IELTS", variations: 12, status: "Draft", }, { id: "p4", title: "Describe a Photo", level: "IELTS", variations: 12, status: "Draft", }, ]; type ModuleDetailState = { moduleName?: string; moduleDescription?: string; }; export function ModuleDetailPage() { const navigate = useNavigate(); const location = useLocation(); const navState = location.state as ModuleDetailState | null; const { level, courseId, moduleId } = useParams<{ level: string; courseId: string; moduleId: string; }>(); const [activeTab, setActiveTab] = useState<"video" | "practice">("video"); const [activeFilter, setActiveFilter] = useState("Draft"); const [lessons, setLessons] = useState([]); const [lessonsLoading, setLessonsLoading] = useState(true); const [lessonsLoadError, setLessonsLoadError] = useState(null); const [editingLesson, setEditingLesson] = useState(null); const [editLessonTitle, setEditLessonTitle] = useState(""); const [editLessonVideoUrl, setEditLessonVideoUrl] = useState(""); const [editLessonThumbnail, setEditLessonThumbnail] = useState(""); const [editLessonDescription, setEditLessonDescription] = useState(""); const [savingLessonEdit, setSavingLessonEdit] = useState(false); const [thumbUploadBusy, setThumbUploadBusy] = useState(false); const [videoUploadBusy, setVideoUploadBusy] = useState(false); const lessonMediaUploadBusy = thumbUploadBusy || videoUploadBusy; const [deletingLesson, setDeletingLesson] = useState(null); const [deletingLessonInFlight, setDeletingLessonInFlight] = useState(false); const [practices] = useState(MOCK_PRACTICES); const [loadedModuleName, setLoadedModuleName] = useState(null); const [loadedModuleDescription, setLoadedModuleDescription] = useState< string | null >(null); const [moduleListResolved, setModuleListResolved] = useState( Boolean(navState?.moduleName?.trim()), ); const moduleTitleFallback = moduleId ?.split("-") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" ") || "Module"; const displayModuleName = navState?.moduleName?.trim() || loadedModuleName || moduleTitleFallback; const hasNavName = Boolean(navState?.moduleName?.trim()); const displayModuleDescription = (() => { if (hasNavName) { return navState?.moduleDescription?.trim() || "—"; } if (!moduleListResolved) { return "Loading…"; } if (loadedModuleDescription !== null) { return loadedModuleDescription.trim() || "—"; } return "—"; })(); useEffect(() => { if (navState?.moduleName?.trim()) { return; } const id = Number(moduleId); const cid = Number(courseId); if (!Number.isFinite(id) || id < 1 || !Number.isFinite(cid) || cid < 1) { setModuleListResolved(true); return; } let cancelled = false; (async () => { try { const res = await getTopLevelCourseModules(cid, { limit: 100, offset: 0 }); if (cancelled) return; const list = res.data?.data?.modules; if (Array.isArray(list)) { const m = list.find((mod) => mod.id === id); if (m) { setLoadedModuleName(m.name); setLoadedModuleDescription(m.description ?? ""); } else { setLoadedModuleName(null); setLoadedModuleDescription(""); } } else { setLoadedModuleName(null); setLoadedModuleDescription(null); } } catch { if (!cancelled) { setLoadedModuleName(null); setLoadedModuleDescription(null); } } finally { if (!cancelled) { setModuleListResolved(true); } } })(); return () => { cancelled = true; }; }, [navState?.moduleName, courseId, moduleId]); const loadModuleLessons = useCallback( async (options?: { showPageLoading?: boolean }) => { const showPageLoading = options?.showPageLoading ?? true; const mid = Number(moduleId); if (!Number.isFinite(mid) || mid < 1) { setLessons([]); setLessonsLoadError(null); setLessonsLoading(false); return; } if (showPageLoading) { setLessonsLoading(true); setLessonsLoadError(null); } try { const res = await getModuleLessons(mid, { limit: 100, offset: 0 }); const list = res.data?.data?.lessons; if (Array.isArray(list)) { setLessons( [...list].sort( (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0), ), ); } else { setLessons([]); } if (showPageLoading) { setLessonsLoadError(null); } } catch { if (showPageLoading) { setLessons([]); setLessonsLoadError("Failed to load lessons. Please try again."); } else { toast.error("Failed to refresh lessons"); } } finally { if (showPageLoading) { setLessonsLoading(false); } } }, [moduleId], ); useEffect(() => { void loadModuleLessons({ showPageLoading: true }); }, [loadModuleLessons]); const openEditLesson = (lesson: TopLevelModuleLessonItem) => { setEditingLesson(lesson); setEditLessonTitle(lesson.title ?? ""); setEditLessonVideoUrl(lesson.video_url ?? ""); setEditLessonThumbnail(lesson.thumbnail ?? ""); setEditLessonDescription(lesson.description ?? ""); }; const closeEditLesson = () => { if (savingLessonEdit || lessonMediaUploadBusy) return; setEditingLesson(null); }; const handleSaveLessonEdit = async () => { if (!editingLesson) return; const title = editLessonTitle.trim(); if (!title) { toast.error("Title is required"); return; } setSavingLessonEdit(true); try { await updateTopLevelModuleLesson(editingLesson.id, { title, video_url: editLessonVideoUrl.trim(), thumbnail: editLessonThumbnail.trim(), description: editLessonDescription.trim(), }); toast.success("Lesson updated"); setEditingLesson(null); await loadModuleLessons({ showPageLoading: false }); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to update lesson"; toast.error(msg); } finally { setSavingLessonEdit(false); } }; const handleConfirmDeleteLesson = async () => { if (!deletingLesson) return; setDeletingLessonInFlight(true); try { await deleteTopLevelModuleLesson(deletingLesson.id); toast.success("Lesson deleted"); setDeletingLesson(null); await loadModuleLessons({ showPageLoading: false }); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to delete lesson"; toast.error(msg); } finally { setDeletingLessonInFlight(false); } }; return (
{/* Header Navigation */}
Back to Modules
{/* Hero Section */}

{displayModuleName}

{displayModuleDescription}

{/* Tabs */}
{/* Content */}
{activeTab === "video" ? ( lessonsLoading ? (
Loading lessons…
) : lessonsLoadError ? (
{lessonsLoadError}
) : lessons.length > 0 ? (
{lessons.map((lesson, i) => ( openEditLesson(lesson)} onDelete={() => setDeletingLesson(lesson)} /> ))}
) : (

No lessons in this module yet

Lessons are a great way to engage students. Add your first lesson to get started.

) ) : (
{/* Practice Tab Filter Bar */}
STATUS:
{["All", "Published", "Draft", "Archived"].map((label) => ( ))}
{/* Practice Cards Grid */}
{practices.map((practice) => ( ))}
)}
{ if (!open && (savingLessonEdit || lessonMediaUploadBusy)) return; if (!open) closeEditLesson(); }} > Edit lesson Update details. Video and thumbnail files use{" "} POST /files/upload ; the form is saved with{" "} PUT /lessons/:id .
setEditLessonTitle(e.target.value)} disabled={savingLessonEdit} />