fix(landing): pricing cards read live plan prices from /api/v1/auth/plans
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m54s

The landing hardcoded 9/19/49 (monthly) and 7/15/39 (annual-equivalent)
while /pricing renders admin-configurable prices — the two surfaces
drifted apart whenever prices changed. The landing now fetches the same
live endpoint (no-store) with the previous values as load/failure
fallback, and shows the French '9 €' format like /pricing (was '€9').
This commit is contained in:
2026-08-30 22:12:52 +02:00
parent 111f3cb69d
commit 017b998941

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import {
FileText,
@@ -27,6 +27,7 @@ import {
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { useI18n } from "@/lib/i18n";
import { API_BASE } from "@/lib/config";
import { LanguageSwitcher } from "@/components/ui/language-switcher";
const Logo = () => (
@@ -325,11 +326,41 @@ const FormatsSection = () => {
const PricingSection = () => {
const { t } = useI18n();
const [isAnnual, setIsAnnual] = useState(true);
// Prices come from the same live endpoint as /pricing (admin-configurable);
// the hardcoded values below are only a fallback while it loads or fails.
const [livePrices, setLivePrices] = useState<Record<string, { monthly: number; annualMonthly: number }>>({});
useEffect(() => {
fetch(`${API_BASE}/api/v1/auth/plans`, { cache: 'no-store' })
.then(r => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then(json => {
const d = json.data ?? json;
const map: Record<string, { monthly: number; annualMonthly: number }> = {};
for (const plan of d.plans ?? []) {
if (typeof plan.price_monthly === 'number' && plan.price_monthly > 0) {
map[plan.id] = {
monthly: plan.price_monthly,
annualMonthly: plan.price_yearly > 0 ? plan.price_yearly / 12 : plan.price_monthly,
};
}
}
setLivePrices(map);
})
.catch(() => { /* keep marketing defaults */ });
}, []);
const priceFor = (id: string, fallbackMonthly: number, fallbackAnnual: number): string => {
const live = livePrices[id];
const value = live
? (isAnnual ? live.annualMonthly : live.monthly)
: (isAnnual ? fallbackAnnual : fallbackMonthly);
return Number.isInteger(value) ? String(value) : value.toFixed(2);
};
const plans = [
{ name: t('landing.pricing.starter.name'), desc: t('landing.pricing.starter.desc'), monthlyPrice: 9, annualPrice: 7, features: [t('landing.pricing.starter.f1'), t('landing.pricing.starter.f2'), t('landing.pricing.starter.f3'), t('landing.pricing.starter.f4')], cta: t('landing.pricing.starter.cta'), popular: false },
{ name: t('landing.pricing.pro.name'), desc: t('landing.pricing.pro.desc'), monthlyPrice: 19, annualPrice: 15, features: [t('landing.pricing.pro.f1'), t('landing.pricing.pro.f2'), t('landing.pricing.pro.f3'), t('landing.pricing.pro.f4'), t('landing.pricing.pro.f5'), t('landing.pricing.pro.f6')], cta: t('landing.pricing.pro.cta'), popular: true },
{ name: t('landing.pricing.business.name'), desc: t('landing.pricing.business.desc'), monthlyPrice: 49, annualPrice: 39, features: [t('landing.pricing.business.f1'), t('landing.pricing.business.f2'), t('landing.pricing.business.f3'), t('landing.pricing.business.f4'), t('landing.pricing.business.f5'), t('landing.pricing.business.f6')], cta: t('landing.pricing.business.cta'), popular: false },
{ id: 'starter', name: t('landing.pricing.starter.name'), desc: t('landing.pricing.starter.desc'), monthlyPrice: 9, annualPrice: 7, features: [t('landing.pricing.starter.f1'), t('landing.pricing.starter.f2'), t('landing.pricing.starter.f3'), t('landing.pricing.starter.f4')], cta: t('landing.pricing.starter.cta'), popular: false },
{ id: 'pro', name: t('landing.pricing.pro.name'), desc: t('landing.pricing.pro.desc'), monthlyPrice: 19, annualPrice: 15, features: [t('landing.pricing.pro.f1'), t('landing.pricing.pro.f2'), t('landing.pricing.pro.f3'), t('landing.pricing.pro.f4'), t('landing.pricing.pro.f5'), t('landing.pricing.pro.f6')], cta: t('landing.pricing.pro.cta'), popular: true },
{ id: 'business', name: t('landing.pricing.business.name'), desc: t('landing.pricing.business.desc'), monthlyPrice: 49, annualPrice: 39, features: [t('landing.pricing.business.f1'), t('landing.pricing.business.f2'), t('landing.pricing.business.f3'), t('landing.pricing.business.f4'), t('landing.pricing.business.f5'), t('landing.pricing.business.f6')], cta: t('landing.pricing.business.cta'), popular: false },
];
return (
@@ -359,7 +390,7 @@ const PricingSection = () => {
<p className="text-brand-dark/60 dark:text-white/60 text-xs font-light mb-14 tracking-widest uppercase">{p.desc}</p>
<div className="mb-14">
<span className="text-5xl font-serif font-medium leading-none text-brand-dark dark:text-white">{isAnnual ? p.annualPrice : p.monthlyPrice}</span>
<span className="text-5xl font-serif font-medium leading-none text-brand-dark dark:text-white">{priceFor(p.id, p.monthlyPrice, p.annualPrice)} </span>
<span className="text-brand-dark/55 dark:text-white/55 font-light ml-2 uppercase text-[10px] tracking-[0.2em]">/ {t('landing.pricing.month')}</span>
</div>