added zoom function, fixed cart quantitu bug
This commit is contained in:
@@ -171,7 +171,7 @@ export default function CartPage() {
|
|||||||
{Object.entries(itemsBySeller).map(
|
{Object.entries(itemsBySeller).map(
|
||||||
([sellerId, { seller, items }]) => (
|
([sellerId, { seller, items }]) => (
|
||||||
<div key={sellerId} className="mb-6">
|
<div key={sellerId} className="mb-6">
|
||||||
<p className="text-base font-semibold mb-3">{seller.name}</p>
|
{/* <p className="text-base font-semibold mb-3">{seller.name}</p> */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const price = parseFloat(
|
const price = parseFloat(
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
const pendingQuantityRef = useRef<number | null>(null);
|
const pendingQuantityRef = useRef<number | null>(null);
|
||||||
const retryCountRef = useRef(0);
|
const retryCountRef = useRef(0);
|
||||||
const retryTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
const retryTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const isInitializedRef = useRef(false); // Track if component has been initialized
|
||||||
|
|
||||||
// Function refs to solve circular dependency
|
// Function refs to solve circular dependency
|
||||||
const syncToServerRef = useRef<((quantity: number) => void) | null>(null);
|
const syncToServerRef = useRef<((quantity: number) => void) | null>(null);
|
||||||
@@ -62,6 +63,10 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
// Initialize from server state
|
// Initialize from server state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalQuantity(item.quantity);
|
setLocalQuantity(item.quantity);
|
||||||
|
// Mark as initialized after first render
|
||||||
|
if (!isInitializedRef.current) {
|
||||||
|
isInitializedRef.current = true;
|
||||||
|
}
|
||||||
}, [item.quantity]);
|
}, [item.quantity]);
|
||||||
|
|
||||||
// Save to sessionStorage
|
// Save to sessionStorage
|
||||||
@@ -81,13 +86,13 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
|
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
PENDING_CART_UPDATES_KEY,
|
PENDING_CART_UPDATES_KEY,
|
||||||
JSON.stringify(pending)
|
JSON.stringify(pending),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save pending update:", error);
|
console.error("Failed to save pending update:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[item.product_id]
|
[item.product_id],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove from sessionStorage
|
// Remove from sessionStorage
|
||||||
@@ -103,7 +108,7 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
} else {
|
} else {
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
PENDING_CART_UPDATES_KEY,
|
PENDING_CART_UPDATES_KEY,
|
||||||
JSON.stringify(pending)
|
JSON.stringify(pending),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +205,7 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
|
|
||||||
retrySyncRef.current?.(quantity);
|
retrySyncRef.current?.(quantity);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -211,13 +216,16 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
removeItem,
|
removeItem,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
clearPendingUpdate,
|
clearPendingUpdate,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update ref
|
// Update ref
|
||||||
syncToServerRef.current = syncToServer;
|
syncToServerRef.current = syncToServer;
|
||||||
|
|
||||||
// Load pending updates from sessionStorage on mount
|
// Load pending updates from sessionStorage on mount
|
||||||
|
// DISABLED: This was causing automatic sync on mount, sending 0 quantity to server
|
||||||
|
// Users should manually refresh or re-add items if there were pending updates
|
||||||
|
/*
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPendingUpdates = () => {
|
const loadPendingUpdates = () => {
|
||||||
try {
|
try {
|
||||||
@@ -246,9 +254,15 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
|
|
||||||
loadPendingUpdates();
|
loadPendingUpdates();
|
||||||
}, [item.product_id, item.quantity]);
|
}, [item.product_id, item.quantity]);
|
||||||
|
*/
|
||||||
|
|
||||||
// Debounced sync
|
// Debounced sync
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Don't sync on initial mount - only sync after user interaction
|
||||||
|
if (!isInitializedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear existing timers
|
// Clear existing timers
|
||||||
if (debounceTimerRef.current) {
|
if (debounceTimerRef.current) {
|
||||||
clearTimeout(debounceTimerRef.current);
|
clearTimeout(debounceTimerRef.current);
|
||||||
@@ -259,6 +273,14 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't sync if quantity is 0 or invalid (unless it's a delete operation)
|
||||||
|
if (localQuantity <= 0 && item.quantity > 0) {
|
||||||
|
// This is a delete operation, allow it
|
||||||
|
} else if (localQuantity <= 0) {
|
||||||
|
// Invalid state, don't sync
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Save to sessionStorage immediately
|
// Save to sessionStorage immediately
|
||||||
savePendingUpdate(localQuantity);
|
savePendingUpdate(localQuantity);
|
||||||
|
|
||||||
@@ -336,14 +358,14 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h3 className="font-semibold text-base">{item.product.name}</h3>
|
<h3 className="font-semibold text-base">{item.product.name}</h3>
|
||||||
<p className="text-sm text-gray-600">
|
{/* <p className="text-sm text-gray-600">
|
||||||
{item.seller?.name || "Store"}
|
{item.seller?.name || "Store"}
|
||||||
</p>
|
</p> */}
|
||||||
{availableStock <= 5 && (
|
{/* {availableStock <= 5 && (
|
||||||
<p className="text-xs text-orange-600 font-medium">
|
<p className="text-xs text-orange-600 font-medium">
|
||||||
{t("only_left", { count: availableStock })}
|
{t("only_left", { count: availableStock })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)} */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { X, ZoomIn, ZoomOut, RotateCw, RotateCcw, Maximize2, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
interface ProductImageGalleryProps {
|
interface ProductImageGalleryProps {
|
||||||
images: string[];
|
images: string[];
|
||||||
productName: string;
|
productName: string;
|
||||||
@@ -13,10 +14,18 @@ export function ProductImageGallery({
|
|||||||
noImageText,
|
noImageText,
|
||||||
}: ProductImageGalleryProps) {
|
}: ProductImageGalleryProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState(0);
|
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 autoplayTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const modalImageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (images.length <= 1) return;
|
if (images.length <= 1 || isModalOpen) return;
|
||||||
|
|
||||||
const startAutoplay = () => {
|
const startAutoplay = () => {
|
||||||
autoplayTimerRef.current = setInterval(() => {
|
autoplayTimerRef.current = setInterval(() => {
|
||||||
@@ -28,35 +37,135 @@ export function ProductImageGallery({
|
|||||||
return () => {
|
return () => {
|
||||||
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
|
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
|
||||||
};
|
};
|
||||||
}, [images.length]);
|
}, [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(
|
const handleImageSelect = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setSelectedImage(index);
|
setSelectedImage(index);
|
||||||
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
|
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
|
||||||
if (images.length > 1) {
|
if (images.length > 1 && !isModalOpen) {
|
||||||
autoplayTimerRef.current = setInterval(() => {
|
autoplayTimerRef.current = setInterval(() => {
|
||||||
setSelectedImage((prev) => (prev + 1) % images.length);
|
setSelectedImage((prev) => (prev + 1) % images.length);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[images.length]
|
[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 (
|
return (
|
||||||
|
<>
|
||||||
<div className="contents max-w-2xl">
|
<div className="contents max-w-2xl">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-white">
|
<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.length > 0 ? (
|
||||||
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={images[selectedImage]}
|
src={images[selectedImage]}
|
||||||
alt={productName}
|
alt={productName}
|
||||||
fill
|
fill
|
||||||
className="object-contain"
|
className="object-contain transition-transform group-hover:scale-105"
|
||||||
priority
|
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">
|
<div className="flex items-center justify-center h-full text-gray-400 text-sm md:text-base">
|
||||||
{noImageText}
|
{noImageText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -86,5 +195,247 @@ export function ProductImageGallery({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
"all_collections_loaded": "Все коллекции загружены"
|
"all_collections_loaded": "Все коллекции загружены"
|
||||||
},
|
},
|
||||||
|
|
||||||
"category": "Категория",
|
"category": "Категория",
|
||||||
"checkout": "Оформить заказ",
|
"checkout": "Оформить заказ",
|
||||||
"price_label": "Цена:",
|
"price_label": "Цена:",
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ const nextConfig: NextConfig = {
|
|||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "http",
|
protocol: "https",
|
||||||
hostname: "shop.post.tm",
|
hostname: "hyzmat.app",
|
||||||
// port: "8080",
|
// port: "8080",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user