feat: rewrite all dashboard views with editorial design
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m29s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m29s
Translate page: editorial dropzone (rounded-[40px]), config panel, progress stepper, download button, Momento promo banner. Profile page: pill tabs, dark subscription card, usage bars, language grid. Context page: preset grid (rounded-[32px]), instruction/glossary textareas. API keys page: editorial-card with production key display. Glossaries page: editorial-card grid with hover lift. Pricing page: 5-column grid, colored headers, plan badges. All pages use editorial design system: accent-pill, editorial-card, premium-button, brand colors, dark mode support. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,13 @@ import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
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
|
||||
Check, CheckCircle2, X, Zap, Building2, Crown, Sparkles, ArrowRight,
|
||||
ArrowLeft, ChevronLeft, Star, Shield, Rocket, Users, Headphones, Lock,
|
||||
Globe, Clock, ChevronDown, ChevronUp, Cpu, BarChart3, Infinity,
|
||||
FileText, Layers, Brain, BadgeCheck, Gauge, Activity,
|
||||
} from "lucide-react";
|
||||
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";
|
||||
@@ -200,41 +199,41 @@ const STATIC_CREDITS: CreditPackage[] = [
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Visual config by plan
|
||||
Visual config by plan — editorial design
|
||||
───────────────────────────────────────────── */
|
||||
const PLAN_ICONS: Record<string, any> = {
|
||||
free: Sparkles,
|
||||
free: Star,
|
||||
starter: Zap,
|
||||
pro: Crown,
|
||||
business: Building2,
|
||||
enterprise: Rocket,
|
||||
business: Globe,
|
||||
enterprise: Shield,
|
||||
};
|
||||
|
||||
const PLAN_COLORS: Record<string, { gradient: string; border: string; badge: string; button: string }> = {
|
||||
free: { gradient: "from-slate-600 to-slate-700", border: "border-slate-700/50", badge: "bg-slate-700", button: "bg-slate-700 hover:bg-slate-600" },
|
||||
starter: { gradient: "from-blue-600 to-blue-700", border: "border-blue-700/50", badge: "bg-blue-600", button: "bg-blue-600 hover:bg-blue-500" },
|
||||
pro: { gradient: "from-violet-600 to-violet-700", border: "border-violet-500/60",badge: "bg-violet-600", button: "bg-violet-600 hover:bg-violet-500" },
|
||||
business: { gradient: "from-emerald-600 to-emerald-700",border:"border-emerald-700/50",badge:"bg-emerald-600", button: "bg-emerald-600 hover:bg-emerald-500" },
|
||||
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" },
|
||||
const PLAN_COLORS: Record<string, { header: string; iconColor: string; nameColor: string }> = {
|
||||
free: { header: "bg-muted", iconColor: "text-foreground/20", nameColor: "text-foreground/40" },
|
||||
starter: { header: "bg-foreground", iconColor: "text-white/20", nameColor: "text-white/50" },
|
||||
pro: { header: "bg-accent", iconColor: "text-white/30", nameColor: "text-white/50" },
|
||||
business: { header: "bg-foreground", iconColor: "text-accent/40", nameColor: "text-white/50" },
|
||||
enterprise: { header: "bg-[#252525]", iconColor: "text-white/10", nameColor: "text-white/50" },
|
||||
};
|
||||
|
||||
/** Avoids flash of static prices before the API responds on refresh. */
|
||||
function PricingDataSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-2xl border border-border/40 bg-card overflow-hidden animate-pulse"
|
||||
className="rounded-[24px] border border-black/[0.08] bg-white overflow-hidden animate-pulse"
|
||||
>
|
||||
<div className="h-28 bg-muted/60" />
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="h-48 bg-muted/60" />
|
||||
<div className="p-8 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 className="h-10 bg-muted rounded-2xl mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -404,27 +403,28 @@ export default function PricingPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* ── Top navigation — breadcrumb bar ── */}
|
||||
<div className="max-w-[1400px] mx-auto px-4 pt-4">
|
||||
<div className="flex justify-between items-center mb-20">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="flex items-center gap-3 text-[10px] font-black uppercase tracking-[0.4em] text-foreground/30 hover:text-foreground transition-all group"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<ChevronLeft size={16} className="group-hover:-translate-x-1 transition-transform" />
|
||||
{t('pricing.nav.back')}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<div className="flex gap-2 p-1.5 bg-muted rounded-full border border-black/5 shadow-inner">
|
||||
<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"
|
||||
className="px-6 py-2 rounded-full text-[9px] font-black uppercase tracking-widest text-foreground/40 hover:text-foreground transition-all"
|
||||
>
|
||||
{isLoggedIn ? "Dashboard" : t('pricing.nav.home')}
|
||||
Dashboard
|
||||
</Link>
|
||||
{isLoggedIn && (
|
||||
<Link
|
||||
href="/dashboard/profile"
|
||||
className="px-3 py-1.5 rounded-lg text-sm bg-accent/80 hover:bg-accent text-accent-foreground transition-colors"
|
||||
className="px-6 py-2 bg-white rounded-full text-[9px] font-black uppercase tracking-widest text-foreground shadow-sm border border-black/5"
|
||||
>
|
||||
{t('pricing.nav.mySubscription')}
|
||||
</Link>
|
||||
@@ -438,185 +438,238 @@ export default function PricingPage() {
|
||||
<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"
|
||||
? "bg-emerald-50 border-emerald-200 text-emerald-700 dark:bg-emerald-950/50 dark:border-emerald-800 dark:text-emerald-300"
|
||||
: "bg-red-50 border-red-200 text-red-700 dark:bg-red-950/50 dark:border-red-800 dark:text-red-300"
|
||||
)}>
|
||||
<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>
|
||||
<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-accent/10 border border-accent/20 text-accent text-sm font-medium mb-6">
|
||||
<Cpu className="w-4 h-4" />
|
||||
{t('pricing.header.badge')}
|
||||
</div>
|
||||
<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">
|
||||
{t('pricing.header.title')}
|
||||
<div className="max-w-[1400px] mx-auto px-4 text-center mb-20">
|
||||
<span className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-black/5 mb-6">
|
||||
<div className="w-2 h-2 bg-accent rounded-full animate-pulse" />
|
||||
<span className="text-[10px] font-black tracking-[0.3em] opacity-40">{t('pricing.header.badge')}</span>
|
||||
</span>
|
||||
<h1 className="text-5xl md:text-7xl font-black uppercase tracking-tighter mb-6 leading-none">
|
||||
{t('pricing.header.title').split(' ').slice(0, -2).join(' ')}{" "}
|
||||
<span className="text-accent">
|
||||
{t('pricing.header.title').split(' ').slice(-2).join(' ')}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
|
||||
<p className="text-foreground/40 font-medium text-xl max-w-2xl mx-auto leading-relaxed">
|
||||
{t('pricing.header.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Monthly / Yearly toggle */}
|
||||
<div className="inline-flex items-center gap-3 bg-muted/60 border border-border/50 rounded-full p-1.5">
|
||||
{/* ── Monthly / Yearly toggle ── */}
|
||||
<div className="flex items-center justify-center gap-10 mb-20">
|
||||
<div className="flex p-1 bg-muted rounded-full border border-black/5 shadow-inner px-2">
|
||||
<button
|
||||
onClick={() => setIsYearly(false)}
|
||||
className={cn(
|
||||
"px-5 py-2 rounded-full text-sm font-medium transition-all",
|
||||
!isYearly ? "bg-foreground text-background shadow" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
className={`px-8 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-all ${!isYearly ? 'bg-foreground text-white shadow-xl' : 'text-foreground/60 hover:text-foreground'}`}
|
||||
>
|
||||
{t('pricing.billing.monthly')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsYearly(true)}
|
||||
className={cn(
|
||||
"px-5 py-2 rounded-full text-sm font-medium transition-all flex items-center gap-2",
|
||||
isYearly ? "bg-foreground text-background shadow" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
className={`px-8 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-all ${isYearly ? 'bg-foreground text-white shadow-xl' : 'text-foreground/60 hover:text-foreground'}`}
|
||||
>
|
||||
{t('pricing.billing.yearly')}
|
||||
<span className="bg-success text-success-foreground text-xs px-1.5 py-0.5 rounded-full">
|
||||
−{annualDiscountPercent} %
|
||||
</span>
|
||||
<span className={`ml-2 transition-colors ${isYearly ? 'text-accent' : 'text-accent/60'}`}>−{annualDiscountPercent} %</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Plan cards (+ comparison + credits): skeleton until API responds to avoid stale price flash ── */}
|
||||
<div className="max-w-7xl mx-auto px-4 pb-20">
|
||||
{/* ── Plan cards (skeleton until API responds) ── */}
|
||||
<div className="max-w-[1400px] 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">
|
||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-6 items-stretch">
|
||||
{plans.map((plan) => {
|
||||
const Icon = PLAN_ICONS[plan.id] ?? Sparkles;
|
||||
const colors = PLAN_COLORS[plan.id] ?? PLAN_COLORS.starter;
|
||||
const price = displayPrice(plan);
|
||||
const isCurrent = currentPlan === plan.id;
|
||||
const isEnterprise = plan.id === "enterprise";
|
||||
const isFree = plan.id === "free";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={cn(
|
||||
"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"
|
||||
"flex flex-col bg-white dark:bg-card rounded-[24px] border border-black/[0.08] dark:border-border/40 overflow-hidden transition-all duration-500 hover:shadow-[0_20px_50px_rgba(0,0,0,0.08)] hover:-translate-y-2 group",
|
||||
plan.popular && "border-accent/30 ring-4 ring-accent/5"
|
||||
)}
|
||||
>
|
||||
{/* Popular badge */}
|
||||
{plan.badge && (
|
||||
<div className={cn(
|
||||
"absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full text-xs font-bold text-white",
|
||||
colors.badge
|
||||
)}>
|
||||
{t(plan.badge)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCurrent && (
|
||||
<div className="absolute -top-3 right-4 px-3 py-1 rounded-full text-xs font-bold bg-emerald-600 text-white flex items-center gap-1">
|
||||
<BadgeCheck className="w-3 h-3" /> {t('pricing.card.myPlan')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className={cn("p-5 rounded-t-2xl bg-gradient-to-br", colors.gradient)}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="p-1.5 bg-white/10 rounded-lg">
|
||||
<Icon className="w-5 h-5 text-white" />
|
||||
{/* ── Header section ── */}
|
||||
<div className={cn("p-8 text-white relative h-48 flex flex-col justify-end", colors.header)}>
|
||||
{/* Badges for popular/current plan */}
|
||||
{plan.popular && (
|
||||
<div className="absolute top-0 right-0 p-3 flex gap-2">
|
||||
{plan.badge && (
|
||||
<span className="bg-foreground/20 backdrop-blur-md text-white text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full border border-white/20 shadow-lg">
|
||||
{t(plan.badge)}
|
||||
</span>
|
||||
)}
|
||||
{isCurrent && (
|
||||
<span className="bg-foreground/40 backdrop-blur-md text-white text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full border border-white/20 shadow-lg flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-accent rounded-full animate-pulse" /> {t('pricing.card.myPlan')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="font-bold text-white">{t(plan.name)}</span>
|
||||
</div>
|
||||
|
||||
{isEnterprise ? (
|
||||
<div className="text-3xl font-bold text-white">{t('pricing.card.onRequest')}</div>
|
||||
) : price === 0 ? (
|
||||
<div className="text-3xl font-bold text-white">{t('pricing.card.free')}</div>
|
||||
) : (
|
||||
<div className="flex items-end gap-1">
|
||||
<span className="text-3xl font-bold text-white">{price} €</span>
|
||||
<span className="text-white/70 text-sm pb-1">{t('pricing.card.perMonth')}</span>
|
||||
)}
|
||||
{plan.badge && !plan.popular && (
|
||||
<div className="absolute top-0 right-0 p-3">
|
||||
<span className="bg-white/10 backdrop-blur-md text-white text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full border border-white/10">
|
||||
{t(plan.badge)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* "My Plan" badge for current non-popular plan */}
|
||||
{isCurrent && !plan.popular && (
|
||||
<div className="absolute top-0 right-0 p-3">
|
||||
<span className="bg-foreground/40 backdrop-blur-md text-white text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full border border-white/20 shadow-lg flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-accent rounded-full animate-pulse" /> {t('pricing.card.myPlan')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Icon + plan name */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Icon size={20} className={colors.iconColor} />
|
||||
<span className={cn("text-sm font-black uppercase tracking-widest", isFree ? "text-foreground/40" : colors.nameColor)}>
|
||||
{t(plan.name)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-baseline gap-2">
|
||||
{isEnterprise ? (
|
||||
<h3 className={cn("text-3xl font-black uppercase tracking-tighter", isFree ? "text-foreground" : "text-white")}>
|
||||
{t('pricing.card.onRequest')}
|
||||
</h3>
|
||||
) : price === 0 ? (
|
||||
<h3 className="text-3xl font-black uppercase tracking-tighter text-foreground">
|
||||
{t('pricing.card.free')}
|
||||
</h3>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-3xl font-black uppercase tracking-tighter text-white">{price} €</h3>
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-white/40">{t('pricing.card.perMonth')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Yearly billing note */}
|
||||
{isYearly && plan.price_yearly > 0 && (
|
||||
<div className="text-white/70 text-xs mt-1">
|
||||
<div className="text-white/70 text-[10px] mt-1">
|
||||
{t('pricing.card.billedYearly', { price: plan.price_yearly.toFixed(2) })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-white/60 text-xs mt-2">{t(plan.description || '')}</p>
|
||||
{/* Description */}
|
||||
<p className={cn("text-[10px] font-medium uppercase mt-3 tracking-widest leading-relaxed", isFree ? "text-foreground/40" : "text-white/60")}>
|
||||
{t(plan.description || '')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="flex-1 p-5 space-y-2.5">
|
||||
{/* Key stats */}
|
||||
<div className="grid grid-cols-1 gap-2 mb-3">
|
||||
<Stat
|
||||
icon={<FileText className="w-3.5 h-3.5" />}
|
||||
label={t('pricing.card.documents')}
|
||||
value={plan.docs_per_month === -1 ? t('pricing.card.unlimited') : `${plan.docs_per_month} ${t('pricing.card.perMonthStat')}`}
|
||||
/>
|
||||
<Stat
|
||||
icon={<Layers className="w-3.5 h-3.5" />}
|
||||
label={t('pricing.card.pagesMax')}
|
||||
value={plan.max_pages_per_doc === -1 ? t('pricing.card.unlimited') : `${plan.max_pages_per_doc} ${t('pricing.card.perDoc')}`}
|
||||
/>
|
||||
{/* ── Content section ── */}
|
||||
<div className="p-8 flex-1 flex flex-col">
|
||||
{/* Metrics */}
|
||||
<div className="space-y-2 mb-10">
|
||||
<div className="flex justify-between items-center py-2 border-b border-black/[0.03] dark:border-border/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText size={12} className="text-foreground/20" />
|
||||
<span className="text-[9px] font-black uppercase tracking-widest text-foreground/40">{t('pricing.card.documents')}</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase text-foreground">
|
||||
{plan.docs_per_month === -1 ? t('pricing.card.unlimited') : `${plan.docs_per_month} ${t('pricing.card.perMonthStat')}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-black/[0.03] dark:border-border/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers size={12} className="text-foreground/20" />
|
||||
<span className="text-[9px] font-black uppercase tracking-widest text-foreground/40">{t('pricing.card.pagesMax')}</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase text-foreground">
|
||||
{plan.max_pages_per_doc === -1 ? t('pricing.card.unlimited') : `${plan.max_pages_per_doc} ${t('pricing.card.perDoc')}`}
|
||||
</span>
|
||||
</div>
|
||||
{plan.ai_translation && (
|
||||
<Stat
|
||||
icon={<Brain className="w-3.5 h-3.5 text-violet-400" />}
|
||||
label={t('pricing.card.aiTranslation')}
|
||||
value={
|
||||
plan.ai_tier === "essential" ? t('pricing.card.aiEssential') :
|
||||
plan.ai_tier === "premium" ? t('pricing.card.aiEssentialPremium') : t('pricing.card.aiCustom')
|
||||
}
|
||||
highlight
|
||||
/>
|
||||
<div className={cn(
|
||||
"flex justify-between items-center py-3 px-3 rounded-lg mt-2",
|
||||
plan.ai_tier === "essential" ? "bg-accent/5" :
|
||||
plan.ai_tier === "premium" ? "bg-foreground/5" :
|
||||
"bg-black/5 dark:bg-border/20"
|
||||
)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity size={12} className={
|
||||
plan.ai_tier === "essential" ? "text-accent/40" :
|
||||
"text-foreground/20"
|
||||
} />
|
||||
<span className={cn(
|
||||
"text-[9px] font-black uppercase tracking-widest",
|
||||
plan.ai_tier === "essential" ? "text-accent/60" :
|
||||
"text-foreground/40"
|
||||
)}>{t('pricing.card.aiTranslation')}</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
"text-[9px] font-black uppercase",
|
||||
plan.ai_tier === "essential" ? "text-accent" :
|
||||
plan.ai_tier === "premium" ? "text-foreground" :
|
||||
"text-foreground"
|
||||
)}>
|
||||
{plan.ai_tier === "essential" ? t('pricing.card.aiEssential') :
|
||||
plan.ai_tier === "premium" ? t('pricing.card.aiEssentialPremium') :
|
||||
t('pricing.card.aiCustom')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/40 pt-3 space-y-2">
|
||||
{/* Features list */}
|
||||
<ul className="space-y-4 mb-12 flex-1">
|
||||
{plan.features.map((feat, i) => (
|
||||
<div key={i} className="flex items-start gap-2">
|
||||
<Check className="w-4 h-4 text-emerald-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground text-xs leading-snug">{t(feat)}</span>
|
||||
</div>
|
||||
<li key={i} className="flex items-start gap-3">
|
||||
<div className="w-4 h-4 rounded-full bg-accent/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<CheckCircle2 size={10} className="text-accent" />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-foreground/60 leading-normal">{t(feat)}</span>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="p-5 pt-0">
|
||||
{/* CTA */}
|
||||
{isCurrent ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-success/50 text-success hover:bg-success/10"
|
||||
onClick={() => router.push("/dashboard/profile")}
|
||||
<Link
|
||||
href="/dashboard/profile"
|
||||
className="w-full py-4 rounded-2xl text-[11px] font-black uppercase tracking-widest transition-all flex items-center justify-center gap-3 border shadow-sm hover:shadow-xl active:scale-95 bg-muted text-foreground border-black/5 hover:bg-foreground hover:text-white"
|
||||
>
|
||||
<BadgeCheck className="w-4 h-4 me-1" /> {t('pricing.card.managePlan')}
|
||||
</Button>
|
||||
) : plan.id === "free" && !currentPlan ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-border text-muted-foreground hover:bg-muted/30"
|
||||
onClick={() => router.push("/auth/register")}
|
||||
{t('pricing.card.managePlan')}
|
||||
<ArrowRight size={14} className="opacity-40" />
|
||||
</Link>
|
||||
) : isFree && !currentPlan ? (
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="w-full py-4 rounded-2xl text-[11px] font-black uppercase tracking-widest transition-all flex items-center justify-center gap-3 border shadow-sm hover:shadow-xl active:scale-95 bg-foreground text-white border-transparent hover:bg-accent"
|
||||
>
|
||||
{t('pricing.card.startFree')}
|
||||
</Button>
|
||||
<ArrowRight size={14} className="opacity-40" />
|
||||
</Link>
|
||||
) : (
|
||||
<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,
|
||||
"w-full py-4 rounded-2xl text-[11px] font-black uppercase tracking-widest transition-all flex items-center justify-center gap-3 border shadow-sm hover:shadow-xl active:scale-95",
|
||||
plan.popular
|
||||
? "bg-muted text-foreground border-black/5 hover:bg-foreground hover:text-white"
|
||||
: "bg-foreground text-white border-transparent hover:bg-accent",
|
||||
loadingPlanId !== null && "opacity-70 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
@@ -631,7 +684,7 @@ export default function PricingPage() {
|
||||
) : (
|
||||
<>
|
||||
{isEnterprise ? t('pricing.card.contactUs') : t('pricing.card.choosePlan')}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<ArrowRight size={14} className="opacity-40" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -719,7 +772,7 @@ export default function PricingPage() {
|
||||
)}
|
||||
<div className="text-2xl font-bold text-foreground">{pkg.credits}</div>
|
||||
<div className="text-muted-foreground text-xs mb-3">{t('pricing.credits.unit')}</div>
|
||||
<div className="text-xl font-bold text-foreground">{pkg.price} €</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)} {t('pricing.credits.centsPerCredit')}</div>
|
||||
<button className="mt-3 w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all">
|
||||
{t('pricing.credits.buy')}
|
||||
@@ -766,7 +819,7 @@ export default function PricingPage() {
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<span className="px-2 py-1 bg-muted rounded">{t('pricing.aiModels.essential.context')}</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">{t('pricing.aiModels.essential.value')}</span>
|
||||
<span className="px-2 py-1 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded">{t('pricing.aiModels.essential.value')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 rounded-xl bg-card border border-accent/30">
|
||||
@@ -840,17 +893,3 @@ export default function PricingPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Sub-components ── */
|
||||
function Stat({ icon, label, value, highlight }: { icon: React.ReactNode; label: string; value: string; highlight?: boolean }) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-lg text-xs",
|
||||
highlight ? "bg-accent/10 border border-accent/20" : "bg-muted/40 border border-border/30"
|
||||
)}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user