microtecflix/src/components/VideoPlayer.tsx

242 lines
8.9 KiB
TypeScript

import React, { useRef, useState, useEffect } from 'react';
import { Play, Pause, RotateCcw, Volume2, Maximize, CheckCircle, ExternalLink } from 'lucide-react';
interface VideoPlayerProps {
videoUrl: string;
videoType: 'youtube' | 'direct';
lessonId: string;
onProgressUpdate: (percentage: number) => void;
initialProgress?: number;
}
export default function VideoPlayer({ videoUrl, videoType, lessonId, onProgressUpdate, initialProgress = 0 }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [isCompleted, setIsCompleted] = useState(initialProgress >= 90);
const [manualProgress, setManualProgress] = useState(initialProgress);
// Sync state when lesson changes
useEffect(() => {
setIsCompleted(initialProgress >= 90);
setManualProgress(initialProgress);
setIsPlaying(false);
setCurrentTime(0);
}, [lessonId, initialProgress]);
// For Direct HTML5 Video Player
const handleTimeUpdate = () => {
if (!videoRef.current) return;
const current = videoRef.current.currentTime;
const dur = videoRef.current.duration;
setCurrentTime(current);
if (dur > 0) {
const percentage = Math.round((current / dur) * 100);
setManualProgress(percentage);
if (percentage >= 90 && !isCompleted) {
setIsCompleted(true);
onProgressUpdate(100);
} else {
// Debounce progress updates or update periodically
onProgressUpdate(percentage);
}
}
};
const togglePlay = () => {
if (!videoRef.current) return;
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play().catch(e => console.log('Autoplay blocked'));
}
setIsPlaying(!isPlaying);
};
const handleLoadedMetadata = () => {
if (!videoRef.current) return;
setDuration(videoRef.current.duration);
};
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVol = parseFloat(e.target.value);
setVolume(newVol);
if (videoRef.current) {
videoRef.current.volume = newVol;
}
};
const handleProgressSlider = (e: React.ChangeEvent<HTMLInputElement>) => {
const pct = parseInt(e.target.value);
setManualProgress(pct);
onProgressUpdate(pct);
if (pct >= 90) {
setIsCompleted(true);
} else {
setIsCompleted(false);
}
};
const triggerManualCompletion = () => {
setIsCompleted(true);
setManualProgress(100);
onProgressUpdate(100);
};
const formatTime = (timeInSeconds: number) => {
const minutes = Math.floor(timeInSeconds / 60);
const seconds = Math.floor(timeInSeconds % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
};
// YouTube embed parser
const getYoutubeEmbedUrl = (url: string) => {
let videoId = '';
// Handle watch?v= format
if (url.includes('youtube.com/watch?v=')) {
videoId = url.split('v=')[1]?.split('&')[0] || '';
}
// Handle embed format
else if (url.includes('youtube.com/embed/')) {
videoId = url.split('embed/')[1]?.split('?')[0] || '';
}
// Handle share format youtu.be/
else if (url.includes('youtu.be/')) {
videoId = url.split('youtu.be/')[1]?.split('?')[0] || '';
} else {
videoId = url; // assume id was passed directly
}
return `https://www.youtube.com/embed/${videoId}?modestbranding=1&rel=0&showinfo=0&controls=1&iv_load_policy=3&disablekb=1&fs=1`;
};
return (
<div className="space-y-4">
{/* Player Frame Container */}
<div className="relative bg-black aspect-video rounded-xl overflow-hidden border border-white/10 shadow-2xl group/player">
{videoType === 'youtube' ? (
<iframe
key={lessonId}
src={getYoutubeEmbedUrl(videoUrl)}
title="YouTube Player"
className="w-full h-full border-0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>
) : (
<div className="relative w-full h-full flex items-center justify-center">
<video
ref={videoRef}
src={videoUrl}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onClick={togglePlay}
className="w-full h-full object-contain"
/>
{/* Custom Direct Controls Overlay (Visible on Hover) */}
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/90 to-transparent p-4 flex flex-col space-y-2 opacity-0 group-hover/player:opacity-100 transition-opacity duration-300">
{/* Progress Bar */}
<div className="flex items-center space-x-2">
<input
type="range"
min={0}
max={duration || 100}
value={currentTime}
onChange={(e) => {
const time = parseFloat(e.target.value);
if (videoRef.current) videoRef.current.currentTime = time;
setCurrentTime(time);
}}
className="w-full accent-[#E50914] bg-zinc-800 h-1.5 rounded-full cursor-pointer appearance-none"
/>
</div>
{/* Bottom Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button onClick={togglePlay} className="text-white hover:text-[#E50914] transition-colors">
{isPlaying ? <Pause className="w-5 h-5 fill-white" /> : <Play className="w-5 h-5 fill-white" />}
</button>
<span className="text-xs text-zinc-300 font-mono">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Volume2 className="w-4 h-4 text-zinc-400" />
<input
type="range"
min={0}
max={1}
step={0.1}
value={volume}
onChange={handleVolumeChange}
className="w-16 accent-white bg-zinc-850 h-1 rounded-full cursor-pointer"
/>
</div>
<button
onClick={() => videoRef.current?.requestFullscreen()}
className="text-white hover:text-[#E50914] transition-colors"
>
<Maximize className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
)}
</div>
{/* Lesson Progress Status & Actions bar */}
<div className="glass-card p-4 rounded-xl flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center space-x-3 w-full sm:w-auto">
<div className={`p-2 rounded-lg ${isCompleted ? 'bg-emerald-500/10 text-emerald-500' : 'bg-white/5 text-zinc-500'}`}>
<CheckCircle className="w-5 h-5" />
</div>
<div>
<p className="text-sm font-bold text-white leading-tight">
{isCompleted ? 'Aula Concluída!' : 'Aula em Progresso'}
</p>
<p className="text-xs text-zinc-400 font-medium mt-0.5">
Progresso atual: <span className="text-white font-semibold">{manualProgress}%</span>
</p>
</div>
</div>
{/* Manual Progress Slider for YouTube or easy debugging */}
<div className="flex items-center space-x-4 w-full sm:w-auto flex-1 max-w-xs">
<span className="text-[10px] text-zinc-500 font-bold uppercase whitespace-nowrap">Ajustar Progresso</span>
<input
type="range"
min={0}
max={100}
value={manualProgress}
onChange={handleProgressSlider}
className="w-full accent-[#E50914] bg-white/10 h-1.5 rounded-full cursor-pointer"
/>
</div>
<div className="w-full sm:w-auto flex justify-end">
{!isCompleted ? (
<button
onClick={triggerManualCompletion}
className="w-full sm:w-auto glass-btn-primary text-white text-xs font-bold px-4 py-2 rounded-lg flex items-center justify-center space-x-1.5 cursor-pointer"
>
<span>Concluir Aula</span>
</button>
) : (
<span className="text-xs text-emerald-500 font-bold flex items-center space-x-1">
<span> Salvo na Área do Aluno</span>
</span>
)}
</div>
</div>
</div>
);
}