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
This commit is contained in:
parent
85df446a66
commit
2fcf2b47b0
|
|
@ -114,6 +114,29 @@ function introVideoUrlFromUploadResponse(data: { url?: string; embed_url?: strin
|
||||||
return pageUrl || null
|
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() {
|
export function SpeakingPage() {
|
||||||
const [audioQuestions, setAudioQuestions] = useState<QuestionDetail[]>([])
|
const [audioQuestions, setAudioQuestions] = useState<QuestionDetail[]>([])
|
||||||
const [audioTotalCount, setAudioTotalCount] = useState(0)
|
const [audioTotalCount, setAudioTotalCount] = useState(0)
|
||||||
|
|
@ -491,6 +514,13 @@ export function SpeakingPage() {
|
||||||
return haystack.includes(q)
|
return haystack.includes(q)
|
||||||
})
|
})
|
||||||
}, [subCourseOptions, subCourseSearch])
|
}, [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) => {
|
const updateDraft = (index: number, updater: (draft: AudioQuestionDraft) => AudioQuestionDraft) => {
|
||||||
setQuestionDrafts((prev) => prev.map((draft, idx) => (idx === index ? updater(draft) : draft)))
|
setQuestionDrafts((prev) => prev.map((draft, idx) => (idx === index ? updater(draft) : draft)))
|
||||||
|
|
@ -1613,6 +1643,22 @@ export function SpeakingPage() {
|
||||||
<p className="text-xs leading-relaxed text-grayScale-500">
|
<p className="text-xs leading-relaxed text-grayScale-500">
|
||||||
Paste a link or upload from your computer; uploads use the same file service as elsewhere. Optional, not tied to sub-course video rows.
|
Paste a link or upload from your computer; uploads use the same file service as elsewhere. Optional, not tied to sub-course video rows.
|
||||||
</p>
|
</p>
|
||||||
|
{introVideoPreview ? (
|
||||||
|
<div className="rounded-xl border border-grayScale-200 bg-black/95 p-2">
|
||||||
|
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-grayScale-300">Preview</p>
|
||||||
|
{introVideoPreview.kind === "iframe" ? (
|
||||||
|
<iframe
|
||||||
|
src={introVideoPreview.src}
|
||||||
|
title="Intro video preview"
|
||||||
|
className="aspect-video w-full rounded-md"
|
||||||
|
allow="autoplay; fullscreen; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<video src={introVideoPreview.src} controls className="aspect-video w-full rounded-md bg-black" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<aside className="space-y-4 lg:col-span-5">
|
<aside className="space-y-4 lg:col-span-5">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user