125 lines
3.3 KiB
TypeScript
125 lines
3.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
|
|
const DESKTOP_MQ = "(min-width: 769px)";
|
|
const LEAVE_DELAY_MS = 150;
|
|
|
|
export default function AdminDropdown() {
|
|
const [open, setOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const isDesktopRef = useRef(false);
|
|
const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
// Track desktop breakpoint via matchMedia
|
|
useEffect(() => {
|
|
const mql = window.matchMedia(DESKTOP_MQ);
|
|
isDesktopRef.current = mql.matches;
|
|
const onChange = (e: MediaQueryListEvent) => {
|
|
isDesktopRef.current = e.matches;
|
|
};
|
|
mql.addEventListener("change", onChange);
|
|
return () => mql.removeEventListener("change", onChange);
|
|
}, []);
|
|
|
|
// Clear leave timer on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
|
|
};
|
|
}, []);
|
|
|
|
const handleMouseEnter = useCallback(() => {
|
|
if (leaveTimerRef.current) {
|
|
clearTimeout(leaveTimerRef.current);
|
|
leaveTimerRef.current = null;
|
|
}
|
|
if (isDesktopRef.current) setOpen(true);
|
|
}, []);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
if (!isDesktopRef.current) return;
|
|
leaveTimerRef.current = setTimeout(() => {
|
|
setOpen(false);
|
|
leaveTimerRef.current = null;
|
|
}, LEAVE_DELAY_MS);
|
|
}, []);
|
|
|
|
// Close on outside click
|
|
useEffect(() => {
|
|
function handler(e: MouseEvent) {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(e.target as Node)
|
|
) {
|
|
setOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handler);
|
|
return () => document.removeEventListener("mousedown", handler);
|
|
}, []);
|
|
|
|
// Close on Escape
|
|
useEffect(() => {
|
|
function handler(e: KeyboardEvent) {
|
|
if (e.key === "Escape") setOpen(false);
|
|
}
|
|
if (open) {
|
|
document.addEventListener("keydown", handler);
|
|
return () => document.removeEventListener("keydown", handler);
|
|
}
|
|
}, [open]);
|
|
|
|
return (
|
|
<div
|
|
className="admin-dropdown"
|
|
ref={dropdownRef}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<button
|
|
className="admin-dropdown__trigger"
|
|
onClick={() => setOpen((prev) => !prev)}
|
|
aria-expanded={open}
|
|
aria-haspopup="true"
|
|
>
|
|
Admin ▾
|
|
</button>
|
|
{open && (
|
|
<div className="admin-dropdown__menu" role="menu">
|
|
<Link
|
|
to="/admin/reports"
|
|
className="admin-dropdown__item"
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
>
|
|
Reports
|
|
</Link>
|
|
<Link
|
|
to="/admin/pipeline"
|
|
className="admin-dropdown__item"
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
>
|
|
Pipeline
|
|
</Link>
|
|
<Link
|
|
to="/admin/techniques"
|
|
className="admin-dropdown__item"
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
>
|
|
Techniques
|
|
</Link>
|
|
<Link
|
|
to="/admin/users"
|
|
className="admin-dropdown__item"
|
|
role="menuitem"
|
|
onClick={() => setOpen(false)}
|
|
>
|
|
Users
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|