2025-12-03 21:56:50 +00:00
|
|
|
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';
|
2025-12-25 10:41:20 +00:00
|
|
|
import { GlobalSearchBar } from '@/components/search/GlobalSearchBar';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { FocusTrap } from '@/components/ui/focus-trap';
|
|
|
|
|
import {
|
|
|
|
|
Menu,
|
|
|
|
|
X,
|
|
|
|
|
User,
|
|
|
|
|
Settings,
|
|
|
|
|
LogOut,
|
|
|
|
|
Moon,
|
|
|
|
|
Sun,
|
|
|
|
|
Monitor,
|
2025-12-25 11:09:20 +00:00
|
|
|
Search,
|
2025-12-03 21:56:50 +00:00
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
export function Header() {
|
|
|
|
|
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
2025-12-25 11:09:20 +00:00
|
|
|
const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);
|
2025-12-03 21:56:50 +00:00
|
|
|
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" />;
|
2025-12-03 21:56:50 +00:00
|
|
|
case 'dark':
|
2025-12-13 02:34:34 +00:00
|
|
|
return <Moon className="h-4 w-4" />;
|
2025-12-03 21:56:50 +00:00
|
|
|
default:
|
2025-12-13 02:34:34 +00:00
|
|
|
return <Monitor className="h-4 w-4" />;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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">
|
2025-12-25 11:09:20 +00:00
|
|
|
<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 */}
|
2025-12-03 21:56:50 +00:00
|
|
|
<Button
|
2025-12-13 02:34:34 +00:00
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="md:hidden"
|
2025-12-25 11:09:20 +00:00
|
|
|
onClick={() => setIsMobileSearchOpen(!isMobileSearchOpen)}
|
|
|
|
|
aria-label={t('common.search')}
|
|
|
|
|
aria-expanded={isMobileSearchOpen}
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
2025-12-25 11:09:20 +00:00
|
|
|
<Search className="h-5 w-5" />
|
2025-12-03 21:56:50 +00:00
|
|
|
</Button>
|
|
|
|
|
|
2025-12-25 11:09:20 +00:00
|
|
|
{/* Actions utilisateur */}
|
|
|
|
|
<div className="flex items-center space-x-2">
|
2025-12-03 21:56:50 +00:00
|
|
|
{/* Notifications */}
|
|
|
|
|
<NotificationMenu />
|
|
|
|
|
|
|
|
|
|
{/* Thème */}
|
|
|
|
|
<Button
|
2025-12-13 02:34:34 +00:00
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2025-12-03 21:56:50 +00:00
|
|
|
onClick={toggleTheme}
|
|
|
|
|
aria-label={`${t('common.changeTheme')} - ${theme}`}
|
|
|
|
|
>
|
|
|
|
|
{getThemeIcon()}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* Menu utilisateur */}
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className="relative">
|
2025-12-03 21:56:50 +00:00
|
|
|
<Button
|
2025-12-13 02:34:34 +00:00
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2025-12-03 21:56:50 +00:00
|
|
|
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
|
|
|
|
|
aria-label={t('common.userMenu')}
|
|
|
|
|
aria-expanded={isUserMenuOpen}
|
2025-12-13 02:34:34 +00:00
|
|
|
aria-haspopup="menu"
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
2025-12-13 02:34:34 +00:00
|
|
|
<User className="h-5 w-5" />
|
2025-12-03 21:56:50 +00:00
|
|
|
</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-03 21:56:50 +00:00
|
|
|
>
|
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">
|
2025-12-03 21:56:50 +00:00
|
|
|
<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">
|
2025-12-03 21:56:50 +00:00
|
|
|
<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"
|
2025-12-03 21:56:50 +00:00
|
|
|
onClick={() => setIsUserMenuOpen(false)}
|
2025-12-13 02:34:34 +00:00
|
|
|
role="menuitem"
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
2025-12-13 02:34:34 +00:00
|
|
|
<User className="mr-2 h-4 w-4" aria-hidden="true" />
|
2025-12-03 21:56:50 +00:00
|
|
|
{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"
|
2025-12-03 21:56:50 +00:00
|
|
|
onClick={() => setIsUserMenuOpen(false)}
|
2025-12-13 02:34:34 +00:00
|
|
|
role="menuitem"
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
2025-12-13 02:34:34 +00:00
|
|
|
<Settings className="mr-2 h-4 w-4" aria-hidden="true" />
|
2025-12-03 21:56:50 +00:00
|
|
|
{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-03 21:56:50 +00:00
|
|
|
>
|
2025-12-13 02:34:34 +00:00
|
|
|
<LogOut className="mr-2 h-4 w-4" aria-hidden="true" />
|
2025-12-03 21:56:50 +00:00
|
|
|
{t('common.logout')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</FocusTrap>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-25 11:09:20 +00:00
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2025-12-25 11:09:20 +00:00
|
|
|
|
|
|
|
|
{/* Mobile search bar */}
|
|
|
|
|
{isMobileSearchOpen && (
|
|
|
|
|
<div className="md:hidden border-t px-4 py-3">
|
|
|
|
|
<GlobalSearchBar
|
|
|
|
|
className="w-full"
|
|
|
|
|
onSearch={() => setIsMobileSearchOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
);
|
|
|
|
|
}
|