chore: initial import
This commit is contained in:
139
frontend/context/CartContext.js
Normal file
139
frontend/context/CartContext.js
Normal file
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user