Files
syndarix/frontend/src/components/layout/UserMenu.tsx
Felipe Cardoso 6e645835dc feat(frontend): Implement navigation and layout (#44)
Implements the main navigation and layout structure:

- Sidebar component with collapsible navigation and keyboard shortcut
- AppHeader with project switcher and user menu
- AppBreadcrumbs with auto-generation from pathname
- ProjectSwitcher dropdown for quick project navigation
- UserMenu with profile, settings, and logout
- AppLayout component combining all layout elements

Features:
- Responsive design (mobile sidebar sheet, desktop sidebar)
- Keyboard navigation (Cmd/Ctrl+B to toggle sidebar)
- Dark mode support
- WCAG AA accessible (ARIA labels, focus management)

All 125 tests passing. Follows design system guidelines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 01:35:39 +01:00

173 lines
4.9 KiB
TypeScript

/**
* User Menu Component
* Dropdown menu with user profile, settings, and logout options
*/
'use client';
import { Link } from '@/lib/i18n/routing';
import { useTranslations } from 'next-intl';
import { useAuth } from '@/lib/auth/AuthContext';
import { useLogout } from '@/lib/api/hooks/useAuth';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuGroup,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import {
User,
LogOut,
Shield,
Lock,
Monitor,
UserCog,
} from 'lucide-react';
import { cn } from '@/lib/utils';
interface UserMenuProps {
/** Additional className */
className?: string;
}
/**
* Get user initials for avatar display
*/
function getUserInitials(firstName?: string | null, lastName?: string | null): string {
if (!firstName) return 'U';
const first = firstName.charAt(0).toUpperCase();
const last = lastName?.charAt(0).toUpperCase() || '';
return `${first}${last}`;
}
export function UserMenu({ className }: UserMenuProps) {
const t = useTranslations('navigation');
const { user } = useAuth();
const { mutate: logout, isPending: isLoggingOut } = useLogout();
const handleLogout = () => {
logout();
};
if (!user) {
return null;
}
const initials = getUserInitials(user.first_name, user.last_name);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className={cn('relative h-9 w-9 rounded-full', className)}
data-testid="user-menu-trigger"
aria-label={`User menu for ${user.first_name || user.email}`}
>
<Avatar className="h-9 w-9">
<AvatarFallback className="text-sm">{initials}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56"
align="end"
data-testid="user-menu-content"
>
{/* User info header */}
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{user.first_name} {user.last_name}
</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Settings group */}
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link
href="/settings/profile"
className="cursor-pointer"
data-testid="user-menu-profile"
>
<User className="mr-2 h-4 w-4" aria-hidden="true" />
{t('profile')}
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
href="/settings/password"
className="cursor-pointer"
data-testid="user-menu-password"
>
<Lock className="mr-2 h-4 w-4" aria-hidden="true" />
{t('settings')}
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
href="/settings/sessions"
className="cursor-pointer"
data-testid="user-menu-sessions"
>
<Monitor className="mr-2 h-4 w-4" aria-hidden="true" />
Sessions
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
href="/settings/preferences"
className="cursor-pointer"
data-testid="user-menu-preferences"
>
<UserCog className="mr-2 h-4 w-4" aria-hidden="true" />
Preferences
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
{/* Admin link (superusers only) */}
{user.is_superuser && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href="/admin"
className="cursor-pointer"
data-testid="user-menu-admin"
>
<Shield className="mr-2 h-4 w-4" aria-hidden="true" />
{t('adminPanel')}
</Link>
</DropdownMenuItem>
</>
)}
{/* Logout */}
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer text-destructive focus:text-destructive focus:bg-destructive/10"
onClick={handleLogout}
disabled={isLoggingOut}
data-testid="user-menu-logout"
>
<LogOut className="mr-2 h-4 w-4" aria-hidden="true" />
{isLoggingOut ? t('loggingOut') : t('logout')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}