From 2fcf2b47b08f6b9000cceb6dddfc1a412f762879 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 7 Apr 2026 03:57:30 -0700 Subject: [PATCH] add intro video preview in speaking create form Show a live intro video preview from the entered URL, using Vimeo embed playback when applicable and HTML5 video fallback for direct links. Made-with: Cursor --- src/pages/content-management/SpeakingPage.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/pages/content-management/SpeakingPage.tsx b/src/pages/content-management/SpeakingPage.tsx index cdd5499..47fd554 100644 --- a/src/pages/content-management/SpeakingPage.tsx +++ b/src/pages/content-management/SpeakingPage.tsx @@ -114,6 +114,29 @@ function introVideoUrlFromUploadResponse(data: { url?: string; embed_url?: strin return pageUrl || null } +function toVimeoEmbedUrl(rawUrl: string): string | null { + try { + const parsed = new URL(rawUrl.trim()) + const host = parsed.hostname.toLowerCase() + if (!host.includes("vimeo.com")) return null + + if (host.includes("player.vimeo.com") && parsed.pathname.includes("/video/")) { + return parsed.toString() + } + + const segments = parsed.pathname.split("/").filter(Boolean) + const videoId = segments.find((segment) => /^\d+$/.test(segment)) + if (!videoId) return null + + const hash = parsed.searchParams.get("h") + return hash + ? `https://player.vimeo.com/video/${videoId}?h=${encodeURIComponent(hash)}` + : `https://player.vimeo.com/video/${videoId}` + } catch { + return null + } +} + export function SpeakingPage() { const [audioQuestions, setAudioQuestions] = useState([]) const [audioTotalCount, setAudioTotalCount] = useState(0) @@ -491,6 +514,13 @@ export function SpeakingPage() { return haystack.includes(q) }) }, [subCourseOptions, subCourseSearch]) + const introVideoPreview = useMemo(() => { + const value = introVideoUrl.trim() + if (!value || !/^https?:\/\//i.test(value)) return null + const vimeoEmbedUrl = toVimeoEmbedUrl(value) + if (vimeoEmbedUrl) return { kind: "iframe" as const, src: vimeoEmbedUrl } + return { kind: "video" as const, src: value } + }, [introVideoUrl]) const updateDraft = (index: number, updater: (draft: AudioQuestionDraft) => AudioQuestionDraft) => { setQuestionDrafts((prev) => prev.map((draft, idx) => (idx === index ? updater(draft) : draft))) @@ -1613,6 +1643,22 @@ export function SpeakingPage() {

Paste a link or upload from your computer; uploads use the same file service as elsewhere. Optional, not tied to sub-course video rows.

+ {introVideoPreview ? ( +
+

Preview

+ {introVideoPreview.kind === "iframe" ? ( +