990 lines
38 KiB
TypeScript
990 lines
38 KiB
TypeScript
/**
|
||
* @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>
|
||
);
|
||
}
|