From f3e621e1537ac0c30de2f7398ce45064858dc818 Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Tue, 23 Jun 2026 08:38:36 +0000 Subject: [PATCH] Deploy --- src/hooks/useMedusaCart.ts | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/hooks/useMedusaCart.ts diff --git a/src/hooks/useMedusaCart.ts b/src/hooks/useMedusaCart.ts new file mode 100644 index 0000000..94b4ecc --- /dev/null +++ b/src/hooks/useMedusaCart.ts @@ -0,0 +1,127 @@ +'use client' +import { useState, useCallback, useEffect } from 'react' + +const PUB_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' +const BASE = '/api/medusa' +const COUNT_KEY = 'medusa_cart_count' +const CART_EVENT = 'medusa-cart-updated' + +// Broadcast a new count to every useMedusaCart instance on the page instantly. +// Also writes to localStorage so the count survives page navigation / refresh. +export function broadcastCartCount(count: number) { + if (typeof window === 'undefined') return + localStorage.setItem(COUNT_KEY, String(count)) + window.dispatchEvent(new CustomEvent(CART_EVENT, { detail: { count } })) +} + +export function openCartDrawer() { + if (typeof window === 'undefined') return + window.dispatchEvent(new CustomEvent('cart-drawer-open')) +} + +async function medusaFetch(path: string, init?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { + 'content-type': 'application/json', + 'x-publishable-api-key': PUB_KEY, + ...(init?.headers ?? {}), + }, + signal: AbortSignal.timeout(30000), + }) + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Medusa ${res.status}: ${body}`) + } + return res.json() +} + +export function useMedusaCart() { + // Start at null/0 so server and client render identically (no hydration mismatch). + // localStorage is loaded in useEffect (client-only). + const [cartId, setCartId] = useState(null) + const [itemCount, setItemCount] = useState(0) + + // Bootstrap from localStorage after mount. + useEffect(() => { + const storedId = localStorage.getItem('medusa_cart_id') + if (storedId) setCartId(storedId) + const storedCount = parseInt(localStorage.getItem(COUNT_KEY) || '0', 10) + if (storedCount > 0) setItemCount(storedCount) + }, []) + + // Listen for count changes broadcast from any component (shop add, cart delete, etc.) + useEffect(() => { + const handler = (e: Event) => setItemCount((e as CustomEvent).detail.count) + window.addEventListener(CART_EVENT, handler) + return () => window.removeEventListener(CART_EVENT, handler) + }, []) + + // Hydrate from live Medusa cart on mount for accuracy after hard refresh. + useEffect(() => { + const id = localStorage.getItem('medusa_cart_id') + if (!id) return + medusaFetch<{ cart: { items: Array<{ quantity: number }> } }>(`/store/carts/${id}`) + .then(({ cart }) => { + const total = cart.items?.reduce((s, i) => s + i.quantity, 0) ?? 0 + broadcastCartCount(total) + }) + .catch(() => { + // Stale cart — clear count so nav badge resets to 0 + localStorage.removeItem('medusa_cart_id') + localStorage.removeItem('medusa_cart_count') + broadcastCartCount(0) + }) + }, []) + + const createCart = useCallback(async () => { + let regionId: string | undefined + try { + const data = await medusaFetch<{ regions: Array<{ id: string }> }>('/store/regions?limit=1') + regionId = data.regions?.[0]?.id + } catch { /* proceed without region_id */ } + const data = await medusaFetch<{ cart: { id: string } }>('/store/carts', { + method: 'POST', + body: JSON.stringify(regionId ? { region_id: regionId } : {}), + }) + localStorage.setItem('medusa_cart_id', data.cart.id) + setCartId(data.cart.id) + return data.cart + }, []) + + const addToCart = useCallback(async (variantId: string, quantity = 1) => { + const postLineItem = async (cid: string) => + medusaFetch(`/store/carts/${cid}/line-items`, { + method: 'POST', + body: JSON.stringify({ variant_id: variantId, quantity }), + }) + try { + let id = cartId + if (!id) { + const cart = await createCart() + id = cart.id + } + try { + await postLineItem(id) + } catch (err: any) { + if (String(err?.message).includes('404')) { + // Stale cart ID — clear and retry with a fresh cart + localStorage.removeItem('medusa_cart_id') + localStorage.removeItem(COUNT_KEY) + setCartId(null) + const fresh = await createCart() + await postLineItem(fresh.id) + } else { + throw err + } + } + const newCount = parseInt(localStorage.getItem(COUNT_KEY) || '0', 10) + quantity + broadcastCartCount(newCount) + } catch (err) { + console.error('Add to cart failed:', err) + alert('Could not add to cart. Please try again.') + } + }, [cartId, createCart]) + + return { cartId, itemCount, addToCart, createCart } +}