Added zoom function to product detail

This commit is contained in:
Jelaletdin12
2025-12-20 13:36:34 +05:00
parent 903d6e1f4f
commit db68bf9c3d
3 changed files with 494 additions and 33 deletions

View File

@@ -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;
}
}

View File

@@ -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 (
<div className={isDetailView ? styles.fixedFrameContainer : undefined}>
@@ -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()}
</div>
);
}
function renderModal() {
if (!isModalOpen) return null;
return (
<div className={styles.modalOverlay} onClick={closeModal}>
<div
className={styles.modalContent}
onClick={(e) => e.stopPropagation()}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<button className={styles.closeButton} onClick={closeModal}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e3e3e3"
>
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
</svg>
</button>
<div className={styles.modalImageContainer} ref={modalImageRef}>
<img
src={currentImage || "/placeholder.svg"}
alt={altText || "Ürün resmi"}
className={styles.modalImage}
style={{
transform: `scale(${scale}) rotate(${rotation}deg) translate(${position.x}px, ${position.y}px)`,
cursor:
scale > 1 ? (isDragging ? "grabbing" : "grab") : "default",
}}
draggable={false}
/>
</div>
{/* Control buttons */}
<div className={styles.modalControls}>
<button
className={styles.controlButton}
onClick={handlePrev}
title="Önceki (←)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<button
className={styles.controlButton}
onClick={handleZoomOut}
title="Uzaklaştır (-)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="8" y1="11" x2="14" y2="11"></line>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
<button
className={styles.controlButton}
onClick={handleZoomIn}
title="Yakınlaştır (+)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
<button
className={styles.controlButton}
onClick={handleFullscreen}
title="Tam ekran"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
</svg>
</button>
<button
className={styles.controlButton}
onClick={handleRotateLeft}
title="Sola döndür"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M2.5 2v6h6M2.66 15.57a10 10 0 1 0 .57-8.38"></path>
</svg>
</button>
<button
className={styles.controlButton}
onClick={handleRotateRight}
title="Sağa döndür (R)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38"></path>
</svg>
</button>
<button
className={styles.controlButton}
onClick={handleReset}
title="Sıfırla"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
<path d="M3 21v-5h5"></path>
</svg>
</button>
<button
className={styles.controlButton}
onClick={handleNext}
title="Sonraki (→)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
</div>
</div>
);
}
@@ -125,6 +442,8 @@ const ImageCarousel = ({
className={`${styles.imageWrapper} ${
isDetailView ? styles.fixedFrameContainer : ""
}`}
onClick={isDetailView ? openModal : undefined}
style={{ cursor: isDetailView ? "pointer" : "default" }}
>
<img
src={currentImage || "/placeholder.svg"}
@@ -135,7 +454,6 @@ const ImageCarousel = ({
/>
</div>
{/* Navigation arrows */}
<button
onClick={handlePrev}
className={`${styles.arrowButton} ${styles.leftArrow}`}
@@ -176,7 +494,6 @@ const ImageCarousel = ({
</svg>
</button>
{/* Indicators (dots) */}
<div className={styles.indicators}>
{images.map((_, idx) => (
<span
@@ -190,7 +507,6 @@ const ImageCarousel = ({
</div>
</div>
{/* Thumbnails - only show if showThumbnails is true */}
{showThumbnails && (
<div className={styles.thumbnailContainer}>
{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}`}
/>
</div>
))}
</div>
)}
{isDetailView && renderModal()}
</div>
);
};