chore: initial import

This commit is contained in:
2025-11-09 00:26:00 -06:00
commit 67fb60e6ca
76 changed files with 3925 additions and 0 deletions

View 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;
}