import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useParams, useNavigate } from "react-router-dom"; import { ArrowLeft, Plus, MessageCircle, PlayCircle, ClipboardCheck, Pencil, Trash2, ArrowRight, X, } from "lucide-react"; import { Button } from "../../components/ui/button"; import { Card } from "../../components/ui/card"; import { Textarea } from "../../components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogClose, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import uploadIcon from "../../assets/icons/upload.png"; import { toast } from "sonner"; import { ResolvedImage } from "../../components/media/ResolvedImage"; import { createExamPrepUnitModule, getExamPrepUnitModules, updateExamPrepUnitModule, deleteExamPrepUnitModule, } from "../../api/courses.api"; import { uploadImageFile } from "../../api/files.api"; export function UnitManagementPage() { const navigate = useNavigate(); const { programType, courseId, unitId } = useParams<{ programType: string; courseId: string; unitId: string; }>(); // Mock titles const unitTitles: Record = { unit1: "Greetings & Introductions", unit2: "Speaking", unit3: "Reading", }; const unitDisplayName = unitTitles[unitId || ""] || "Greetings & Introductions"; const parsedUnitId = Number(unitId); const [addModuleOpen, setAddModuleOpen] = useState(false); const [createName, setCreateName] = useState(""); const [createDescription, setCreateDescription] = useState(""); const [createThumbnail, setCreateThumbnail] = useState(""); const [createIcon, setCreateIcon] = useState(""); const [creating, setCreating] = useState(false); const [uploadingThumbnail, setUploadingThumbnail] = useState(false); const [uploadingIcon, setUploadingIcon] = useState(false); const createThumbnailFileInputRef = useRef(null); const createIconFileInputRef = useRef(null); const [modulesLoading, setModulesLoading] = useState(false); const [modules, setModules] = useState< Array<{ id: number; name: string; description: string; thumbnail: string; icon: string; sortOrder: number; lessons: number; practices: number; gradient: string; }> >([]); const [editingModuleId, setEditingModuleId] = useState(null); const [editName, setEditName] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editThumbnail, setEditThumbnail] = useState(""); const [editIcon, setEditIcon] = useState(""); const [editSortOrder, setEditSortOrder] = useState("1"); const [savingEdit, setSavingEdit] = useState(false); const [uploadingEditThumbnail, setUploadingEditThumbnail] = useState(false); const [uploadingEditIcon, setUploadingEditIcon] = useState(false); const editThumbnailFileInputRef = useRef(null); const editIconFileInputRef = useRef(null); const [deletingModuleId, setDeletingModuleId] = useState(null); const [deletingModule, setDeletingModule] = useState(false); 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 resolveToMinioUrl = 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 URL to MinIO"); return uploadedUrl; }; const loadModules = useCallback(async () => { if (!Number.isFinite(parsedUnitId) || parsedUnitId < 1) { setModules([]); return; } setModulesLoading(true); try { const response = await getExamPrepUnitModules(parsedUnitId, { limit: 20, offset: 0, }); const rows = response.data?.data?.modules; const list = Array.isArray(rows) ? rows : []; setModules( list.map((row, index) => ({ id: Number(row.id), name: row.name?.trim() || `Module ${row.id}`, description: row.description?.trim() || "—", thumbnail: row.thumbnail?.trim() || "", icon: row.icon?.trim() || "", sortOrder: Number(row.sort_order ?? 0), lessons: Number(row.lessons_count ?? row.videos_count ?? 0), practices: Number(row.practices_count ?? 0), gradient: index % 3 === 1 ? "linear-gradient(135deg, rgba(79, 70, 229, 0.4) 0%, rgba(79, 70, 229, 0.7) 100%)" : index % 3 === 2 ? "linear-gradient(135deg, rgba(124, 58, 237, 0.4) 0%, rgba(124, 58, 237, 0.7) 100%)" : "linear-gradient(135deg, rgba(158, 40, 145, 0.4) 0%, rgba(158, 40, 145, 0.7) 100%)", })), ); } catch (error) { console.error(error); toast.error("Failed to load modules"); setModules([]); } finally { setModulesLoading(false); } }, [parsedUnitId]); useEffect(() => { void loadModules(); }, [loadModules]); const clearCreateModuleForm = () => { setCreateName(""); setCreateDescription(""); setCreateThumbnail(""); setCreateIcon(""); if (createThumbnailFileInputRef.current) { createThumbnailFileInputRef.current.value = ""; } if (createIconFileInputRef.current) { createIconFileInputRef.current.value = ""; } }; const handleCreateImageFile = async ( event: React.ChangeEvent, target: "thumbnail" | "icon", ) => { 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; } if (target === "thumbnail") { setUploadingThumbnail(true); } else { setUploadingIcon(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"); if (target === "thumbnail") { setCreateThumbnail(url); } else { setCreateIcon(url); } toast.success(`${target === "thumbnail" ? "Thumbnail" : "Icon"} uploaded`); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload image"; toast.error(message); } finally { if (target === "thumbnail") { setUploadingThumbnail(false); } else { setUploadingIcon(false); } } }; const autoUploadCreateUrl = async ( value: string, target: "thumbnail" | "icon", ) => { const trimmed = value.trim(); if (!trimmed || !isHttpUrl(trimmed) || isMinioUrl(trimmed)) return; if (target === "thumbnail") { setUploadingThumbnail(true); } else { setUploadingIcon(true); } try { const minioUrl = await resolveToMinioUrl(trimmed); if (target === "thumbnail") { setCreateThumbnail(minioUrl); } else { setCreateIcon(minioUrl); } if (minioUrl !== trimmed) { toast.success(`${target === "thumbnail" ? "Thumbnail" : "Icon"} 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 { if (target === "thumbnail") { setUploadingThumbnail(false); } else { setUploadingIcon(false); } } }; const handleCreateModule = async () => { if (!Number.isFinite(parsedUnitId) || parsedUnitId < 1) { toast.error("Invalid unit"); return; } const name = createName.trim(); if (!name) { toast.error("Module name is required"); return; } setCreating(true); try { const minioThumbnail = await resolveToMinioUrl(createThumbnail); const minioIcon = await resolveToMinioUrl(createIcon); await createExamPrepUnitModule(parsedUnitId, { name, description: createDescription.trim() || null, thumbnail: minioThumbnail || null, icon: minioIcon || null, }); await loadModules(); toast.success("Module created"); clearCreateModuleForm(); setAddModuleOpen(false); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to create module"; toast.error(message); } finally { setCreating(false); } }; const openEditModule = (module: (typeof modules)[number]) => { setEditingModuleId(module.id); setEditName(module.name ?? ""); setEditDescription(module.description ?? ""); setEditThumbnail(module.thumbnail ?? ""); setEditIcon(module.icon ?? ""); setEditSortOrder(String(module.sortOrder ?? 1)); }; const closeEditModule = () => { if (savingEdit || uploadingEditThumbnail || uploadingEditIcon) return; setEditingModuleId(null); setEditName(""); setEditDescription(""); setEditThumbnail(""); setEditIcon(""); setEditSortOrder("1"); }; const handleEditImageFile = async ( event: React.ChangeEvent, target: "thumbnail" | "icon", ) => { 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; } if (target === "thumbnail") { setUploadingEditThumbnail(true); } else { setUploadingEditIcon(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"); if (target === "thumbnail") { setEditThumbnail(url); } else { setEditIcon(url); } toast.success(`${target === "thumbnail" ? "Thumbnail" : "Icon"} uploaded`); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload image"; toast.error(message); } finally { if (target === "thumbnail") { setUploadingEditThumbnail(false); } else { setUploadingEditIcon(false); } } }; const autoUploadEditUrl = async (value: string, target: "thumbnail" | "icon") => { const trimmed = value.trim(); if (!trimmed || !isHttpUrl(trimmed) || isMinioUrl(trimmed)) return; if (target === "thumbnail") { setUploadingEditThumbnail(true); } else { setUploadingEditIcon(true); } try { const minioUrl = await resolveToMinioUrl(trimmed); if (target === "thumbnail") { setEditThumbnail(minioUrl); } else { setEditIcon(minioUrl); } if (minioUrl !== trimmed) { toast.success(`${target === "thumbnail" ? "Thumbnail" : "Icon"} 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 { if (target === "thumbnail") { setUploadingEditThumbnail(false); } else { setUploadingEditIcon(false); } } }; const handleSaveEditModule = async () => { if (!editingModuleId) return; const name = editName.trim(); if (!name) { toast.error("Module name 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 resolveToMinioUrl(editThumbnail); const minioIcon = await resolveToMinioUrl(editIcon); await updateExamPrepUnitModule(editingModuleId, { name, description: editDescription.trim() || null, thumbnail: minioThumbnail || null, icon: minioIcon || null, sort_order: sortOrderNum, }); await loadModules(); toast.success("Module updated"); closeEditModule(); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to update module"; toast.error(message); } finally { setSavingEdit(false); } }; const handleDeleteModule = async () => { if (!deletingModuleId) return; setDeletingModule(true); try { await deleteExamPrepUnitModule(deletingModuleId); await loadModules(); toast.success("Module deleted"); setDeletingModuleId(null); } catch (error: unknown) { console.error(error); const message = (error as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to delete module"; toast.error(message); } finally { setDeletingModule(false); } }; return (
{/* Navigation */} Back to Courses {/* Header section */}

{unitDisplayName}

{ if (!open && (creating || uploadingThumbnail || uploadingIcon)) return; setAddModuleOpen(open); }} >
Create Modules Close
setCreateName(e.target.value)} placeholder="e.g. Present tense" className="h-12 border-grayScale-400 rounded-[8px] px-4 placeholder:text-grayScale-400 text-[15px] focus:ring-brand-500/20" disabled={creating || uploadingThumbnail || uploadingIcon} />