feat(billing): redesign page abonnement — layout asymétrique + identité Rubis
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 19s
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 19s
Mensuel par défaut dans le toggle (au lieu de yearly).
Layout asymétrique 1fr/1.3fr/1fr avec Pro en hero card centrale, plus
large et plus dense visuellement que Free et Business — sort du pattern
"3 cards égales" générique des landing pages SaaS.
Identité Rubis :
- Gem 280px en filigrane sur la Pro card (rubis-glow, opacity 70%) —
le motif signature de la marque, pas un blob de gradient
- Bullets ◆ losanges rubis tournés (5×5px) au lieu des check-circles
Lucide génériques — cohérent avec PlanCard, Timeline, etc.
- Pastille "Le plus pris" avec mini-gem inline sur Pro
- Border rubis + shadow-card sur Pro, line + sobre pour les voisines
- Free : bg cream-2 (ton "découverte"), Business : bg blanc + accents
rubis-deep
- Strip "plan courant" : remplace le gros bloc card → ligne flex avec
gem + texte + mini progress bar 32px + bouton ghost. Discret.
Voice direct dans les bénéfices :
- Header narratif "Trois mois pour voir. Puis vous décidez."
- "Tester sans poser sa CB" (Free)
- "Pour les TPE qui n'ont plus jamais à y penser" (Pro titre)
- "Moins cher qu'une heure de votre temps mensuel" (Pro footer)
- Annulation 1-clic mentionné en sous-titre du toggle
Prix Pro XL 44px (vs 28px pour les voisines) — hiérarchie visuelle qui
guide l'œil vers l'option qu'on veut pousser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1952265217
commit
fd24ef42a6
@ -1,20 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
CreditCard,
|
||||
Sparkles,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { ArrowLeft, ArrowRight, CreditCard } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useOpenPortal, useStartCheckout, useSubscription, type BillingCycle, type PlanKey } from "@/lib/billing";
|
||||
import { useOpenPortal, useStartCheckout, useSubscription, type BillingCycle } from "@/lib/billing";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { Gem } from "@/components/brand/Gem";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
@ -30,22 +23,13 @@ export const Route = createFileRoute("/_app/parametres_/abonnement")({
|
||||
component: AbonnementPage,
|
||||
});
|
||||
|
||||
/**
|
||||
* /parametres/abonnement — gestion du plan et de la facturation Rubis.
|
||||
*
|
||||
* Sections :
|
||||
* - Plan actuel + caps + état grace period
|
||||
* - Comparaison Free / Pro / Business avec toggle mensuel/annuel
|
||||
* - CTA upgrade (Stripe Checkout) ou portail (Customer Portal)
|
||||
*/
|
||||
function AbonnementPage() {
|
||||
const search = Route.useSearch();
|
||||
const { data: sub, isPending } = useSubscription();
|
||||
const checkout = useStartCheckout();
|
||||
const portal = useOpenPortal();
|
||||
const [cycle, setCycle] = useState<BillingCycle>("yearly");
|
||||
const [cycle, setCycle] = useState<BillingCycle>("monthly");
|
||||
|
||||
// Toast post-redirect Stripe
|
||||
useEffect(() => {
|
||||
if (search.checkout === "success") {
|
||||
toast.success("Bienvenue sur le nouveau plan ! Activation en cours…");
|
||||
@ -75,8 +59,10 @@ function AbonnementPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const currentPlan = sub?.plan;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-7">
|
||||
<Link
|
||||
to="/parametres"
|
||||
className="inline-flex items-center gap-1.5 self-start text-[12.5px] text-ink-3 hover:text-rubis"
|
||||
@ -84,71 +70,69 @@ function AbonnementPage() {
|
||||
<ArrowLeft size={13} aria-hidden="true" /> Paramètres
|
||||
</Link>
|
||||
|
||||
<header>
|
||||
{/* Header — direct, pas trop de baratin */}
|
||||
<header className="max-w-2xl">
|
||||
<Eyebrow>Abonnement</Eyebrow>
|
||||
<h1 className="mt-2 font-display text-[28px] font-bold tracking-[-0.022em] text-ink lg:text-[32px]">
|
||||
Choisir <em className="text-rubis">son plan</em>
|
||||
<h1 className="mt-2 font-display text-[30px] font-bold tracking-[-0.024em] text-ink lg:text-[36px] leading-[1.05]">
|
||||
Trois mois pour voir.
|
||||
<br />
|
||||
Puis vous <em className="text-rubis">décidez</em>.
|
||||
</h1>
|
||||
<p className="mt-1.5 text-[14px] text-ink-3 max-w-2xl leading-relaxed">
|
||||
Trois mois offerts au démarrage. Au-delà, passez Pro pour
|
||||
continuer à relancer plus de 5 factures à la fois.
|
||||
<p className="mt-3 text-[14.5px] text-ink-2 leading-relaxed">
|
||||
Pas de carte demandée pour démarrer. Au-delà de 5 factures actives,
|
||||
vous passez Pro pour continuer — quand vous êtes prêt, pas avant.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Plan courant */}
|
||||
{/* État courant — slim, info-dense, pas un gros bloc */}
|
||||
{!isPending && sub && (
|
||||
<CurrentPlanCard
|
||||
<CurrentPlanStrip
|
||||
state={sub}
|
||||
onOpenPortal={onOpenPortal}
|
||||
isOpeningPortal={portal.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Toggle mensuel / annuel */}
|
||||
<div className="self-start">
|
||||
{/* Toggle cycle */}
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<CycleToggle value={cycle} onChange={setCycle} />
|
||||
<p className="text-[11.5px] italic text-ink-3">
|
||||
Annulation 1-clic · pas d'engagement
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cards plans */}
|
||||
<section className="grid grid-cols-1 gap-4 lg:grid-cols-3 lg:gap-5">
|
||||
<PlanCard
|
||||
plan="free"
|
||||
currentPlan={sub?.plan}
|
||||
{/* Layout asymétrique : Pro en hero centré, Free et Business plus modestes */}
|
||||
<section className="grid grid-cols-1 gap-4 lg:grid-cols-[1fr_1.3fr_1fr] lg:gap-5 lg:items-stretch">
|
||||
<PlanFree current={currentPlan === "free"} />
|
||||
<PlanProHero
|
||||
current={currentPlan === "pro"}
|
||||
cycle={cycle}
|
||||
ctaDisabled
|
||||
ctaLabel={sub?.plan === "free" ? "Plan actuel" : "Plan gratuit"}
|
||||
/>
|
||||
<PlanCard
|
||||
plan="pro"
|
||||
currentPlan={sub?.plan}
|
||||
cycle={cycle}
|
||||
highlight
|
||||
loading={checkout.isPending && checkout.variables?.plan === "pro"}
|
||||
onUpgrade={() => onUpgrade("pro")}
|
||||
/>
|
||||
<PlanCard
|
||||
plan="business"
|
||||
currentPlan={sub?.plan}
|
||||
<PlanBusiness
|
||||
current={currentPlan === "business"}
|
||||
cycle={cycle}
|
||||
loading={checkout.isPending && checkout.variables?.plan === "business"}
|
||||
onUpgrade={() => onUpgrade("business")}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Footer rassurant */}
|
||||
<p className="text-[12px] text-ink-3 italic max-w-2xl leading-relaxed">
|
||||
Paiement sécurisé via Stripe (CB, SEPA). TVA selon votre pays.
|
||||
Annulation possible à tout moment depuis le portail client. Aucun
|
||||
engagement.
|
||||
Paiement sécurisé via Stripe — CB ou SEPA, TVA selon votre pays.
|
||||
Vous gardez la main sur votre abonnement à tout moment depuis le
|
||||
portail client.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Card plan courant
|
||||
// Strip "Plan courant" — discret mais informatif (vs gros bloc IA-template)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function CurrentPlanCard({
|
||||
function CurrentPlanStrip({
|
||||
state,
|
||||
onOpenPortal,
|
||||
isOpeningPortal,
|
||||
@ -158,72 +142,71 @@ function CurrentPlanCard({
|
||||
isOpeningPortal: boolean;
|
||||
}) {
|
||||
const { plan, activeInvoicesCount, caps, inGracePeriod, gracePeriodEndsAt } = state;
|
||||
const isLimited = plan === "free" && caps.activeInvoicesLimit !== null;
|
||||
const limit = caps.activeInvoicesLimit;
|
||||
const isLimited = plan === "free" && limit !== null;
|
||||
const limitReached =
|
||||
isLimited && limit !== null && !inGracePeriod && activeInvoicesCount >= limit;
|
||||
|
||||
return (
|
||||
<Card padding="md" className="flex flex-col gap-4">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<Eyebrow tone="ink">Plan actuel</Eyebrow>
|
||||
<p className="mt-2 font-display text-[22px] font-bold text-ink">
|
||||
Rubis {planLabel(plan)}
|
||||
{state.subscriptionStatus && state.subscriptionStatus !== "active" && (
|
||||
<span className="ml-2 text-[12px] font-medium text-rubis-deep uppercase tracking-[0.1em]">
|
||||
· {state.subscriptionStatus}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{state.currentPeriodEnd && (
|
||||
<p className="mt-1 text-[12.5px] text-ink-3">
|
||||
Prochaine facture le{" "}
|
||||
<strong className="font-medium text-ink-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-4 rounded-card border bg-white px-5 py-4",
|
||||
"border-line",
|
||||
)}
|
||||
>
|
||||
<Gem size={22} className="shrink-0" />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-display text-[15px] font-bold text-ink leading-tight">
|
||||
{plan === "free" ? "Plan Rubis" : "Rubis"}
|
||||
{plan !== "free" && (
|
||||
<span className="ml-1 text-rubis">{plan === "pro" ? " Pro" : " Business"}</span>
|
||||
)}
|
||||
{state.subscriptionStatus && state.subscriptionStatus !== "active" && (
|
||||
<span className="ml-2 text-[10.5px] font-medium text-rubis-deep uppercase tracking-[0.1em]">
|
||||
· {state.subscriptionStatus}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[12px] text-ink-3 leading-tight">
|
||||
{plan === "free" && inGracePeriod && gracePeriodEndsAt && (
|
||||
<>
|
||||
Période illimitée jusqu'au{" "}
|
||||
<strong className="text-ink-2 font-medium">
|
||||
{formatDate(gracePeriodEndsAt)}
|
||||
</strong>
|
||||
</>
|
||||
)}
|
||||
{plan === "free" && !inGracePeriod && limit !== null && (
|
||||
<>
|
||||
<strong
|
||||
className={cn(
|
||||
"tabular-nums font-medium",
|
||||
limitReached ? "text-rubis-deep" : "text-ink-2",
|
||||
)}
|
||||
>
|
||||
{activeInvoicesCount} / {limit}
|
||||
</strong>{" "}
|
||||
factures actives
|
||||
{limitReached && " — limite atteinte"}
|
||||
</>
|
||||
)}
|
||||
{plan !== "free" && state.currentPeriodEnd && (
|
||||
<>
|
||||
Renouvellement le{" "}
|
||||
<strong className="text-ink-2 font-medium">
|
||||
{formatDate(state.currentPeriodEnd)}
|
||||
</strong>
|
||||
{state.billingCycle === "yearly" ? " (annuel)" : " (mensuel)"}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{plan === "free" && inGracePeriod && gracePeriodEndsAt && (
|
||||
<p className="mt-1 text-[12.5px] text-rubis-deep">
|
||||
<Sparkles size={12} className="inline mr-1" aria-hidden="true" />
|
||||
Période de grâce — illimité jusqu'au{" "}
|
||||
<strong className="font-semibold">
|
||||
{formatDate(gracePeriodEndsAt)}
|
||||
</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{state.hasStripeCustomer && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={onOpenPortal}
|
||||
loading={isOpeningPortal}
|
||||
>
|
||||
<CreditCard size={14} aria-hidden="true" /> Gérer
|
||||
</Button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Compteur de factures actives — visible pour Free uniquement */}
|
||||
{isLimited && limit !== null && (
|
||||
<div>
|
||||
<div className="flex items-baseline justify-between mb-1">
|
||||
<p className="text-[12.5px] text-ink-3">
|
||||
Factures actives en relance
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"text-[12.5px] font-semibold tabular-nums",
|
||||
limitReached ? "text-rubis-deep" : "text-ink-2",
|
||||
)}
|
||||
>
|
||||
{activeInvoicesCount} / {limit}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-1.5 w-full rounded-full bg-cream-2 overflow-hidden">
|
||||
{/* Mini progress bar mobile-OK pour Free */}
|
||||
{isLimited && limit !== null && !inGracePeriod && (
|
||||
<div className="hidden sm:block w-32 shrink-0">
|
||||
<div className="h-1 w-full rounded-full bg-cream-2 overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-[width] duration-300",
|
||||
@ -234,19 +217,25 @@ function CurrentPlanCard({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{limitReached && (
|
||||
<p className="mt-2 text-[12.5px] text-rubis-deep font-medium">
|
||||
Limite atteinte — passez Pro pour ajouter de nouvelles factures.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{state.hasStripeCustomer && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onOpenPortal}
|
||||
loading={isOpeningPortal}
|
||||
>
|
||||
<CreditCard size={13} aria-hidden="true" /> Gérer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Toggle cycle
|
||||
// Toggle cycle — mensuel par défaut, annuel à droite avec −17%
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function CycleToggle({
|
||||
@ -295,148 +284,221 @@ function CycleToggle({
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cards plans (Free / Pro / Business)
|
||||
// PlanFree — petite card sobre. Pas de pricing affiché en gros (= 0 €).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PRICES: Record<
|
||||
PlanKey,
|
||||
{ monthly: number | null; yearly: number | null }
|
||||
> = {
|
||||
free: { monthly: 0, yearly: 0 },
|
||||
pro: { monthly: 19, yearly: 190 },
|
||||
business: { monthly: 49, yearly: 490 },
|
||||
};
|
||||
|
||||
const FEATURES_BY_PLAN: Record<PlanKey, string[]> = {
|
||||
free: [
|
||||
"5 factures actives en relance",
|
||||
"1 utilisateur",
|
||||
"Plans de relance fournis",
|
||||
"OCR illimité (3 premiers mois)",
|
||||
],
|
||||
pro: [
|
||||
"Factures illimitées",
|
||||
"OCR illimité",
|
||||
"Plans custom + IA générative",
|
||||
"Toutes les automatisations",
|
||||
"1 utilisateur",
|
||||
],
|
||||
business: [
|
||||
"Tout le plan Pro",
|
||||
"Réponses depuis votre email pro",
|
||||
"5 sièges utilisateurs",
|
||||
"Support prioritaire",
|
||||
"SMS (à venir)",
|
||||
],
|
||||
};
|
||||
|
||||
function PlanCard({
|
||||
plan,
|
||||
currentPlan,
|
||||
cycle,
|
||||
highlight = false,
|
||||
ctaDisabled = false,
|
||||
ctaLabel,
|
||||
loading = false,
|
||||
onUpgrade,
|
||||
}: {
|
||||
plan: PlanKey;
|
||||
currentPlan?: PlanKey;
|
||||
cycle: BillingCycle;
|
||||
highlight?: boolean;
|
||||
ctaDisabled?: boolean;
|
||||
ctaLabel?: string;
|
||||
loading?: boolean;
|
||||
onUpgrade?: () => void;
|
||||
}) {
|
||||
const isCurrent = currentPlan === plan;
|
||||
const price = PRICES[plan][cycle];
|
||||
const features = FEATURES_BY_PLAN[plan];
|
||||
const planIcon =
|
||||
plan === "pro" ? <Zap size={16} /> : plan === "business" ? <Users size={16} /> : null;
|
||||
|
||||
function PlanFree({ current }: { current: boolean }) {
|
||||
return (
|
||||
<Card
|
||||
padding="md"
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
highlight && !isCurrent && "border-rubis shadow-card",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{planIcon && <span className="text-rubis">{planIcon}</span>}
|
||||
<p className="font-display text-[18px] font-bold text-ink">
|
||||
{planLabel(plan)}
|
||||
</p>
|
||||
{highlight && !isCurrent && (
|
||||
<span className="ml-auto text-[10px] uppercase tracking-[0.12em] font-semibold text-rubis bg-rubis-glow rounded-full px-2 py-0.5">
|
||||
Recommandé
|
||||
</span>
|
||||
)}
|
||||
{isCurrent && (
|
||||
<span className="ml-auto text-[10px] uppercase tracking-[0.12em] font-semibold text-ink-3">
|
||||
Actuel
|
||||
</span>
|
||||
)}
|
||||
<div className="rounded-card border border-line bg-cream-2/40 p-6 flex flex-col">
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<p className="font-display text-[16px] font-bold text-ink">Découverte</p>
|
||||
{current && <CurrentBadge />}
|
||||
</div>
|
||||
<p className="text-[11.5px] uppercase tracking-[0.12em] text-ink-3 font-semibold mb-5">
|
||||
Gratuit
|
||||
</p>
|
||||
|
||||
<div className="mt-2 mb-5">
|
||||
<p className="font-display tabular-nums">
|
||||
<span className="text-[36px] font-bold tracking-[-0.02em] text-ink">
|
||||
{price === 0 ? "0" : price}
|
||||
</span>
|
||||
<span className="text-[14px] font-medium text-ink-3 ml-1">
|
||||
{cycle === "monthly" ? "€/mois" : "€/an"}
|
||||
</span>
|
||||
</p>
|
||||
{price !== null && price > 0 && cycle === "yearly" && (
|
||||
<p className="text-[11.5px] text-ink-3">
|
||||
soit {(price / 12).toFixed(2).replace(".", ",")} €/mois
|
||||
</p>
|
||||
)}
|
||||
{plan === "free" && (
|
||||
<p className="text-[11.5px] text-ink-3">3 mois illimités, puis 5 factures actives</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="font-display text-[28px] font-bold leading-none text-ink mb-1">
|
||||
0 €
|
||||
</p>
|
||||
<p className="text-[11.5px] text-ink-3 mb-5">3 mois illimités, puis 5 factures actives</p>
|
||||
|
||||
<ul className="flex flex-col gap-2 mb-6 flex-1">
|
||||
{features.map((f) => (
|
||||
<li
|
||||
key={f}
|
||||
className="flex items-start gap-2 text-[13px] text-ink-2 leading-snug"
|
||||
>
|
||||
<Check size={13} className="text-rubis shrink-0 mt-0.5" aria-hidden="true" />
|
||||
<span>{f}</span>
|
||||
</li>
|
||||
))}
|
||||
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug">
|
||||
<FeatureLine>Tester sans poser sa CB</FeatureLine>
|
||||
<FeatureLine>Plans de relance fournis</FeatureLine>
|
||||
<FeatureLine>OCR + automatisations 3 mois</FeatureLine>
|
||||
<FeatureLine>Au-delà : 5 factures à la fois</FeatureLine>
|
||||
</ul>
|
||||
|
||||
{!ctaDisabled && onUpgrade ? (
|
||||
<Button
|
||||
size="md"
|
||||
variant={highlight ? "primary" : "secondary"}
|
||||
loading={loading}
|
||||
disabled={isCurrent}
|
||||
onClick={onUpgrade}
|
||||
className="w-full"
|
||||
>
|
||||
{isCurrent ? (
|
||||
"Plan actuel"
|
||||
) : (
|
||||
<>
|
||||
Passer {planLabel(plan)} <ArrowRight size={14} aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="md" variant="ghost" disabled className="w-full">
|
||||
{ctaLabel ?? "—"}
|
||||
</Button>
|
||||
)}
|
||||
<p className="mt-auto pt-6 text-[11px] italic text-ink-3 leading-snug">
|
||||
Tout ce qu'il faut pour comprendre l'outil. Vous prendrez Pro
|
||||
quand votre volume mensuel dépasse 5 factures à relancer.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PlanProHero — la pièce maîtresse. Bg cream + gem en filigrane, prix XL.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function PlanProHero({
|
||||
current,
|
||||
cycle,
|
||||
loading,
|
||||
onUpgrade,
|
||||
}: {
|
||||
current: boolean;
|
||||
cycle: BillingCycle;
|
||||
loading: boolean;
|
||||
onUpgrade: () => void;
|
||||
}) {
|
||||
const monthlyPrice = cycle === "monthly" ? 19 : Math.round((190 / 12) * 100) / 100;
|
||||
const totalLabel = cycle === "monthly" ? "facturé chaque mois" : "soit 190 € facturés annuellement";
|
||||
|
||||
return (
|
||||
<Card padding="none" className="relative overflow-hidden flex flex-col p-7 border-rubis shadow-card">
|
||||
{/* Gem en filigrane, bas-droite, très large pour devenir un motif */}
|
||||
<Gem
|
||||
aria-hidden="true"
|
||||
size={280}
|
||||
className="absolute -bottom-16 -right-16 text-rubis-glow opacity-70 pointer-events-none"
|
||||
/>
|
||||
|
||||
{/* Pastille "Recommandé" */}
|
||||
<div className="relative flex items-center gap-2 mb-3">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-rubis text-white px-2.5 py-0.5 text-[10.5px] font-bold uppercase tracking-[0.12em]">
|
||||
<Gem size={10} className="text-white" />
|
||||
Le plus pris
|
||||
</span>
|
||||
{current && <CurrentBadge />}
|
||||
</div>
|
||||
|
||||
<p className="relative font-display text-[24px] font-bold tracking-[-0.018em] text-ink leading-tight">
|
||||
Pour les TPE qui n'ont plus jamais à y penser
|
||||
</p>
|
||||
|
||||
{/* Bloc prix — affiché de manière narrative */}
|
||||
<div className="relative mt-5 mb-5">
|
||||
<p className="font-display flex items-baseline">
|
||||
<span className="text-[44px] font-bold tabular-nums tracking-[-0.025em] text-ink leading-none">
|
||||
{monthlyPrice.toString().replace(".", ",")} €
|
||||
</span>
|
||||
<span className="ml-2 text-[13px] font-medium text-ink-3">/ mois</span>
|
||||
</p>
|
||||
<p className="mt-1 text-[12px] text-ink-3">{totalLabel}</p>
|
||||
</div>
|
||||
|
||||
{/* Liste de bénéfices, formulés en valeur (pas en feature spec) */}
|
||||
<ul className="relative flex flex-col gap-2.5 text-[13px] text-ink-2 leading-snug mb-7">
|
||||
<FeatureLine strong>Factures et relances illimitées</FeatureLine>
|
||||
<FeatureLine>OCR sans limite de volume</FeatureLine>
|
||||
<FeatureLine>Plans custom + génération IA des relances</FeatureLine>
|
||||
<FeatureLine>Mode démo pour montrer l'outil aux prospects</FeatureLine>
|
||||
<FeatureLine>Toutes les automatisations</FeatureLine>
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
loading={loading}
|
||||
disabled={current}
|
||||
onClick={onUpgrade}
|
||||
className="relative w-full"
|
||||
>
|
||||
{current ? (
|
||||
"Plan actuel"
|
||||
) : (
|
||||
<>
|
||||
Passer Pro <ArrowRight size={14} aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<p className="relative mt-3 text-[11px] italic text-ink-3 text-center leading-snug">
|
||||
Moins cher qu'une heure de votre temps mensuel.
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function planLabel(plan: PlanKey): string {
|
||||
return plan === "free" ? "Free" : plan === "pro" ? "Pro" : "Business";
|
||||
// ---------------------------------------------------------------------------
|
||||
// PlanBusiness — card sobre comme Free mais avec accents rubis-deep.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function PlanBusiness({
|
||||
current,
|
||||
cycle,
|
||||
loading,
|
||||
onUpgrade,
|
||||
}: {
|
||||
current: boolean;
|
||||
cycle: BillingCycle;
|
||||
loading: boolean;
|
||||
onUpgrade: () => void;
|
||||
}) {
|
||||
const monthlyPrice = cycle === "monthly" ? 49 : Math.round((490 / 12) * 100) / 100;
|
||||
const totalLabel = cycle === "monthly" ? "facturé chaque mois" : "soit 490 € facturés annuellement";
|
||||
|
||||
return (
|
||||
<div className="rounded-card border border-line bg-white p-6 flex flex-col">
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<p className="font-display text-[16px] font-bold text-ink">Équipe</p>
|
||||
{current && <CurrentBadge />}
|
||||
</div>
|
||||
<p className="text-[11.5px] uppercase tracking-[0.12em] text-rubis-deep font-semibold mb-5">
|
||||
Business
|
||||
</p>
|
||||
|
||||
<p className="font-display flex items-baseline mb-1">
|
||||
<span className="text-[28px] font-bold tabular-nums tracking-[-0.02em] text-ink leading-none">
|
||||
{monthlyPrice.toString().replace(".", ",")} €
|
||||
</span>
|
||||
<span className="ml-1.5 text-[12px] font-medium text-ink-3">/ mois</span>
|
||||
</p>
|
||||
<p className="text-[11.5px] text-ink-3 mb-5">{totalLabel}</p>
|
||||
|
||||
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug">
|
||||
<FeatureLine strong>Tout Pro, plus :</FeatureLine>
|
||||
<FeatureLine>5 sièges utilisateurs</FeatureLine>
|
||||
<FeatureLine>Réponses depuis votre email pro</FeatureLine>
|
||||
<FeatureLine>Support prioritaire</FeatureLine>
|
||||
<FeatureLine subtle>SMS (à venir)</FeatureLine>
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
loading={loading}
|
||||
disabled={current}
|
||||
onClick={onUpgrade}
|
||||
className="mt-auto pt-6 w-full"
|
||||
>
|
||||
{current ? "Plan actuel" : "Passer Business"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Atomes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function FeatureLine({
|
||||
children,
|
||||
strong = false,
|
||||
subtle = false,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
strong?: boolean;
|
||||
subtle?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<li className="flex items-start gap-2">
|
||||
{/* Petit losange rubis = bullet maison cohérent avec la marque */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"mt-1.5 size-[5px] rotate-45 shrink-0",
|
||||
subtle ? "bg-ink-3/40" : "bg-rubis",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
strong && "font-semibold text-ink",
|
||||
subtle && "text-ink-3 italic",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function CurrentBadge() {
|
||||
return (
|
||||
<span className="text-[10px] uppercase tracking-[0.14em] font-semibold text-rubis bg-rubis-glow rounded-full px-2 py-0.5">
|
||||
Actuel
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user