275 lines
8.8 KiB
TypeScript
275 lines
8.8 KiB
TypeScript
import React, { forwardRef } from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { Link } from 'react-router-dom';
|
|
import { Button, ThemeToggle } from '../../ui';
|
|
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
|
|
|
|
export interface PublicHeaderProps {
|
|
className?: string;
|
|
/**
|
|
* Custom logo component
|
|
*/
|
|
logo?: React.ReactNode;
|
|
/**
|
|
* Show theme toggle
|
|
*/
|
|
showThemeToggle?: boolean;
|
|
/**
|
|
* Show authentication buttons (login/register)
|
|
*/
|
|
showAuthButtons?: boolean;
|
|
/**
|
|
* Show language selector
|
|
*/
|
|
showLanguageSelector?: boolean;
|
|
/**
|
|
* 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,
|
|
showLanguageSelector = true,
|
|
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 */}
|
|
<div className="flex items-center gap-2 lg:gap-3">
|
|
{/* Language selector - More compact */}
|
|
{showLanguageSelector && (
|
|
<div className="hidden sm:flex">
|
|
<CompactLanguageSelector className="w-[70px]" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Theme toggle */}
|
|
{showThemeToggle && (
|
|
<ThemeToggle
|
|
variant="button"
|
|
size="md"
|
|
className="hidden sm:flex"
|
|
/>
|
|
)}
|
|
|
|
{/* Authentication buttons - Enhanced */}
|
|
{showAuthButtons && (
|
|
<div className="flex items-center gap-2 lg:gap-3">
|
|
<Link to="/login">
|
|
<Button
|
|
variant="ghost"
|
|
size="md"
|
|
className="hidden sm:inline-flex font-medium hover:bg-[var(--bg-secondary)] transition-all duration-200"
|
|
>
|
|
Iniciar Sesión
|
|
</Button>
|
|
</Link>
|
|
<Link to="/register">
|
|
<Button
|
|
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"
|
|
>
|
|
<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>
|
|
))}
|
|
|
|
{/* 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>
|
|
)}
|
|
|
|
{/* Mobile auth buttons */}
|
|
{showAuthButtons && (
|
|
<div className="flex flex-col gap-3 pt-4 sm:hidden">
|
|
<Link to="/login">
|
|
<Button
|
|
variant="ghost"
|
|
size="md"
|
|
className="w-full font-medium border border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]"
|
|
>
|
|
Iniciar Sesión
|
|
</Button>
|
|
</Link>
|
|
<Link to="/register">
|
|
<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"
|
|
>
|
|
Comenzar Gratis
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
);
|
|
});
|
|
|
|
PublicHeader.displayName = 'PublicHeader'; |