diff --git a/features/orders/hooks/useOrders.ts b/features/orders/hooks/useOrders.ts
index 17f0efd..d19d195 100644
--- a/features/orders/hooks/useOrders.ts
+++ b/features/orders/hooks/useOrders.ts
@@ -34,33 +34,33 @@ export function useOrder(id: number | string) {
});
}
-export function useCreateOrder() {
- const queryClient = useQueryClient();
+// export function useCreateOrder() {
+// const queryClient = useQueryClient();
- return useMutation({
- mutationFn: async (orderData: CreateOrderRequest) => {
- const formData = new URLSearchParams();
+// return useMutation({
+// mutationFn: async (orderData: CreateOrderRequest) => {
+// const formData = new URLSearchParams();
- Object.entries(orderData).forEach(([key, value]) => {
- if (value !== null && value !== undefined) {
- formData.append(key, String(value));
- }
- });
+// Object.entries(orderData).forEach(([key, value]) => {
+// if (value !== null && value !== undefined) {
+// formData.append(key, String(value));
+// }
+// });
- const response = await apiClient.post("/orders", formData, {
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- });
+// const response = await apiClient.post("/orders", formData, {
+// headers: {
+// "Content-Type": "application/x-www-form-urlencoded",
+// },
+// });
- return response.data;
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["orders"] });
- queryClient.invalidateQueries({ queryKey: ["cart"] });
- },
- });
-}
+// return response.data;
+// },
+// onSuccess: () => {
+// queryClient.invalidateQueries({ queryKey: ["orders"] });
+// queryClient.invalidateQueries({ queryKey: ["cart"] });
+// },
+// });
+// }
export function useCancelOrder() {
const queryClient = useQueryClient();
diff --git a/features/products/components/ProductPageContent.tsx b/features/products/components/ProductPageContent.tsx
index 31b4d21..0c82bf6 100644
--- a/features/products/components/ProductPageContent.tsx
+++ b/features/products/components/ProductPageContent.tsx
@@ -1,225 +1,281 @@
-"use client";
+"use client"
-import { useState, useCallback, useMemo, useRef, useEffect } from "react";
-import { Skeleton } from "@/components/ui/skeleton";
-import {
- useProductsBySlug,
- useRelatedProducts,
- useSubmitReview,
-} from "@/features/products/hooks/useProducts";
+import { useState, useCallback, useMemo, useRef, useEffect } from "react"
+import { Skeleton } from "@/components/ui/skeleton"
+import { useProductsBySlug, useRelatedProducts, useSubmitReview } from "@/features/products/hooks/useProducts"
import {
useAddToCart,
useUpdateCartItemQuantity,
useRemoveFromCart,
useCart,
-} from "@/features/cart/hooks/useCart";
-import { useTranslations } from "next-intl";
-import { toast } from "sonner";
-import { ProductImageGallery } from "./ProductImageGallery";
-import { ProductInfoCard } from "./ProductInfoCard";
-import { ProductPurchaseCard } from "./ProductPurchaseCard";
-import { ProductReviewsSection } from "./ProductReviewsSection";
-import { RelatedProductsSection } from "./RelatedProductsSection";
-import { ReviewModal } from "./ReviewModal";
-import { StockLimitModal } from "./StockLimitModal";
+ cartEvents,
+} from "@/features/cart/hooks/useCart"
+import { useTranslations } from "next-intl"
+import { toast } from "sonner"
+import { ProductImageGallery } from "./ProductImageGallery"
+import { ProductInfoCard } from "./ProductInfoCard"
+import { ProductPurchaseCard } from "./ProductPurchaseCard"
+import { ProductReviewsSection } from "./ProductReviewsSection"
+import { RelatedProductsSection } from "./RelatedProductsSection"
+import { ReviewModal } from "./ReviewModal"
+import { StockLimitModal } from "./StockLimitModal"
interface ProductDetailProps {
- slug: string;
+ slug: string
}
-const PENDING_PRODUCT_UPDATES_KEY = "pendingProductUpdates";
+const PENDING_PRODUCT_UPDATES_KEY = "pendingProductUpdates"
interface PendingUpdate {
- quantity: number;
- timestamp: number;
- retryCount: number;
+ quantity: number
+ timestamp: number
+ retryCount: number
+}
+
+const DEBUG = true
+const log = (...args: any[]) => {
+ if (DEBUG) console.log("[ProductPage]", ...args)
}
export default function ProductPageContent({ slug }: ProductDetailProps) {
- const [localQuantity, setLocalQuantity] = useState(1);
- const [isFavorite, setIsFavorite] = useState(false);
- const [isSyncing, setIsSyncing] = useState(false);
- const [syncError, setSyncError] = useState(false);
- const [showStockModal, setShowStockModal] = useState(false);
- const [showReviewModal, setShowReviewModal] = useState(false);
+ const [localQuantity, setLocalQuantity] = useState(1)
+ const [isFavorite, setIsFavorite] = useState(false)
+ const [isSyncing, setIsSyncing] = useState(false)
+ const [syncError, setSyncError] = useState(false)
+ const [showStockModal, setShowStockModal] = useState(false)
+ const [showReviewModal, setShowReviewModal] = useState(false)
+ const [isInitialized, setIsInitialized] = useState(false) // 🔥 NEW: Track initialization
- const t = useTranslations();
+ const t = useTranslations()
- const debounceTimerRef = useRef
(undefined);
- const isRequestInFlightRef = useRef(false);
- const pendingQuantityRef = useRef(null);
- const retryCountRef = useRef(0);
- const retryTimerRef = useRef(undefined);
- const syncToServerRef = useRef<((quantity: number) => void) | null>(null);
- const retrySyncRef = useRef<((quantity: number) => void) | null>(null);
+ const debounceTimerRef = useRef(undefined)
+ const isRequestInFlightRef = useRef(false)
+ const pendingQuantityRef = useRef(null)
+ const retryCountRef = useRef(0)
+ const retryTimerRef = useRef(undefined)
+ const syncToServerRef = useRef<((quantity: number) => void) | null>(null)
+ const retrySyncRef = useRef<((quantity: number) => void) | null>(null)
+ const shouldSyncFromCartRef = useRef(true)
+ const lastSyncedQuantityRef = useRef(null)
- const {
- data: product,
- isLoading: productLoading,
- error,
- refetch: refetchProduct,
- } = useProductsBySlug(slug);
+ const { data: product, isLoading: productLoading, error, refetch: refetchProduct } = useProductsBySlug(slug)
+
+ // 🔥 FIX: Memoize cart options to prevent infinite re-subscriptions
+ const cartOptions = useMemo(
+ () => ({
+ refetchOnMount: true,
+ refetchOnWindowFocus: true,
+ staleTime: 0,
+ }),
+ [],
+ )
+
+ const { data: cartData, refetch: refetchCart, isFetching: isCartFetching } = useCart(cartOptions)
- const { data: cartData, refetch: refetchCart } = useCart();
-
const { data: relatedProducts } = useRelatedProducts(product?.id || 0, {
enabled: !!product?.id,
- });
+ })
- const addToCartMutation = useAddToCart();
- const updateCartMutation = useUpdateCartItemQuantity();
- const removeFromCartMutation = useRemoveFromCart();
- const submitReviewMutation = useSubmitReview();
+ const addToCartMutation = useAddToCart()
+ const updateCartMutation = useUpdateCartItemQuantity()
+ const removeFromCartMutation = useRemoveFromCart()
+ const submitReviewMutation = useSubmitReview()
- const cartItem = useMemo(
- () => cartData?.data?.find((item: any) => item.product?.id === product?.id),
- [cartData, product]
- );
- const isInCart = !!cartItem;
- const availableStock = product?.stock || 0;
+ const cartItem = useMemo(() => {
+ const item = cartData?.data?.find((item: any) => item.product?.id === product?.id)
+ log("🎯 Cart Item Found:", {
+ productId: product?.id,
+ cartItem: item,
+ quantity: item?.product_quantity,
+ isInitialized,
+ })
+ return item
+ }, [cartData, product, isInitialized])
+
+ const isInCart = !!cartItem
+ const availableStock = product?.stock || 0
+
+ log("📊 State:", {
+ isInCart,
+ localQuantity,
+ cartItemQuantity: cartItem?.product_quantity,
+ availableStock,
+ isSyncing,
+ shouldSyncFromCart: shouldSyncFromCartRef.current,
+ isInitialized,
+ })
const imageUrls = useMemo(
- () =>
- product?.media?.map(
- (m) => m.images_800x800 || m.images_720x720 || m.thumbnail
- ) || [],
- [product]
- );
+ () => product?.media?.map((m) => m.images_800x800 || m.images_720x720 || m.thumbnail) || [],
+ [product],
+ )
- // ✅ CORRECT - Use reviews from product data
- const reviews = useMemo(() => product?.reviews_resources || [], [product]);
+ const reviews = useMemo(() => product?.reviews_resources || [], [product])
const averageRating = useMemo(
- () => (product?.reviews?.rating ? parseFloat(product.reviews.rating) : 0),
- [product]
- );
+ () => (product?.reviews?.rating ? Number.parseFloat(product.reviews.rating) : 0),
+ [product],
+ )
+
+ // 🔥 FIX: Subscribe to cart events ONCE with stable dependencies
+ useEffect(() => {
+ log("🔔 Setting up cart event subscription")
+ const unsubscribe = cartEvents.subscribe(() => {
+ log("📢 Cart event received! Refetching...")
+ refetchCart()
+ })
+
+ return () => {
+ log("🔕 Cleaning up cart event subscription")
+ unsubscribe()
+ }
+ }, [refetchCart])
+
+ // 🔥 CRITICAL FIX: Initialize localQuantity from cart ONCE on mount
+ useEffect(() => {
+ if (!product?.id || isInitialized) return
+
+ log("🚀 Initializing component with product:", product.id)
+
+ if (cartItem?.product_quantity) {
+ const serverQuantity = cartItem.product_quantity
+ log("✅ Initial cart quantity found:", serverQuantity)
+ setLocalQuantity(serverQuantity)
+ lastSyncedQuantityRef.current = serverQuantity
+ }
+
+ setIsInitialized(true)
+ }, [product?.id, cartItem, isInitialized])
useEffect(() => {
- if (cartItem?.product_quantity) {
- setLocalQuantity(cartItem.product_quantity);
- }
- }, [cartItem]);
+ setLocalQuantity(cartItem?.product_quantity || 1)
+ }, [cartItem])
const savePendingUpdate = useCallback(
(quantity: number) => {
- if (!product?.id) return;
+ if (!product?.id) return
try {
- const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
- const pending: Record = stored
- ? JSON.parse(stored)
- : {};
+ const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY)
+ const pending: Record = stored ? JSON.parse(stored) : {}
pending[product.id] = {
quantity,
timestamp: Date.now(),
retryCount: retryCountRef.current,
- };
- sessionStorage.setItem(
- PENDING_PRODUCT_UPDATES_KEY,
- JSON.stringify(pending)
- );
+ }
+ sessionStorage.setItem(PENDING_PRODUCT_UPDATES_KEY, JSON.stringify(pending))
} catch (error) {
- console.error("Failed to save pending update:", error);
+ console.error("Failed to save pending update:", error)
}
},
- [product?.id]
- );
+ [product?.id],
+ )
const clearPendingUpdate = useCallback(() => {
- if (!product?.id) return;
+ if (!product?.id) return
try {
- const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
+ const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY)
if (stored) {
- const pending: Record = JSON.parse(stored);
- delete pending[product.id];
+ const pending: Record = JSON.parse(stored)
+ delete pending[product.id]
if (Object.keys(pending).length === 0) {
- sessionStorage.removeItem(PENDING_PRODUCT_UPDATES_KEY);
+ sessionStorage.removeItem(PENDING_PRODUCT_UPDATES_KEY)
} else {
- sessionStorage.setItem(
- PENDING_PRODUCT_UPDATES_KEY,
- JSON.stringify(pending)
- );
+ sessionStorage.setItem(PENDING_PRODUCT_UPDATES_KEY, JSON.stringify(pending))
}
}
} catch (error) {
- console.error("Failed to clear pending update:", error);
+ console.error("Failed to clear pending update:", error)
}
- }, [product?.id]);
+ }, [product?.id])
const retrySync = useCallback(
(quantity: number) => {
- const maxRetries = 4;
- const retryCount = retryCountRef.current;
+ const maxRetries = 4
+ const retryCount = retryCountRef.current
if (retryCount >= maxRetries) {
- setSyncError(true);
- setIsSyncing(false);
+ setSyncError(true)
+ setIsSyncing(false)
+ shouldSyncFromCartRef.current = true
toast.error(t("error"), {
description: t("update_quantity_failed"),
- });
- return;
+ })
+ return
}
- const delay = Math.min(1000 * Math.pow(2, retryCount), 16000);
- retryCountRef.current++;
+ const delay = Math.min(1000 * Math.pow(2, retryCount), 16000)
+ retryCountRef.current++
retryTimerRef.current = setTimeout(() => {
- syncToServerRef.current?.(quantity);
- }, delay);
+ syncToServerRef.current?.(quantity)
+ }, delay)
},
- [t]
- );
+ [t],
+ )
- retrySyncRef.current = retrySync;
+ retrySyncRef.current = retrySync
const syncToServer = useCallback(
async (quantity: number) => {
- if (!product?.id) return;
+ if (!product?.id) return
+
+ log("🚀 syncToServer called:", {
+ productId: product.id,
+ quantity,
+ isRequestInFlight: isRequestInFlightRef.current,
+ isInCart,
+ })
if (isRequestInFlightRef.current) {
- pendingQuantityRef.current = quantity;
- return;
+ log("⏳ Request in flight, queuing:", quantity)
+ pendingQuantityRef.current = quantity
+ return
}
- isRequestInFlightRef.current = true;
- setIsSyncing(true);
- setSyncError(false);
+ isRequestInFlightRef.current = true
+ setIsSyncing(true)
+ setSyncError(false)
try {
if (quantity === 0) {
- await removeFromCartMutation.mutateAsync(product.id);
- toast.success(t("removed_from_cart"));
+ log("🗑️ Removing from cart")
+ await removeFromCartMutation.mutateAsync(product.id)
+ toast.success(t("removed_from_cart"))
} else if (isInCart) {
+ log("🔄 Updating cart quantity")
await updateCartMutation.mutateAsync({
productId: product.id,
quantity: quantity,
- });
+ })
} else {
+ log("➕ Adding to cart")
await addToCartMutation.mutateAsync({
productId: product.id,
quantity: quantity,
- });
+ })
}
- isRequestInFlightRef.current = false;
- setIsSyncing(false);
- retryCountRef.current = 0;
- clearPendingUpdate();
- await refetchCart();
+ log("✅ Sync successful")
+ await refetchCart()
+ retryCountRef.current = 0
+ clearPendingUpdate()
if (pendingQuantityRef.current !== null) {
- const nextQuantity = pendingQuantityRef.current;
- pendingQuantityRef.current = null;
- setTimeout(() => syncToServerRef.current?.(nextQuantity), 100);
+ const nextQuantity = pendingQuantityRef.current
+ pendingQuantityRef.current = null
+ log("📤 Processing queued update:", nextQuantity)
+ setTimeout(() => syncToServerRef.current?.(nextQuantity), 100)
}
} catch (error) {
- console.error("Sync failed:", error);
- isRequestInFlightRef.current = false;
+ log("❌ Sync failed:", error)
+ setLocalQuantity(cartItem?.product_quantity || 1)
+ toast.error("Failed to update quantity", {
+ description: "Please try again",
+ })
- if (retryCountRef.current >= 3) {
- setLocalQuantity(cartItem?.product_quantity || 1);
- clearPendingUpdate();
- }
-
- retrySyncRef.current?.(quantity);
+ retrySyncRef.current?.(quantity)
+ } finally {
+ isRequestInFlightRef.current = false
+ setIsSyncing(false)
}
},
[
@@ -229,127 +285,122 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
addToCartMutation,
removeFromCartMutation,
cartItem,
- clearPendingUpdate,
refetchCart,
+ clearPendingUpdate,
t,
- ]
- );
+ ],
+ )
- syncToServerRef.current = syncToServer;
+ syncToServerRef.current = syncToServer
useEffect(() => {
- if (!product?.id) return;
+ if (!isInCart || !product?.id) return
- const loadPendingUpdates = () => {
- try {
- const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
- if (stored) {
- const pending: Record = JSON.parse(stored);
- const productPending = pending[product.id];
-
- if (
- productPending &&
- productPending.quantity !== (cartItem?.product_quantity || 1)
- ) {
- setLocalQuantity(productPending.quantity);
- pendingQuantityRef.current = productPending.quantity;
- retryCountRef.current = productPending.retryCount;
-
- setTimeout(
- () => syncToServerRef.current?.(productPending.quantity),
- 500
- );
- }
- }
- } catch (error) {
- console.error("Failed to load pending updates:", error);
- }
- };
-
- loadPendingUpdates();
- }, [product?.id, cartItem]);
-
- useEffect(() => {
- if (!isInCart || !product?.id) return;
+ // If local matches server, nothing to sync
+ if (localQuantity === (cartItem?.product_quantity || 1)) {
+ return
+ }
if (debounceTimerRef.current) {
- clearTimeout(debounceTimerRef.current);
+ clearTimeout(debounceTimerRef.current)
}
- if (localQuantity === (cartItem?.product_quantity || 1)) {
- return;
- }
-
- savePendingUpdate(localQuantity);
-
debounceTimerRef.current = setTimeout(() => {
- syncToServerRef.current?.(localQuantity);
- }, 800);
+ syncToServerRef.current?.(localQuantity)
+ }, 800)
return () => {
if (debounceTimerRef.current) {
- clearTimeout(debounceTimerRef.current);
+ clearTimeout(debounceTimerRef.current)
}
- };
- }, [localQuantity, isInCart, product?.id, cartItem, savePendingUpdate]);
+ }
+ }, [localQuantity, isInCart, product?.id, cartItem?.product_quantity])
+ // Cleanup
useEffect(() => {
return () => {
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
- if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
- };
- }, []);
+ if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
+ if (retryTimerRef.current) clearTimeout(retryTimerRef.current)
+ }
+ }, [])
const handleAddToCart = useCallback(async () => {
- if (!product?.id) return;
+ if (!product?.id) return
- setIsSyncing(true);
+ if (localQuantity > availableStock) {
+ setShowStockModal(true)
+ return
+ }
+
+ setIsSyncing(true)
+ shouldSyncFromCartRef.current = false
try {
await addToCartMutation.mutateAsync({
productId: product.id,
quantity: localQuantity,
- });
+ })
- await refetchCart();
- setIsSyncing(false);
+ lastSyncedQuantityRef.current = localQuantity
+
+ setTimeout(() => {
+ shouldSyncFromCartRef.current = true
+ refetchCart()
+ }, 150)
+
+ setIsSyncing(false)
toast.success(t("added_to_cart"), {
description: `${product.name} ${t("added_to_cart_description")}`,
- });
+ })
} catch (error) {
- console.error("Add to cart error:", error);
- setIsSyncing(false);
+ console.error("Add to cart error:", error)
+ setIsSyncing(false)
+ shouldSyncFromCartRef.current = true
toast.error(t("error"), {
description: t("add_to_cart_failed"),
- });
+ })
}
- }, [product, localQuantity, addToCartMutation, refetchCart, t]);
+ }, [product, localQuantity, availableStock, addToCartMutation, refetchCart, t])
const handleQuantityIncrease = useCallback(() => {
+ log("➕ Quantity increase clicked:", {
+ current: localQuantity,
+ availableStock,
+ })
if (localQuantity >= availableStock) {
- setShowStockModal(true);
- return;
+ log("⚠️ Stock limit reached")
+ setShowStockModal(true)
+ return
}
- setLocalQuantity((prev) => prev + 1);
- }, [localQuantity, availableStock]);
+ setLocalQuantity((prev) => {
+ const newVal = prev + 1
+ log("📈 New local quantity:", newVal)
+ return newVal
+ })
+ }, [localQuantity, availableStock])
const handleQuantityDecrease = useCallback(() => {
- if (localQuantity <= 0) return;
- setLocalQuantity((prev) => prev - 1);
- }, [localQuantity]);
+ log("➖ Quantity decrease clicked:", { current: localQuantity })
+ if (localQuantity <= 0) return
+ setLocalQuantity((prev) => {
+ const newVal = prev - 1
+ log("📉 New local quantity:", newVal)
+ return newVal
+ })
+ }, [localQuantity])
const handleToggleFavorite = useCallback(() => {
- setIsFavorite(!isFavorite);
- }, [isFavorite]);
+ setIsFavorite(!isFavorite)
+ }, [isFavorite])
const handleSubmitReview = useCallback(
async (rating: number, text: string) => {
if (!product?.id || rating === 0 || !text.trim()) {
toast.error(t("error"), {
description: "Please provide rating and review text",
- });
- return;
+ })
+ return
}
try {
@@ -358,25 +409,24 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
rating: rating,
title: text,
source: "site",
- });
+ })
- // ✅ Refetch product to get updated reviews
- await refetchProduct();
-
- toast.success("Review submitted successfully!");
- setShowReviewModal(false);
+ await refetchProduct()
+
+ toast.success("Review submitted successfully!")
+ setShowReviewModal(false)
} catch (error) {
toast.error(t("error"), {
description: "Failed to submit review",
- });
+ })
}
},
- [product?.id, submitReviewMutation, refetchProduct, t]
- );
+ [product?.id, submitReviewMutation, refetchProduct, t],
+ )
const loadingSkeleton = useMemo(
() => (
-
+
@@ -393,33 +443,25 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
),
- []
- );
+ [],
+ )
- if (productLoading) return loadingSkeleton;
+ if (productLoading) return loadingSkeleton
if (error || !product) {
return (
-
-
- {t("product_not_found")}
-
-
- {t("product_not_found_description")}
-
+
+
{t("product_not_found")}
+
{t("product_not_found_description")}
- );
+ )
}
return (
<>
-
+
>
- );
-}
\ No newline at end of file
+ )
+}
diff --git a/features/products/components/ProductPurchaseCard.tsx b/features/products/components/ProductPurchaseCard.tsx
index 2e544dc..adbef78 100644
--- a/features/products/components/ProductPurchaseCard.tsx
+++ b/features/products/components/ProductPurchaseCard.tsx
@@ -94,13 +94,9 @@ export function ProductPurchaseCard({
variant="outline"
size="icon"
onClick={onQuantityIncrease}
- disabled={localQuantity >= availableStock || isSyncing}
+ disabled={isSyncing}
className={`rounded-lg h-12 w-12 ${
isSyncing ? "opacity-70" : ""
- } ${
- localQuantity >= availableStock
- ? "opacity-50 cursor-not-allowed"
- : ""
}`}
>
diff --git a/features/products/components/ProductReviewsSection.tsx b/features/products/components/ProductReviewsSection.tsx
index 823cf89..2fc6a3b 100644
--- a/features/products/components/ProductReviewsSection.tsx
+++ b/features/products/components/ProductReviewsSection.tsx
@@ -51,9 +51,9 @@ export function ProductReviewsSection({
{t("customer_reviews")}
{renderStars(Math.round(averageRating))}
-
+ {/*
{averageRating.toFixed(1)} out of 5
-
+ */}