ifixkart-storefront/app/account/page.tsx

990 lines
38 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @page Customer Account & Orders (`app/account/page.tsx`)
* @purpose TechShop-styled customer account: profile, addresses, orders, settings.
* @dependencies useAuthStore, apiFetch
*/
'use client';
import React, { useState, useEffect, Suspense } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import {
User,
Package,
MapPin,
Settings,
LogOut,
Plus,
Edit2,
Trash2,
Check,
ShieldCheck,
ShoppingBag,
Loader2,
KeyRound,
X,
} from 'lucide-react';
import { useAuthStore } from '@/store/authStore';
import { apiFetch, clearTokens } from '@/services/api/client';
import { formatCurrency } from '@/lib/utils';
import GoogleAuthModal from '@/components/auth/GoogleAuthModal';
import { AnnouncementBar } from '@/layout/AnnouncementBar';
import { Header } from '@/layout/Header';
import { Navbar } from '@/layout/Navbar';
import { Footer } from '@/layout/Footer';
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
import { PhoneInput, PincodeInput } from '@/components/PhoneInput';
import { validatePhone, validatePincode, validateAddress } from '@/lib/validation';
import { FloatingButtons } from '@/components/FloatingButtons';
type TabType = 'dashboard' | 'orders' | 'addresses' | 'settings';
const VALID_TABS: TabType[] = [
'dashboard',
'orders',
'addresses',
'settings',
];
function parseTab(raw: string | null): TabType {
if (raw && VALID_TABS.includes(raw as TabType)) return raw as TabType;
return 'dashboard';
}
interface Address {
address_id?: string;
address_type: string;
full_name: string;
phone: string;
street_address: string;
city: string;
state: string;
pincode: string;
is_default: boolean;
}
interface Order {
order_id: string;
order_no: string;
final_amount: number;
status: string;
payment_status: string;
created_at: string;
}
const TABS: { id: TabType; label: string; icon: typeof User }[] = [
{ id: 'dashboard', label: 'Dashboard', icon: User },
{ id: 'orders', label: 'Order History', icon: Package },
{ id: 'addresses', label: 'Address Book', icon: MapPin },
{ id: 'settings', label: 'Settings', icon: Settings },
];
const inputClass =
'w-full px-3 py-2.5 bg-white border border-[#e5e5e5] text-[13px] text-[#222] outline-none focus:border-primary transition-colors';
const labelClass =
'block text-[11px] font-semibold text-[#666] uppercase tracking-wide mb-1.5';
function AccountDashboardPage() {
const router = useRouter();
const searchParams = useSearchParams();
const {
isAuthenticated,
clearAuth,
setAuth,
authReady,
bootstrapAuth,
} = useAuthStore();
const [authModalOpen, setAuthModalOpen] = useState(false);
const [activeTab, setActiveTab] = useState<TabType>(() =>
parseTab(searchParams.get('tab'))
);
const [profile, setProfile] = useState<any>(null);
const [addresses, setAddresses] = useState<Address[]>([]);
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [addressModalOpen, setAddressModalOpen] = useState(false);
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
const [addressForm, setAddressForm] = useState<Address>({
address_type: 'SHIPPING',
full_name: '',
phone: '',
street_address: '',
city: '',
state: '',
pincode: '',
is_default: false,
});
const [settingsForm, setSettingsForm] = useState({
first_name: '',
last_name: '',
phone: '',
});
const [actionLoading, setActionLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
useEffect(() => {
void bootstrapAuth();
}, [bootstrapAuth]);
useEffect(() => {
setActiveTab(parseTab(searchParams.get('tab')));
}, [searchParams]);
const goToTab = (tab: TabType) => {
setActiveTab(tab);
setErrorMsg(null);
const qs = tab === 'dashboard' ? '/account' : `/account?tab=${tab}`;
router.replace(qs, { scroll: false });
};
useEffect(() => {
if (!authReady) return;
if (!isAuthenticated) {
setAuthModalOpen(true);
} else {
setAuthModalOpen(false);
fetchAccountData();
}
}, [isAuthenticated, authReady]);
const fetchAccountData = async () => {
setLoading(true);
setErrorMsg(null);
try {
const prof = await apiFetch<any>('/api/v1/customer/profile');
setProfile(prof);
setSettingsForm({
first_name: prof.first_name || '',
last_name: prof.last_name || '',
phone: prof.phone || '',
});
const addrs = await apiFetch<Address[]>(
'/api/v1/customer/profile/addresses'
);
setAddresses(addrs);
const ords = await apiFetch<Order[]>('/api/v1/orders');
setOrders(ords);
} catch (err: any) {
console.error(err);
setErrorMsg(err.message || 'Failed to retrieve account records.');
} finally {
setLoading(false);
}
};
const handleLogout = async () => {
try {
await apiFetch('/api/v1/customer/auth/logout', {
method: 'POST',
skipAuth: true,
});
} catch (err) {
console.error('Logout endpoint fail:', err);
} finally {
clearTokens();
clearAuth();
setProfile(null);
setAddresses([]);
setOrders([]);
router.push('/');
}
};
const handleProfileUpdate = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMsg(null);
if (settingsForm.phone) {
const phoneErr = validatePhone(settingsForm.phone);
if (phoneErr) {
setErrorMsg(phoneErr);
return;
}
}
setActionLoading(true);
try {
const updated = await apiFetch<any>('/api/v1/customer/profile', {
method: 'PUT',
body: JSON.stringify(settingsForm),
});
setProfile(updated);
setAuth(useAuthStore.getState().accessToken || '', updated);
alert('Profile updated successfully!');
} catch (err: any) {
setErrorMsg(err.message || 'Failed to update profile.');
} finally {
setActionLoading(false);
}
};
const handleAddressSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMsg(null);
const phoneErr = validatePhone(addressForm.phone, true);
if (phoneErr) {
setErrorMsg(phoneErr);
return;
}
const pincodeErr = validatePincode(addressForm.pincode, true);
if (pincodeErr) {
setErrorMsg(pincodeErr);
return;
}
const addressErr = validateAddress(addressForm.street_address, true);
if (addressErr) {
setErrorMsg(addressErr);
return;
}
setActionLoading(true);
try {
if (editingAddress?.address_id) {
await apiFetch<Address>(
`/api/v1/customer/profile/addresses/${editingAddress.address_id}`,
{
method: 'PUT',
body: JSON.stringify(addressForm),
}
);
} else {
await apiFetch<Address>('/api/v1/customer/profile/addresses', {
method: 'POST',
body: JSON.stringify(addressForm),
});
}
setAddressModalOpen(false);
setEditingAddress(null);
const addrs = await apiFetch<Address[]>(
'/api/v1/customer/profile/addresses'
);
setAddresses(addrs);
} catch (err: any) {
setErrorMsg(err.message || 'Failed to save address record.');
} finally {
setActionLoading(false);
}
};
const handleEditAddressClick = (addr: Address) => {
setEditingAddress(addr);
setAddressForm({
address_type: addr.address_type,
full_name: addr.full_name,
phone: addr.phone,
street_address: addr.street_address,
city: addr.city,
state: addr.state,
pincode: addr.pincode,
is_default: addr.is_default,
});
setAddressModalOpen(true);
};
const handleDeleteAddress = async (addressId: string) => {
if (!confirm('Are you sure you want to delete this address?')) return;
setActionLoading(true);
try {
await apiFetch(`/api/v1/customer/profile/addresses/${addressId}`, {
method: 'DELETE',
});
const addrs = await apiFetch<Address[]>(
'/api/v1/customer/profile/addresses'
);
setAddresses(addrs);
} catch (err: any) {
setErrorMsg(err.message || 'Failed to delete address.');
} finally {
setActionLoading(false);
}
};
const handleCancelOrder = async (orderId: string) => {
if (!confirm('Are you sure you want to cancel this order?')) return;
setActionLoading(true);
try {
await apiFetch(`/api/v1/orders/${orderId}/cancel`, { method: 'POST' });
setOrders(
orders.map((o) =>
o.order_id === orderId ? { ...o, status: 'CANCELLED' } : o
)
);
alert('Order cancelled successfully.');
} catch (err: any) {
setErrorMsg(err.message || 'Failed to cancel order.');
} finally {
setActionLoading(false);
}
};
const openNewAddressModal = () => {
setEditingAddress(null);
setAddressForm({
address_type: 'SHIPPING',
full_name: '',
phone: '',
street_address: '',
city: '',
state: '',
pincode: '',
is_default: false,
});
setAddressModalOpen(true);
};
const defaultAddress = addresses.find((a) => a.is_default);
const activeTabMeta = TABS.find((t) => t.id === activeTab);
const pageShell = (children: React.ReactNode) => (
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
<AnnouncementBar />
<Header />
<StickyHeaderSpacer />
<Navbar />
{children}
<Footer />
<FloatingButtons />
</div>
);
if (!authReady) {
return (
<div className="w-full min-h-screen bg-white flex items-center justify-center">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
);
}
if (!isAuthenticated) {
return pageShell(
<>
<main className="flex-grow pb-16">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-10 flex justify-center">
<div className="max-w-[420px] w-full bg-white border border-[#e5e5e5] overflow-hidden">
<div className="px-6 pt-8 pb-2 text-center space-y-3">
<div className="text-[24px] font-extrabold tracking-tight leading-none">
<span className="text-primary">iFix</span>
<span className="text-[#222]">Kart</span>
</div>
<div className="w-14 h-14 bg-primary/10 text-primary flex items-center justify-center mx-auto">
<KeyRound className="w-7 h-7" />
</div>
<div className="space-y-1.5">
<h2 className="text-[20px] font-bold text-[#222]">
Sign in to your account
</h2>
<p className="text-[13px] text-gray-500 leading-relaxed px-2">
View your profile, manage shipping addresses, and track
order history in one place.
</p>
</div>
</div>
<div className="px-6 py-6 space-y-4">
<button
type="button"
onClick={() => setAuthModalOpen(true)}
className="w-full bg-primary hover:bg-primary-hover text-white font-semibold py-3.5 text-[14px] transition cursor-pointer"
>
Continue with Google
</button>
</div>
</div>
</div>
</main>
<GoogleAuthModal
isOpen={authModalOpen}
onClose={() => setAuthModalOpen(false)}
onSuccess={() => {
setAuthModalOpen(false);
router.push('/');
}}
/>
</>
);
}
return pageShell(
<>
<main className="flex-grow pb-16 antialiased">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-8">
{/* Profile strip */}
<div className="bg-white border border-[#e5e5e5] px-5 py-4 mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-12 h-12 shrink-0 bg-primary text-white flex items-center justify-center text-[15px] font-bold select-none">
{profile?.first_name?.charAt(0).toUpperCase() || 'U'}
{profile?.last_name?.charAt(0).toUpperCase() || ''}
</div>
<div className="min-w-0 text-left">
<p className="text-[16px] font-bold text-[#222] truncate">
{profile?.first_name} {profile?.last_name}
</p>
<p className="text-[12px] text-gray-500 truncate">
{profile?.email}
</p>
</div>
</div>
<button
type="button"
onClick={handleLogout}
className="inline-flex items-center gap-2 text-[13px] font-medium text-[#666] hover:text-primary transition-colors cursor-pointer self-start sm:self-auto"
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
{errorMsg && (
<div className="px-4 py-3 mb-6 text-[13px] text-red-600 bg-red-50 border border-red-200">
{errorMsg}
</div>
)}
{loading ? (
<div className="w-full py-20 flex justify-center items-center bg-white border border-[#e5e5e5]">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start">
{/* Sidebar — matches category rail language */}
<aside className="w-full lg:w-[260px] shrink-0 bg-white border border-[#e5e5e5]">
<ul className="flex flex-col">
{TABS.map((tab, idx) => {
const isActive = activeTab === tab.id;
return (
<li key={tab.id}>
<button
type="button"
onClick={() => {
goToTab(tab.id);
}}
className={`w-full flex items-center gap-3 px-5 py-[13px] text-[13px] font-medium transition-colors text-left cursor-pointer ${
idx < TABS.length - 1
? 'border-b border-[#ececec]'
: ''
} ${
isActive
? 'text-primary bg-[#f7f9fc]'
: 'text-[#333] hover:text-primary hover:bg-[#f7f9fc]'
}`}
>
<tab.icon
className={`w-4 h-4 shrink-0 ${
isActive ? 'text-primary' : 'text-gray-400'
}`}
/>
{tab.label}
</button>
</li>
);
})}
</ul>
</aside>
{/* Content panel */}
<div className="flex-1 min-w-0 w-full bg-white border border-[#e5e5e5]">
<div className="flex items-center justify-between gap-4 px-5 md:px-6 py-4 border-b border-[#e5e5e5]">
<h2 className="text-[18px] md:text-[20px] font-bold text-[#222] tracking-tight">
{activeTabMeta?.label}
</h2>
{activeTab === 'addresses' ? (
<button
type="button"
onClick={openNewAddressModal}
className="inline-flex items-center gap-1.5 bg-primary hover:bg-primary-hover text-white text-[12px] font-semibold px-3.5 py-2 transition-colors cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
New Address
</button>
) : null}
</div>
<div className="p-5 md:p-6">
{/* Dashboard */}
{activeTab === 'dashboard' && (
<div className="space-y-5">
<p className="text-[13px] text-gray-500">
Hello, {profile?.first_name}! View recent activity and
manage your account here.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="border border-[#e5e5e5] p-5 flex flex-col justify-between min-h-[160px]">
<div>
<span className="text-[11px] font-semibold text-[#888] uppercase tracking-wide block mb-2.5">
Default Shipping Address
</span>
{defaultAddress ? (
<p className="text-[13px] text-[#444] leading-relaxed">
<span className="font-bold text-[#222] block mb-1">
{defaultAddress.full_name}
</span>
{defaultAddress.street_address}
<br />
{defaultAddress.city}, {defaultAddress.state} –{' '}
{defaultAddress.pincode}
<br />
<span className="text-gray-500">
Phone: {defaultAddress.phone}
</span>
</p>
) : (
<p className="text-[13px] text-gray-500">
No default address saved in address book.
</p>
)}
</div>
<button
type="button"
onClick={() => goToTab('addresses')}
className="mt-4 text-primary hover:underline text-[13px] font-semibold text-left w-max cursor-pointer"
>
Manage Addresses
</button>
</div>
<div className="border border-[#e5e5e5] p-5 flex flex-col justify-between min-h-[160px]">
<div>
<span className="text-[11px] font-semibold text-[#888] uppercase tracking-wide block mb-2.5">
Recent Order Activity
</span>
{orders.length > 0 ? (
<div className="text-[13px] text-[#444] space-y-1">
<p className="font-bold text-[#222]">
Order {orders[0].order_no}
</p>
<p>
Total:{' '}
{formatCurrency(orders[0].final_amount)}
</p>
<p>
Status:{' '}
<span className="text-primary font-semibold">
{orders[0].status.replace('_', ' ')}
</span>
</p>
</div>
) : (
<p className="text-[13px] text-gray-500">
No purchase order history recorded.
</p>
)}
</div>
<button
type="button"
onClick={() => goToTab('orders')}
className="mt-4 text-primary hover:underline text-[13px] font-semibold text-left w-max cursor-pointer"
>
Track Purchases
</button>
</div>
</div>
</div>
)}
{/* Orders */}
{activeTab === 'orders' && (
<div className="space-y-4">
{orders.length === 0 ? (
<div className="py-14 text-center border border-dashed border-[#e5e5e5]">
<ShoppingBag className="w-10 h-10 mx-auto text-gray-300 mb-3" />
<p className="text-[13px] font-semibold text-[#444]">
No orders placed yet.
</p>
<Link
href="/shop"
className="inline-block mt-3 text-[13px] font-semibold text-primary hover:underline"
>
Continue Shopping
</Link>
</div>
) : (
<div className="border border-[#e5e5e5] divide-y divide-[#e5e5e5]">
{orders.map((order) => (
<div
key={order.order_id}
className="px-4 py-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3"
>
<div className="space-y-0.5 text-left">
<span className="font-bold text-[#222] block text-[14px]">
{order.order_no}
</span>
<span className="text-gray-400 block text-[11px]">
Placed:{' '}
{new Date(
order.created_at.endsWith('Z') ||
order.created_at.includes('+')
? order.created_at
: order.created_at + 'Z'
).toLocaleDateString('en-IN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
<div className="flex flex-wrap items-center gap-3 sm:gap-5">
<div className="text-left sm:text-right">
<span className="font-bold text-[#222] block text-[14px]">
{formatCurrency(order.final_amount)}
</span>
<span className="text-[10px] text-gray-400 uppercase block font-semibold tracking-wider">
{order.payment_status}
</span>
</div>
<span
className={`text-[10px] font-bold px-2 py-0.5 uppercase tracking-wide border ${
order.status === 'DELIVERED'
? 'border-emerald-200 text-emerald-700 bg-emerald-50'
: order.status === 'CANCELLED'
? 'border-red-200 text-red-700 bg-red-50'
: 'border-primary/20 text-primary bg-primary/5'
}`}
>
{order.status.replace('_', ' ')}
</span>
{(order.status === 'ORDER_CREATED' ||
order.status === 'PAYMENT_PENDING') && (
<button
type="button"
onClick={() =>
handleCancelOrder(order.order_id)
}
className="px-3 py-1 border border-red-200 text-red-600 hover:bg-red-50 font-semibold text-[11px] uppercase tracking-wider transition cursor-pointer"
>
Cancel
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Addresses */}
{activeTab === 'addresses' && (
<div className="space-y-4">
{addresses.length === 0 ? (
<div className="py-14 text-center border border-dashed border-[#e5e5e5]">
<MapPin className="w-10 h-10 mx-auto text-gray-300 mb-3" />
<p className="text-[13px] font-semibold text-[#444]">
No addresses saved yet.
</p>
<button
type="button"
onClick={openNewAddressModal}
className="inline-block mt-3 text-[13px] font-semibold text-primary hover:underline cursor-pointer"
>
Add your first address
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{addresses.map((addr) => (
<div
key={addr.address_id}
className={`p-4 border flex flex-col justify-between ${
addr.is_default
? 'border-primary'
: 'border-[#e5e5e5]'
}`}
>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-[10px] font-bold uppercase bg-[#f5f5f5] px-2 py-0.5 text-[#666]">
{addr.address_type}
</span>
{addr.is_default && (
<span className="text-[10px] font-bold text-primary flex items-center gap-1">
<Check className="w-3.5 h-3.5" />
Default
</span>
)}
</div>
<h4 className="font-bold text-[#222] text-[14px] mb-1">
{addr.full_name}
</h4>
<p className="text-[13px] text-[#555] leading-relaxed">
{addr.street_address}
<br />
{addr.city}, {addr.state} – {addr.pincode}
</p>
<p className="text-[12px] text-gray-500 mt-2">
Phone: {addr.phone}
</p>
</div>
<div className="flex justify-end gap-4 mt-4 pt-3 border-t border-[#ececec]">
<button
type="button"
onClick={() => handleEditAddressClick(addr)}
className="text-[#666] hover:text-primary text-[12px] font-semibold flex items-center gap-1 cursor-pointer"
>
<Edit2 className="w-3.5 h-3.5" /> Edit
</button>
<button
type="button"
onClick={() =>
addr.address_id &&
handleDeleteAddress(addr.address_id)
}
className="text-[#666] hover:text-red-600 text-[12px] font-semibold flex items-center gap-1 cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" /> Delete
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Settings */}
{activeTab === 'settings' && (
<form
onSubmit={handleProfileUpdate}
className="space-y-4 max-w-lg text-left"
>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>First Name</label>
<input
type="text"
value={settingsForm.first_name}
onChange={(e) =>
setSettingsForm({
...settingsForm,
first_name: e.target.value,
})
}
required
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>Last Name</label>
<input
type="text"
value={settingsForm.last_name}
onChange={(e) =>
setSettingsForm({
...settingsForm,
last_name: e.target.value,
})
}
required
className={inputClass}
/>
</div>
</div>
<div>
<label className={labelClass}>Email Address</label>
<input
type="email"
value={profile?.email || ''}
disabled
className={`${inputClass} bg-[#f5f5f5] text-gray-500 cursor-not-allowed`}
/>
<span className="text-[11px] text-gray-400 block mt-1.5">
Email is verified via Google login and cannot be
changed.
</span>
</div>
<PhoneInput
label="Phone Number"
value={settingsForm.phone}
onChange={(val) => setSettingsForm({ ...settingsForm, phone: val })}
/>
<button
type="submit"
disabled={actionLoading}
className="px-6 py-3 bg-primary hover:bg-primary-hover text-white font-semibold text-[13px] transition cursor-pointer disabled:opacity-60"
>
{actionLoading
? 'Saving Changes...'
: 'Save Profile Details'}
</button>
</form>
)}
</div>
</div>
</div>
)}
</div>
</main>
{addressModalOpen && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/50 p-4 sm:p-6 overflow-y-auto">
<div
className="relative w-full max-w-md my-auto bg-white border border-[#e5e5e5] shadow-xl flex flex-col max-h-[min(640px,calc(100vh-2rem))]"
role="dialog"
aria-modal="true"
aria-labelledby="address-modal-title"
>
<div className="flex items-center justify-between px-5 py-4 border-b border-[#e5e5e5] shrink-0 bg-white">
<h3
id="address-modal-title"
className="text-[16px] font-bold text-[#222]"
>
{editingAddress ? 'Edit Address' : 'New Address'}
</h3>
<button
type="button"
onClick={() => setAddressModalOpen(false)}
className="p-1 text-gray-400 hover:text-[#222] transition cursor-pointer"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</div>
<form
onSubmit={handleAddressSubmit}
className="p-5 space-y-4 text-left overflow-y-auto min-h-0"
>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelClass}>Full Name</label>
<input
type="text"
value={addressForm.full_name}
onChange={(e) =>
setAddressForm({
...addressForm,
full_name: e.target.value,
})
}
required
placeholder="John Doe"
className={inputClass}
/>
</div>
<PhoneInput
label="Phone"
required
value={addressForm.phone}
onChange={(val) => setAddressForm({ ...addressForm, phone: val })}
/>
</div>
<div>
<label className={labelClass}>Street Address</label>
<input
type="text"
value={addressForm.street_address}
onChange={(e) =>
setAddressForm({
...addressForm,
street_address: e.target.value,
})
}
required
placeholder="123 Technology Park Drive"
className={inputClass}
/>
</div>
<div className="grid grid-cols-3 gap-2">
<div>
<label className={labelClass}>City</label>
<input
type="text"
value={addressForm.city}
onChange={(e) =>
setAddressForm({ ...addressForm, city: e.target.value })
}
required
placeholder="Chennai"
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>State</label>
<input
type="text"
value={addressForm.state}
onChange={(e) =>
setAddressForm({ ...addressForm, state: e.target.value })
}
required
placeholder="Tamil Nadu"
className={inputClass}
/>
</div>
<PincodeInput
label="Pincode"
required
value={addressForm.pincode}
onChange={(val) => setAddressForm({ ...addressForm, pincode: val })}
/>
</div>
<label className="flex items-center gap-2 pt-1 cursor-pointer">
<input
type="checkbox"
checked={addressForm.is_default}
onChange={(e) =>
setAddressForm({
...addressForm,
is_default: e.target.checked,
})
}
className="w-4 h-4 accent-[var(--color-primary,#1976F3)] border-[#e5e5e5]"
/>
<span className="text-[13px] text-[#444] font-medium">
Set as default shipping address
</span>
</label>
<div className="flex gap-3 pt-4 border-t border-[#e5e5e5]">
<button
type="button"
onClick={() => setAddressModalOpen(false)}
disabled={actionLoading}
className="flex-1 py-2.5 border border-[#e5e5e5] text-[13px] font-semibold text-[#333] hover:bg-[#f7f9fc] transition cursor-pointer"
>
Cancel
</button>
<button
type="submit"
disabled={actionLoading}
className="flex-1 py-2.5 bg-primary hover:bg-primary-hover text-white text-[13px] font-semibold transition disabled:opacity-50 cursor-pointer"
>
{actionLoading ? 'Saving...' : 'Save Address'}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}
export default function AccountPage() {
return (
<Suspense
fallback={
<div className="w-full min-h-screen bg-white flex items-center justify-center">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
}
>
<AccountDashboardPage />
</Suspense>
);
}