195 lines
4.9 KiB
TypeScript
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;
|
|
}
|