import { useEffect, useState, useRef } from "react"; import { Link, useParams, useNavigate } from "react-router-dom"; import { ArrowLeft, ToggleLeft, ToggleRight, MoreVertical, X, Trash2, AlertCircle, Edit, Link2, Plus, LayoutGrid, GitBranch, ChevronDown, Lock, ArrowRight, } from "lucide-react"; import practiceSrc from "../../assets/Practice.svg"; import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg"; import { Card, CardContent } from "../../components/ui/card"; import alertSrc from "../../assets/Alert.svg"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { getSubModulesByCourse, getCoursesByCategory, getCourseCategories, createSubModule, updateSubModule, updateSubModuleStatus, deleteSubModule, getSubModulePrerequisites, addSubModulePrerequisite, removeSubModulePrerequisite, } from "../../api/courses.api"; import { uploadImageFile } from "../../api/files.api"; import { Input } from "../../components/ui/input"; import { FileUpload } from "../../components/ui/file-upload"; import type { SubCourse, Course, CourseCategory, SubCoursePrerequisite, } from "../../types/course.types"; import { SpinnerIcon } from "../../components/ui/spinner-icon"; import { toast } from "sonner"; export function SubModulesPage() { const { categoryId, courseId } = useParams<{ categoryId: string; courseId: string; }>(); const navigate = useNavigate(); const [subCourses, setSubCourses] = useState([]); const [course, setCourse] = useState(null); const [category, setCategory] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [openMenuId, setOpenMenuId] = useState(null); const [togglingId, setTogglingId] = useState(null); const [showDeleteModal, setShowDeleteModal] = useState(false); const [subCourseToDelete, setSubCourseToDelete] = useState( null, ); const [deleting, setDeleting] = useState(false); const menuRef = useRef(null); const [showAddModal, setShowAddModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [subCourseToEdit, setSubCourseToEdit] = useState( null, ); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [level, setLevel] = useState("BEGINNER"); const [subLevel, setSubLevel] = useState(""); const [thumbnailUrl, setThumbnailUrl] = useState(""); const [thumbnailFile, setThumbnailFile] = useState(null); const [displayOrder, setDisplayOrder] = useState("1"); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); // View mode const [viewMode, setViewMode] = useState<"grid" | "flow">("grid"); // All prerequisites map: subCourseId -> prerequisites[] const [allPrereqMap, setAllPrereqMap] = useState< Record >({}); const [allPrereqLoading, setAllPrereqLoading] = useState(false); // Prerequisites state const [showPrereqModal, setShowPrereqModal] = useState(false); const [prereqSubCourse, setPrereqSubCourse] = useState( null, ); const [prerequisites, setPrerequisites] = useState( [], ); const [prereqLoading, setPrereqLoading] = useState(false); const [prereqAdding, setPrereqAdding] = useState(false); const [prereqRemoving, setPrereqRemoving] = useState(null); const [selectedPrereqId, setSelectedPrereqId] = useState(0); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setOpenMenuId(null); } }; if (openMenuId !== null) { document.addEventListener("mousedown", handleClickOutside); } return () => document.removeEventListener("mousedown", handleClickOutside); }, [openMenuId]); const fetchSubCourses = async () => { if (!courseId) return; try { const subCoursesRes = await getSubModulesByCourse(Number(courseId)); setSubCourses(subCoursesRes.data.data.sub_courses ?? []); } catch (err) { console.error("Failed to fetch sub-modules:", err); } }; const fetchAllPrerequisites = async (scs: SubCourse[]) => { if (scs.length === 0) return; setAllPrereqLoading(true); try { const results = await Promise.all( scs.map((sc) => getSubModulePrerequisites(sc.id).then((res) => ({ id: sc.id, data: res.data.data ?? [], })), ), ); const map: Record = {}; for (const r of results) { map[r.id] = r.data; } setAllPrereqMap(map); } catch (err) { console.error("Failed to fetch all prerequisites:", err); } finally { setAllPrereqLoading(false); } }; useEffect(() => { const fetchData = async () => { if (!courseId || !categoryId) return; try { const [subCoursesRes, coursesRes, categoriesRes] = await Promise.all([ getSubModulesByCourse(Number(courseId)), getCoursesByCategory(Number(categoryId)), getCourseCategories(), ]); setSubCourses(subCoursesRes.data.data.sub_courses ?? []); const foundCourse = coursesRes.data.data.courses?.find( (c) => c.id === Number(courseId), ); setCourse(foundCourse ?? null); const foundCategory = categoriesRes.data.data.categories?.find( (c) => c.id === Number(categoryId), ); setCategory(foundCategory ?? null); } catch (err) { console.error("Failed to fetch sub-modules:", err); setError("Failed to load courses"); } finally { setLoading(false); } }; fetchData(); }, [courseId, categoryId]); useEffect(() => { if (subCourses.length > 0) { fetchAllPrerequisites(subCourses); } }, [subCourses]); const handleToggleStatus = async (subCourse: SubCourse) => { setTogglingId(subCourse.id); try { await updateSubModuleStatus(subCourse.id, { is_active: !subCourse.is_active, level: subCourse.level, title: subCourse.title, }); await fetchSubCourses(); } catch (err) { console.error("Failed to update sub-course status:", err); } finally { setTogglingId(null); } }; const handleDeleteClick = (subCourse: SubCourse) => { setSubCourseToDelete(subCourse); setShowDeleteModal(true); }; const handleConfirmDelete = async () => { if (!subCourseToDelete) return; setDeleting(true); try { await deleteSubModule(subCourseToDelete.id); setShowDeleteModal(false); setSubCourseToDelete(null); await fetchSubCourses(); } catch (err) { console.error("Failed to delete sub-course:", err); } finally { setDeleting(false); } }; const nextSubCourseDisplayOrder = () => subCourses.length === 0 ? 1 : Math.max(0, ...subCourses.map((s) => s.display_order ?? 0)) + 1; const handleAddSubCourse = () => { setTitle(""); setDescription(""); setLevel("BEGINNER"); setSubLevel(""); setThumbnailUrl(""); setThumbnailFile(null); setDisplayOrder(String(nextSubCourseDisplayOrder())); setSaveError(null); setShowAddModal(true); }; const handleSaveNewSubCourse = async () => { if (!courseId) return; setSaving(true); setSaveError(null); try { let thumbnail = thumbnailUrl.trim(); if (thumbnailFile) { const uploadRes = await uploadImageFile(thumbnailFile); const uploadedUrl = uploadRes.data?.data?.url?.trim(); if (!uploadedUrl) throw new Error("Missing uploaded image url"); thumbnail = uploadedUrl; } const parsedOrder = parseInt(displayOrder, 10); const display_order = Number.isFinite(parsedOrder) && parsedOrder >= 0 ? parsedOrder : nextSubCourseDisplayOrder(); await createSubModule({ course_id: Number(courseId), title: title.trim(), description: description.trim(), thumbnail, display_order, level: level.trim() || "BEGINNER", sub_level: subLevel.trim(), }); setShowAddModal(false); setTitle(""); setDescription(""); setLevel("BEGINNER"); setSubLevel(""); setThumbnailUrl(""); setThumbnailFile(null); setDisplayOrder("1"); await fetchSubCourses(); toast.success("Course created successfully"); } catch (err: unknown) { console.error("Failed to create sub-course:", err); const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? "Failed to create course"; setSaveError(msg); toast.error(msg); } finally { setSaving(false); } }; const handleEditClick = (subCourse: SubCourse) => { setSubCourseToEdit(subCourse); setTitle(subCourse.title); setDescription(subCourse.description); setLevel(subCourse.level); setSaveError(null); setShowEditModal(true); }; const handleSaveEditSubCourse = async () => { if (!subCourseToEdit) return; setSaving(true); setSaveError(null); try { await updateSubModule(subCourseToEdit.id, { title, description, level, }); setShowEditModal(false); setSubCourseToEdit(null); setTitle(""); setDescription(""); setLevel(""); await fetchSubCourses(); } catch (err) { console.error("Failed to update sub-course:", err); setSaveError("Failed to update course"); } finally { setSaving(false); } }; const handleSubModuleClick = (subModuleId: number) => { navigate( `/content/category/${categoryId}/courses/${courseId}/sub-modules/${subModuleId}`, ); }; const handlePrereqClick = async (subCourse: SubCourse) => { setPrereqSubCourse(subCourse); setShowPrereqModal(true); setPrereqLoading(true); setSelectedPrereqId(0); try { const res = await getSubModulePrerequisites(subCourse.id); setPrerequisites(res.data.data ?? []); } catch (err) { console.error("Failed to fetch prerequisites:", err); setPrerequisites([]); } finally { setPrereqLoading(false); } }; const handleAddPrerequisite = async () => { if (!prereqSubCourse || !selectedPrereqId) return; setPrereqAdding(true); try { await addSubModulePrerequisite(prereqSubCourse.id, { prerequisite_sub_course_id: selectedPrereqId, }); const res = await getSubModulePrerequisites(prereqSubCourse.id); setPrerequisites(res.data.data ?? []); setSelectedPrereqId(0); } catch (err) { console.error("Failed to add prerequisite:", err); } finally { setPrereqAdding(false); } }; const handleRemovePrerequisite = async (prereqId: number) => { if (!prereqSubCourse) return; setPrereqRemoving(prereqId); try { await removeSubModulePrerequisite(prereqSubCourse.id, prereqId); const res = await getSubModulePrerequisites(prereqSubCourse.id); setPrerequisites(res.data.data ?? []); } catch (err) { console.error("Failed to remove prerequisite:", err); } finally { setPrereqRemoving(null); } }; // Build flow layers using topological sort const flowLayers = (() => { if (subCourses.length === 0) return []; // Find sub-modules with no prerequisites (roots) const hasPrereqs = new Set(); const isPrereqOf = new Map(); // prereqId -> [subCourseIds that depend on it] for (const sc of subCourses) { const prereqs = allPrereqMap[sc.id] ?? []; if (prereqs.length > 0) { hasPrereqs.add(sc.id); } for (const p of prereqs) { const dependents = isPrereqOf.get(p.prerequisite_sub_course_id) ?? []; dependents.push(sc.id); isPrereqOf.set(p.prerequisite_sub_course_id, dependents); } } // BFS-based layering const layers: SubCourse[][] = []; const placed = new Set(); // Layer 0: no prerequisites const roots = subCourses.filter((sc) => !hasPrereqs.has(sc.id)); if (roots.length > 0) { layers.push(roots); roots.forEach((sc) => placed.add(sc.id)); } // Subsequent layers: all prereqs already placed let maxIterations = subCourses.length; while (placed.size < subCourses.length && maxIterations-- > 0) { const nextLayer = subCourses.filter((sc) => { if (placed.has(sc.id)) return false; const prereqs = allPrereqMap[sc.id] ?? []; return prereqs.every((p) => placed.has(p.prerequisite_sub_course_id)); }); if (nextLayer.length === 0) { // Remaining have circular deps or missing prereqs — just add them const remaining = subCourses.filter((sc) => !placed.has(sc.id)); if (remaining.length > 0) layers.push(remaining); break; } layers.push(nextLayer); nextLayer.forEach((sc) => placed.add(sc.id)); } return layers; })(); const availablePrerequisites = subCourses.filter( (sc) => prereqSubCourse && sc.id !== prereqSubCourse.id && !prerequisites.some((p) => p.prerequisite_sub_course_id === sc.id), ); if (loading) { return (
); } if (error) { return (

{error}

); } return (
{/* Header */}
{category?.name} {course?.title}

Courses

{subCourses.length} course{subCourses.length !== 1 ? "s" : ""}{" "} available

{subCourses.length > 0 && (
)}
{/* Sub-course grid or empty state */} {subCourses.length === 0 ? (

No courses yet

Get started by adding your first course to this sub-category

) : (
{subCourses.map((subCourse, index) => { const gradients = [ "bg-gradient-to-br from-blue-100 to-blue-200", "bg-gradient-to-br from-purple-100 to-purple-200", "bg-gradient-to-br from-green-100 to-green-200", "bg-gradient-to-br from-yellow-100 to-yellow-200", ]; return ( handleSubModuleClick(subCourse.id)} > {/* Thumbnail with level badge */}
{subCourse.thumbnail ? ( {subCourse.title} ) : (
)} {subCourse.level && (
{subCourse.level}
)}
{/* Content */}
{/* Status and menu */}
{subCourse.is_active ? "Active" : "Inactive"}
e.stopPropagation()} > {openMenuId === subCourse.id && (
)}
{/* Title */}

{subCourse.title}

{subCourse.description || "No description available"}

{/* Edit button */}
); })}
)} {/* Delete Modal */} {showDeleteModal && subCourseToDelete && (

Delete Course

Are you sure you want to delete{" "} {subCourseToDelete.title} ? This action cannot be undone.

)} {/* Add Sub-module Modal */} {showAddModal && (

Add New Course

setTitle(e.target.value)} placeholder="Enter course title" className="min-h-[44px]" />