From 017b998941d6fecf844b6557734ef16bb0ecfd01 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 30 Aug 2026 22:12:52 +0200 Subject: [PATCH] fix(landing): pricing cards read live plan prices from /api/v1/auth/plans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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'). --- .../src/components/landing/landing-page.tsx | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/landing/landing-page.tsx b/frontend/src/components/landing/landing-page.tsx index e12d6c1..eea636b 100644 --- a/frontend/src/components/landing/landing-page.tsx +++ b/frontend/src/components/landing/landing-page.tsx @@ -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>({}); + + 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 = {}; + 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.desc}

- €{isAnnual ? p.annualPrice : p.monthlyPrice} + {priceFor(p.id, p.monthlyPrice, p.annualPrice)} € / {t('landing.pricing.month')}