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

205 lines
7.1 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 { GlobalSearchBar } from '@/components/search/GlobalSearchBar';
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 [isMobileSearchOpen, setIsMobileSearchOpen] = 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':
2025-12-13 02:34:34 +00:00
return <Sun className="h-4 w-4" />;
case 'dark':
2025-12-13 02:34:34 +00:00
return <Moon className="h-4 w-4" />;
default:
2025-12-13 02:34:34 +00:00
return <Monitor className="h-4 w-4" />;
}
};
return (
2025-12-13 02:34:34 +00:00
<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">
{/* Main header row */}
<div className="flex h-16 items-center justify-between">
{/* Logo et menu mobile */}
<div className="flex items-center space-x-2 sm: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-lg sm:text-xl">Veza</span>
</Link>
</div>
{/* Barre de recherche (desktop) */}
<div className="hidden md:flex flex-1 max-w-md mx-4">
<GlobalSearchBar className="w-full" />
</div>
{/* Mobile search button */}
<Button
2025-12-13 02:34:34 +00:00
variant="ghost"
size="icon"
className="md:hidden"
onClick={() => setIsMobileSearchOpen(!isMobileSearchOpen)}
aria-label={t('common.search')}
aria-expanded={isMobileSearchOpen}
>
<Search className="h-5 w-5" />
</Button>
{/* Actions utilisateur */}
<div className="flex items-center space-x-2">
{/* Notifications */}
<NotificationMenu />
{/* Thème */}
<Button
2025-12-13 02:34:34 +00:00
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label={`${t('common.changeTheme')} - ${theme}`}
>
{getThemeIcon()}
</Button>
{/* Menu utilisateur */}
2025-12-13 02:34:34 +00:00
<div className="relative">
<Button
2025-12-13 02:34:34 +00:00
variant="ghost"
size="icon"
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
aria-label={t('common.userMenu')}
aria-expanded={isUserMenuOpen}
2025-12-13 02:34:34 +00:00
aria-haspopup="menu"
>
2025-12-13 02:34:34 +00:00
<User className="h-5 w-5" />
</Button>
{isUserMenuOpen && (
<FocusTrap
active={isUserMenuOpen}
onEscape={() => setIsUserMenuOpen(false)}
>
<div
2025-12-13 02:34:34 +00:00
className="absolute right-0 mt-2 w-48 bg-popover border rounded-md shadow-lg z-50"
role="menu"
aria-orientation="vertical"
>
2025-12-13 02:34:34 +00:00
<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>
2025-12-13 02:34:34 +00:00
<div className="py-1">
<Link
2025-12-13 02:34:34 +00:00
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)}
2025-12-13 02:34:34 +00:00
role="menuitem"
>
2025-12-13 02:34:34 +00:00
<User className="mr-2 h-4 w-4" aria-hidden="true" />
{t('navigation.profile')}
</Link>
<Link
2025-12-13 02:34:34 +00:00
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)}
2025-12-13 02:34:34 +00:00
role="menuitem"
>
2025-12-13 02:34:34 +00:00
<Settings className="mr-2 h-4 w-4" aria-hidden="true" />
{t('navigation.settings')}
</Link>
<button
onClick={() => {
handleLogout();
setIsUserMenuOpen(false);
}}
2025-12-13 02:34:34 +00:00
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"
>
2025-12-13 02:34:34 +00:00
<LogOut className="mr-2 h-4 w-4" aria-hidden="true" />
{t('common.logout')}
</button>
</div>
</div>
</div>
</FocusTrap>
)}
</div>
</div>
</div>
{/* Mobile search bar */}
{isMobileSearchOpen && (
<div className="md:hidden border-t px-4 py-3">
<GlobalSearchBar
className="w-full"
onSearch={() => setIsMobileSearchOpen(false)}
/>
</div>
)}
</div>
</header>
);
}