Add swipe gesture to navigate between videos within the player

Implement touch and mouse drag gestures for seamless video navigation, allowing users to swipe left or right to move to the next or previous video, respectively.

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/aSWZcEZ
This commit is contained in:
sebastjanartic 2025-08-28 20:20:12 +00:00
parent 19c1358391
commit b9fa335300
2 changed files with 136 additions and 2 deletions

View File

@ -14,6 +14,8 @@ interface BunnyVideoModalProps {
isOpen: boolean;
onClose: () => void;
onEdit?: () => void;
videos?: Video[];
onVideoChange?: (video: Video) => void;
}
function formatDuration(seconds: number): string {
@ -49,8 +51,11 @@ function formatDate(date: Date | string): string {
return `Pred ${Math.floor(diffDays / 30)} mesec${Math.floor(diffDays / 30) > 1 ? 'i' : 'em'}`;
}
export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: BunnyVideoModalProps) {
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);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
@ -137,6 +142,100 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: Bunn
setShowShareMenu(false);
};
// Navigation functions
const getCurrentVideoIndex = () => {
if (!video || !videos.length) return -1;
return videos.findIndex(v => v.id === video.id);
};
const navigateToVideo = (direction: 'next' | 'prev') => {
const currentIndex = getCurrentVideoIndex();
if (currentIndex === -1) return;
let newIndex;
if (direction === 'next') {
newIndex = currentIndex + 1 >= videos.length ? 0 : currentIndex + 1;
} else {
newIndex = currentIndex - 1 < 0 ? videos.length - 1 : currentIndex - 1;
}
const newVideo = videos[newIndex];
if (newVideo && onVideoChange) {
onVideoChange(newVideo);
}
};
// 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) => {
if (e.target === e.currentTarget) return; // only on video area
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;
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();
};
if (!isOpen || !video) return null;
return (
@ -231,7 +330,17 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: Bunn
<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 cursor-grab active:cursor-grabbing select-none"
style={{ transform: `translateX(${dragOffset}px)`, transition: isDragging ? 'none' : 'transform 0.3s ease-out' }}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{video.videoUrlIframe ? (
<iframe
src={video.videoUrlIframe}
@ -248,6 +357,25 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: Bunn
</div>
)}
</div>
{/* Navigation indicators */}
{videos.length > 1 && (
<div className="absolute top-1/2 left-4 right-4 flex justify-between items-center pointer-events-none z-10">
<div className={`bg-black bg-opacity-50 rounded-full p-2 text-white transition-opacity ${dragOffset > 50 ? 'opacity-100' : 'opacity-30'}`}>
<span className="text-sm"> Prejšnji</span>
</div>
<div className={`bg-black bg-opacity-50 rounded-full p-2 text-white transition-opacity ${dragOffset < -50 ? 'opacity-100' : 'opacity-30'}`}>
<span className="text-sm">Naslednji </span>
</div>
</div>
)}
{/* Video counter */}
{videos.length > 1 && (
<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">
{getCurrentVideoIndex() + 1} od {videos.length}
</div>
)}
</div>
{/* Video info sidebar */}

View File

@ -27,6 +27,10 @@ export default function VideoGrid({ videos, isLoading, hasMore, onLoadMore, view
setSelectedVideo(null);
};
const handleVideoChange = (video: Video) => {
setSelectedVideo(video);
};
if (isLoading && videos.length === 0) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" data-testid="grid-loading">
@ -99,6 +103,8 @@ export default function VideoGrid({ videos, isLoading, hasMore, onLoadMore, view
video={selectedVideo}
isOpen={isModalOpen}
onClose={handleCloseModal}
videos={videos}
onVideoChange={handleVideoChange}
/>
</>
);