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
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user