fixed order, image carousel

This commit is contained in:
@jcarymuhammedow
2026-02-05 19:01:57 +05:00
parent b546deeac0
commit bf5980e3b3
12 changed files with 392 additions and 155 deletions

View File

@@ -40,7 +40,7 @@ interface OrderSummaryProps {
deliveryType: DeliveryType;
selectedRegion: string;
selectedProvince: number | null;
note: string;
notes: string;
regionGroups: Record<string, Province[]>;
availableRegions: string[];
paymentTypes: PaymentType[];
@@ -54,7 +54,7 @@ interface OrderSummaryProps {
onDeliveryTypeChange: (type: DeliveryType) => void;
onRegionChange: (regionCode: string) => void;
onProvinceChange: (provinceId: number) => void;
onNoteChange: (note: string) => void;
onNoteChange: (notes: string) => void;
onCompleteOrder: () => void;
isLoading: boolean;
}
@@ -65,7 +65,7 @@ export default function OrderSummary({
deliveryType,
selectedRegion,
selectedProvince,
note,
notes,
regionGroups,
availableRegions,
paymentTypes,
@@ -303,7 +303,7 @@ export default function OrderSummary({
<div className="mb-6">
<Label className="text-lg font-semibold mb-3 block">{t("note")}</Label>
<Textarea
value={note}
value={notes}
onChange={(e) => onNoteChange(e.target.value)}
className="rounded-xl resize-none"
rows={3}

View File

@@ -151,7 +151,7 @@ export function useAddToCart() {
pendingUpdates.forEach((pendingQty, pendingId) => {
const idx = updated.data.findIndex(
(item: any) => item.product?.id === pendingId
(item: any) => item.product?.id === pendingId,
);
if (idx !== -1) {
updated.data[idx] = {
@@ -162,7 +162,7 @@ export function useAddToCart() {
});
const existingItem = updated.data.find(
(item: any) => item.product?.id === productId
(item: any) => item.product?.id === productId,
);
if (existingItem) {
@@ -172,7 +172,7 @@ export function useAddToCart() {
...item,
product_quantity: item.product_quantity + quantity,
}
: item
: item,
);
} else {
updated.data = [
@@ -185,7 +185,7 @@ export function useAddToCart() {
}
const finalItem = updated.data.find(
(item: any) => item.product?.id === productId
(item: any) => item.product?.id === productId,
);
if (finalItem) {
pendingUpdates.set(productId, finalItem.product_quantity);
@@ -261,7 +261,7 @@ export function useRemoveFromCart() {
pendingUpdates.forEach((pendingQty, pendingId) => {
if (pendingId !== productId) {
const idx = updated.data.findIndex(
(item: any) => item.product?.id === pendingId
(item: any) => item.product?.id === pendingId,
);
if (idx !== -1) {
updated.data[idx] = {
@@ -273,7 +273,7 @@ export function useRemoveFromCart() {
});
updated.data = updated.data.filter(
(item: any) => item.product?.id !== productId
(item: any) => item.product?.id !== productId,
);
pendingUpdates.delete(productId);
@@ -413,7 +413,7 @@ export function useUpdateCartItemQuantity() {
pendingUpdates.forEach((pendingQty, pendingId) => {
const idx = updated.data.findIndex(
(item: any) => item.product?.id === pendingId
(item: any) => item.product?.id === pendingId,
);
if (idx !== -1) {
updated.data[idx] = {
@@ -426,7 +426,7 @@ export function useUpdateCartItemQuantity() {
updated.data = updated.data.map((item: any) =>
item.product?.id === productId
? { ...item, product_quantity: quantity }
: item
: item,
);
pendingUpdates.set(productId, quantity);
@@ -470,14 +470,16 @@ export function useCreateOrder() {
delivery_time?: string;
delivery_at?: string;
region: string;
note?: string;
notes?: string;
}) => {
const response = await apiClient.post("/orders", payload);
return response.data;
},
onSuccess: (data) => {
if (data && data.payment_url) {
window.open(data.payment_url, '_blank')?.focus();
// Handle payment URL - check both data.payment_url and data.data.payment_url
const paymentUrl = data?.data?.payment_url || data?.payment_url;
if (paymentUrl) {
window.open(paymentUrl, "_blank")?.focus();
}
pendingUpdates.clear();
@@ -491,7 +493,7 @@ export function useCreateOrder() {
onError: (error: any) => {
console.error(
"Create order error:",
error.response?.data?.message || error.message
error.response?.data?.message || error.message,
);
},
});
@@ -502,7 +504,7 @@ export function useCartCount() {
return (
data?.data?.reduce(
(sum: number, item: any) => sum + (item.product_quantity || 0),
0
0,
) || 0
);
}

View File

@@ -1,6 +1,15 @@
import { useState, useEffect, useRef, useCallback } from "react";
import Image from "next/image";
import { X, ZoomIn, ZoomOut, RotateCw, RotateCcw, Maximize2, ChevronLeft, ChevronRight } from "lucide-react";
import {
X,
ZoomIn,
ZoomOut,
RotateCw,
RotateCcw,
Maximize2,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { useTranslations } from "next-intl";
interface ProductImageGalleryProps {
images: string[];
@@ -20,10 +29,14 @@ export function ProductImageGallery({
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 t = useTranslations();
const autoplayTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const modalImageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setSelectedImage(0);
}, [images]);
useEffect(() => {
if (images.length <= 1 || isModalOpen) return;
@@ -61,7 +74,7 @@ export function ProductImageGallery({
}, 3000);
}
},
[images.length, isModalOpen]
[images.length, isModalOpen],
);
const openModal = () => {
@@ -99,8 +112,8 @@ export function ProductImageGallery({
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;
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,
@@ -110,8 +123,8 @@ export function ProductImageGallery({
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;
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,
@@ -143,18 +156,19 @@ export function ProductImageGallery({
return (
<>
<div className="contents max-w-2xl">
<div className="w-full lg:flex-1 max-w-2xl">
<div className="relative">
<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 && images[selectedImage] ? (
<>
<Image
src={images[selectedImage]}
alt={productName}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-contain transition-transform group-hover:scale-105"
priority
/>
@@ -173,25 +187,25 @@ export function ProductImageGallery({
{images.length > 1 && (
<div className="mt-4 flex gap-2 overflow-x-auto pb-2">
{images.map((image, index) => (
<button
key={index}
onClick={() => handleImageSelect(index)}
className={`relative w-16 h-16 shrink-0 rounded cursor-pointer overflow-hidden border-2 transition-all ${
selectedImage === index
? "border-primary ring-2 ring-primary/20"
: "border-gray-200 hover:border-gray-300"
}`}
>
<Image
src={image}
alt={`${productName} ${index + 1}`}
fill
className="object-cover"
/>
</button>
))}
</div>
{images.map((image, index) => (
<button
key={index}
onClick={() => handleImageSelect(index)}
className={`relative w-16 h-16 shrink-0 rounded cursor-pointer overflow-hidden border-2 transition-all ${
selectedImage === index
? "border-primary ring-2 ring-primary/20"
: "border-gray-200 hover:border-gray-300"
}`}
>
<Image
src={image}
alt={`${productName} ${index + 1}`}
fill
className="object-cover"
/>
</button>
))}
</div>
)}
</div>
</div>
@@ -204,7 +218,9 @@ export function ProductImageGallery({
<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>
<span className="text-white font-medium text-sm md:text-base truncate">
{productName}
</span>
</div>
<button
onClick={closeModal}
@@ -241,12 +257,17 @@ export function ProductImageGallery({
onTouchMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onWheel={handleWheel}
style={{ cursor: zoom > 1 ? (isDragging ? "grabbing" : "grab") : "default" }}
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)",
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]"
>
@@ -302,7 +323,7 @@ export function ProductImageGallery({
</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">
@@ -314,7 +335,9 @@ export function ProductImageGallery({
<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>
<span className="text-white text-xs font-medium">
{Math.round(zoom * 100)}%
</span>
</div>
<button
onClick={handleZoomIn}
@@ -372,7 +395,9 @@ export function ProductImageGallery({
<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>
<span className="text-white text-sm font-medium">
{Math.round(zoom * 100)}%
</span>
</div>
<button
onClick={handleZoomIn}
@@ -438,4 +463,4 @@ export function ProductImageGallery({
)}
</>
);
}
}

View File

@@ -72,7 +72,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
refetch: refetchProduct,
} = useProductsBySlug(slug);
const { isFavorite, isLoading: isFavLoading } = useIsFavorite(
product?.id || 0
product?.id || 0,
);
const cartOptions = useMemo(
() => ({
@@ -80,7 +80,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
refetchOnWindowFocus: true,
staleTime: 0,
}),
[]
[],
);
const { mutate: toggleFavoriteMutation } = useToggleFavorite();
const {
@@ -100,7 +100,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
const cartItem = useMemo(() => {
const item = cartData?.data?.find(
(item: any) => item.product?.id === product?.id
(item: any) => item.product?.id === product?.id,
);
return item;
@@ -109,19 +109,26 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
const isInCart = !!cartItem;
const availableStock = product?.stock || 0;
const imageUrls = useMemo(
() =>
product?.media?.map(
(m) => m.images_800x800 || m.images_720x720 || m.thumbnail
) || [],
[product]
);
const imageUrls = useMemo(() => {
if (!product?.media || product.media.length === 0) {
return [];
}
const urls = product.media
.map((m) => {
const url = m.images_800x800 || m.images_720x720 || m.images_400x400 || m.thumbnail;
return url;
})
.filter(Boolean);
return urls;
}, [product]);
const reviews = useMemo(() => product?.reviews_resources || [], [product]);
const averageRating = useMemo(
() =>
product?.reviews?.rating ? Number.parseFloat(product.reviews.rating) : 0,
[product]
[product],
);
const transformedRelatedProducts = useMemo(() => {
@@ -173,13 +180,13 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
};
sessionStorage.setItem(
PENDING_PRODUCT_UPDATES_KEY,
JSON.stringify(pending)
JSON.stringify(pending),
);
} catch (error) {
console.error("Failed to save pending update:", error);
}
},
[product?.id]
[product?.id],
);
const clearPendingUpdate = useCallback(() => {
@@ -194,7 +201,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
} else {
sessionStorage.setItem(
PENDING_PRODUCT_UPDATES_KEY,
JSON.stringify(pending)
JSON.stringify(pending),
);
}
}
@@ -225,7 +232,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
syncToServerRef.current?.(quantity);
}, delay);
},
[t]
[t],
);
retrySyncRef.current = retrySync;
@@ -288,7 +295,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
cartItem,
clearPendingUpdate,
t,
]
],
);
syncToServerRef.current = syncToServer;
@@ -401,7 +408,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
toast.success(
data?.wasAdded
? t("added_to_favorites")
: t("removed_from_favorites")
: t("removed_from_favorites"),
);
},
onError: () => {
@@ -409,10 +416,10 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
description: "Try again later",
});
},
}
},
);
},
[product?.id, isFavorite, toggleFavoriteMutation, t]
[product?.id, isFavorite, toggleFavoriteMutation, t],
);
const handleSubmitReview = useCallback(
@@ -442,7 +449,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
});
}
},
[product?.id, submitReviewMutation, refetchProduct, t]
[product?.id, submitReviewMutation, refetchProduct, t],
);
const loadingSkeleton = useMemo(
@@ -464,7 +471,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
</div>
</div>
),
[]
[],
);
if (productLoading) return loadingSkeleton;