From 6138690b898319246d781d4449b05a008db426ed Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Tue, 23 Jun 2026 08:38:41 +0000 Subject: [PATCH] Deploy --- src/components/shop/CartDrawer.tsx | 181 +++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/components/shop/CartDrawer.tsx diff --git a/src/components/shop/CartDrawer.tsx b/src/components/shop/CartDrawer.tsx new file mode 100644 index 0000000..3f8d265 --- /dev/null +++ b/src/components/shop/CartDrawer.tsx @@ -0,0 +1,181 @@ +'use client' +import { useState, useEffect, useCallback } from 'react' +import Link from 'next/link' +import { ArrowRight, Minus, Package, Plus, ShoppingCart, Trash2, X } from 'lucide-react' +import { broadcastCartCount } from '@/hooks/useMedusaCart' + +const CURRENCY_SYMBOLS: Record = { zar: 'R', usd: '$', eur: '€', gbp: '£', kes: 'KSh', ngn: '₦', ghs: '₵' } +const formatPrice = (amount: number, code?: string) => { + const sym = CURRENCY_SYMBOLS[(code ?? '').toLowerCase()] ?? (code?.toUpperCase() ?? '') + return `${sym}${(amount / 100).toFixed(2)}` +} +const PUB_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' + +async function medusaFetch(path: string, init?: RequestInit): Promise { + const res = await fetch(`/api/medusa${path}`, { + ...init, + headers: { 'content-type': 'application/json', 'x-publishable-api-key': PUB_KEY, ...(init?.headers ?? {}) }, + signal: AbortSignal.timeout(30000), + }) + if (!res.ok) throw new Error(`Medusa ${res.status}`) + return res.json() +} + +interface CartItem { id: string; title: string; product_title?: string; thumbnail: string | null; quantity: number; unit_price: number; subtotal: number; variant: { id: string; title: string } | null } +interface Cart { id: string; items: CartItem[]; subtotal: number; region: { currency_code: string } | null } + +export default function CartDrawer() { + const [isOpen, setIsOpen] = useState(false) + const [cart, setCart] = useState(null) + const [loading, setLoading] = useState(false) + + const fetchCart = useCallback(async () => { + const cartId = localStorage.getItem('medusa_cart_id') + if (!cartId) { setCart(null); return } + setLoading(true) + try { + const { cart: c } = await medusaFetch<{ cart: Cart }>(`/store/carts/${cartId}`) + setCart(c) + } catch { + setCart(null) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + const open = () => { setIsOpen(true); fetchCart() } + window.addEventListener('cart-drawer-open', open) + return () => window.removeEventListener('cart-drawer-open', open) + }, [fetchCart]) + + useEffect(() => { + const handler = () => { if (isOpen) fetchCart() } + window.addEventListener('medusa-cart-updated', handler) + return () => window.removeEventListener('medusa-cart-updated', handler) + }, [isOpen, fetchCart]) + + useEffect(() => { + document.body.style.overflow = isOpen ? 'hidden' : '' + return () => { document.body.style.overflow = '' } + }, [isOpen]) + + const close = () => setIsOpen(false) + + const removeItem = async (itemId: string) => { + if (!cart) return + await medusaFetch(`/store/carts/${cart.id}/line-items/${itemId}`, { method: 'DELETE' }) + const newItems = cart.items.filter(i => i.id !== itemId) + setCart(prev => prev ? { ...prev, items: newItems, subtotal: newItems.reduce((s, i) => s + i.subtotal, 0) } : null) + broadcastCartCount(newItems.reduce((s, i) => s + i.quantity, 0)) + } + + const updateQty = async (itemId: string, quantity: number) => { + if (!cart) return + if (quantity < 1) { removeItem(itemId); return } + const { cart: updated } = await medusaFetch<{ cart: Cart }>( + `/store/carts/${cart.id}/line-items/${itemId}`, + { method: 'POST', body: JSON.stringify({ quantity }) } + ) + setCart(updated) + broadcastCartCount(updated.items?.reduce((s, i) => s + i.quantity, 0) ?? 0) + } + + const items = cart?.items ?? [] + const currencyCode = cart?.region?.currency_code + + return ( + <> + {isOpen && ( +