'use client'; import { useRef, useState, useEffect } from 'react'; import { Link } from '@/i18n/navigation'; import Image from 'next/image'; import { motion, useInView, useReducedMotion, useScroll, useTransform } from 'framer-motion'; import { Ship, Shield, Zap, BarChart3, Package, Clock, CheckCircle2, ArrowRight, Search, Anchor, Container, FileText, LayoutDashboard, Bell, BookOpen, Users, Check, X, } from 'lucide-react'; import { useTranslations, useLocale } from 'next-intl'; import { useAuth } from '@/lib/context/auth-context'; import { LandingHeader, LandingFooter } from '@/components/layout'; import { DemoVideoModal } from '@/components/DemoVideoModal'; import { RateSearchDemo } from '@/components/landing/RateSearchDemo'; import { EASE, reveal, revealGroup } from '@/components/landing/motion'; /** * Rythme vertical de la page : une seule echelle pour toutes les sections, * afin que la lecture avance a pas constant plutot que par a-coups. */ const SECTION_PADDING = 'py-16 sm:py-24 lg:py-32'; const SECTION_PADDING_TIGHT = 'py-12 sm:py-16'; const CONTAINER = 'mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'; /** Anneau de focus commun aux elements interactifs de la page. */ const FOCUS_RING = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-turquoise focus-visible:ring-offset-2'; /** * En-tete de section : surtitre, titre, sous-titre. * * Les sections repetaient jusqu'ici le meme bloc avec des tailles et des * marges legerement differentes. Le centraliser garantit que tous les titres * de la page partagent la meme hierarchie. */ function SectionHeading({ eyebrow, title, subtitle, id, tone = 'light', animate = true, inView = true, }: { eyebrow?: string; title: string; subtitle?: string; id?: string; tone?: 'light' | 'dark'; animate?: boolean; inView?: boolean; }) { const dark = tone === 'dark'; return ( {eyebrow && ( {eyebrow} )}

{title}

{subtitle && (

{subtitle}

)}
); } function AnimatedCounter({ end, suffix = '', prefix = '', decimals = 0, isActive, duration = 2, }: { end: number; suffix?: string; prefix?: string; decimals?: number; isActive: boolean; duration?: number; }) { const [count, setCount] = useState(0); const prefersReducedMotion = useReducedMotion(); useEffect(() => { if (!isActive) return; // Mouvement reduit : on affiche directement la valeur finale. if (prefersReducedMotion) { setCount(end); return; } let startTime: number | undefined; let frame = 0; const animate = (timestamp: number) => { if (!startTime) startTime = timestamp; const progress = Math.min((timestamp - startTime) / (duration * 1000), 1); const eased = 1 - Math.pow(1 - progress, 3); setCount(eased * end); if (progress < 1) frame = requestAnimationFrame(animate); else setCount(end); }; frame = requestAnimationFrame(animate); // Sans annulation, la boucle survivait au demontage et continuait de // declencher setCount sur un composant disparu. return () => cancelAnimationFrame(frame); }, [end, duration, isActive, prefersReducedMotion]); const display = decimals > 0 ? count.toFixed(decimals) : Math.floor(count).toString(); return ( <> {prefix} {display} {suffix} ); } export default function LandingPage() { const t = useTranslations('landing'); const tCommon = useTranslations('common'); const locale = useLocale(); const { user, isAuthenticated } = useAuth(); const heroRef = useRef(null); const featuresRef = useRef(null); const statsRef = useRef(null); const pricingRef = useRef(null); const testimonialsRef = useRef(null); const ctaRef = useRef(null); const howRef = useRef(null); const isHeroInView = useInView(heroRef, { once: true }); const isFeaturesInView = useInView(featuresRef, { once: true }); const isStatsInView = useInView(statsRef, { once: true, amount: 0.3 }); const isPricingInView = useInView(pricingRef, { once: true }); const isTestimonialsInView = useInView(testimonialsRef, { once: true }); const isCtaInView = useInView(ctaRef, { once: true }); const isHowInView = useInView(howRef, { once: true, amount: 0.2 }); const [billingYearly, setBillingYearly] = useState(false); const [isDemoOpen, setIsDemoOpen] = useState(false); const prefersReducedMotion = useReducedMotion(); const { scrollYProgress } = useScroll(); // Parallaxe neutralisee si l'utilisateur a demande moins de mouvement. const backgroundY = useTransform( scrollYProgress, [0, 1], prefersReducedMotion ? ['0%', '0%'] : ['0%', '50%'] ); // Six modules, un seul traitement. Les degrades arc-en-ciel qui les // distinguaient (violet, orange, indigo...) n'encodaient rien : ils // n'existaient que pour colorer. L'icone suffit a identifier le module. const features = [ { icon: BarChart3, title: t('features.dashboard.title'), description: t('features.dashboard.description'), link: '/dashboard', }, { icon: Package, title: t('features.bookings.title'), description: t('features.bookings.description'), link: '/dashboard/bookings', }, { icon: FileText, title: t('features.documents.title'), description: t('features.documents.description'), link: '/dashboard/documents', }, { icon: Search, title: t('features.tracking.title'), description: t('features.tracking.description'), link: '/dashboard/track-trace', }, { icon: BookOpen, title: t('features.wiki.title'), description: t('features.wiki.description'), link: '/dashboard/wiki', }, { icon: Bell, title: t('features.notifications.title'), description: t('features.notifications.description'), link: '/dashboard', }, ]; const stats = [ { end: 50, prefix: '', suffix: '+', decimals: 0, label: t('stats.carriers'), icon: Ship }, { end: 10, prefix: '', suffix: 'K+', decimals: 0, label: t('stats.ports'), icon: Anchor }, { end: 2, prefix: '<', suffix: 's', decimals: 0, label: t('stats.responseTime'), icon: Zap }, { end: 99.5, prefix: '', suffix: '%', decimals: 1, label: t('stats.availability'), icon: CheckCircle2, }, ]; const planFeaturesByKey: Record> = { bronze: [ { key: 'lclBooking', included: true }, { key: 'tracking', included: true }, { key: 'dashboard', included: false }, { key: 'wiki', included: false }, { key: 'userManagement', included: false }, { key: 'csvExport', included: false }, { key: 'apiAccess', included: false }, { key: 'kam', included: false }, ], silver: [ { key: 'lclBooking', included: true }, { key: 'tracking', included: true }, { key: 'dashboardAdvanced', included: true }, { key: 'wikiFull', included: true }, { key: 'userManagement', included: true }, { key: 'csvExport', included: true }, { key: 'apiAccess', included: false }, { key: 'kam', included: false }, ], gold: [ { key: 'lclBooking', included: true }, { key: 'tracking', included: true }, { key: 'dashboardAdvanced', included: true }, { key: 'wikiFull', included: true }, { key: 'userManagement', included: true }, { key: 'csvExport', included: true }, { key: 'apiFull', included: true }, { key: 'kam', included: false }, ], platinium: [ { key: 'lclBooking', included: true }, { key: 'tracking', included: true }, { key: 'dashboardAdvanced', included: true }, { key: 'wikiFull', included: true }, { key: 'userManagement', included: true }, { key: 'csvExport', included: true }, { key: 'apiFull', included: true }, { key: 'kamCustom', included: true }, ], }; const pricingPlans = [ { key: 'bronze', badge: null, monthlyPrice: 0, yearlyPrice: 0, yearlyMonthly: 0, bookingFee: 15, ctaLink: '/register', highlighted: false, accentColor: 'from-amber-600 to-yellow-500', textAccent: 'text-amber-700', badgeBg: 'bg-amber-100 text-amber-800', }, { key: 'silver', badge: 'popular', monthlyPrice: 299, yearlyPrice: 3289, yearlyMonthly: 274, bookingFee: 10, ctaLink: '/register', highlighted: true, accentColor: 'from-slate-400 to-slate-500', textAccent: 'text-slate-600', badgeBg: 'bg-slate-100 text-slate-700', }, { key: 'gold', badge: null, monthlyPrice: 799, yearlyPrice: 8789, yearlyMonthly: 732, bookingFee: 5, ctaLink: '/register', highlighted: false, accentColor: 'from-yellow-400 to-amber-400', textAccent: 'text-amber-600', badgeBg: 'bg-yellow-50 text-amber-700', }, { key: 'platinium', badge: 'custom', monthlyPrice: null, yearlyPrice: null, yearlyMonthly: null, bookingFee: -1, ctaLink: '/contact', highlighted: false, accentColor: 'from-brand-navy to-brand-turquoise', textAccent: 'text-brand-turquoise', badgeBg: 'bg-brand-navy/10 text-brand-navy', }, ] as const; const testimonials = (t.raw('testimonials.items') as Array<{ quote: string; author: string; role: string; company: string; }>) ?? []; // Grammaire commune : meme courbe, meme distance, meme cascade partout. const containerVariants = revealGroup; const itemVariants = reveal; const numberFormat = new Intl.NumberFormat(locale === 'fr' ? 'fr-FR' : 'en-US'); return (
{/* Hero Section */}
{/* Stats Section */}
{/* Separateurs plutot que grands ecarts : les quatre chiffres se lisent comme un seul bandeau et non comme quatre blocs isoles. */}
{stats.map((stat, index) => { const IconComponent = stat.icon; return (
{/* `dt` precede `dd` dans le source (contrainte de
) ; l'inversion visuelle est faite par flex-col-reverse. */}
{stat.label}
); })}
{/* Features Section */}
{features.map((feature, index) => { const IconComponent = feature.icon; return (

{feature.title}

{feature.description}

{tCommon('discover')}
); })}
{/* Partner Logos Section */}
{/* Bandeau de preuve, pas un chapitre : titre discret pour ne pas concurrencer les vrais titres de section. */}

{t('partners.title')}

{t('partners.subtitle')}

{[ 'ECU Line 2.png', 'ICL 1.png', 'NVO Consolidation 1.png', 'TCC LOG 1.png', 'VANGUARD 1.png', 'image 1.png', ].map((logo, index) => ( {`Partner ))}
{/* Pricing Section */}
{t('pricing.monthly')} {t('pricing.yearly')} {/* Place reservee en permanence : sans cela, l'apparition du badge decalait toute la ligne au moment du basculement. */} {t('pricing.yearlySaving')} {pricingPlans.map(plan => { const planName = t(`pricing.plans.${plan.key}.name` as any); const planDescription = t(`pricing.plans.${plan.key}.description` as any); const planUsers = t(`pricing.plans.${plan.key}.users` as any); const planShipments = t(`pricing.plans.${plan.key}.shipments` as any); const planBookingFee = plan.bookingFee === -1 ? t('pricing.bookingFeeCustom') : t('pricing.bookingFee', { price: `${plan.bookingFee}€` }); const planCta = t(`pricing.plans.${plan.key}.cta` as any); const planFeatures = planFeaturesByKey[plan.key] ?? []; return (
{/* How It Works Section */}
{/* Testimonials Section */}
{testimonials.map((testimonial, index) => (
{[...Array(5)].map((_, i) => ( ))}
“{testimonial.quote}”
{testimonial.author}
{testimonial.role} — {testimonial.company}
))}
{/* CTA Section */}

{t('cta.title')}

{t('cta.subtitle')}

{isAuthenticated && user ? ( {t('cta.ctaAuthenticated')} ) : ( <> {t('cta.ctaRegister')} {t('cta.ctaLogin')} )} {[ { icon: CheckCircle2, label: t('cta.features.noCard') }, { icon: Clock, label: t('cta.features.quickSetup') }, { icon: Shield, label: t('cta.features.secure') }, ].map(({ icon: Icon, label }) => (
  • ))}
    setIsDemoOpen(false)} />
    ); }