queue-med/client/src/pages/SubscriptionPage.tsx

251 lines
9.6 KiB
TypeScript

import { useLocation } from "wouter";
import {
CreditCard, Check, Sparkles, Clock, Loader2, AlertTriangle,
TrendingUp, Crown, Heart,
} from "lucide-react";
import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
const PLANS = [
{
plan: "trial" as const,
name: "Essai",
price: "0€",
period: "30 jours",
description: "Découvrez QueueMed sans engagement.",
features: ["1 cabinet", "Patients illimités", "Statistiques de base", "Support email"],
icon: Sparkles,
color: "from-slate-500 to-slate-600",
},
{
plan: "basic" as const,
name: "Basic",
price: "29€",
period: "/ mois",
description: "Pour un cabinet individuel.",
features: ["1 cabinet", "Patients illimités", "Écran d'affichage", "Statistiques avancées", "Support prioritaire"],
icon: TrendingUp,
color: "from-emerald-500 to-teal-500",
highlighted: true,
},
{
plan: "pro" as const,
name: "Pro",
price: "79€",
period: "/ mois",
description: "Centres médicaux et multi-praticiens.",
features: ["Cabinets illimités", "Multi-praticiens", "Recommandations IA", "Export CSV", "Support téléphonique"],
icon: Crown,
color: "from-violet-500 to-fuchsia-500",
},
];
export default function SubscriptionPage() {
const [, navigate] = useLocation();
const subQuery = trpc.subscription.get.useQuery();
const checkQuery = trpc.subscription.check.useQuery();
const sub = subQuery.data;
const check = checkQuery.data;
const handleSubscribe = (plan: "basic" | "pro") => {
toast.info(`Redirection vers le paiement ${plan}`, {
description: "L'intégration Stripe sera activée prochainement.",
});
};
if (subQuery.isLoading) {
return (
<div className="container py-20 flex justify-center">
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" />
</div>
);
}
const isTrialing = sub?.status === "trialing";
const isActive = sub?.status === "active";
const daysLeft = check?.daysRemaining ?? 0;
const expired = !check?.active;
return (
<div className="container py-8">
<div className="mb-8">
<h1 className="font-bold text-3xl mb-1">Abonnement</h1>
<p className="text-slate-600">Gérez votre plan et votre période d'essai.</p>
</div>
{/* Current status */}
<div
className={`glass-card-strong rounded-3xl p-8 mb-8 relative overflow-hidden ${
expired ? "border-2 border-red-200" : ""
}`}
>
{!expired && (
<div
className="absolute -top-20 -right-20 w-72 h-72 rounded-full opacity-20 blur-3xl"
style={{
background:
isTrialing
? "linear-gradient(135deg, #fbbf24, #f97316)"
: "linear-gradient(135deg, #10b981, #06b6d4)",
}}
/>
)}
<div className="relative z-10 flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
<div>
<div className="text-xs uppercase tracking-widest text-emerald-700 font-bold mb-2">Plan actuel</div>
<div className="flex items-center gap-3 mb-2">
<h2 className="font-bold text-3xl capitalize">{sub?.plan ?? "—"}</h2>
<span
className={`px-3 py-1 rounded-full text-xs font-bold ${
expired
? "bg-red-100 text-red-700"
: isTrialing
? "bg-amber-100 text-amber-700"
: "bg-emerald-100 text-emerald-700"
}`}
>
{expired ? "Expiré" : isTrialing ? "Essai" : isActive ? "Actif" : sub?.status}
</span>
</div>
{expired ? (
<p className="text-red-600 text-sm flex items-center gap-1.5">
<AlertTriangle className="w-4 h-4" />
Votre abonnement est expiré. Renouvelez pour continuer à utiliser QueueMed.
</p>
) : (
<p className="text-slate-600 text-sm flex items-center gap-1.5">
<Clock className="w-4 h-4 text-emerald-600" />
{isTrialing ? "Essai gratuit" : "Prochain renouvellement"} dans <strong>{daysLeft} jour{daysLeft > 1 ? "s" : ""}</strong>
{sub?.trialEndsAt && isTrialing && (
<span> (jusqu'au {new Date(sub.trialEndsAt).toLocaleDateString("fr-FR")})</span>
)}
{sub?.currentPeriodEnd && isActive && (
<span> (jusqu'au {new Date(sub.currentPeriodEnd).toLocaleDateString("fr-FR")})</span>
)}
</p>
)}
</div>
{(isTrialing || expired) && (
<Button
variant="gradient"
size="lg"
onClick={() => handleSubscribe("basic")}
>
<Sparkles className="w-4 h-4 mr-2" />
S'abonner maintenant
</Button>
)}
</div>
{isTrialing && daysLeft > 0 && (
<div className="relative z-10 mt-6">
<div className="flex justify-between text-xs text-slate-500 mb-1.5">
<span>Jour {30 - daysLeft}</span>
<span>{daysLeft} jours restants</span>
</div>
<div className="h-2 rounded-full bg-slate-100 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-emerald-500 to-cyan-500 transition-all"
style={{ width: `${((30 - daysLeft) / 30) * 100}%` }}
/>
</div>
</div>
)}
</div>
{/* Plans grid */}
<h2 className="font-bold text-2xl mb-4">Choisissez votre plan</h2>
<div className="grid md:grid-cols-3 gap-6 mb-8">
{PLANS.map((p) => {
const Icon = p.icon;
const isCurrent = sub?.plan === p.plan;
return (
<div
key={p.plan}
className={`relative rounded-3xl p-8 transition-all ${
p.highlighted
? "bg-gradient-to-br from-emerald-500 to-cyan-500 text-white shadow-2xl md:scale-105"
: "glass-card-strong"
} ${isCurrent ? "ring-4 ring-emerald-300/60" : ""}`}
>
{p.highlighted && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full bg-orange-500 text-white text-xs font-bold uppercase tracking-wider shadow-md">
Populaire
</div>
)}
{isCurrent && (
<div className="absolute -top-3 right-4 px-3 py-1 rounded-full bg-white text-emerald-700 text-xs font-bold uppercase tracking-wider shadow-md">
Actuel
</div>
)}
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center mb-4 ${
p.highlighted ? "bg-white/30" : `bg-gradient-to-br ${p.color}`
}`}
>
<Icon className={`w-6 h-6 ${p.highlighted ? "text-white" : "text-white"}`} />
</div>
<h3 className={`font-bold text-2xl mb-1 ${p.highlighted ? "text-white" : "text-slate-900"}`}>{p.name}</h3>
<p className={`text-sm mb-4 ${p.highlighted ? "text-emerald-50" : "text-slate-500"}`}>{p.description}</p>
<div className="flex items-baseline gap-1 mb-6">
<span className={`font-black text-4xl ${p.highlighted ? "text-white" : "gradient-text"}`}>{p.price}</span>
<span className={`text-sm ${p.highlighted ? "text-emerald-100" : "text-slate-500"}`}>{p.period}</span>
</div>
<ul className="space-y-2.5 mb-6">
{p.features.map((f) => (
<li key={f} className="flex items-start gap-2 text-sm">
<Check className={`w-4 h-4 mt-0.5 flex-shrink-0 ${p.highlighted ? "text-white" : "text-emerald-500"}`} />
<span className={p.highlighted ? "text-white" : "text-slate-700"}>{f}</span>
</li>
))}
</ul>
{p.plan === "trial" ? (
<Button
variant="outline"
className="w-full"
disabled
>
{isCurrent ? "Plan actuel" : "Essai automatique"}
</Button>
) : isCurrent ? (
<Button
variant={p.highlighted ? "default" : "outline"}
className={`w-full ${p.highlighted ? "bg-white text-emerald-700 hover:bg-emerald-50" : ""}`}
disabled
>
Plan actuel
</Button>
) : (
<Button
className={`w-full ${
p.highlighted
? "bg-white text-emerald-700 hover:bg-emerald-50"
: "bg-teal-600 hover:bg-teal-700 text-white"
}`}
onClick={() => handleSubscribe(p.plan)}
>
S'abonner
</Button>
)}
</div>
);
})}
</div>
{/* Guarantee */}
<div className="glass-card rounded-2xl p-6 text-center">
<Heart className="w-8 h-8 text-rose-500 mx-auto mb-3" />
<h3 className="font-bold text-lg mb-1">Notre engagement</h3>
<p className="text-sm text-slate-600 max-w-2xl mx-auto">
Annulation à tout moment. Données hébergées en France. Conformité RGPD.
Migration et configuration assistées gratuites.
</p>
</div>
</div>
);
}