Add dropdown menus for user and admin actions #21
1 changed files with 64 additions and 22 deletions
|
|
@ -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">
|
||||
<Icon name="profile" /> {t('navProfile')}
|
||||
</Link>
|
||||
<Link href="/listings/mine" className="button secondary">
|
||||
<Icon name="list" /> {t('navMyListings')}
|
||||
</Link>
|
||||
<Link href="/listings/new" className="button secondary">
|
||||
<Icon name="plus" /> {t('navNewListing')}
|
||||
</Link>
|
||||
<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="nav-admin-item button secondary" role="menuitem" onClick={() => setUserMenuOpen(false)}>
|
||||
<Icon name="list" /> {t('navMyListings')}
|
||||
</Link>
|
||||
<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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue