import { useCallback, useEffect, useRef, useState } from "react"; import { ArrowLeft, Plus, FileText, Pencil, Trash2 } from "lucide-react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { Button } from "../../components/ui/button"; import { cn } from "../../lib/utils"; import { Card } from "../../components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogClose, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Textarea } from "../../components/ui/textarea"; import uploadIcon from "../../assets/icons/upload.png"; import { toast } from "sonner"; import { ResolvedImage } from "../../components/media/ResolvedImage"; import { VideoCard } from "./components/VideoCard"; import { createExamPrepModuleLesson, updateExamPrepModuleLesson, deleteExamPrepModuleLesson, getExamPrepModuleLessons, } from "../../api/courses.api"; import { uploadImageFile, uploadVideoFile } from "../../api/files.api"; const MOCK_PRACTICES = [ { id: "p1", title: "1.1 Conversation Practice", duration: "08:45", status: "Published", thumbnailColor: "bg-[#E0F2FE]", }, { id: "p2", title: "1.2 Roleplay Scenario", duration: "08:45", status: "Draft", thumbnailColor: "bg-[#F0FDF4]", }, ]; export function CourseModuleDetailPage() { const navigate = useNavigate(); const { programType, courseId, unitId, moduleId } = useParams<{ programType: string; courseId: string; unitId: string; moduleId: string; }>(); const parsedModuleId = Number(moduleId); const [activeTab, setActiveTab] = useState<"video" | "practice">("video"); const [lessonsLoading, setLessonsLoading] = useState(false); const [lessons, setLessons] = useState< Array<{ id: number; title: string; videoUrl: string; description: string; thumbnail: string; sortOrder: number; gradient: string; }> >([]); const [createLessonOpen, setCreateLessonOpen] = useState(false); const [createTitle, setCreateTitle] = useState(""); const [createVideoUrl, setCreateVideoUrl] = useState(""); const [createThumbnail, setCreateThumbnail] = useState(""); const [createDescription, setCreateDescription] = useState(""); const [creatingLesson, setCreatingLesson] = useState(false); const [uploadingThumbnail, setUploadingThumbnail] = useState(false); const [uploadingVideo, setUploadingVideo] = useState(false); const createThumbnailFileInputRef = useRef(null); const createVideoFileInputRef = useRef(null); const [editingLessonId, setEditingLessonId] = useState(null); const [editTitle, setEditTitle] = useState(""); const [editVideoUrl, setEditVideoUrl] = useState(""); const [editThumbnail, setEditThumbnail] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editSortOrder, setEditSortOrder] = useState("1"); const [savingEdit, setSavingEdit] = useState(false); const [uploadingEditThumbnail, setUploadingEditThumbnail] = useState(false); const [uploadingEditVideo, setUploadingEditVideo] = useState(false); const editThumbnailFileInputRef = useRef(null); const editVideoFileInputRef = useRef(null); const [deletingLessonId, setDeletingLessonId] = useState(null); const [deletingLesson, setDeletingLesson] = useState(false); const moduleTitle = "Module 1: Basic Phrases"; const moduleDescription = "Learn essential phrases for daily conversations."; const isHttpUrl = (value: string) => value.startsWith("http://") || value.startsWith("https://"); const isMinioUrl = (value: string) => { try { const url = new URL(value); return url.host === "s3.yimaruacademy.com"; } catch { return false; } }; const resolveThumbnailToMinioUrl = async (rawValue: string) => { const trimmed = rawValue.trim(); if (!trimmed) return ""; if (!isHttpUrl(trimmed) || isMinioUrl(trimmed)) return trimmed; const uploaded = await uploadImageFile(trimmed); const uploadedUrl = uploaded.data?.data?.url?.trim(); if (!uploadedUrl) throw new Error("Failed to upload thumbnail URL to MinIO"); return uploadedUrl; }; const loadLessons = useCallback(async () => { if (!Number.isFinite(parsedModuleId) || parsedModuleId < 1) { setLessons([]); return; } setLessonsLoading(true); try { const response = await getExamPrepModuleLessons(parsedModuleId, { limit: 20, offset: 0, }); const rows = response.data?.data?.lessons; const list = Array.isArray(rows) ? rows : []; setLessons( list.map((row, index) => ({ id: Number(row.id), title: row.title?.trim() || `Lesson ${row.id}`, videoUrl: row.video_url?.trim() || "", description: row.description?.trim() || "—", thumbnail: row.thumbnail?.trim() || "", sortOrder: Number(row.sort_order ?? 0), gradient: index % 3 === 1 ? "linear-gradient(135deg, rgba(79, 70, 229, 0.35) 0%, rgba(79, 70, 229, 0.6) 100%)" : index % 3 === 2 ? "linear-gradient(135deg, rgba(124, 58, 237, 0.35) 0%, rgba(124, 58, 237, 0.6) 100%)" : "linear-gradient(135deg, rgba(158, 40, 145, 0.35) 0%, rgba(158, 40, 145, 0.6) 100%)", })), ); } catch (error) { console.error(error); toast.error("Failed to load lessons"); setLessons([]); } finally { setLessonsLoading(false); } }, [parsedModuleId]); useEffect(() => { if (activeTab !== "video") return; void loadLessons(); }, [activeTab, loadLessons]); const clearCreateLessonForm = () => { setCreateTitle(""); setCreateVideoUrl(""); setCreateThumbnail(""); setCreateDescription(""); if (createThumbnailFileInputRef.current) { createThumbnailFileInputRef.current.value = ""; } if (createVideoFileInputRef.current) { createVideoFileInputRef.current.value = ""; } }; const handleCreateLessonVideoFile = async ( event: React.ChangeEvent, ) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; if (!file.type.startsWith("video/")) { toast.error("Please choose a video file"); return; } setUploadingVideo(true); try { const res = await uploadVideoFile(file, { title: createTitle.trim() || "Lesson video", description: createDescription.trim() || undefined, }); const finalUrl = res.data?.data?.url?.trim() || res.data?.data?.embed_url?.trim() || ""; if (!finalUrl) throw new Error("Upload did not return a video URL"); setCreateVideoUrl(finalUrl); toast.success("Video uploaded"); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload video"; toast.error(message); } finally { setUploadingVideo(false); } }; const handleCreateLessonThumbnailFile = async ( event: React.ChangeEvent, ) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; if (!file.type.startsWith("image/")) { toast.error("Please choose an image file"); return; } setUploadingThumbnail(true); try { const res = await uploadImageFile(file); const url = res.data?.data?.url?.trim(); if (!url) throw new Error("Upload did not return a file URL"); setCreateThumbnail(url); toast.success("Thumbnail uploaded"); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload thumbnail"; toast.error(message); } finally { setUploadingThumbnail(false); } }; const autoUploadCreateThumbnailUrl = async (rawValue: string) => { const trimmed = rawValue.trim(); if (!trimmed || !isHttpUrl(trimmed) || isMinioUrl(trimmed)) return; setUploadingThumbnail(true); try { const minioUrl = await resolveThumbnailToMinioUrl(trimmed); if (minioUrl && minioUrl !== trimmed) { setCreateThumbnail(minioUrl); toast.success("Thumbnail uploaded to MinIO"); } } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload URL to MinIO"; toast.error(message); } finally { setUploadingThumbnail(false); } }; const handleCreateLesson = async () => { if (!Number.isFinite(parsedModuleId) || parsedModuleId < 1) { toast.error("Invalid module"); return; } const title = createTitle.trim(); const videoUrl = createVideoUrl.trim(); if (!title) { toast.error("Lesson title is required"); return; } if (!videoUrl) { toast.error("Video URL is required"); return; } setCreatingLesson(true); try { const minioThumbnail = await resolveThumbnailToMinioUrl(createThumbnail); await createExamPrepModuleLesson(parsedModuleId, { title, video_url: videoUrl, thumbnail: minioThumbnail || null, description: createDescription.trim() || null, }); await loadLessons(); toast.success("Lesson created"); clearCreateLessonForm(); setCreateLessonOpen(false); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to create lesson"; toast.error(message); } finally { setCreatingLesson(false); } }; const openEditLesson = (lesson: (typeof lessons)[number]) => { setEditingLessonId(lesson.id); setEditTitle(lesson.title ?? ""); setEditVideoUrl(lesson.videoUrl ?? ""); setEditThumbnail(lesson.thumbnail ?? ""); setEditDescription(lesson.description ?? ""); setEditSortOrder(String(lesson.sortOrder ?? 1)); }; const closeEditLesson = () => { if (savingEdit || uploadingEditThumbnail || uploadingEditVideo) return; setEditingLessonId(null); setEditTitle(""); setEditVideoUrl(""); setEditThumbnail(""); setEditDescription(""); setEditSortOrder("1"); }; const handleEditLessonVideoFile = async ( event: React.ChangeEvent, ) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; if (!file.type.startsWith("video/")) { toast.error("Please choose a video file"); return; } setUploadingEditVideo(true); try { const res = await uploadVideoFile(file, { title: editTitle.trim() || "Lesson video", description: editDescription.trim() || undefined, }); const finalUrl = res.data?.data?.url?.trim() || res.data?.data?.embed_url?.trim() || ""; if (!finalUrl) throw new Error("Upload did not return a video URL"); setEditVideoUrl(finalUrl); toast.success("Video uploaded"); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload video"; toast.error(message); } finally { setUploadingEditVideo(false); } }; const handleEditLessonThumbnailFile = async ( event: React.ChangeEvent, ) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; if (!file.type.startsWith("image/")) { toast.error("Please choose an image file"); return; } setUploadingEditThumbnail(true); try { const res = await uploadImageFile(file); const url = res.data?.data?.url?.trim(); if (!url) throw new Error("Upload did not return a file URL"); setEditThumbnail(url); toast.success("Thumbnail uploaded"); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload thumbnail"; toast.error(message); } finally { setUploadingEditThumbnail(false); } }; const autoUploadEditThumbnailUrl = async (rawValue: string) => { const trimmed = rawValue.trim(); if (!trimmed || !isHttpUrl(trimmed) || isMinioUrl(trimmed)) return; setUploadingEditThumbnail(true); try { const minioUrl = await resolveThumbnailToMinioUrl(trimmed); if (minioUrl && minioUrl !== trimmed) { setEditThumbnail(minioUrl); toast.success("Thumbnail uploaded to MinIO"); } } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload URL to MinIO"; toast.error(message); } finally { setUploadingEditThumbnail(false); } }; const handleSaveEditLesson = async () => { if (!editingLessonId) return; const title = editTitle.trim(); if (!title) { toast.error("Lesson title is required"); return; } const sortOrderNum = Number(editSortOrder); if (!Number.isFinite(sortOrderNum) || sortOrderNum < 0) { toast.error("Sort order must be a valid number"); return; } setSavingEdit(true); try { const minioThumbnail = await resolveThumbnailToMinioUrl(editThumbnail); await updateExamPrepModuleLesson(editingLessonId, { title, video_url: editVideoUrl.trim() || null, thumbnail: minioThumbnail || null, description: editDescription.trim() || null, sort_order: sortOrderNum, }); await loadLessons(); toast.success("Lesson updated"); closeEditLesson(); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to update lesson"; toast.error(message); } finally { setSavingEdit(false); } }; const handleDeleteLesson = async () => { if (!deletingLessonId) return; setDeletingLesson(true); try { await deleteExamPrepModuleLesson(deletingLessonId); await loadLessons(); toast.success("Lesson deleted"); setDeletingLessonId(null); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to delete lesson"; toast.error(message); } finally { setDeletingLesson(false); } }; return (
{/* Navigation */} Back to Modules {/* Header section */}

{moduleTitle}

{moduleDescription}

{ if (!open && (creatingLesson || uploadingThumbnail || uploadingVideo)) return; setCreateLessonOpen(open); }} >
Create Lesson
setCreateTitle(e.target.value)} placeholder="e.g. Intro lesson" className="h-12 border-grayScale-400 rounded-[8px] px-4" disabled={creatingLesson || uploadingThumbnail || uploadingVideo} />
void handleCreateLessonVideoFile(e)} disabled={creatingLesson || uploadingThumbnail || uploadingVideo} /> setCreateVideoUrl(e.target.value)} placeholder="https://example.com/video" className="h-12 border-grayScale-400 rounded-[8px] px-4" disabled={creatingLesson || uploadingThumbnail || uploadingVideo} />