import { useEffect, useMemo, useState } from "react" import { Link } from "react-router-dom" import { ChevronDown, ChevronRight, ClipboardList, Languages, Loader2, Plus, Search, Trash2, Video, } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card" import { Button } from "../../components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog" import { SpinnerIcon } from "../../components/ui/spinner-icon" import { createCourse, createCourseCategory, createHumanLanguageLesson, deleteSubCourse, getHumanLanguageHierarchy, getPracticeQuestions, getPracticeQuestionsByPractice, } from "../../api/courses.api" import { Badge } from "../../components/ui/badge" import type { HumanLanguageCourseTree, HumanLanguageSubCategoryTree, LearningPathPractice, LearningPathVideo, QuestionSetQuestion, } from "../../types/course.types" import { cn } from "../../lib/utils" import { toast } from "sonner" const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const type SubModulePanelTab = "lessons" | "practices" type SubModuleCardSelection = { lessonId: number | null; practiceId: number | null } type PracticeQuestionsFetchState = | { status: "idle" } | { status: "loading"; startedAt: number } | { status: "ok"; questions: QuestionSetQuestion[]; totalCount: number } | { status: "error"; message: string } function formatDurationSeconds(total: number): string { const s = Math.max(0, Math.floor(total)) const m = Math.floor(s / 60) const r = s % 60 return `${m}:${r.toString().padStart(2, "0")}` } function practiceStatusStyle(status: string): string { const u = status.toUpperCase() if (u === "PUBLISHED") return "bg-green-50 text-green-700 ring-1 ring-inset ring-green-200" if (u === "DRAFT") return "bg-grayScale-50 text-grayScale-600 ring-1 ring-inset ring-grayScale-200" if (u === "ARCHIVED") return "bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-200" return "bg-grayScale-50 text-grayScale-600 ring-1 ring-inset ring-grayScale-200" } const URL_REGEX = /(https?:\/\/[^\s<>"')\]]+)/gi function extractUrls(text: string): string[] { const out = text.match(URL_REGEX) ?? [] return [...new Set(out)] } function normalizeUrl(raw: string): string { return raw.trim().replace(/[),.;!?]+$/, "") } function getVimeoEmbedUrl(url: string): string | null { const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/i) return m?.[1] ? `https://player.vimeo.com/video/${m[1]}` : null } function detectMediaType(url: string, hint?: "audio" | "video" | "image"): "audio" | "video" | "image" | "unknown" { if (hint) return hint const vimeo = getVimeoEmbedUrl(url) if (vimeo) return "video" const clean = url.split("?")[0].toLowerCase() if (/\.(png|jpe?g|gif|webp|svg|avif|bmp)$/.test(clean)) return "image" if (/\.(mp4|webm|ogg|mov|m4v)$/.test(clean)) return "video" if (/\.(mp3|wav|m4a|aac|ogg|webm)$/.test(clean)) return "audio" return "unknown" } function withTimeout(promise: Promise, ms: number): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("Request timed out")), ms) promise .then((value) => { clearTimeout(timer) resolve(value) }) .catch((err) => { clearTimeout(timer) reject(err) }) }) } type CefrLevel = (typeof CEFR_LEVELS)[number] type PendingRemove = { ids: number[] key: string successMessage: string title: string description: string } export function HumanLanguagePage() { const [loading, setLoading] = useState(false) const [categoryId, setCategoryId] = useState(null) const [subCategories, setSubCategories] = useState([]) const [selectedSubCategoryId, setSelectedSubCategoryId] = useState("ALL") const [selectedCourseId, setSelectedCourseId] = useState("ALL") const [selectedLevel, setSelectedLevel] = useState("ALL") const [collapsedLevels, setCollapsedLevels] = useState([]) const [creatingKey, setCreatingKey] = useState(null) const [quickSubCategoryName, setQuickSubCategoryName] = useState("") const [quickCourseName, setQuickCourseName] = useState("") const [quickSearch, setQuickSearch] = useState("") const [quickCreating, setQuickCreating] = useState(false) const [deletingKey, setDeletingKey] = useState(null) /** Course IDs whose path body is collapsed (headers stay visible). */ const [collapsedPathIds, setCollapsedPathIds] = useState([]) const [pendingRemove, setPendingRemove] = useState(null) /** Per sub-module panel tab (lessons vs practices). */ const [subModulePanelTab, setSubModulePanelTab] = useState>({}) /** Selected lesson / practice card per sub-module (for inline detail panel). */ const [subModuleCardSelection, setSubModuleCardSelection] = useState>({}) const [practiceQuestionsState, setPracticeQuestionsState] = useState>({}) const renderMediaPreview = ( urlRaw: string, hint?: "audio" | "video" | "image", className = "mt-2", ) => { const url = normalizeUrl(urlRaw) if (!url) return null const mediaType = detectMediaType(url, hint) const vimeoEmbed = getVimeoEmbedUrl(url) return (
{mediaType === "image" ? ( preview ) : mediaType === "video" ? ( vimeoEmbed ? (