Add user delete process

This commit is contained in:
Urtzi Alfaro
2025-10-31 11:54:19 +01:00
parent 63f5c6d512
commit 269d3b5032
74 changed files with 16783 additions and 213 deletions

View File

@@ -1,10 +1,11 @@
import React, { forwardRef } from 'react';
import React, { forwardRef, useState, useEffect, useCallback } from 'react';
import { clsx } from 'clsx';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button, ThemeToggle } from '../../ui';
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
import { getRegisterUrl, getLoginUrl } from '../../../utils/navigation';
import { X } from 'lucide-react';
export interface PublicHeaderProps {
className?: string;
@@ -67,17 +68,113 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
const { t } = useTranslation();
const headerRef = React.useRef<HTMLDivElement>(null);
// State for mobile menu
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// State for sticky header
const [isScrolled, setIsScrolled] = useState(false);
// State for active section
const [activeSection, setActiveSection] = useState<string>('');
// 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 defaultNavItems: Array<{id: string; label: string; href: string; external?: boolean}> = [];
const navItems = navigationItems.length > 0 ? navigationItems : defaultNavItems;
// Smooth scroll to section
const scrollToSection = useCallback((href: string) => {
if (href.startsWith('#')) {
const element = document.querySelector(href);
if (element) {
const headerHeight = headerRef.current?.offsetHeight || 0;
const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementPosition - headerHeight - 20; // 20px additional offset
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
// Update URL hash
window.history.pushState(null, '', href);
// Close mobile menu
setIsMobileMenuOpen(false);
}
}
}, []);
// Handle scroll for sticky header
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Active section detection with Intersection Observer
useEffect(() => {
const observerOptions = {
rootMargin: '-100px 0px -66%',
threshold: 0
};
const observerCallback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.getAttribute('id');
if (id) {
setActiveSection(id);
}
}
});
};
const observer = new IntersectionObserver(observerCallback, observerOptions);
// Observe all sections that are navigation targets
navItems.forEach(item => {
if (item.href.startsWith('#')) {
const element = document.querySelector(item.href);
if (element) {
observer.observe(element);
}
}
});
return () => observer.disconnect();
}, [navItems]);
// Close mobile menu on ESC key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isMobileMenuOpen) {
setIsMobileMenuOpen(false);
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isMobileMenuOpen]);
// Prevent body scroll when mobile menu is open
useEffect(() => {
if (isMobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isMobileMenuOpen]);
// Scroll into view
const scrollIntoView = React.useCallback(() => {
const scrollIntoView = useCallback(() => {
headerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, []);
@@ -86,22 +183,57 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
scrollIntoView,
}), [scrollIntoView]);
// Render navigation link
const renderNavLink = (item: typeof navItems[0]) => {
// Render navigation link with improved styles and active state
const renderNavLink = (item: typeof navItems[0], isMobile = false) => {
const isActive = activeSection === item.id || (item.href.startsWith('#') && item.href === `#${activeSection}`);
const linkContent = (
<span className="text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors duration-200">
<span className={clsx(
"relative text-sm font-medium transition-all duration-200",
isMobile ? "text-base py-1" : "",
isActive
? "text-[var(--color-primary)]"
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
// Animated underline on hover
!isMobile && "after:content-[''] after:absolute after:bottom-[-4px] after:left-0 after:w-0 after:h-[2px] after:bg-[var(--color-primary)] after:transition-all after:duration-300 hover:after:w-full",
// Active state indicator
isActive && !isMobile && "after:w-full"
)}>
{item.label}
</span>
);
if (item.external || item.href.startsWith('http') || item.href.startsWith('#')) {
if (item.href.startsWith('#')) {
return (
<a
key={item.id}
href={item.href}
onClick={(e) => {
e.preventDefault();
scrollToSection(item.href);
}}
className={clsx(
"focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 rounded-sm",
isMobile && "block w-full py-3 px-4 hover:bg-[var(--bg-secondary)] transition-colors"
)}
aria-current={isActive ? 'page' : undefined}
>
{linkContent}
</a>
);
}
if (item.external || item.href.startsWith('http')) {
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"
className={clsx(
"focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 rounded-sm",
isMobile && "block w-full py-3 px-4 hover:bg-[var(--bg-secondary)] transition-colors"
)}
>
{linkContent}
</a>
@@ -112,7 +244,10 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
<Link
key={item.id}
to={item.href}
className="hover:underline focus:outline-none focus:underline"
className={clsx(
"focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 rounded-sm",
isMobile && "block w-full py-3 px-4 hover:bg-[var(--bg-secondary)] transition-colors"
)}
>
{linkContent}
</Link>
@@ -120,21 +255,43 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
};
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">
<>
{/* Skip to main content link for accessibility */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:top-4 focus:left-4 focus:px-4 focus:py-2 focus:bg-[var(--color-primary)] focus:text-white focus:rounded-md"
>
{t('common:header.skip_to_content', 'Saltar al contenido principal')}
</a>
<header
ref={headerRef}
className={clsx(
'w-full transition-all duration-300',
// Sticky header
'fixed top-0 left-0 right-0 z-40',
// Base styles with scroll effect
variant === 'default' && [
isScrolled
? 'bg-[var(--bg-primary)]/95 backdrop-blur-md border-b border-[var(--border-primary)] shadow-lg'
: 'bg-[var(--bg-primary)] border-b border-[var(--border-primary)] shadow-sm'
],
variant === 'transparent' && [
isScrolled
? 'bg-[var(--bg-primary)]/95 backdrop-blur-md shadow-lg'
: '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={clsx(
"flex justify-between items-center transition-all duration-300",
isScrolled ? "py-3 lg:py-4" : "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">
@@ -153,7 +310,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
{/* Desktop navigation */}
<nav className="hidden md:flex items-center space-x-8" role="navigation">
{navItems.map(renderNavLink)}
{navItems.map((item) => renderNavLink(item, false))}
</nav>
{/* Right side actions */}
@@ -212,66 +369,142 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
<Button
variant="ghost"
size="sm"
className="p-2"
aria-label={t('common:header.open_menu')}
onClick={() => {
// TODO: Implement mobile menu
console.log('Mobile menu toggle');
}}
className="p-2 min-h-[44px] min-w-[44px]"
aria-label={isMobileMenuOpen ? t('common:header.close_menu', 'Cerrar menú') : t('common:header.open_menu', 'Abrir menú')}
aria-expanded={isMobileMenuOpen}
aria-controls="mobile-menu"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
<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
className={clsx("w-6 h-6 transition-transform duration-300", isMobileMenuOpen && "rotate-90")}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{isMobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</Button>
</div>
</div>
</div>
</div>
</header>
{/* 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>
))}
{/* Spacer to prevent content from hiding under fixed header */}
<div className={clsx(
"transition-all duration-300",
isScrolled ? "h-[72px] lg:h-[80px]" : "h-[80px] lg:h-[96px]"
)} aria-hidden="true" />
{/* 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">
{t('common:header.language')}
</div>
<CompactLanguageSelector className="w-full" />
</div>
{/* Mobile menu drawer */}
{isMobileMenuOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 md:hidden animate-in fade-in duration-200"
onClick={() => setIsMobileMenuOpen(false)}
aria-hidden="true"
/>
{/* Drawer */}
<div
id="mobile-menu"
className={clsx(
"fixed top-0 right-0 bottom-0 w-[85%] max-w-sm bg-[var(--bg-primary)] shadow-2xl z-50 md:hidden",
"animate-in slide-in-from-right duration-300",
"flex flex-col"
)}
role="dialog"
aria-modal="true"
aria-label={t('common:header.mobile_menu', 'Menú de navegación móvil')}
>
{/* Drawer header */}
<div className="flex items-center justify-between p-4 border-b border-[var(--border-primary)]">
<div className="flex items-center gap-2">
<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>
<h2 className="text-lg font-bold text-[var(--text-primary)]">
Panadería IA
</h2>
</div>
<Button
variant="ghost"
size="sm"
className="p-2 min-h-[44px] min-w-[44px]"
onClick={() => setIsMobileMenuOpen(false)}
aria-label={t('common:header.close_menu', 'Cerrar menú')}
>
<X className="w-6 h-6" />
</Button>
</div>
{/* Mobile auth buttons */}
{/* Drawer content */}
<div className="flex-1 overflow-y-auto">
{/* Navigation items */}
<nav className="py-4" role="navigation">
<div className="flex flex-col">
{navItems.map((item) => (
<div key={item.id}>
{renderNavLink(item, true)}
</div>
))}
</div>
</nav>
{/* Mobile language selector */}
{showLanguageSelector && (
<div className="px-4 py-4 border-t border-[var(--border-primary)]">
<div className="text-sm font-medium text-[var(--text-secondary)] mb-3">
{t('common:header.language', 'Idioma')}
</div>
<CompactLanguageSelector className="w-full" />
</div>
)}
{/* Theme toggle for mobile */}
{showThemeToggle && (
<div className="px-4 py-4 border-t border-[var(--border-primary)]">
<div className="text-sm font-medium text-[var(--text-secondary)] mb-3">
{t('common:header.theme', 'Tema')}
</div>
<ThemeToggle variant="button" size="md" className="w-full" />
</div>
)}
</div>
{/* Drawer footer with auth buttons */}
{showAuthButtons && (
<div className="flex flex-col gap-3 pt-4 sm:hidden">
<Link to={getLoginUrl()}>
<Button
variant="ghost"
size="md"
className="w-full font-medium border border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]"
>
{t('common:header.login')}
</Button>
</Link>
<Link to={getRegisterUrl()}>
<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"
>
{t('common:header.start_free')}
</Button>
</Link>
<div className="p-4 border-t border-[var(--border-primary)] bg-[var(--bg-secondary)]">
<div className="flex flex-col gap-3">
<Link to={getLoginUrl()} onClick={() => setIsMobileMenuOpen(false)}>
<Button
variant="outline"
size="lg"
className="w-full font-medium min-h-[48px]"
>
{t('common:header.login', 'Iniciar Sesión')}
</Button>
</Link>
<Link to={getRegisterUrl()} onClick={() => setIsMobileMenuOpen(false)}>
<Button
size="lg"
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 min-h-[48px]"
>
{t('common:header.start_free', 'Comenzar Gratis')}
</Button>
</Link>
</div>
</div>
)}
</div>
</nav>
</div>
</header>
</>
)}
</>
);
});