feat: Implement user profile management, add multi-language interactive transcripts, and lay groundwork for SSO.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Clock } from "lucide-react";
|
||||
|
||||
interface Cue {
|
||||
@@ -10,15 +10,22 @@ interface Cue {
|
||||
}
|
||||
|
||||
interface InteractiveTranscriptProps {
|
||||
cues: Cue[];
|
||||
transcription: {
|
||||
en?: string;
|
||||
es?: string;
|
||||
cues?: Cue[];
|
||||
};
|
||||
currentTime: number;
|
||||
onSeek: (time: number) => void;
|
||||
}
|
||||
|
||||
export default function InteractiveTranscript({ cues, currentTime, onSeek }: InteractiveTranscriptProps) {
|
||||
export default function InteractiveTranscript({ transcription, currentTime, onSeek }: InteractiveTranscriptProps) {
|
||||
const [lang, setLang] = useState<'en' | 'es'>('es'); // Default to Spanish as per Experience portal target
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const activeCueRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const cues = transcription.cues || [];
|
||||
|
||||
// Auto-scroll to active cue
|
||||
useEffect(() => {
|
||||
if (activeCueRef.current && scrollRef.current) {
|
||||
@@ -41,9 +48,25 @@ export default function InteractiveTranscript({ cues, currentTime, onSeek }: Int
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full glass-card overflow-hidden border-white/5 bg-black/20">
|
||||
<div className="p-6 border-b border-white/5 flex items-center gap-3 bg-white/5">
|
||||
<Clock className="w-4 h-4 text-blue-400" />
|
||||
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400">Transcriptor Interactivo</h3>
|
||||
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="w-4 h-4 text-blue-400" />
|
||||
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400">Transcripción</h3>
|
||||
</div>
|
||||
<div className="flex bg-black/40 rounded-lg p-1 border border-white/5">
|
||||
<button
|
||||
onClick={() => setLang('en')}
|
||||
className={`px-3 py-1 text-[10px] font-black rounded-md transition-all ${lang === 'en' ? 'bg-blue-600 text-white' : 'text-gray-500 hover:text-white'}`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLang('es')}
|
||||
className={`px-3 py-1 text-[10px] font-black rounded-md transition-all ${lang === 'es' ? 'bg-blue-600 text-white' : 'text-gray-500 hover:text-white'}`}
|
||||
>
|
||||
ES
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -53,19 +76,22 @@ export default function InteractiveTranscript({ cues, currentTime, onSeek }: Int
|
||||
{cues.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-8">
|
||||
<span className="text-4xl mb-4">🤐</span>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-widest font-bold">No hay transcripción disponible para este contenido</p>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-widest font-bold">No hay transcripción disponible</p>
|
||||
</div>
|
||||
) : (
|
||||
cues.map((cue, index) => {
|
||||
const active = isCueActive(cue);
|
||||
// In a more advanced implementation, we'd have translated cues.
|
||||
// For now, if lang is 'es' and we have a full translation but no cue-level translation,
|
||||
// we'd ideally align them. To keep it simple and working:
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
ref={active ? activeCueRef : null}
|
||||
onClick={() => onSeek(cue.start)}
|
||||
className={`group cursor-pointer p-4 rounded-2xl transition-all border ${active
|
||||
? 'bg-blue-500/10 border-blue-500/30 text-white translate-x-1'
|
||||
: 'bg-white/5 border-transparent text-gray-400 hover:bg-white/10 hover:border-white/10'
|
||||
? 'bg-blue-500/10 border-blue-500/30 text-white translate-x-1'
|
||||
: 'bg-white/5 border-transparent text-gray-400 hover:bg-white/10 hover:border-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
@@ -80,10 +106,19 @@ export default function InteractiveTranscript({ cues, currentTime, onSeek }: Int
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{lang === 'es' && transcription.es && cues.length > 0 && (
|
||||
<div className="mt-8 pt-8 border-t border-white/10">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-blue-400 mb-4">Traducción Completa (Beta)</h4>
|
||||
<p className="text-sm text-gray-400 leading-relaxed italic">
|
||||
{transcription.es}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white/5 border-t border-white/5 flex items-center justify-between">
|
||||
<span className="text-[8px] font-bold text-gray-500 uppercase tracking-widest">Haz clic en cualquier segmento para saltar</span>
|
||||
<span className="text-[8px] font-bold text-gray-500 uppercase tracking-widest">Haz clic para saltar al tiempo</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1 h-1 rounded-full bg-blue-500 animate-pulse"></div>
|
||||
<div className="w-1 h-1 rounded-full bg-blue-500/50"></div>
|
||||
|
||||
@@ -5,18 +5,20 @@ import { Play, Lock, AlertCircle } from "lucide-react";
|
||||
|
||||
interface MediaPlayerProps {
|
||||
id: string;
|
||||
lessonId?: string;
|
||||
title?: string;
|
||||
url: string;
|
||||
media_type: 'video' | 'audio';
|
||||
config?: {
|
||||
maxPlays?: number;
|
||||
};
|
||||
hasTranscription?: boolean;
|
||||
initialPlayCount?: number;
|
||||
onTimeUpdate?: (time: number) => void;
|
||||
onPlay?: () => void;
|
||||
}
|
||||
|
||||
export default function MediaPlayer({ id, title, url, media_type, config, initialPlayCount, onTimeUpdate, onPlay }: MediaPlayerProps) {
|
||||
export default function MediaPlayer({ id, lessonId, title, url, media_type, config, hasTranscription, initialPlayCount, onTimeUpdate, onPlay }: MediaPlayerProps) {
|
||||
const [playCount, setPlayCount] = useState(initialPlayCount || 0);
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
const [locked, setLocked] = useState(false);
|
||||
@@ -86,6 +88,16 @@ export default function MediaPlayer({ id, title, url, media_type, config, initia
|
||||
return rawUrl;
|
||||
};
|
||||
|
||||
const getToken = () => typeof window !== 'undefined' ? localStorage.getItem('experience_token') : null;
|
||||
const selectedOrgId = typeof window !== 'undefined' ? localStorage.getItem('experience_selected_org_id') : null;
|
||||
|
||||
// Construct VTT URLs with auth if possible, or assume public/handled by backend
|
||||
// Since browser <track> doesn't support custom headers easily,
|
||||
// we might need to handle this via a proxy or temporary signed URLs.
|
||||
// For now, we'll assume the backend allows VTT access if requested with the correct lesson ID.
|
||||
const vttEn = lessonId ? `${CMS_API_URL}/lessons/${lessonId}/vtt?lang=en` : null;
|
||||
const vttEs = lessonId ? `${CMS_API_URL}/lessons/${lessonId}/vtt?lang=es` : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6" id={id}>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -102,6 +114,7 @@ export default function MediaPlayer({ id, title, url, media_type, config, initia
|
||||
<video
|
||||
src={getFullUrl(url)}
|
||||
controls
|
||||
crossOrigin="anonymous"
|
||||
className="w-full h-full rounded-xl"
|
||||
onPlay={handlePlay}
|
||||
onTimeUpdate={(e) => {
|
||||
@@ -109,7 +122,14 @@ export default function MediaPlayer({ id, title, url, media_type, config, initia
|
||||
onTimeUpdate(e.currentTarget.currentTime);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{hasTranscription && vttEn && (
|
||||
<track kind="subtitles" src={vttEn} srcLang="en" label="English" />
|
||||
)}
|
||||
{hasTranscription && vttEs && (
|
||||
<track kind="subtitles" src={vttEs} srcLang="es" label="Español" />
|
||||
)}
|
||||
</video>
|
||||
) : (
|
||||
<iframe
|
||||
src={getEmbedUrl(url)}
|
||||
|
||||
Reference in New Issue
Block a user