trptk/components/cart/CartContext.tsx
2026-02-24 17:14:07 +01:00

195 lines
4.9 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import type { MedusaCart } from "@/lib/medusa";
const CART_ID_KEY = "trptk_cart_id";
async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, {
...init,
headers: { "Content-Type": "application/json", ...init?.headers },
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
const msg = body?.error ?? `Cart API error: ${res.status}`;
console.error("[cart]", msg);
throw new Error(msg);
}
return res.json();
}
type CartState = {
cart: MedusaCart | null;
isLoading: boolean;
isAdding: boolean;
itemCount: number;
drawerOpen: boolean;
setDrawerOpen: (open: boolean) => void;
addItem: (variantId: string, quantity?: number) => Promise<void>;
removeItem: (lineItemId: string) => Promise<void>;
updateItem: (lineItemId: string, quantity: number) => Promise<void>;
applyPromo: (code: string) => Promise<void>;
removePromo: (code: string) => Promise<void>;
resetCart: () => void;
};
const CartContext = createContext<CartState | null>(null);
export function CartProvider({ children }: { children: ReactNode }) {
const [cart, setCart] = useState<MedusaCart | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isAdding, setIsAdding] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
// Restore cart from localStorage on mount
useEffect(() => {
async function restore() {
const cartId = localStorage.getItem(CART_ID_KEY);
if (cartId) {
try {
const existing = await apiJson<MedusaCart>(`/api/cart?id=${cartId}`);
setCart(existing);
} catch {
localStorage.removeItem(CART_ID_KEY);
}
}
setIsLoading(false);
}
restore();
}, []);
const ensureCart = useCallback(async (): Promise<string> => {
if (cart?.id) return cart.id;
const newCart = await apiJson<MedusaCart>("/api/cart", { method: "POST" });
localStorage.setItem(CART_ID_KEY, newCart.id);
setCart(newCart);
return newCart.id;
}, [cart?.id]);
const addItem = useCallback(
async (variantId: string, quantity = 1) => {
setIsAdding(true);
try {
const cartId = await ensureCart();
const updated = await apiJson<MedusaCart>(`/api/cart/${cartId}/items`, {
method: "POST",
body: JSON.stringify({ variant_id: variantId, quantity }),
});
setCart(updated);
setDrawerOpen(true);
} finally {
setIsAdding(false);
}
},
[ensureCart],
);
const removeItem = useCallback(
async (lineItemId: string) => {
if (!cart?.id) return;
const updated = await apiJson<MedusaCart>(`/api/cart/${cart.id}/items/${lineItemId}`, {
method: "DELETE",
});
setCart(updated);
},
[cart?.id],
);
const updateItem = useCallback(
async (lineItemId: string, quantity: number) => {
if (!cart?.id) return;
if (quantity <= 0) {
await removeItem(lineItemId);
return;
}
const updated = await apiJson<MedusaCart>(`/api/cart/${cart.id}/items/${lineItemId}`, {
method: "POST",
body: JSON.stringify({ quantity }),
});
setCart(updated);
},
[cart?.id, removeItem],
);
const applyPromo = useCallback(
async (code: string) => {
if (!cart?.id) return;
const updated = await apiJson<MedusaCart>(`/api/cart/${cart.id}/promotions`, {
method: "POST",
body: JSON.stringify({ code }),
});
setCart(updated);
},
[cart?.id],
);
const removePromo = useCallback(
async (code: string) => {
if (!cart?.id) return;
const updated = await apiJson<MedusaCart>(`/api/cart/${cart.id}/promotions`, {
method: "DELETE",
body: JSON.stringify({ code }),
});
setCart(updated);
},
[cart?.id],
);
const resetCart = useCallback(() => {
setCart(null);
localStorage.removeItem(CART_ID_KEY);
}, []);
const itemCount = useMemo(
() => cart?.items?.reduce((sum, item) => sum + item.quantity, 0) ?? 0,
[cart?.items],
);
const value = useMemo<CartState>(
() => ({
cart,
isLoading,
isAdding,
itemCount,
drawerOpen,
setDrawerOpen,
addItem,
removeItem,
updateItem,
applyPromo,
removePromo,
resetCart,
}),
[
cart,
isLoading,
isAdding,
itemCount,
drawerOpen,
setDrawerOpen,
addItem,
removeItem,
updateItem,
applyPromo,
removePromo,
resetCart,
],
);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
export function useCart() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error("useCart must be used within CartProvider");
return ctx;
}