forked from cardosofelipe/fast-next-template
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>
173 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
}
|