Merge pull request 'Add dropdown menus for user and admin actions' (#21) from feature/user-nav-menu into master
Some checks failed
CI / checks (push) Has been cancelled
Some checks failed
CI / checks (push) Has been cancelled
Reviewed-on: #21
This commit is contained in:
commit
464e6687e4
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" />
|
<path d="M9.5 12.5l1.8 1.8 3.7-3.7" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
case 'chevron-down':
|
||||||
|
return (
|
||||||
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
||||||
|
<path d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +121,9 @@ export default function NavBar() {
|
||||||
const [user, setUser] = useState<SessionUser | null>(null);
|
const [user, setUser] = useState<SessionUser | null>(null);
|
||||||
const [pendingCount, setPendingCount] = useState<number>(0);
|
const [pendingCount, setPendingCount] = useState<number>(0);
|
||||||
const [adminMenuOpen, setAdminMenuOpen] = useState(false);
|
const [adminMenuOpen, setAdminMenuOpen] = useState(false);
|
||||||
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||||
const adminMenuRef = useRef<HTMLDivElement | null>(null);
|
const adminMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const userMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
async function loadUser() {
|
async function loadUser() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -133,7 +141,10 @@ export default function NavBar() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) setAdminMenuOpen(false);
|
if (!user) {
|
||||||
|
setAdminMenuOpen(false);
|
||||||
|
setUserMenuOpen(false);
|
||||||
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -167,15 +178,19 @@ export default function NavBar() {
|
||||||
const showAdminMenu = Boolean(user && (showApprovals || isAdmin));
|
const showAdminMenu = Boolean(user && (showApprovals || isAdmin));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!adminMenuOpen) return;
|
if (!userMenuOpen && !adminMenuOpen) return;
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
const target = e.target as Node | null;
|
const target = e.target as Node | null;
|
||||||
if (target && adminMenuRef.current && !adminMenuRef.current.contains(target)) {
|
const insideAdmin = adminMenuRef.current && target && adminMenuRef.current.contains(target);
|
||||||
setAdminMenuOpen(false);
|
const insideUser = userMenuRef.current && target && userMenuRef.current.contains(target);
|
||||||
}
|
if (!insideAdmin) setAdminMenuOpen(false);
|
||||||
|
if (!insideUser) setUserMenuOpen(false);
|
||||||
};
|
};
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') setAdminMenuOpen(false);
|
if (e.key === 'Escape') {
|
||||||
|
setAdminMenuOpen(false);
|
||||||
|
setUserMenuOpen(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('mousedown', onMouseDown);
|
document.addEventListener('mousedown', onMouseDown);
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
|
@ -183,7 +198,7 @@ export default function NavBar() {
|
||||||
document.removeEventListener('mousedown', onMouseDown);
|
document.removeEventListener('mousedown', onMouseDown);
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}, [adminMenuOpen]);
|
}, [adminMenuOpen, userMenuOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header style={{ padding: '12px 20px', borderBottom: '1px solid #eee', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<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 }}>
|
<nav style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
<>
|
||||||
<span style={{ fontSize: 12, color: '#444', border: '1px solid #ddd', borderRadius: 12, padding: '4px 10px' }}>
|
<div className="nav-admin" ref={userMenuRef}>
|
||||||
{user.email} · {user.role}
|
<button
|
||||||
</span>
|
type="button"
|
||||||
<Link href="/me" className="button secondary">
|
className="button secondary"
|
||||||
<Icon name="profile" /> {t('navProfile')}
|
aria-haspopup="menu"
|
||||||
</Link>
|
aria-expanded={userMenuOpen}
|
||||||
<Link href="/listings/mine" className="button secondary">
|
onClick={() => setUserMenuOpen((v) => !v)}
|
||||||
<Icon name="list" /> {t('navMyListings')}
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}
|
||||||
</Link>
|
>
|
||||||
<Link href="/listings/new" className="button secondary">
|
<div style={{ display: 'grid', gap: 2, textAlign: 'left' }}>
|
||||||
<Icon name="plus" /> {t('navNewListing')}
|
<span style={{ fontWeight: 600 }}>{user.email}</span>
|
||||||
</Link>
|
<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 ? (
|
{showAdminMenu ? (
|
||||||
<div className="nav-admin" ref={adminMenuRef}>
|
<div className="nav-admin" ref={adminMenuRef}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -219,6 +262,7 @@ export default function NavBar() {
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-expanded={adminMenuOpen}
|
aria-expanded={adminMenuOpen}
|
||||||
onClick={() => setAdminMenuOpen((v) => !v)}
|
onClick={() => setAdminMenuOpen((v) => !v)}
|
||||||
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}
|
||||||
>
|
>
|
||||||
<Icon name="admin" /> {t('navAdmin')}
|
<Icon name="admin" /> {t('navAdmin')}
|
||||||
{showApprovals && pendingCount > 0 ? (
|
{showApprovals && pendingCount > 0 ? (
|
||||||
|
|
@ -226,6 +270,7 @@ export default function NavBar() {
|
||||||
{t('approvalsBadge', { count: pendingCount })}
|
{t('approvalsBadge', { count: pendingCount })}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Icon name="chevron-down" />
|
||||||
</button>
|
</button>
|
||||||
{adminMenuOpen ? (
|
{adminMenuOpen ? (
|
||||||
<div className="nav-admin-menu" role="menu" aria-label={t('navAdmin')}>
|
<div className="nav-admin-menu" role="menu" aria-label={t('navAdmin')}>
|
||||||
|
|
@ -258,9 +303,6 @@ export default function NavBar() {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<button className="button secondary" onClick={logout}>
|
|
||||||
<Icon name="logout" /> {t('navLogout')}
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue