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: [ links: [
{ id: 'about', label: t('common:footer.links.about', 'Acerca de'), href: '/about' }, { id: 'about', label: t('common:footer.links.about', 'Acerca de'), href: '/about' },
{ id: 'blog', label: t('common:footer.links.blog', 'Blog'), href: '/blog' }, { 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';

View File

@@ -371,7 +371,7 @@
"feedback": "Feedback", "feedback": "Feedback",
"about": "Acerca de", "about": "Acerca de",
"blog": "Blog", "blog": "Blog",
"careers": "Carreras", "careers": "Empleo",
"press": "Prensa", "press": "Prensa",
"privacy": "Privacidad", "privacy": "Privacidad",
"terms": "Términos", "terms": "Términos",

View File

@@ -48,7 +48,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
const FeaturesPage: React.FC = () => { const FeaturesPage: React.FC = () => {
const { t } = useTranslation('features'); const { t } = useTranslation(['features', 'about']);
const navigate = useNavigate(); const navigate = useNavigate();
// Automatic System Timeline Steps // Automatic System Timeline Steps
@@ -913,57 +913,30 @@ const FeaturesPage: React.FC = () => {
</div> </div>
</section> </section>
{/* Final CTA */} {/* Final CTA - Replicated from AboutPage */}
<section className="relative overflow-hidden py-24 bg-[var(--bg-secondary)] border-y border-[var(--border-primary)]"> <section className="py-20 bg-gradient-to-r from-[var(--color-primary)] to-orange-600">
{/* Background Pattern */} <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="absolute inset-0 bg-pattern opacity-30"></div> <h2 className="text-3xl lg:text-4xl font-bold text-white mb-6">
{t('about:cta.title')}
<div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> </h2>
<ScrollReveal variant="fadeUp"> <p className="text-xl text-white/90 mb-8 leading-relaxed">
<div className="text-center space-y-6"> {t('about:cta.subtitle')}
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--color-primary)]/10 dark:bg-[var(--color-primary)]/20 border border-[var(--color-primary)]/20 dark:border-[var(--color-primary)]/30 mb-4"> </p>
<Sparkles className="w-5 h-5 text-[var(--color-primary)]" /> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<span className="text-sm font-medium text-[var(--color-primary)]">{t('cta.badge', 'Prueba Gratuita Disponible')}</span> <Link
</div> to="/register"
className="inline-flex items-center justify-center gap-2 px-8 py-4 bg-white text-[var(--color-primary)] rounded-xl font-bold hover:shadow-2xl transition-all hover:scale-105"
<h2 className="text-4xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6 leading-tight"> >
{t('cta.title', 'Ver Bakery-IA en Acción')} <span>{t('about:cta.primary')}</span>
</h2> <ArrowRight className="w-5 h-5" />
</Link>
<p className="text-xl lg:text-2xl text-[var(--text-secondary)] mb-8 max-w-2xl mx-auto leading-relaxed"> <Link
{t('cta.subtitle', 'Solicita una demo personalizada para tu panadería')} to="/demo"
</p> className="inline-flex items-center justify-center gap-2 px-8 py-4 border-2 border-white text-white rounded-xl font-bold hover:bg-white hover:text-[var(--color-primary)] transition-all"
>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4"> <span>{t('about:cta.secondary')}</span>
<Link to={getDemoUrl()} className="w-full sm:w-auto"> </Link>
<Button </div>
size="xl"
variant="primary"
className="w-full sm:w-auto px-12 py-6 text-xl font-bold shadow-2xl hover:shadow-orange-500/20 transform hover:scale-105 transition-all group"
>
<Sparkles className="mr-2 w-6 h-6" />
{t('cta.button', 'Solicitar Demo')}
<ArrowRight className="ml-3 w-6 h-6 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
<div className="flex items-center justify-center gap-8 pt-8 text-sm text-[var(--text-tertiary)]">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)]" />
<span>{t('cta.feature1', 'Sin compromiso')}</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)]" />
<span>{t('cta.feature2', 'Configuración en minutos')}</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)]" />
<span>{t('cta.feature3', 'Soporte dedicado')}</span>
</div>
</div>
</div>
</ScrollReveal>
</div> </div>
</section> </section>
</PublicLayout> </PublicLayout>

View File

@@ -26,7 +26,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
const LandingPage: React.FC = () => { const LandingPage: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation(['landing', 'about']);
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@@ -671,33 +671,30 @@ const LandingPage: React.FC = () => {
</div> </div>
</section> </section>
{/* Final CTA */} {/* Final CTA - Replicated from AboutPage */}
<section className="py-24 bg-[var(--bg-secondary)] border-y border-[var(--border-primary)]"> <section className="py-20 bg-gradient-to-r from-[var(--color-primary)] to-orange-600">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<ScrollReveal variant="fadeUp"> <h2 className="text-3xl lg:text-4xl font-bold text-white mb-6">
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6"> {t('about:cta.title')}
{t('landing:final_cta.title', 'Deja de Perder €2,000 al Mes en Desperdicios')} </h2>
</h2> <p className="text-xl text-white/90 mb-8 leading-relaxed">
<p className="text-xl text-[var(--text-secondary)] mb-10 max-w-2xl mx-auto"> {t('about:cta.subtitle')}
{t('landing:final_cta.subtitle', 'Únete a las primeras 20 panaderías. Solo quedan 12 plazas.')} </p>
</p> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> <Link
<Link to={getRegisterUrl()} className="w-full sm:w-auto"> to="/register"
<Button className="inline-flex items-center justify-center gap-2 px-8 py-4 bg-white text-[var(--color-primary)] rounded-xl font-bold hover:shadow-2xl transition-all hover:scale-105"
size="xl" >
variant="primary" <span>{t('about:cta.primary')}</span>
className="w-full sm:w-auto px-12 py-6 text-xl font-bold shadow-2xl hover:shadow-orange-500/20 transform hover:scale-105 transition-all" <ArrowRight className="w-5 h-5" />
> </Link>
{t('landing:final_cta.button', 'Comenzar Ahora')} <Link
<ArrowRight className="ml-3 w-6 h-6" /> to="/demo"
</Button> className="inline-flex items-center justify-center gap-2 px-8 py-4 border-2 border-white text-white rounded-xl font-bold hover:bg-white hover:text-[var(--color-primary)] transition-all"
</Link> >
</div> <span>{t('about:cta.secondary')}</span>
<p className="mt-8 text-[var(--text-tertiary)] text-sm font-medium"> </Link>
<CheckCircle2 className="inline-block w-4 h-4 mr-2 text-green-500" /> </div>
{t('landing:final_cta.guarantee', 'Tarjeta requerida. Sin cargo por 3 meses. Cancela cuando quieras.')}
</p>
</ScrollReveal>
</div> </div>
</section> </section>
</PublicLayout> </PublicLayout>