mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
227 lines
6.4 KiB
TypeScript
227 lines
6.4 KiB
TypeScript
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
import {
|
|
Bell,
|
|
ChevronDown,
|
|
Languages,
|
|
LogOut,
|
|
MessageSquare,
|
|
Moon,
|
|
Sun,
|
|
User,
|
|
} from "lucide-react";
|
|
import { Group, Stack, Text, Menu, Tooltip, Box } from "@mantine/core";
|
|
|
|
import type { PageMeta } from "./types";
|
|
import "./FreightDashboardHeader.css";
|
|
|
|
export interface FreightDashboardHeaderProps {
|
|
pageMeta: PageMeta;
|
|
headerRight?: ReactNode;
|
|
enableThemeToggle?: boolean;
|
|
userName?: string;
|
|
userEmail?: string;
|
|
userInitials?: string;
|
|
onLogout?: () => void;
|
|
theme: "light" | "dark";
|
|
onToggleTheme: () => void;
|
|
}
|
|
|
|
const FreightDashboardHeader = ({
|
|
pageMeta,
|
|
headerRight,
|
|
enableThemeToggle = false,
|
|
userName = "User",
|
|
userEmail,
|
|
userInitials,
|
|
onLogout,
|
|
theme,
|
|
onToggleTheme,
|
|
}: FreightDashboardHeaderProps) => {
|
|
const initials =
|
|
userInitials ??
|
|
(userName
|
|
.split(" ")
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((n) => n[0].toUpperCase())
|
|
.join("") ||
|
|
"U");
|
|
|
|
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
|
const userMenuRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isUserMenuOpen) return;
|
|
|
|
const handlePointerDown = (event: MouseEvent) => {
|
|
if (
|
|
userMenuRef.current &&
|
|
!userMenuRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsUserMenuOpen(false);
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") setIsUserMenuOpen(false);
|
|
};
|
|
|
|
document.addEventListener("mousedown", handlePointerDown);
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handlePointerDown);
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [isUserMenuOpen]);
|
|
|
|
return (
|
|
<header className="fdh-root">
|
|
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
|
<span className="fdh-eyebrow">
|
|
{/* <span className="fdh-eyebrow-dot" />
|
|
Freight Backoffice */}
|
|
</span>
|
|
<Text
|
|
fw={700}
|
|
truncate
|
|
style={{ fontSize: "20px", lineHeight: 1.2, color: "#0f172a", letterSpacing: "-0.4px" }}
|
|
>
|
|
{pageMeta.title}
|
|
</Text>
|
|
{/* <Text size="sm" truncate style={{ color: "#94a3b8", lineHeight: 1.35 }}>
|
|
{pageMeta.subtitle}
|
|
</Text> */}
|
|
</Stack>
|
|
|
|
<Group gap={10} wrap="nowrap">
|
|
{enableThemeToggle && (
|
|
<Tooltip
|
|
label={theme === "dark" ? "Light mode" : "Dark mode"}
|
|
withArrow
|
|
openDelay={300}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="fdh-icon-btn"
|
|
onClick={onToggleTheme}
|
|
aria-label="Toggle theme"
|
|
>
|
|
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
|
|
</button>
|
|
</Tooltip>
|
|
)}
|
|
|
|
<Tooltip label="Language" withArrow openDelay={300}>
|
|
<button type="button" className="fdh-icon-btn" aria-label="Language">
|
|
<Languages size={18} />
|
|
</button>
|
|
</Tooltip>
|
|
|
|
<Tooltip label="Messages" withArrow openDelay={300}>
|
|
<button type="button" className="fdh-icon-btn" aria-label="Messages">
|
|
<MessageSquare size={18} />
|
|
<span className="fdh-badge">3</span>
|
|
</button>
|
|
</Tooltip>
|
|
|
|
<Tooltip label="Notifications" withArrow openDelay={300}>
|
|
<button
|
|
type="button"
|
|
className="fdh-icon-btn"
|
|
aria-label="Notifications"
|
|
>
|
|
<Bell size={18} />
|
|
<span className="fdh-badge">5</span>
|
|
</button>
|
|
</Tooltip>
|
|
|
|
<div className="fdh-divider" />
|
|
|
|
<Menu
|
|
position="bottom-end"
|
|
shadow="lg"
|
|
radius="md"
|
|
width={240}
|
|
opened={isUserMenuOpen}
|
|
onOpen={() => setIsUserMenuOpen(true)}
|
|
onClose={() => setIsUserMenuOpen(false)}
|
|
>
|
|
<Menu.Target>
|
|
<div className="fdh-user">
|
|
<div className="fdh-avatar-ring">
|
|
<div className="fdh-avatar">{initials}</div>
|
|
</div>
|
|
<Stack gap={0} style={{ minWidth: 0 }} visibleFrom="sm">
|
|
<Text
|
|
size="sm"
|
|
fw={600}
|
|
truncate
|
|
style={{ color: "#0f172a", lineHeight: 1.25, maxWidth: 140 }}
|
|
>
|
|
{userName}
|
|
</Text>
|
|
<Text
|
|
size="xs"
|
|
truncate
|
|
style={{ color: "#94a3b8", lineHeight: 1.25, maxWidth: 140 }}
|
|
>
|
|
{userEmail ?? "Administrator"}
|
|
</Text>
|
|
</Stack>
|
|
<ChevronDown
|
|
size={16}
|
|
style={{
|
|
color: "#94a3b8",
|
|
flexShrink: 0,
|
|
transition: "transform 0.2s",
|
|
transform: isUserMenuOpen ? "rotate(180deg)" : "rotate(0deg)",
|
|
}}
|
|
/>
|
|
</div>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
<Box px="sm" py="xs">
|
|
<Group gap={10} wrap="nowrap">
|
|
<div className="fdh-avatar-ring">
|
|
<div className="fdh-avatar">{initials}</div>
|
|
</div>
|
|
<Stack gap={0} style={{ minWidth: 0 }}>
|
|
<Text size="sm" fw={600} truncate style={{ color: "#0f172a" }}>
|
|
{userName}
|
|
</Text>
|
|
{userEmail && (
|
|
<Text size="xs" truncate style={{ color: "#94a3b8" }}>
|
|
{userEmail}
|
|
</Text>
|
|
)}
|
|
</Stack>
|
|
</Group>
|
|
</Box>
|
|
<Menu.Divider />
|
|
<Menu.Item
|
|
leftSection={<User size={15} />}
|
|
onClick={() => setIsUserMenuOpen(false)}
|
|
>
|
|
Profile
|
|
</Menu.Item>
|
|
<Menu.Item
|
|
leftSection={<LogOut size={15} />}
|
|
color="red"
|
|
onClick={() => {
|
|
setIsUserMenuOpen(false);
|
|
onLogout?.();
|
|
}}
|
|
>
|
|
Logout
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
|
|
{headerRight}
|
|
</Group>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
export default FreightDashboardHeader;
|