Primer corte de checkout real, a proposito acotado: el carrito pasa de estado 100% local (localStorage con productos completos) a sincronizar con un cart_id real de Medusa via /api/cart/* (rutas puente same-origin, mismo patron que /api/search -- MEDUSA_BACKEND_URL es DNS interno del cluster, no resuelve desde el navegador). Formas de respuesta de la API verificadas en vivo con un carrito de prueba real antes de escribir el mapeo (no asumidas contra documentacion): agregar el mismo variant_id dos veces hace merge de cantidad automatico (no crea linea duplicada); eliminar una linea devuelve el carrito actualizado en 'parent', no en 'cart' como los demas endpoints. La firma publica de useCartStore (addItem/removeItem/updateQuantity/ items) no cambia, asi que ningun componente consumidor (cart-drawer, navbar, product-card, product-purchase, mobile-add-to-cart) necesito tocarse. Sigue cerrando por WhatsApp -- direccion/envio/pago quedan para los siguientes cortes del ADR 0009.
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { create } from "zustand";
|
|
import { createJSONStorage, persist } from "zustand/middleware";
|
|
import type { CartLine } from "@/lib/headless/providers/medusa-cart";
|
|
|
|
// ADR 0009: carrito real conectado a un cart_id de Medusa (Store API,
|
|
// via /api/cart/*) en vez de estado 100% local -- precios y existencia
|
|
// de linea son los que confirma el servidor, no un calculo optimista
|
|
// en el cliente. Primer corte a proposito: sin UI de direccion/envio
|
|
// todavia, el checkout sigue cerrando por WhatsApp (CartDrawer).
|
|
export type CartItem = CartLine;
|
|
|
|
interface CartState {
|
|
cartId: string | null;
|
|
items: CartItem[];
|
|
isOpen: boolean;
|
|
hasHydrated: boolean;
|
|
openCart: () => void;
|
|
closeCart: () => void;
|
|
setHasHydrated: (value: boolean) => void;
|
|
addItem: (product: { id: string }, variantId?: string) => Promise<void>;
|
|
removeItem: (productId: string) => Promise<void>;
|
|
updateQuantity: (productId: string, quantity: number) => Promise<void>;
|
|
clearCart: () => Promise<void>;
|
|
syncCart: () => Promise<void>;
|
|
}
|
|
|
|
async function postJson(url: string, body: unknown) {
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
export const useCartStore = create<CartState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
cartId: null,
|
|
items: [],
|
|
isOpen: false,
|
|
hasHydrated: false,
|
|
openCart: () => set({ isOpen: true }),
|
|
closeCart: () => set({ isOpen: false }),
|
|
setHasHydrated: (value) => set({ hasHydrated: value }),
|
|
|
|
addItem: async (product, variantId) => {
|
|
set({ isOpen: true });
|
|
if (!variantId) return;
|
|
|
|
let cartId = get().cartId;
|
|
if (!cartId) {
|
|
const cart = await (await fetch("/api/cart", { method: "POST" })).json();
|
|
if (!cart.cartId) return;
|
|
cartId = cart.cartId;
|
|
set({ cartId, items: cart.items ?? [] });
|
|
}
|
|
|
|
const cart = await postJson("/api/cart/items", { cartId, variantId, quantity: 1 });
|
|
if (cart.items) set({ items: cart.items });
|
|
},
|
|
|
|
removeItem: async (productId) => {
|
|
const { cartId, items } = get();
|
|
const line = items.find((item) => item.product.id === productId);
|
|
if (!cartId || !line) return;
|
|
const response = await fetch(`/api/cart/items/${line.lineItemId}?cartId=${cartId}`, { method: "DELETE" });
|
|
const cart = await response.json();
|
|
if (cart.items) set({ items: cart.items });
|
|
},
|
|
|
|
updateQuantity: async (productId, quantity) => {
|
|
if (quantity <= 0) return get().removeItem(productId);
|
|
const { cartId, items } = get();
|
|
const line = items.find((item) => item.product.id === productId);
|
|
if (!cartId || !line) return;
|
|
const cart = await postJson(`/api/cart/items/${line.lineItemId}`, { cartId, quantity });
|
|
if (cart.items) set({ items: cart.items });
|
|
},
|
|
|
|
clearCart: async () => {
|
|
const { cartId, items } = get();
|
|
if (!cartId) return;
|
|
await Promise.all(
|
|
items.map((item) => fetch(`/api/cart/items/${item.lineItemId}?cartId=${cartId}`, { method: "DELETE" })),
|
|
);
|
|
set({ items: [] });
|
|
},
|
|
|
|
// Al recargar la pagina, el cart_id guardado en localStorage puede
|
|
// llevar dias -- se refresca contra el servidor en vez de confiar en
|
|
// el ultimo snapshot local (precios/stock pudieron cambiar).
|
|
syncCart: async () => {
|
|
const cartId = get().cartId;
|
|
if (!cartId) return;
|
|
try {
|
|
const response = await fetch(`/api/cart?cartId=${cartId}`);
|
|
if (!response.ok) return;
|
|
const cart = await response.json();
|
|
if (cart.items) set({ items: cart.items });
|
|
} catch {
|
|
// Sin red: se queda con el ultimo estado local conocido.
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: "ari-shopping-cart-v5",
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({ cartId: state.cartId, items: state.items }),
|
|
onRehydrateStorage: () => (state) => {
|
|
state?.setHasHydrated(true);
|
|
void state?.syncCart();
|
|
},
|
|
},
|
|
),
|
|
);
|