ADD new frontend
This commit is contained in:
245
frontend/src/components/layout/PublicHeader/PublicHeader.tsx
Normal file
245
frontend/src/components/layout/PublicHeader/PublicHeader.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, ThemeToggle } from '../../ui';
|
||||
|
||||
export interface PublicHeaderProps {
|
||||
className?: string;
|
||||
/**
|
||||
* Custom logo component
|
||||
*/
|
||||
logo?: React.ReactNode;
|
||||
/**
|
||||
* Show theme toggle
|
||||
*/
|
||||
showThemeToggle?: boolean;
|
||||
/**
|
||||
* Show authentication buttons (login/register)
|
||||
*/
|
||||
showAuthButtons?: 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,
|
||||
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-3">
|
||||
{/* Theme toggle */}
|
||||
{showThemeToggle && (
|
||||
<ThemeToggle
|
||||
variant="button"
|
||||
size="md"
|
||||
className="hidden sm:flex"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Authentication buttons */}
|
||||
{showAuthButtons && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Link to="/login">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="hidden sm:inline-flex"
|
||||
>
|
||||
Iniciar Sesión
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/register">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white"
|
||||
>
|
||||
<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 auth buttons */}
|
||||
{showAuthButtons && (
|
||||
<div className="flex flex-col gap-2 pt-4 sm:hidden">
|
||||
<Link to="/login">
|
||||
<Button variant="ghost" size="sm" className="w-full">
|
||||
Iniciar Sesión
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/register">
|
||||
<Button size="sm" className="w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white">
|
||||
Comenzar Gratis
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
||||
PublicHeader.displayName = 'PublicHeader';
|
||||
Reference in New Issue
Block a user