From 0cc2e4ce4e6943242819f63e8de543fe22b6ba01 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 14 Apr 2026 07:08:38 -0700 Subject: [PATCH] add files-upload URL import and intro video preview for lessons Update the lesson wizard intro video section to support both file upload and general URL import via /files/upload, with Vimeo-safe normalization and inline video preview. Made-with: Cursor --- .../content-management/AddNewLessonPage.tsx | 144 ++++++++++++++++-- 1 file changed, 132 insertions(+), 12 deletions(-) diff --git a/src/pages/content-management/AddNewLessonPage.tsx b/src/pages/content-management/AddNewLessonPage.tsx index 7c6e955..8da79af 100644 --- a/src/pages/content-management/AddNewLessonPage.tsx +++ b/src/pages/content-management/AddNewLessonPage.tsx @@ -90,6 +90,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 + } +} + +function isDirectVideoFile(url: string): boolean { + const clean = url.split("?")[0].toLowerCase() + return /\.(mp4|webm|ogg|mov|m4v)$/.test(clean) +} + export function AddNewLessonPage() { const { categoryId, courseId, subModuleId } = useParams() const navigate = useNavigate() @@ -112,6 +135,7 @@ export function AddNewLessonPage() { const [lessonDescription, setLessonDescription] = useState("") const [introVideoUrl, setIntroVideoUrl] = useState("") const [uploadingIntroVideo, setUploadingIntroVideo] = useState(false) + const [importingIntroVideoUrl, setImportingIntroVideoUrl] = useState(false) const [shuffleQuestions, setShuffleQuestions] = useState(false) const [passingScore, setPassingScore] = useState(50) const [timeLimitMinutes, setTimeLimitMinutes] = useState(60) @@ -143,6 +167,44 @@ export function AddNewLessonPage() { } } + const handleImportIntroVideoFromUrl = async () => { + const source = introVideoUrl.trim() + if (!source || !/^https?:\/\//i.test(source)) return + const vimeoEmbed = toVimeoEmbedUrl(source) + // Vimeo page URLs can be protected by anti-bot checks when server-side fetched. + // For those links, prefer local normalization to player URL instead of failing import. + if (vimeoEmbed) { + setIntroVideoUrl(vimeoEmbed) + return + } + + setImportingIntroVideoUrl(true) + try { + const uploadRes = await uploadVideoFile(source, { + title: lessonTitle.trim() || "Lesson intro", + description: lessonDescription.trim() || undefined, + }) + const finalUrl = introVideoUrlFromUploadResponse(uploadRes.data?.data) + if (!finalUrl) throw new Error("Missing uploaded video url") + setIntroVideoUrl(finalUrl) + toast.success("Intro video URL imported", { description: "Processed via /files/upload." }) + } catch (error) { + console.error("Failed to import intro video URL:", error) + toast.error("Failed to import intro video URL") + } finally { + setImportingIntroVideoUrl(false) + } + } + + const introVideoPreview = useMemo(() => { + const raw = introVideoUrl.trim() + if (!raw) return null + const vimeoEmbedUrl = toVimeoEmbedUrl(raw) + if (vimeoEmbedUrl) return { kind: "vimeo" as const, url: vimeoEmbedUrl } + if (isDirectVideoFile(raw)) return { kind: "video" as const, url: raw } + return null + }, [introVideoUrl]) + const addQuestion = () => setQuestions((prev) => [...prev, createEmptyQuestion(String(Date.now()))]) const removeQuestion = (id: string) => setQuestions((prev) => (prev.length > 1 ? prev.filter((q) => q.id !== id) : prev)) const updateQuestion = (id: string, updates: Partial) => @@ -279,25 +341,83 @@ export function AddNewLessonPage() {
- setIntroVideoUrl(e.target.value)} placeholder="https://..." /> + setIntroVideoUrl(e.target.value)} + onBlur={() => void handleImportIntroVideoFromUrl()} + placeholder="https://..." + type="url" + inputMode="url" + autoComplete="off" + className="font-mono text-[13px]" + /> - +
+ + + {introVideoUrl.trim() ? ( + + ) : null} +
+ {introVideoPreview ? ( +
+

Preview

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