189 lines
6.7 KiB
TypeScript
189 lines
6.7 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|