diff --git a/src/pages/content-management/HumanLanguagePage.tsx b/src/pages/content-management/HumanLanguagePage.tsx index fad29ba..0dd300d 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -28,6 +28,7 @@ import { createHumanLanguageLesson, deleteSubCourse, getHumanLanguageHierarchy, + getPracticeQuestions, getPracticeQuestionsByPractice, } from "../../api/courses.api" import { Badge } from "../../components/ui/badge" @@ -48,7 +49,7 @@ type SubModuleCardSelection = { lessonId: number | null; practiceId: number | nu type PracticeQuestionsFetchState = | { status: "idle" } - | { status: "loading" } + | { status: "loading"; startedAt: number } | { status: "ok"; questions: QuestionSetQuestion[]; totalCount: number } | { status: "error"; message: string } @@ -66,6 +67,48 @@ function practiceStatusStyle(status: string): string { 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 = { @@ -99,6 +142,41 @@ export function HumanLanguagePage() { 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 ? ( +