feat: add user dropdown menu with sign-out to top bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
89955f6db8
commit
a7c6aae8c6
|
|
@ -1,10 +1,53 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useTenantStore } from '@/stores/zustand/tenant-store';
|
import { useTenantStore } from '@/stores/zustand/tenant-store';
|
||||||
import { Search, Bell } from 'lucide-react';
|
import { Search, Bell, LogOut, User, Settings } from 'lucide-react';
|
||||||
|
|
||||||
|
interface StoredUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export function TopBar() {
|
export function TopBar() {
|
||||||
|
const router = useRouter();
|
||||||
const { currentTenant } = useTenantStore();
|
const { currentTenant } = useTenantStore();
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const [user, setUser] = useState<StoredUser | null>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('user');
|
||||||
|
if (raw) setUser(JSON.parse(raw));
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close menu on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClick(e: MouseEvent) {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||||
|
setMenuOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (menuOpen) document.addEventListener('mousedown', handleClick);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick);
|
||||||
|
}, [menuOpen]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('current_tenant');
|
||||||
|
router.replace('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
const initials = user?.name
|
||||||
|
? user.name.split(' ').map((w) => w[0]).join('').toUpperCase().slice(0, 2)
|
||||||
|
: 'U';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-12 border-b bg-card/50 backdrop-blur-sm flex items-center justify-between px-4 shrink-0">
|
<header className="h-12 border-b bg-card/50 backdrop-blur-sm flex items-center justify-between px-4 shrink-0">
|
||||||
|
|
@ -37,13 +80,54 @@ export function TopBar() {
|
||||||
{/* Tenant indicator */}
|
{/* Tenant indicator */}
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<span className="text-muted-foreground">Tenant: </span>
|
<span className="text-muted-foreground">Tenant: </span>
|
||||||
<span className="font-medium">{currentTenant?.name || 'Not selected'}</span>
|
<span className="font-medium">{currentTenant?.name || currentTenant?.id || 'Not selected'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User avatar */}
|
{/* User avatar + dropdown */}
|
||||||
<button className="w-7 h-7 rounded-full bg-primary/15 border border-primary/20 flex items-center justify-center text-xs font-semibold text-primary hover:bg-primary/25 transition-colors">
|
<div className="relative" ref={menuRef}>
|
||||||
A
|
<button
|
||||||
|
onClick={() => setMenuOpen(!menuOpen)}
|
||||||
|
className="w-7 h-7 rounded-full bg-primary/15 border border-primary/20 flex items-center justify-center text-xs font-semibold text-primary hover:bg-primary/25 transition-colors"
|
||||||
|
>
|
||||||
|
{initials}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{menuOpen && (
|
||||||
|
<div className="absolute right-0 top-full mt-1.5 w-56 bg-card border rounded-lg shadow-lg py-1 z-50">
|
||||||
|
{/* User info */}
|
||||||
|
<div className="px-3 py-2 border-b">
|
||||||
|
<p className="text-sm font-medium truncate">{user?.name || 'User'}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">{user?.email}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Menu items */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setMenuOpen(false); router.push('/settings'); }}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="h-3.5 w-3.5" />
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setMenuOpen(false); router.push('/users'); }}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<User className="h-3.5 w-3.5" />
|
||||||
|
Users
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="border-t my-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-500 hover:bg-red-500/10 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="h-3.5 w-3.5" />
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue