From db68bf9c3d78a0cf023c4e6118dc9ca116574907 Mon Sep 17 00:00:00 2001 From: Jelaletdin12 Date: Sat, 20 Dec 2025 13:36:34 +0500 Subject: [PATCH] Added zoom function to product detail --- .../imageCarousel/ImageCarousel.module.scss | 167 +++++++- .../ProductCard/imageCarousel/index.jsx | 358 +++++++++++++++++- .../ProductDetail/ProductPage.module.scss | 2 +- 3 files changed, 494 insertions(+), 33 deletions(-) diff --git a/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss b/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss index 21a34b4..2b2ad8c 100644 --- a/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss +++ b/src/components/ProductCard/imageCarousel/ImageCarousel.module.scss @@ -10,14 +10,13 @@ width: 99%; height: auto; object-fit: contain; - // transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; } /* Fixed frame container to prevent layout shifts */ .fixedFrameContainer { position: relative; width: 100%; - aspect-ratio: 1; /* Square container */ + aspect-ratio: 1; display: flex; align-items: center; justify-content: center; @@ -28,7 +27,7 @@ /* Style for product card (non-detail view) images */ .cardImage { width: auto; - height: 300px; /* Increased height for card view */ + height: 300px; max-width: 100%; margin: auto; object-fit: contain; @@ -64,11 +63,6 @@ justify-content: center; } -// .transitioning img { -// opacity: 0.8; -// transition: opacity 0.3s ease-in-out; -// } - .arrowButton { position: absolute; top: 50%; @@ -96,7 +90,6 @@ } } -/* Fixed positions for arrows - they won't move with image size changes */ .leftArrow { left: 8px; } @@ -105,7 +98,6 @@ right: 8px; } -/* Make sure the carousel wrapper has a defined position */ .carouselWrapper { position: relative; width: 100%; @@ -134,6 +126,7 @@ border-radius: 50%; background-color: rgba(200, 200, 200, 0.7); transition: background-color 0.2s ease; + cursor: pointer; &:hover { background-color: rgba(255, 255, 255, 0.8); @@ -180,6 +173,125 @@ border: 2px solid #ff6b00; } +/* Modal Styles */ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.modalContent { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.closeButton { + position: absolute; + top: 20px; + right: 20px; + width: 40px; + height: 40px; + background-color: rgba(0, 0, 0, 0.7); + border: none; + border-radius: 50%; + font-size: 24px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 10001; + transition: all 0.2s ease; + color: #333; + + &:hover { + background-color: grey; + transform: scale(1.1); + } +} + +.modalImageContainer { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + overflow: hidden; + user-select: none; +} + +.modalImage { + max-width: 90%; + max-height: 80vh; + object-fit: contain; + transition: transform 0.3s ease; + user-select: none; + -webkit-user-drag: none; +} + +.modalControls { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 8px; + background-color: rgba(0, 0, 0, 0.7); + padding: 10px; + border-radius: 50px; + backdrop-filter: blur(10px); + z-index: 10001; +} + +.controlButton { + width: 30px; + height: 30px; + background-color: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + color: white; + + &:hover { + background-color: rgba(255, 255, 255, 0.25); + transform: scale(1.1); + } + + &:active { + transform: scale(0.95); + } + + svg { + width: 20px; + height: 20px; + } +} + /* Responsive styling */ @media (max-width: 768px) { .thumbnailContainer { @@ -202,6 +314,37 @@ } .cardImage { - height: 180px; /* Adjusted height for mobile view */ + height: 180px; } -} + + .closeButton { + top: 10px; + right: 10px; + width: 36px; + height: 36px; + font-size: 20px; + } + + .modalControls { + bottom: 10px; + padding: 8px; + gap: 6px; + // flex-wrap: wrap; + max-width: 90%; + } + + .controlButton { + width: 28px; + height: 28px; + + svg { + width: 18px; + height: 18px; + } + } + + .modalImage { + max-width: 95%; + max-height: 70vh; + } +} \ No newline at end of file diff --git a/src/components/ProductCard/imageCarousel/index.jsx b/src/components/ProductCard/imageCarousel/index.jsx index f84d828..b1d31f3 100644 --- a/src/components/ProductCard/imageCarousel/index.jsx +++ b/src/components/ProductCard/imageCarousel/index.jsx @@ -5,54 +5,65 @@ const ImageCarousel = ({ images, altText, showThumbnails = false, - isDetailView = false, // Prop to differentiate between card and detail view + isDetailView = false, }) => { const [currentIndex, setCurrentIndex] = useState(0); + const [isModalOpen, setIsModalOpen] = useState(false); + const [scale, setScale] = 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 touchStartX = useRef(0); const touchEndX = useRef(0); const carouselRef = useRef(null); + const modalImageRef = useRef(null); - // Check if there are multiple images const hasMultipleImages = Array.isArray(images) && images.length > 1; - // Get current image URL const currentImage = hasMultipleImages && images[currentIndex] ? images[currentIndex].images_400x400 : images[0]?.images_400x400 || ""; - // Auto-slide functionality - every 9 seconds + // Auto-slide functionality useEffect(() => { - if (!hasMultipleImages) return; + if (!hasMultipleImages || isModalOpen) return; const interval = setInterval(() => { setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); }, 9000); return () => clearInterval(interval); - }, [hasMultipleImages, images]); + }, [hasMultipleImages, images, isModalOpen]); + + // Reset zoom/rotation when modal closes or image changes + useEffect(() => { + if (!isModalOpen) { + setScale(1); + setRotation(0); + setPosition({ x: 0, y: 0 }); + } + }, [isModalOpen, currentIndex]); - // Navigate to previous image const handlePrev = (e) => { if (e) e.stopPropagation(); if (!hasMultipleImages) return; setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1)); }; - // Navigate to next image const handleNext = (e) => { if (e) e.stopPropagation(); if (!hasMultipleImages) return; setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); }; - // Handle thumbnail click const handleThumbnailClick = (index, e) => { if (e) e.stopPropagation(); setCurrentIndex(index); }; - // Touch event handlers const handleTouchStart = (e) => { touchStartX.current = e.touches[0].clientX; }; @@ -66,19 +77,15 @@ const ImageCarousel = ({ const touchDiff = touchStartX.current - touchEndX.current; - // Swipe threshold - only respond to intentional swipes if (Math.abs(touchDiff) > 50) { if (touchDiff > 0) { - // Swipe left -> Next image handleNext(); } else { - // Swipe right -> Previous image handlePrev(); } } }; - // Apply transition effect using CSS useEffect(() => { if (carouselRef.current) { carouselRef.current.classList.add(styles.transitioning); @@ -87,13 +94,122 @@ const ImageCarousel = ({ if (carouselRef.current) { carouselRef.current.classList.remove(styles.transitioning); } - }, 900); // Match this timing with CSS transition duration + }, 900); return () => clearTimeout(timer); } }, [currentIndex]); - // If there's only one image, just show it - applying different classes based on view + // Modal zoom functions + const handleZoomIn = (e) => { + e.stopPropagation(); + setScale((prev) => Math.min(prev + 0.5, 5)); + }; + + const handleZoomOut = (e) => { + e.stopPropagation(); + setScale((prev) => Math.max(prev - 0.5, 0.5)); + }; + + const handleRotateLeft = (e) => { + e.stopPropagation(); + setRotation((prev) => prev - 90); + }; + + const handleRotateRight = (e) => { + e.stopPropagation(); + setRotation((prev) => prev + 90); + }; + + const handleReset = (e) => { + e.stopPropagation(); + setScale(1); + setRotation(0); + setPosition({ x: 0, y: 0 }); + }; + + const handleFullscreen = (e) => { + e.stopPropagation(); + if (modalImageRef.current) { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + modalImageRef.current.requestFullscreen(); + } + } + }; + + // Dragging functions + const handleMouseDown = (e) => { + if (scale > 1) { + setIsDragging(true); + setDragStart({ + x: e.clientX - position.x, + y: e.clientY - position.y, + }); + } + }; + + const handleMouseMove = (e) => { + if (isDragging && scale > 1) { + setPosition({ + x: e.clientX - dragStart.x, + y: e.clientY - dragStart.y, + }); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const openModal = (e) => { + if (e) e.stopPropagation(); + if (isDetailView) { + setIsModalOpen(true); + } + }; + + const closeModal = (e) => { + if (e) e.stopPropagation(); + setIsModalOpen(false); + }; + + // Keyboard controls + useEffect(() => { + if (!isModalOpen) return; + + const handleKeyDown = (e) => { + switch (e.key) { + case "Escape": + closeModal(); + break; + case "ArrowLeft": + handlePrev(); + break; + case "ArrowRight": + handleNext(); + break; + case "+": + case "=": + handleZoomIn(e); + break; + case "-": + handleZoomOut(e); + break; + case "r": + case "R": + handleRotateRight(e); + break; + default: + break; + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isModalOpen]); + if (!hasMultipleImages) { return (
@@ -103,7 +219,208 @@ const ImageCarousel = ({ className={`${styles.productImage} ${ isDetailView ? styles.detailImage : styles.cardImage }`} + onClick={isDetailView ? openModal : undefined} + style={{ cursor: isDetailView ? "pointer" : "default" }} /> + {isDetailView && renderModal()} +
+ ); + } + + function renderModal() { + if (!isModalOpen) return null; + + return ( +
+
e.stopPropagation()} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} + > + + +
+ {altText 1 ? (isDragging ? "grabbing" : "grab") : "default", + }} + draggable={false} + /> +
+ + {/* Control buttons */} +
+ + + + + + + + + + + + + + + +
+
); } @@ -125,6 +442,8 @@ const ImageCarousel = ({ className={`${styles.imageWrapper} ${ isDetailView ? styles.fixedFrameContainer : "" }`} + onClick={isDetailView ? openModal : undefined} + style={{ cursor: isDetailView ? "pointer" : "default" }} > - {/* Navigation arrows */} - {/* Indicators (dots) */}
{images.map((_, idx) => (
- {/* Thumbnails - only show if showThumbnails is true */} {showThumbnails && (
{images.map((image, idx) => ( @@ -205,12 +521,14 @@ const ImageCarousel = ({ src={ image.thumbnail || image.images_400x400 || "/placeholder.svg" } - alt={`${altText || "Ürün"} thumbnail ${idx + 1}`} + alt={`${altText || "Product"} thumbnail ${idx + 1}`} />
))} )} + + {isDetailView && renderModal()} ); }; diff --git a/src/pages/ProductDetail/ProductPage.module.scss b/src/pages/ProductDetail/ProductPage.module.scss index 641a9de..33bc94f 100644 --- a/src/pages/ProductDetail/ProductPage.module.scss +++ b/src/pages/ProductDetail/ProductPage.module.scss @@ -49,7 +49,7 @@ width: 99%; height: auto; object-fit: contain; - border: 1px solid #eee; + // border: 1px solid #eee; @media screen and (max-width: 900px) { height: 100%; }