225 lines
5.4 KiB
JavaScript
225 lines
5.4 KiB
JavaScript
/*
|
|
* Provides shopping cart state and actions like add, remove, and checkout.
|
|
*
|
|
* Author: Shiv
|
|
* Date: April 2026
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import { createContext, useContext, useState, useEffect, useCallback } from "react";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
import {
|
|
fetchCart,
|
|
apiAddToCart,
|
|
apiUpdateCartItem,
|
|
apiRemoveCartItem,
|
|
apiClearCart,
|
|
apiApplyCoupon,
|
|
apiRemoveCoupon,
|
|
apiApplyPoints,
|
|
apiCheckout,
|
|
apiCancelCheckout,
|
|
} from "@/lib/cartApi";
|
|
|
|
//Cart context
|
|
//Holds the user's cart and all cart actions
|
|
const CartContext = createContext(null);
|
|
|
|
//Key used to save the selected store in localStorage
|
|
const STORE_KEY = "selected_store_id";
|
|
|
|
//Provides cart state to all child components
|
|
export function CartProvider({ children }) {
|
|
const { user, token } = useAuth();
|
|
const [cart, setCart] = useState(null);
|
|
const [selectedStoreId, setSelectedStoreIdState] = useState(null);
|
|
const [cartLoading, setCartLoading] = useState(false);
|
|
const [cartError, setCartError] = useState(null);
|
|
|
|
//Saves the selected store in state and localStorage
|
|
const setStoreId = useCallback((id) => {
|
|
const parsed = id ? Number(id) : null;
|
|
setSelectedStoreIdState(parsed);
|
|
if (parsed) {
|
|
localStorage.setItem(STORE_KEY, String(parsed));
|
|
}
|
|
|
|
else {
|
|
localStorage.removeItem(STORE_KEY);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const stored = localStorage.getItem(STORE_KEY);
|
|
if (stored) {
|
|
setSelectedStoreIdState(Number(stored));
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (user?.storeId && !localStorage.getItem(STORE_KEY)) {
|
|
setStoreId(user.storeId);
|
|
}
|
|
}, [user, setStoreId]);
|
|
|
|
//Fetches the latest cart from the backend
|
|
const refreshCart = useCallback(async () => {
|
|
if (!token || !selectedStoreId) {
|
|
setCart(null);
|
|
return;
|
|
}
|
|
setCartLoading(true);
|
|
setCartError(null);
|
|
try {
|
|
const data = await fetchCart(token, selectedStoreId);
|
|
setCart(data);
|
|
}
|
|
|
|
catch (err) {
|
|
setCartError(err.message);
|
|
}
|
|
|
|
finally {
|
|
setCartLoading(false);
|
|
}
|
|
}, [token, selectedStoreId]);
|
|
|
|
useEffect(() => {
|
|
if (user && selectedStoreId) {
|
|
refreshCart();
|
|
}
|
|
|
|
else {
|
|
setCart(null);
|
|
}
|
|
}, [user, selectedStoreId, refreshCart]);
|
|
|
|
const addItem = useCallback(
|
|
async (prodId, quantity = 1) => {
|
|
if (!token || !selectedStoreId) throw new Error("Select a store first");
|
|
const updated = await apiAddToCart(token, { prodId, storeId: selectedStoreId, quantity });
|
|
setCart(updated);
|
|
|
|
return updated;
|
|
},
|
|
[token, selectedStoreId]
|
|
);
|
|
|
|
const updateItem = useCallback(
|
|
async (cartItemId, quantity) => {
|
|
if (!token) return;
|
|
const updated = await apiUpdateCartItem(token, { cartItemId, quantity });
|
|
setCart(updated);
|
|
|
|
return updated;
|
|
},
|
|
[token]
|
|
);
|
|
|
|
const removeItem = useCallback(
|
|
async (cartItemId) => {
|
|
if (!token) return;
|
|
const updated = await apiRemoveCartItem(token, cartItemId);
|
|
setCart(updated);
|
|
|
|
return updated;
|
|
},
|
|
[token]
|
|
);
|
|
|
|
const clearCart = useCallback(async () => {
|
|
if (!token || !selectedStoreId) return;
|
|
await apiClearCart(token, selectedStoreId);
|
|
setCart(null);
|
|
}, [token, selectedStoreId]);
|
|
|
|
const applyCoupon = useCallback(
|
|
async (couponCode) => {
|
|
if (!token || !selectedStoreId) throw new Error("Select a store first");
|
|
const updated = await apiApplyCoupon(token, selectedStoreId, couponCode);
|
|
setCart(updated);
|
|
|
|
return updated;
|
|
},
|
|
[token, selectedStoreId]
|
|
);
|
|
|
|
const applyPoints = useCallback(
|
|
async (useLoyaltyPoints) => {
|
|
if (!token || !selectedStoreId) throw new Error("Select a store first");
|
|
const updated = await apiApplyPoints(token, selectedStoreId, useLoyaltyPoints);
|
|
setCart(updated);
|
|
|
|
return updated;
|
|
},
|
|
[token, selectedStoreId]
|
|
);
|
|
|
|
const removeCoupon = useCallback(
|
|
async () => {
|
|
if (!token || !selectedStoreId) throw new Error("Select a store first");
|
|
const updated = await apiRemoveCoupon(token, selectedStoreId);
|
|
setCart(updated);
|
|
|
|
return updated;
|
|
},
|
|
[token, selectedStoreId]
|
|
);
|
|
|
|
const checkout = useCallback(
|
|
async () => {
|
|
if (!token || !selectedStoreId) throw new Error("Select a store first");
|
|
const result = await apiCheckout(token, { storeId: selectedStoreId });
|
|
|
|
return result;
|
|
},
|
|
[token, selectedStoreId]
|
|
);
|
|
|
|
const cancelCheckout = useCallback(
|
|
async () => {
|
|
if (!token || !selectedStoreId) return;
|
|
await apiCancelCheckout(token, selectedStoreId);
|
|
await refreshCart();
|
|
},
|
|
[token, selectedStoreId, refreshCart]
|
|
);
|
|
|
|
//Total number of items across all cart rows, used for the cart badge in the nav
|
|
const itemCount = cart?.items?.reduce((sum, i) => sum + i.quantity, 0) ?? 0;
|
|
|
|
return (
|
|
<CartContext.Provider
|
|
value={{
|
|
cart,
|
|
cartLoading,
|
|
cartError,
|
|
itemCount,
|
|
selectedStoreId,
|
|
setStoreId,
|
|
addItem,
|
|
updateItem,
|
|
removeItem,
|
|
clearCart,
|
|
applyCoupon,
|
|
applyPoints,
|
|
removeCoupon,
|
|
checkout,
|
|
cancelCheckout,
|
|
refreshCart,
|
|
}}
|
|
>
|
|
{children}
|
|
</CartContext.Provider>
|
|
);
|
|
}
|
|
|
|
//Hook to access cart state
|
|
//Must be used inside a CartProvider
|
|
export function useCart() {
|
|
const ctx = useContext(CartContext);
|
|
if (!ctx) throw new Error("useCart must be used within a CartProvider");
|
|
return ctx;
|
|
}
|