Files
frontend/src/components/layout/CommonHeader.tsx

346 lines
14 KiB
TypeScript

'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { useSession } from 'next-auth/react';
import { agentsService } from '@/services/agents.service';
import { usersService } from '@/services/users.service';
import { uploadService } from '@/services/upload.service';
import { notificationsApiService } from '@/services/notifications-api.service';
const navLinks = [
{ label: 'Education', href: '/education' },
{ label: 'About Us', href: '/about' },
{ label: "FAQ's", href: '/faq' },
];
export function CommonHeader() {
const { data: session } = useSession();
const [showProfileMenu, setShowProfileMenu] = useState(false);
const [showMobileMenu, setShowMobileMenu] = useState(false);
const profileMenuRef = useRef<HTMLDivElement>(null);
const mobileMenuRef = useRef<HTMLDivElement>(null);
const [profileImage, setProfileImage] = useState<string | null>(null);
const [profileName, setProfileName] = useState<string | null>(null);
const [notificationCount, setNotificationCount] = useState(0);
// Close dropdowns when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) {
setShowProfileMenu(false);
}
if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) {
setShowMobileMenu(false);
}
}
if (showProfileMenu || showMobileMenu) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showProfileMenu, showMobileMenu]);
// Fetch profile data (image and name) from backend based on user role
const fetchProfileData = useCallback(async () => {
try {
const role = (session?.user as any)?.role;
let avatar: string | null = null;
let name: string | null = null;
if (role === 'AGENT') {
const profile = await agentsService.getMyProfile();
avatar = profile.avatar;
name = profile.firstName ? `${profile.firstName} ${profile.lastName || ''}`.trim() : null;
} else if (role === 'USER') {
const profile = await usersService.getMyProfile();
avatar = profile.avatar;
name = profile.firstName ? `${profile.firstName} ${profile.lastName || ''}`.trim() : null;
}
if (name) {
setProfileName(name);
}
if (avatar) {
try {
const avatarUrl = await uploadService.getPresignedDownloadUrl(avatar);
setProfileImage(avatarUrl);
} catch {
setProfileImage(avatar);
}
}
} catch (err) {
console.error('Failed to fetch profile data:', err);
}
}, [session]);
useEffect(() => {
if (session) {
fetchProfileData();
}
}, [session, fetchProfileData]);
// Fetch notification unread count
const fetchUnreadCount = useCallback(async () => {
try {
const count = await notificationsApiService.getUnreadCount();
setNotificationCount(count);
} catch {
// Silently fail - user may not be authenticated yet
}
}, []);
useEffect(() => {
if (session) {
fetchUnreadCount();
// Poll every 30 seconds for new notifications
const interval = setInterval(fetchUnreadCount, 30000);
// Also refresh when a foreground notification arrives
const handleNotification = () => fetchUnreadCount();
window.addEventListener('notification-received', handleNotification);
return () => {
clearInterval(interval);
window.removeEventListener('notification-received', handleNotification);
};
}
}, [session, fetchUnreadCount]);
// Listen for profile update events to refresh the header
useEffect(() => {
const handleProfileUpdate = () => {
if (session) {
fetchProfileData();
}
};
window.addEventListener('profile-updated', handleProfileUpdate);
return () => {
window.removeEventListener('profile-updated', handleProfileUpdate);
};
}, [session, fetchProfileData]);
// Use fetched profile name, fallback to session name
const userName = profileName || session?.user?.name;
const userEmail = session?.user?.email;
const userRole = (session?.user as any)?.role;
// Use fetched profile image, fallback to session image
const userImage = profileImage || session?.user?.image;
// Determine dashboard link based on user role
const dashboardLink = userRole === 'AGENT' ? '/agent/dashboard' : '/user/dashboard';
return (
<header className="bg-[#648188] rounded-[20px] px-4 md:px-8 relative" ref={mobileMenuRef}>
<div className="flex justify-between items-center h-[60px] md:h-[70px]">
{/* Logo */}
<Link href={dashboardLink} className="flex-shrink-0">
<Image
src="/assets/logo.svg"
alt="RE-QuestN"
width={150}
height={40}
className="h-8 md:h-10 w-auto"
/>
</Link>
{/* Navigation - Desktop only */}
<nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="font-fractul font-bold text-[14px] leading-[18px] text-[#00293D] hover:text-[#e58625] transition-colors"
>
{link.label}
</Link>
))}
</nav>
{/* Right Side Icons */}
<div className="flex items-center gap-2 md:gap-4">
{session ? (
<>
{/* Notification Bell */}
<Link
href={userRole === 'AGENT' ? '/agent/notifications' : '/user/notifications'}
className="relative w-6 h-6 flex items-center justify-center hover:opacity-80 transition-opacity cursor-pointer"
>
<Image
src="/assets/notification-icon.svg"
alt="Notifications"
width={20}
height={22}
/>
{notificationCount > 0 && (
<span className="absolute -top-1.5 -right-2 min-w-[18px] h-[18px] bg-red-500 rounded-full flex items-center justify-center px-1">
<span className="text-white text-[10px] font-bold leading-none">
{notificationCount > 99 ? '99+' : notificationCount}
</span>
</span>
)}
</Link>
{/* Profile Section */}
<div className="relative" ref={profileMenuRef}>
<button
onClick={() => setShowProfileMenu(!showProfileMenu)}
className="flex items-center gap-2 md:gap-3 hover:opacity-80 transition-opacity cursor-pointer"
>
{/* Avatar */}
<div className="w-[32px] h-[32px] md:w-[35px] md:h-[35px] rounded-full overflow-hidden border-2 border-[#e58625] bg-gray-100 flex-shrink-0">
{userImage ? (
<img
src={userImage}
alt="Profile"
className="w-full h-full object-cover"
/>
) : (
<Image
src="/assets/icons/user-placeholder-icon.svg"
alt="Profile"
width={35}
height={35}
className="object-cover"
style={{ width: '100%', height: '100%' }}
/>
)}
</div>
{/* Greeting Text - hidden on mobile */}
<div className="hidden sm:flex flex-col text-left">
<span className="font-bold text-[14px] text-[#e58625] leading-tight">
Hi {userName?.split(' ')[0] || 'User'} !
</span>
<span className="font-bold text-[9px] text-[#00293d] leading-tight">
Welcome back !
</span>
</div>
</button>
{/* Profile Dropdown Menu */}
{showProfileMenu && (
<div className="absolute right-0 top-full mt-3 w-[271px] bg-white rounded-[15px] border border-black/20 z-50 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)]">
<div className="absolute -top-2 right-6 w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-b-[8px] border-b-white"></div>
<div className="flex items-center gap-3 px-4 py-4 border-b border-black/10">
<div className="w-[42px] h-[42px] rounded-full overflow-hidden flex-shrink-0 bg-gray-100">
{userImage ? (
<img
src={userImage}
alt="Profile"
className="w-full h-full object-cover"
/>
) : (
<Image
src="/assets/icons/user-placeholder-icon.svg"
alt="Profile"
width={42}
height={42}
className="object-cover"
style={{ width: '100%', height: '100%' }}
/>
)}
</div>
<div className="flex flex-col min-w-0 overflow-hidden">
<p className="font-fractul font-medium text-[16px] leading-[20px] text-black">
{userName?.split(' ')[0] || 'User'}
</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50 truncate">
{userEmail}
</p>
</div>
</div>
<Link
href={userRole === 'AGENT' ? '/agent/settings' : '/user/settings'}
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowProfileMenu(false)}
>
<Image src="/assets/icons/settings-icon.svg" alt="Settings" width={24} height={24} />
<div className="flex flex-col">
<p className="font-fractul text-[16px] leading-[18px] text-black">Account Settings</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50">Profile, Security</p>
</div>
</Link>
<Link
href={userRole === 'AGENT' ? '/agent/settings/notifications' : '/user/settings/notifications'}
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowProfileMenu(false)}
>
<Image src="/assets/icons/notification-settings-icon.svg" alt="Notifications" width={24} height={24} />
<p className="font-fractul text-[16px] leading-[18px] text-black">Notification Settings</p>
</Link>
<button
onClick={() => {
setShowProfileMenu(false);
window.location.href = '/logout';
}}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-black/5 transition-colors"
>
<Image src="/assets/icons/logout-icon.svg" alt="Log Out" width={24} height={24} />
<p className="font-fractul font-bold text-[16px] leading-[18px] text-[#e58625]">Log Out</p>
</button>
</div>
)}
</div>
</>
) : (
<Link href="/login" className="hover:opacity-80 transition-opacity">
<Image
src="/assets/icons/profile-circle-orange-icon.svg"
alt="Login"
width={32}
height={32}
/>
</Link>
)}
{/* Mobile Menu Button */}
<button
className="md:hidden p-1 hover:opacity-80 transition-opacity"
onClick={() => setShowMobileMenu(!showMobileMenu)}
>
<svg
className="w-6 h-6 text-[#00293d]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{showMobileMenu ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
</div>
{/* Mobile Navigation Menu */}
{showMobileMenu && (
<div className="md:hidden border-t border-white/20 py-3 pb-4">
<nav className="flex flex-col gap-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setShowMobileMenu(false)}
className="font-fractul font-bold text-[14px] text-[#00293D] hover:text-[#e58625] transition-colors px-2 py-2 rounded-[10px] hover:bg-white/10"
>
{link.label}
</Link>
))}
</nav>
</div>
)}
</header>
);
}