ifixkart-storefront/components/AccountMenu.tsx

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}
/>
</>
);
};