import { useCallback, useEffect, useRef, useState } from "react"; import { ArrowLeft, Plus, FileText, Pencil, Trash2, X } from "lucide-react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { toast } from "sonner"; import { Card, CardContent } from "../../components/ui/card"; import { Button } from "../../components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Textarea } from "../../components/ui/textarea"; import uploadIcon from "../../assets/icons/upload.png"; import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg"; import alertSrc from "../../assets/Alert.svg"; import { createProgramCourse, deleteTopLevelCourse, getLearningPrograms, getProgramCourses, updateTopLevelCourse, } from "../../api/courses.api"; import { uploadImageFile } from "../../api/files.api"; import type { LearningProgramListItem, ProgramCourseListItem, } from "../../types/course.types"; export function ProgramCoursesPage() { const navigate = useNavigate(); /** Route segment is the numeric program id (see Learn English program cards). */ const { level: programIdParam } = useParams<{ level: string }>(); const programId = Number(programIdParam); const [program, setProgram] = useState(null); const [courses, setCourses] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [deletingCourse, setDeletingCourse] = useState( null, ); const [deleting, setDeleting] = useState(false); const [editingCourse, setEditingCourse] = useState( null, ); const [editName, setEditName] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editThumbnail, setEditThumbnail] = useState(""); const [savingEdit, setSavingEdit] = useState(false); const [uploadingEditThumbnail, setUploadingEditThumbnail] = useState(false); const editThumbnailFileInputRef = useRef(null); const [createCourseOpen, setCreateCourseOpen] = useState(false); const [createName, setCreateName] = useState(""); const [createDescription, setCreateDescription] = useState(""); const [createThumbnail, setCreateThumbnail] = useState(""); const [createSaving, setCreateSaving] = useState(false); const [createUploadingThumbnail, setCreateUploadingThumbnail] = useState(false); const createThumbnailFileInputRef = useRef(null); const programIdValid = Number.isFinite(programId) && programId >= 1; const loadData = useCallback(async () => { if (!Number.isFinite(programId) || programId < 1) { setError("Invalid program"); setLoading(false); setCourses([]); setProgram(null); return; } setLoading(true); setError(null); try { const [coursesRes, programsRes] = await Promise.all([ getProgramCourses(programId, { limit: 100, offset: 0 }), getLearningPrograms({ limit: 100, offset: 0 }), ]); const programRows = programsRes.data?.data?.programs; const list = Array.isArray(programRows) ? programRows : []; const found = list.find((p) => p.id === programId) ?? null; setProgram(found); const raw = coursesRes.data?.data?.courses; const courseList = Array.isArray(raw) ? raw : []; const sorted = [...courseList].sort( (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0), ); setCourses(sorted); } catch (e) { console.error(e); setError("Failed to load courses"); setCourses([]); setProgram(null); toast.error("Could not load courses", { description: "Check your connection or try again.", }); } finally { setLoading(false); } }, [programId]); useEffect(() => { void loadData(); }, [loadData]); const handleConfirmDeleteCourse = async () => { if (!deletingCourse) return; setDeleting(true); try { await deleteTopLevelCourse(deletingCourse.id); toast.success("Course deleted"); setDeletingCourse(null); await loadData(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to delete course"; toast.error(msg); } finally { setDeleting(false); } }; const openEditCourse = (course: ProgramCourseListItem) => { setEditingCourse(course); setEditName(course.name ?? ""); setEditDescription(course.description?.trim() ?? ""); setEditThumbnail( course.thumbnail?.trim() || course.thumbnail_url?.trim() || "", ); }; const closeEditCourse = () => { setEditingCourse(null); setEditName(""); setEditDescription(""); setEditThumbnail(""); setUploadingEditThumbnail(false); if (editThumbnailFileInputRef.current) { editThumbnailFileInputRef.current.value = ""; } }; const handleEditCourseThumbnailFile = async ( event: React.ChangeEvent, ) => { 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; } const maxBytes = 5 * 1024 * 1024; if (file.size > maxBytes) { toast.error("Image is too large", { description: "Maximum size is 5 MB." }); return; } setUploadingEditThumbnail(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"); } setEditThumbnail(url); toast.success("Thumbnail uploaded"); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload thumbnail"; toast.error(msg); } finally { setUploadingEditThumbnail(false); } }; const handleSaveEditCourse = async () => { if (!editingCourse) return; const name = editName.trim(); if (!name) { toast.error("Course name is required"); return; } setSavingEdit(true); try { await updateTopLevelCourse(editingCourse.id, { name, description: editDescription.trim(), thumbnail: editThumbnail.trim(), }); toast.success("Course updated"); closeEditCourse(); await loadData(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to update course"; toast.error(msg); } finally { setSavingEdit(false); } }; const clearCreateCourseForm = () => { setCreateName(""); setCreateDescription(""); setCreateThumbnail(""); setCreateUploadingThumbnail(false); if (createThumbnailFileInputRef.current) { createThumbnailFileInputRef.current.value = ""; } }; const handleCreateCourseDialogOpenChange = (open: boolean) => { if (!open && (createSaving || createUploadingThumbnail)) return; clearCreateCourseForm(); setCreateCourseOpen(open); }; const handleCreateCourseThumbnailFile = async ( event: React.ChangeEvent, ) => { 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; } const maxBytes = 5 * 1024 * 1024; if (file.size > maxBytes) { toast.error("Image is too large", { description: "Maximum size is 5 MB." }); return; } setCreateUploadingThumbnail(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"); } setCreateThumbnail(url); toast.success("Thumbnail uploaded"); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to upload thumbnail"; toast.error(msg); } finally { setCreateUploadingThumbnail(false); } }; const handleCreateCourse = async () => { if (!programIdValid) return; const name = createName.trim(); if (!name) { toast.error("Course name is required"); return; } setCreateSaving(true); try { await createProgramCourse(programId, { name, description: createDescription.trim(), thumbnail: createThumbnail.trim(), }); toast.success("Course created"); clearCreateCourseForm(); setCreateCourseOpen(false); await loadData(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to create course"; toast.error(msg); } finally { setCreateSaving(false); } }; const programTitle = !programIdValid ? "Program not found" : program?.name?.trim() || `Program ${programId}`; const programDescription = program?.description?.trim() || (!loading && programIdValid && !program ? "Program details are unavailable. You can still browse courses below if they loaded." : ""); return (
{/* Navigation */} Back to Programs {/* Header section */}

{programTitle}

{programDescription ? (

{programDescription}

) : loading ? (
) : null}
{programIdValid ? ( <>
Add New Course Create a course via{" "} POST /programs/:program_id/courses . Thumbnail can be a URL or a file from{" "} POST /files/upload . {/* Gradient Divider */}
{ e.preventDefault(); void handleCreateCourse(); }} >
setCreateName(e.target.value)} placeholder="e.g. Introduction to German A1" className="h-12 rounded-xl" disabled={createSaving || createUploadingThumbnail} />