"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import Image from "next/image"; import { Minus, Plus, Trash2, AlertTriangle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { useUpdateCartItemQuantity, useRemoveFromCart } from "@/lib/hooks"; import { useTranslations } from "next-intl"; import type { CartItem } from "@/lib/types/api"; interface CartItemCardProps { item: CartItem; onUpdate?: () => void; } // Session Storage Key const PENDING_CART_UPDATES_KEY = "pendingCartUpdates"; interface PendingUpdate { quantity: number; timestamp: number; retryCount: number; } export default function CartItemCard({ item, onUpdate }: CartItemCardProps) { const t = useTranslations(); // Local UI State (Instant feedback) const [localQuantity, setLocalQuantity] = useState(item.quantity); // Sync State const [isSyncing, setIsSyncing] = useState(false); const [syncError, setSyncError] = useState(false); // Stock limit modal const [showStockModal, setShowStockModal] = useState(false); // Refs const debounceTimerRef = useRef(undefined); const isRequestInFlightRef = useRef(false); const pendingQuantityRef = useRef(null); const retryCountRef = useRef(0); const retryTimerRef = useRef(undefined); // Function refs to solve circular dependency const syncToServerRef = useRef<((quantity: number) => void) | null>(null); const retrySyncRef = useRef<((quantity: number) => void) | null>(null); const { mutate: updateQuantity } = useUpdateCartItemQuantity(); const { mutate: removeItem, isPending: isRemoving } = useRemoveFromCart(); // Get available stock const availableStock = item.product.stock || 0; // Initialize from server state useEffect(() => { setLocalQuantity(item.quantity); }, [item.quantity]); // Save to sessionStorage const savePendingUpdate = useCallback( (quantity: number) => { try { const stored = sessionStorage.getItem(PENDING_CART_UPDATES_KEY); const pending: Record = stored ? JSON.parse(stored) : {}; pending[item.product_id] = { quantity, timestamp: Date.now(), retryCount: retryCountRef.current, }; sessionStorage.setItem( PENDING_CART_UPDATES_KEY, JSON.stringify(pending) ); } catch (error) { console.error("Failed to save pending update:", error); } }, [item.product_id] ); // Remove from sessionStorage const clearPendingUpdate = useCallback(() => { try { const stored = sessionStorage.getItem(PENDING_CART_UPDATES_KEY); if (stored) { const pending: Record = JSON.parse(stored); delete pending[item.product_id]; if (Object.keys(pending).length === 0) { sessionStorage.removeItem(PENDING_CART_UPDATES_KEY); } else { sessionStorage.setItem( PENDING_CART_UPDATES_KEY, JSON.stringify(pending) ); } } } catch (error) { console.error("Failed to clear pending update:", error); } }, [item.product_id]); // Exponential backoff retry const retrySync = useCallback((quantity: number) => { const maxRetries = 4; const retryCount = retryCountRef.current; if (retryCount >= maxRetries) { setSyncError(true); setIsSyncing(false); return; } const delay = Math.min(1000 * Math.pow(2, retryCount), 16000); // Max 16s retryCountRef.current++; retryTimerRef.current = setTimeout(() => { syncToServerRef.current?.(quantity); }, delay); }, []); // Update ref retrySyncRef.current = retrySync; // Sync to server const syncToServer = useCallback( (quantity: number) => { // If already syncing, queue this update if (isRequestInFlightRef.current) { pendingQuantityRef.current = quantity; return; } // Mark as syncing isRequestInFlightRef.current = true; setIsSyncing(true); setSyncError(false); if (quantity <= 0) { removeItem(item.product_id, { onSuccess: () => { isRequestInFlightRef.current = false; setIsSyncing(false); retryCountRef.current = 0; clearPendingUpdate(); onUpdate?.(); // Process queued update if any if (pendingQuantityRef.current !== null) { const nextQuantity = pendingQuantityRef.current; pendingQuantityRef.current = null; setTimeout(() => syncToServerRef.current?.(nextQuantity), 100); } }, onError: (error) => { console.error("Remove failed:", error); isRequestInFlightRef.current = false; retrySyncRef.current?.(quantity); }, }); } else { updateQuantity( { productId: item.product_id, quantity }, { onSuccess: () => { isRequestInFlightRef.current = false; setIsSyncing(false); retryCountRef.current = 0; clearPendingUpdate(); onUpdate?.(); // Process queued update if any if (pendingQuantityRef.current !== null) { const nextQuantity = pendingQuantityRef.current; pendingQuantityRef.current = null; setTimeout(() => syncToServerRef.current?.(nextQuantity), 100); } }, onError: (error) => { console.error("Update failed:", error); isRequestInFlightRef.current = false; // Rollback on error after retries exhausted if (retryCountRef.current >= 3) { setLocalQuantity(item.quantity); clearPendingUpdate(); } retrySyncRef.current?.(quantity); }, } ); } }, [ item.product_id, item.quantity, updateQuantity, removeItem, onUpdate, clearPendingUpdate, ] ); // Update ref syncToServerRef.current = syncToServer; // Load pending updates from sessionStorage on mount useEffect(() => { const loadPendingUpdates = () => { try { const stored = sessionStorage.getItem(PENDING_CART_UPDATES_KEY); if (stored) { const pending: Record = JSON.parse(stored); const productPending = pending[item.product_id]; if (productPending && productPending.quantity !== item.quantity) { // Apply pending update setLocalQuantity(productPending.quantity); pendingQuantityRef.current = productPending.quantity; retryCountRef.current = productPending.retryCount; // Trigger sync after a short delay setTimeout( () => syncToServerRef.current?.(productPending.quantity), 500 ); } } } catch (error) { console.error("Failed to load pending updates:", error); } }; loadPendingUpdates(); }, [item.product_id, item.quantity]); // Debounced sync useEffect(() => { // Clear existing timers if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // If local quantity matches server, no sync needed if (localQuantity === item.quantity) { return; } // Save to sessionStorage immediately savePendingUpdate(localQuantity); // Debounce the API call debounceTimerRef.current = setTimeout(() => { syncToServerRef.current?.(localQuantity); }, 800); return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, [localQuantity, item.quantity, savePendingUpdate]); // Cleanup useEffect(() => { return () => { if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); if (retryTimerRef.current) clearTimeout(retryTimerRef.current); }; }, []); const handleQuantityIncrease = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // Check stock limit if (localQuantity >= availableStock) { setShowStockModal(true); return; } // Optimistic update (instant UI feedback) setLocalQuantity((prev) => prev + 1); }; const handleQuantityDecrease = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (localQuantity <= 1) { handleDelete(); return; } // Optimistic update (instant UI feedback) setLocalQuantity((prev) => prev - 1); }; const handleDelete = () => { setLocalQuantity(0); clearPendingUpdate(); }; const getImageSrc = () => { if (item.product.image) return item.product.image; if (item.product.images && item.product.images.length > 0) return item.product.images[0]; return "/placeholder.svg"; }; return ( <>
{item.product.name}

{item.product.name}

{item.seller?.name || "Store"}

{availableStock <= 5 && (

{t("only_left", { count: availableStock })}

)}

{t("unit_price")}{" "} {item.price_formatted}

{item.discount_formatted && item.discount_formatted !== "0 TMT" && (

{t("discount")} {item.discount_formatted}

)}
{t("total_price")} {( parseFloat(item.product.price_amount || "0") * localQuantity ).toFixed(2)}{" "} TMT
{localQuantity} {syncError && ( )}
{/* Stock Limit Modal */}
{t("stock_limit_title")} {t("stock_limit_message", { product: item.product.name, stock: availableStock, })}
); }