2025-08-28 10:41:04 +02:00
|
|
|
import React, { useState, useCallback, forwardRef } from 'react';
|
|
|
|
|
import { clsx } from 'clsx';
|
2025-08-28 23:40:44 +02:00
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-09-22 11:04:03 +02:00
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
import { useAuthUser, useIsAuthenticated } from '../../../stores';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { useTheme } from '../../../contexts/ThemeContext';
|
2025-09-21 17:35:36 +02:00
|
|
|
import { useNotifications } from '../../../hooks/useNotifications';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { Button } from '../../ui';
|
|
|
|
|
import { Badge } from '../../ui';
|
2025-09-03 18:29:56 +02:00
|
|
|
import { TenantSwitcher } from '../../ui/TenantSwitcher';
|
2025-09-21 22:56:55 +02:00
|
|
|
import { ThemeToggle } from '../../ui/ThemeToggle';
|
2025-09-21 17:35:36 +02:00
|
|
|
import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel';
|
2025-09-22 11:04:03 +02:00
|
|
|
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
|
2025-09-21 22:56:55 +02:00
|
|
|
import {
|
|
|
|
|
Menu,
|
|
|
|
|
Bell,
|
2025-08-28 10:41:04 +02:00
|
|
|
X
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
export interface HeaderProps {
|
|
|
|
|
className?: string;
|
|
|
|
|
/**
|
|
|
|
|
* Callback when menu button is clicked (for mobile sidebar toggle)
|
|
|
|
|
*/
|
|
|
|
|
onMenuClick?: () => void;
|
|
|
|
|
/**
|
|
|
|
|
* Whether the sidebar is currently collapsed (affects logo display)
|
|
|
|
|
*/
|
|
|
|
|
sidebarCollapsed?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Show/hide search functionality
|
|
|
|
|
*/
|
|
|
|
|
showSearch?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Show/hide notifications
|
|
|
|
|
*/
|
|
|
|
|
showNotifications?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Show/hide theme toggle
|
|
|
|
|
*/
|
|
|
|
|
showThemeToggle?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Custom logo component
|
|
|
|
|
*/
|
|
|
|
|
logo?: React.ReactNode;
|
|
|
|
|
/**
|
|
|
|
|
* Custom search placeholder
|
|
|
|
|
*/
|
|
|
|
|
searchPlaceholder?: string;
|
|
|
|
|
/**
|
|
|
|
|
* Notification count
|
|
|
|
|
*/
|
|
|
|
|
notificationCount?: number;
|
|
|
|
|
/**
|
|
|
|
|
* Custom notification handler
|
|
|
|
|
*/
|
|
|
|
|
onNotificationClick?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface HeaderRef {
|
2025-09-24 19:40:51 +02:00
|
|
|
// No search-related methods anymore
|
2025-08-28 10:41:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-22 11:04:03 +02:00
|
|
|
* Header - Top navigation header with logo, notifications, theme toggle
|
|
|
|
|
*
|
2025-08-28 10:41:04 +02:00
|
|
|
* Features:
|
|
|
|
|
* - Logo/brand area with responsive sizing
|
|
|
|
|
* - Global search functionality with keyboard shortcuts
|
|
|
|
|
* - Notifications bell with badge count
|
|
|
|
|
* - Theme toggle button (light/dark/system)
|
|
|
|
|
* - Mobile hamburger menu integration
|
|
|
|
|
* - Keyboard navigation support
|
|
|
|
|
*/
|
|
|
|
|
export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|
|
|
|
className,
|
|
|
|
|
onMenuClick,
|
|
|
|
|
sidebarCollapsed = false,
|
|
|
|
|
showSearch = true,
|
|
|
|
|
showNotifications = true,
|
|
|
|
|
showThemeToggle = true,
|
|
|
|
|
logo,
|
2025-09-22 11:04:03 +02:00
|
|
|
searchPlaceholder,
|
2025-08-28 10:41:04 +02:00
|
|
|
notificationCount = 0,
|
|
|
|
|
onNotificationClick,
|
|
|
|
|
}, ref) => {
|
2025-09-22 11:04:03 +02:00
|
|
|
const { t } = useTranslation();
|
2025-08-28 23:40:44 +02:00
|
|
|
const navigate = useNavigate();
|
2025-08-28 10:41:04 +02:00
|
|
|
const user = useAuthUser();
|
|
|
|
|
const isAuthenticated = useIsAuthenticated();
|
|
|
|
|
const { theme, resolvedTheme, setTheme } = useTheme();
|
2025-09-21 17:35:36 +02:00
|
|
|
const {
|
|
|
|
|
notifications,
|
|
|
|
|
unreadCount,
|
|
|
|
|
isConnected,
|
|
|
|
|
markAsRead,
|
|
|
|
|
markAllAsRead,
|
|
|
|
|
removeNotification,
|
|
|
|
|
clearAll
|
|
|
|
|
} = useNotifications();
|
|
|
|
|
|
|
|
|
|
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-22 11:04:03 +02:00
|
|
|
const defaultSearchPlaceholder = searchPlaceholder || t('common:forms.search_placeholder', 'Search...');
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
// Expose ref methods
|
|
|
|
|
React.useImperativeHandle(ref, () => ({
|
2025-09-24 19:40:51 +02:00
|
|
|
// No search functions available anymore
|
|
|
|
|
}), []);
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard shortcuts
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
// Escape to close menus
|
|
|
|
|
if (e.key === 'Escape') {
|
2025-09-21 17:35:36 +02:00
|
|
|
setIsNotificationPanelOpen(false);
|
2025-08-28 10:41:04 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
2025-09-24 19:40:51 +02:00
|
|
|
}, []);
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
// Close menus when clicking outside
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
|
|
|
const target = event.target as Element;
|
2025-09-21 17:35:36 +02:00
|
|
|
if (!target.closest('[data-notification-panel]')) {
|
|
|
|
|
setIsNotificationPanelOpen(false);
|
|
|
|
|
}
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', handleClickOutside);
|
|
|
|
|
return () => document.removeEventListener('click', handleClickOutside);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<header
|
|
|
|
|
className={clsx(
|
|
|
|
|
'fixed top-0 left-0 right-0 h-[var(--header-height)]',
|
|
|
|
|
'bg-[var(--bg-primary)] border-b border-[var(--border-primary)]',
|
|
|
|
|
'flex items-center justify-between px-4 lg:px-6',
|
|
|
|
|
'transition-all duration-300 ease-in-out',
|
|
|
|
|
'backdrop-blur-sm bg-[var(--bg-primary)]/95',
|
2025-08-28 17:15:29 +02:00
|
|
|
'z-[var(--z-fixed)]',
|
2025-08-28 10:41:04 +02:00
|
|
|
className
|
|
|
|
|
)}
|
|
|
|
|
role="banner"
|
2025-09-25 12:14:46 +02:00
|
|
|
aria-label={t('common:header.main_navigation', 'Navegación principal')}
|
2025-08-28 10:41:04 +02:00
|
|
|
>
|
|
|
|
|
{/* Left section */}
|
2025-08-31 22:14:05 +02:00
|
|
|
<div className="flex items-center gap-2 sm:gap-4 flex-1 min-w-0 h-full">
|
2025-08-28 10:41:04 +02:00
|
|
|
{/* Mobile menu button */}
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onMenuClick}
|
2025-08-28 23:40:44 +02:00
|
|
|
className="lg:hidden w-10 h-10 p-0 flex items-center justify-center hover:bg-[var(--bg-secondary)] active:scale-95 transition-all duration-150"
|
2025-09-25 12:14:46 +02:00
|
|
|
aria-label={t('common:header.open_menu', 'Abrir menú de navegación')}
|
2025-08-28 10:41:04 +02:00
|
|
|
>
|
2025-08-28 23:40:44 +02:00
|
|
|
<Menu className="h-5 w-5 text-[var(--text-primary)]" />
|
2025-08-28 10:41:04 +02:00
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* Logo */}
|
2025-08-31 22:14:05 +02:00
|
|
|
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-shrink-0">
|
2025-08-28 10:41:04 +02:00
|
|
|
{logo || (
|
|
|
|
|
<>
|
2025-08-28 17:15:29 +02:00
|
|
|
<div className="w-8 h-8 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
|
2025-08-28 10:41:04 +02:00
|
|
|
PI
|
|
|
|
|
</div>
|
|
|
|
|
<h1 className={clsx(
|
|
|
|
|
'font-semibold text-[var(--text-primary)] transition-opacity duration-300',
|
2025-08-31 22:14:05 +02:00
|
|
|
'hidden md:block text-lg leading-tight whitespace-nowrap',
|
2025-08-28 17:15:29 +02:00
|
|
|
'self-center',
|
2025-08-28 10:41:04 +02:00
|
|
|
sidebarCollapsed ? 'lg:block' : 'lg:hidden xl:block'
|
|
|
|
|
)}>
|
2025-09-25 12:14:46 +02:00
|
|
|
{t('common:app.name', 'Panadería IA')}
|
2025-08-28 10:41:04 +02:00
|
|
|
</h1>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-03 18:29:56 +02:00
|
|
|
{/* Tenant Switcher - Desktop */}
|
|
|
|
|
{isAuthenticated && (
|
2025-08-31 22:14:05 +02:00
|
|
|
<div className="hidden md:block mx-2 lg:mx-4 flex-shrink-0">
|
2025-09-03 18:29:56 +02:00
|
|
|
<TenantSwitcher
|
|
|
|
|
showLabel={true}
|
2025-08-31 22:14:05 +02:00
|
|
|
className="min-w-[160px] max-w-[220px] lg:min-w-[200px] lg:max-w-[280px]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-09-03 18:29:56 +02:00
|
|
|
{/* Tenant Switcher - Mobile (in title area) */}
|
|
|
|
|
{isAuthenticated && (
|
2025-08-31 22:14:05 +02:00
|
|
|
<div className="md:hidden flex-1 min-w-0 ml-3">
|
2025-09-03 18:29:56 +02:00
|
|
|
<TenantSwitcher
|
|
|
|
|
showLabel={false}
|
2025-08-31 22:14:05 +02:00
|
|
|
className="w-full max-w-none"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-09-24 19:40:51 +02:00
|
|
|
{/* Space for potential future content */ }
|
|
|
|
|
{isAuthenticated && (
|
|
|
|
|
<div className="hidden md:flex items-center flex-1 max-w-md mx-4">
|
|
|
|
|
{/* Empty space to maintain layout consistency */}
|
|
|
|
|
</div>
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Right section */}
|
|
|
|
|
{isAuthenticated && (
|
2025-08-30 19:21:15 +02:00
|
|
|
<div className="flex items-center gap-1">
|
2025-09-24 19:40:51 +02:00
|
|
|
{/* Placeholder for potential future items */ }
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-22 11:04:03 +02:00
|
|
|
{/* Language selector */}
|
|
|
|
|
<CompactLanguageSelector className="w-auto min-w-[60px]" />
|
|
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
{/* Theme toggle */}
|
|
|
|
|
{showThemeToggle && (
|
2025-09-21 22:56:55 +02:00
|
|
|
<ThemeToggle variant="button" size="md" />
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Notifications */}
|
|
|
|
|
{showNotifications && (
|
2025-09-21 17:35:36 +02:00
|
|
|
<div className="relative" data-notification-panel>
|
2025-08-28 10:41:04 +02:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
2025-09-21 17:35:36 +02:00
|
|
|
onClick={() => setIsNotificationPanelOpen(!isNotificationPanelOpen)}
|
|
|
|
|
className={clsx(
|
|
|
|
|
"w-10 h-10 p-0 flex items-center justify-center relative",
|
|
|
|
|
!isConnected && "opacity-50",
|
|
|
|
|
isNotificationPanelOpen && "bg-[var(--bg-secondary)]"
|
|
|
|
|
)}
|
2025-09-22 11:04:03 +02:00
|
|
|
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}
|
2025-09-21 17:35:36 +02:00
|
|
|
aria-expanded={isNotificationPanelOpen}
|
|
|
|
|
aria-haspopup="true"
|
2025-08-28 10:41:04 +02:00
|
|
|
>
|
2025-09-21 17:35:36 +02:00
|
|
|
<Bell className={clsx(
|
|
|
|
|
"h-5 w-5 transition-colors",
|
|
|
|
|
unreadCount > 0 && "text-[var(--color-warning)]"
|
|
|
|
|
)} />
|
|
|
|
|
{unreadCount > 0 && (
|
2025-08-28 10:41:04 +02:00
|
|
|
<Badge
|
|
|
|
|
variant="error"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="absolute -top-1 -right-1 min-w-[18px] h-[18px] text-xs flex items-center justify-center"
|
|
|
|
|
>
|
2025-09-21 17:35:36 +02:00
|
|
|
{unreadCount > 99 ? '99+' : unreadCount}
|
2025-08-28 10:41:04 +02:00
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
2025-09-21 17:35:36 +02:00
|
|
|
|
|
|
|
|
<NotificationPanel
|
|
|
|
|
notifications={notifications}
|
|
|
|
|
isOpen={isNotificationPanelOpen}
|
|
|
|
|
onClose={() => setIsNotificationPanelOpen(false)}
|
|
|
|
|
onMarkAsRead={markAsRead}
|
|
|
|
|
onMarkAllAsRead={markAllAsRead}
|
|
|
|
|
onRemoveNotification={removeNotification}
|
|
|
|
|
onClearAll={clearAll}
|
|
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</header>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Header.displayName = 'Header';
|