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