From dc519c8551ced9d644add4ca830b7257c1a05a7d Mon Sep 17 00:00:00 2001 From: Nurfog Date: Thu, 29 Jan 2026 14:56:01 -0300 Subject: [PATCH] feat: Introduce `isGraded` prop to MediaPlayer to conditionally hide interactive elements and refine its layout. --- .../courses/[id]/lessons/[lessonId]/page.tsx | 2 + web/studio/src/components/MediaPlayer.tsx | 173 +++++++++++------- .../src/components/blocks/MediaBlock.tsx | 5 +- .../components/blocks/VideoMarkerBlock.tsx | 19 +- 4 files changed, 122 insertions(+), 77 deletions(-) diff --git a/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx b/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx index 53ebd2b..945416c 100644 --- a/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx +++ b/web/studio/src/app/courses/[id]/lessons/[lessonId]/page.tsx @@ -546,6 +546,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI config={block.config || {}} editMode={editMode} transcription={lesson.transcription} + isGraded={isGraded} onChange={(updates) => updateBlock(block.id, updates)} /> )} @@ -610,6 +611,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI videoUrl={block.url || ""} markers={block.markers || []} editMode={editMode} + isGraded={isGraded} onChange={(updates) => updateBlock(block.id, updates)} /> )} diff --git a/web/studio/src/components/MediaPlayer.tsx b/web/studio/src/components/MediaPlayer.tsx index 9b3017b..3790b26 100644 --- a/web/studio/src/components/MediaPlayer.tsx +++ b/web/studio/src/components/MediaPlayer.tsx @@ -12,22 +12,31 @@ interface MediaPlayerProps { } | null; locked?: boolean; onEnded?: () => void; + isGraded?: boolean; + showInteractive?: boolean; } -export default function MediaPlayer({ src, type, transcription, locked, onEnded }: MediaPlayerProps) { +export default function MediaPlayer({ src, type, transcription, locked, onEnded, isGraded, showInteractive = true }: MediaPlayerProps) { const videoRef = useRef(null); const audioRef = useRef(null); + const [currentTime, setCurrentTime] = useState(0); const [currentCaption, setCurrentCaption] = useState(""); const [language, setLanguage] = useState<"en" | "es">("en"); + // Hide everything if graded activity or explicitely disabled + const shouldShowTranscription = transcription && !locked && !isGraded && showInteractive; + useEffect(() => { const media = type === "video" ? videoRef.current : audioRef.current; if (!media) return; const handleTimeUpdate = () => { + const time = media.currentTime; + setCurrentTime(time); + if (transcription?.cues) { const activeCue = transcription.cues.find(cue => - media.currentTime >= cue.start && media.currentTime <= cue.end + time >= cue.start && time <= cue.end ); setCurrentCaption(activeCue?.text || ""); } @@ -45,6 +54,14 @@ export default function MediaPlayer({ src, type, transcription, locked, onEnded }; }, [type, transcription, onEnded]); + const handleSeek = (time: number) => { + const media = type === "video" ? videoRef.current : audioRef.current; + if (media) { + media.currentTime = time; + media.play(); + } + }; + if (!src) { return (
@@ -68,18 +85,18 @@ export default function MediaPlayer({ src, type, transcription, locked, onEnded return match ? match[1] : null; }; - if (isYouTube || isVimeo) { - let embedUrl = ""; - if (isYouTube) { - const id = getYouTubeId(src); - embedUrl = `https://www.youtube.com/embed/${id}`; - } else { - const id = getVimeoId(src); - embedUrl = `https://player.vimeo.com/video/${id}`; - } + const renderMedia = () => { + if (isYouTube || isVimeo) { + let embedUrl = ""; + if (isYouTube) { + const id = getYouTubeId(src); + embedUrl = `https://www.youtube.com/embed/${id}`; + } else { + const id = getVimeoId(src); + embedUrl = `https://player.vimeo.com/video/${id}`; + } - return ( -
+ return (
+ ); + } - {locked && ( -
-
- 🔒 -
-

Playback Limited

-

This content can only be played once according to the activity rules.

-
- )} - - {transcription && !locked && ( -
-
-

- Transcription AI Enhanced -

-
- - -
-
-
- "{transcription[language] || "Transcription not available."}" -
-
- )} -
- ); - } - - return ( -
+ return (
{type === "video" ? (
+ ); + }; - {locked && ( -
-
- 🔒 + return ( +
+ {/* Media Content */} +
+ {renderMedia()} + + {locked && ( +
+
+ 🔒 +
+

Playback Limited

+

This content can only be played once according to the activity rules.

-

Playback Limited

-

This content can only be played once according to the activity rules.

-
- )} + )} - {transcription && !locked && ( -
-
-

- Transcription AI Enhanced -

+ {/* Simple Transcript (Fallback or Mobile) */} + {shouldShowTranscription && !transcription?.cues && ( +
+
+

+ Transcription AI Enhanced +

+
+ + +
+
+
+ "{transcription[language] || "Transcription not available."}" +
+
+ )} +
+ + {/* Interactive Sidebar */} + {shouldShowTranscription && transcription?.cues && ( +
+
+

Interactive Content

-
- "{transcription[language] || "Transcription not available."}" +
+ {language === "en" && transcription.en && !transcription.cues.some(c => c.text === transcription.en) && ( +

+ {transcription.en} +

+ )} + {transcription.cues.map((cue, idx) => ( + + ))}
)} diff --git a/web/studio/src/components/blocks/MediaBlock.tsx b/web/studio/src/components/blocks/MediaBlock.tsx index e770363..7cde9ff 100644 --- a/web/studio/src/components/blocks/MediaBlock.tsx +++ b/web/studio/src/components/blocks/MediaBlock.tsx @@ -39,9 +39,10 @@ interface MediaBlockProps { es?: string; cues?: { start: number; end: number; text: string }[]; } | null; + isGraded?: boolean; } -export default function MediaBlock({ title, url, type, config, editMode, onChange, transcription }: MediaBlockProps) { +export default function MediaBlock({ title, url, type, config, editMode, onChange, transcription, isGraded }: MediaBlockProps) { const [localPlays, setLocalPlays] = useState(config.currentPlays || 0); const [sourceType, setSourceType] = useState<"url" | "upload">(url.startsWith("/assets/") ? "upload" : "url"); const maxPlays = config.maxPlays || 0; @@ -298,6 +299,8 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang type={type} transcription={transcription} locked={isLocked} + isGraded={isGraded} + showInteractive={config.show_transcript !== false} onEnded={handleEnded} /> diff --git a/web/studio/src/components/blocks/VideoMarkerBlock.tsx b/web/studio/src/components/blocks/VideoMarkerBlock.tsx index 366d6ac..be1a3e8 100644 --- a/web/studio/src/components/blocks/VideoMarkerBlock.tsx +++ b/web/studio/src/components/blocks/VideoMarkerBlock.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Clock, Plus, Trash2, Play, AlertCircle } from "lucide-react"; +import MediaPlayer from "../MediaPlayer"; interface VideoMarker { timestamp: number; @@ -16,6 +17,7 @@ interface VideoMarkerBlockProps { markers: VideoMarker[]; onChange: (updates: { title?: string; markers?: VideoMarker[] }) => void; editMode: boolean; + isGraded?: boolean; } export default function VideoMarkerBlock({ @@ -23,7 +25,8 @@ export default function VideoMarkerBlock({ videoUrl, markers, onChange, - editMode + editMode, + isGraded }: VideoMarkerBlockProps) { const [currentTime, setCurrentTime] = useState(0); const [editingIndex, setEditingIndex] = useState(null); @@ -124,12 +127,14 @@ export default function VideoMarkerBlock({ {formatTime(currentTime)}
-