xpeditis2.0/apps/frontend/app/[locale]/pricing/page.tsx
2026-05-12 21:01:52 +02:00

331 lines
12 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import Image from 'next/image';
import { Link } from '@/i18n/navigation';
import { useTranslations, useLocale } from 'next-intl';
import { Check, X, ArrowRight, Shield } from 'lucide-react';
type BillingInterval = 'monthly' | 'yearly';
type PlanKey = 'bronze' | 'silver' | 'gold' | 'platinium';
interface FeatureRow {
key: string;
included: boolean;
}
interface PlanConfig {
key: PlanKey;
monthlyPrice: number;
yearlyPrice: number;
maxUsersKey: 'unlimited' | null;
maxUsers?: number;
shipmentsKey: 'shipmentsPerYear' | 'shipmentsUnlimited';
commission: string;
supportKey: 'supportNone' | 'supportEmail' | 'supportDirect' | 'supportKam';
badge: 'silver' | 'gold' | 'platinium' | null;
ctaStyle: string;
popular: boolean;
features: FeatureRow[];
}
const PLANS: PlanConfig[] = [
{
key: 'bronze',
monthlyPrice: 0,
yearlyPrice: 0,
maxUsers: 1,
maxUsersKey: null,
shipmentsKey: 'shipmentsPerYear',
commission: '5%',
supportKey: 'supportNone',
badge: null,
ctaStyle: 'bg-gray-900 text-white hover:bg-gray-800',
popular: false,
features: [
{ key: 'rates', included: true },
{ key: 'bookings', included: true },
{ key: 'dashboard', included: false },
{ key: 'wiki', included: false },
{ key: 'userManagement', included: false },
{ key: 'csvImport', included: false },
{ key: 'apiAccess', included: false },
{ key: 'customUI', included: false },
{ key: 'dedicatedKam', included: false },
],
},
{
key: 'silver',
monthlyPrice: 249,
yearlyPrice: 2739,
maxUsers: 5,
maxUsersKey: null,
shipmentsKey: 'shipmentsUnlimited',
commission: '3%',
supportKey: 'supportEmail',
badge: 'silver',
ctaStyle: 'bg-brand-turquoise text-white hover:opacity-90',
popular: true,
features: [
{ key: 'rates', included: true },
{ key: 'bookings', included: true },
{ key: 'dashboard', included: true },
{ key: 'wiki', included: true },
{ key: 'userManagement', included: true },
{ key: 'csvImport', included: true },
{ key: 'apiAccess', included: false },
{ key: 'customUI', included: false },
{ key: 'dedicatedKam', included: false },
],
},
{
key: 'gold',
monthlyPrice: 899,
yearlyPrice: 9889,
maxUsers: 20,
maxUsersKey: null,
shipmentsKey: 'shipmentsUnlimited',
commission: '2%',
supportKey: 'supportDirect',
badge: 'gold',
ctaStyle: 'bg-yellow-500 text-white hover:bg-yellow-600',
popular: false,
features: [
{ key: 'rates', included: true },
{ key: 'bookings', included: true },
{ key: 'dashboard', included: true },
{ key: 'wiki', included: true },
{ key: 'userManagement', included: true },
{ key: 'csvImport', included: true },
{ key: 'apiAccess', included: true },
{ key: 'customUI', included: false },
{ key: 'dedicatedKam', included: false },
],
},
{
key: 'platinium',
monthlyPrice: -1,
yearlyPrice: -1,
maxUsersKey: 'unlimited',
shipmentsKey: 'shipmentsUnlimited',
commission: '1%',
supportKey: 'supportKam',
badge: 'platinium',
ctaStyle: 'bg-purple-600 text-white hover:bg-purple-700',
popular: false,
features: [
{ key: 'rates', included: true },
{ key: 'bookings', included: true },
{ key: 'dashboard', included: true },
{ key: 'wiki', included: true },
{ key: 'userManagement', included: true },
{ key: 'csvImport', included: true },
{ key: 'apiAccess', included: true },
{ key: 'customUI', included: true },
{ key: 'dedicatedKam', included: true },
],
},
];
export default function PricingPage() {
const t = useTranslations('marketing.pricing');
const locale = useLocale();
const numberLocale = locale === 'fr' ? 'fr-FR' : 'en-US';
const [billing, setBilling] = useState<BillingInterval>('monthly');
const formatPrice = (amount: number): string =>
new Intl.NumberFormat(numberLocale, {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header className="border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<Link href="/">
<Image
src="/assets/logos/logo-black.svg"
alt="Xpeditis"
width={40}
height={48}
priority
/>
</Link>
<div className="flex items-center gap-4">
<Link href="/login" className="text-sm text-gray-600 hover:text-gray-900">
{t('header.login')}
</Link>
<Link
href="/register"
className="text-sm bg-brand-turquoise text-white px-4 py-2 rounded-lg hover:opacity-90"
>
{t('header.register')}
</Link>
</div>
</div>
</header>
{/* Hero */}
<section className="py-16 text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">{t('hero.title')}</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto mb-8">{t('hero.subtitle')}</p>
{/* Billing toggle */}
<div className="flex items-center justify-center gap-4 mb-12">
<span
className={`text-sm font-medium ${billing === 'monthly' ? 'text-gray-900' : 'text-gray-500'}`}
>
{t('hero.monthly')}
</span>
<button
onClick={() => setBilling(billing === 'monthly' ? 'yearly' : 'monthly')}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
billing === 'yearly' ? 'bg-brand-turquoise' : 'bg-gray-300'
}`}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
billing === 'yearly' ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
<span
className={`text-sm font-medium ${billing === 'yearly' ? 'text-gray-900' : 'text-gray-500'}`}
>
{t('hero.yearly')}
</span>
{billing === 'yearly' && (
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full font-medium">
{t('hero.yearlyBadge')}
</span>
)}
</div>
</section>
{/* Plans grid */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{PLANS.map(plan => (
<div
key={plan.key}
className={`relative rounded-2xl border-2 p-6 flex flex-col ${
plan.popular
? 'border-brand-turquoise shadow-lg shadow-brand-turquoise/10'
: 'border-gray-200'
}`}
>
{plan.popular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="bg-brand-turquoise text-white text-xs font-semibold px-3 py-1 rounded-full">
{t('popular')}
</span>
</div>
)}
{/* Plan name & badge */}
<div className="flex items-center gap-2 mb-2">
<h3 className="text-xl font-bold text-gray-900">{t(`plans.${plan.key}.name`)}</h3>
{plan.badge && (
<Shield
className={`w-5 h-5 ${
plan.badge === 'silver'
? 'text-slate-500'
: plan.badge === 'gold'
? 'text-yellow-500'
: 'text-purple-500'
}`}
/>
)}
</div>
<p className="text-sm text-gray-500 mb-4">{t(`plans.${plan.key}.description`)}</p>
{/* Price */}
<div className="mb-6">
{plan.monthlyPrice === -1 ? (
<p className="text-3xl font-bold text-gray-900">{t('currency.onQuote')}</p>
) : plan.monthlyPrice === 0 ? (
<p className="text-3xl font-bold text-gray-900">{t('currency.free')}</p>
) : (
<>
<p className="text-3xl font-bold text-gray-900">
{billing === 'monthly'
? formatPrice(plan.monthlyPrice)
: formatPrice(Math.round(plan.yearlyPrice / 12))}
<span className="text-base font-normal text-gray-500">
{t('currency.perMonth')}
</span>
</p>
{billing === 'yearly' && (
<p className="text-sm text-gray-500 mt-1">
{t('currency.yearlyPrice', { price: formatPrice(plan.yearlyPrice) })}
</p>
)}
</>
)}
</div>
{/* Quick stats */}
<div className="space-y-2 mb-6 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">{t('stats.users')}</span>
<span className="font-medium">
{plan.maxUsersKey ? t(`values.${plan.maxUsersKey}`) : plan.maxUsers}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">{t('stats.shipments')}</span>
<span className="font-medium">{t(`values.${plan.shipmentsKey}`)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">{t('stats.commission')}</span>
<span className="font-medium">{plan.commission}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">{t('stats.support')}</span>
<span className="font-medium">{t(`values.${plan.supportKey}`)}</span>
</div>
</div>
{/* Features */}
<div className="flex-1 space-y-2 mb-6">
{plan.features.map(feature => (
<div key={feature.key} className="flex items-center gap-2 text-sm">
{feature.included ? (
<Check className="w-4 h-4 text-green-500 flex-shrink-0" />
) : (
<X className="w-4 h-4 text-gray-300 flex-shrink-0" />
)}
<span className={feature.included ? 'text-gray-700' : 'text-gray-400'}>
{t(`features.${feature.key}` as any)}
</span>
</div>
))}
</div>
{/* CTA */}
<Link
href={plan.key === 'platinium' ? '/contact' : '/register'}
className={`block text-center py-3 px-4 rounded-lg text-sm font-semibold transition-all ${plan.ctaStyle}`}
>
{t(`plans.${plan.key}.cta`)}
<ArrowRight className="inline-block w-4 h-4 ml-1" />
</Link>
</div>
))}
</div>
</section>
{/* Footer */}
<footer className="border-t py-8 text-center text-sm text-gray-500">
<p>{t('footer')}</p>
</footer>
</div>
);
}