import { useCallback, useEffect, useState } from "react"; import { ArrowLeft, Plus, Calendar, Layers, Pencil, Trash2, X, } from "lucide-react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { toast } from "sonner"; import { Button } from "../../components/ui/button"; import { Card } from "../../components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Textarea } from "../../components/ui/textarea"; import { cn } from "../../lib/utils"; import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg"; import alertSrc from "../../assets/Alert.svg"; import { deleteTopLevelCourseModule, getProgramCourses, getTopLevelCourseModules, updateTopLevelCourseModule, } from "../../api/courses.api"; import type { ProgramCourseListItem, TopLevelCourseModuleItem, } from "../../types/course.types"; import { AddModuleModal } from "./components/AddModuleModal"; import { ModuleIconUploadField } from "./components/ModuleIconUploadField"; const MODULE_CARD_GRADIENT = "from-[#8E44AD] to-[#C39BD3]" as const; function isLikelyImageUrl(src: string): boolean { const t = src.trim(); return ( t.startsWith("http://") || t.startsWith("https://") || t.startsWith("/") || t.startsWith("data:") ); } /** Default purple gradient with optional cover image; gradient stays if URL missing or image errors. */ function ModuleCardTopMedia({ iconSrc }: { iconSrc: string }) { const [coverFailed, setCoverFailed] = useState(false); const tryCover = Boolean(iconSrc.trim()) && isLikelyImageUrl(iconSrc) && !coverFailed; return (
{tryCover ? ( setCoverFailed(true)} /> ) : null}
); } /** Circular module icon: image when load succeeds, otherwise default Layers icon. */ function ModuleIconCircle({ iconSrc, index, }: { iconSrc: string; index: number; }) { const [imgFailed, setImgFailed] = useState(false); const showImg = Boolean(iconSrc.trim()) && isLikelyImageUrl(iconSrc) && !imgFailed; return (
{showImg ? ( setImgFailed(true)} /> ) : ( )}
); } export function CourseDetailPage() { const navigate = useNavigate(); const { level: programIdParam, courseId: courseIdParam } = useParams<{ level: string; courseId: string; }>(); const programId = Number(programIdParam); const courseIdNum = Number(courseIdParam); const [course, setCourse] = useState(null); const [modules, setModules] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isAddModuleOpen, setIsAddModuleOpen] = useState(false); const [editingModule, setEditingModule] = useState(null); const [editModuleName, setEditModuleName] = useState(""); const [editModuleDescription, setEditModuleDescription] = useState(""); const [editModuleIcon, setEditModuleIcon] = useState(""); const [editModuleIconUploadBusy, setEditModuleIconUploadBusy] = useState(false); const [savingModuleEdit, setSavingModuleEdit] = useState(false); const [deletingModule, setDeletingModule] = useState(null); const [deletingModuleInFlight, setDeletingModuleInFlight] = useState(false); const openEditModule = (module: TopLevelCourseModuleItem) => { setEditingModule(module); setEditModuleName(module.name ?? ""); setEditModuleDescription(module.description ?? ""); setEditModuleIcon(module.icon?.trim() ?? ""); setEditModuleIconUploadBusy(false); }; const closeEditModule = () => { if (savingModuleEdit || editModuleIconUploadBusy) return; setEditingModule(null); setEditModuleIconUploadBusy(false); }; const loadPage = useCallback(async () => { if (!Number.isFinite(programId) || programId < 1) { setError("Invalid program"); setCourse(null); setModules([]); setLoading(false); return; } if (!Number.isFinite(courseIdNum) || courseIdNum < 1) { setError("Invalid course"); setCourse(null); setModules([]); setLoading(false); return; } setLoading(true); setError(null); try { const [courseOutcome, modulesOutcome] = await Promise.allSettled([ getProgramCourses(programId, { limit: 200, offset: 0 }), getTopLevelCourseModules(courseIdNum, { limit: 100, offset: 0 }), ]); if (courseOutcome.status === "fulfilled") { const raw = courseOutcome.value.data?.data?.courses; const list = Array.isArray(raw) ? raw : []; const found = list.find((c) => c.id === courseIdNum) ?? null; setCourse(found); if (!found) { setError("Course not found in this program"); } } else { console.error(courseOutcome.reason); setCourse(null); setError("Failed to load course"); } if (modulesOutcome.status === "fulfilled") { const raw = modulesOutcome.value.data?.data?.modules; const list = Array.isArray(raw) ? raw : []; const sorted = [...list].sort( (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0), ); setModules(sorted); } else { console.error(modulesOutcome.reason); setModules([]); toast.error("Could not load modules", { description: "Check your connection or try again.", }); } } catch (e) { console.error(e); setError("Failed to load course"); setCourse(null); setModules([]); toast.error("Could not load course", { description: "Check your connection or try again.", }); } finally { setLoading(false); } }, [programId, courseIdNum]); useEffect(() => { void loadPage(); }, [loadPage]); const handleSaveModuleEdit = async () => { if (!editingModule) return; const name = editModuleName.trim(); if (!name) { toast.error("Module name is required"); return; } setSavingModuleEdit(true); try { await updateTopLevelCourseModule(editingModule.id, { name, description: editModuleDescription.trim(), icon: editModuleIcon.trim(), }); toast.success("Module updated"); setEditModuleIconUploadBusy(false); setEditingModule(null); await loadPage(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to update module"; toast.error(msg); } finally { setSavingModuleEdit(false); } }; const handleConfirmDeleteModule = async () => { if (!deletingModule) return; setDeletingModuleInFlight(true); try { await deleteTopLevelCourseModule(deletingModule.id); toast.success("Module deleted"); setDeletingModule(null); await loadPage(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to delete module"; toast.error(msg); } finally { setDeletingModuleInFlight(false); } }; const displayTitle = course?.name?.trim() || courseIdParam || "Course"; const displayDescription = course?.description?.trim() || (!loading && !course ? "This course could not be loaded." : !course?.description?.trim() && course ? "—" : ""); return (
{/* Header Navigation */}
Back to Courses
{loading ? (
) : error && !course ? (

{error}

) : ( <> {/* Hero Section */}

{displayTitle}

{displayDescription}