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