251 lines
9.6 KiB
TypeScript
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>
|
|
);
|
|
}
|