image gallery modal
This commit is contained in:
@@ -11,10 +11,11 @@ const Categories = () => {
|
||||
const parentCategories = data?.results?.data?.filter(cat => !cat.parent) || []
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className='w-full flex justify-center'>
|
||||
<Swiper
|
||||
spaceBetween={16}
|
||||
slidesPerView={'auto'}
|
||||
centeredSlides={true}
|
||||
className='h-full'
|
||||
breakpoints={{
|
||||
640: {
|
||||
|
||||
@@ -87,7 +87,7 @@ const ActionProduct: FC<ActionProductProps> = ({ product }) => {
|
||||
}
|
||||
|
||||
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'>
|
||||
<button
|
||||
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
|
||||
@@ -4,6 +4,7 @@ import { FC, useState } from 'react'
|
||||
import ActionProduct from './ActionProduct'
|
||||
import { Product } from '@/types/product.types'
|
||||
import ModalReport from './ModalReport'
|
||||
import ImageGalleryModal from './ImageGalleryModal'
|
||||
|
||||
interface ProductImageProps {
|
||||
product: Product
|
||||
@@ -11,19 +12,23 @@ interface ProductImageProps {
|
||||
|
||||
const ProductImage: FC<ProductImageProps> = ({ product }) => {
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0)
|
||||
const [isGalleryOpen, setIsGalleryOpen] = useState(false)
|
||||
|
||||
const allImages = [product.imagesUrl.cover, ...product.imagesUrl.list]
|
||||
const selectedImage = allImages[selectedImageIndex]
|
||||
|
||||
return (
|
||||
<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
|
||||
src={selectedImage}
|
||||
width={300}
|
||||
height={300}
|
||||
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} />
|
||||
</div>
|
||||
@@ -48,6 +53,15 @@ const ProductImage: FC<ProductImageProps> = ({ product }) => {
|
||||
))}
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user