Files
bakery-ia/frontend/src/components/layout/PublicHeader/PublicHeader.tsx

275 lines
8.8 KiB
TypeScript
Raw Normal View History

2025-08-28 10:41:04 +02:00
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { Link } from 'react-router-dom';
import { Button, ThemeToggle } from '../../ui';
2025-09-25 12:14:46 +02:00
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
2025-08-28 10:41:04 +02:00
export interface PublicHeaderProps {
className?: string;
/**
* Custom logo component
*/
logo?: React.ReactNode;
/**
* Show theme toggle
*/
showThemeToggle?: boolean;
/**
* Show authentication buttons (login/register)
*/
showAuthButtons?: boolean;
2025-09-25 12:14:46 +02:00
/**
* Show language selector
*/
showLanguageSelector?: boolean;
2025-08-28 10:41:04 +02:00
/**
* Custom navigation items
*/
navigationItems?: Array<{
id: string;
label: string;
href: string;
external?: boolean;
}>;
/**
* Header variant
*/
variant?: 'default' | 'transparent' | 'minimal';
}
export interface PublicHeaderRef {
scrollIntoView: () => void;
}
/**
* PublicHeader - Header component for public pages (landing, login, register)
*
* Features:
* - Clean, minimal design suitable for public pages
* - Integrated theme toggle for consistent theming
* - Authentication buttons (login/register)
* - Optional custom navigation items
* - Multiple visual variants
* - Responsive design with mobile optimization
* - Accessible navigation structure
*/
export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
className,
logo,
showThemeToggle = true,
showAuthButtons = true,
2025-09-25 12:14:46 +02:00
showLanguageSelector = true,
2025-08-28 10:41:04 +02:00
navigationItems = [],
variant = 'default',
}, ref) => {
const headerRef = React.useRef<HTMLDivElement>(null);
// Default navigation items
const defaultNavItems = [
// { id: 'features', label: 'Características', href: '#features', external: false },
// { id: 'pricing', label: 'Precios', href: '#pricing', external: false },
// { id: 'contact', label: 'Contacto', href: '#contact', external: false },
];
const navItems = navigationItems.length > 0 ? navigationItems : defaultNavItems;
// Scroll into view
const scrollIntoView = React.useCallback(() => {
headerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, []);
// Expose ref methods
React.useImperativeHandle(ref, () => ({
scrollIntoView,
}), [scrollIntoView]);
// Render navigation link
const renderNavLink = (item: typeof navItems[0]) => {
const linkContent = (
<span className="text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors duration-200">
{item.label}
</span>
);
if (item.external || item.href.startsWith('http') || item.href.startsWith('#')) {
return (
<a
key={item.id}
href={item.href}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
className="hover:underline focus:outline-none focus:underline"
>
{linkContent}
</a>
);
}
return (
<Link
key={item.id}
to={item.href}
className="hover:underline focus:outline-none focus:underline"
>
{linkContent}
</Link>
);
};
return (
<header
ref={headerRef}
className={clsx(
'w-full',
// Base styles
variant === 'default' && 'bg-[var(--bg-primary)] border-b border-[var(--border-primary)] shadow-sm',
variant === 'transparent' && 'bg-transparent',
variant === 'minimal' && 'bg-[var(--bg-primary)]',
className
)}
role="banner"
aria-label="Navegación principal"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4 lg:py-6">
{/* Logo and brand */}
<div className="flex items-center gap-3 min-w-0">
<Link to="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity">
{logo || (
<>
<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">
PI
</div>
<h1 className="text-xl lg:text-2xl font-bold text-[var(--text-primary)]">
Panadería IA
</h1>
</>
)}
</Link>
</div>
{/* Desktop navigation */}
<nav className="hidden md:flex items-center space-x-8" role="navigation">
{navItems.map(renderNavLink)}
</nav>
{/* Right side actions */}
2025-10-03 14:09:34 +02:00
<div className="flex items-center gap-2 lg:gap-3">
{/* Language selector - More compact */}
2025-09-25 12:14:46 +02:00
{showLanguageSelector && (
2025-10-03 14:09:34 +02:00
<div className="hidden sm:flex">
<CompactLanguageSelector className="w-[70px]" />
</div>
2025-09-25 12:14:46 +02:00
)}
2025-08-28 10:41:04 +02:00
{/* Theme toggle */}
{showThemeToggle && (
<ThemeToggle
variant="button"
size="md"
className="hidden sm:flex"
/>
)}
2025-10-03 14:09:34 +02:00
{/* Authentication buttons - Enhanced */}
2025-08-28 10:41:04 +02:00
{showAuthButtons && (
2025-10-03 14:09:34 +02:00
<div className="flex items-center gap-2 lg:gap-3">
2025-08-28 10:41:04 +02:00
<Link to="/login">
2025-09-25 12:14:46 +02:00
<Button
variant="ghost"
2025-10-03 14:09:34 +02:00
size="md"
className="hidden sm:inline-flex font-medium hover:bg-[var(--bg-secondary)] transition-all duration-200"
2025-08-28 10:41:04 +02:00
>
Iniciar Sesión
</Button>
</Link>
<Link to="/register">
2025-09-25 12:14:46 +02:00
<Button
2025-10-03 14:09:34 +02:00
size="md"
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:opacity-90 text-white font-semibold shadow-lg hover:shadow-xl transition-all duration-200 px-6"
2025-08-28 10:41:04 +02:00
>
<span className="hidden sm:inline">Comenzar Gratis</span>
<span className="sm:hidden">Registro</span>
</Button>
</Link>
</div>
)}
{/* Mobile theme toggle */}
{showThemeToggle && (
<ThemeToggle
variant="button"
size="sm"
className="sm:hidden"
/>
)}
{/* Mobile menu button */}
<div className="md:hidden">
<Button
variant="ghost"
size="sm"
className="p-2"
aria-label="Abrir menú de navegación"
onClick={() => {
// TODO: Implement mobile menu
console.log('Mobile menu toggle');
}}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</Button>
</div>
</div>
</div>
{/* Mobile navigation */}
<nav className="md:hidden pb-4" role="navigation">
<div className="flex flex-col space-y-2">
{navItems.map(item => (
<div key={item.id} className="py-2 border-b border-[var(--border-primary)] last:border-b-0">
{renderNavLink(item)}
</div>
))}
2025-09-25 12:14:46 +02:00
{/* Mobile language selector */}
{showLanguageSelector && (
<div className="py-2 border-b border-[var(--border-primary)] sm:hidden">
<div className="text-sm font-medium text-[var(--text-secondary)] mb-2">
Idioma
</div>
<CompactLanguageSelector className="w-full" />
</div>
)}
2025-08-28 10:41:04 +02:00
{/* Mobile auth buttons */}
{showAuthButtons && (
2025-10-03 14:09:34 +02:00
<div className="flex flex-col gap-3 pt-4 sm:hidden">
2025-08-28 10:41:04 +02:00
<Link to="/login">
2025-10-03 14:09:34 +02:00
<Button
variant="ghost"
size="md"
className="w-full font-medium border border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]"
>
2025-08-28 10:41:04 +02:00
Iniciar Sesión
</Button>
</Link>
<Link to="/register">
2025-10-03 14:09:34 +02:00
<Button
size="md"
className="w-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:opacity-90 text-white font-semibold shadow-lg"
>
2025-08-28 10:41:04 +02:00
Comenzar Gratis
</Button>
</Link>
</div>
)}
</div>
</nav>
</div>
</header>
);
});
PublicHeader.displayName = 'PublicHeader';