Add interactive video playback controls and progress tracking

Update VideoModal component to include a seekable progress bar, volume slider, time display, and mute functionality. Adds event listeners for timeupdate, durationchange, and volumechange. Implements custom CSS for the volume slider.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/ogtPjnM
This commit is contained in:
sebastjanartic 2025-08-07 10:01:17 +00:00
parent 532651d911
commit dd5d0b5ba1
2 changed files with 115 additions and 11 deletions

View File

@ -63,6 +63,9 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
const [isMuted, setIsMuted] = useState(false);
const [showControls, setShowControls] = useState(true);
const [controlsTimeout, setControlsTimeout] = useState<NodeJS.Timeout | null>(null);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
@ -245,7 +248,13 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
videoElement.addEventListener('play', () => setIsPlaying(true));
videoElement.addEventListener('pause', () => setIsPlaying(false));
videoElement.addEventListener('volumechange', () => setIsMuted(videoElement.muted));
videoElement.addEventListener('volumechange', () => {
setIsMuted(videoElement.muted);
setVolume(videoElement.volume);
});
videoElement.addEventListener('timeupdate', () => setCurrentTime(videoElement.currentTime));
videoElement.addEventListener('loadedmetadata', () => setDuration(videoElement.duration));
videoElement.addEventListener('durationchange', () => setDuration(videoElement.duration));
}
// Cleanup when modal closes
@ -345,6 +354,31 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
return `${window.location.origin}?video=${video.id}`;
};
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (videoRef.current && duration > 0) {
const rect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const newTime = (clickX / rect.width) * duration;
videoRef.current.currentTime = newTime;
setCurrentTime(newTime);
}
};
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value);
if (videoRef.current) {
videoRef.current.volume = newVolume;
setVolume(newVolume);
setIsMuted(newVolume === 0);
}
};
const formatTime = (time: number): string => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(getShareUrl());
@ -434,7 +468,21 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
{/* Bottom Control Bar */}
{showControls && (
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/60 to-transparent p-4">
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/70 to-transparent p-4">
{/* Progress Bar */}
<div className="mb-3">
<div
className="w-full h-2 bg-white/20 rounded-full cursor-pointer hover:h-3 transition-all duration-200"
onClick={handleProgressClick}
data-testid="progress-bar"
>
<div
className="h-full bg-bunny-blue rounded-full transition-all duration-200"
style={{ width: `${duration > 0 ? (currentTime / duration) * 100 : 0}%` }}
/>
</div>
</div>
<div className="flex items-center gap-4">
{/* Play/Pause Button */}
<Button
@ -448,15 +496,32 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
</Button>
{/* Volume Control */}
<Button
onClick={toggleMute}
variant="ghost"
size="icon"
className="text-white hover:text-bunny-blue transition-colors"
data-testid="button-volume"
>
{isMuted ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
</Button>
<div className="flex items-center gap-2">
<Button
onClick={toggleMute}
variant="ghost"
size="icon"
className="text-white hover:text-bunny-blue transition-colors"
data-testid="button-volume"
>
{isMuted ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
</Button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={isMuted ? 0 : volume}
onChange={handleVolumeChange}
className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer slider"
data-testid="volume-slider"
/>
</div>
{/* Time Display */}
<div className="text-white text-sm font-mono" data-testid="time-display">
{formatTime(currentTime)} / {formatTime(duration)}
</div>
{/* Fullscreen Button */}
<Button

View File

@ -93,4 +93,43 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Video player volume slider styles */
.slider {
-webkit-appearance: none;
background: transparent;
cursor: pointer;
}
.slider::-webkit-slider-track {
background: rgba(255, 255, 255, 0.2);
height: 4px;
border-radius: 2px;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
height: 12px;
width: 12px;
border-radius: 50%;
background: hsl(217, 91%, 60%);
cursor: pointer;
border: none;
}
.slider::-moz-range-track {
background: rgba(255, 255, 255, 0.2);
height: 4px;
border-radius: 2px;
border: none;
}
.slider::-moz-range-thumb {
height: 12px;
width: 12px;
border-radius: 50%;
background: hsl(217, 91%, 60%);
cursor: pointer;
border: none;
}
}