This commit is contained in:
Vula Builder
2026-07-31 18:44:30 +00:00
parent e172ef5a6e
commit d6754b48a9
+165
View File
@@ -0,0 +1,165 @@
'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 || ''
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 AccountProfilePage() {
const { customer, isAuthenticated, loading: authLoading, logout, updateProfile } = useCustomerAuth()
const router = useRouter()
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [error, setError] = useState<string | null>(null)
const [form, setForm] = useState({
first_name: '', last_name: '', phone: '',
address_line1: '', address_line2: '', city: '', province: '', postal_code: '',
current_password: '', new_password: '', confirm_password: '',
})
useEffect(() => {
if (!authLoading && !isAuthenticated) router.replace('/account/login')
}, [authLoading, isAuthenticated, router])
useEffect(() => {
if (!customer) return
setForm(f => ({
...f,
first_name: customer.first_name ?? '',
last_name: customer.last_name ?? '',
phone: customer.phone ?? '',
address_line1: customer.address_line1 ?? '',
address_line2: customer.address_line2 ?? '',
city: customer.city ?? '',
province: customer.province ?? '',
postal_code: customer.postal_code ?? '',
}))
}, [customer])
const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement>) =>
setForm(f => ({ ...f, [k]: e.target.value }))
const handleLogout = () => { logout(); router.push('/') }
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
if (form.new_password && form.new_password !== form.confirm_password) {
setError('New passwords do not match'); return
}
setSaving(true)
try {
await updateProfile({
first_name: form.first_name || undefined,
last_name: form.last_name || undefined,
phone: form.phone || undefined,
address_line1: form.address_line1 || undefined,
address_line2: form.address_line2 || undefined,
city: form.city || undefined,
province: form.province || undefined,
postal_code: form.postal_code || undefined,
...(form.new_password ? { password: form.current_password, new_password: form.new_password } : {}),
})
setSaved(true)
setTimeout(() => setSaved(false), 3000)
setForm(f => ({ ...f, current_password: '', new_password: '', confirm_password: '' }))
} catch (e: any) { setError(e.message) }
finally { setSaving(false) }
}
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>
)
const inputCls = "w-full px-3 py-2.5 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"
const labelCls = "block text-sm font-medium mb-1.5 text-foreground"
return (
<AccountShell customer={customer} onLogout={handleLogout}>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Personal details */}
<section className="bg-card border border-border rounded-xl p-6 space-y-4">
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Personal details</h2>
<div className="grid grid-cols-2 gap-4">
<div><label className={labelCls}>First name</label><input className={inputCls} value={form.first_name} onChange={set('first_name')} placeholder="Jane" /></div>
<div><label className={labelCls}>Last name</label><input className={inputCls} value={form.last_name} onChange={set('last_name')} placeholder="Doe" /></div>
</div>
<div>
<label className={labelCls}>Email</label>
<input className={`${inputCls} opacity-60 cursor-not-allowed`} value={customer?.email ?? ''} readOnly />
<p className="text-xs text-muted-foreground mt-1">Email address cannot be changed</p>
</div>
<div><label className={labelCls}>Phone</label><input className={inputCls} value={form.phone} onChange={set('phone')} placeholder="0XX XXX XXXX" /></div>
</section>
{/* Delivery address */}
<section className="bg-card border border-border rounded-xl p-6 space-y-4">
<div>
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Delivery address</h2>
<p className="text-xs text-muted-foreground mt-1">Saved address auto-fills at checkout</p>
</div>
<div><label className={labelCls}>Street address</label><input className={inputCls} value={form.address_line1} onChange={set('address_line1')} placeholder="123 Main Street" /></div>
<div>
<label className={labelCls}>Apartment / Unit <span className="font-normal text-muted-foreground">(optional)</span></label>
<input className={inputCls} value={form.address_line2} onChange={set('address_line2')} placeholder="Apt 4B" />
</div>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-1"><label className={labelCls}>City</label><input 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>
</section>
{/* Change password */}
<section className="bg-card border border-border rounded-xl p-6 space-y-4">
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Change password</h2>
<div><label className={labelCls}>Current password</label><input type="password" className={inputCls} value={form.current_password} onChange={set('current_password')} placeholder="Current password" /></div>
<div className="grid grid-cols-2 gap-4">
<div><label className={labelCls}>New password</label><input type="password" className={inputCls} value={form.new_password} onChange={set('new_password')} placeholder="Min. 6 characters" minLength={6} /></div>
<div><label className={labelCls}>Confirm password</label><input type="password" className={inputCls} value={form.confirm_password} onChange={set('confirm_password')} placeholder="Confirm" /></div>
</div>
</section>
{error && <p className="text-sm text-destructive bg-destructive/10 rounded-lg px-4 py-3">{error}</p>}
{saved && <p className="text-sm text-green-700 bg-green-50 rounded-lg px-4 py-3">Profile saved successfully</p>}
<button type="submit" disabled={saving}
className="w-full bg-primary text-primary-foreground py-3 rounded-lg font-medium hover:opacity-90 transition-opacity disabled:opacity-50 text-sm">
{saving ? 'Saving…' : 'Save changes'}
</button>
</form>
</AccountShell>
)
}