140 lines
4.1 KiB
JavaScript
140 lines
4.1 KiB
JavaScript
import { createContext, useContext, useEffect, useMemo, useReducer } from 'react';
|
|
|
|
const CartContext = createContext(undefined);
|
|
|
|
const initialState = {
|
|
items: [],
|
|
};
|
|
|
|
function cartReducer(state, action) {
|
|
switch (action.type) {
|
|
case 'HYDRATE':
|
|
return { ...state, items: action.payload };
|
|
case 'ADD_ITEM': {
|
|
const { item } = action.payload;
|
|
const existing = state.items.find((entry) => entry.id === item.id);
|
|
if (!existing) {
|
|
return { ...state, items: [...state.items, item] };
|
|
}
|
|
const updated = state.items.map((entry) =>
|
|
entry.id === item.id ? { ...entry, quantity: item.quantity, limit: item.limit } : entry
|
|
);
|
|
return { ...state, items: updated };
|
|
}
|
|
case 'UPDATE_QUANTITY': {
|
|
const { id, quantity } = action.payload;
|
|
const updated = state.items
|
|
.map((entry) => (entry.id === id ? { ...entry, quantity } : entry))
|
|
.filter((entry) => entry.quantity > 0);
|
|
return { ...state, items: updated };
|
|
}
|
|
case 'REMOVE_ITEM': {
|
|
const { id } = action.payload;
|
|
return { ...state, items: state.items.filter((entry) => entry.id !== id) };
|
|
}
|
|
case 'CLEAR':
|
|
return { ...state, items: [] };
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
const STORAGE_KEY = 'fullmoon.cart';
|
|
|
|
export function CartProvider({ children }) {
|
|
const [state, dispatch] = useReducer(cartReducer, initialState);
|
|
|
|
useEffect(() => {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return;
|
|
const parsed = JSON.parse(raw);
|
|
if (Array.isArray(parsed)) {
|
|
dispatch({ type: 'HYDRATE', payload: parsed });
|
|
}
|
|
} catch (error) {
|
|
console.warn('Cart hydration failed', error);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.items));
|
|
}, [state.items]);
|
|
|
|
const itemCount = useMemo(
|
|
() => state.items.reduce((sum, entry) => sum + entry.quantity, 0),
|
|
[state.items]
|
|
);
|
|
|
|
const subtotal = useMemo(
|
|
() => state.items.reduce((sum, entry) => sum + entry.price * entry.quantity, 0),
|
|
[state.items]
|
|
);
|
|
|
|
const addItem = (product) => {
|
|
if (!product?.id) return;
|
|
const price = Number(product.price || 0);
|
|
const maxPerOrder = Number(product.max_per_order || 0) > 0 ? Number(product.max_per_order) : Infinity;
|
|
const inventoryLimit =
|
|
typeof product.inventory_quantity === 'number' && product.inventory_quantity >= 0
|
|
? product.inventory_quantity
|
|
: Infinity;
|
|
const limit = Math.min(maxPerOrder, inventoryLimit);
|
|
if (limit <= 0) return;
|
|
|
|
const numericLimit = Number.isFinite(limit) ? limit : Infinity;
|
|
const existing = state.items.find((entry) => entry.id === product.id);
|
|
const nextQuantity = existing ? Math.min(existing.quantity + 1, numericLimit) : 1;
|
|
|
|
dispatch({
|
|
type: 'ADD_ITEM',
|
|
payload: {
|
|
item: {
|
|
id: product.id,
|
|
slug: product.slug,
|
|
name: product.name,
|
|
price,
|
|
quantity: nextQuantity,
|
|
imageUrl: product.imageUrl,
|
|
limit: Number.isFinite(limit) ? limit : null,
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
const updateQuantity = (id, quantity) => {
|
|
if (quantity < 0) return;
|
|
const entry = state.items.find((item) => item.id === id);
|
|
if (!entry) return;
|
|
const limit = Number.isFinite(entry.limit) ? entry.limit : Infinity;
|
|
const clamped = Math.min(quantity, limit);
|
|
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity: clamped } });
|
|
};
|
|
|
|
const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: { id } });
|
|
const clearCart = () => dispatch({ type: 'CLEAR' });
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
items: state.items,
|
|
addItem,
|
|
updateQuantity,
|
|
removeItem,
|
|
clearCart,
|
|
itemCount,
|
|
subtotal,
|
|
}),
|
|
[state.items, itemCount, subtotal]
|
|
);
|
|
|
|
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
|
|
}
|
|
|
|
export function useCart() {
|
|
const context = useContext(CartContext);
|
|
if (!context) {
|
|
throw new Error('useCart must be used within a CartProvider');
|
|
}
|
|
return context;
|
|
}
|