Improve public pages

This commit is contained in:
Urtzi Alfaro
2026-01-05 19:51:28 +01:00
parent 18627f02d4
commit 6c6be6f5a5
5 changed files with 51 additions and 562 deletions

View File

@@ -196,7 +196,7 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
links: [
{ id: 'about', label: t('common:footer.links.about', 'Acerca de'), href: '/about' },
{ id: 'blog', label: t('common:footer.links.blog', 'Blog'), href: '/blog' },
{ id: 'careers', label: t('common:footer.links.careers', 'Carreras'), href: '/careers' },
{ id: 'careers', label: t('common:footer.links.careers', 'Empleo'), href: '/careers' },
],
},
];

View File

@@ -1,481 +0,0 @@
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { Link } from 'react-router-dom';
import { Button } from '../../ui';
import {
Heart,
ExternalLink,
Mail,
Phone,
MapPin,
Github,
Twitter,
Linkedin,
Globe,
Shield,
FileText,
HelpCircle,
MessageSquare
} from 'lucide-react';
export interface FooterLink {
id: string;
label: string;
href: string;
external?: boolean;
icon?: React.ComponentType<{ className?: string }>;
}
export interface FooterSection {
id: string;
title: string;
links: FooterLink[];
}
export interface SocialLink {
id: string;
label: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
}
export interface CompanyInfo {
name: string;
description?: string;
logo?: React.ReactNode;
email?: string;
phone?: string;
address?: string;
website?: string;
}
export interface FooterProps {
className?: string;
/**
* Company information
*/
companyInfo?: CompanyInfo;
/**
* Footer sections with links
*/
sections?: FooterSection[];
/**
* Social media links
*/
socialLinks?: SocialLink[];
/**
* Copyright text (auto-generated if not provided)
*/
copyrightText?: string;
/**
* Show version info
*/
showVersion?: boolean;
/**
* Version string
*/
version?: string;
/**
* Show language selector
*/
showLanguageSelector?: boolean;
/**
* Available languages
*/
languages?: Array<{ code: string; name: string }>;
/**
* Current language
*/
currentLanguage?: string;
/**
* Language change handler
*/
onLanguageChange?: (language: string) => void;
/**
* Show theme toggle
*/
showThemeToggle?: boolean;
/**
* Theme toggle handler
*/
onThemeToggle?: () => void;
/**
* Show privacy links
*/
showPrivacyLinks?: boolean;
/**
* Compact mode (smaller footer)
*/
compact?: boolean;
/**
* Custom content
*/
children?: React.ReactNode;
}
export interface FooterRef {
scrollIntoView: () => void;
}
/**
* Footer - Application footer with links and copyright info
*
* Features:
* - Company information and branding
* - Organized link sections for easy navigation
* - Social media links with proper icons
* - Copyright notice with automatic year
* - Version information display
* - Language selector for i18n
* - Privacy and legal links
* - Responsive design with mobile adaptations
* - Accessible link handling with external indicators
* - Customizable sections and content
*/
export const Footer = forwardRef<FooterRef, FooterProps>(({
className,
companyInfo,
sections,
socialLinks,
copyrightText,
showVersion = true,
version = '2.0.0',
showLanguageSelector = false,
languages = [],
currentLanguage = 'es',
onLanguageChange,
showThemeToggle = false,
onThemeToggle,
showPrivacyLinks = true,
compact = false,
children,
}, ref) => {
const footerRef = React.useRef<HTMLDivElement>(null);
const currentYear = new Date().getFullYear();
// Default company info
const defaultCompanyInfo: CompanyInfo = {
name: 'Panadería IA',
description: 'Sistema inteligente de gestión para panaderías. Optimiza tu producción, inventario y ventas con inteligencia artificial.',
email: 'contacto@panaderia-ia.com',
website: 'https://panaderia-ia.com',
};
const company = companyInfo || defaultCompanyInfo;
// Default sections
const defaultSections: FooterSection[] = [
{
id: 'product',
title: 'Producto',
links: [
{ id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
{ id: 'inventory', label: 'Inventario', href: '/inventory' },
{ id: 'production', label: 'Producción', href: '/production' },
{ id: 'sales', label: 'Ventas', href: '/sales' },
{ id: 'forecasting', label: 'Predicciones', href: '/forecasting' },
],
},
{
id: 'support',
title: 'Soporte',
links: [
{ id: 'help', label: 'Centro de Ayuda', href: '/help', icon: HelpCircle },
{ id: 'docs', label: 'Documentación', href: '/help/docs', icon: FileText },
{ id: 'contact', label: 'Contacto', href: '/help/support', icon: MessageSquare },
{ id: 'feedback', label: 'Feedback', href: '/help/feedback' },
],
},
{
id: 'company',
title: 'Empresa',
links: [
{ id: 'about', label: 'Acerca de', href: '/about', external: true },
{ id: 'blog', label: 'Blog', href: 'https://blog.panaderia-ia.com', external: true },
{ id: 'careers', label: 'Carreras', href: 'https://careers.panaderia-ia.com', external: true },
{ id: 'press', label: 'Prensa', href: '/press', external: true },
],
},
];
const footerSections = sections || defaultSections;
// Default social links
const defaultSocialLinks: SocialLink[] = [
{
id: 'github',
label: 'GitHub',
href: 'https://github.com/panaderia-ia',
icon: Github,
},
{
id: 'twitter',
label: 'Twitter',
href: 'https://twitter.com/panaderia_ia',
icon: Twitter,
},
{
id: 'linkedin',
label: 'LinkedIn',
href: 'https://linkedin.com/company/panaderia-ia',
icon: Linkedin,
},
];
const socialLinksToShow = socialLinks || defaultSocialLinks;
// Privacy links
const privacyLinks: FooterLink[] = [
{ id: 'privacy', label: 'Privacidad', href: '/privacy', icon: Shield },
{ id: 'terms', label: 'Términos', href: '/terms', icon: FileText },
{ id: 'cookies', label: 'Cookies', href: '/cookies' },
];
// Scroll into view
const scrollIntoView = React.useCallback(() => {
footerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, []);
// Expose ref methods
React.useImperativeHandle(ref, () => ({
scrollIntoView,
}), [scrollIntoView]);
// Render link
const renderLink = (link: FooterLink) => {
const LinkIcon = link.icon;
const linkContent = (
<span className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors duration-200">
{LinkIcon && <LinkIcon className="w-4 h-4" />}
{link.label}
{link.external && <ExternalLink className="w-3 h-3" />}
</span>
);
if (link.external) {
return (
<a
key={link.id}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="hover:underline focus:outline-none focus:underline"
>
{linkContent}
</a>
);
}
return (
<Link
key={link.id}
to={link.href}
className="hover:underline focus:outline-none focus:underline"
>
{linkContent}
</Link>
);
};
// Render social link
const renderSocialLink = (social: SocialLink) => {
const SocialIcon = social.icon;
return (
<a
key={social.id}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] transition-colors duration-200"
aria-label={social.label}
>
<SocialIcon className="w-5 h-5" />
</a>
);
};
return (
<footer
ref={footerRef}
className={clsx(
'bg-[var(--bg-secondary)] border-t border-[var(--border-primary)]',
'mt-auto', // Push to bottom when using flex layout
compact ? 'py-6' : 'py-12',
className
)}
role="contentinfo"
>
<div className="max-w-7xl mx-auto px-4 lg:px-6">
{!compact && (
<>
{/* Main footer content */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8">
{/* Company info */}
<div className="lg:col-span-2">
<div className="flex items-center gap-3 mb-4">
{company.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>
)}
<span className="text-lg font-semibold text-[var(--text-primary)]">
{company.name}
</span>
</div>
{company.description && (
<p className="text-sm text-[var(--text-secondary)] mb-4 max-w-md">
{company.description}
</p>
)}
{/* Contact info */}
<div className="space-y-2">
{company.email && (
<a
href={`mailto:${company.email}`}
className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors duration-200"
>
<Mail className="w-4 h-4" />
{company.email}
</a>
)}
{company.phone && (
<a
href={`tel:${company.phone}`}
className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors duration-200"
>
<Phone className="w-4 h-4" />
{company.phone}
</a>
)}
{company.address && (
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
<MapPin className="w-4 h-4" />
{company.address}
</div>
)}
{company.website && (
<a
href={company.website}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors duration-200"
>
<Globe className="w-4 h-4" />
{company.website}
<ExternalLink className="w-3 h-3" />
</a>
)}
</div>
</div>
{/* Footer sections */}
{footerSections.map(section => (
<div key={section.id}>
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-4">
{section.title}
</h3>
<ul className="space-y-3">
{section.links.map(link => (
<li key={link.id}>
{renderLink(link)}
</li>
))}
</ul>
</div>
))}
</div>
{/* Social links */}
{socialLinksToShow.length > 0 && (
<div className="flex items-center justify-center lg:justify-start gap-2 mb-8">
{socialLinksToShow.map(renderSocialLink)}
</div>
)}
{/* Custom children */}
{children && (
<div className="mb-8">
{children}
</div>
)}
</>
)}
{/* Bottom bar */}
<div className={clsx(
'flex flex-col sm:flex-row items-center justify-between gap-4',
!compact && 'pt-8 border-t border-[var(--border-primary)]'
)}>
{/* Copyright and version */}
<div className="flex flex-col sm:flex-row items-center gap-4 text-sm text-[var(--text-tertiary)]">
<div className="flex items-center gap-1">
<span>© {currentYear}</span>
<span>{company.name}</span>
<Heart className="w-4 h-4 text-red-500 mx-1" />
<span>Hecho en España</span>
</div>
{showVersion && (
<div className="flex items-center gap-1">
<span>v{version}</span>
</div>
)}
</div>
{/* Right side utilities */}
<div className="flex items-center gap-4">
{/* Language selector */}
{showLanguageSelector && languages.length > 0 && (
<select
value={currentLanguage}
onChange={(e) => onLanguageChange?.(e.target.value)}
className="text-sm bg-transparent border-none text-[var(--text-secondary)] hover:text-[var(--text-primary)] cursor-pointer focus:outline-none"
>
{languages.map(lang => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
)}
{/* Theme toggle */}
{showThemeToggle && (
<Button
variant="ghost"
size="sm"
onClick={onThemeToggle}
className="text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
>
Cambiar tema
</Button>
)}
{/* Privacy links */}
{showPrivacyLinks && (
<div className="flex items-center gap-4">
{privacyLinks.map(link => renderLink(link))}
</div>
)}
</div>
</div>
{/* Copyright text override */}
{copyrightText && (
<div className="text-center text-sm text-[var(--text-tertiary)] mt-4 pt-4 border-t border-[var(--border-primary)]">
{copyrightText}
</div>
)}
</div>
</footer>
);
});
Footer.displayName = 'Footer';