From 0f46d9587b88ae16d4734c7484cd83890e5a7819 Mon Sep 17 00:00:00 2001 From: Hasini Date: Sat, 27 Jun 2026 22:32:52 +0530 Subject: [PATCH] refactor: optimize Zustand cart store subscriptions --- FRONTEND/src/components/ui/Navbar.jsx | 5 +- FRONTEND/src/components/ui/ProductCard.jsx | 4 +- FRONTEND/src/components/ui/QuickViewModal.jsx | 2 +- FRONTEND/src/store/cart.js | 136 +++++++++++++----- 4 files changed, 105 insertions(+), 42 deletions(-) diff --git a/FRONTEND/src/components/ui/Navbar.jsx b/FRONTEND/src/components/ui/Navbar.jsx index c718d56b..76b84d95 100644 --- a/FRONTEND/src/components/ui/Navbar.jsx +++ b/FRONTEND/src/components/ui/Navbar.jsx @@ -10,7 +10,7 @@ import { LanguageSwitcher } from '../LanguageSwitcher.jsx'; import { PlusSquareIcon } from "@chakra-ui/icons" import { IoMoon } from "react-icons/io5"; import { LuSun, LuShoppingCart, LuHeart } from "react-icons/lu"; -import { useCartStore } from "../../store/cart"; +import { useCart } from "../../store/cart"; import { useWishlist } from "../../context/WishlistContext.jsx"; import { useProductStore } from "../../store/product"; import { FaBalanceScale } from "react-icons/fa"; @@ -18,13 +18,14 @@ import { useCurrencyStore } from "../../store/currency"; import { formatPrice } from "../../utils/currency"; import { useAuth } from "../../context/AuthContext"; + const API = (import.meta.env.VITE_API_URL || '').replace(/\/$/, ''); const Navbar = () => { const { t } = useTranslation(); const { colorMode, toggleColorMode } = useColorMode(); const { isOpen, onOpen, onClose } = useDisclosure(); - const { cartItems, removeFromCart, totalPrice, emptyCart } = useCartStore(); + const { cartItems, removeFromCart, emptyCart, totalPrice } = useCart(); const { currency, rates, setCurrency } = useCurrencyStore(); const { wishlistCount, clearWishlist } = useWishlist(); const { searchQuery, setSearchQuery, products, fetchProducts, compareList } = useProductStore(); diff --git a/FRONTEND/src/components/ui/ProductCard.jsx b/FRONTEND/src/components/ui/ProductCard.jsx index 90c64a19..94e4d175 100644 --- a/FRONTEND/src/components/ui/ProductCard.jsx +++ b/FRONTEND/src/components/ui/ProductCard.jsx @@ -73,7 +73,9 @@ const ProductCard = ({ product }) => { } = useProductStore(); const isInCompare = compareList.some((p) => p._id === product._id); - const { addToCart } = useCartStore(); + const addToCart = useCartStore( + (state) => state.addToCart + ); const { addToWishlist, removeFromWishlist, checkInWishlist } = useWishlist(); const toast = useToast(); const { currency, rates } = useCurrencyStore(); diff --git a/FRONTEND/src/components/ui/QuickViewModal.jsx b/FRONTEND/src/components/ui/QuickViewModal.jsx index 9a91a638..99b38df4 100644 --- a/FRONTEND/src/components/ui/QuickViewModal.jsx +++ b/FRONTEND/src/components/ui/QuickViewModal.jsx @@ -19,7 +19,7 @@ import { useCurrencyStore } from "../../store/currency"; import { formatPrice } from "../../utils/currency"; const QuickViewModal = ({ isOpen, onClose, product }) => { - const { addToCart } = useCartStore(); + const addToCart = useCartStore((state) => state.addToCart); const { currency, rates } = useCurrencyStore(); if (!product) return null; diff --git a/FRONTEND/src/store/cart.js b/FRONTEND/src/store/cart.js index b2b75bc8..382e978d 100644 --- a/FRONTEND/src/store/cart.js +++ b/FRONTEND/src/store/cart.js @@ -1,40 +1,100 @@ import { create } from 'zustand'; -import axios from 'axios'; - -export const useCartStore = create((set) => ({ - cart: null, - loading: false, - - fetchCart: async () => { - set({ loading: true }); - try { - const response = await axios.get('/api/cart'); - set({ cart: response.data, loading: false }); - } catch (error) { - set({ loading: false }); - console.error('Error fetching cart:', error); - } - }, - - addToCart: async (productId, variantId = null, quantity = 1) => { - set({ loading: true }); - try { - const response = await axios.post('/api/cart', { productId, variantId, quantity }); - set({ cart: response.data, loading: false }); - } catch (error) { - set({ loading: false }); - console.error('Error adding to cart:', error); - } - }, - - removeFromCart: async (productId, variantId = null) => { - set({ loading: true }); - try { - const response = await axios.delete('/api/cart', { data: { productId, variantId } }); - set({ cart: response.data, loading: false }); - } catch (error) { - set({ loading: false }); - console.error('Error removing from cart:', error); +import { persist } from 'zustand/middleware'; + +export const useCartStore = create( + persist( + (set) => ({ + cartItems: [], + + addToCart: (product, quantity = 1) => { + const stockTracked = product.stock != null; + if (stockTracked && product.stock === 0) return { status: 'out_of_stock', added: 0 }; + + let status = 'added'; + let added = 0; + + set((state) => { + const existingItem = state.cartItems.find((item) => item._id === product._id); + const currentQty = existingItem ? existingItem.quantity : 0; + + let canAdd = quantity; + if (stockTracked) { + const available = product.stock - currentQty; + if (available <= 0) { + status = 'capped'; + added = 0; + return state; + } + canAdd = Math.min(quantity, available); + if (canAdd < quantity) status = 'capped'; + } + added = canAdd; + + if (existingItem) { + return { + cartItems: state.cartItems.map((item) => + item._id === product._id ? { ...item, quantity: item.quantity + canAdd } : item + ), + }; + } + return { cartItems: [...state.cartItems, { ...product, quantity: canAdd }] }; + }); + + return { status, added }; + }, + + removeFromCart: (id) => { + set((state) => ({ + cartItems: state.cartItems.filter((item) => item._id !== id), + })); + }, + + addBundleToCart: (items) => { + let addedCount = 0; + let skippedCount = 0; + set((state) => { + const updated = [...state.cartItems]; + for (const item of items) { + const stockTracked = item.stock != null; + const idx = updated.findIndex((i) => i._id === item._id); + const currentQty = idx >= 0 ? updated[idx].quantity : 0; + + if (stockTracked) { + const available = item.stock - currentQty; + if (available <= 0) { + skippedCount++; + continue; + } + } + + if (idx >= 0) { + updated[idx] = { ...updated[idx], quantity: updated[idx].quantity + 1 }; + } else { + updated.push({ ...item, quantity: 1 }); + } + addedCount++; + } + return { cartItems: updated }; + }); + return { addedCount, skippedCount }; + }, + + emptyCart: () => set({ cartItems: [] }), + }), + { + name: 'productStoreCart', } - } -})); \ No newline at end of file + ) +); + +export const useCart = () => { + const cartItems = useCartStore((state) => state.cartItems); + const addToCart = useCartStore((state) => state.addToCart); + const removeFromCart = useCartStore((state) => state.removeFromCart); + const addBundleToCart = useCartStore((state) => state.addBundleToCart); + const emptyCart = useCartStore((state) => state.emptyCart); + + const totalPrice = cartItems.reduce((total, item) => total + item.price * item.quantity, 0); + + return { cartItems, addToCart, removeFromCart, addBundleToCart, emptyCart, totalPrice }; +};