From fd177ca5fa34151a469d89f8a94c853c9b1bf4ac Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Thu, 18 Dec 2025 13:02:22 +0200 Subject: [PATCH] ui: group admin links under dropdown --- PROGRESS.md | 1 + app/components/NavBar.tsx | 89 +++++++++++++++++++++++++++++++-------- app/globals.css | 35 +++++++++++++++ lib/i18n.ts | 3 ++ 4 files changed, 110 insertions(+), 18 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f8d47f2..f842433 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -87,3 +87,4 @@ - Footer now includes a minimal cookie usage statement (essential cookies only; site requires acceptance). - Forgejo deployment scaffolding added: Docker Compose + runner config guidance and Apache vhost for git.halla-aho.net, plus CI workflow placeholder under `.forgejo/workflows/`. - Amenities: added separate EV charging flags (on-site vs nearby) plus wheelchair accessibility, including browse filters and admin approvals view badges. +- Navbar: combined admin actions (approvals/users/monitoring) under a single “Admin” dropdown menu. diff --git a/app/components/NavBar.tsx b/app/components/NavBar.tsx index 438b679..aea646d 100644 --- a/app/components/NavBar.tsx +++ b/app/components/NavBar.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import Image from 'next/image'; -import { useEffect, useState, type SVGProps } from 'react'; +import { useEffect, useRef, useState, type SVGProps } from 'react'; import { useI18n } from './I18nProvider'; import logo from '../../img/logo.png'; @@ -91,6 +91,13 @@ function Icon({ name }: { name: string }) { ); + case 'admin': + return ( + + + + + ); default: return null; } @@ -100,6 +107,8 @@ export default function NavBar() { const { t, locale, setLocale } = useI18n(); const [user, setUser] = useState(null); const [pendingCount, setPendingCount] = useState(0); + const [adminMenuOpen, setAdminMenuOpen] = useState(false); + const adminMenuRef = useRef(null); async function loadUser() { try { @@ -116,6 +125,10 @@ export default function NavBar() { loadUser(); }, []); + useEffect(() => { + if (!user) setAdminMenuOpen(false); + }, [user]); + useEffect(() => { const role = user?.role; const canSeeApprovals = role === 'ADMIN' || role === 'LISTING_MODERATOR' || role === 'USER_MODERATOR'; @@ -144,6 +157,26 @@ export default function NavBar() { const isListingMod = user?.role === 'LISTING_MODERATOR'; const isUserMod = user?.role === 'USER_MODERATOR'; const showApprovals = Boolean(user && (isAdmin || isListingMod || isUserMod)); + const showAdminMenu = Boolean(user && (showApprovals || isAdmin)); + + useEffect(() => { + if (!adminMenuOpen) return; + const onMouseDown = (e: MouseEvent) => { + const target = e.target as Node | null; + if (target && adminMenuRef.current && !adminMenuRef.current.contains(target)) { + setAdminMenuOpen(false); + } + }; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setAdminMenuOpen(false); + }; + document.addEventListener('mousedown', onMouseDown); + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('keydown', onKeyDown); + }; + }, [adminMenuOpen]); return (
@@ -171,27 +204,47 @@ export default function NavBar() { {t('navNewListing')} - {showApprovals ? ( - <> - - {t('navApprovals')} - {pendingCount > 0 ? ( - + {showAdminMenu ? ( +
+ + {adminMenuOpen ? ( +
+ {showApprovals ? ( + setAdminMenuOpen(false)}> + {t('navApprovals')} + {pendingCount > 0 ? ( + + {t('approvalsBadge', { count: pendingCount })} + + ) : null} + + ) : null} + {isAdmin ? ( + setAdminMenuOpen(false)}> + {t('navUsers')} + + ) : null} + {isAdmin ? ( + setAdminMenuOpen(false)}> + {t('navMonitoring')} + + ) : null} +
) : null} - {isAdmin ? ( - - {t('navMonitoring')} - - ) : null} - +
) : null}