Spaces:
Running
Running
| import { | |
| Award, | |
| Briefcase, | |
| CalendarDays, | |
| ChevronDown, | |
| ClipboardList, | |
| LayoutDashboard, | |
| Shield, | |
| UserRound, | |
| Users, | |
| } from "lucide-react"; | |
| import { useEffect, useRef, useState, type ComponentType } from "react"; | |
| import { NavLink, Outlet, useLocation } from "react-router-dom"; | |
| import { useAuth } from "../../auth"; | |
| const navItems = [ | |
| { to: "/people", label: "People" }, | |
| { to: "/projects", label: "Projects" }, | |
| ]; | |
| interface ManageItem { | |
| to: string; | |
| label: string; | |
| icon: ComponentType<{ size?: number; className?: string }>; | |
| } | |
| const manageGroups: ManageItem[][] = [ | |
| [ | |
| { to: "/manage", label: "Manage Hub", icon: LayoutDashboard }, | |
| ], | |
| [ | |
| { to: "/manage/people", label: "People", icon: UserRound }, | |
| { to: "/manage/projects", label: "Projects", icon: ClipboardList }, | |
| ], | |
| [ | |
| { to: "/manage/roles", label: "Roles", icon: Briefcase }, | |
| { to: "/manage/teams", label: "Teams", icon: Users }, | |
| { to: "/manage/skills", label: "Skills", icon: Award }, | |
| ], | |
| [ | |
| { to: "/manage/holidays", label: "Holidays", icon: CalendarDays }, | |
| { to: "/manage/users", label: "Users", icon: Shield }, | |
| ], | |
| ]; | |
| const tailNavItems = [ | |
| { to: "/reports", label: "Reports" }, | |
| { to: "/insights", label: "Insights" }, | |
| ]; | |
| export function Layout() { | |
| const { logout, user } = useAuth(); | |
| const location = useLocation(); | |
| const [menuOpen, setMenuOpen] = useState(false); | |
| const [manageOpen, setManageOpen] = useState(false); | |
| const manageRef = useRef<HTMLDivElement>(null); | |
| const closeTimer = useRef<number | undefined>(undefined); | |
| const isManageActive = location.pathname === "/manage" || location.pathname.startsWith("/manage/"); | |
| useEffect(() => { | |
| setManageOpen(false); | |
| }, [location.pathname]); | |
| useEffect(() => { | |
| if (!menuOpen) return; | |
| const onClick = (event: MouseEvent) => { | |
| const target = event.target as HTMLElement; | |
| if (!target.closest(".topnav-actions")) setMenuOpen(false); | |
| }; | |
| document.addEventListener("mousedown", onClick); | |
| return () => document.removeEventListener("mousedown", onClick); | |
| }, [menuOpen]); | |
| useEffect(() => { | |
| if (!manageOpen) return; | |
| const handler = (event: MouseEvent) => { | |
| if (manageRef.current && !manageRef.current.contains(event.target as Node)) { | |
| setManageOpen(false); | |
| } | |
| }; | |
| document.addEventListener("mousedown", handler); | |
| return () => document.removeEventListener("mousedown", handler); | |
| }, [manageOpen]); | |
| const openManage = () => { | |
| if (closeTimer.current) window.clearTimeout(closeTimer.current); | |
| setManageOpen(true); | |
| }; | |
| const scheduleCloseManage = () => { | |
| if (closeTimer.current) window.clearTimeout(closeTimer.current); | |
| closeTimer.current = window.setTimeout(() => setManageOpen(false), 140); | |
| }; | |
| return ( | |
| <div className="app-shell"> | |
| <header className="topnav"> | |
| <div className="topnav-brand"> | |
| <div className="brand-mark">R</div> | |
| <div> | |
| <strong>Resource Portal</strong> | |
| <small>Internal IT</small> | |
| </div> | |
| </div> | |
| <nav className="topnav-tabs"> | |
| {navItems.map((item) => ( | |
| <NavLink | |
| key={item.to} | |
| to={item.to} | |
| className={({ isActive }) => `topnav-tab${isActive ? " active" : ""}`} | |
| > | |
| {item.label} | |
| </NavLink> | |
| ))} | |
| <div | |
| className="topnav-dropdown" | |
| onMouseEnter={openManage} | |
| onMouseLeave={scheduleCloseManage} | |
| ref={manageRef} | |
| > | |
| <NavLink | |
| to="/manage" | |
| end | |
| className={`topnav-tab${isManageActive ? " active" : ""}`} | |
| onClick={() => setManageOpen(false)} | |
| > | |
| Manage | |
| <ChevronDown size={14} className="topnav-tab-caret" /> | |
| </NavLink> | |
| {manageOpen ? ( | |
| <div className="topnav-menu" role="menu"> | |
| {manageGroups.map((group, groupIndex) => ( | |
| <div className="topnav-menu-group" key={groupIndex}> | |
| {group.map((item) => ( | |
| <NavLink | |
| key={item.to} | |
| to={item.to} | |
| end={item.to === "/manage"} | |
| className={({ isActive }) => `topnav-menu-item${isActive ? " active" : ""}`} | |
| role="menuitem" | |
| > | |
| <item.icon size={16} className="topnav-menu-icon" /> | |
| <span>{item.label}</span> | |
| </NavLink> | |
| ))} | |
| </div> | |
| ))} | |
| </div> | |
| ) : null} | |
| </div> | |
| {tailNavItems.map((item) => ( | |
| <NavLink | |
| key={item.to} | |
| to={item.to} | |
| className={({ isActive }) => `topnav-tab${isActive ? " active" : ""}`} | |
| > | |
| {item.label} | |
| </NavLink> | |
| ))} | |
| </nav> | |
| <div className="topnav-actions"> | |
| <button | |
| aria-haspopup="menu" | |
| aria-expanded={menuOpen} | |
| className="user-chip" | |
| onClick={() => setMenuOpen((open) => !open)} | |
| type="button" | |
| > | |
| <span className="user-avatar">{user?.username?.[0]?.toUpperCase() ?? "?"}</span> | |
| <span className="user-meta"> | |
| <strong>{user?.username}</strong> | |
| <small className="badge live">{user?.role}</small> | |
| </span> | |
| </button> | |
| {menuOpen ? ( | |
| <div className="user-menu" role="menu"> | |
| <button | |
| onClick={() => { | |
| setMenuOpen(false); | |
| logout(); | |
| }} | |
| role="menuitem" | |
| type="button" | |
| > | |
| Sign out | |
| </button> | |
| </div> | |
| ) : null} | |
| </div> | |
| </header> | |
| <main className="content"> | |
| <Outlet /> | |
| </main> | |
| </div> | |
| ); | |
| } | |