This commit is contained in:
Vula Builder
2026-08-01 16:13:33 +00:00
parent 27ca13cddd
commit 5b852a93ee
+134
View File
@@ -0,0 +1,134 @@
'use client'
import { useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import Link from 'next/link'
import { Customer, useCustomerAuth } from '@/hooks/useCustomerAuth'
const COMMERCE_URL = process.env.NEXT_PUBLIC_VULA_COMMERCE_URL || 'https://ide.vulai.co.za'
const PROJECT_ID = process.env.NEXT_PUBLIC_VULA_PROJECT_ID || ''
const STATUS_LABELS: Record<string, string> = {
pending: 'Pending', confirmed: 'Confirmed', processing: 'Processing',
shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled',
}
const STATUS_COLOURS: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800',
confirmed: 'bg-blue-100 text-blue-800',
processing: 'bg-blue-100 text-blue-800',
shipped: 'bg-purple-100 text-purple-800',
delivered: 'bg-green-100 text-green-800',
cancelled: 'bg-red-100 text-red-800',
}
const fmt = (n: number) => `R${(n / 100).toFixed(2)}`
const fmtDate = (s: string) => new Date(s).toLocaleDateString('en-ZA', { year: 'numeric', month: 'short', day: 'numeric' })
interface Order {
id: string
order_number: string
status: string
total: number
created_at: string
items: Array<{ title: string; quantity: number; unit_price: number }>
}
function AccountShell({ customer, onLogout, children }: { customer: Customer | null; onLogout: () => void; children: React.ReactNode }) {
const pathname = usePathname()
const navLink = (href: string, label: string) => (
<Link href={href} className={`px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors ${pathname === href ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}`}>
{label}
</Link>
)
return (
<main className="min-h-screen bg-background px-4 py-20">
<div className="max-w-2xl mx-auto">
<div className="flex items-start justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-foreground">My Account</h1>
{customer && <p className="text-muted-foreground mt-1 text-sm">{customer.first_name} {customer.last_name}</p>}
</div>
<button onClick={onLogout} className="text-sm text-muted-foreground hover:text-foreground transition-colors">Sign out</button>
</div>
<nav className="flex border-b border-border mb-8">
{navLink('/account/profile', 'Profile')}
{navLink('/account/orders', 'Orders')}
</nav>
{children}
</div>
</main>
)
}
export default function AccountOrdersPage() {
const { customer, token, isAuthenticated, loading: authLoading, logout } = useCustomerAuth()
const router = useRouter()
const [orders, setOrders] = useState<Order[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!authLoading && !isAuthenticated) router.replace('/account/login')
}, [authLoading, isAuthenticated, router])
useEffect(() => {
if (!token) return
fetch(`${COMMERCE_URL}/api/commerce/${PROJECT_ID}/customers/orders`, {
headers: { Authorization: `Bearer ${token}` },
})
.then(r => r.json())
.then(d => { if (d.success) setOrders(d.orders) })
.finally(() => setLoading(false))
}, [token])
const handleLogout = () => { logout(); router.push('/') }
if (authLoading) return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
)
return (
<AccountShell customer={customer} onLogout={handleLogout}>
{loading ? (
<div className="flex justify-center py-16">
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
) : orders.length === 0 ? (
<div className="text-center py-16">
<p className="text-muted-foreground mb-4">No orders yet</p>
<Link href="/products" className="inline-block bg-primary text-primary-foreground px-6 py-2.5 rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
Start shopping
</Link>
</div>
) : (
<div className="space-y-4">
{orders.map(order => (
<div key={order.id} className="border border-border rounded-xl p-5 bg-card">
<div className="flex items-center justify-between mb-3">
<div>
<span className="font-semibold text-foreground">{order.order_number}</span>
<span className="text-muted-foreground text-sm ml-3">{fmtDate(order.created_at)}</span>
</div>
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${STATUS_COLOURS[order.status] ?? 'bg-muted text-muted-foreground'}`}>
{STATUS_LABELS[order.status] ?? order.status}
</span>
</div>
<div className="space-y-1 mb-3">
{(order.items ?? []).map((item, i) => (
<div key={i} className="flex justify-between text-sm">
<span className="text-foreground">{item.title} <span className="text-muted-foreground">x{item.quantity}</span></span>
<span className="text-foreground">{fmt(item.unit_price * item.quantity)}</span>
</div>
))}
</div>
<div className="flex justify-between pt-3 border-t border-border">
<span className="text-sm font-medium text-foreground">Total</span>
<span className="font-bold text-foreground">{fmt(order.total)}</span>
</div>
</div>
))}
</div>
)}
</AccountShell>
)
}