298 lines
9.2 KiB
TypeScript
298 lines
9.2 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
User,
|
|
Package,
|
|
MapPin,
|
|
Settings,
|
|
Heart,
|
|
GitCompare,
|
|
ShoppingCart,
|
|
Wrench,
|
|
LogOut,
|
|
ChevronDown,
|
|
LogIn,
|
|
} from 'lucide-react';
|
|
import { useAuthStore } from '@/store/authStore';
|
|
import { apiFetch, clearTokens } from '@/services/api/client';
|
|
import { useCartStore } from '@/store/cartStore';
|
|
import GoogleAuthModal from '@/components/auth/GoogleAuthModal';
|
|
|
|
type MenuItem = {
|
|
label: string;
|
|
href?: string;
|
|
icon: React.ComponentType<{ size?: number; className?: string }>;
|
|
onClick?: () => void;
|
|
dividerAfter?: boolean;
|
|
};
|
|
|
|
export const AccountMenu: React.FC = () => {
|
|
const router = useRouter();
|
|
const { isAuthenticated, customer, clearAuth, bootstrapAuth, authReady } =
|
|
useAuthStore();
|
|
const setCartDrawerOpen = useCartStore((s) => s.setCartDrawerOpen);
|
|
const [open, setOpen] = useState(false);
|
|
const [authModalOpen, setAuthModalOpen] = useState(false);
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
useEffect(() => {
|
|
void bootstrapAuth();
|
|
}, [bootstrapAuth]);
|
|
|
|
useEffect(() => {
|
|
const onDoc = (e: MouseEvent) => {
|
|
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
|
|
};
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') setOpen(false);
|
|
};
|
|
document.addEventListener('mousedown', onDoc);
|
|
document.addEventListener('keydown', onKey);
|
|
return () => {
|
|
document.removeEventListener('mousedown', onDoc);
|
|
document.removeEventListener('keydown', onKey);
|
|
};
|
|
}, []);
|
|
|
|
const clearCloseTimer = () => {
|
|
if (closeTimer.current) {
|
|
clearTimeout(closeTimer.current);
|
|
closeTimer.current = null;
|
|
}
|
|
};
|
|
|
|
const scheduleClose = () => {
|
|
clearCloseTimer();
|
|
closeTimer.current = setTimeout(() => setOpen(false), 160);
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
setOpen(false);
|
|
try {
|
|
await apiFetch('/api/v1/customer/auth/logout', {
|
|
method: 'POST',
|
|
skipAuth: true,
|
|
});
|
|
} catch {
|
|
/* ignore */
|
|
} finally {
|
|
clearTokens();
|
|
clearAuth();
|
|
router.push('/');
|
|
}
|
|
};
|
|
|
|
const handleAuthSuccess = () => {
|
|
setAuthModalOpen(false);
|
|
setOpen(false);
|
|
router.push('/');
|
|
};
|
|
|
|
const loggedInItems: MenuItem[] = [
|
|
{
|
|
label: 'My Profile',
|
|
href: '/account?tab=dashboard',
|
|
icon: User,
|
|
},
|
|
{
|
|
label: 'Orders',
|
|
href: '/account?tab=orders',
|
|
icon: Package,
|
|
},
|
|
{
|
|
label: 'Saved Addresses',
|
|
href: '/account?tab=addresses',
|
|
icon: MapPin,
|
|
},
|
|
{
|
|
label: 'Account Settings',
|
|
href: '/account?tab=settings',
|
|
icon: Settings,
|
|
dividerAfter: true,
|
|
},
|
|
{
|
|
label: 'Wishlist',
|
|
href: '/wishlist',
|
|
icon: Heart,
|
|
},
|
|
{
|
|
label: 'Compare',
|
|
href: '/compare',
|
|
icon: GitCompare,
|
|
},
|
|
{
|
|
label: 'My Cart',
|
|
icon: ShoppingCart,
|
|
onClick: () => {
|
|
setOpen(false);
|
|
setCartDrawerOpen(true);
|
|
},
|
|
dividerAfter: true,
|
|
},
|
|
{
|
|
label: 'Track Repair',
|
|
href: '/services/tracking',
|
|
icon: Wrench,
|
|
dividerAfter: true,
|
|
},
|
|
{
|
|
label: 'Sign Out',
|
|
icon: LogOut,
|
|
onClick: handleLogout,
|
|
},
|
|
];
|
|
|
|
const displayName = customer?.first_name || 'Account';
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
ref={rootRef}
|
|
className="relative"
|
|
onMouseEnter={() => {
|
|
if (!authReady) return;
|
|
clearCloseTimer();
|
|
setOpen(true);
|
|
}}
|
|
onMouseLeave={scheduleClose}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (!isAuthenticated) {
|
|
setAuthModalOpen(true);
|
|
return;
|
|
}
|
|
setOpen((v) => !v);
|
|
}}
|
|
className="flex items-center gap-2 p-2 text-gray-650 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg group cursor-pointer"
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
>
|
|
<User
|
|
size={22}
|
|
strokeWidth={1.75}
|
|
className="shrink-0 translate-y-px group-hover:scale-105 transition-transform"
|
|
/>
|
|
<span className="hidden md:flex flex-col items-start justify-center gap-[3px] min-w-0 text-left">
|
|
<span className="text-[11px] leading-none text-gray-400 font-normal">
|
|
{isAuthenticated ? 'Account' : 'My Account'}
|
|
</span>
|
|
<span className="inline-flex items-center gap-1 max-w-[110px] text-[13px] leading-none font-bold text-gray-800 group-hover:text-primary transition-colors">
|
|
<span className="truncate">
|
|
{isAuthenticated ? displayName : 'Login'}
|
|
</span>
|
|
<ChevronDown
|
|
size={12}
|
|
strokeWidth={2.25}
|
|
className={`shrink-0 text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`}
|
|
/>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
|
|
{open && (
|
|
<div
|
|
role="menu"
|
|
className="absolute right-0 top-full mt-1 w-[240px] bg-white border border-[#e5e5e5] shadow-[0_8px_24px_rgba(0,0,0,0.12)] z-[200] py-2"
|
|
onMouseEnter={clearCloseTimer}
|
|
onMouseLeave={scheduleClose}
|
|
>
|
|
{!isAuthenticated ? (
|
|
<div className="px-1 py-1">
|
|
<p className="px-4 pt-1 pb-2 text-[13px] font-bold text-[#222]">
|
|
Welcome
|
|
</p>
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
setAuthModalOpen(true);
|
|
}}
|
|
className="w-full flex items-center gap-3 px-4 py-2.5 text-[13px] text-[#333] hover:bg-[#f7f9fc] hover:text-primary transition-colors cursor-pointer text-left"
|
|
>
|
|
<LogIn size={16} className="text-gray-400 shrink-0" />
|
|
Login / Sign Up
|
|
</button>
|
|
<div className="my-2 border-t border-[#ececec]" />
|
|
<Link
|
|
href="/wishlist"
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-2.5 text-[13px] text-[#333] hover:bg-[#f7f9fc] hover:text-primary transition-colors"
|
|
>
|
|
<Heart size={16} className="text-gray-400 shrink-0" />
|
|
Wishlist
|
|
</Link>
|
|
<Link
|
|
href="/compare"
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-2.5 text-[13px] text-[#333] hover:bg-[#f7f9fc] hover:text-primary transition-colors"
|
|
>
|
|
<GitCompare size={16} className="text-gray-400 shrink-0" />
|
|
Compare
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="px-1 py-1">
|
|
<p className="px-4 pt-1 pb-2 text-[13px] font-bold text-[#222]">
|
|
Your Account
|
|
</p>
|
|
{loggedInItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const className =
|
|
'w-full flex items-center gap-3 px-4 py-2.5 text-[13px] text-[#333] hover:bg-[#f7f9fc] hover:text-primary transition-colors text-left';
|
|
|
|
const row = item.href ? (
|
|
<Link
|
|
key={item.label}
|
|
href={item.href}
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
className={className}
|
|
>
|
|
<Icon size={16} className="text-gray-400 shrink-0" />
|
|
{item.label}
|
|
</Link>
|
|
) : (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
role="menuitem"
|
|
onClick={item.onClick}
|
|
className={`${className} cursor-pointer`}
|
|
>
|
|
<Icon size={16} className="text-gray-400 shrink-0" />
|
|
{item.label}
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<React.Fragment key={item.label}>
|
|
{row}
|
|
{item.dividerAfter ? (
|
|
<div className="my-1.5 border-t border-[#ececec]" />
|
|
) : null}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<GoogleAuthModal
|
|
isOpen={authModalOpen}
|
|
onClose={() => setAuthModalOpen(false)}
|
|
onSuccess={handleAuthSuccess}
|
|
/>
|
|
</>
|
|
);
|
|
};
|