Add i18 support
This commit is contained in:
@@ -1,23 +1,20 @@
|
||||
import React, { useState, useCallback, forwardRef } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthUser, useIsAuthenticated } from '../../../stores';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { useNotifications } from '../../../hooks/useNotifications';
|
||||
import { Button } from '../../ui';
|
||||
import { Avatar } from '../../ui';
|
||||
import { Badge } from '../../ui';
|
||||
import { Modal } from '../../ui';
|
||||
import { TenantSwitcher } from '../../ui/TenantSwitcher';
|
||||
import { ThemeToggle } from '../../ui/ThemeToggle';
|
||||
import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel';
|
||||
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
|
||||
import {
|
||||
Menu,
|
||||
Search,
|
||||
Bell,
|
||||
Settings,
|
||||
User,
|
||||
LogOut,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -43,10 +40,6 @@ export interface HeaderProps {
|
||||
* Show/hide theme toggle
|
||||
*/
|
||||
showThemeToggle?: boolean;
|
||||
/**
|
||||
* Show/hide user menu
|
||||
*/
|
||||
showUserMenu?: boolean;
|
||||
/**
|
||||
* Custom logo component
|
||||
*/
|
||||
@@ -67,17 +60,14 @@ export interface HeaderProps {
|
||||
|
||||
export interface HeaderRef {
|
||||
focusSearch: () => void;
|
||||
toggleUserMenu: () => void;
|
||||
closeUserMenu: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header - Top navigation header with logo, user menu, notifications, theme toggle
|
||||
*
|
||||
* Header - Top navigation header with logo, notifications, theme toggle
|
||||
*
|
||||
* Features:
|
||||
* - Logo/brand area with responsive sizing
|
||||
* - Global search functionality with keyboard shortcuts
|
||||
* - User avatar with dropdown menu
|
||||
* - Notifications bell with badge count
|
||||
* - Theme toggle button (light/dark/system)
|
||||
* - Mobile hamburger menu integration
|
||||
@@ -90,16 +80,15 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
showSearch = true,
|
||||
showNotifications = true,
|
||||
showThemeToggle = true,
|
||||
showUserMenu = true,
|
||||
logo,
|
||||
searchPlaceholder = 'Buscar...',
|
||||
searchPlaceholder,
|
||||
notificationCount = 0,
|
||||
onNotificationClick,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthUser();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { logout } = useAuthActions();
|
||||
const { theme, resolvedTheme, setTheme } = useTheme();
|
||||
const {
|
||||
notifications,
|
||||
@@ -111,34 +100,22 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
clearAll
|
||||
} = useNotifications();
|
||||
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
|
||||
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const defaultSearchPlaceholder = searchPlaceholder || t('common:forms.search_placeholder', 'Search...');
|
||||
|
||||
// Focus search input
|
||||
const focusSearch = useCallback(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Toggle user menu
|
||||
const toggleUserMenu = useCallback(() => {
|
||||
setIsUserMenuOpen(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// Close user menu
|
||||
const closeUserMenu = useCallback(() => {
|
||||
setIsUserMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
// Expose ref methods
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focusSearch,
|
||||
toggleUserMenu,
|
||||
closeUserMenu,
|
||||
}), [focusSearch, toggleUserMenu, closeUserMenu]);
|
||||
}), [focusSearch]);
|
||||
|
||||
// Handle search
|
||||
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -158,11 +135,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
searchInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Handle logout
|
||||
const handleLogout = useCallback(async () => {
|
||||
await logout();
|
||||
setIsUserMenuOpen(false);
|
||||
}, [logout]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
React.useEffect(() => {
|
||||
@@ -175,7 +147,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
|
||||
// Escape to close menus
|
||||
if (e.key === 'Escape') {
|
||||
setIsUserMenuOpen(false);
|
||||
setIsNotificationPanelOpen(false);
|
||||
if (isSearchFocused) {
|
||||
searchInputRef.current?.blur();
|
||||
@@ -191,9 +162,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Element;
|
||||
if (!target.closest('[data-user-menu]')) {
|
||||
setIsUserMenuOpen(false);
|
||||
}
|
||||
if (!target.closest('[data-notification-panel]')) {
|
||||
setIsNotificationPanelOpen(false);
|
||||
}
|
||||
@@ -287,7 +255,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
onChange={handleSearchChange}
|
||||
onFocus={() => setIsSearchFocused(true)}
|
||||
onBlur={() => setIsSearchFocused(false)}
|
||||
placeholder={searchPlaceholder}
|
||||
placeholder={defaultSearchPlaceholder}
|
||||
className={clsx(
|
||||
'w-full pl-10 pr-12 py-2.5 text-sm',
|
||||
'bg-[var(--bg-secondary)] border border-[var(--border-primary)]',
|
||||
@@ -297,14 +265,14 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
'placeholder:text-[var(--text-tertiary)]',
|
||||
'h-9'
|
||||
)}
|
||||
aria-label="Buscar en la aplicación"
|
||||
aria-label={t('common:accessibility.search', 'Search in the application')}
|
||||
/>
|
||||
{searchValue ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-3 top-0 bottom-0 flex items-center p-1 hover:bg-[var(--bg-tertiary)] rounded-full transition-colors"
|
||||
aria-label="Limpiar búsqueda"
|
||||
aria-label={t('common:actions.clear', 'Clear search')}
|
||||
>
|
||||
<X className="h-3 w-3 text-[var(--text-tertiary)]" />
|
||||
</button>
|
||||
@@ -330,12 +298,15 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
size="sm"
|
||||
onClick={focusSearch}
|
||||
className="md:hidden w-10 h-10 p-0 flex items-center justify-center"
|
||||
aria-label="Buscar"
|
||||
aria-label={t('common:actions.search', 'Search')}
|
||||
>
|
||||
<Search className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Language selector */}
|
||||
<CompactLanguageSelector className="w-auto min-w-[60px]" />
|
||||
|
||||
{/* Theme toggle */}
|
||||
{showThemeToggle && (
|
||||
<ThemeToggle variant="button" size="md" />
|
||||
@@ -353,8 +324,8 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
!isConnected && "opacity-50",
|
||||
isNotificationPanelOpen && "bg-[var(--bg-secondary)]"
|
||||
)}
|
||||
aria-label={`Notificaciones${unreadCount > 0 ? ` (${unreadCount})` : ''}${!isConnected ? ' - Desconectado' : ''}`}
|
||||
title={!isConnected ? 'Sin conexión en tiempo real' : undefined}
|
||||
aria-label={`${t('common:navigation.notifications', 'Notifications')}${unreadCount > 0 ? ` (${unreadCount})` : ''}${!isConnected ? ` - ${t('common:status.disconnected', 'Disconnected')}` : ''}`}
|
||||
title={!isConnected ? t('common:status.no_realtime_connection', 'No real-time connection') : undefined}
|
||||
aria-expanded={isNotificationPanelOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
@@ -385,93 +356,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User menu */}
|
||||
{showUserMenu && user && (
|
||||
<div className="relative" data-user-menu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleUserMenu}
|
||||
className={clsx(
|
||||
"flex items-center gap-2 pl-2 pr-3 py-1.5 h-9 min-w-0 rounded-lg",
|
||||
"hover:bg-[var(--bg-secondary)] hover:ring-2 hover:ring-[var(--color-primary)]/20",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
"active:scale-95",
|
||||
isUserMenuOpen && "bg-[var(--bg-secondary)] ring-2 ring-[var(--color-primary)]/20"
|
||||
)}
|
||||
aria-label="Menú de usuario"
|
||||
aria-expanded={isUserMenuOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Avatar
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
name={user.name}
|
||||
size="xs"
|
||||
className={clsx(
|
||||
"flex-shrink-0 transition-all duration-200",
|
||||
"hover:ring-2 hover:ring-[var(--color-primary)]/30",
|
||||
isUserMenuOpen && "ring-2 ring-[var(--color-primary)]/30"
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{isUserMenuOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-56 bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-lg py-2 z-[var(--z-dropdown)]">
|
||||
{/* User info */}
|
||||
<div className="px-4 py-3 border-b border-[var(--border-primary)] flex items-center gap-3">
|
||||
<Avatar
|
||||
src={user.avatar || undefined}
|
||||
alt={user.name}
|
||||
name={user.name}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs text-[var(--text-tertiary)] truncate">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Menu items */}
|
||||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate('/app/settings/profile');
|
||||
setIsUserMenuOpen(false);
|
||||
}}
|
||||
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors"
|
||||
>
|
||||
<User className="h-4 w-4" />
|
||||
Perfil
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate('/app/settings');
|
||||
setIsUserMenuOpen(false);
|
||||
}}
|
||||
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
Configuración
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="border-t border-[var(--border-primary)] pt-1">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors text-[var(--color-error)]"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Cerrar Sesión
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user