Files
saas_frontend/src/components/layout/AppHeader.tsx
T
2026-08-31 20:05:33 -04:00

165 lines
6.0 KiB
TypeScript

import {
Menu,
ChevronDown,
LogOut,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useSidebar } from "../../context/SidebarContext";
import { useAuth } from "../../context/AuthContext";
import { useTranslation } from "react-i18next";
import { authApi } from "../../application/authentication/AuthApi";
import { clearAuthCookies } from "../../lib/authCookies";
import NotificationBell from "../../application/notifications/NotificationBell";
const AppHeader: React.FC = () => {
const { t } = useTranslation(['profile', 'common']);
const { user } = useAuth();
const { toggleMobileSidebar } = useSidebar();
const navigate = useNavigate();
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null);
const handleLogout = async () => {
try {
await authApi.logout();
} catch {
// ignore
}
clearAuthCookies();
navigate("/signin");
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (!userMenuRef.current?.contains(event.target as Node)) {
setIsUserMenuOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const fullName = user
? `${user.first_name || ""} ${user.last_name || ""}`.trim() || t('common:common.loading')
: t('common:common.loading');
const initials =
user?.first_name && user?.last_name
? `${user.first_name[0]}${user.last_name[0]}`
: "U";
return (
<header className="sticky top-0 z-40 flex w-full bg-white/80 backdrop-blur-md border-b border-gray-200 transition-all duration-200">
<div className="flex flex-grow items-center gap-4 px-4 py-3 lg:px-6">
{/* LEFT: Mobile Toggle & Brand (Mobile only) */}
<div className="flex items-center gap-3">
<button
className="flex items-center justify-center p-2 text-gray-500 hover:bg-gray-100 rounded-lg lg:hidden transition-colors"
onClick={toggleMobileSidebar}
aria-label="Toggle Sidebar"
>
<Menu size={24} />
</button>
<Link to="/" className="lg:hidden flex items-center gap-2">
<span className="font-bold text-gray-900 text-lg">SaaS</span>
</Link>
</div>
{/* RIGHT: Actions */}
<div className="flex items-center gap-2 sm:gap-4 ltr:ml-auto rtl:mr-auto">
{/* Before the user menu: this is the thing that changes, and the one
somebody is looking for when they arrive. */}
<NotificationBell />
{/* User Menu */}
<div className="relative" ref={userMenuRef}>
<button
type="button"
onClick={() => setIsUserMenuOpen((prev) => !prev)}
className="
flex items-center gap-2
p-1 pr-3
rounded-full border border-gray-200
hover:border-gray-300 hover:bg-gray-50
transition-all bg-white/50
"
aria-expanded={isUserMenuOpen}
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-700 text-sm font-semibold">
{initials}
</div>
<div className="hidden md:block ltr:text-left rtl:text-right ltr:mr-1 rtl:ml-1 leading-tight">
<p className="text-sm font-semibold text-gray-700">{fullName}</p>
</div>
<ChevronDown
size={14}
className={`text-gray-400 transition-transform duration-200 ${isUserMenuOpen ? "rotate-180" : ""
}`}
/>
</button>
{/* Dropdown */}
{isUserMenuOpen && (
<div
className="
absolute ltr:right-0 rtl:left-0 mt-2 w-64
rounded-xl border border-gray-100
bg-white
shadow-xl
overflow-hidden
animate-in fade-in zoom-in-95 duration-150
ltr:origin-top-right rtl:origin-top-left
z-50
"
>
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50/50">
<p className="text-sm font-semibold text-gray-900 truncate">
{user?.email}
</p>
<p className="text-xs text-gray-500 truncate mt-0.5 uppercase tracking-wider">
{user?.role?.role_name || "User"}
</p>
</div>
<div className="py-1">
<Link
to="/profile"
onClick={() => setIsUserMenuOpen(false)}
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<div className="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500">
<ChevronDown size={16} className="-rotate-90" />
</div>
{t('profile:header.profileSettings')}
</Link>
<div className="h-px bg-gray-100 my-1 mx-2"></div>
<button
onClick={handleLogout}
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors"
>
<div className="w-8 h-8 rounded-lg bg-red-50 flex items-center justify-center text-red-600">
<LogOut size={16} />
</div>
{t('profile:header.signOut')}
</button>
</div>
</div>
)}
</div>
</div>
</div>
</header>
);
};
export default AppHeader;