1078 lines
43 KiB
TypeScript
1078 lines
43 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useState, useEffect } from 'react';
|
|
import { Link } from '@/i18n/navigation';
|
|
import Image from 'next/image';
|
|
import { motion, useInView, 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';
|
|
|
|
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);
|
|
|
|
useEffect(() => {
|
|
if (!isActive) return;
|
|
let startTime: number | undefined;
|
|
|
|
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) requestAnimationFrame(animate);
|
|
else setCount(end);
|
|
};
|
|
|
|
requestAnimationFrame(animate);
|
|
}, [end, duration, isActive]);
|
|
|
|
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 { scrollYProgress } = useScroll();
|
|
const backgroundY = useTransform(scrollYProgress, [0, 1], ['0%', '50%']);
|
|
|
|
const features = [
|
|
{
|
|
icon: BarChart3,
|
|
title: t('features.dashboard.title'),
|
|
description: t('features.dashboard.description'),
|
|
color: 'from-blue-500 to-cyan-500',
|
|
link: '/dashboard',
|
|
},
|
|
{
|
|
icon: Package,
|
|
title: t('features.bookings.title'),
|
|
description: t('features.bookings.description'),
|
|
color: 'from-purple-500 to-pink-500',
|
|
link: '/dashboard/bookings',
|
|
},
|
|
{
|
|
icon: FileText,
|
|
title: t('features.documents.title'),
|
|
description: t('features.documents.description'),
|
|
color: 'from-orange-500 to-red-500',
|
|
link: '/dashboard/documents',
|
|
},
|
|
{
|
|
icon: Search,
|
|
title: t('features.tracking.title'),
|
|
description: t('features.tracking.description'),
|
|
color: 'from-green-500 to-emerald-500',
|
|
link: '/dashboard/track-trace',
|
|
},
|
|
{
|
|
icon: BookOpen,
|
|
title: t('features.wiki.title'),
|
|
description: t('features.wiki.description'),
|
|
color: 'from-yellow-500 to-orange-500',
|
|
link: '/dashboard/wiki',
|
|
},
|
|
{
|
|
icon: Bell,
|
|
title: t('features.notifications.title'),
|
|
description: t('features.notifications.description'),
|
|
color: 'from-indigo-500 to-purple-500',
|
|
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<string, Array<{ key: string; included: boolean }>> = {
|
|
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,
|
|
commission: '5%',
|
|
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: 249,
|
|
yearlyPrice: 2739,
|
|
yearlyMonthly: 228,
|
|
commission: '3%',
|
|
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: 899,
|
|
yearlyPrice: 9889,
|
|
yearlyMonthly: 824,
|
|
commission: '2%',
|
|
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,
|
|
commission: '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;
|
|
}>) ?? [];
|
|
|
|
const containerVariants = {
|
|
hidden: { opacity: 0, y: 50 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.6,
|
|
staggerChildren: 0.1,
|
|
},
|
|
},
|
|
};
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, y: 20 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: { duration: 0.5 },
|
|
},
|
|
};
|
|
|
|
const numberFormat = new Intl.NumberFormat(locale === 'fr' ? 'fr-FR' : 'en-US');
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
<LandingHeader transparentOnTop={true} />
|
|
|
|
{/* Hero Section */}
|
|
<section ref={heroRef} className="relative min-h-screen flex items-center overflow-hidden">
|
|
<motion.div style={{ y: backgroundY }} className="absolute inset-0 z-0">
|
|
<video
|
|
autoPlay
|
|
loop
|
|
muted
|
|
playsInline
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
>
|
|
<source src="https://assets.mixkit.co/videos/36264/36264-720.mp4" type="video/mp4" />
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
backgroundImage: 'url(/assets/images/background-section-1-landingpage.png)',
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
}}
|
|
/>
|
|
</video>
|
|
<div className="absolute inset-0 bg-brand-navy/65" />
|
|
<div className="absolute inset-0 bg-gradient-to-br from-brand-navy/70 via-brand-navy/50 to-brand-turquoise/20" />
|
|
</motion.div>
|
|
|
|
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8 }}
|
|
className="text-center"
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.8, opacity: 0 }}
|
|
animate={isHeroInView ? { scale: 1, opacity: 1 } : {}}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
className="inline-flex items-center space-x-2 bg-white/10 backdrop-blur-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full mb-6 sm:mb-8 border border-white/20"
|
|
>
|
|
<Ship className="w-4 h-4 sm:w-5 sm:h-5 text-brand-turquoise flex-shrink-0" />
|
|
<span className="text-white/90 text-xs sm:text-sm font-medium">
|
|
{t('hero.badge')}
|
|
</span>
|
|
</motion.div>
|
|
|
|
<motion.h1
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8, delay: 0.3 }}
|
|
className="text-3xl sm:text-5xl lg:text-7xl font-bold text-white mb-4 sm:mb-6 leading-tight"
|
|
>
|
|
{t('hero.titleLine1')}
|
|
<br />
|
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand-turquoise to-brand-green">
|
|
{t('hero.titleLine2')}
|
|
</span>
|
|
</motion.h1>
|
|
|
|
<motion.p
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8, delay: 0.4 }}
|
|
className="text-base sm:text-xl lg:text-2xl text-white/80 mb-8 sm:mb-12 max-w-3xl mx-auto leading-relaxed px-2"
|
|
>
|
|
{t('hero.subtitle')}
|
|
</motion.p>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8, delay: 0.5 }}
|
|
className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-6 mb-12 px-4 sm:px-0"
|
|
>
|
|
{isAuthenticated && user ? (
|
|
<Link
|
|
href="/dashboard"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
|
|
>
|
|
<span>{t('hero.ctaAuthenticated')}</span>
|
|
<LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<Link
|
|
href="/register"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
|
|
>
|
|
<span>{t('hero.ctaRegister')}</span>
|
|
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
</Link>
|
|
<Link
|
|
href="/login"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-6 sm:px-8 py-3 sm:py-4 bg-white text-brand-navy rounded-lg hover:bg-gray-50 transition-all hover:shadow-xl font-semibold text-base sm:text-lg w-full sm:w-auto text-center"
|
|
>
|
|
{t('hero.ctaDemo')}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<div className="absolute bottom-0 left-0 right-0">
|
|
<svg className="w-full h-24 lg:h-32" viewBox="0 0 1440 120" preserveAspectRatio="none">
|
|
<motion.path
|
|
initial={{ pathLength: 0 }}
|
|
animate={{ pathLength: 1 }}
|
|
transition={{ duration: 2, ease: 'easeInOut' }}
|
|
d="M0,60 C240,90 480,30 720,60 C960,90 1200,30 1440,60 L1440,120 L0,120 Z"
|
|
fill="white"
|
|
opacity="0.8"
|
|
/>
|
|
<motion.path
|
|
initial={{ pathLength: 0 }}
|
|
animate={{ pathLength: 1 }}
|
|
transition={{ duration: 2.5, ease: 'easeInOut' }}
|
|
d="M0,80 C240,50 480,110 720,80 C960,50 1200,110 1440,80 L1440,120 L0,120 Z"
|
|
fill="white"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Stats Section */}
|
|
<section ref={statsRef} className="py-10 sm:py-16 bg-gray-50">
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate={isStatsInView ? 'visible' : 'hidden'}
|
|
className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
|
|
>
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
|
{stats.map((stat, index) => {
|
|
const IconComponent = stat.icon;
|
|
return (
|
|
<motion.div
|
|
key={index}
|
|
variants={itemVariants}
|
|
className="text-center group cursor-pointer"
|
|
>
|
|
<div className="flex justify-center mb-3">
|
|
<div className="p-3 bg-brand-turquoise/10 rounded-full group-hover:bg-brand-turquoise/20 transition-colors">
|
|
<IconComponent className="w-8 h-8 text-brand-turquoise" />
|
|
</div>
|
|
</div>
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={isStatsInView ? { scale: 1 } : {}}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
className="text-3xl sm:text-5xl lg:text-6xl font-bold text-brand-navy mb-2 tabular-nums"
|
|
>
|
|
<AnimatedCounter
|
|
end={stat.end}
|
|
prefix={stat.prefix}
|
|
suffix={stat.suffix}
|
|
decimals={stat.decimals}
|
|
isActive={isStatsInView}
|
|
duration={2.2}
|
|
/>
|
|
</motion.div>
|
|
<div className="text-gray-600 font-medium">{stat.label}</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</motion.div>
|
|
</section>
|
|
|
|
{/* Features Section */}
|
|
<section ref={featuresRef} id="features" className="py-12 sm:py-20 lg:py-32">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={isFeaturesInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8 }}
|
|
className="text-center mb-10 sm:mb-16"
|
|
>
|
|
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4">
|
|
{t('features.title')}
|
|
</h2>
|
|
<p className="text-base sm:text-xl text-gray-600 max-w-2xl mx-auto">
|
|
{t('features.subtitle')}
|
|
</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate={isFeaturesInView ? 'visible' : 'hidden'}
|
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
|
>
|
|
{features.map((feature, index) => {
|
|
const IconComponent = feature.icon;
|
|
return (
|
|
<motion.div
|
|
key={index}
|
|
variants={itemVariants}
|
|
whileHover={{ y: -8 }}
|
|
className="h-full"
|
|
>
|
|
<Link
|
|
href={feature.link}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="group flex flex-col h-full bg-white p-8 rounded-2xl shadow-lg hover:shadow-2xl transition-all border border-gray-100 hover:border-brand-turquoise/30 relative overflow-hidden"
|
|
>
|
|
<div
|
|
className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${feature.color} opacity-40 group-hover:opacity-100 transition-opacity duration-300`}
|
|
/>
|
|
<div
|
|
className={`w-14 h-14 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center mb-5 group-hover:scale-110 transition-transform flex-shrink-0`}
|
|
>
|
|
<IconComponent className="w-7 h-7 text-white" />
|
|
</div>
|
|
<h3 className="text-xl font-bold text-brand-navy mb-3 group-hover:text-brand-turquoise transition-colors">
|
|
{feature.title}
|
|
</h3>
|
|
<p className="text-gray-600 leading-relaxed flex-1">{feature.description}</p>
|
|
<div className="mt-5 pt-4 border-t border-gray-100 flex items-center justify-between">
|
|
<span className="text-brand-turquoise font-semibold text-sm">
|
|
{tCommon('discover')}
|
|
</span>
|
|
<div className="w-8 h-8 rounded-full bg-brand-turquoise/10 group-hover:bg-brand-turquoise/20 flex items-center justify-center transition-colors">
|
|
<ArrowRight className="w-4 h-4 text-brand-turquoise group-hover:translate-x-0.5 transition-transform" />
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Partner Logos Section */}
|
|
<section className="py-16 bg-white">
|
|
<div className="max-w-7xl mx-auto px-6 lg:px-8">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6 }}
|
|
className="text-center mb-12"
|
|
>
|
|
<h3 className="text-2xl font-bold text-brand-navy mb-2">{t('partners.title')}</h3>
|
|
<p className="text-gray-600">{t('partners.subtitle')}</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
whileInView={{ opacity: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8, delay: 0.2 }}
|
|
className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-8 items-center"
|
|
>
|
|
{[
|
|
'ECU Line 2.png',
|
|
'ICL 1.png',
|
|
'NVO Consolidation 1.png',
|
|
'TCC LOG 1.png',
|
|
'VANGUARD 1.png',
|
|
'image 1.png',
|
|
].map((logo, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
whileInView={{ opacity: 1, scale: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
whileHover={{ scale: 1.1 }}
|
|
className="flex items-center justify-center p-4 grayscale hover:grayscale-0 transition-all cursor-pointer"
|
|
>
|
|
<Image
|
|
src={`/assets/logos/partner/${logo}`}
|
|
alt={`Partner ${index + 1}`}
|
|
width={120}
|
|
height={60}
|
|
className="object-contain h-12 w-auto"
|
|
/>
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Pricing Section */}
|
|
<section
|
|
ref={pricingRef}
|
|
id="pricing"
|
|
className="py-12 sm:py-20 lg:py-32 bg-gradient-to-b from-white to-gray-50"
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={isPricingInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8 }}
|
|
className="text-center mb-8 sm:mb-12"
|
|
>
|
|
<span className="inline-block bg-brand-turquoise/10 text-brand-turquoise text-sm font-semibold px-4 py-1.5 rounded-full mb-4 uppercase tracking-wide">
|
|
{t('pricing.badge')}
|
|
</span>
|
|
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4">
|
|
{t('pricing.title')}
|
|
</h2>
|
|
<p className="text-base sm:text-xl text-gray-600 max-w-2xl mx-auto">
|
|
{t('pricing.subtitle')}
|
|
</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={isPricingInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
className="flex items-center justify-center gap-4 mb-12"
|
|
>
|
|
<span
|
|
className={`text-sm font-medium ${!billingYearly ? 'text-brand-navy' : 'text-gray-400'}`}
|
|
>
|
|
{t('pricing.monthly')}
|
|
</span>
|
|
<button
|
|
onClick={() => setBillingYearly(v => !v)}
|
|
className={`relative inline-flex h-7 w-14 items-center rounded-full transition-colors focus:outline-none ${
|
|
billingYearly ? 'bg-brand-turquoise' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${
|
|
billingYearly ? 'translate-x-8' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
<span
|
|
className={`text-sm font-medium ${billingYearly ? 'text-brand-navy' : 'text-gray-400'}`}
|
|
>
|
|
{t('pricing.yearly')}
|
|
</span>
|
|
{billingYearly && (
|
|
<span className="bg-brand-green/10 text-brand-green text-xs font-bold px-2.5 py-1 rounded-full">
|
|
{t('pricing.yearlySaving')}
|
|
</span>
|
|
)}
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate={isPricingInView ? 'visible' : 'hidden'}
|
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-stretch"
|
|
>
|
|
{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 planCta = t(`pricing.plans.${plan.key}.cta` as any);
|
|
const planFeatures = planFeaturesByKey[plan.key] ?? [];
|
|
|
|
return (
|
|
<motion.div
|
|
key={plan.key}
|
|
variants={itemVariants}
|
|
whileHover={{ y: -6 }}
|
|
className={`relative flex flex-col rounded-2xl transition-all overflow-hidden ${
|
|
plan.highlighted
|
|
? 'bg-brand-navy shadow-2xl ring-2 ring-brand-turquoise'
|
|
: 'bg-white shadow-lg border border-gray-100 hover:shadow-xl hover:border-brand-turquoise/30'
|
|
}`}
|
|
>
|
|
<div className={`h-1.5 w-full bg-gradient-to-r ${plan.accentColor}`} />
|
|
|
|
{plan.badge === 'popular' && (
|
|
<div className="absolute top-4 right-4">
|
|
<span className="bg-brand-turquoise text-white text-xs font-bold px-2.5 py-1 rounded-full">
|
|
{t('pricing.popularBadge')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{plan.badge === 'custom' && (
|
|
<div className="absolute top-4 right-4">
|
|
<span className="bg-gradient-to-r from-brand-navy to-brand-turquoise text-white text-xs font-bold px-2.5 py-1 rounded-full">
|
|
{t('pricing.customBadge')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col flex-1 p-6">
|
|
<div className="mb-4">
|
|
<div
|
|
className={`inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider px-2.5 py-1 rounded-full mb-3 ${plan.highlighted ? 'bg-white/10 text-white/70' : plan.badgeBg}`}
|
|
>
|
|
<div
|
|
className={`w-1.5 h-1.5 rounded-full bg-gradient-to-r ${plan.accentColor}`}
|
|
/>
|
|
{planName}
|
|
</div>
|
|
<h3
|
|
className={`text-xl font-bold mb-1 ${plan.highlighted ? 'text-white' : 'text-brand-navy'}`}
|
|
>
|
|
{planDescription}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
{plan.monthlyPrice === null ? (
|
|
<div>
|
|
<span
|
|
className={`text-3xl font-bold ${plan.highlighted ? 'text-white' : 'text-brand-navy'}`}
|
|
>
|
|
{t('pricing.custom')}
|
|
</span>
|
|
<p
|
|
className={`text-sm mt-1 ${plan.highlighted ? 'text-white/60' : 'text-gray-500'}`}
|
|
>
|
|
{t('pricing.customSubtitle')}
|
|
</p>
|
|
</div>
|
|
) : plan.monthlyPrice === 0 ? (
|
|
<div>
|
|
<span
|
|
className={`text-4xl font-bold ${plan.highlighted ? 'text-white' : 'text-brand-navy'}`}
|
|
>
|
|
{t('pricing.free')}
|
|
</span>
|
|
<p
|
|
className={`text-sm mt-1 ${plan.highlighted ? 'text-white/60' : 'text-gray-500'}`}
|
|
>
|
|
{t('pricing.freeSubtitle')}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div className="flex items-end gap-1">
|
|
<span
|
|
className={`text-4xl font-bold ${plan.highlighted ? 'text-white' : 'text-brand-navy'}`}
|
|
>
|
|
{billingYearly ? plan.yearlyMonthly : plan.monthlyPrice}€
|
|
</span>
|
|
<span
|
|
className={`text-sm pb-1.5 ${plan.highlighted ? 'text-white/60' : 'text-gray-500'}`}
|
|
>
|
|
{t('pricing.perMonth')}
|
|
</span>
|
|
</div>
|
|
{billingYearly ? (
|
|
<p
|
|
className={`text-xs mt-1 ${plan.highlighted ? 'text-white/60' : 'text-gray-500'}`}
|
|
>
|
|
{t('pricing.billedYearly', {
|
|
price: numberFormat.format(plan.yearlyPrice ?? 0),
|
|
})}
|
|
</p>
|
|
) : (
|
|
<p className="text-xs mt-1 text-brand-turquoise">
|
|
{t('pricing.saveWithYearly')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className={`rounded-xl p-3 mb-5 space-y-2 ${plan.highlighted ? 'bg-white/10' : 'bg-gray-50'}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Users className="w-3.5 h-3.5 flex-shrink-0 text-brand-turquoise" />
|
|
<span
|
|
className={`text-xs font-medium ${plan.highlighted ? 'text-white/80' : 'text-gray-700'}`}
|
|
>
|
|
{planUsers}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Ship className="w-3.5 h-3.5 flex-shrink-0 text-brand-turquoise" />
|
|
<span
|
|
className={`text-xs font-medium ${plan.highlighted ? 'text-white/80' : 'text-gray-700'}`}
|
|
>
|
|
{planShipments}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<BarChart3 className="w-3.5 h-3.5 flex-shrink-0 text-brand-turquoise" />
|
|
<span
|
|
className={`text-xs font-medium ${plan.highlighted ? 'text-white/80' : 'text-gray-700'}`}
|
|
>
|
|
{t('pricing.commission', { rate: plan.commission })}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<ul className="space-y-2.5 mb-6 flex-1">
|
|
{planFeatures.map((feature, featureIndex) => (
|
|
<li key={featureIndex} className="flex items-start gap-2.5">
|
|
{feature.included ? (
|
|
<Check
|
|
className={`w-4 h-4 flex-shrink-0 mt-0.5 ${plan.highlighted ? 'text-brand-turquoise' : 'text-brand-green'}`}
|
|
/>
|
|
) : (
|
|
<X
|
|
className={`w-4 h-4 flex-shrink-0 mt-0.5 ${plan.highlighted ? 'text-white/20' : 'text-gray-300'}`}
|
|
/>
|
|
)}
|
|
<span
|
|
className={`text-sm ${
|
|
feature.included
|
|
? plan.highlighted
|
|
? 'text-white/90'
|
|
: 'text-gray-700'
|
|
: plan.highlighted
|
|
? 'text-white/30'
|
|
: 'text-gray-400'
|
|
}`}
|
|
>
|
|
{t(`pricing.features.${feature.key}` as any)}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<Link
|
|
href={plan.ctaLink}
|
|
className={`block w-full text-center py-3 px-6 rounded-xl font-semibold text-sm transition-all ${
|
|
plan.highlighted
|
|
? 'bg-brand-turquoise text-white hover:bg-brand-turquoise/90 shadow-lg shadow-brand-turquoise/30 hover:shadow-xl'
|
|
: plan.key === 'bronze'
|
|
? 'bg-gray-100 text-brand-navy hover:bg-gray-200'
|
|
: 'bg-brand-navy text-white hover:bg-brand-navy/90 shadow-md hover:shadow-lg'
|
|
}`}
|
|
>
|
|
{planCta}
|
|
</Link>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={isPricingInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8, delay: 0.5 }}
|
|
className="mt-12 text-center space-y-2"
|
|
>
|
|
<p className="text-gray-600 text-sm">{t('pricing.noCommitment')}</p>
|
|
<p className="text-sm text-gray-500">
|
|
{t('pricing.questions')}{' '}
|
|
<Link href="/contact" className="text-brand-turquoise font-medium hover:underline">
|
|
{t('pricing.contactSales')}
|
|
</Link>
|
|
</p>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* How It Works Section */}
|
|
<section
|
|
ref={howRef}
|
|
className="py-20 lg:py-32 bg-gradient-to-br from-brand-navy to-brand-navy/95 relative overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 opacity-10 pointer-events-none">
|
|
<div className="absolute top-10 left-10 w-72 h-72 bg-brand-turquoise rounded-full blur-3xl" />
|
|
<div className="absolute bottom-10 right-10 w-72 h-72 bg-brand-green rounded-full blur-3xl" />
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto px-6 lg:px-8 relative z-10">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={isHowInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8 }}
|
|
className="text-center mb-16"
|
|
>
|
|
<h2 className="text-4xl lg:text-5xl font-bold text-white mb-4">
|
|
{t('howItWorks.title')}
|
|
</h2>
|
|
<p className="text-xl text-white/80 max-w-2xl mx-auto">{t('howItWorks.subtitle')}</p>
|
|
</motion.div>
|
|
|
|
<div className="relative">
|
|
<div className="hidden lg:block absolute top-10 left-[12.5%] right-[12.5%] h-px bg-white/15">
|
|
<motion.div
|
|
initial={{ scaleX: 0 }}
|
|
animate={isHowInView ? { scaleX: 1 } : {}}
|
|
transition={{ duration: 1.6, delay: 0.5, ease: 'easeInOut' }}
|
|
style={{ transformOrigin: 'left' }}
|
|
className="absolute inset-0 bg-brand-turquoise"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
{[
|
|
{ stepKey: 'step1', step: '01', icon: Search },
|
|
{ stepKey: 'step2', step: '02', icon: BarChart3 },
|
|
{ stepKey: 'step3', step: '03', icon: CheckCircle2 },
|
|
{ stepKey: 'step4', step: '04', icon: Container },
|
|
].map((step, index) => {
|
|
const IconComponent = step.icon;
|
|
return (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={isHowInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.6, delay: 0.3 + index * 0.15 }}
|
|
className="text-center relative z-10"
|
|
>
|
|
<div className="mb-5">
|
|
<div className="w-20 h-20 bg-brand-turquoise rounded-full flex items-center justify-center text-3xl font-bold text-white mx-auto shadow-xl ring-4 ring-brand-turquoise/20">
|
|
{step.step}
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-center mb-3">
|
|
<div className="p-2 bg-white/10 rounded-lg">
|
|
<IconComponent className="w-5 h-5 text-brand-turquoise" />
|
|
</div>
|
|
</div>
|
|
<h3 className="text-xl font-bold text-white mb-2">
|
|
{t(`howItWorks.${step.stepKey}.title` as any)}
|
|
</h3>
|
|
<p className="text-white/70 text-sm leading-relaxed">
|
|
{t(`howItWorks.${step.stepKey}.description` as any)}
|
|
</p>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={isHowInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.6, delay: 1.4 }}
|
|
className="mt-16 text-center"
|
|
>
|
|
<Link
|
|
href="/register"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center space-x-2 px-8 py-4 bg-brand-turquoise text-white rounded-xl font-semibold text-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 group"
|
|
>
|
|
<span>{tCommon('tryNow')}</span>
|
|
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
</Link>
|
|
<p className="mt-3 text-white/50 text-sm">{t('howItWorks.ctaHint')}</p>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Testimonials Section */}
|
|
<section
|
|
ref={testimonialsRef}
|
|
className="py-12 sm:py-20 lg:py-32 bg-gradient-to-br from-gray-50 to-white"
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={isTestimonialsInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.8 }}
|
|
className="text-center mb-10 sm:mb-16"
|
|
>
|
|
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4">
|
|
{t('testimonials.title')}
|
|
</h2>
|
|
<p className="text-base sm:text-xl text-gray-600 max-w-2xl mx-auto">
|
|
{t('testimonials.subtitle')}
|
|
</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate={isTestimonialsInView ? 'visible' : 'hidden'}
|
|
className="grid grid-cols-1 md:grid-cols-3 gap-8"
|
|
>
|
|
{testimonials.map((testimonial, index) => (
|
|
<motion.div
|
|
key={index}
|
|
variants={itemVariants}
|
|
whileHover={{ y: -10 }}
|
|
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
|
>
|
|
<div className="flex mb-4">
|
|
{[...Array(5)].map((_, i) => (
|
|
<svg
|
|
key={i}
|
|
className="w-5 h-5 text-yellow-400 fill-current"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
|
|
</svg>
|
|
))}
|
|
</div>
|
|
<p className="text-gray-700 mb-6 leading-relaxed italic">"{testimonial.quote}"</p>
|
|
<div className="border-t pt-4">
|
|
<div className="font-bold text-brand-navy">{testimonial.author}</div>
|
|
<div className="text-sm text-gray-600">
|
|
{testimonial.role} - {testimonial.company}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* CTA Section */}
|
|
<section ref={ctaRef} className="py-12 sm:py-20 lg:py-32">
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate={isCtaInView ? 'visible' : 'hidden'}
|
|
className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"
|
|
>
|
|
<motion.div variants={itemVariants}>
|
|
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-4 sm:mb-6">
|
|
{t('cta.title')}
|
|
</h2>
|
|
<p className="text-base sm:text-xl text-gray-600 mb-8 sm:mb-10">{t('cta.subtitle')}</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
variants={itemVariants}
|
|
className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-6 px-2 sm:px-0"
|
|
>
|
|
{isAuthenticated && user ? (
|
|
<Link
|
|
href="/dashboard"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
|
|
>
|
|
<span>{t('cta.ctaAuthenticated')}</span>
|
|
<LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<Link
|
|
href="/register"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
|
|
>
|
|
<span>{t('cta.ctaRegister')}</span>
|
|
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
</Link>
|
|
<Link
|
|
href="/login"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-6 sm:px-8 py-3 sm:py-4 bg-brand-navy text-white rounded-lg hover:bg-brand-navy/90 transition-all hover:shadow-xl font-semibold text-base sm:text-lg w-full sm:w-auto text-center"
|
|
>
|
|
{t('cta.ctaLogin')}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
variants={itemVariants}
|
|
className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 mt-8 text-sm text-gray-500"
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<CheckCircle2 className="w-4 h-4 text-brand-green" />
|
|
<span>{t('cta.features.noCard')}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Clock className="w-4 h-4 text-brand-green" />
|
|
<span>{t('cta.features.quickSetup')}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Shield className="w-4 h-4 text-brand-green" />
|
|
<span>{t('cta.features.secure')}</span>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</section>
|
|
|
|
<LandingFooter />
|
|
</div>
|
|
);
|
|
}
|