import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { ProductResponse, ProductVariantResponse } from '@/services/api/catalogService'; import { getAccessToken, apiFetch } from '@/services/api/client'; import { getCatalogPriceInfo, parseMoney } from '@/lib/utils'; import { compareKindLabel, detectProductKind, isSameCompareKind, sameTypeCompareList, } from '@/lib/compareSpecs'; export interface CartItem { id: string; product: ProductResponse; selectedVariant: ProductVariantResponse; quantity: number; } function normalizeVariantPrices( variant: ProductVariantResponse, product?: ProductResponse ): ProductVariantResponse { let price = parseMoney(variant.price); if (!price && product) { price = getCatalogPriceInfo(product).price; } return { ...variant, price, compare_price: variant.compare_price != null && variant.compare_price !== '' ? parseMoney(variant.compare_price) : null, cost_price: parseMoney(variant.cost_price), }; } function normalizeCartItems(cart: CartItem[]): CartItem[] { return (cart || []).map((item) => ({ ...item, quantity: Number(item.quantity) || 1, selectedVariant: normalizeVariantPrices( item.selectedVariant, item.product ), })); } export const COMPARE_LIMIT = 4; export type AddToCompareResult = | { ok: true; already: boolean } | { ok: false; reason: 'full' | 'type_mismatch'; expected?: string; actual?: string }; export function isSameCompareProduct( a: ProductResponse, b: ProductResponse | string ): boolean { if (typeof b === 'string') { return a.product_id === b || a.slug === b; } if (a.product_id && b.product_id && a.product_id === b.product_id) return true; if (a.slug && b.slug && a.slug === b.slug) return true; return false; } interface CartState { cart: CartItem[]; wishlist: ProductResponse[]; compareList: ProductResponse[]; cartDrawerOpen: boolean; quickViewProduct: ProductResponse | null; _hasHydrated: boolean; addToCart: ( product: ProductResponse, variant?: ProductVariantResponse, quantity?: number ) => Promise<{ ok: boolean; reason?: 'no_variant' | 'out_of_stock' | 'stock_limit' }>; removeFromCart: (itemId: string) => Promise; updateQuantity: (itemId: string, quantity: number) => Promise; clearCart: () => Promise; setCartDrawerOpen: (open: boolean) => void; setHasHydrated: (value: boolean) => void; toggleWishlist: (product: ProductResponse) => void; isInWishlist: (productId: string) => boolean; toggleCompare: (product: ProductResponse) => AddToCompareResult; addToCompare: (product: ProductResponse) => AddToCompareResult; removeFromCompare: (productId: string) => void; replaceCompareProduct: (oldId: string, product: ProductResponse) => AddToCompareResult; clearCompare: () => void; isInCompare: (productId: string) => boolean; setQuickViewProduct: (product: ProductResponse | null) => void; getCartTotal: () => number; getItemCount: () => number; } async function syncAddToBackend(variantId: string, qty: number) { const token = getAccessToken(); if (!token) return; try { await apiFetch('/api/v1/cart/items', { method: 'POST', body: JSON.stringify({ variant_id: variantId, qty }), }); } catch (err) { console.error('Failed to sync cart add', err); } } async function syncCartSnapshot() { const token = getAccessToken(); if (!token) return; try { const items = useCartStore.getState().cart.map((item) => ({ variant_id: item.selectedVariant.variant_id, qty: item.quantity, })); await apiFetch('/api/v1/cart/items', { method: 'PUT', body: JSON.stringify({ items }), }); } catch (err) { console.error('Failed to sync cart snapshot', err); } } export const useCartStore = create()( persist( (set, get) => ({ cart: [], wishlist: [], compareList: [], cartDrawerOpen: false, quickViewProduct: null, _hasHydrated: false, setHasHydrated: (value) => set({ _hasHydrated: value }), addToCart: async (product, variant, quantity = 1) => { const rawVariant = variant || product.variants?.[0]; if (!rawVariant?.variant_id) { console.error('Cannot add to cart: product has no variant', product.product_id); return { ok: false, reason: 'no_variant' }; } const selectedVariant = normalizeVariantPrices(rawVariant, product); const available = selectedVariant.available_stock; const itemId = `${product.product_id}-${selectedVariant.variant_id}`; const existing = get().cart.find((item) => item.id === itemId); const nextQty = (existing?.quantity || 0) + quantity; if (typeof available === 'number') { if (available <= 0) { console.warn('Out of stock', selectedVariant.sku); return { ok: false, reason: 'out_of_stock' }; } if (nextQty > available) { console.warn(`Only ${available} units available for`, selectedVariant.sku); return { ok: false, reason: 'stock_limit' }; } } set((state) => { const existingIndex = state.cart.findIndex((item) => item.id === itemId); let updatedCart: CartItem[]; if (existingIndex > -1) { updatedCart = state.cart.map((item, idx) => idx === existingIndex ? { ...item, quantity: item.quantity + quantity } : item ); } else { updatedCart = [ ...state.cart, { id: itemId, product, selectedVariant, quantity }, ]; } return { cart: updatedCart, cartDrawerOpen: true }; }); await syncAddToBackend(selectedVariant.variant_id, quantity); return { ok: true }; }, removeFromCart: async (itemId) => { set((state) => ({ cart: state.cart.filter((row) => row.id !== itemId) })); await syncCartSnapshot(); }, updateQuantity: async (itemId, quantity) => { set((state) => { const row = state.cart.find((item) => item.id === itemId); const available = row?.selectedVariant?.available_stock; let nextQty = quantity; if (typeof available === 'number' && nextQty > available) { nextQty = available; } return { cart: nextQty <= 0 ? state.cart.filter((item) => item.id !== itemId) : state.cart.map((item) => (item.id === itemId ? { ...item, quantity: nextQty } : item)), }; }); await syncCartSnapshot(); }, clearCart: async () => { set({ cart: [] }); const token = getAccessToken(); if (!token) return; try { await apiFetch('/api/v1/cart', { method: 'DELETE' }); } catch (err) { console.error('Failed to clear cart', err); } }, setCartDrawerOpen: (open) => set({ cartDrawerOpen: open }), toggleWishlist: (product) => set((state) => { const exists = state.wishlist.some((item) => item.product_id === product.product_id); const updatedWishlist = exists ? state.wishlist.filter((item) => item.product_id !== product.product_id) : [...state.wishlist, product]; try { localStorage.setItem('wishlist', JSON.stringify(updatedWishlist.map((p) => p.product_id))); } catch { /* ignore */ } if (typeof window !== 'undefined') { window.dispatchEvent(new Event('wishlistUpdated')); } return { wishlist: updatedWishlist }; }), isInWishlist: (productId) => get().wishlist.some((item) => item.product_id === productId), toggleCompare: (product) => { const exists = get().compareList.some((item) => isSameCompareProduct(item, product)); if (exists) { get().removeFromCompare(product.product_id || product.slug); return { ok: true, already: true }; } return get().addToCompare(product); }, addToCompare: (product) => { const list = sameTypeCompareList(get().compareList); if (list.length !== get().compareList.length) { set({ compareList: list }); } if (list.some((item) => isSameCompareProduct(item, product))) { return { ok: true, already: true }; } if (list.length > 0 && !isSameCompareKind(list, product)) { return { ok: false, reason: 'type_mismatch', expected: compareKindLabel(detectProductKind(list[0])), actual: compareKindLabel(detectProductKind(product)), }; } if (list.length >= COMPARE_LIMIT) { return { ok: false, reason: 'full' }; } set({ compareList: [...list, product] }); return { ok: true, already: false }; }, removeFromCompare: (productId) => set((state) => ({ compareList: state.compareList.filter((item) => !isSameCompareProduct(item, productId)), })), replaceCompareProduct: (oldId, product) => { const remaining = sameTypeCompareList( get().compareList.filter((item) => !isSameCompareProduct(item, oldId)) ); if (remaining.some((item) => isSameCompareProduct(item, product))) { set({ compareList: remaining }); return { ok: true, already: true }; } if (remaining.length > 0 && !isSameCompareKind(remaining, product)) { return { ok: false, reason: 'type_mismatch', expected: compareKindLabel(detectProductKind(remaining[0])), actual: compareKindLabel(detectProductKind(product)), }; } if (remaining.length >= COMPARE_LIMIT) { return { ok: false, reason: 'full' }; } set({ compareList: [...remaining, product] }); return { ok: true, already: false }; }, clearCompare: () => set({ compareList: [] }), isInCompare: (productId) => get().compareList.some((item) => isSameCompareProduct(item, productId)), setQuickViewProduct: (product) => set({ quickViewProduct: product }), getCartTotal: () => get().cart.reduce((total, item) => { const unit = parseMoney(item.selectedVariant?.price); const qty = Number(item.quantity) || 0; return total + unit * qty; }, 0), getItemCount: () => get().cart.reduce((count, item) => count + (Number(item.quantity) || 0), 0), }), { name: 'ifixkart-ecommerce-storage', skipHydration: true, storage: createJSONStorage(() => localStorage), partialize: (state) => ({ cart: state.cart, wishlist: state.wishlist, compareList: state.compareList, }), onRehydrateStorage: () => (state) => { if (state) { state.cart = normalizeCartItems(state.cart || []); state.compareList = sameTypeCompareList(state.compareList || []); state.setHasHydrated(true); } }, } ) );