diff --git a/src/components/products/MediaGallery.tsx b/src/components/products/MediaGallery.tsx index dbe2868b..ffc80ed4 100644 --- a/src/components/products/MediaGallery.tsx +++ b/src/components/products/MediaGallery.tsx @@ -4,9 +4,12 @@ import type { Media } from "@spree/sdk"; import { ZoomIn } from "lucide-react"; import dynamic from "next/dynamic"; import { useTranslations } from "next-intl"; -import { useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { ProductImage } from "@/components/ui/product-image"; +const SWIPE_THRESHOLD_PX = 50; +const SWIPE_MAX_VERTICAL_PX = 75; + /** Tiny 10×10 neutral gray PNG used as a blur placeholder while images load. */ const BLUR_PLACEHOLDER = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIElEQVQYV2P4////MwwMDAxMDAwMDGQJMJCvkGwNZCsEAGebBwVss9lRAAAAAElFTkSuQmCC"; @@ -63,6 +66,47 @@ function MediaGalleryInner({ null, ); + const safeIndex = Math.max(0, Math.min(selectedIndex, images.length - 1)); + + // Horizontal swipe on the main image navigates between media. When a + // swipe is detected we suppress the synthetic click so the lightbox + // doesn't open from the same gesture. + const touchStartRef = useRef<{ x: number; y: number } | null>(null); + const suppressClickRef = useRef(false); + + const handleTouchStart = useCallback((e: React.TouchEvent) => { + const touch = e.touches[0]; + if (!touch) return; + touchStartRef.current = { x: touch.clientX, y: touch.clientY }; + suppressClickRef.current = false; + }, []); + + const handleTouchEnd = useCallback( + (e: React.TouchEvent) => { + const start = touchStartRef.current; + touchStartRef.current = null; + if (!start || images.length <= 1) return; + const touch = e.changedTouches[0]; + if (!touch) return; + const dx = touch.clientX - start.x; + const dy = touch.clientY - start.y; + if ( + Math.abs(dx) < SWIPE_THRESHOLD_PX || + Math.abs(dy) > SWIPE_MAX_VERTICAL_PX + ) { + return; + } + suppressClickRef.current = true; + const nextIndex = + dx < 0 + ? (safeIndex + 1) % images.length + : (safeIndex - 1 + images.length) % images.length; + setSelectedIndex(nextIndex); + setMainImageErrorUrl(null); + }, + [images.length, safeIndex], + ); + if (images.length === 0) { return (
@@ -81,7 +125,6 @@ function MediaGalleryInner({ setMainImageErrorUrl(null); }; - const safeIndex = Math.max(0, Math.min(selectedIndex, images.length - 1)); const selectedImage = images[safeIndex]; const mainImageUrl = getMainImageUrl(selectedImage); const showMainImage = mainImageUrl && mainImageErrorUrl !== mainImageUrl; @@ -91,8 +134,16 @@ function MediaGalleryInner({ {/* Main Image */}