diff --git a/src/pages/content-management/components/ContentHierarchyList.tsx b/src/pages/content-management/components/ContentHierarchyList.tsx index 93c98af..e3950b6 100644 --- a/src/pages/content-management/components/ContentHierarchyList.tsx +++ b/src/pages/content-management/components/ContentHierarchyList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { DndContext, closestCenter, @@ -33,8 +33,15 @@ import { Edit2, Trash2, Image as ImageIcon, + Loader2, } from "lucide-react"; import { cn } from "../../../lib/utils"; +import { + getLearningPrograms, + getProgramCourses, + getTopLevelCourseModules, + getModuleLessons, +} from "../../../api/courses.api"; // --- Types --- export type ItemType = "program" | "course" | "module" | "lesson"; @@ -56,46 +63,6 @@ export interface Lesson extends BaseItem { moduleId: string; } -// --- Mock Data --- -const initialPrograms: Program[] = [ - { - id: "p1", - name: "Web Development Masterclass", - thumbnail: - "https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=100&h=100&fit=crop", - }, - { - id: "p2", - name: "Mobile App Development", - thumbnail: - "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=100&h=100&fit=crop", - }, - { - id: "p3", - name: "UI/UX Design Fundamentals", - thumbnail: - "https://images.unsplash.com/photo-1586717791821-3f44a563eb4c?w=100&h=100&fit=crop", - }, -]; - -const initialCourses: Course[] = [ - { id: "c1", name: "React for Beginners", programId: "p1" }, - { id: "c2", name: "Advanced Node.js", programId: "p1" }, - { id: "c3", name: "Swift UI Intro", programId: "p2" }, -]; - -const initialModules: Module[] = [ - { id: "m1", name: "Introduction to Hooks", courseId: "c1" }, - { id: "m2", name: "State Management", courseId: "c1" }, - { id: "m3", name: "Backend Architecture", courseId: "c2" }, -]; - -const initialLessons: Lesson[] = [ - { id: "l1", name: "What is useState?", moduleId: "m1" }, - { id: "l2", name: "useEffect deep dive", moduleId: "m1" }, - { id: "l3", name: "Redux Setup", moduleId: "m2" }, -]; - // --- Components --- interface SortableItemProps { @@ -345,14 +312,100 @@ function HierarchySection({ } export function ContentHierarchyList() { - const [programs, setPrograms] = useState(initialPrograms); - const [courses, setCourses] = useState(initialCourses); - const [modules, setModules] = useState(initialModules); - const [lessons, setLessons] = useState(initialLessons); + const [programs, setPrograms] = useState([]); + const [courses, setCourses] = useState([]); + const [modules, setModules] = useState([]); + const [lessons, setLessons] = useState([]); + const [loading, setLoading] = useState>({}); const [openSections, setOpenSections] = useState>({ program: true, }); + const fetchHierarchy = useCallback(async () => { + setLoading({ program: true }); + try { + // 1. Fetch Programs + const programsRes = await getLearningPrograms(); + const programData = programsRes.data?.data; + const fetchedPrograms: Program[] = (programData?.programs || []).map( + (p) => ({ + id: String(p.id), + name: p.name, + thumbnail: p.thumbnail || undefined, + }), + ); + setPrograms(fetchedPrograms); + setLoading((prev) => ({ ...prev, program: false })); + + if (fetchedPrograms.length === 0) return; + + // 2. Fetch Courses for all programs + setLoading((prev) => ({ ...prev, course: true })); + const coursesPromises = fetchedPrograms.map((p) => + getProgramCourses(Number(p.id)), + ); + const coursesResults = await Promise.all(coursesPromises); + const fetchedCourses: Course[] = coursesResults.flatMap((res, idx) => { + const courseData = res.data?.data; + return (courseData?.courses || []).map((c) => ({ + id: String(c.id), + name: c.name, + thumbnail: c.thumbnail_url || c.thumbnail || undefined, + programId: fetchedPrograms[idx].id, + })); + }); + setCourses(fetchedCourses); + setLoading((prev) => ({ ...prev, course: false })); + + if (fetchedCourses.length === 0) return; + + // 3. Fetch Modules for all courses + setLoading((prev) => ({ ...prev, module: true })); + const modulesPromises = fetchedCourses.map((c) => + getTopLevelCourseModules(Number(c.id)), + ); + const modulesResults = await Promise.all(modulesPromises); + const fetchedModules: Module[] = modulesResults.flatMap((res, idx) => { + const moduleData = res.data?.data; + return (moduleData?.modules || []).map((m) => ({ + id: String(m.id), + name: m.name, + thumbnail: m.icon || undefined, + courseId: fetchedCourses[idx].id, + })); + }); + setModules(fetchedModules); + setLoading((prev) => ({ ...prev, module: false })); + + if (fetchedModules.length === 0) return; + + // 4. Fetch Lessons for all modules + setLoading((prev) => ({ ...prev, lesson: true })); + const lessonsPromises = fetchedModules.map((m) => + getModuleLessons(Number(m.id)), + ); + const lessonsResults = await Promise.all(lessonsPromises); + const fetchedLessons: Lesson[] = lessonsResults.flatMap((res, idx) => { + const lessonData = res.data?.data; + return (lessonData?.lessons || []).map((l) => ({ + id: String(l.id), + name: l.title, + thumbnail: l.thumbnail || undefined, + moduleId: fetchedModules[idx].id, + })); + }); + setLessons(fetchedLessons); + } catch (error) { + console.error("Failed to fetch content hierarchy:", error); + } finally { + setLoading({}); + } + }, []); + + useEffect(() => { + fetchHierarchy(); + }, [fetchHierarchy]); + const toggleSection = (id: string) => { setOpenSections((prev) => ({ ...prev, [id]: !prev[id] })); }; @@ -360,11 +413,11 @@ export function ContentHierarchyList() { const reorder = ( list: T[], setList: React.Dispatch>, - activeId: string, - overId: string, + activeId: UniqueIdentifier, + overId: UniqueIdentifier, ) => { - const oldIndex = list.findIndex((i) => i.id === activeId); - const newIndex = list.findIndex((i) => i.id === overId); + const oldIndex = list.findIndex((i) => i.id === String(activeId)); + const newIndex = list.findIndex((i) => i.id === String(overId)); if (oldIndex !== -1 && newIndex !== -1) { setList(arrayMove(list, oldIndex, newIndex)); } @@ -372,7 +425,6 @@ export function ContentHierarchyList() { const handleEdit = (type: ItemType, id: string) => { console.log(`Edit ${type}: ${id}`); - // Logic for opening edit modal would go here }; const handleDelete = (type: ItemType, id: string) => { @@ -396,10 +448,7 @@ export function ContentHierarchyList() { }; const handleReset = () => { - setPrograms(initialPrograms); - setCourses(initialCourses); - setModules(initialModules); - setLessons(initialLessons); + fetchHierarchy(); }; return ( @@ -418,7 +467,7 @@ export function ContentHierarchyList() { className="text-[13px] font-bold text-brand-300 hover:text-brand-400 transition-colors flex items-center gap-2 group" > - Reset All + Sync with API @@ -430,15 +479,21 @@ export function ContentHierarchyList() { isOpen={openSections.program} onToggle={() => toggleSection("program")} > - - reorder(programs, setPrograms, active, over) - } - icon={} - onEdit={(id) => handleEdit("program", id)} - onDelete={(id) => handleDelete("program", id)} - /> + {loading.program ? ( +
+ +
+ ) : ( + + reorder(programs, setPrograms, active, over) + } + icon={} + onEdit={(id) => handleEdit("program", id)} + onDelete={(id) => handleDelete("program", id)} + /> + )} {/* Course Section */} @@ -448,28 +503,34 @@ export function ContentHierarchyList() { isOpen={openSections.course} onToggle={() => toggleSection("course")} > - {programs.map((program) => { - const programCourses = courses.filter( - (c) => c.programId === program.id, - ); - if (programCourses.length === 0) return null; - return ( -
-

- {program.name} -

- - reorder(courses, setCourses, active, over) - } - icon={} - onEdit={(id) => handleEdit("course", id)} - onDelete={(id) => handleDelete("course", id)} - /> -
- ); - })} + {loading.course ? ( +
+ +
+ ) : ( + programs.map((program) => { + const programCourses = courses.filter( + (c) => c.programId === program.id, + ); + if (programCourses.length === 0) return null; + return ( +
+

+ {program.name} +

+ + reorder(courses, setCourses, active, over) + } + icon={} + onEdit={(id) => handleEdit("course", id)} + onDelete={(id) => handleDelete("course", id)} + /> +
+ ); + }) + )} {/* Module Section */} @@ -479,28 +540,34 @@ export function ContentHierarchyList() { isOpen={openSections.module} onToggle={() => toggleSection("module")} > - {courses.map((course) => { - const courseModules = modules.filter( - (m) => m.courseId === course.id, - ); - if (courseModules.length === 0) return null; - return ( -
-

- {course.name} -

- - reorder(modules, setModules, active, over) - } - icon={} - onEdit={(id) => handleEdit("module", id)} - onDelete={(id) => handleDelete("module", id)} - /> -
- ); - })} + {loading.module ? ( +
+ +
+ ) : ( + courses.map((course) => { + const courseModules = modules.filter( + (m) => m.courseId === course.id, + ); + if (courseModules.length === 0) return null; + return ( +
+

+ {course.name} +

+ + reorder(modules, setModules, active, over) + } + icon={} + onEdit={(id) => handleEdit("module", id)} + onDelete={(id) => handleDelete("module", id)} + /> +
+ ); + }) + )} {/* Lesson Section */} @@ -510,28 +577,34 @@ export function ContentHierarchyList() { isOpen={openSections.lesson} onToggle={() => toggleSection("lesson")} > - {modules.map((module) => { - const moduleLessons = lessons.filter( - (l) => l.moduleId === module.id, - ); - if (moduleLessons.length === 0) return null; - return ( -
-

- {module.name} -

- - reorder(lessons, setLessons, active, over) - } - icon={} - onEdit={(id) => handleEdit("lesson", id)} - onDelete={(id) => handleDelete("lesson", id)} - /> -
- ); - })} + {loading.lesson ? ( +
+ +
+ ) : ( + modules.map((module) => { + const moduleLessons = lessons.filter( + (l) => l.moduleId === module.id, + ); + if (moduleLessons.length === 0) return null; + return ( +
+

+ {module.name} +

+ + reorder(lessons, setLessons, active, over) + } + icon={} + onEdit={(id) => handleEdit("lesson", id)} + onDelete={(id) => handleDelete("lesson", id)} + /> +
+ ); + }) + )}