veza/apps/web/src/components/layout/Header.tsx

189 lines
6.7 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuthStore } from '@/stores/auth';
import { useUIStore } from '@/stores/ui';
import { useTranslation } from '@/hooks/useTranslation';
import { EmailVerificationBadge } from '@/features/auth/components/EmailVerificationBadge';
import { NotificationMenu } from '@/components/notifications/NotificationMenu';
import { Button } from '@/components/ui/button';
import { FocusTrap } from '@/components/ui/focus-trap';
import {
Menu,
X,
User,
Settings,
LogOut,
Moon,
Sun,
Monitor,
Search,
} from 'lucide-react';
export function Header() {
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const { user, logout } = useAuthStore();
const { theme, setTheme, sidebarOpen, setSidebarOpen } = useUIStore();
const { t } = useTranslation();
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate('/login');
};
const toggleTheme = () => {
const newTheme =
theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light';
setTheme(newTheme);
};
const getThemeIcon = () => {
switch (theme) {
case 'light':
return <Sun className='h-4 w-4' />;
case 'dark':
return <Moon className='h-4 w-4' />;
default:
return <Monitor className='h-4 w-4' />;
}
};
return (
<header className='sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60'>
<div className='container flex h-16 items-center justify-between'>
{/* Logo et menu mobile */}
<div className='flex items-center space-x-4'>
<Button
variant='ghost'
size='icon'
className='md:hidden'
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={
sidebarOpen ? t('navigation.close') : t('navigation.menu')
}
aria-expanded={sidebarOpen}
>
{sidebarOpen ? (
<X className='h-5 w-5' />
) : (
<Menu className='h-5 w-5' />
)}
</Button>
<Link to='/dashboard' className='flex items-center space-x-2'>
<div
className='h-8 w-8 rounded-lg bg-primary flex items-center justify-center'
aria-hidden='true'
>
<span className='text-primary-foreground font-bold text-lg'>
V
</span>
</div>
<span className='font-bold text-xl'>Veza</span>
</Link>
</div>
{/* Barre de recherche (desktop) */}
<div className='hidden md:flex flex-1 max-w-md mx-4'>
<div className='relative w-full'>
<Search
className='absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground'
aria-hidden='true'
/>
<input
type='text'
placeholder={t('common.search')}
aria-label={t('common.search')}
className='w-full pl-10 pr-4 py-2 border border-input rounded-md bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2'
/>
</div>
</div>
{/* Actions utilisateur */}
<div className='flex items-center space-x-2'>
{/* Notifications */}
<NotificationMenu />
{/* Thème */}
<Button
variant='ghost'
size='icon'
onClick={toggleTheme}
aria-label={`${t('common.changeTheme')} - ${theme}`}
>
{getThemeIcon()}
</Button>
{/* Menu utilisateur */}
<div className='relative'>
<Button
variant='ghost'
size='icon'
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
aria-label={t('common.userMenu')}
aria-expanded={isUserMenuOpen}
aria-haspopup='menu'
>
<User className='h-5 w-5' />
</Button>
{isUserMenuOpen && (
<FocusTrap
active={isUserMenuOpen}
onEscape={() => setIsUserMenuOpen(false)}
>
<div
className='absolute right-0 mt-2 w-48 bg-popover border rounded-md shadow-lg z-50'
role='menu'
aria-orientation='vertical'
>
<div className='p-2'>
<div className='px-3 py-2 text-sm font-medium text-foreground border-b space-y-2'>
<div>{user?.username}</div>
{/* T0190: Afficher badge si email non vérifié */}
{user && !user.is_verified && (
<EmailVerificationBadge verified={false} />
)}
</div>
<div className='py-1'>
<Link
to='/profile'
className='flex items-center px-3 py-2 text-sm text-foreground hover:bg-accent rounded-sm focus:outline-none focus:ring-2 focus:ring-ring'
onClick={() => setIsUserMenuOpen(false)}
role='menuitem'
>
<User className='mr-2 h-4 w-4' aria-hidden='true' />
{t('navigation.profile')}
</Link>
<Link
to='/settings'
className='flex items-center px-3 py-2 text-sm text-foreground hover:bg-accent rounded-sm focus:outline-none focus:ring-2 focus:ring-ring'
onClick={() => setIsUserMenuOpen(false)}
role='menuitem'
>
<Settings className='mr-2 h-4 w-4' aria-hidden='true' />
{t('navigation.settings')}
</Link>
<button
onClick={() => {
handleLogout();
setIsUserMenuOpen(false);
}}
className='flex items-center w-full px-3 py-2 text-sm text-foreground hover:bg-accent rounded-sm focus:outline-none focus:ring-2 focus:ring-ring'
role='menuitem'
>
<LogOut className='mr-2 h-4 w-4' aria-hidden='true' />
{t('common.logout')}
</button>
</div>
</div>
</div>
</FocusTrap>
)}
</div>
</div>
</div>
</header>
);
}