This commit is contained in:
Vula Builder
2026-07-31 14:12:35 +00:00
parent 9cf87fef25
commit 011474b63f
+143
View File
@@ -0,0 +1,143 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useVulaCart } from '@/hooks/useVulaCart'
import { useCustomerAuth } from '@/hooks/useCustomerAuth'
import { CircleCheck, Loader2 } from 'lucide-react'
function fmt(amount: number) { return `R${(amount / 100).toFixed(2)}` }
export default function CheckoutPage() {
const { items, subtotal, itemCount, checkout, loading, error } = useVulaCart()
const { customer, isAuthenticated } = useCustomerAuth()
const router = useRouter()
const [done, setDone] = useState<{ orderNumber: string } | null>(null)
const [form, setForm] = useState({
customerName: '', customerEmail: '', customerPhone: '',
fulfillmentType: 'delivery', notes: '',
address_line1: '', city: '', province: '', postal_code: ''
})
useEffect(() => {
if (!customer) return
setForm(f => ({
...f,
customerName: f.customerName || [customer.first_name, customer.last_name].filter(Boolean).join(' '),
customerEmail: f.customerEmail || customer.email,
customerPhone: f.customerPhone || (customer.phone ?? ''),
address_line1: f.address_line1 || (customer.address_line1 ?? ''),
city: f.city || (customer.city ?? ''),
province: f.province || (customer.province ?? ''),
postal_code: f.postal_code || (customer.postal_code ?? ''),
}))
}, [customer])
const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
setForm(f => ({ ...f, [k]: e.target.value }))
if (done) return (
<main className="min-h-screen bg-background flex flex-col items-center justify-center gap-6 text-center px-4">
<CircleCheck className="w-20 h-20 text-emerald-500" />
<h1 className="text-3xl font-bold">Order Placed!</h1>
<p className="text-muted-foreground text-lg">Order <strong>{done.orderNumber}</strong> has been received. We'll be in touch shortly.</p>
<Link href="/products" className="bg-primary text-primary-foreground px-6 py-3 rounded-lg font-medium hover:opacity-90 transition-opacity">
Continue Shopping
</Link>
</main>
)
if (!itemCount) return (
<main className="min-h-screen bg-background flex flex-col items-center justify-center gap-6 text-center px-4">
<p className="text-muted-foreground">Your cart is empty.</p>
<Link href="/products" className="bg-primary text-primary-foreground px-6 py-3 rounded-lg font-medium">Browse Products</Link>
</main>
)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
try {
const order = await checkout({
customerName: form.customerName,
customerEmail: form.customerEmail,
customerPhone: form.customerPhone || undefined,
fulfillmentType: form.fulfillmentType,
notes: form.notes || undefined,
shippingAddress: form.fulfillmentType !== 'pickup' ? {
address_line1: form.address_line1, city: form.city,
province: form.province || undefined, postal_code: form.postal_code || undefined
} : undefined
})
setDone({ orderNumber: order.order_number })
} catch {}
}
const inputCls = "w-full border border-border rounded-lg px-3 py-2.5 bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary/40"
const labelCls = "block text-sm font-medium mb-1"
return (
<main className="min-h-screen bg-background py-12 px-4">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8">Checkout</h1>
<form onSubmit={handleSubmit} className="grid md:grid-cols-3 gap-8">
<div className="md:col-span-2 space-y-6">
<section className="bg-card rounded-xl border border-border p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="font-semibold text-lg">Your Details</h2>
{isAuthenticated && (
<span className="text-xs text-muted-foreground">
Signed in · <Link href="/account/orders" className="hover:underline">My orders</Link>
</span>
)}
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div><label className={labelCls}>Full Name *</label><input required className={inputCls} value={form.customerName} onChange={set('customerName')} placeholder="Full name" /></div>
<div><label className={labelCls}>Email *</label><input required type="email" className={inputCls} value={form.customerEmail} onChange={set('customerEmail')} placeholder="email@example.com" /></div>
<div><label className={labelCls}>Phone</label><input className={inputCls} value={form.customerPhone} onChange={set('customerPhone')} placeholder="0XX XXX XXXX" /></div>
</div>
</section>
<section className="bg-card rounded-xl border border-border p-6 space-y-4">
<h2 className="font-semibold text-lg">Delivery</h2>
<div>
<label className={labelCls}>Fulfilment Type</label>
<select className={inputCls} value={form.fulfillmentType} onChange={set('fulfillmentType')}>
<option value="delivery">Home Delivery</option>
<option value="pickup">Collect In-Store</option>
</select>
</div>
{form.fulfillmentType === 'delivery' && (
<div className="space-y-4">
<div><label className={labelCls}>Street Address *</label><input required className={inputCls} value={form.address_line1} onChange={set('address_line1')} placeholder="123 Main Street" /></div>
<div className="grid sm:grid-cols-3 gap-4">
<div className="sm:col-span-1"><label className={labelCls}>City *</label><input required className={inputCls} value={form.city} onChange={set('city')} placeholder="Cape Town" /></div>
<div><label className={labelCls}>Province</label><input className={inputCls} value={form.province} onChange={set('province')} placeholder="Western Cape" /></div>
<div><label className={labelCls}>Postal Code</label><input className={inputCls} value={form.postal_code} onChange={set('postal_code')} placeholder="8001" /></div>
</div>
</div>
)}
<div><label className={labelCls}>Order Notes</label><textarea className={inputCls} rows={2} value={form.notes} onChange={set('notes')} placeholder="Special instructions, allergies, delivery instructions..." /></div>
</section>
{error && <p className="text-destructive text-sm bg-destructive/10 rounded-lg px-4 py-3">{error}</p>}
</div>
<div className="bg-card rounded-xl border border-border p-6 h-fit space-y-4">
<h3 className="font-semibold text-lg">Order Summary</h3>
{items.map(i => (
<div key={i.variantId} className="flex justify-between text-sm gap-2">
<span className="text-muted-foreground truncate">{i.title}{i.variant_title !== 'Default' ? ` · ${i.variant_title}` : ''} ×{i.quantity}</span>
<span className="shrink-0">{fmt(i.unit_price * i.quantity)}</span>
</div>
))}
<div className="border-t border-border pt-4 flex justify-between font-semibold"><span>Total</span><span>{fmt(subtotal)}</span></div>
<p className="text-xs text-muted-foreground">Payment will be arranged by the merchant upon order confirmation.</p>
<button type="submit" disabled={loading} className="w-full bg-primary text-primary-foreground py-3 rounded-lg font-medium hover:opacity-90 transition-opacity disabled:opacity-50 flex items-center justify-center gap-2">
{loading ? <><Loader2 className="w-4 h-4 animate-spin" />Processing...</> : 'Place Order'}
</button>
</div>
</form>
</div>
</main>
)
}