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:
parent
22aac877b9
commit
b751bf93d1
@ -53,6 +53,9 @@ function formatDate(date: Date | string): string {
|
|||||||
|
|
||||||
export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos = [], onVideoChange }: BunnyVideoModalProps) {
|
export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos = [], onVideoChange }: BunnyVideoModalProps) {
|
||||||
const [showShareMenu, setShowShareMenu] = useState(false);
|
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
|
// Navigation functions
|
||||||
const getCurrentVideoIndex = () => {
|
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(() => {
|
useEffect(() => {
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape" && isOpen) {
|
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">
|
<div className="flex-1 flex flex-col lg:flex-row gap-4 min-h-0">
|
||||||
{/* Main video player */}
|
{/* Main video player */}
|
||||||
<div className="flex-1">
|
<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 ? (
|
{video.videoUrlIframe ? (
|
||||||
<iframe
|
<iframe
|
||||||
src={video.videoUrlIframe}
|
src={video.videoUrlIframe}
|
||||||
@ -274,12 +359,19 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
|
|||||||
</div>
|
</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 */}
|
{/* Navigation buttons */}
|
||||||
{videos.length > 1 && (
|
{videos.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigateToVideo('prev')}
|
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"
|
size="sm"
|
||||||
data-testid="button-prev-video"
|
data-testid="button-prev-video"
|
||||||
>
|
>
|
||||||
@ -287,15 +379,25 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigateToVideo('next')}
|
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"
|
size="sm"
|
||||||
data-testid="button-next-video"
|
data-testid="button-next-video"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-6 h-6" />
|
<ChevronRight className="w-6 h-6" />
|
||||||
</Button>
|
</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 */}
|
{/* 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}
|
{getCurrentVideoIndex() + 1} od {videos.length}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user