Files
Momento/memento-note/app/(public)/pricing/page.tsx
Antigravity 4fc5e3ce04
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m5s
CI / Deploy production (on server) (push) Successful in 24s
fix(ui): tarifs publics unifiés et prix annuel lisible
La page tarifs reprend la barre du site public. En annuel, Pro affiche
8,25 € par mois (99 € l’année), plus le 99 € comme gros chiffre.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 17:43:05 +00:00

152 lines
6.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'
import { PublicSiteChrome } from '@/components/public-site-chrome'
import {
DEFAULT_PRICES,
annualDiscountPercent,
} from '@/lib/billing/price-catalog'
export default function PricingPage() {
const { t } = useLanguage()
const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly')
const trialDays = SUBSCRIPTION_TRIAL_DAYS
const annualSavePercent = annualDiscountPercent(
DEFAULT_PRICES.PRO.month.amount,
DEFAULT_PRICES.PRO.year.amount,
)
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 (
<PublicSiteChrome currentPage="pricing">
<section className="px-5 sm:px-8 py-20 sm:py-28">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-12">
<h1 className="font-serif text-3xl sm:text-5xl tracking-tight mb-4">{t('landing.pricing.title')}</h1>
<p className="text-white/80 mb-8">{t('landing.pricing.desc')}</p>
<div className="inline-flex p-1 rounded-full border border-white/15 bg-white/[0.04]">
<button
type="button"
onClick={() => setBillingInterval('monthly')}
className={`px-5 py-2 rounded-full text-[13px] font-semibold transition-all ${billingInterval === 'monthly' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/80'}`}
>
{t('landing.pricing.monthly')}
</button>
<button
type="button"
onClick={() => setBillingInterval('annual')}
className={`px-5 py-2 rounded-full text-[13px] font-semibold transition-all relative ${billingInterval === 'annual' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/80'}`}
>
{t('landing.pricing.annual')}
<span className="absolute -top-3 -right-1 text-[11px] text-[#E8C39A] whitespace-nowrap">
{t('landing.pricing.savePercent', { percent: annualSavePercent })}
</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]/60 bg-[#D4A373]/12'
: 'border-white/[0.12] bg-white/[0.04]'
}`}
>
{plan.popular && (
<span className="text-[11px] font-bold uppercase tracking-widest text-[#E8C39A] mb-3">
{t('landing.pricing.popular')}
</span>
)}
<h2 className="text-[14px] font-medium tracking-wide text-[#F4F1EA] 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/80">{plan.period}</span>}
</div>
{plan.hasTrial && (
<p className="text-[12px] font-semibold text-[#E8C39A] mb-3">
{t('landing.pricing.trialBadge', { days: trialDays })}
</p>
)}
<p className="text-sm text-white/80 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-[#E8C39A]">
<Check size={12} className="text-[#E8C39A] 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-[#F4F1EA]/90">
<Check size={12} className="text-[#E8C39A] 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/15 text-white hover:bg-white/20'
}`}
>
{plan.hasTrial
? t('landing.pricing.trialCta', { days: trialDays })
: t(`landing.pricing.${plan.key}.cta`)}
</Link>
</div>
))}
</div>
</div>
</section>
</PublicSiteChrome>
)
}