image gallery modal

This commit is contained in:
hamid zarghami
2025-12-09 15:48:52 +03:30
parent 4a59d560eb
commit 1e97e2ec47
4 changed files with 231 additions and 4 deletions
+2 -1
View File
@@ -11,10 +11,11 @@ const Categories = () => {
const parentCategories = data?.results?.data?.filter(cat => !cat.parent) || [] const parentCategories = data?.results?.data?.filter(cat => !cat.parent) || []
return ( return (
<div className='w-full'> <div className='w-full flex justify-center'>
<Swiper <Swiper
spaceBetween={16} spaceBetween={16}
slidesPerView={'auto'} slidesPerView={'auto'}
centeredSlides={true}
className='h-full' className='h-full'
breakpoints={{ breakpoints={{
640: { 640: {
+1 -1
View File
@@ -87,7 +87,7 @@ const ActionProduct: FC<ActionProductProps> = ({ product }) => {
} }
return ( return (
<div className='absolute top-6 right-4'> <div className='absolute top-6 right-4' onClick={(e) => e.stopPropagation()}>
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<button <button
onClick={handleWishlist} onClick={handleWishlist}
@@ -0,0 +1,212 @@
'use client'
import { FC, useEffect, useState, useRef, MouseEvent } from 'react'
import Image from 'next/image'
import { CloseCircle, ArrowLeft2, ArrowRight2, SearchZoomIn, SearchZoomOut } from 'iconsax-react'
interface ImageGalleryModalProps {
images: string[]
currentIndex: number
productName: string
isOpen: boolean
onClose: () => void
onIndexChange: (index: number) => void
}
const ImageGalleryModal: FC<ImageGalleryModalProps> = ({
images,
currentIndex,
productName,
isOpen,
onClose,
onIndexChange,
}) => {
const [isZoomed, setIsZoomed] = useState(false)
const [position, setPosition] = useState({ x: 50, y: 50 })
const imageContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
return () => {
document.body.style.overflow = 'unset'
}
}, [isOpen])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen) return
if (e.key === 'Escape') {
if (isZoomed) {
setIsZoomed(false)
} else {
onClose()
}
} else if (e.key === 'ArrowLeft' && !isZoomed) {
handleNext()
} else if (e.key === 'ArrowRight' && !isZoomed) {
handlePrev()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, currentIndex, isZoomed])
useEffect(() => {
setIsZoomed(false)
}, [currentIndex])
const handlePrev = () => {
const newIndex = currentIndex === 0 ? images.length - 1 : currentIndex - 1
onIndexChange(newIndex)
}
const handleNext = () => {
const newIndex = currentIndex === images.length - 1 ? 0 : currentIndex + 1
onIndexChange(newIndex)
}
const toggleZoom = () => {
setIsZoomed(!isZoomed)
}
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (!isZoomed || !imageContainerRef.current) return
const rect = imageContainerRef.current.getBoundingClientRect()
const x = ((e.clientX - rect.left) / rect.width) * 100
const y = ((e.clientY - rect.top) / rect.height) * 100
setPosition({ x: Math.max(0, Math.min(100, x)), y: Math.max(0, Math.min(100, y)) })
}
if (!isOpen) return null
return (
<div
className='fixed top-0 z-[99999] inset-0 bg-black/80 flex flex-col'
onClick={onClose}
>
{/* Header */}
<div className='flex justify-between items-center p-4 sm:p-6'>
<div className='flex items-center gap-3'>
<button
onClick={onClose}
className='text-white hover:text-gray-300 transition-colors'
aria-label='بستن'
>
<CloseCircle color='white' size={32} />
</button>
<button
onClick={toggleZoom}
className='text-white hover:text-gray-300 transition-colors hidden sm:block'
aria-label={isZoomed ? 'خروج از حالت زوم' : 'بزرگنمایی'}
>
{isZoomed ? (
<SearchZoomOut color='white' size={28} />
) : (
<SearchZoomIn color='white' size={28} />
)}
</button>
</div>
<div className='text-white text-sm sm:text-base'>
{currentIndex + 1} / {images.length}
</div>
</div>
{/* Main Image */}
<div className='flex-1 flex items-center justify-center px-4 sm:px-16 relative'>
<div
ref={imageContainerRef}
className={`relative w-full h-full max-w-4xl max-h-[60vh] sm:max-h-[70vh] overflow-hidden ${isZoomed ? 'cursor-zoom-out' : 'cursor-zoom-in'
}`}
onClick={(e) => {
e.stopPropagation()
toggleZoom()
}}
onMouseMove={handleMouseMove}
>
<div
className='w-full h-full transition-transform duration-200 ease-out'
style={{
transform: isZoomed ? 'scale(2.5)' : 'scale(1)',
transformOrigin: `${position.x}% ${position.y}%`,
}}
>
<Image
src={images[currentIndex]}
alt={`${productName} - تصویر ${currentIndex + 1}`}
fill
className='object-contain'
priority
/>
</div>
</div>
{/* Navigation Buttons */}
{images.length > 1 && !isZoomed && (
<>
<button
onClick={(e) => {
e.stopPropagation()
handlePrev()
}}
className='absolute left-4 sm:left-8 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/20 text-white rounded-full p-2 sm:p-3 transition-all backdrop-blur-sm'
aria-label='تصویر قبلی'
>
<ArrowLeft2 color='white' size={24} />
</button>
<button
onClick={(e) => {
e.stopPropagation()
handleNext()
}}
className='absolute right-4 sm:right-8 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/20 text-white rounded-full p-2 sm:p-3 transition-all backdrop-blur-sm'
aria-label='تصویر بعدی'
>
<ArrowRight2 color='white' size={24} />
</button>
</>
)}
</div>
{/* راهنما */}
{isZoomed && (
<div className='text-center text-white/80 text-xs sm:text-sm pb-2'>
برای خروج از حالت زوم ESC یا دوباره کلیک کنید
</div>
)}
{/* Thumbnails */}
<div
className='p-4 sm:p-6 flex gap-2 sm:gap-3 justify-center overflow-x-auto'
onClick={(e) => e.stopPropagation()}
>
{images.map((image, index) => (
<div
key={index}
className={`size-14 sm:size-16 md:size-20 shrink-0 rounded-lg overflow-hidden cursor-pointer transition-all border-2 ${currentIndex === index
? 'border-primary ring-2 ring-primary/50'
: 'border-white/30 hover:border-white/60'
}`}
onClick={() => onIndexChange(index)}
>
<Image
src={image}
width={80}
height={80}
alt={`${productName} - تصویر ${index + 1}`}
className='w-full h-full object-contain bg-white/5'
/>
</div>
))}
</div>
</div>
)
}
export default ImageGalleryModal
+16 -2
View File
@@ -4,6 +4,7 @@ import { FC, useState } from 'react'
import ActionProduct from './ActionProduct' import ActionProduct from './ActionProduct'
import { Product } from '@/types/product.types' import { Product } from '@/types/product.types'
import ModalReport from './ModalReport' import ModalReport from './ModalReport'
import ImageGalleryModal from './ImageGalleryModal'
interface ProductImageProps { interface ProductImageProps {
product: Product product: Product
@@ -11,19 +12,23 @@ interface ProductImageProps {
const ProductImage: FC<ProductImageProps> = ({ product }) => { const ProductImage: FC<ProductImageProps> = ({ product }) => {
const [selectedImageIndex, setSelectedImageIndex] = useState(0) const [selectedImageIndex, setSelectedImageIndex] = useState(0)
const [isGalleryOpen, setIsGalleryOpen] = useState(false)
const allImages = [product.imagesUrl.cover, ...product.imagesUrl.list] const allImages = [product.imagesUrl.cover, ...product.imagesUrl.list]
const selectedImage = allImages[selectedImageIndex] const selectedImage = allImages[selectedImageIndex]
return ( return (
<div className='w-full max-w-[380px] mx-auto lg:mx-0'> <div className='w-full max-w-[380px] mx-auto lg:mx-0'>
<div className='w-full border border-border rounded-xl h-[300px] sm:h-[350px] md:h-[400px] flex justify-center items-center relative'> <div
className='w-full border border-border rounded-xl h-[300px] sm:h-[350px] md:h-[400px] flex justify-center items-center relative cursor-pointer group'
onClick={() => setIsGalleryOpen(true)}
>
<Image <Image
src={selectedImage} src={selectedImage}
width={300} width={300}
height={300} height={300}
alt={product.title_fa} alt={product.title_fa}
className='max-w-[200px] max-h-[200px] sm:max-w-[240px] sm:max-h-[240px] object-contain' className='max-w-[200px] max-h-[200px] sm:max-w-[240px] sm:max-h-[240px] object-contain transition-transform group-hover:scale-105'
/> />
<ActionProduct product={product} /> <ActionProduct product={product} />
</div> </div>
@@ -48,6 +53,15 @@ const ProductImage: FC<ProductImageProps> = ({ product }) => {
))} ))}
</div> </div>
<ModalReport productId={product._id} productName={product.title_fa} /> <ModalReport productId={product._id} productName={product.title_fa} />
<ImageGalleryModal
images={allImages}
currentIndex={selectedImageIndex}
productName={product.title_fa}
isOpen={isGalleryOpen}
onClose={() => setIsGalleryOpen(false)}
onIndexChange={setSelectedImageIndex}
/>
</div> </div>
) )
} }