import { useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { useCourseDetail, useUpdateCourse } from "@/hooks/queries/useCourseGeneration"; import { courseGenerationService } from "@/services/course-generation.service"; import type { ModuleConfig } from "@/types"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { ChevronDown, GripVertical, Search, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; export default function ModuleBuilder() { const { courseId: courseIdParam } = useParams(); const courseId = Number(courseIdParam); const { data: course, isLoading } = useCourseDetail(courseId); const updateCourse = useUpdateCourse(); const [modules, setModules] = useState(null); const [resourceOpen, setResourceOpen] = useState(false); const [aiOpen, setAiOpen] = useState(false); const [aiForm, setAiForm] = useState({ topic: "", difficulty: "B2", type: "reading" }); const [dragId, setDragId] = useState(null); const mergedModules = modules ?? course?.modules ?? []; const bySkill = useMemo(() => { const map = new Map(); mergedModules.forEach((m) => { const k = m.skill || "General"; if (!map.has(k)) map.set(k, []); map.get(k)!.push(m); }); return map; }, [mergedModules]); const totalHours = mergedModules.reduce((s, m) => s + m.estimated_hours, 0); const syncModules = (next: ModuleConfig[]) => { setModules(next); if (course) { updateCourse.mutate({ courseId, payload: { modules: next }, }); } }; const onDragStart = (id: number) => setDragId(id); const onDrop = (targetId: number) => { if (dragId === null || dragId === targetId) return; const list = [...mergedModules]; const from = list.findIndex((m) => m.id === dragId); const to = list.findIndex((m) => m.id === targetId); if (from < 0 || to < 0) return; const [item] = list.splice(from, 1); list.splice(to, 0, item); syncModules(list); setDragId(null); }; const publish = () => { updateCourse.mutate({ courseId, payload: { status: "published" } }); }; if (isLoading) { return (
); } return (

Module builder

{course?.title ?? `Course #${courseId}`} · {totalHours}h total · {mergedModules.length} modules

{mergedModules.length === 0 && (

No modules yet

Add modules manually or use the AI assistant to auto-generate a module structure.

)} {Array.from(bySkill.entries()).map(([skill, mods]) => ( {skill} · {mods.reduce((s, m) => s + m.estimated_hours, 0)}h · {mods.length} modules {mods.map((mod, idx) => (
onDragStart(mod.id)} onDragOver={(e) => e.preventDefault()} onDrop={() => onDrop(mod.id)} className={cn("rounded-lg border p-4", dragId === mod.id && "opacity-60")} >
Module {idx + 1}: {mod.title}
{ const next = mergedModules.map((m) => m.id === mod.id ? { ...m, title: e.target.value } : m, ); syncModules(next); }} />
{ const v = Number(e.target.value); const next = mergedModules.map((m) => m.id === mod.id ? { ...m, estimated_hours: v } : m, ); syncModules(next); }} />
{mod.resources.map((r) => ( {r.title} ({r.type}) ))}