Les boutons suivent la couleur d’apparence, les libellés trop petits ou trop techniques sont clarifiés, et le catalogue des fournisseurs se met à jour tout seul. Co-authored-by: Cursor <cursoragent@cursor.com>
166 lines
7.4 KiB
TypeScript
166 lines
7.4 KiB
TypeScript
'use client'
|
|
|
|
import { Check } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { SUBSCRIPTION_TRIAL_DAYS } from '@/lib/billing/trial-constants'
|
|
|
|
export default function PricingPage() {
|
|
const { t } = useLanguage()
|
|
const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly')
|
|
const trialDays = SUBSCRIPTION_TRIAL_DAYS
|
|
const { data: byokCatalog } = useQuery({
|
|
queryKey: ['public', 'byok-catalog'],
|
|
queryFn: async () => {
|
|
const res = await fetch('/api/public/byok-catalog')
|
|
if (!res.ok) throw new Error('catalog')
|
|
return res.json() as Promise<{ providers: { id: string }[] }>
|
|
},
|
|
staleTime: 60_000,
|
|
})
|
|
const providerCount = byokCatalog?.providers.length || '…'
|
|
|
|
const plans = [
|
|
{ key: 'basic', popular: false, hasTrial: false, price: t('landing.pricing.basicPrice'), period: '' },
|
|
{
|
|
key: 'pro',
|
|
popular: true,
|
|
hasTrial: true,
|
|
price: billingInterval === 'monthly' ? t('landing.pricing.proMonthly') : t('landing.pricing.proAnnualMonthly'),
|
|
period: billingInterval === 'monthly' ? t('landing.pricing.perMonth') : t('landing.pricing.perMonthAnnual'),
|
|
},
|
|
{
|
|
key: 'business',
|
|
popular: false,
|
|
hasTrial: true,
|
|
price: billingInterval === 'monthly' ? t('landing.pricing.businessMonthly') : t('landing.pricing.businessAnnualMonthly'),
|
|
period: billingInterval === 'monthly' ? t('landing.pricing.perMonth') : t('landing.pricing.perMonthAnnual'),
|
|
},
|
|
{
|
|
key: 'enterprise',
|
|
popular: false,
|
|
hasTrial: false,
|
|
price: t('landing.pricing.enterprisePrice'),
|
|
period: '',
|
|
},
|
|
]
|
|
|
|
return (
|
|
<main className="min-h-screen bg-[#0B0A09] text-[#F4F1EA] font-[family-name:var(--font-manrope)] selection:bg-[#D4A373]/40 selection:text-white">
|
|
<nav className="sticky top-0 z-[100] px-5 sm:px-8 py-4 flex items-center justify-between bg-[#0B0A09]/70 backdrop-blur-xl border-b border-white/[0.06]">
|
|
<Link href="/" className="flex items-center gap-2.5 group">
|
|
<div className="w-9 h-9 bg-[#F4F1EA] text-[#0B0A09] flex items-center justify-center rounded-lg">
|
|
<span className="font-serif text-xl font-bold leading-none">M</span>
|
|
</div>
|
|
<span className="font-serif text-xl font-medium tracking-tight">Memento</span>
|
|
</Link>
|
|
<div className="flex items-center gap-2 sm:gap-3">
|
|
<Link href="/login" className="text-[13px] text-white/75 hover:text-white transition-colors px-2">
|
|
{t('landing.nav.login')}
|
|
</Link>
|
|
<Link
|
|
href="/register"
|
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-[#F4F1EA] text-[#0B0A09] text-[13px] font-semibold hover:bg-white transition-colors"
|
|
>
|
|
{t('landing.nav.cta')}
|
|
</Link>
|
|
</div>
|
|
</nav>
|
|
|
|
<section className="px-5 sm:px-8 py-28">
|
|
<div className="max-w-6xl mx-auto">
|
|
<div className="text-center mb-12">
|
|
<span className="text-[11px] font-bold uppercase tracking-[0.3em] text-[#D4A373] mb-4 block">
|
|
{t('landing.pricing.label')}
|
|
</span>
|
|
<h1 className="font-serif text-3xl sm:text-5xl tracking-tight mb-4">{t('landing.pricing.title')}</h1>
|
|
<p className="text-white/70 mb-8">{t('landing.pricing.desc')}</p>
|
|
<div className="inline-flex p-1 rounded-full border border-white/10 bg-white/[0.03]">
|
|
<button
|
|
type="button"
|
|
onClick={() => setBillingInterval('monthly')}
|
|
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all ${billingInterval === 'monthly' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
|
|
>
|
|
{t('landing.pricing.monthly')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setBillingInterval('annual')}
|
|
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all relative ${billingInterval === 'annual' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
|
|
>
|
|
{t('landing.pricing.annual')}
|
|
<span className="absolute -top-3 -right-1 text-[10px] text-[#D4A373] whitespace-nowrap">
|
|
{t('landing.pricing.savePercent')}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
{plans.map((plan) => (
|
|
<div
|
|
key={plan.key}
|
|
className={`rounded-2xl border p-6 flex flex-col ${
|
|
plan.popular
|
|
? 'border-[#D4A373]/50 bg-[#D4A373]/10'
|
|
: 'border-white/[0.08] bg-white/[0.02]'
|
|
}`}
|
|
>
|
|
{plan.popular && (
|
|
<span className="text-[10px] font-bold uppercase tracking-widest text-[#D4A373] mb-3">
|
|
{t('landing.pricing.popular')}
|
|
</span>
|
|
)}
|
|
<h2 className="text-[13px] font-medium tracking-wide text-white/80 mb-2">
|
|
{t(`landing.pricing.${plan.key}.name`)}
|
|
</h2>
|
|
<div className="flex items-baseline gap-1 mb-2">
|
|
<span className="text-3xl font-serif">{plan.price}</span>
|
|
{plan.period && <span className="text-sm text-white/70">{plan.period}</span>}
|
|
</div>
|
|
{plan.hasTrial && (
|
|
<p className="text-[11px] font-semibold text-[#D4A373] mb-3">
|
|
{t('landing.pricing.trialBadge', { days: trialDays })}
|
|
</p>
|
|
)}
|
|
<p className="text-sm text-white/70 mb-6">{t(`landing.pricing.${plan.key}.desc`)}</p>
|
|
<ul className="space-y-2.5 mb-8 flex-1">
|
|
{plan.hasTrial && (
|
|
<li className="flex gap-2 text-xs text-[#D4A373]/90">
|
|
<Check size={12} className="text-[#D4A373] mt-0.5 shrink-0" />
|
|
{t('landing.pricing.trialFeature', { days: trialDays })}
|
|
</li>
|
|
)}
|
|
{[0, 1, 2, 3, 4, 5].map((j) => {
|
|
const feat = t(`landing.pricing.${plan.key}.feature${j}`, { count: providerCount })
|
|
if (!feat || feat.startsWith('landing.')) return null
|
|
return (
|
|
<li key={j} className="flex gap-2 text-sm text-white/80">
|
|
<Check size={12} className="text-[#D4A373] mt-0.5 shrink-0" />
|
|
{feat}
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
<Link
|
|
href="/register"
|
|
className={`py-3 rounded-xl text-center text-[13px] font-semibold transition-colors ${
|
|
plan.popular
|
|
? 'bg-[#F4F1EA] text-[#0B0A09] hover:bg-white'
|
|
: 'bg-white/10 text-white hover:bg-white/15'
|
|
}`}
|
|
>
|
|
{plan.hasTrial
|
|
? t('landing.pricing.trialCta', { days: trialDays })
|
|
: t(`landing.pricing.${plan.key}.cta`)}
|
|
</Link>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
)
|
|
}
|