All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m3s
Dev CI / Frontend — Unit Tests (push) Successful in 10m33s
Dev CI / Notify Failure (push) Has been skipped
321 lines
11 KiB
TypeScript
321 lines
11 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>
|
|
);
|
|
}
|