feat: production deployment - full update with providers, admin, glossaries, pricing, tests

Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-25 15:01:47 +02:00
parent 2ba4fedfc8
commit 26bd096a06
1178 changed files with 136435 additions and 3047 deletions

View File

@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { API_BASE } from "@/lib/config";
import {
Crown, Zap, Sparkles, Building2, Rocket, BadgeCheck,
ArrowRight, AlertTriangle, CheckCircle2, XCircle,
@@ -99,24 +100,24 @@ function UsageBar({
return (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2 text-gray-300">
<div className="flex items-center gap-2 text-muted-foreground">
{icon}
{label}
</div>
<span className={cn(
"font-mono text-xs",
isUnlimited ? "text-emerald-400" :
p >= 90 ? "text-red-400" : p >= 70 ? "text-amber-400" : "text-gray-400"
isUnlimited ? "text-emerald-500" :
p >= 90 ? "text-destructive" : p >= 70 ? "text-warning" : "text-muted-foreground"
)}>
{isUnlimited ? "∞" : `${used} / ${limit}`}
</span>
</div>
{!isUnlimited && (
<div className="h-1.5 bg-gray-700/50 rounded-full overflow-hidden">
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
<div
className={cn(
"h-full rounded-full transition-all duration-700",
p >= 90 ? "bg-red-500" : p >= 70 ? "bg-amber-500" : "bg-violet-500"
p >= 90 ? "bg-destructive" : p >= 70 ? "bg-warning" : "bg-accent"
)}
style={{ width: `${p}%` }}
/>
@@ -142,6 +143,7 @@ export default function SubscriptionPage() {
const [cancelConfirm, setCancelConfirm] = useState(false);
const [statusMsg, setStatusMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null);
const [loading, setLoading] = useState(true);
const [isProcessingCheckout, setIsProcessingCheckout] = useState(false);
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
const authHeaders = { Authorization: `Bearer ${token}` };
@@ -150,9 +152,9 @@ export default function SubscriptionPage() {
if (!token) { router.push("/auth/login?redirect=/settings/subscription"); return; }
try {
const [meRes, usageRes, plansRes] = await Promise.all([
fetch("/api/v1/auth/me", { headers: authHeaders }),
fetch("/api/v1/auth/usage", { headers: authHeaders }),
fetch("/api/v1/auth/plans"),
fetch(`${API_BASE}/api/v1/auth/me`, { headers: authHeaders }),
fetch(`${API_BASE}/api/v1/auth/usage`, { headers: authHeaders }),
fetch(`${API_BASE}/api/v1/auth/plans`),
]);
if (meRes.ok) {
const j = await meRes.json();
@@ -174,12 +176,13 @@ export default function SubscriptionPage() {
}
}, [token]);
useEffect(() => { fetchData(); }, [fetchData]);
const handleBillingPortal = async () => {
setLoadingPortal(true);
try {
const res = await fetch("/api/v1/auth/billing-portal", { headers: authHeaders });
const res = await fetch(`${API_BASE}/api/v1/auth/billing-portal`, { headers: authHeaders });
const j = await res.json();
const url = j.data?.url ?? j.url;
if (url) window.open(url, "_blank");
@@ -194,7 +197,7 @@ export default function SubscriptionPage() {
const handleCancel = async () => {
if (!cancelConfirm) { setCancelConfirm(true); return; }
try {
const res = await fetch("/api/v1/auth/cancel-subscription", {
const res = await fetch(`${API_BASE}/api/v1/auth/cancel-subscription`, {
method: "POST",
headers: authHeaders,
});
@@ -210,21 +213,84 @@ export default function SubscriptionPage() {
}
};
const handleSubscribe = (planId: string) => {
const handleSubscribe = async (planId: string) => {
if (planId === "enterprise") {
window.location.href = "mailto:contact@votre-domaine.com?subject=Offre Enterprise";
return;
}
// In a real app, this would initiate a Stripe Checkout session
// For now, redirect to billing portal or show info
setStatusMsg({
type: "ok",
text: `Redirection vers Stripe pour activer le forfait ${PLAN_LABELS[planId] ?? planId}`,
text: `Création de votre session de paiement pour le forfait ${PLAN_LABELS[planId] ?? planId}`,
});
setTimeout(() => handleBillingPortal(), 1000);
try {
const res = await fetch(`${API_BASE}/api/v1/auth/create-checkout`, {
method: "POST",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify({ plan: planId, billing_period: isYearly ? "yearly" : "monthly" }),
});
const data = await res.json();
if (!res.ok) {
const msg = data.message ?? data.error ?? "Erreur lors de la création de la session de paiement.";
setStatusMsg({ type: "err", text: msg });
return;
}
const url = data.data?.url ?? data.url;
if (url) {
window.location.replace(url);
} else {
await handleBillingPortal();
}
} catch {
setStatusMsg({ type: "err", text: "Erreur réseau lors de la création de la session de paiement." });
} finally {
setIsProcessingCheckout(false);
}
};
if (loading) {
// Handle targetPlan from URL query param automatically (MUST be after handleSubscribe declaration)
useEffect(() => {
if (targetPlan && plans.length > 0 && !loading && !isProcessingCheckout) {
const p = plans.find(plan => plan.id === targetPlan);
if (p && user?.plan !== targetPlan) {
setIsProcessingCheckout(true);
// Clean URL to prevent infinite re-triggering
router.replace("/settings/subscription", { scroll: false });
handleSubscribe(targetPlan);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetPlan, plans, loading, user]);
const handleCreditsCheckout = async (packageIndex: number) => {
setStatusMsg({
type: "ok",
text: "Préparation de l'achat de crédits...",
});
try {
const res = await fetch(`${API_BASE}/api/v1/auth/create-credits-checkout`, {
method: "POST",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify({ package_index: packageIndex }),
});
const data = await res.json();
const url = data.data?.url ?? data.url;
if (url) {
window.location.replace(url);
} else {
setTimeout(() => handleBillingPortal(), 1000);
}
} catch {
setStatusMsg({ type: "err", text: "Erreur lors de la création de la session de paiement." });
}
};
if (loading || isProcessingCheckout) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<RefreshCw className="w-8 h-8 text-violet-400 animate-spin" />
@@ -251,8 +317,8 @@ export default function SubscriptionPage() {
return (
<div className="max-w-4xl mx-auto px-4 py-10 space-y-8">
<div>
<h1 className="text-3xl font-bold text-white">Mon abonnement</h1>
<p className="text-gray-400 mt-1">Gérez votre forfait, votre usage et votre facturation.</p>
<h1 className="text-3xl font-bold text-foreground">Mon abonnement</h1>
<p className="text-muted-foreground mt-1">Gérez votre forfait, votre usage et votre facturation.</p>
</div>
{/* Status message */}
@@ -260,20 +326,20 @@ export default function SubscriptionPage() {
<div className={cn(
"flex items-start gap-3 p-4 rounded-xl border",
statusMsg.type === "ok"
? "bg-emerald-900/20 border-emerald-600/30 text-emerald-300"
: "bg-red-900/20 border-red-600/30 text-red-300"
? "bg-success/10 border-success/30 text-success"
: "bg-destructive/10 border-destructive/30 text-destructive"
)}>
{statusMsg.type === "ok"
? <CheckCircle2 className="w-5 h-5 flex-shrink-0 mt-0.5" />
: <XCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />}
<span className="text-sm">{statusMsg.text}</span>
<button className="ml-auto text-gray-500 hover:text-white" onClick={() => setStatusMsg(null)}></button>
<button className="ml-auto text-muted-foreground hover:text-foreground" onClick={() => setStatusMsg(null)}></button>
</div>
)}
{/* ── Current plan card ── */}
<div className={cn("rounded-2xl p-1 bg-gradient-to-br", gradient)}>
<div className="bg-gray-900/90 rounded-xl p-6">
<div className="bg-card/95 rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className={cn("p-3 rounded-xl bg-gradient-to-br", gradient)}>
@@ -281,13 +347,13 @@ export default function SubscriptionPage() {
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold text-white">Forfait {currentPlanLabel}</h2>
<h2 className="text-xl font-bold text-foreground">Forfait {currentPlanLabel}</h2>
{user?.subscription_status && (
<Badge className={cn(
"text-xs",
user.subscription_status === "active" ? "bg-emerald-600/20 text-emerald-300 border-emerald-600/30" :
user.subscription_status === "trialing" ? "bg-blue-600/20 text-blue-300 border-blue-600/30" :
"bg-amber-600/20 text-amber-300 border-amber-600/30"
user.subscription_status === "active" ? "bg-success/10 text-success border-success/30" :
user.subscription_status === "trialing" ? "bg-primary/10 text-primary border-primary/30" :
"bg-warning/10 text-warning border-warning/30"
)}>
{user.subscription_status === "active" ? "Actif" :
user.subscription_status === "trialing" ? "Essai" :
@@ -297,7 +363,7 @@ export default function SubscriptionPage() {
)}
</div>
{user?.subscription_ends_at && (
<p className="text-gray-400 text-sm mt-0.5">
<p className="text-muted-foreground text-sm mt-0.5">
<Calendar className="w-3.5 h-3.5 inline mr-1" />
{user.cancel_at_period_end ? "Expire le " : "Renouvellement le "}
{new Date(user.subscription_ends_at).toLocaleDateString("fr-FR", { day: "2-digit", month: "long", year: "numeric" })}
@@ -309,7 +375,6 @@ export default function SubscriptionPage() {
{currentPlanId !== "free" && (
<Button
variant="outline"
className="border-gray-600 text-gray-300 hover:bg-gray-700/30"
onClick={handleBillingPortal}
disabled={loadingPortal}
>
@@ -320,7 +385,7 @@ export default function SubscriptionPage() {
)}
{upgradePlans.length > 0 && (
<Button
className="bg-violet-600 hover:bg-violet-500"
className="bg-accent hover:bg-accent/90 text-accent-foreground"
onClick={() => router.push("/pricing")}
>
<TrendingUp className="w-4 h-4 mr-1" /> Upgrader
@@ -333,12 +398,12 @@ export default function SubscriptionPage() {
{/* ── Usage this month ── */}
{usage && (
<Card className="bg-gray-900/60 border-gray-700/40">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-violet-400" />
<BarChart3 className="w-5 h-5 text-accent" />
Utilisation ce mois
<span className="ml-auto text-xs text-gray-500 font-normal">Remise à zéro chaque 1er du mois</span>
<span className="ml-auto text-xs text-muted-foreground font-normal">Remise à zéro chaque 1er du mois</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-5">
@@ -346,37 +411,37 @@ export default function SubscriptionPage() {
label="Documents traduits"
used={usage.docs_used}
limit={usage.docs_limit}
icon={<FileText className="w-4 h-4 text-gray-400" />}
icon={<FileText className="w-4 h-4 text-muted-foreground" />}
/>
<UsageBar
label="Pages traduites"
used={usage.pages_used}
limit={usage.pages_limit}
icon={<Layers className="w-4 h-4 text-gray-400" />}
icon={<Layers className="w-4 h-4 text-muted-foreground" />}
/>
{usage.api_calls_limit !== 0 && (
<UsageBar
label="Appels API"
used={usage.api_calls_used}
limit={usage.api_calls_limit}
icon={<Gauge className="w-4 h-4 text-gray-400" />}
icon={<Gauge className="w-4 h-4 text-muted-foreground" />}
/>
)}
{usage.extra_credits > 0 && (
<div className="flex items-center gap-3 p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 text-sm">
<Info className="w-4 h-4 text-amber-400 flex-shrink-0" />
<span className="text-amber-300">
<div className="flex items-center gap-3 p-3 rounded-xl bg-warning/10 border border-warning/20 text-sm">
<Info className="w-4 h-4 text-warning flex-shrink-0" />
<span className="text-warning">
{usage.extra_credits} crédit{usage.extra_credits > 1 ? "s" : ""} supplémentaire{usage.extra_credits > 1 ? "s" : ""} disponible{usage.extra_credits > 1 ? "s" : ""}
</span>
</div>
)}
{usage.upgrade_required && (
<div className="flex items-center gap-3 p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-sm">
<AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
<span className="text-red-300">
<div className="flex items-center gap-3 p-3 rounded-xl bg-destructive/10 border border-destructive/20 text-sm">
<AlertTriangle className="w-4 h-4 text-destructive flex-shrink-0" />
<span className="text-destructive">
Quota atteint. Achetez des crédits ou upgradez votre forfait pour continuer.
</span>
<Button size="sm" className="ml-auto bg-red-600 hover:bg-red-500 flex-shrink-0" onClick={() => router.push("/pricing")}>
<Button size="sm" className="ml-auto bg-destructive hover:bg-destructive/90 text-destructive-foreground flex-shrink-0" onClick={() => router.push("/pricing")}>
Upgrader
</Button>
</div>
@@ -387,10 +452,10 @@ export default function SubscriptionPage() {
{/* ── Plan features recap ── */}
{currentPlanData && (
<Card className="bg-gray-900/60 border-gray-700/40">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<BadgeCheck className="w-5 h-5 text-emerald-400" />
<BadgeCheck className="w-5 h-5 text-success" />
Inclus dans votre forfait
</CardTitle>
</CardHeader>
@@ -398,8 +463,8 @@ export default function SubscriptionPage() {
<div className="grid sm:grid-cols-2 gap-2">
{currentPlanData.features.map((f, i) => (
<div key={i} className="flex items-start gap-2 text-sm">
<CheckCircle2 className="w-4 h-4 text-emerald-400 flex-shrink-0 mt-0.5" />
<span className="text-gray-300">{f}</span>
<CheckCircle2 className="w-4 h-4 text-success flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground">{f}</span>
</div>
))}
</div>
@@ -410,23 +475,23 @@ export default function SubscriptionPage() {
{/* ── Upgrade options ── */}
{upgradePlans.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-violet-400" />
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-accent" />
Passer à un forfait supérieur
</h3>
{/* Billing toggle */}
<div className="inline-flex items-center gap-2 bg-gray-800/60 border border-gray-700/50 rounded-full p-1 mb-4">
<div className="inline-flex items-center gap-2 bg-muted/60 border border-border/50 rounded-full p-1 mb-4">
<button
onClick={() => setIsYearly(false)}
className={cn("px-4 py-1.5 rounded-full text-xs font-medium transition-all", !isYearly ? "bg-white text-gray-900" : "text-gray-400")}
className={cn("px-4 py-1.5 rounded-full text-xs font-medium transition-all", !isYearly ? "bg-foreground text-background" : "text-muted-foreground")}
>Mensuel</button>
<button
onClick={() => setIsYearly(true)}
className={cn("px-4 py-1.5 rounded-full text-xs font-medium transition-all flex items-center gap-1.5", isYearly ? "bg-white text-gray-900" : "text-gray-400")}
className={cn("px-4 py-1.5 rounded-full text-xs font-medium transition-all flex items-center gap-1.5", isYearly ? "bg-foreground text-background" : "text-muted-foreground")}
>
Annuel
<span className="bg-emerald-500 text-white text-xs px-1.5 py-0.5 rounded-full">20 %</span>
<span className="bg-success text-success-foreground text-xs px-1.5 py-0.5 rounded-full">20 %</span>
</button>
</div>
@@ -444,13 +509,13 @@ export default function SubscriptionPage() {
<div
key={plan.id}
className={cn(
"relative rounded-2xl border bg-gray-900/60 overflow-hidden",
plan.popular ? "border-violet-500/50" : "border-gray-700/40"
"relative rounded-2xl border bg-card overflow-hidden",
plan.popular ? "border-accent/50" : "border-border/40"
)}
>
{plan.badge && (
<div className="absolute top-3 right-3">
<Badge className="bg-violet-600 text-white text-xs">{plan.badge}</Badge>
<Badge className="bg-accent text-accent-foreground text-xs">{plan.badge}</Badge>
</div>
)}
<div className={cn("p-4 bg-gradient-to-br", grad)}>
@@ -471,13 +536,13 @@ export default function SubscriptionPage() {
</div>
<div className="p-4 space-y-2">
{plan.features.slice(0, 4).map((f, i) => (
<div key={i} className="flex items-start gap-2 text-xs text-gray-300">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-400 flex-shrink-0 mt-0.5" />
<div key={i} className="flex items-start gap-2 text-xs text-muted-foreground">
<CheckCircle2 className="w-3.5 h-3.5 text-success flex-shrink-0 mt-0.5" />
{f}
</div>
))}
{plan.features.length > 4 && (
<p className="text-xs text-gray-500">+{plan.features.length - 4} autres avantages</p>
<p className="text-xs text-muted-foreground">+{plan.features.length - 4} autres avantages</p>
)}
<button
onClick={() => handleSubscribe(plan.id)}
@@ -497,13 +562,13 @@ export default function SubscriptionPage() {
)}
{/* ── Buy credits ── */}
<Card className="bg-gray-900/60 border-gray-700/40">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<CreditCard className="w-5 h-5 text-amber-400" />
<CreditCard className="w-5 h-5 text-warning" />
Crédits supplémentaires
</CardTitle>
<p className="text-gray-400 text-sm">1 crédit = 1 page traduite. Utilisables sans expiration.</p>
<p className="text-muted-foreground text-sm">1 crédit = 1 page traduite. Utilisables sans expiration.</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
@@ -518,22 +583,22 @@ export default function SubscriptionPage() {
className={cn(
"relative p-4 rounded-xl border text-center",
pkg.popular
? "border-amber-500/50 bg-amber-500/10"
: "border-gray-700/40 bg-gray-800/30"
? "border-warning/50 bg-warning/10"
: "border-border/40 bg-muted/20"
)}
>
{pkg.popular && (
<div className="absolute -top-2 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-amber-600 text-white text-xs rounded-full font-bold whitespace-nowrap">
<div className="absolute -top-2 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-warning text-warning-foreground text-xs rounded-full font-bold whitespace-nowrap">
Meilleure valeur
</div>
)}
<div className="text-xl font-bold text-white">{pkg.credits}</div>
<div className="text-gray-400 text-xs mb-2">crédits</div>
<div className="text-lg font-bold text-white">{pkg.price} </div>
<div className="text-gray-500 text-xs mb-3">{((pkg.price / pkg.credits) * 100).toFixed(0)} cts/crédit</div>
<div className="text-xl font-bold text-foreground">{pkg.credits}</div>
<div className="text-muted-foreground text-xs mb-2">crédits</div>
<div className="text-lg font-bold text-foreground">{pkg.price} </div>
<div className="text-muted-foreground text-xs mb-3">{((pkg.price / pkg.credits) * 100).toFixed(0)} cts/crédit</div>
<button
onClick={handleBillingPortal}
className="w-full py-1.5 rounded-lg bg-gray-700 hover:bg-gray-600 text-white text-xs transition-all"
onClick={() => handleCreditsCheckout(i)}
className="w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all"
>
Acheter
</button>
@@ -545,9 +610,9 @@ export default function SubscriptionPage() {
{/* ── Downgrade / Cancel ── */}
{currentPlanId !== "free" && (
<Card className="bg-gray-900/60 border-gray-700/40">
<Card className="border-destructive/20">
<CardHeader className="pb-3">
<CardTitle className="text-lg text-red-400 flex items-center gap-2">
<CardTitle className="text-lg text-destructive flex items-center gap-2">
<AlertTriangle className="w-5 h-5" />
Zone de danger
</CardTitle>
@@ -555,13 +620,13 @@ export default function SubscriptionPage() {
<CardContent className="space-y-4">
{downgradePlans.length > 0 && (
<div>
<p className="text-sm text-gray-400 mb-2">Rétrograder vers un forfait inférieur :</p>
<p className="text-sm text-muted-foreground mb-2">Rétrograder vers un forfait inférieur :</p>
<div className="flex flex-wrap gap-2">
{downgradePlans.map((p) => (
<button
key={p.id}
onClick={() => handleSubscribe(p.id)}
className="px-4 py-2 rounded-lg bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm border border-gray-700/50 transition-all"
className="px-4 py-2 rounded-lg bg-muted hover:bg-secondary text-muted-foreground text-sm border border-border/50 transition-all"
>
Passer à {p.name}
</button>
@@ -570,40 +635,39 @@ export default function SubscriptionPage() {
</div>
)}
<div className="border-t border-gray-700/30 pt-4">
<div className="border-t border-border/30 pt-4">
{!cancelConfirm ? (
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-white">Annuler mon abonnement</p>
<p className="text-xs text-gray-500 mt-0.5">Vous conservez l'accès jusqu'à la fin de la période payée.</p>
<p className="text-sm font-medium text-foreground">Annuler mon abonnement</p>
<p className="text-xs text-muted-foreground mt-0.5">Vous conservez l'accès jusqu'à la fin de la période payée.</p>
</div>
<Button
variant="outline"
className="border-red-700/50 text-red-400 hover:bg-red-900/20"
className="border-destructive/50 text-destructive hover:bg-destructive/10"
onClick={() => setCancelConfirm(true)}
>
Annuler l'abonnement
</Button>
</div>
) : (
<div className="p-4 rounded-xl bg-red-900/20 border border-red-600/30 space-y-3">
<p className="text-sm text-red-300 font-medium">
<div className="p-4 rounded-xl bg-destructive/10 border border-destructive/30 space-y-3">
<p className="text-sm text-destructive font-medium">
⚠️ Confirmer l'annulation ?
</p>
<p className="text-xs text-gray-400">
<p className="text-xs text-muted-foreground">
Votre abonnement sera annulé et vous reviendrez au forfait Gratuit à la fin de la période en cours.
Vos documents traduits resteront accessibles pendant 30 jours.
</p>
<div className="flex gap-2">
<Button
className="bg-red-600 hover:bg-red-500 text-white"
className="bg-destructive hover:bg-destructive/90 text-destructive-foreground"
onClick={handleCancel}
>
Oui, annuler mon abonnement
</Button>
<Button
variant="outline"
className="border-gray-600 text-gray-300"
onClick={() => setCancelConfirm(false)}
>
Non, conserver
@@ -620,7 +684,7 @@ export default function SubscriptionPage() {
<div className="text-center">
<button
onClick={() => router.push("/pricing")}
className="text-violet-400 hover:text-violet-300 text-sm flex items-center gap-1 mx-auto"
className="text-accent hover:text-accent/80 text-sm flex items-center gap-1 mx-auto"
>
Voir tous les forfaits en détail <ChevronRight className="w-4 h-4" />
</button>