import { useCallback, useEffect, useRef, useState } from "react"; import { Plus, ArrowRight, Pencil, Trash2, X } from "lucide-react"; import { Link } 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, DialogTrigger, DialogFooter, } 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 { getLearningPrograms, createLearningProgram, updateLearningProgram, deleteLearningProgram, } from "../../api/courses.api"; import { uploadImageFile } from "../../api/files.api"; import type { LearningProgramListItem } from "../../types/course.types"; export function LearnEnglishPage() { const [programs, setPrograms] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingProgram, setEditingProgram] = 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 [createOpen, setCreateOpen] = 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 [deletingProgram, setDeletingProgram] = useState(null); const [deleting, setDeleting] = useState(false); const openEdit = (program: LearningProgramListItem) => { setEditingProgram(program); setEditName(program.name ?? ""); setEditDescription(program.description?.trim() ?? ""); setEditThumbnail(program.thumbnail?.trim() ?? ""); }; const closeEdit = () => { setEditingProgram(null); setEditName(""); setEditDescription(""); setEditThumbnail(""); setUploadingEditThumbnail(false); if (editThumbnailFileInputRef.current) editThumbnailFileInputRef.current.value = ""; }; const handleEditThumbnailFile = 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 clearCreateFormFields = () => { setCreateName(""); setCreateDescription(""); setCreateThumbnail(""); if (createThumbnailFileInputRef.current) { createThumbnailFileInputRef.current.value = ""; } }; const handleCreateDialogOpenChange = (open: boolean) => { if (!open && (createSaving || createUploadingThumbnail)) return; clearCreateFormFields(); setCreateOpen(open); }; const handleCreateThumbnailFile = 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 handleCreateProgram = async () => { const name = createName.trim(); if (!name) { toast.error("Program name is required"); return; } setCreateSaving(true); try { await createLearningProgram({ name, description: createDescription.trim(), thumbnail: createThumbnail.trim(), }); toast.success("Program created"); clearCreateFormFields(); setCreateOpen(false); await fetchPrograms(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to create program"; toast.error(msg); } finally { setCreateSaving(false); } }; const handleSaveEdit = async () => { if (!editingProgram) return; const name = editName.trim(); if (!name) { toast.error("Program name is required"); return; } setSavingEdit(true); try { await updateLearningProgram(editingProgram.id, { name, description: editDescription.trim(), thumbnail: editThumbnail.trim(), }); toast.success("Program updated"); closeEdit(); await fetchPrograms(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to update program"; toast.error(msg); } finally { setSavingEdit(false); } }; const handleConfirmDelete = async () => { if (!deletingProgram) return; setDeleting(true); try { await deleteLearningProgram(deletingProgram.id); toast.success("Program deleted"); setDeletingProgram(null); await fetchPrograms(); } catch (e: unknown) { console.error(e); const msg = (e as { response?: { data?: { message?: string } } })?.response?.data ?.message ?? "Failed to delete program"; toast.error(msg); } finally { setDeleting(false); } }; const fetchPrograms = useCallback(async () => { setLoading(true); setError(null); try { const res = await getLearningPrograms({ limit: 100, offset: 0 }); const raw = res.data?.data?.programs; const list = Array.isArray(raw) ? raw : []; const sorted = [...list].sort( (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0), ); setPrograms(sorted); } catch (e) { console.error(e); setError("Failed to load programs"); setPrograms([]); toast.error("Could not load programs", { description: "Check your connection or try again.", }); } finally { setLoading(false); } }, []); useEffect(() => { void fetchPrograms(); }, [fetchPrograms]); return (
{/* Header section */}

Learn English

Manage learning content by program — cards load from the server

Add New Program Create a learning program via{" "} POST /programs . Thumbnail can be a URL or a file uploaded through{" "} POST /files/upload . {/* Gradient Divider */}
{ e.preventDefault(); void handleCreateProgram(); }} >
setCreateName(e.target.value)} placeholder="e.g. Intermediate Track" className="h-12 rounded-xl ring-0" disabled={createSaving || createUploadingThumbnail} />