Deploy
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user