Add dropdown menus for user and admin actions
Some checks failed
CI / checks (push) Has been cancelled
CI / checks (pull_request) Has been cancelled

This commit is contained in:
Tero Halla-aho 2025-12-21 22:57:27 +02:00
parent 20292d5985
commit 24a7578a5f

View file

@ -105,6 +105,12 @@ function Icon({ name }: { name: string }) {
<path d="M9.5 12.5l1.8 1.8 3.7-3.7" />
</svg>
);
case 'chevron-down':
return (
<svg {...common} viewBox="0 0 24 24" aria-hidden>
<path d="M6 9l6 6 6-6" />
</svg>
);
default:
return null;
}
@ -115,7 +121,9 @@ export default function NavBar() {
const [user, setUser] = useState<SessionUser | null>(null);
const [pendingCount, setPendingCount] = useState<number>(0);
const [adminMenuOpen, setAdminMenuOpen] = useState(false);
const [userMenuOpen, setUserMenuOpen] = useState(false);
const adminMenuRef = useRef<HTMLDivElement | null>(null);
const userMenuRef = useRef<HTMLDivElement | null>(null);
async function loadUser() {
try {
@ -133,7 +141,10 @@ export default function NavBar() {
}, []);
useEffect(() => {
if (!user) setAdminMenuOpen(false);
if (!user) {
setAdminMenuOpen(false);
setUserMenuOpen(false);
}
}, [user]);
useEffect(() => {
@ -167,15 +178,19 @@ export default function NavBar() {
const showAdminMenu = Boolean(user && (showApprovals || isAdmin));
useEffect(() => {
if (!adminMenuOpen) return;
if (!userMenuOpen && !adminMenuOpen) return;
const onMouseDown = (e: MouseEvent) => {
const target = e.target as Node | null;
if (target && adminMenuRef.current && !adminMenuRef.current.contains(target)) {
setAdminMenuOpen(false);
}
const insideAdmin = adminMenuRef.current && target && adminMenuRef.current.contains(target);
const insideUser = userMenuRef.current && target && userMenuRef.current.contains(target);
if (!insideAdmin) setAdminMenuOpen(false);
if (!insideUser) setUserMenuOpen(false);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setAdminMenuOpen(false);
if (e.key === 'Escape') {
setAdminMenuOpen(false);
setUserMenuOpen(false);
}
};
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('keydown', onKeyDown);
@ -183,7 +198,7 @@ export default function NavBar() {
document.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('keydown', onKeyDown);
};
}, [adminMenuOpen]);
}, [adminMenuOpen, userMenuOpen]);
return (
<header style={{ padding: '12px 20px', borderBottom: '1px solid #eee', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
@ -199,18 +214,46 @@ export default function NavBar() {
<nav style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{user ? (
<>
<span style={{ fontSize: 12, color: '#444', border: '1px solid #ddd', borderRadius: 12, padding: '4px 10px' }}>
{user.email} · {user.role}
</span>
<Link href="/me" className="button secondary">
<div className="nav-admin" ref={userMenuRef}>
<button
type="button"
className="button secondary"
aria-haspopup="menu"
aria-expanded={userMenuOpen}
onClick={() => setUserMenuOpen((v) => !v)}
style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}
>
<div style={{ display: 'grid', gap: 2, textAlign: 'left' }}>
<span style={{ fontWeight: 600 }}>{user.email}</span>
<span style={{ fontSize: 12, opacity: 0.7 }}>{user.role}</span>
</div>
<Icon name="chevron-down" />
</button>
{userMenuOpen ? (
<div className="nav-admin-menu" role="menu" aria-label={t('navProfile')}>
<Link href="/me" className="nav-admin-item button secondary" role="menuitem" onClick={() => setUserMenuOpen(false)}>
<Icon name="profile" /> {t('navProfile')}
</Link>
<Link href="/listings/mine" className="button secondary">
<Link href="/listings/mine" className="nav-admin-item button secondary" role="menuitem" onClick={() => setUserMenuOpen(false)}>
<Icon name="list" /> {t('navMyListings')}
</Link>
<Link href="/listings/new" className="button secondary">
<Link href="/listings/new" className="nav-admin-item button secondary" role="menuitem" onClick={() => setUserMenuOpen(false)}>
<Icon name="plus" /> {t('navNewListing')}
</Link>
<button
type="button"
className="nav-admin-item button secondary"
role="menuitem"
onClick={() => {
setUserMenuOpen(false);
logout();
}}
>
<Icon name="logout" /> {t('navLogout')}
</button>
</div>
) : null}
</div>
{showAdminMenu ? (
<div className="nav-admin" ref={adminMenuRef}>
<button
@ -219,6 +262,7 @@ export default function NavBar() {
aria-haspopup="menu"
aria-expanded={adminMenuOpen}
onClick={() => setAdminMenuOpen((v) => !v)}
style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}
>
<Icon name="admin" /> {t('navAdmin')}
{showApprovals && pendingCount > 0 ? (
@ -226,6 +270,7 @@ export default function NavBar() {
{t('approvalsBadge', { count: pendingCount })}
</span>
) : null}
<Icon name="chevron-down" />
</button>
{adminMenuOpen ? (
<div className="nav-admin-menu" role="menu" aria-label={t('navAdmin')}>
@ -258,9 +303,6 @@ export default function NavBar() {
) : null}
</div>
) : null}
<button className="button secondary" onClick={logout}>
<Icon name="logout" /> {t('navLogout')}
</button>
</>
) : (
<>