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,9 +2,9 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import {
Check, X, Zap, Building2, Crown, Sparkles, ArrowRight,
Check, X, Zap, Building2, Crown, Sparkles, ArrowRight, ArrowLeft,
Star, Shield, Rocket, Users, Headphones, Lock, Globe,
Clock, ChevronDown, ChevronUp, Cpu, BarChart3, Infinity,
FileText, Layers, Brain, BadgeCheck, Gauge
@@ -13,6 +13,8 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { API_BASE } from "@/lib/config";
import { ANNUAL_DISCOUNT_PERCENT } from "@/lib/pricing";
/* ─────────────────────────────────────────────
Types
@@ -76,8 +78,8 @@ const STATIC_PLANS: Plan[] = [
{
id: "starter",
name: "Starter",
price_monthly: 7.99,
price_yearly: 76.70,
price_monthly: 9.00,
price_yearly: 86.40,
docs_per_month: 50,
max_pages_per_doc: 50,
max_file_size_mb: 10,
@@ -100,8 +102,8 @@ const STATIC_PLANS: Plan[] = [
{
id: "pro",
name: "Pro",
price_monthly: 19.99,
price_yearly: 191.90,
price_monthly: 19.00,
price_yearly: 182.40,
docs_per_month: 200,
max_pages_per_doc: 200,
max_file_size_mb: 25,
@@ -129,8 +131,8 @@ const STATIC_PLANS: Plan[] = [
{
id: "business",
name: "Business",
price_monthly: 49.99,
price_yearly: 479.90,
price_monthly: 49.00,
price_yearly: 470.40,
docs_per_month: 1000,
max_pages_per_doc: 500,
max_file_size_mb: 50,
@@ -213,6 +215,47 @@ const PLAN_COLORS: Record<string, { gradient: string; border: string; badge: str
enterprise: { gradient: "from-amber-600 to-amber-700", border: "border-amber-700/50", badge: "bg-amber-600", button: "bg-amber-600 hover:bg-amber-500" },
};
/** Évite le flash des tarifs statiques (STATIC_PLANS) avant la réponse API au refresh. */
function PricingDataSkeleton() {
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-5">
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="rounded-2xl border border-border/40 bg-card overflow-hidden animate-pulse"
>
<div className="h-28 bg-muted/60" />
<div className="p-5 space-y-3">
<div className="h-4 bg-muted rounded w-3/4" />
<div className="h-4 bg-muted rounded w-1/2" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-5/6" />
<div className="h-9 bg-muted rounded-lg mt-4" />
</div>
</div>
))}
</div>
<div className="mt-20">
<div className="h-9 bg-muted rounded w-64 mx-auto mb-4 animate-pulse" />
<div className="h-4 bg-muted rounded w-96 max-w-full mx-auto mb-10 animate-pulse" />
<div className="overflow-x-auto rounded-2xl border border-border/40">
<div className="h-64 bg-muted/30 animate-pulse rounded-xl" />
</div>
</div>
<div className="mt-20">
<div className="h-9 bg-muted rounded w-72 mx-auto mb-4 animate-pulse" />
<div className="h-4 bg-muted rounded w-full max-w-lg mx-auto mb-8 animate-pulse" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-3xl mx-auto">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-36 rounded-2xl border border-border/40 bg-muted/20 animate-pulse" />
))}
</div>
</div>
</>
);
}
const FAQS = [
{
q: "Puis-je changer de forfait à tout moment ?",
@@ -249,27 +292,53 @@ const FAQS = [
───────────────────────────────────────────── */
export default function PricingPage() {
const router = useRouter();
const searchParams = useSearchParams();
const planFromUrl = searchParams.get("plan");
const [isYearly, setIsYearly] = useState(false);
const [plans, setPlans] = useState<Plan[]>(STATIC_PLANS);
const [credits, setCredits] = useState<CreditPackage[]>(STATIC_CREDITS);
const [currentPlan, setCurrentPlan] = useState<string | null>(null);
const [openFAQ, setOpenFAQ] = useState<number | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loadingPlanId, setLoadingPlanId] = useState<string | null>(null);
const [toastMsg, setToastMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [annualDiscountPercent, setAnnualDiscountPercent] = useState(ANNUAL_DISCOUNT_PERCENT);
/** Tant que false : on naffiche pas STATIC_PLANS (évite le flash de faux prix au refresh). */
const [pricingLoaded, setPricingLoaded] = useState(false);
useEffect(() => {
// Fetch live plans
fetch("/api/v1/auth/plans")
.then((r) => r.json())
.then((json) => {
const data = json.data ?? json;
if (Array.isArray(data.plans) && data.plans.length) setPlans(data.plans);
if (Array.isArray(data.credit_packages)) setCredits(data.credit_packages);
// Fetch live plans — backend returns prices set by admin (pas de cache navigateur)
const plansUrl = `${API_BASE}/api/v1/auth/plans`;
fetch(plansUrl, { cache: "no-store" })
.then(async (r) => {
if (!r.ok) {
throw new Error(`HTTP ${r.status}`);
}
return r.json();
})
.catch(() => {/* keep static fallback */});
.then((json) => {
const d = json.data ?? json;
const meta = (json.meta ?? d.meta) as { annual_discount_percent?: number } | undefined;
if (typeof meta?.annual_discount_percent === "number") {
setAnnualDiscountPercent(meta.annual_discount_percent);
}
if (Array.isArray(d.plans) && d.plans.length) setPlans(d.plans);
if (Array.isArray(d.credit_packages)) setCredits(d.credit_packages);
})
.catch((err) => {
if (process.env.NODE_ENV === "development") {
console.warn("[pricing] Impossible de charger /api/v1/auth/plans — affichage des tarifs par défaut.", err);
}
})
.finally(() => {
setPricingLoaded(true);
});
// Fetch current user plan (if logged in)
const token = localStorage.getItem("token");
if (token) {
fetch("/api/v1/auth/me", { headers: { Authorization: `Bearer ${token}` } })
setIsLoggedIn(true);
fetch(`${API_BASE}/api/v1/auth/me`, { headers: { Authorization: `Bearer ${token}` } })
.then((r) => r.json())
.then((json) => {
const user = json.data ?? json;
@@ -279,15 +348,27 @@ export default function PricingPage() {
}
}, []);
// Auto-trigger checkout when ?plan= is in the URL and user is already logged in
useEffect(() => {
if (!pricingLoaded) return;
if (planFromUrl && isLoggedIn && plans.length > 0 && currentPlan !== null) {
const targetPlan = plans.find(p => p.id === planFromUrl);
if (targetPlan && targetPlan.price_monthly > 0 && currentPlan !== planFromUrl) {
// Directly initiate checkout — no more redirect to /settings/subscription
handleSubscribe(planFromUrl);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [planFromUrl, isLoggedIn, plans, currentPlan, pricingLoaded]);
const displayPrice = (plan: Plan) => {
if (plan.price_monthly === -1) return null;
if (plan.price_monthly === 0) return 0;
return isYearly
? (plan.price_yearly / 12).toFixed(2)
: plan.price_monthly.toFixed(2);
const price = isYearly ? (plan.price_yearly / 12) : plan.price_monthly;
return Number.isInteger(price) ? price.toString() : price.toFixed(2);
};
const handleSubscribe = (planId: string) => {
const handleSubscribe = async (planId: string) => {
const token = localStorage.getItem("token");
if (!token) {
router.push(`/auth/login?redirect=/pricing`);
@@ -297,35 +378,113 @@ export default function PricingPage() {
window.location.href = "mailto:contact@votre-domaine.com?subject=Offre Enterprise";
return;
}
// Redirect to subscription management
router.push(`/settings/subscription?plan=${planId}`);
if (planId === currentPlan) return;
setLoadingPlanId(planId);
setToastMsg(null);
try {
const res = await fetch(`${API_BASE}/api/v1/auth/create-checkout`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
plan: planId,
billing_period: isYearly ? "yearly" : "monthly",
}),
});
const data = await res.json();
const url = data.data?.url ?? data.url;
const backendMessage = data.message ?? data.error ?? data.data?.error;
if (!res.ok) {
throw new Error(backendMessage || "Erreur lors de la création du paiement.");
}
if (url) {
window.location.replace(url);
} else {
// Demo mode: Stripe not yet configured
setToastMsg({
type: 'ok',
text: `Mode démo — Stripe n'est pas encore configuré. En production, vous seriez redirigé vers le paiement pour activer le forfait ${planId}.`,
});
}
} catch (error) {
const message = error instanceof Error ? error.message : "Erreur réseau. Veuillez réessayer.";
setToastMsg({ type: 'err', text: message });
} finally {
setLoadingPlanId(null);
}
};
const savingsPercent = Math.round((1 - (10 / 12)) * 100); // ~17 %
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 text-white">
<div className="min-h-screen bg-background text-foreground">
{/* ── Top navigation ── */}
<div className="sticky top-0 z-40 border-b border-border/60 bg-background/85 backdrop-blur">
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between gap-3">
<button
onClick={() => router.back()}
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Retour
</button>
<div className="flex items-center gap-2">
<Link
href={isLoggedIn ? "/dashboard" : "/"}
className="px-3 py-1.5 rounded-lg text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground transition-colors"
>
{isLoggedIn ? "Dashboard" : "Accueil"}
</Link>
{isLoggedIn && (
<Link
href="/settings/subscription"
className="px-3 py-1.5 rounded-lg text-sm bg-accent/80 hover:bg-accent text-accent-foreground transition-colors"
>
Mon abonnement
</Link>
)}
</div>
</div>
</div>
{/* ── Toast notification ── */}
{toastMsg && (
<div className={cn(
"fixed top-4 left-1/2 -translate-x-1/2 z-50 flex items-start gap-3 px-5 py-4 rounded-2xl shadow-2xl border max-w-lg w-full mx-4 backdrop-blur-sm",
toastMsg.type === 'ok'
? "bg-success/10 border-success/30 text-success"
: "bg-destructive/10 border-destructive/30 text-destructive"
)}>
<span className="text-lg">{toastMsg.type === 'ok' ? '✓' : '✕'}</span>
<p className="text-sm flex-1">{toastMsg.text}</p>
<button onClick={() => setToastMsg(null)} className="text-muted-foreground hover:text-foreground text-lg leading-none"></button>
</div>
)}
{/* ── Header ── */}
<div className="max-w-7xl mx-auto px-4 pt-20 pb-12 text-center">
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-300 text-sm font-medium mb-6">
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-accent/10 border border-accent/20 text-accent text-sm font-medium mb-6">
<Cpu className="w-4 h-4" />
Modèles IA mis à jour Mars 2026
</div>
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-4 bg-gradient-to-r from-white via-violet-200 to-violet-400 bg-clip-text text-transparent">
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-4 bg-gradient-to-r from-foreground via-accent/80 to-accent bg-clip-text text-transparent">
Un forfait pour chaque besoin
</h1>
<p className="text-xl text-gray-400 max-w-2xl mx-auto mb-8">
<p className="text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
Traduisez vos documents Word, Excel et PowerPoint en conservant
la mise en page originale. Sans jamais saisir de clé API.
</p>
{/* Monthly / Yearly toggle */}
<div className="inline-flex items-center gap-3 bg-gray-800/60 border border-gray-700/50 rounded-full p-1.5">
<div className="inline-flex items-center gap-3 bg-muted/60 border border-border/50 rounded-full p-1.5">
<button
onClick={() => setIsYearly(false)}
className={cn(
"px-5 py-2 rounded-full text-sm font-medium transition-all",
!isYearly ? "bg-white text-gray-900 shadow" : "text-gray-400 hover:text-white"
!isYearly ? "bg-foreground text-background shadow" : "text-muted-foreground hover:text-foreground"
)}
>
Mensuel
@@ -334,19 +493,23 @@ export default function PricingPage() {
onClick={() => setIsYearly(true)}
className={cn(
"px-5 py-2 rounded-full text-sm font-medium transition-all flex items-center gap-2",
isYearly ? "bg-white text-gray-900 shadow" : "text-gray-400 hover:text-white"
isYearly ? "bg-foreground text-background shadow" : "text-muted-foreground hover:text-foreground"
)}
>
Annuel
<span className="bg-emerald-500 text-white text-xs px-1.5 py-0.5 rounded-full">
20 %
<span className="bg-success text-success-foreground text-xs px-1.5 py-0.5 rounded-full">
{annualDiscountPercent} %
</span>
</button>
</div>
</div>
{/* ── Plan cards ── */}
{/* ── Plan cards (+ comparaison + crédits) : squelette jusquà lAPI pour éviter le flash des tarifs statiques */}
<div className="max-w-7xl mx-auto px-4 pb-20">
{!pricingLoaded ? (
<PricingDataSkeleton />
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-5">
{plans.map((plan) => {
const Icon = PLAN_ICONS[plan.id] ?? Sparkles;
@@ -359,7 +522,7 @@ export default function PricingPage() {
<div
key={plan.id}
className={cn(
"relative flex flex-col rounded-2xl border bg-gray-900/60 backdrop-blur transition-all duration-300",
"relative flex flex-col rounded-2xl border bg-card backdrop-blur transition-all duration-300",
"hover:scale-[1.02] hover:shadow-2xl",
colors.border,
plan.popular && "ring-2 ring-violet-500/50 shadow-violet-500/20 shadow-xl"
@@ -437,11 +600,11 @@ export default function PricingPage() {
)}
</div>
<div className="border-t border-gray-700/40 pt-3 space-y-2">
<div className="border-t border-border/40 pt-3 space-y-2">
{plan.features.map((feat, i) => (
<div key={i} className="flex items-start gap-2">
<Check className="w-4 h-4 text-emerald-400 flex-shrink-0 mt-0.5" />
<span className="text-gray-300 text-xs leading-snug">{feat}</span>
<Check className="w-4 h-4 text-emerald-500 flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground text-xs leading-snug">{feat}</span>
</div>
))}
</div>
@@ -452,7 +615,7 @@ export default function PricingPage() {
{isCurrent ? (
<Button
variant="outline"
className="w-full border-emerald-600/50 text-emerald-400 hover:bg-emerald-600/10"
className="w-full border-success/50 text-success hover:bg-success/10"
onClick={() => router.push("/settings/subscription")}
>
<BadgeCheck className="w-4 h-4 mr-1" /> Gérer mon forfait
@@ -460,7 +623,7 @@ export default function PricingPage() {
) : plan.id === "free" && !currentPlan ? (
<Button
variant="outline"
className="w-full border-gray-600/50 text-gray-400 hover:bg-gray-700/30"
className="w-full border-border text-muted-foreground hover:bg-muted/30"
onClick={() => router.push("/auth/register")}
>
Commencer gratuitement
@@ -468,13 +631,27 @@ export default function PricingPage() {
) : (
<button
onClick={() => handleSubscribe(plan.id)}
disabled={loadingPlanId !== null}
className={cn(
"w-full py-2.5 px-4 rounded-xl text-sm font-semibold text-white transition-all flex items-center justify-center gap-2",
colors.button
colors.button,
loadingPlanId !== null && "opacity-70 cursor-not-allowed"
)}
>
{isEnterprise ? "Nous contacter" : "Choisir ce forfait"}
<ArrowRight className="w-4 h-4" />
{loadingPlanId === plan.id ? (
<>
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Traitement
</>
) : (
<>
{isEnterprise ? "Nous contacter" : "Choisir ce forfait"}
<ArrowRight className="w-4 h-4" />
</>
)}
</button>
)}
</div>
@@ -486,15 +663,15 @@ export default function PricingPage() {
{/* ── Feature comparison table ── */}
<div className="mt-20">
<h2 className="text-3xl font-bold text-center mb-2">Comparaison détaillée</h2>
<p className="text-gray-400 text-center mb-10">Tout ce qui est inclus dans chaque forfait</p>
<p className="text-muted-foreground text-center mb-10">Tout ce qui est inclus dans chaque forfait</p>
<div className="overflow-x-auto rounded-2xl border border-gray-700/40">
<div className="overflow-x-auto rounded-2xl border border-border/40">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-800/60 border-b border-gray-700/40">
<th className="text-left py-4 px-6 text-gray-300 font-medium">Fonctionnalité</th>
<tr className="bg-muted/60 border-b border-border/40">
<th className="text-left py-4 px-6 text-muted-foreground font-medium">Fonctionnalité</th>
{plans.slice(0, 4).map((p) => (
<th key={p.id} className={cn("py-4 px-4 text-center font-medium", p.popular ? "text-violet-300" : "text-gray-300")}>
<th key={p.id} className={cn("py-4 px-4 text-center font-medium", p.popular ? "text-accent" : "text-muted-foreground")}>
{p.name}
</th>
))}
@@ -513,16 +690,16 @@ export default function PricingPage() {
{ label: "Traitement prioritaire", vals: plans.slice(0,4).map(p => p.priority_processing) },
{ label: "Support", vals: ["Communauté", "E-mail", "Prioritaire", "Dédié"] },
].map((row, i) => (
<tr key={i} className={cn("border-b border-gray-700/20", i % 2 === 0 ? "bg-gray-800/20" : "")}>
<td className="py-3 px-6 text-gray-300">{row.label}</td>
<tr key={i} className={cn("border-b border-border/20", i % 2 === 0 ? "bg-muted/20" : "")}>
<td className="py-3 px-6 text-muted-foreground">{row.label}</td>
{row.vals.map((val, j) => (
<td key={j} className="py-3 px-4 text-center">
{typeof val === "boolean" ? (
val
? <Check className="w-4 h-4 text-emerald-400 mx-auto" />
: <X className="w-4 h-4 text-gray-600 mx-auto" />
? <Check className="w-4 h-4 text-emerald-500 mx-auto" />
: <X className="w-4 h-4 text-muted-foreground/40 mx-auto" />
) : (
<span className={cn("text-sm", plans.slice(0,4)[j]?.popular ? "text-violet-200 font-medium" : "text-gray-300")}>
<span className={cn("text-sm", plans.slice(0,4)[j]?.popular ? "text-accent font-medium" : "text-muted-foreground")}>
{val}
</span>
)}
@@ -538,9 +715,9 @@ export default function PricingPage() {
{/* ── Credits ── */}
<div className="mt-20">
<h2 className="text-3xl font-bold text-center mb-2">Crédits supplémentaires</h2>
<p className="text-gray-400 text-center mb-8">
<p className="text-muted-foreground text-center mb-8">
Besoin de plus ? Achetez des crédits à l'unité, sans abonnement.
<span className="text-gray-500"> 1 crédit = 1 page traduite.</span>
<span className="text-muted-foreground/70"> 1 crédit = 1 page traduite.</span>
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-3xl mx-auto">
{credits.map((pkg, i) => (
@@ -549,80 +726,82 @@ export default function PricingPage() {
className={cn(
"relative p-5 rounded-2xl border text-center transition-all hover:scale-105",
pkg.popular
? "border-violet-500/50 bg-violet-500/10 shadow-violet-500/20 shadow-lg"
: "border-gray-700/40 bg-gray-800/40"
? "border-accent/50 bg-accent/10 shadow-accent/20 shadow-lg"
: "border-border/40 bg-card"
)}
>
{pkg.popular && (
<div className="absolute -top-2.5 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-violet-600 text-white text-xs rounded-full font-bold">
<div className="absolute -top-2.5 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-accent text-accent-foreground text-xs rounded-full font-bold">
Le meilleur rapport
</div>
)}
<div className="text-2xl font-bold text-white">{pkg.credits}</div>
<div className="text-gray-400 text-xs mb-3">crédits</div>
<div className="text-xl font-bold text-white">{pkg.price} €</div>
<div className="text-gray-500 text-xs">{(pkg.price_per_credit * 100).toFixed(0)} cts / crédit</div>
<button className="mt-3 w-full py-1.5 rounded-lg bg-gray-700 hover:bg-gray-600 text-white text-xs transition-all">
<div className="text-2xl font-bold text-foreground">{pkg.credits}</div>
<div className="text-muted-foreground text-xs mb-3">crédits</div>
<div className="text-xl font-bold text-foreground">{pkg.price} €</div>
<div className="text-muted-foreground text-xs">{(pkg.price_per_credit * 100).toFixed(0)} cts / crédit</div>
<button className="mt-3 w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all">
Acheter
</button>
</div>
))}
</div>
</div>
</>
)}
{/* ── Trust signals ── */}
<div className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-6">
{[
{ icon: <Shield className="w-6 h-6 text-emerald-400" />, title: "Chiffrement de bout en bout", sub: "TLS 1.3 + AES-256 au repos" },
{ icon: <Globe className="w-6 h-6 text-blue-400" />, title: "130+ langues", sub: "Dont arabe, persan, hébreu (RTL)" },
{ icon: <Gauge className="w-6 h-6 text-violet-400" />, title: "Traitement parallèle", sub: "IA multi-thread ultra-rapide" },
{ icon: <Clock className="w-6 h-6 text-amber-400" />, title: "Disponible 24/7", sub: "Uptime garanti 99,9 %" },
{ icon: <Shield className="w-6 h-6 text-emerald-500" />, title: "Chiffrement de bout en bout", sub: "TLS 1.3 + AES-256 au repos" },
{ icon: <Globe className="w-6 h-6 text-blue-500" />, title: "130+ langues", sub: "Dont arabe, persan, hébreu (RTL)" },
{ icon: <Gauge className="w-6 h-6 text-accent" />, title: "Traitement parallèle", sub: "IA multi-thread ultra-rapide" },
{ icon: <Clock className="w-6 h-6 text-amber-500" />, title: "Disponible 24/7", sub: "Uptime garanti 99,9 %" },
].map((t, i) => (
<div key={i} className="flex flex-col items-center text-center p-6 rounded-2xl bg-gray-800/30 border border-gray-700/30">
<div className="p-3 rounded-full bg-gray-700/50 mb-3">{t.icon}</div>
<div className="font-semibold text-white text-sm mb-1">{t.title}</div>
<div className="text-gray-500 text-xs">{t.sub}</div>
<div key={i} className="flex flex-col items-center text-center p-6 rounded-2xl bg-card border border-border/30">
<div className="p-3 rounded-full bg-muted/50 mb-3">{t.icon}</div>
<div className="font-semibold text-foreground text-sm mb-1">{t.title}</div>
<div className="text-muted-foreground text-xs">{t.sub}</div>
</div>
))}
</div>
{/* ── AI Models info ── */}
<div className="mt-20 p-8 rounded-2xl bg-gradient-to-br from-violet-900/20 to-gray-800/40 border border-violet-500/20">
<div className="mt-20 p-8 rounded-2xl bg-gradient-to-br from-accent/10 to-card border border-accent/20">
<div className="flex items-center gap-3 mb-6">
<Brain className="w-6 h-6 text-violet-400" />
<Brain className="w-6 h-6 text-accent" />
<h2 className="text-2xl font-bold">Nos modèles IA — Mars 2026</h2>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="p-5 rounded-xl bg-gray-800/50 border border-gray-700/40">
<div className="p-5 rounded-xl bg-card border border-border/40">
<div className="flex items-center gap-2 mb-2">
<Zap className="w-4 h-4 text-blue-400" />
<Zap className="w-4 h-4 text-blue-500" />
<span className="font-semibold">Traduction IA Essentielle</span>
<Badge className="ml-auto text-xs bg-blue-600/20 text-blue-300 border-blue-600/30">Forfait Pro</Badge>
<Badge className="ml-auto text-xs bg-blue-500/10 text-blue-600 border-blue-500/30 dark:text-blue-300">Forfait Pro</Badge>
</div>
<div className="text-sm text-gray-400 mb-3">
Basée sur <strong className="text-white">DeepSeek V3.2</strong> — le modèle IA le plus rentable de 2026.
<div className="text-sm text-muted-foreground mb-3">
Basée sur <strong className="text-foreground">DeepSeek V3.2</strong> — le modèle IA le plus rentable de 2026.
Qualité comparable aux modèles frontier à 1/50ème du coût.
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="px-2 py-1 bg-gray-700/50 rounded">163K tokens de contexte</span>
<span className="px-2 py-1 bg-gray-700/50 rounded">$0.25/$0.38 per 1M</span>
<span className="px-2 py-1 bg-emerald-600/20 text-emerald-300 rounded">Excellent rapport qualité/prix</span>
<span className="px-2 py-1 bg-muted rounded">163K tokens de contexte</span>
<span className="px-2 py-1 bg-muted rounded">$0.25/$0.38 per 1M</span>
<span className="px-2 py-1 bg-success/10 text-success rounded">Excellent rapport qualité/prix</span>
</div>
</div>
<div className="p-5 rounded-xl bg-gray-800/50 border border-violet-700/40">
<div className="p-5 rounded-xl bg-card border border-accent/30">
<div className="flex items-center gap-2 mb-2">
<Crown className="w-4 h-4 text-violet-400" />
<Crown className="w-4 h-4 text-accent" />
<span className="font-semibold">Traduction IA Premium</span>
<Badge className="ml-auto text-xs bg-violet-600/20 text-violet-300 border-violet-600/30">Forfait Business</Badge>
<Badge className="ml-auto text-xs bg-accent/10 text-accent border-accent/30">Forfait Business</Badge>
</div>
<div className="text-sm text-gray-400 mb-3">
Basée sur <strong className="text-white">Claude 3.5 Haiku</strong> d'Anthropic précis sur les documents
<div className="text-sm text-muted-foreground mb-3">
Basée sur <strong className="text-foreground">Claude 3.5 Haiku</strong> d'Anthropic précis sur les documents
juridiques, médicaux et techniques complexes.
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="px-2 py-1 bg-gray-700/50 rounded">200K tokens de contexte</span>
<span className="px-2 py-1 bg-gray-700/50 rounded">$0.25/$1.25 per 1M</span>
<span className="px-2 py-1 bg-violet-600/20 text-violet-300 rounded">Meilleure précision</span>
<span className="px-2 py-1 bg-muted rounded">200K tokens de contexte</span>
<span className="px-2 py-1 bg-muted rounded">$0.25/$1.25 per 1M</span>
<span className="px-2 py-1 bg-accent/10 text-accent rounded">Meilleure précision</span>
</div>
</div>
</div>
@@ -635,20 +814,20 @@ export default function PricingPage() {
{FAQS.map((faq, i) => (
<div
key={i}
className="rounded-xl border border-gray-700/40 bg-gray-800/30 overflow-hidden"
className="rounded-xl border border-border/40 bg-card overflow-hidden"
>
<button
className="w-full flex items-center justify-between p-5 text-left hover:bg-gray-700/20 transition-colors"
className="w-full flex items-center justify-between p-5 text-left hover:bg-muted/20 transition-colors"
onClick={() => setOpenFAQ(openFAQ === i ? null : i)}
>
<span className="font-medium text-gray-200">{faq.q}</span>
<span className="font-medium text-foreground">{faq.q}</span>
{openFAQ === i
? <ChevronUp className="w-5 h-5 text-gray-400 flex-shrink-0" />
: <ChevronDown className="w-5 h-5 text-gray-400 flex-shrink-0" />
? <ChevronUp className="w-5 h-5 text-muted-foreground flex-shrink-0" />
: <ChevronDown className="w-5 h-5 text-muted-foreground flex-shrink-0" />
}
</button>
{openFAQ === i && (
<div className="px-5 pb-5 text-gray-400 text-sm leading-relaxed border-t border-gray-700/30 pt-4">
<div className="px-5 pb-5 text-muted-foreground text-sm leading-relaxed border-t border-border/30 pt-4">
{faq.a}
</div>
)}
@@ -658,20 +837,20 @@ export default function PricingPage() {
</div>
{/* ── CTA bottom ── */}
<div className="mt-20 text-center p-12 rounded-2xl bg-gradient-to-br from-violet-900/30 to-gray-800/40 border border-violet-500/20">
<div className="mt-20 text-center p-12 rounded-2xl bg-gradient-to-br from-accent/10 to-card border border-accent/20">
<h2 className="text-3xl font-bold mb-3">Prêt à commencer ?</h2>
<p className="text-gray-400 mb-8 max-w-lg mx-auto">
<p className="text-muted-foreground mb-8 max-w-lg mx-auto">
Commencez gratuitement, sans carte bancaire. Passez à un forfait supérieur quand vous en avez besoin.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/auth/register">
<Button className="bg-violet-600 hover:bg-violet-500 text-white px-8 py-3 text-base font-semibold">
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground px-8 py-3 text-base font-semibold">
Créer un compte gratuit
<ArrowRight className="ml-2 w-5 h-5" />
</Button>
</Link>
<Link href="/auth/login">
<Button variant="outline" className="border-gray-600 text-gray-300 hover:bg-gray-700/30 px-8 py-3 text-base">
<Button variant="outline" className="px-8 py-3 text-base">
Se connecter
</Button>
</Link>
@@ -687,11 +866,11 @@ function Stat({ icon, label, value, highlight }: { icon: React.ReactNode; label:
return (
<div className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg text-xs",
highlight ? "bg-violet-500/10 border border-violet-500/20" : "bg-gray-800/40 border border-gray-700/30"
highlight ? "bg-accent/10 border border-accent/20" : "bg-muted/40 border border-border/30"
)}>
<span className={highlight ? "text-violet-400" : "text-gray-500"}>{icon}</span>
<span className="text-gray-400">{label} :</span>
<span className={cn("font-medium ml-auto", highlight ? "text-violet-300" : "text-gray-200")}>{value}</span>
<span className={highlight ? "text-accent" : "text-muted-foreground"}>{icon}</span>
<span className="text-muted-foreground">{label} :</span>
<span className={cn("font-medium ml-auto", highlight ? "text-accent" : "text-foreground")}>{value}</span>
</div>
);
}