Files
smart-electronics-frontend/features/products/components/ProductImageGallery.tsx
@jcarymuhammedow f32e7538e1 fixed some bugs
2026-02-05 19:36:12 +05:00

467 lines
18 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from "react";
import Image from "next/image";
import {
X,
ZoomIn,
ZoomOut,
RotateCw,
RotateCcw,
Maximize2,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { useTranslations } from "next-intl";
interface ProductImageGalleryProps {
images: string[];
productName: string;
noImageText: string;
}
export function ProductImageGallery({
images,
productName,
noImageText,
}: ProductImageGalleryProps) {
const [selectedImage, setSelectedImage] = useState(0);
const [isModalOpen, setIsModalOpen] = useState(false);
const [zoom, setZoom] = useState(1);
const [rotation, setRotation] = useState(0);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const t = useTranslations();
const autoplayTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const modalImageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setSelectedImage(0);
}, [images]);
useEffect(() => {
if (images.length <= 1 || isModalOpen) return;
const startAutoplay = () => {
autoplayTimerRef.current = setInterval(() => {
setSelectedImage((prev) => (prev + 1) % images.length);
}, 3000);
};
startAutoplay();
return () => {
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
};
}, [images.length, isModalOpen]);
useEffect(() => {
if (isModalOpen) {
document.body.style.overflow = "hidden";
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isModalOpen]);
const handleImageSelect = useCallback(
(index: number) => {
setSelectedImage(index);
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
if (images.length > 1 && !isModalOpen) {
autoplayTimerRef.current = setInterval(() => {
setSelectedImage((prev) => (prev + 1) % images.length);
}, 3000);
}
},
[images.length, isModalOpen],
);
const openModal = () => {
setIsModalOpen(true);
resetTransform();
};
const closeModal = () => {
setIsModalOpen(false);
resetTransform();
};
const resetTransform = () => {
setZoom(1);
setRotation(0);
setPosition({ x: 0, y: 0 });
};
const handleZoomIn = () => {
setZoom((prev) => Math.min(prev + 0.25, 5));
};
const handleZoomOut = () => {
setZoom((prev) => Math.max(prev - 0.25, 0.5));
};
const handleRotateClockwise = () => {
setRotation((prev) => (prev + 90) % 360);
};
const handleRotateCounterClockwise = () => {
setRotation((prev) => (prev - 90 + 360) % 360);
};
const handleMouseDown = (e: React.MouseEvent | React.TouchEvent) => {
if (zoom > 1) {
setIsDragging(true);
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
setDragStart({
x: clientX - position.x,
y: clientY - position.y,
});
}
};
const handleMouseMove = (e: React.MouseEvent | React.TouchEvent) => {
if (isDragging && zoom > 1) {
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
setPosition({
x: clientX - dragStart.x,
y: clientY - dragStart.y,
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
const handleWheel = (e: React.WheelEvent) => {
e.preventDefault();
if (e.deltaY < 0) {
handleZoomIn();
} else {
handleZoomOut();
}
};
const handleModalImageChange = (direction: "prev" | "next") => {
if (direction === "next") {
setSelectedImage((prev) => (prev + 1) % images.length);
} else {
setSelectedImage((prev) => (prev - 1 + images.length) % images.length);
}
resetTransform();
};
return (
<>
<div className="w-full lg:flex-1 max-w-2xl">
<div className="relative">
<div
className="relative aspect-square w-full rounded-xl md:rounded-2xl overflow-hidden bg-gradient-to-br from-gray-50 to-gray-100 cursor-pointer group shadow-sm hover:shadow-md transition-all"
onClick={openModal}
>
{images.length > 0 && images[selectedImage] ? (
<>
<Image
src={images[selectedImage]}
alt={productName}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-contain transition-transform group-hover:scale-105"
priority
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 md:p-3 transform translate-y-4 group-hover:translate-y-0 transition-transform">
<Maximize2 className="w-4 h-4 md:w-5 md:h-5 text-gray-800" />
</div>
</div>
</>
) : (
<div className="flex items-center justify-center h-full text-gray-400 text-sm md:text-base">
{noImageText}
</div>
)}
</div>
{images.length > 1 && (
<div className="mt-4 flex gap-2 overflow-x-auto pb-2">
{images.map((image, index) => (
<button
key={index}
onClick={() => handleImageSelect(index)}
className={`relative w-16 h-16 shrink-0 rounded cursor-pointer overflow-hidden border-2 transition-all ${
selectedImage === index
? "border-primary ring-2 ring-primary/20"
: "border-gray-200 hover:border-gray-300"
}`}
>
<Image
src={image}
alt={`${productName} ${index + 1}`}
fill
className="object-cover"
/>
</button>
))}
</div>
)}
</div>
</div>
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-99 bg-gradient-to-br from-gray-900/95 via-gray-800/95 to-gray-900/95 backdrop-blur-xl flex flex-col">
{/* Top Bar */}
<div className="absolute top-0 left-0 right-0 p-3 md:p-4 z-20 bg-gradient-to-b from-black/20 to-transparent">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-2 md:gap-3 min-w-0 flex-1">
<div className="w-1 h-4 md:h-6 bg-blue-500 rounded-full shrink-0" />
<span className="text-white font-medium text-sm md:text-base truncate">
{productName}
</span>
</div>
<button
onClick={closeModal}
className="p-2 md:p-2.5 bg-white/10 hover:bg-white/20 rounded-lg md:rounded-xl transition-all backdrop-blur-sm border border-white/10 shrink-0 ml-2"
aria-label="Close"
>
<X className="w-4 h-4 md:w-5 md:h-5 text-white" />
</button>
</div>
</div>
{/* Main Image Area */}
<div className="flex-1 flex items-center justify-center relative px-2 md:px-16 lg:px-20">
{/* Left Arrow - Desktop */}
{images.length > 1 && (
<button
onClick={() => handleModalImageChange("prev")}
className="hidden md:flex absolute left-3 lg:left-6 p-2.5 md:p-3 bg-white/10 hover:bg-white/20 rounded-xl md:rounded-2xl transition-all backdrop-blur-md border border-white/10 hover:scale-110 z-10 group"
aria-label="Previous image"
>
<ChevronLeft className="w-5 h-5 md:w-6 md:h-6 text-white" />
</button>
)}
{/* Image Container */}
<div
ref={modalImageRef}
className="w-full h-full flex items-center justify-center overflow-hidden"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onWheel={handleWheel}
style={{
cursor:
zoom > 1 ? (isDragging ? "grabbing" : "grab") : "default",
}}
>
<div
style={{
transform: `translate(${position.x}px, ${position.y}px) scale(${zoom}) rotate(${rotation}deg)`,
transition: isDragging
? "none"
: "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
}}
className="relative w-[90vw] h-[60vh] md:w-[75vw] md:h-[70vh]"
>
<Image
src={images[selectedImage]}
alt={productName}
fill
className="object-contain pointer-events-none select-none"
priority
draggable={false}
/>
</div>
</div>
{/* Right Arrow - Desktop */}
{images.length > 1 && (
<button
onClick={() => handleModalImageChange("next")}
className="hidden md:flex absolute right-3 lg:right-6 p-2.5 md:p-3 bg-white/10 hover:bg-white/20 rounded-xl md:rounded-2xl transition-all backdrop-blur-md border border-white/10 hover:scale-110 z-10 group"
aria-label="Next image"
>
<ChevronRight className="w-5 h-5 md:w-6 md:h-6 text-white" />
</button>
)}
</div>
{/* Bottom Control Bar */}
<div className="bg-gradient-to-t from-black/40 via-black/20 to-transparent backdrop-blur-xl border-t border-white/10">
<div className="max-w-7xl mx-auto px-3 md:px-6 py-3 md:py-4">
{/* Mobile Layout */}
<div className="md:hidden flex flex-col gap-2">
{/* Row 1: Navigation */}
{images.length > 1 && (
<div className="flex items-center justify-between gap-2">
<button
onClick={() => handleModalImageChange("prev")}
className="flex-1 p-2 bg-white/10 hover:bg-white/20 rounded-lg transition-all border border-white/10"
aria-label="Previous"
>
<ChevronLeft className="w-5 h-5 text-white mx-auto" />
</button>
<div className="px-4 py-2 bg-white/10 backdrop-blur-md rounded-lg border border-white/10">
<span className="text-white text-sm font-medium whitespace-nowrap">
{selectedImage + 1} / {images.length}
</span>
</div>
<button
onClick={() => handleModalImageChange("next")}
className="flex-1 p-2 bg-white/10 hover:bg-white/20 rounded-lg transition-all border border-white/10"
aria-label="Next"
>
<ChevronRight className="w-5 h-5 text-white mx-auto" />
</button>
</div>
)}
{/* Row 2: Zoom & Rotate */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 bg-white/10 backdrop-blur-md rounded-lg p-1 border border-white/10 flex-1">
<button
onClick={handleZoomOut}
className="p-2 hover:bg-white/20 rounded-md transition-all flex-1"
aria-label="Zoom out"
>
<ZoomOut className="w-4 h-4 text-white mx-auto" />
</button>
<div className="px-2 py-1 bg-white/10 rounded text-center min-w-[50px]">
<span className="text-white text-xs font-medium">
{Math.round(zoom * 100)}%
</span>
</div>
<button
onClick={handleZoomIn}
className="p-2 hover:bg-white/20 rounded-md transition-all flex-1"
aria-label="Zoom in"
>
<ZoomIn className="w-4 h-4 text-white mx-auto" />
</button>
</div>
<div className="flex items-center gap-1 bg-white/10 backdrop-blur-md rounded-lg p-1 border border-white/10">
<button
onClick={handleRotateCounterClockwise}
className="p-2 hover:bg-white/20 rounded-md transition-all"
aria-label="Rotate counter-clockwise"
>
<RotateCcw className="w-4 h-4 text-white" />
</button>
<button
onClick={handleRotateClockwise}
className="p-2 hover:bg-white/20 rounded-md transition-all"
aria-label="Rotate clockwise"
>
<RotateCw className="w-4 h-4 text-white" />
</button>
</div>
<button
onClick={resetTransform}
className="px-3 py-2 bg-white/10 hover:bg-white/20 rounded-lg transition-all text-white text-xs font-medium border border-white/10"
aria-label="Reset view"
>
{t("reset")}
</button>
</div>
</div>
{/* Desktop Layout */}
<div className="hidden md:flex items-center justify-center gap-2">
<button
onClick={() => handleModalImageChange("prev")}
disabled={images.length <= 1}
className="p-2 bg-white/10 hover:bg-white/20 rounded-lg transition-all disabled:opacity-30 disabled:cursor-not-allowed border border-white/10"
aria-label="Previous"
>
<ChevronLeft className="w-4 h-4 text-white" />
</button>
<div className="flex items-center gap-1.5 bg-white/10 backdrop-blur-md rounded-lg p-1 border border-white/10">
<button
onClick={handleZoomOut}
className="p-2 hover:bg-white/20 rounded-md transition-all"
aria-label="Zoom out"
>
<ZoomOut className="w-4 h-4 text-white" />
</button>
<div className="px-3 py-1 bg-white/10 rounded min-w-[60px] text-center">
<span className="text-white text-sm font-medium">
{Math.round(zoom * 100)}%
</span>
</div>
<button
onClick={handleZoomIn}
className="p-2 hover:bg-white/20 rounded-md transition-all"
aria-label="Zoom in"
>
<ZoomIn className="w-4 h-4 text-white" />
</button>
</div>
<div className="w-px h-8 bg-white/20" />
<div className="flex items-center gap-1.5 bg-white/10 backdrop-blur-md rounded-lg p-1 border border-white/10">
<button
onClick={handleRotateCounterClockwise}
className="p-2 hover:bg-white/20 rounded-md transition-all"
aria-label="Rotate counter-clockwise"
>
<RotateCcw className="w-4 h-4 text-white" />
</button>
<button
onClick={handleRotateClockwise}
className="p-2 hover:bg-white/20 rounded-md transition-all"
aria-label="Rotate clockwise"
>
<RotateCw className="w-4 h-4 text-white" />
</button>
</div>
<button
onClick={resetTransform}
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg transition-all text-white text-sm font-medium border border-white/10"
aria-label="Reset view"
>
Reset
</button>
<div className="w-px h-8 bg-white/20" />
<button
onClick={() => handleModalImageChange("next")}
disabled={images.length <= 1}
className="p-2 bg-white/10 hover:bg-white/20 rounded-lg transition-all disabled:opacity-30 disabled:cursor-not-allowed border border-white/10"
aria-label="Next"
>
<ChevronRight className="w-4 h-4 text-white" />
</button>
{images.length > 1 && (
<>
<div className="w-px h-8 bg-white/20" />
<div className="px-4 py-2 bg-white/10 backdrop-blur-md rounded-lg border border-white/10">
<span className="text-white text-sm font-medium">
{selectedImage + 1} / {images.length}
</span>
</div>
</>
)}
</div>
</div>
</div>
</div>
)}
</>
);
}