This commit is contained in:
Vula Builder
2026-06-23 08:47:11 +00:00
parent aa8bc03add
commit ef17c7933c
+179
View File
@@ -0,0 +1,179 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { ArrowLeft, Minus, Package, Plus, ShoppingCart, Trash2 } from 'lucide-react'
import { broadcastCartCount } from '@/hooks/useMedusaCart'
const CURRENCY_SYMBOLS: Record<string, string> = { 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 || ''
const BASE = '/api/medusa'
async function medusaFetch<T>(path: string, init?: RequestInit): Promise<T> {
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) 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
variant: { id: string; title: string } | null
}
interface Cart {
id: string
items: CartItem[]
subtotal: number
region: { currency_code: string } | null
}
export default function CartPage() {
const [cart, setCart] = useState<Cart | null>(null)
const [loading, setLoading] = useState(true)
const cartId = typeof window !== 'undefined' ? localStorage.getItem('medusa_cart_id') : null
useEffect(() => {
if (!cartId) { setLoading(false); return }
medusaFetch<{ cart: Cart }>(`/store/carts/${cartId}`)
.then(({ cart: c }) => setCart(c))
.catch(() => setCart(null))
.finally(() => setLoading(false))
}, [cartId])
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 } : null)
broadcastCartCount(newItems.reduce((s, i) => s + i.quantity, 0))
}
const updateQuantity = async (itemId: string, quantity: number) => {
if (!cart) return
if (quantity < 1) return removeItem(itemId)
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 currency = cart?.region?.currency_code?.toUpperCase() ?? 'USD'
const items = cart?.items ?? []
const subtotal = cart?.subtotal ?? 0
if (loading) return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary" />
</div>
)
return (
<main className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-12 max-w-5xl">
<div className="flex items-center gap-4 mb-8">
<Link href="/products" className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors text-sm">
<ArrowLeft className="h-4 w-4" />
Continue Shopping
</Link>
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
<ShoppingCart className="h-8 w-8" />
Your Cart
</h1>
</div>
{items.length === 0 ? (
<div className="text-center py-20">
<ShoppingCart className="h-16 w-16 mx-auto mb-4 text-muted-foreground/40" />
<p className="text-muted-foreground text-lg mb-6">Your cart is empty.</p>
<Link href="/products">
<button className="bg-primary text-primary-foreground py-3 px-8 rounded-lg hover:bg-primary/90 transition-colors font-medium">
Browse Products
</button>
</Link>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-4">
{items.map((item) => (
<div key={item.id} className="flex gap-4 bg-card border border-border rounded-xl p-4">
{item.thumbnail ? (
<img src={item.thumbnail} alt={item.product_title || item.title} className="w-20 h-20 object-cover rounded-lg flex-shrink-0" />
) : (
<div className="w-20 h-20 rounded-lg flex-shrink-0 bg-muted flex items-center justify-center">
<Package className="h-8 w-8 text-muted-foreground/40" />
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground line-clamp-1">{item.product_title || item.title}</h3>
{item.variant?.title && item.variant.title !== 'Default Title' && (
<p className="text-sm text-muted-foreground">{item.variant.title}</p>
)}
<p className="text-primary font-bold mt-1">
{formatPrice(item.unit_price ?? 0, cart?.region?.currency_code)}
</p>
</div>
<div className="flex flex-col items-end gap-2 flex-shrink-0">
<button onClick={() => removeItem(item.id)} className="text-muted-foreground hover:text-destructive transition-colors">
<Trash2 className="h-4 w-4" />
</button>
<div className="flex items-center gap-1 border border-border rounded-lg overflow-hidden">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)} className="px-2 py-1 hover:bg-muted transition-colors">
<Minus className="h-3 w-3" />
</button>
<span className="px-3 py-1 text-sm font-medium border-x border-border">{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)} className="px-2 py-1 hover:bg-muted transition-colors">
<Plus className="h-3 w-3" />
</button>
</div>
</div>
</div>
))}
</div>
<div className="lg:col-span-1">
<div className="bg-card border border-border rounded-xl p-6 sticky top-6">
<h2 className="text-xl font-bold mb-4 text-foreground">Order Summary</h2>
<div className="space-y-3 text-sm text-muted-foreground mb-4">
<div className="flex justify-between">
<span>Subtotal ({items.length} items)</span>
<span className="text-foreground font-medium">{formatPrice(subtotal, cart?.region?.currency_code)}</span>
</div>
<div className="flex justify-between">
<span>Shipping</span>
<span>Calculated at checkout</span>
</div>
</div>
<div className="border-t border-border pt-4 flex justify-between font-bold text-lg mb-6">
<span>Total</span>
<span className="text-primary">{formatPrice(subtotal, cart?.region?.currency_code)}</span>
</div>
<Link href="/checkout" className="block w-full bg-primary text-primary-foreground py-3 rounded-lg hover:bg-primary/90 transition-colors font-semibold text-center">
Proceed to Checkout
</Link>
<Link href="/products" className="block text-center mt-3 text-sm text-muted-foreground hover:text-foreground transition-colors">
Continue Shopping
</Link>
</div>
</div>
</div>
)}
</div>
</main>
)
}