Add swipe gesture navigation between videos in modal

Implement touch and mouse drag event handlers for horizontal swiping within the video modal to allow navigation between videos, utilizing Video.js player with HLS.js and Bunny.net CDN integration.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 2eb1084e-b728-4449-9231-f1665924c8d5
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2eb1084e-b728-4449-9231-f1665924c8d5/NpZDdK4
This commit is contained in:
sebastjanartic 2025-08-28 20:26:16 +00:00
parent 22aac877b9
commit b751bf93d1

View File

@ -53,6 +53,9 @@ function formatDate(date: Date | string): string {
export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos = [], onVideoChange }: BunnyVideoModalProps) {
const [showShareMenu, setShowShareMenu] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const [dragOffset, setDragOffset] = useState(0);
// Navigation functions
const getCurrentVideoIndex = () => {
@ -77,6 +80,80 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
}
};
// Touch and mouse drag handlers
const handleDragStart = (clientX: number, clientY: number) => {
setIsDragging(true);
setDragStart({ x: clientX, y: clientY });
setDragOffset(0);
};
const handleDragMove = (clientX: number, clientY: number) => {
if (!isDragging) return;
const deltaX = clientX - dragStart.x;
const deltaY = Math.abs(clientY - dragStart.y);
// Only allow horizontal drag if it's more horizontal than vertical
if (deltaY < Math.abs(deltaX)) {
setDragOffset(deltaX);
}
};
const handleDragEnd = () => {
if (!isDragging) return;
const threshold = 100; // minimum drag distance to trigger navigation
if (Math.abs(dragOffset) > threshold && videos.length > 1) {
if (dragOffset > 0) {
navigateToVideo('prev'); // drag right = previous video
} else {
navigateToVideo('next'); // drag left = next video
}
}
setIsDragging(false);
setDragOffset(0);
};
// Mouse events
const handleMouseDown = (e: React.MouseEvent) => {
// Allow drag on video area but not on buttons
if ((e.target as Element).closest('button')) return;
handleDragStart(e.clientX, e.clientY);
};
const handleMouseMove = (e: React.MouseEvent) => {
handleDragMove(e.clientX, e.clientY);
};
const handleMouseUp = () => {
handleDragEnd();
};
// Touch events
const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches.length !== 1) return;
// Allow drag on video area but not on buttons
if ((e.target as Element).closest('button')) return;
const touch = e.touches[0];
handleDragStart(touch.clientX, touch.clientY);
e.preventDefault();
};
const handleTouchMove = (e: React.TouchEvent) => {
if (e.touches.length !== 1) return;
if (!isDragging) return;
const touch = e.touches[0];
handleDragMove(touch.clientX, touch.clientY);
e.preventDefault();
};
const handleTouchEnd = (e: React.TouchEvent) => {
handleDragEnd();
e.preventDefault();
};
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
@ -257,7 +334,15 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
<div className="flex-1 flex flex-col lg:flex-row gap-4 min-h-0">
{/* Main video player */}
<div className="flex-1">
<div className="relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden">
<div
className="relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden select-none"
style={{ transform: `translateX(${dragOffset}px)`, transition: isDragging ? 'none' : 'transform 0.3s ease-out' }}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{video.videoUrlIframe ? (
<iframe
src={video.videoUrlIframe}
@ -274,12 +359,19 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
</div>
)}
{/* Transparent overlay for drag detection */}
<div
className="absolute inset-0 z-10 cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
></div>
{/* Navigation buttons */}
{videos.length > 1 && (
<>
<Button
onClick={() => navigateToVideo('prev')}
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-80 text-white border-none p-2 rounded-full z-10"
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-80 text-white border-none p-2 rounded-full z-20"
size="sm"
data-testid="button-prev-video"
>
@ -287,15 +379,25 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
</Button>
<Button
onClick={() => navigateToVideo('next')}
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-80 text-white border-none p-2 rounded-full z-10"
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-80 text-white border-none p-2 rounded-full z-20"
size="sm"
data-testid="button-next-video"
>
<ChevronRight className="w-6 h-6" />
</Button>
{/* Swipe indicators */}
<div className="absolute top-1/2 left-8 right-8 flex justify-between items-center pointer-events-none z-10">
<div className={`bg-black bg-opacity-70 rounded-full p-3 text-white transition-opacity ${dragOffset > 50 ? 'opacity-100' : 'opacity-0'}`}>
<span className="text-sm font-medium"> Prejšnji</span>
</div>
<div className={`bg-black bg-opacity-70 rounded-full p-3 text-white transition-opacity ${dragOffset < -50 ? 'opacity-100' : 'opacity-0'}`}>
<span className="text-sm font-medium">Naslednji </span>
</div>
</div>
{/* Video counter */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 rounded-full px-3 py-1 text-white text-sm pointer-events-none">
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 rounded-full px-3 py-1 text-white text-sm pointer-events-none z-10">
{getCurrentVideoIndex() + 1} od {videos.length}
</div>
</>