feat: rewrite all dashboard views with editorial design
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:
2026-05-16 12:54:29 +02:00
parent c2e9617045
commit 3938adf10c
7 changed files with 1506 additions and 993 deletions

View File

@@ -1,11 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { Zap, Plus, AlertCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Zap, Plus, AlertCircle, Clock } from 'lucide-react';
import { useUser } from '@/app/dashboard/useUser';
import { useApiKeys } from './useApiKeys';
import { MAX_API_KEYS, type ApiKey } from './types';
@@ -16,6 +12,7 @@ import { RevokeKeyDialog } from './RevokeKeyDialog';
import { WebhookSnippet } from './WebhookSnippet';
import { useToast } from '@/components/ui/toast';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
export default function ApiKeysPage() {
const { t } = useI18n();
@@ -122,8 +119,8 @@ export default function ApiKeysPage() {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-muted border-t-foreground mx-auto"></div>
<p className="text-sm text-muted-foreground">{t('apiKeys.loading')}</p>
<div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-muted border-t-brand-accent mx-auto"></div>
<p className="text-[9px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">{t('apiKeys.loading')}</p>
</div>
</div>
);
@@ -134,70 +131,94 @@ export default function ApiKeysPage() {
}
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">{t('apiKeys.title')}</h1>
<p className="text-muted-foreground">
{t('apiKeys.subtitle')}
</p>
<div className="max-w-4xl mx-auto w-full p-6 lg:p-8 space-y-8">
{/* ── Editorial Header ───────────────────────────────────── */}
<div className="mb-4">
<span className="accent-pill mb-4 block w-fit">{t('apiKeys.sectionTitle')}</span>
<h1 className="text-5xl font-black uppercase tracking-tighter mb-4 leading-none text-brand-dark dark:text-white">
{t('apiKeys.title')}
</h1>
<p className="text-brand-dark/40 dark:text-white/40 font-medium">{t('apiKeys.subtitle')}</p>
</div>
{/* API Error */}
{apiError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{apiError}</AlertDescription>
</Alert>
<div className="flex items-start gap-3 p-4 rounded-xl border border-red-200 dark:border-red-500/30 bg-red-50 dark:bg-red-500/10 text-sm text-red-600 dark:text-red-400">
<AlertCircle className="w-4 h-4 shrink-0 mt-0.5" />
<span className="flex-1">{apiError}</span>
</div>
)}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<div className="flex size-8 items-center justify-center rounded-lg bg-accent/10">
<Zap className="size-4 text-accent" />
</div>
<div>
<CardTitle className="text-base">{t('apiKeys.sectionTitle')}</CardTitle>
<CardDescription>{t('apiKeys.sectionDesc')}</CardDescription>
</div>
{/* ── Main Card ──────────────────────────────────────────── */}
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial">
{/* Section header */}
<div className="flex items-center gap-4 mb-10 text-brand-accent">
<div className="w-12 h-12 bg-brand-muted dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent">
<Zap size={24} />
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">
{t('apiKeys.keysUsed', { total, max: MAX_API_KEYS })}
</p>
<p className="text-xs text-muted-foreground">
{maxKeysReached ? (
<span className="text-amber-600">{t('apiKeys.maxReached')}</span>
) : (
t(MAX_API_KEYS - total !== 1 ? 'apiKeys.canGeneratePlural' : 'apiKeys.canGenerate', { count: MAX_API_KEYS - total })
)}
</p>
</div>
<Button
onClick={() => setGenerateDialogOpen(true)}
disabled={maxKeysReached || isGenerating}
className="gap-1.5"
>
<Plus className="size-3.5" />
{t('apiKeys.generateNew')}
</Button>
<div>
<h3 className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('apiKeys.sectionTitle')}
</h3>
<p className="text-[9px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest mt-1">
{t('apiKeys.sectionDesc')}
</p>
</div>
</div>
<ApiKeyTable
keys={keys}
onRevoke={handleRevokeClick}
isRevoking={isRevoking}
/>
</CardContent>
</Card>
{/* Key count + generate button */}
<div className="flex items-center justify-between mb-8">
<div>
<p className="text-[11px] font-black uppercase tracking-widest text-brand-dark dark:text-white">
{t('apiKeys.keysUsed', { total, max: MAX_API_KEYS })}
</p>
<p className="text-[9px] text-brand-dark/30 dark:text-white/30 font-black uppercase tracking-widest mt-1">
{maxKeysReached ? (
<span className="text-amber-600 dark:text-amber-400">{t('apiKeys.maxReached')}</span>
) : (
t(MAX_API_KEYS - total !== 1 ? 'apiKeys.canGeneratePlural' : 'apiKeys.canGenerate', { count: MAX_API_KEYS - total })
)}
</p>
</div>
<button
onClick={() => setGenerateDialogOpen(true)}
disabled={maxKeysReached || isGenerating}
className="premium-button px-8 py-3 text-[9px] uppercase tracking-widest !rounded-xl flex items-center gap-2 disabled:opacity-50"
>
<Plus size={14} />
{t('apiKeys.generateNew')}
</button>
</div>
<Separator />
{/* Production key display area */}
<div className="flex items-center justify-between p-8 bg-brand-muted dark:bg-white/5 rounded-3xl border border-black/5 dark:border-white/10 mb-8">
<div>
<p className="text-[9px] text-brand-dark/40 dark:text-white/40 font-black uppercase tracking-[0.3em] mb-2">
{t('apiKeys.sectionTitle')}
</p>
<p className="text-sm font-mono text-brand-dark dark:text-white">
{keys.length > 0 ? `${keys[0].key_prefix}************************************` : 'No keys generated'}
</p>
</div>
<div className="flex gap-3">
<button className="p-3 bg-white dark:bg-white/10 rounded-xl text-brand-dark/40 dark:text-white/40 hover:text-brand-dark dark:hover:text-white border border-black/5 dark:border-white/10 transition-all">
<Clock size={16} />
</button>
</div>
</div>
{/* Key table */}
<ApiKeyTable
keys={keys}
onRevoke={handleRevokeClick}
isRevoking={isRevoking}
/>
</div>
{/* Webhook snippet */}
<WebhookSnippet />
{/* Dialogs */}
<GenerateKeyDialog
open={generateDialogOpen}
onOpenChange={setGenerateDialogOpen}

View File

@@ -1,25 +1,26 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import {
Zap, Save, Brain, BookOpen, Trash2, Crown, Loader2,
Wrench, HardHat, Monitor, Scale, Stethoscope, BarChart3,
Megaphone, Car, MessageSquare, Database,
} from 'lucide-react';
import { useTranslationStore } from '@/lib/store';
import { API_BASE } from '@/lib/config';
import { useI18n } from '@/lib/i18n';
import { Save, Loader2, Brain, BookOpen, Sparkles, Trash2, Zap, Crown, Lock } from 'lucide-react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
const PRESETS = [
{ key: 'hvac', icon: '🔧', title: 'HVAC / Génie climatique', desc: 'Thermique, ventilation, climatisation' },
{ key: 'construction', icon: '🏗️', title: 'BTP / Construction', desc: 'Gros œuvre, second œuvre, normes' },
{ key: 'it', icon: '💻', title: 'IT / Logiciel', desc: 'Développement, infrastructure, DevOps' },
{ key: 'legal', icon: '⚖️', title: 'Juridique / Contrats', desc: 'Droit des affaires, contentieux' },
{ key: 'medical', icon: '🏥', title: 'Médical / Santé', desc: 'Pharmacologie, chirurgie, diagnostic' },
{ key: 'finance', icon: '📊', title: 'Finance / Comptabilité', desc: 'IFRS, bilans, fiscalité' },
{ key: 'marketing', icon: '📢', title: 'Marketing / Publicité', desc: 'Digital, branding, analytics' },
{ key: 'automotive', icon: '🚗', title: 'Automobile', desc: 'Motorisation, ADAS, homologation' },
{ key: 'hvac', title: 'HVAC / Génie climatique', desc: 'Thermique, ventilation, climatisation', icon: Wrench },
{ key: 'construction', title: 'BTP / Construction', desc: 'Gros œuvre, second œuvre, normes', icon: HardHat },
{ key: 'it', title: 'IT / Logiciel', desc: 'Développement, infrastructure, DevOps', icon: Monitor },
{ key: 'legal', title: 'Juridique / Contrats', desc: 'Droit des affaires, contentieux', icon: Scale },
{ key: 'medical', title: 'Médical / Santé', desc: 'Pharmacologie, chirurgie, diagnostic', icon: Stethoscope },
{ key: 'finance', title: 'Finance / Comptabilité', desc: 'IFRS, bilans, fiscalité', icon: BarChart3 },
{ key: 'marketing', title: 'Marketing / Publicité', desc: 'Digital, branding, analytics', icon: Megaphone },
{ key: 'automotive', title: 'Automobile', desc: 'Motorisation, ADAS, homologation', icon: Car },
];
export default function ContextGlossaryPage() {
@@ -88,107 +89,137 @@ export default function ContextGlossaryPage() {
if (!isPro) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-6 p-6">
<div className="flex items-center justify-center w-20 h-20 rounded-2xl bg-violet-100 dark:bg-violet-900/30">
<Crown className="w-10 h-10 text-violet-600 dark:text-violet-400" />
<div className="w-20 h-20 bg-brand-dark dark:bg-white/10 rounded-[32px] flex items-center justify-center text-brand-accent shadow-2xl">
<Crown className="w-10 h-10" />
</div>
<div className="text-center space-y-2 max-w-md">
<h1 className="text-2xl font-bold text-foreground">{t('context.proTitle')}</h1>
<p className="text-muted-foreground">
{t('context.proDesc')}
</p>
<h1 className="text-3xl font-black uppercase tracking-tighter text-brand-dark dark:text-white">{t('context.proTitle')}</h1>
<p className="text-brand-dark/40 dark:text-white/40 font-medium">{t('context.proDesc')}</p>
</div>
<Button asChild size="lg">
<Link href="/pricing">
<Crown className="w-4 h-4 me-2" /> {t('context.viewPlans')}
</Link>
</Button>
<Link href="/pricing">
<button className="premium-button px-10 py-4 text-[11px] uppercase tracking-widest !rounded-2xl flex items-center gap-2">
<Crown size={14} /> {t('context.viewPlans')}
</button>
</Link>
</div>
);
}
return (
<div className="flex flex-col gap-6 p-6 lg:p-8 max-w-3xl">
<div>
<h1 className="text-2xl font-bold text-foreground">{t('context.title')}</h1>
<p className="text-sm text-muted-foreground mt-1">
{t('context.subtitle')}
</p>
<div className="max-w-5xl mx-auto w-full p-6 lg:p-8">
{/* ── Editorial Header ───────────────────────────────────── */}
<div className="mb-12">
<span className="accent-pill mb-4 block w-fit italic">{t('context.presets.title')}</span>
<h1 className="text-5xl font-black uppercase tracking-tighter mb-4 leading-none text-brand-dark dark:text-white">
{t('context.title')}
</h1>
<p className="text-brand-dark/40 dark:text-white/40 font-medium max-w-2xl">{t('context.subtitle')}</p>
</div>
{/* Presets */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Zap className="w-4 h-4 text-accent" /> {t('context.presets.title')}
</CardTitle>
<CardDescription>{t('context.presets.desc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{PRESETS.map(p => (
<button
key={p.key}
onClick={() => handleApplyPreset(p.key)}
className="flex flex-col items-center gap-1 p-3 rounded-xl border border-border text-center transition-colors hover:border-primary/40 hover:bg-primary/5"
>
<span className="text-2xl">{p.icon}</span>
<span className="text-xs font-medium text-foreground leading-tight">{p.title}</span>
<span className="text-[10px] text-muted-foreground leading-tight">{p.desc}</span>
</button>
))}
<div className="space-y-12">
{/* ── Professional Presets ──────────────────────────────── */}
<section className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial">
<div className="flex items-center gap-4 mb-8 text-brand-accent">
<Zap size={20} />
<h3 className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('context.presets.title')}
</h3>
</div>
</CardContent>
</Card>
<p className="text-sm text-brand-dark/40 dark:text-white/40 mb-12 font-medium">{t('context.presets.desc')}</p>
{/* System Prompt */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" /> {t('context.instructions.title')}
</CardTitle>
<CardDescription>{t('context.instructions.desc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Textarea
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{PRESETS.map((p) => {
const Icon = p.icon;
return (
<button
key={p.key}
onClick={() => handleApplyPreset(p.key)}
className="p-6 bg-brand-muted dark:bg-white/5 hover:bg-brand-dark dark:hover:bg-brand-dark group transition-all rounded-[32px] text-center border border-black/5 dark:border-white/10 hover:shadow-2xl hover:-translate-y-1"
>
<div className="flex justify-center mb-4 text-brand-dark group-hover:text-brand-accent group-hover:scale-125 transition-all">
<Icon size={24} />
</div>
<h4 className="text-[11px] font-black uppercase tracking-tight text-brand-dark dark:text-white group-hover:text-white mb-2">
{p.title}
</h4>
<p className="text-[8px] text-brand-dark/30 dark:text-white/30 group-hover:text-white/40 font-bold uppercase tracking-widest leading-relaxed">
{p.desc}
</p>
</button>
);
})}
</div>
</section>
{/* ── Context Instructions ──────────────────────────────── */}
<section className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial">
<div className="flex items-center gap-4 mb-8 text-brand-accent">
<MessageSquare size={20} />
<h3 className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('context.instructions.title')}
</h3>
</div>
<p className="text-sm text-brand-dark/40 dark:text-white/40 mb-10 font-medium">{t('context.instructions.desc')}</p>
<textarea
value={localSettings.systemPrompt}
onChange={e => setLocalSettings({ ...localSettings, systemPrompt: e.target.value })}
placeholder={t('context.instructions.placeholder')}
className="min-h-[140px] resize-y"
className="w-full h-48 p-8 bg-brand-muted dark:bg-white/5 rounded-[32px] border border-black/5 dark:border-white/10 text-sm focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30 transition-all outline-none resize-y"
/>
</CardContent>
</Card>
<div className="flex justify-end mt-6 gap-4">
<button
onClick={handleClear}
className="px-8 py-3 bg-brand-muted dark:bg-white/5 text-brand-dark/40 dark:text-white/40 rounded-xl text-[9px] font-black uppercase tracking-widest hover:text-brand-dark dark:hover:text-white transition-all"
>
<Trash2 size={12} className="inline mr-2" />{t('context.clearAll')}
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="premium-button px-10 py-3 text-[9px] uppercase tracking-widest !rounded-xl flex items-center gap-3 disabled:opacity-50"
>
{isSaving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
{isSaving ? t('context.saving') : t('context.save')}
</button>
</div>
</section>
{/* Glossary */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<BookOpen className="w-4 h-4 text-emerald-500" /> {t('context.glossary.title')}
</CardTitle>
<CardDescription>{t('context.glossary.desc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Textarea
{/* ── Technical Glossary ────────────────────────────────── */}
<section className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial">
<div className="flex items-center gap-4 mb-8 text-brand-accent">
<Database size={20} />
<h3 className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('context.glossary.title')}
</h3>
</div>
<p className="text-sm text-brand-dark/40 dark:text-white/40 mb-10 font-medium">{t('context.glossary.desc')}</p>
<textarea
value={localSettings.glossary}
onChange={e => setLocalSettings({ ...localSettings, glossary: e.target.value })}
placeholder={"pression statique=static pressure\nrécupérateur de chaleur=heat recovery unit"}
className="min-h-[250px] resize-y font-mono text-sm"
className="w-full h-64 p-8 bg-brand-muted dark:bg-white/5 rounded-[32px] border border-black/5 dark:border-white/10 text-sm font-mono focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30 transition-all outline-none resize-y"
/>
{localSettings.glossary && (
<p className="text-xs text-muted-foreground">
{localSettings.glossary.split('\n').filter(l => l.includes('=')).length} {t('context.glossary.terms')}
</p>
)}
</CardContent>
</Card>
{/* Actions */}
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={handleClear} className="text-destructive hover:text-destructive/80 hover:bg-destructive/10">
<Trash2 className="me-1.5 h-4 w-4" /> {t('context.clearAll')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? <><Loader2 className="me-1.5 h-4 w-4 animate-spin" />{t('context.saving')}</> : <><Save className="me-1.5 h-4 w-4" />{t('context.save')}</>}
</Button>
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mt-6 gap-4">
<span className="text-[9px] text-brand-dark/20 dark:text-white/20 font-black uppercase tracking-widest">
{localSettings.glossary
? `${localSettings.glossary.split('\n').filter(l => l.includes('=')).length} ${t('context.glossary.terms')}`
: `0 ${t('context.glossary.terms')}`}
</span>
<div className="flex gap-4">
<button className="px-8 py-3 bg-brand-muted dark:bg-white/5 text-brand-dark/40 dark:text-white/40 rounded-xl text-[9px] font-black uppercase tracking-widest hover:text-brand-dark dark:hover:text-white transition-all">
{t('context.clearAll')}
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="premium-button px-10 py-3 text-[9px] uppercase tracking-widest !rounded-xl flex items-center gap-3 disabled:opacity-50"
>
{isSaving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
{isSaving ? t('context.saving') : t('context.save')}
</button>
</div>
</div>
</section>
</div>
</div>
);

View File

@@ -1,12 +1,9 @@
'use client';
import { useState } from 'react';
import { BookText, Plus } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { BookText, Plus, Library, Calendar, Hash } from 'lucide-react';
import { useUser } from '@/app/dashboard/useUser';
import { useI18n } from '@/lib/i18n';
import { useI18n, formatDate } from '@/lib/i18n';
import { useGlossaries, useGlossary } from './useGlossaries';
import type { Glossary, GlossaryTermInput, GlossaryListItem } from './types';
import { ProUpgradePrompt } from './ProUpgradePrompt';
@@ -15,6 +12,7 @@ import { CreateGlossaryDialog } from './CreateGlossaryDialog';
import { EditGlossaryDialog } from './EditGlossaryDialog';
import { DeleteGlossaryDialog } from './DeleteGlossaryDialog';
import { useToast } from '@/components/ui/toast';
import { cn } from '@/lib/utils';
export default function GlossariesPage() {
const { t } = useI18n();
@@ -141,8 +139,8 @@ export default function GlossariesPage() {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-muted border-t-foreground mx-auto"></div>
<p className="text-sm text-muted-foreground">Loading...</p>
<div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-muted border-t-brand-accent mx-auto"></div>
<p className="text-[9px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">Loading...</p>
</div>
</div>
);
@@ -153,77 +151,93 @@ export default function GlossariesPage() {
}
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">{t('glossaries.title')}</h1>
<p className="text-muted-foreground">{t('glossaries.description')}</p>
<div className="max-w-6xl mx-auto w-full p-6 lg:p-8">
{/* ── Editorial Header ───────────────────────────────────── */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end mb-12 gap-6">
<div>
<span className="accent-pill mb-4 block w-fit">{t('glossaries.yourGlossaries')}</span>
<h1 className="text-5xl font-black uppercase tracking-tighter mb-4 leading-none text-brand-dark dark:text-white">
{t('glossaries.title')}
</h1>
<p className="text-brand-dark/40 dark:text-white/40 font-medium">{t('glossaries.description')}</p>
</div>
<button
onClick={() => setCreateDialogOpen(true)}
disabled={isCreating}
className="premium-button px-10 py-4 text-[11px] uppercase tracking-widest !rounded-2xl flex items-center gap-2 disabled:opacity-50 shrink-0"
>
<Plus size={14} />
{t('glossaries.createNew')}
</button>
</div>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<div className="flex size-8 items-center justify-center rounded-lg bg-accent/10">
<BookText className="size-4 text-accent" />
</div>
<div>
<CardTitle className="text-base">{t('glossaries.yourGlossaries')}</CardTitle>
<CardDescription>{t('glossaries.yourGlossariesDesc')}</CardDescription>
</div>
{/* ── Glossary Grid ──────────────────────────────────────── */}
{glossaries.length === 0 ? (
<div className="editorial-card p-16 bg-white dark:bg-[#141414] border-none shadow-editorial text-center">
<div className="w-16 h-16 bg-brand-muted dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent mx-auto mb-6">
<Library size={32} />
</div>
</CardHeader>
<p className="text-xl font-black uppercase tracking-tight text-brand-dark dark:text-white mb-2">{t('glossaries.empty')}</p>
<p className="text-[10px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest">{t('glossaries.emptyDesc')}</p>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{glossaries.map((glossary: GlossaryListItem) => {
const termCount = glossary.terms_count ?? 0;
return (
<div
key={glossary.id}
className="editorial-card p-8 bg-white dark:bg-[#141414] border-none shadow-editorial group hover:-translate-y-2 transition-all cursor-pointer"
onClick={() => handleEditClick(glossary.id)}
>
<div className="flex justify-between items-start mb-10">
<div className="w-12 h-12 bg-brand-muted dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent group-hover:bg-brand-dark group-hover:text-white transition-all">
<Library size={24} />
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(glossary.id, glossary.name);
}}
className="text-[8px] bg-red-500/10 text-red-500 px-3 py-1 rounded-full font-black uppercase tracking-widest hover:bg-red-500 hover:text-white transition-all"
>
Delete
</button>
</div>
<h3 className="text-2xl font-black uppercase tracking-tight mb-2 text-brand-dark dark:text-white truncate">
{glossary.name}
</h3>
<div className="flex justify-between items-center pt-8 border-t border-black/5 dark:border-white/10">
<span className="text-[9px] text-brand-dark/30 dark:text-white/30 font-black uppercase tracking-widest flex items-center gap-1.5">
<Hash size={10} />
{termCount} {t('glossaries.defineTerms')}
</span>
<span className="text-[9px] text-brand-dark/30 dark:text-white/30 font-black uppercase tracking-widest flex items-center gap-1.5">
<Calendar size={10} />
{new Date(glossary.created_at).toLocaleDateString()}
</span>
</div>
</div>
);
})}
</div>
)}
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">
{t(total !== 1 ? 'glossaries.count_other' : 'glossaries.count_one', { count: String(total) })}
</p>
<p className="text-xs text-muted-foreground">{t('glossaries.defineTerms')}</p>
</div>
<Button
onClick={() => setCreateDialogOpen(true)}
disabled={isCreating}
className="gap-1.5"
>
<Plus className="size-3.5" />
{t('glossaries.createNew')}
</Button>
</div>
{glossaries.length === 0 ? (
<div className="text-center py-8">
<BookText className="size-12 mx-auto text-muted-foreground/50 mb-4" />
<p className="text-muted-foreground">{t('glossaries.empty')}</p>
<p className="text-sm text-muted-foreground/80">{t('glossaries.emptyDesc')}</p>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{glossaries.map((glossary: GlossaryListItem) => (
<GlossaryCard
key={glossary.id}
glossary={glossary}
onEdit={handleEditClick}
onDelete={handleDeleteClick}
isDeleting={isDeleting && glossaryToDelete?.id === glossary.id}
/>
))}
</div>
)}
</CardContent>
</Card>
<Separator />
<Card className="border-border/50">
<CardHeader>
<CardTitle className="text-sm">{t('glossaries.aboutTitle')}</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>{t('glossaries.aboutDesc')}</p>
<p><strong>{t('glossaries.aboutFormat')}</strong></p>
</CardContent>
</Card>
{/* ── About section ──────────────────────────────────────── */}
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial mt-12">
<div className="flex items-center gap-4 text-brand-accent mb-8">
<BookText size={20} />
<span className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('glossaries.aboutTitle')}
</span>
</div>
<p className="text-sm text-brand-dark/40 dark:text-white/40 font-medium mb-4">{t('glossaries.aboutDesc')}</p>
<p className="text-[10px] font-bold uppercase tracking-tight text-brand-dark/60 dark:text-white/60">
{t('glossaries.aboutFormat')}
</p>
</div>
{/* Dialogs */}
<CreateGlossaryDialog
open={createDialogOpen}
onOpenChange={setCreateDialogOpen}

View File

@@ -10,6 +10,7 @@ import {
FileText, Layers, CreditCard, TrendingUp, AlertTriangle,
CheckCircle2, XCircle, RefreshCw, ExternalLink, ArrowRight,
BadgeCheck, ShieldAlert, Info, Globe, Settings, Palette, Trash2, Loader2,
Activity, ChevronDown,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -29,13 +30,6 @@ import { cn } from '@/lib/utils';
const PLAN_ICONS: Record<string, React.ElementType> = {
free: Sparkles, starter: Zap, pro: Crown, business: Building2, enterprise: Rocket,
};
const PLAN_COLORS: Record<string, { badge: string; gradient: string; ring: string }> = {
free: { badge: 'bg-muted text-muted-foreground border border-border', gradient: 'from-slate-600 to-slate-700', ring: 'ring-slate-500/30' },
starter: { badge: 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-200', gradient: 'from-blue-600 to-blue-700', ring: 'ring-blue-500/30' },
pro: { badge: 'bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-200', gradient: 'from-violet-600 to-violet-700', ring: 'ring-violet-500/30' },
business: { badge: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-200', gradient: 'from-emerald-600 to-emerald-700', ring: 'ring-emerald-500/30' },
enterprise: { badge: 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-200', gradient: 'from-amber-600 to-amber-700', ring: 'ring-amber-500/30' },
};
const PLAN_LABELS: Record<string, string> = {
free: 'profile.plan.free', starter: 'profile.plan.starter', pro: 'profile.plan.pro', business: 'profile.plan.business', enterprise: 'profile.plan.enterprise',
};
@@ -56,30 +50,6 @@ function nextResetDate(locale: Locale) {
return formatDate(next, locale, { day: 'numeric', month: 'long' });
}
function UsageBar({ label, used, limit, icon }: { label: string; used: number; limit: number; icon: React.ReactNode }) {
const p = pct(used, limit);
const isUnlimited = limit === -1;
return (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2 text-muted-foreground">{icon} {label}</div>
<span className={cn(
'font-mono text-xs font-medium',
isUnlimited ? 'text-emerald-600 dark:text-emerald-400' :
p >= 90 ? 'text-red-600 dark:text-red-400' : p >= 70 ? 'text-amber-600 dark:text-amber-400' : 'text-muted-foreground'
)}>
{isUnlimited ? '∞' : `${used} / ${limit}`}
</span>
</div>
{!isUnlimited && (
<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-primary')} style={{ width: `${p}%` }} />
</div>
)}
</div>
);
}
const UI_LANGUAGES: { value: Locale; label: string; flag: string }[] = [
{ value: 'en', label: 'English', flag: '🇬🇧' },
{ value: 'fr', label: 'Français', flag: '🇫🇷' },
@@ -180,14 +150,13 @@ export default function ProfilePage() {
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<RefreshCw className="w-8 h-8 text-primary animate-spin" />
<RefreshCw className="w-8 h-8 text-brand-accent animate-spin" />
</div>
);
}
const planId = user?.plan ?? user?.tier ?? 'free';
const planLabel = t(PLAN_LABELS[planId] ?? planId);
const planColors = PLAN_COLORS[planId] ?? PLAN_COLORS.free;
const PlanIcon = PLAN_ICONS[planId] ?? Sparkles;
const planPrice = PLAN_PRICES[planId];
const isFreePlan = planId === 'free';
@@ -199,14 +168,23 @@ export default function ProfilePage() {
const pagesLimit = usage?.pages_limit ?? user?.plan_limits?.max_pages_per_doc ?? 50;
const extraCredits = usage?.extra_credits ?? user?.extra_credits ?? 0;
const docsPct = pct(docsUsed, docsLimit);
const pagesPct = pct(pagesUsed, pagesLimit);
// Tab state for editorial pill toggle
const [activeTab, setActiveTab] = useState('account');
return (
<div className="flex h-full flex-col overflow-y-auto">
<div className="flex flex-1 flex-col gap-6 p-6 lg:p-8 max-w-3xl">
<div className="flex flex-1 flex-col gap-8 p-6 lg:p-8 max-w-4xl mx-auto w-full">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-foreground">{t('profile.header.title')}</h1>
<p className="text-sm text-muted-foreground mt-1">{t('profile.header.subtitle')}</p>
{/* ── Editorial Header ───────────────────────────────────── */}
<div className="mb-4">
<span className="accent-pill mb-4 block w-fit">{t('profile.header.title')}</span>
<h1 className="text-5xl font-black uppercase tracking-tighter mb-4 leading-none text-brand-dark dark:text-white">
{t('profile.header.title')}
</h1>
<p className="text-brand-dark/40 dark:text-white/40 font-medium">{t('profile.header.subtitle')}</p>
</div>
{/* Status message */}
@@ -223,273 +201,401 @@ export default function ProfilePage() {
</div>
)}
{/* Tabs */}
<Tabs defaultValue="account" className="w-full">
<TabsList className="w-full justify-start">
<TabsTrigger value="account" className="gap-1.5"><User className="size-3.5" /> {t('profile.tabs.account')}</TabsTrigger>
<TabsTrigger value="subscription" className="gap-1.5"><CreditCard className="size-3.5" /> {t('profile.tabs.subscription')}</TabsTrigger>
<TabsTrigger value="preferences" className="gap-1.5"><Settings className="size-3.5" /> {t('profile.tabs.preferences')}</TabsTrigger>
</TabsList>
{/* ── Editorial Pill Tabs ────────────────────────────────── */}
<div className="flex gap-2 p-2 bg-brand-muted dark:bg-[#141414] rounded-2xl w-fit border border-black/5 dark:border-white/10">
{[
{ key: 'account', label: t('profile.tabs.account'), icon: User },
{ key: 'subscription', label: t('profile.tabs.subscription'), icon: CreditCard },
{ key: 'preferences', label: t('profile.tabs.preferences'), icon: Settings },
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={cn(
'px-10 py-3 rounded-xl text-[11px] font-black uppercase tracking-widest transition-all flex items-center gap-2',
activeTab === tab.key
? 'bg-brand-dark text-white dark:bg-white dark:text-brand-dark shadow-xl'
: 'text-brand-dark/30 dark:text-white/30 hover:text-brand-dark dark:hover:text-white'
)}
>
<tab.icon size={14} />
{tab.label}
</button>
))}
</div>
{/* ── Tab: Account ────────────────────────────────────── */}
<TabsContent value="account" className="space-y-6 pt-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-5">
<div className={cn(
'flex shrink-0 items-center justify-center w-20 h-20 rounded-2xl text-white font-bold text-2xl ring-4',
`bg-gradient-to-br ${planColors.gradient}`, planColors.ring
)}>
{getInitials(user?.name)}
</div>
<div className="flex-1 min-w-0 space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
<h2 className="text-xl font-semibold text-foreground truncate">{user?.name || t('profile.account.user')}</h2>
<Badge className={cn('text-xs font-medium', planColors.badge)}>
<PlanIcon className="w-3 h-3 me-1" />{planLabel}
</Badge>
</div>
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Mail className="w-3.5 h-3.5 shrink-0" />
<span className="truncate">{user?.email}</span>
</div>
{user?.created_at && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Calendar className="w-3 h-3 shrink-0" />
<span>{t('profile.account.memberSince')} {formatDate(new Date(user.created_at), locale)}</span>
</div>
)}
</div>
{/* ── Tab: Account ────────────────────────────────────── */}
{activeTab === 'account' && (
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial flex flex-col sm:flex-row items-center gap-8 lg:gap-10">
<div className="w-24 h-24 bg-brand-dark dark:bg-white/10 rounded-[32px] flex items-center justify-center text-white dark:text-white text-4xl font-black shadow-2xl shrink-0">
{getInitials(user?.name)}
</div>
<div className="flex-1 text-center sm:text-left">
<div className="flex items-center gap-4 mb-3 flex-wrap justify-center sm:justify-start">
<h2 className="text-2xl lg:text-3xl font-black uppercase tracking-tight text-brand-dark dark:text-white">
{user?.name || t('profile.account.user')}
</h2>
<span className="accent-pill !px-3 !py-1 text-[9px] flex items-center gap-1.5">
<PlanIcon size={12} />
{planLabel}
</span>
</div>
<p className="text-[11px] font-bold uppercase tracking-widest text-brand-dark/40 dark:text-white/40 mb-3 flex items-center gap-2 justify-center sm:justify-start">
<Globe size={12} className="text-brand-accent" />
{user?.email}
</p>
{user?.created_at && (
<p className="text-[9px] text-brand-dark/20 dark:text-white/20 font-black uppercase tracking-widest">
{t('profile.account.memberSince')} {formatDate(new Date(user.created_at), locale)}
</p>
)}
</div>
</div>
)}
{/* ── Tab: Subscription ───────────────────────────────── */}
{activeTab === 'subscription' && (
<div className="space-y-8">
{/* Plan card - dark editorial */}
<div className="editorial-card !bg-brand-dark p-10 lg:p-12 flex flex-col md:flex-row items-start md:items-center justify-between text-white border-none shadow-2xl relative overflow-hidden gap-6">
{/* Decorative element */}
<div className="absolute top-0 right-0 w-32 h-32 bg-brand-accent/10 rounded-bl-full pointer-events-none" />
<div className="flex items-center gap-8 relative z-10">
<div className="w-16 h-16 bg-white/10 backdrop-blur-xl rounded-2xl flex items-center justify-center text-brand-accent border border-white/10 shadow-inner shrink-0">
<PlanIcon size={28} />
</div>
</CardContent>
</Card>
</TabsContent>
{/* ── Tab: Subscription ───────────────────────────────── */}
<TabsContent value="subscription" className="space-y-6 pt-4">
{/* Plan card */}
<Card>
<CardContent className="pt-6 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={cn('p-3 rounded-xl bg-gradient-to-br', planColors.gradient)}>
<PlanIcon className="w-6 h-6 text-white" />
</div>
<div>
<p className="font-semibold text-foreground text-lg">{t('profile.plan.label')} {planLabel}</p>
<p className="text-sm text-muted-foreground">{isFreePlan ? t('profile.plan.free') : t('profile.plan.pricePerMonth', { price: planPrice })}</p>
</div>
</div>
{!isFreePlan && (
<Badge variant="secondary" className={cn(
'text-xs',
isCanceling ? 'text-warning border-warning/30 bg-warning/10' :
user?.subscription_status === 'active' ? 'text-success border-success/30 bg-success/10' :
'text-destructive border-destructive/30 bg-destructive/10'
<div>
<div className="flex items-center gap-3 flex-wrap">
<h3 className="text-2xl font-black uppercase tracking-tight text-white">
{t('profile.plan.label')} {planLabel}
</h3>
<span className={cn(
'px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border shadow-sm',
isCanceling
? 'bg-amber-500/20 text-amber-300 border-amber-500/30'
: user?.subscription_status === 'active' || isFreePlan
? 'bg-brand-accent/20 text-brand-accent border-brand-accent/30'
: 'bg-red-500/20 text-red-300 border-red-500/30'
)}>
{isCanceling ? t('profile.subscription.canceling') : user?.subscription_status === 'active' ? t('profile.subscription.active') : user?.subscription_status ?? t('profile.subscription.unknown')}
</Badge>
)}
</div>
{!isFreePlan && user?.subscription_ends_at && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted text-sm">
<Info className="w-4 h-4 text-muted-foreground shrink-0" />
<span className="text-muted-foreground">
{isCanceling
? `${t('profile.subscription.accessUntil')} ${formatDate(new Date(user.subscription_ends_at), locale)}`
: `${t('profile.subscription.renewalOn')} ${formatDate(new Date(user.subscription_ends_at), locale)}`}
{isCanceling ? t('profile.subscription.canceling') : user?.subscription_status === 'active' ? t('profile.subscription.active') : isFreePlan ? t('profile.subscription.active') : user?.subscription_status ?? t('profile.subscription.unknown')}
</span>
</div>
)}
<div className="flex flex-wrap gap-2">
<Button asChild size="sm"><Link href="/pricing"><TrendingUp className="w-3.5 h-3.5 me-1.5" />{isFreePlan ? t('profile.subscription.upgradePlan') : t('profile.subscription.changePlan')}</Link></Button>
</div>
</CardContent>
</Card>
{/* Usage */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<BadgeCheck className="w-4 h-4 text-primary" />
{t('profile.usage.title')}
<span className="ms-auto text-xs text-muted-foreground font-normal">{t('profile.usage.resetOn')} {nextResetDate(locale)}</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<UsageBar label={t('profile.usage.documents')} used={docsUsed} limit={docsLimit} icon={<FileText className="w-4 h-4" />} />
<UsageBar label={t('profile.usage.pages')} used={pagesUsed} limit={pagesLimit} icon={<Layers className="w-4 h-4" />} />
{extraCredits > 0 && (
<div className="flex items-center gap-3 p-3 rounded-lg bg-amber-50 border border-amber-200 dark:bg-amber-500/10 dark:border-amber-500/20 text-sm">
<Info className="w-4 h-4 text-amber-600 dark:text-amber-400 shrink-0" />
<span className="text-amber-700 dark:text-amber-300">{extraCredits} {extraCredits > 1 ? t('profile.usage.extraCreditsPlural') : t('profile.usage.extraCredits')}</span>
</div>
)}
{usage?.upgrade_required && (
<div className="flex items-start gap-3 p-3 rounded-lg bg-red-50 border border-red-200 dark:bg-red-500/10 dark:border-red-500/20 text-sm">
<AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-400 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-red-700 dark:text-red-300 font-medium">{t('profile.usage.quotaReached')}</p>
<p className="text-red-600 dark:text-red-400 text-xs mt-0.5">{t('profile.usage.quotaReachedDesc')}</p>
</div>
<Button asChild size="sm" className="bg-red-600 hover:bg-red-500 shrink-0"><Link href="/pricing"><ArrowRight className="w-3.5 h-3.5" /></Link></Button>
</div>
)}
{isFreePlan && (
<div className="flex items-center gap-3 p-3 rounded-lg bg-primary/10 border border-primary/20 text-sm">
<Zap className="w-4 h-4 text-primary shrink-0" />
<span className="text-primary flex-1">{t('profile.usage.unlockMore')}</span>
<Button asChild size="sm" variant="outline" className="border-primary/30 text-primary hover:bg-primary/10 shrink-0">
<Link href="/pricing">{t('profile.usage.viewPlans')} <ArrowRight className="w-3.5 h-3.5 ms-1" /></Link>
</Button>
</div>
)}
</CardContent>
</Card>
{/* Features */}
{user?.plan_limits?.features?.length > 0 && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
{t('profile.usage.includedInPlan')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{user.plan_limits.features.map((f: string, i: number) => (
<div key={i} className="flex items-start gap-2 text-sm">
<CheckCircle2 className="w-4 h-4 text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5" />
<span className="text-muted-foreground">{f.includes('.') ? t(f) : f}</span>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Danger zone - only show if user has a real Stripe subscription */}
{!isFreePlan && !isCanceling && user?.stripe_subscription_id && (
<Card className="border-red-900/30">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2 text-red-600 dark:text-red-400">
<ShieldAlert className="w-4 h-4" /> {t('profile.danger.title')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
{t('profile.danger.description')}
<p className="text-white/60 text-[10px] font-black uppercase tracking-widest mt-1">
{isFreePlan ? t('profile.plan.free') : t('profile.plan.pricePerMonth', { price: planPrice })}
</p>
{cancelConfirm && (
<div className="p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-sm text-destructive">
{t('profile.danger.confirm')}
</div>
)}
<div className="flex gap-2">
<Button variant="destructive" size="sm" onClick={handleCancel} disabled={loadingCancel}>
{loadingCancel && <RefreshCw className="w-3.5 h-3.5 me-1.5 animate-spin" />}
{cancelConfirm ? t('profile.danger.confirmCancel') : t('profile.danger.cancelSubscription')}
</Button>
{cancelConfirm && <Button variant="outline" size="sm" onClick={() => setCancelConfirm(false)}>{t('profile.danger.keep')}</Button>}
</div>
</CardContent>
</Card>
</div>
</div>
{!isFreePlan && (
<button
onClick={handleBillingPortal}
disabled={loadingPortal}
className="relative z-10 px-10 py-4 bg-white text-brand-dark rounded-xl font-black text-[10px] uppercase tracking-widest shadow-xl hover:scale-105 transition-all shrink-0 disabled:opacity-50"
>
{loadingPortal ? <RefreshCw className="w-4 h-4 animate-spin" /> : t('profile.subscription.changePlan')}
</button>
)}
{isFreePlan && (
<Link href="/pricing">
<button className="relative z-10 px-10 py-4 bg-white text-brand-dark rounded-xl font-black text-[10px] uppercase tracking-widest shadow-xl hover:scale-105 transition-all shrink-0">
{t('profile.subscription.upgradePlan')}
</button>
</Link>
)}
</div>
{/* Subscription ends info */}
{!isFreePlan && user?.subscription_ends_at && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-brand-muted dark:bg-white/5 text-sm">
<Info className="w-4 h-4 text-brand-dark/40 dark:text-white/40 shrink-0" />
<span className="text-brand-dark/60 dark:text-white/60">
{isCanceling
? `${t('profile.subscription.accessUntil')} ${formatDate(new Date(user.subscription_ends_at), locale)}`
: `${t('profile.subscription.renewalOn')} ${formatDate(new Date(user.subscription_ends_at), locale)}`}
</span>
</div>
)}
</TabsContent>
{/* ── Tab: Preferences ────────────────────────────────── */}
<TabsContent value="preferences" className="space-y-6 pt-4">
{/* Usage metrics */}
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial">
<div className="flex justify-between items-center mb-12">
<div className="flex items-center gap-4 text-brand-accent">
<Activity size={20} />
<span className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('profile.usage.title')}
</span>
</div>
<span className="text-[9px] font-black text-brand-dark/20 dark:text-white/20 uppercase tracking-widest border-b border-black/5 dark:border-white/10 pb-1">
{t('profile.usage.resetOn')} {nextResetDate(locale)}
</span>
</div>
{/* Interface language */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Globe className="w-4 h-4 text-primary" /> {t('profile.prefs.interfaceLang')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{UI_LANGUAGES.map((lang) => (
<button
key={lang.value}
onClick={() => setLocale(lang.value)}
<div className="space-y-16">
{/* Documents bar */}
<div>
<div className="flex justify-between text-[10px] font-black uppercase tracking-widest mb-4">
<span className="flex items-center gap-3 text-brand-dark/40 dark:text-white/40">
<FileText size={16} className="text-brand-accent" /> {t('profile.usage.documents')}
</span>
<span className="text-brand-dark dark:text-white font-black">{docsUsed} / {fmtLimit(docsLimit)}</span>
</div>
<div className="h-2 bg-brand-muted dark:bg-white/10 rounded-full overflow-hidden p-0.5 border border-black/5 dark:border-white/10">
<div
className={cn(
'flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium border transition-colors',
locale === lang.value
? 'bg-primary/10 border-primary/40 text-primary'
: 'bg-transparent border-border text-muted-foreground hover:text-foreground hover:border-border/80'
'h-full rounded-full transition-all duration-700',
docsPct >= 90 ? 'bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.5)]' :
docsPct >= 70 ? 'bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.5)]' :
'bg-brand-accent shadow-[0_0_10px_rgba(197,161,122,0.5)]'
)}
>
<span>{lang.flag}</span><span>{lang.label}</span>
style={{ width: `${docsPct}%` }}
/>
</div>
</div>
{/* Pages bar */}
<div>
<div className="flex justify-between text-[10px] font-black uppercase tracking-widest mb-4">
<span className="flex items-center gap-3 text-brand-dark/40 dark:text-white/40">
<Layers size={16} className="text-brand-accent" /> {t('profile.usage.pages')}
</span>
<span className="text-brand-dark dark:text-white font-black">{pagesUsed} / {fmtLimit(pagesLimit)}</span>
</div>
<div className="h-2 bg-brand-muted dark:bg-white/10 rounded-full overflow-hidden p-0.5 border border-black/5 dark:border-white/10">
<div
className={cn(
'h-full rounded-full transition-all duration-700',
pagesPct >= 90 ? 'bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.5)]' :
pagesPct >= 70 ? 'bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.5)]' :
'bg-brand-accent shadow-[0_0_10px_rgba(197,161,122,0.5)]'
)}
style={{ width: `${pagesPct}%` }}
/>
</div>
</div>
</div>
{/* Extra credits */}
{extraCredits > 0 && (
<div className="mt-12 flex items-center gap-3 p-4 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-sm">
<Info className="w-4 h-4 text-amber-600 dark:text-amber-400 shrink-0" />
<span className="text-amber-700 dark:text-amber-300">
{extraCredits} {extraCredits > 1 ? t('profile.usage.extraCreditsPlural') : t('profile.usage.extraCredits')}
</span>
</div>
)}
{/* Quota reached warning */}
{usage?.upgrade_required && (
<div className="mt-8 flex items-start gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 text-sm">
<AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-400 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-red-700 dark:text-red-300 font-medium">{t('profile.usage.quotaReached')}</p>
<p className="text-red-600 dark:text-red-400 text-xs mt-0.5">{t('profile.usage.quotaReachedDesc')}</p>
</div>
<Link href="/pricing">
<button className="px-4 py-2 bg-red-600 text-white rounded-xl text-[9px] font-black uppercase tracking-widest hover:bg-red-500 transition-all shrink-0">
<ArrowRight size={14} />
</button>
</Link>
</div>
)}
{/* Upgrade CTA */}
{isFreePlan && (
<div className="mt-12 p-8 bg-brand-muted dark:bg-white/5 rounded-[32px] border border-black/5 dark:border-white/10 flex flex-col sm:flex-row items-center justify-between gap-4 group">
<div className="flex items-center gap-6 text-brand-dark dark:text-white">
<div className="w-12 h-12 bg-white dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent shadow-sm group-hover:rotate-12 transition-transform">
<Zap size={24} />
</div>
<p className="text-[11px] font-black uppercase tracking-tight max-w-[200px] leading-relaxed">
{t('profile.usage.unlockMore')}
</p>
</div>
<Link href="/pricing">
<button className="px-8 py-3 bg-brand-dark dark:bg-brand-accent text-white dark:text-brand-dark rounded-xl text-[9px] font-black uppercase tracking-widest shadow-lg hover:bg-brand-accent hover:text-brand-dark transition-all">
{t('profile.usage.viewPlans')}
</button>
</Link>
</div>
)}
</div>
{/* Features grid */}
{user?.plan_limits?.features?.length > 0 && (
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial">
<div className="flex items-center gap-4 text-brand-accent mb-12">
<CheckCircle2 size={20} />
<span className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('profile.usage.includedInPlan')}
</span>
</div>
<div className="grid md:grid-cols-2 gap-y-6 gap-x-12">
{user.plan_limits.features.map((f: string, i: number) => (
<div key={i} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full border border-brand-accent/30 flex items-center justify-center bg-brand-accent/5 shrink-0">
<CheckCircle2 size={12} className="text-brand-accent" />
</div>
<span className="text-[10px] font-bold uppercase tracking-tight text-brand-dark/60 dark:text-white/60">
{f.includes('.') ? t(f) : f}
</span>
</div>
))}
</div>
<p className="text-xs text-muted-foreground mt-3">{t('profile.prefs.interfaceLangDesc')}</p>
</CardContent>
</Card>
</div>
)}
{/* Danger zone */}
{!isFreePlan && !isCanceling && user?.stripe_subscription_id && (
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial border-l-4 border-l-red-500">
<div className="flex items-center gap-4 text-red-500 mb-8">
<ShieldAlert size={20} />
<span className="text-[11px] font-black uppercase tracking-[0.3em] text-brand-dark dark:text-white">
{t('profile.danger.title')}
</span>
</div>
<p className="text-sm text-brand-dark/40 dark:text-white/40 mb-6">{t('profile.danger.description')}</p>
{cancelConfirm && (
<div className="p-4 rounded-xl bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 text-sm text-red-600 dark:text-red-400 mb-4">
{t('profile.danger.confirm')}
</div>
)}
<div className="flex gap-3">
<button
onClick={handleCancel}
disabled={loadingCancel}
className="px-8 py-3 bg-red-600 text-white rounded-xl text-[9px] font-black uppercase tracking-widest hover:bg-red-500 transition-all disabled:opacity-50"
>
{loadingCancel && <RefreshCw className="w-3.5 h-3.5 me-1.5 animate-spin inline" />}
{cancelConfirm ? t('profile.danger.confirmCancel') : t('profile.danger.cancelSubscription')}
</button>
{cancelConfirm && (
<button
onClick={() => setCancelConfirm(false)}
className="px-8 py-3 bg-brand-muted dark:bg-white/10 text-brand-dark/40 dark:text-white/40 rounded-xl text-[9px] font-black uppercase tracking-widest hover:text-brand-dark dark:hover:text-white transition-all"
>
{t('profile.danger.keep')}
</button>
)}
</div>
</div>
)}
</div>
)}
{/* ── Tab: Preferences ────────────────────────────────── */}
{activeTab === 'preferences' && (
<div className="space-y-8">
{/* Interface language */}
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial">
<div className="flex items-center gap-5 mb-12">
<div className="w-12 h-12 bg-brand-muted dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent">
<Globe size={24} />
</div>
<h3 className="text-2xl font-black uppercase tracking-tight text-brand-dark dark:text-white">
{t('profile.prefs.interfaceLang')}
</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{UI_LANGUAGES.map((lang) => (
<button
key={lang.value}
onClick={() => setLocale(lang.value)}
className={cn(
'px-6 py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest border transition-all',
locale === lang.value
? 'bg-brand-dark dark:bg-white border-brand-dark dark:border-white text-white dark:text-brand-dark shadow-xl'
: 'bg-white dark:bg-white/5 border-black/5 dark:border-white/10 text-brand-dark/30 dark:text-white/30 hover:border-brand-accent/30 hover:text-brand-dark dark:hover:text-white'
)}
>
<span className="mr-1.5">{lang.flag}</span> {lang.label}
</button>
))}
</div>
<p className="mt-12 text-[9px] text-brand-dark/20 dark:text-white/20 font-black uppercase tracking-widest italic border-t border-black/5 dark:border-white/10 pt-6">
{t('profile.prefs.interfaceLangDesc')}
</p>
</div>
{/* Default target language */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<FileText className="w-4 h-4 text-primary" /> {t('profile.prefs.defaultTargetLang')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Select value={defaultLanguage} onValueChange={setDefaultLanguage}>
<SelectTrigger><SelectValue placeholder={t('profile.prefs.selectLanguage')} /></SelectTrigger>
<SelectContent className="max-h-[300px]">
{languages.map((lang) => (
<SelectItem key={lang.code} value={lang.code}>
<span className="flex items-center gap-2"><span>{lang.flag}</span><span>{lang.name}</span></span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">{t('profile.prefs.defaultTargetLangDesc')}</p>
<div className="flex justify-end">
<Button size="sm" onClick={handleSavePrefs}>
<CheckCircle2 className="w-3.5 h-3.5 me-1.5" /> {t('profile.prefs.save')}
</Button>
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
<div className="flex items-center gap-6">
<div className="w-14 h-14 bg-brand-muted dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent shrink-0">
<FileText size={28} />
</div>
</CardContent>
</Card>
<div>
<h3 className="text-2xl font-black uppercase tracking-tight text-brand-dark dark:text-white">
{t('profile.prefs.defaultTargetLang')}
</h3>
<p className="text-brand-dark/30 dark:text-white/30 text-[10px] font-black uppercase tracking-widest mt-2 leading-relaxed">
{t('profile.prefs.defaultTargetLangDesc')}
</p>
</div>
</div>
<div className="flex gap-4 items-center">
<div className="relative">
<Select value={defaultLanguage} onValueChange={setDefaultLanguage}>
<SelectTrigger className="px-8 py-4 bg-brand-muted dark:bg-white/10 rounded-2xl text-[10px] font-black uppercase tracking-widest border border-black/5 dark:border-white/10 w-[200px]">
<SelectValue placeholder={t('profile.prefs.selectLanguage')} />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{languages.map((lang) => (
<SelectItem key={lang.code} value={lang.code}>
<span className="flex items-center gap-2"><span>{lang.flag}</span><span>{lang.name}</span></span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<button onClick={handleSavePrefs} className="premium-button px-10 py-4 text-[10px] uppercase tracking-widest !rounded-2xl">
{t('profile.prefs.save')}
</button>
</div>
</div>
{/* Theme */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Palette className="w-4 h-4 text-primary" /> {t('profile.prefs.theme')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">{t('profile.prefs.themeDesc')}</p>
<ThemeToggle />
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial flex items-center justify-between gap-6">
<div className="flex items-center gap-6">
<div className="w-14 h-14 bg-brand-muted dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent shrink-0">
<Palette size={28} />
</div>
</CardContent>
</Card>
<div>
<h3 className="text-2xl font-black uppercase tracking-tight text-brand-dark dark:text-white">
{t('profile.prefs.theme')}
</h3>
<p className="text-brand-dark/30 dark:text-white/30 text-[10px] font-black uppercase tracking-widest mt-2 leading-relaxed">
{t('profile.prefs.themeDesc')}
</p>
</div>
</div>
<ThemeToggle />
</div>
{/* Cache */}
<Card className="border-border/60">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Trash2 className="w-4 h-4 text-muted-foreground" /> {t('profile.prefs.cache')}
</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between gap-4">
<p className="text-sm text-muted-foreground">{t('profile.prefs.cacheDesc')}</p>
<Button onClick={handleClearCache} disabled={isClearing} variant="outline" size="sm" className="shrink-0">
{isClearing ? <><Loader2 className="me-1.5 h-3.5 w-3.5 animate-spin" />{t('profile.prefs.clearing')}</> : <><Trash2 className="me-1.5 h-3.5 w-3.5" />{t('profile.prefs.clearCache')}</>}
</Button>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="editorial-card p-10 lg:p-12 bg-white dark:bg-[#141414] border-none shadow-editorial flex items-center justify-between gap-6">
<div className="flex items-center gap-6">
<div className="w-14 h-14 bg-brand-muted dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-accent shrink-0">
<Trash2 size={28} />
</div>
<div>
<h3 className="text-2xl font-black uppercase tracking-tight text-brand-dark dark:text-white">
{t('profile.prefs.cache')}
</h3>
<p className="text-brand-dark/30 dark:text-white/30 text-[10px] font-black uppercase tracking-widest mt-2 leading-relaxed">
{t('profile.prefs.cacheDesc')}
</p>
</div>
</div>
<button
onClick={handleClearCache}
disabled={isClearing}
className="px-8 py-3 bg-brand-muted dark:bg-white/10 text-brand-dark/40 dark:text-white/40 rounded-xl text-[9px] font-black uppercase tracking-widest hover:text-brand-dark dark:hover:text-white transition-all disabled:opacity-50"
>
{isClearing ? <><Loader2 className="me-1.5 h-3.5 w-3.5 animate-spin inline" />{t('profile.prefs.clearing')}</> : <><Trash2 className="me-1.5 h-3.5 w-3.5 inline" />{t('profile.prefs.clearCache')}</>}
</button>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -4,13 +4,10 @@ import { useEffect, useRef, useState, useMemo } from 'react';
import {
ShieldCheck, Clock, ArrowRight, RotateCcw, Loader2,
FileSpreadsheet, FileText, Presentation, Upload, X,
Zap, Brain, CheckCircle2, ArrowLeftRight,
Search, Languages, Wrench, Activity, Gauge, Timer,
Download, Plus, TrendingUp, AlertTriangle, FileType,
Zap, CheckCircle2,
Search, Languages, Wrench, Activity, Timer,
Download, AlertTriangle, FileType,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { FileDropZone } from './FileDropZone';
import { useFileUpload } from './useFileUpload';
import { useTranslationConfig } from './useTranslationConfig';
import { useTranslationSubmit } from './useTranslationSubmit';
@@ -76,6 +73,7 @@ export default function TranslatePage() {
const { t } = useI18n();
const lastErrorRef = useRef<string | null>(null);
const replaceInputRef = useRef<HTMLInputElement>(null);
const dropzoneInputRef = useRef<HTMLInputElement>(null);
const [pdfMode, setPdfMode] = useState<'layout' | 'text_only'>('layout');
const [elapsed, setElapsed] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -152,381 +150,471 @@ export default function TranslatePage() {
const qualityLabel = useMemo(() => getQualityLabel(t, config.provider), [t, config.provider]);
/* ═══════════════════════════════════════════════════════════════ */
/* UNIFIED LAYOUT — always the same grid */
/* EDITORIAL LAYOUT */
/* ═══════════════════════════════════════════════════════════════ */
return (
<div className="flex h-full flex-col gap-6 overflow-y-auto p-6 lg:p-8">
{/* Header */}
<div>
<h1 className="text-2xl font-bold tracking-tight text-foreground">
{t('dashboard.translate.pageTitle')}
</h1>
<p className="mt-1 text-sm text-muted-foreground">
{t('dashboard.translate.pageSubtitle')}
</p>
</div>
<div className="min-h-full p-6 lg:p-8 dark:bg-[#0a0a0a]">
<div className="max-w-6xl mx-auto">
{/* Grid: LEFT (2/3) + RIGHT (1/3) — always present */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* ═══════════════════════════════════════════════════════ */}
{/* LEFT CARD (2/3) — content swaps based on state */}
{/* ═══════════════════════════════════════════════════════ */}
<div className="lg:col-span-2 flex flex-col gap-0 rounded-2xl border border-border bg-card shadow-sm overflow-hidden">
{/* ── UPLOAD STATE: Dropzone ─────────────────────────── */}
{showUpload && (
{/* ── HEADER ────────────────────────────────────────────── */}
<div className="mb-12">
{showProcessing ? (
<>
<div className="px-6 pt-6 pb-4">
<h2 className="text-lg font-semibold text-foreground">{t('dashboard.translate.sourceDocument')}</h2>
</div>
<div className="flex-1 px-6 pb-6">
<FileDropZone upload={upload} />
{upload.error && <p className="mt-2 text-sm text-destructive">{upload.error}</p>}
</div>
<span className="accent-pill mb-4 block w-fit italic">{t('landing.translate.processing')}</span>
<h1 className="text-5xl font-black uppercase tracking-tighter mb-4 leading-none text-brand-dark dark:text-white">
{t('landing.translate.aiAnalysis')}
</h1>
<p className="text-brand-dark/40 font-medium dark:text-white/40">
{t('landing.translate.preservingLayout')}
</p>
</>
) : showComplete ? (
<>
<span className="accent-pill mb-4 block w-fit italic">{t('dashboard.translate.completed')}</span>
<h1 className="text-5xl font-black uppercase tracking-tighter mb-4 leading-none text-brand-dark dark:text-white">
{t('dashboard.translate.completed')}
</h1>
<p className="text-brand-dark/40 font-medium dark:text-white/40">
{submit.fileName}
</p>
</>
) : (
<>
<span className="accent-pill mb-4 block w-fit">{t('landing.translate.newProject')}</span>
<h1 className="text-5xl font-black uppercase tracking-tighter mb-4 leading-none text-brand-dark dark:text-white">
{t('landing.translate.title')}
</h1>
<p className="text-brand-dark/40 font-medium dark:text-white/40">
{t('landing.translate.subtitle')}
</p>
</>
)}
</div>
{/* ── CONFIGURING STATE: File strip ──────────────────── */}
{showConfiguring && (
<>
<div className="px-6 pt-6 pb-4">
<h2 className="text-lg font-semibold text-foreground">{t('dashboard.translate.sourceDocument')}</h2>
{/* ── GRID: 8/4 SPLIT ───────────────────────────────────── */}
<div className="grid lg:grid-cols-12 gap-12">
{/* ═══════════════════════════════════════════════════════ */}
{/* LEFT (8 cols) — content swaps based on state */}
{/* ═══════════════════════════════════════════════════════ */}
<div className="lg:col-span-8">
{/* ── UPLOAD STATE: Editorial Dropzone ──────────────── */}
{showUpload && (
<div
className="bg-white border-2 border-dashed border-brand-accent/20 rounded-[40px] p-20 flex flex-col items-center justify-center text-center group cursor-pointer hover:border-brand-accent transition-all shadow-editorial dark:bg-[#141414] dark:border-white/10"
onDragOver={upload.handleDragOver}
onDragLeave={upload.handleDragLeave}
onDrop={upload.handleDrop}
onClick={() => dropzoneInputRef.current?.click()}
>
<div className="w-20 h-20 bg-brand-muted rounded-3xl flex items-center justify-center text-brand-accent group-hover:scale-110 group-hover:bg-brand-dark group-hover:text-white transition-all mb-8 shadow-sm dark:bg-white/10">
<Upload size={32} />
</div>
<h3 className="text-2xl font-black uppercase tracking-tight mb-4 text-brand-dark dark:text-white">
{t('landing.translate.dropHere')}
</h3>
<p className="text-sm text-brand-dark/40 mb-12 font-medium dark:text-white/40">
{t('landing.translate.supportedFormats')}
</p>
<div className="flex flex-wrap justify-center gap-4">
{[
{ label: 'Word', icon: <FileText size={12} className="text-blue-500" /> },
{ label: 'Excel', icon: <FileSpreadsheet size={12} className="text-green-500" /> },
{ label: 'Slides', icon: <Presentation size={12} className="text-orange-500" /> },
{ label: 'PDF', icon: <FileType size={12} className="text-red-500" /> },
].map(f => (
<span key={f.label} className="flex items-center gap-3 px-4 py-2 bg-brand-muted rounded-xl text-[10px] font-black uppercase tracking-widest text-brand-dark/60 border border-transparent hover:border-brand-accent/30 transition-all dark:bg-white/10 dark:text-white/60">
{f.icon} {f.label}
</span>
))}
</div>
{/* Hidden file input for click-to-upload */}
<input
ref={dropzoneInputRef}
type="file"
accept=".xlsx,.docx,.pptx,.pdf"
className="hidden"
onChange={upload.handleFileSelect}
/>
</div>
<div className="px-6 pb-6">
)}
{/* ── CONFIGURING STATE: File strip ─────────────────── */}
{showConfiguring && (
<div className="editorial-card p-10 bg-white border-none shadow-editorial dark:bg-[#141414]">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] mb-10 text-brand-dark/30 border-b border-black/5 pb-6 dark:text-white/30 dark:border-white/5">
{t('landing.translate.sourceDocument')}
</h4>
<FileStrip file={upload.file!} onRemove={upload.removeFile} onReplace={() => replaceInputRef.current?.click()} t={t} />
<input ref={replaceInputRef} type="file" accept=".xlsx,.docx,.pptx,.pdf" className="hidden" onChange={upload.handleFileSelect} />
{upload.error && <p className="mt-2 text-sm text-destructive">{upload.error}</p>}
</div>
</>
)}
)}
{/* ── PROCESSING STATE: Rich progress ───────────────── */}
{showProcessing && (
<div className="flex flex-col">
{/* Header band */}
<div className="border-b border-border bg-gradient-to-r from-primary/5 via-primary/8 to-primary/3 px-6 py-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10">
<Loader2 className="size-5 animate-spin text-primary" />
</div>
<div>
<h2 className="text-base font-bold text-foreground leading-tight">{t('dashboard.translate.translating')}</h2>
{(submit.fileName || upload.file?.name) && (
<p className="text-xs text-muted-foreground mt-0.5 truncate max-w-[300px]">
{submit.fileName || upload.file?.name}
</p>
)}
</div>
{/* ── PROCESSING STATE: Rich progress ───────────────── */}
{showProcessing && (
<div className="editorial-card p-16 h-full border-none shadow-editorial bg-white dark:bg-[#141414]">
<div className="flex items-center gap-6 mb-20">
<div className="w-16 h-16 bg-brand-muted rounded-2xl flex items-center justify-center text-brand-accent border border-brand-accent/10 animate-pulse dark:bg-white/10 dark:border-white/10">
<Activity size={32} />
</div>
{submit.estimatedRemaining != null && submit.estimatedRemaining > 0 && (
<div className="flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1.5 text-xs font-semibold text-primary">
<Clock className="size-3.5" />
~{submit.estimatedRemaining}s
</div>
)}
</div>
</div>
<div className="flex flex-col gap-5 p-6">
{/* Pipeline stepper */}
<PipelineStepper activeIdx={activeStepIdx} t={t} />
{/* Progress bar */}
<div className="space-y-2">
<div className="flex items-center justify-between gap-4">
<p className="text-sm font-medium text-foreground animate-pulse truncate">
{submit.currentStep || (submit.isSubmitting ? t('dashboard.translate.steps.uploading') : t('dashboard.translate.steps.starting'))}
</p>
<p className="text-2xl font-black tabular-nums tracking-tight text-primary shrink-0">
{Math.round(submit.progress)}%
</p>
</div>
<div className="h-3 w-full overflow-hidden rounded-full bg-primary/10">
<div
className="h-full rounded-full bg-gradient-to-r from-primary via-primary/80 to-primary transition-all duration-700 ease-out"
style={{ width: `${Math.max(0, submit.progress)}%` }}
/>
</div>
</div>
{/* Live stats */}
<div className="grid grid-cols-4 gap-2.5">
<StatBox icon={<FileText className="size-4" />} value={`${Math.round(submit.progress)}%`} label={t('dashboard.translate.segments')} />
<StatBox icon={<Activity className="size-4" />} value={t('dashboard.translate.translating')} label={t('dashboard.translate.characters')} />
<StatBox icon={<Gauge className="size-4" />} value="—" label={t('dashboard.translate.segPerMin')} />
<StatBox icon={<Timer className="size-4" />} value={formatElapsed(elapsed)} label={t('dashboard.translate.elapsed')} />
</div>
</div>
</div>
)}
{/* ── COMPLETE STATE: Success ────────────────────────── */}
{showComplete && (
<div className="flex flex-col">
{/* Success header */}
<div className="border-b border-emerald-200/50 bg-gradient-to-r from-emerald-500/8 via-emerald-500/5 to-transparent px-6 py-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-xl bg-emerald-500 shadow-lg shadow-emerald-500/20">
<CheckCircle2 className="size-5 text-white" />
</div>
<div>
<h2 className="text-base font-bold text-foreground leading-tight">{t('dashboard.translate.completed')}</h2>
{submit.fileName && (
<p className="text-xs text-muted-foreground mt-0.5 truncate max-w-[260px]">{submit.fileName}</p>
)}
</div>
</div>
<div className="flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700 dark:border-emerald-800/50 dark:bg-emerald-950/30 dark:text-emerald-400">
<TrendingUp className="size-3" />{qualityLabel}
</div>
</div>
</div>
<div className="flex flex-col gap-5 p-6">
{/* Actions */}
<div className="flex items-center gap-3">
<Button size="lg" className="h-11 gap-2 flex-1 font-semibold" onClick={handleDownload}>
<Download className="size-4" />{t('dashboard.translate.complete.download')}
</Button>
<Button variant="outline" size="lg" className="h-11 gap-2 flex-1" onClick={handleNewTranslation}>
<Plus className="size-4" />{t('dashboard.translate.complete.newTranslation')}
</Button>
</div>
</div>
</div>
)}
{/* ── FAILED STATE ───────────────────────────────────── */}
{showFailed && (
<div className="flex flex-col gap-4 p-6">
<div className="rounded-xl bg-destructive/10 border border-destructive/20 p-4" role="alert">
<div className="flex items-start gap-3">
<AlertTriangle className="size-5 text-destructive shrink-0 mt-0.5" />
<div>
<p className="text-sm font-semibold text-destructive mb-1">{t('dashboard.translate.progress.failedTitle')}</p>
<p className="text-sm text-destructive/80">{submit.error}</p>
<h3 className="text-2xl font-black uppercase tracking-tight mb-2 text-brand-dark dark:text-white">
{t('dashboard.translate.translating')}
</h3>
<p className="text-[10px] text-brand-dark/30 font-black uppercase tracking-widest dark:text-white/30">
{submit.fileName || upload.file?.name}
</p>
</div>
</div>
{/* Progress Line with step icons */}
<div className="relative h-2 bg-brand-muted rounded-full mb-24 dark:bg-white/10">
<div className="absolute top-1/2 left-0 w-full -translate-y-1/2 flex justify-between px-2">
{PIPELINE_ICONS.map((Icon, i) => (
<div
key={i}
className={cn(
'w-12 h-12 rounded-2xl border-4 border-white dark:border-[#141414] shadow-xl flex items-center justify-center z-10 transition-all duration-500',
submit.progress > (i * 25)
? 'bg-brand-dark text-white scale-110 dark:bg-brand-accent'
: 'bg-brand-muted text-brand-dark/20 dark:bg-white/10 dark:text-white/20'
)}
>
<Icon size={18} />
</div>
))}
</div>
<div
className="absolute top-0 left-0 h-full bg-brand-accent shadow-[0_0_20px_rgba(197,161,122,0.4)] transition-all duration-700 rounded-full"
style={{ width: `${Math.max(0, submit.progress)}%` }}
/>
</div>
<div className="flex justify-between items-end mb-8">
<span className="text-[10px] font-black text-brand-dark/30 uppercase tracking-[0.3em] dark:text-white/30">
{activeStepIdx < 2 ? t('dashboard.translate.steps.uploading') : t('dashboard.translate.steps.starting')}
</span>
<span className="text-7xl font-black text-brand-dark dark:text-white">
{Math.round(submit.progress)}%
</span>
</div>
<div className="grid grid-cols-4 gap-6 pt-12 border-t border-black/5 dark:border-white/5">
<StatBox icon={<FileText size={18} />} value={`${Math.round(submit.progress)}%`} label={t('dashboard.translate.segments')} />
<StatBox icon={<Zap size={18} />} value="99.9%" label={t('dashboard.translate.quality')} />
<StatBox icon={<Clock size={18} />} value="Turbo" label={t('dashboard.translate.segPerMin')} />
<StatBox icon={<Activity size={18} />} value={formatElapsed(elapsed)} label={t('dashboard.translate.elapsed')} />
</div>
</div>
)}
{/* ── COMPLETE STATE: Success with download ─────────── */}
{showComplete && (
<div className="editorial-card p-16 h-full border-none shadow-editorial bg-white dark:bg-[#141414]">
<div className="h-full flex flex-col">
<div className="p-8 bg-brand-accent/5 border border-brand-accent/10 rounded-[32px] flex items-center justify-between mb-16 shadow-inner dark:bg-brand-accent/10 dark:border-brand-accent/20">
<div className="flex items-center gap-6">
<div className="w-14 h-14 bg-brand-accent rounded-full flex items-center justify-center text-white shadow-xl">
<CheckCircle2 size={28} />
</div>
<div>
<p className="text-[13px] font-black uppercase tracking-[0.1em] text-brand-dark dark:text-white">
{t('dashboard.translate.completed')}
</p>
<p className="text-[10px] text-brand-dark/40 font-bold uppercase mt-1 tracking-widest dark:text-white/40">
{submit.fileName}
</p>
</div>
</div>
<span className="px-5 py-2 bg-white dark:bg-[#1a1a1a] rounded-full text-[9px] font-black uppercase tracking-widest text-brand-accent border border-brand-accent/20 shadow-sm">
{qualityLabel}
</span>
</div>
<div className="flex-1 flex flex-col items-center justify-center py-20 bg-brand-muted/20 rounded-[40px] border border-black/5 dark:bg-white/5 dark:border-white/5">
<button
onClick={handleDownload}
className="premium-button px-24 py-6 text-xl !rounded-full flex items-center gap-6 mb-8 group"
>
<Download size={28} className="group-hover:translate-y-1 transition-transform" />
{t('dashboard.translate.complete.download')}
</button>
<button
onClick={handleNewTranslation}
className="text-[10px] font-black uppercase tracking-[0.3em] text-brand-dark/20 hover:text-brand-dark transition-colors dark:text-white/20 dark:hover:text-white"
>
+ {t('dashboard.translate.complete.newTranslation')}
</button>
</div>
</div>
</div>
{(submit.fileName || upload.file?.name) && (
<FileStrip file={upload.file!} onRemove={upload.removeFile} onReplace={() => replaceInputRef.current?.click()} t={t} />
)}
</div>
)}
</div>
)}
{/* ═══════════════════════════════════════════════════════ */}
{/* RIGHT CARD (1/3) — Config / Monitor / Summary */}
{/* ═══════════════════════════════════════════════════════ */}
<div className="flex flex-col gap-0 rounded-2xl border border-border bg-card shadow-sm">
{/* ── CONFIG (upload / configuring / failed) ──────────── */}
{(showUpload || showConfiguring || showFailed) && (
<>
<div className="px-6 pt-6 pb-4">
<h2 className="text-lg font-semibold text-foreground">{t('dashboard.translate.configuration')}</h2>
</div>
<div className="flex flex-1 flex-col gap-5 px-6">
<LanguageSelector
sourceLang={config.sourceLang} targetLang={config.targetLang}
languages={config.languages} isLoading={config.isLoadingLanguages}
error={config.languagesError} onSourceChange={config.setSourceLang}
onTargetChange={config.setTargetLang}
/>
<ProviderSelector
provider={config.provider} onProviderChange={config.setProvider}
availableProviders={config.availableProviders} isLoadingProviders={config.isLoadingProviders}
isPro={config.isPro}
/>
{/* PDF mode selector — only shown for PDF files */}
{isPdf && (
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{t('dashboard.translate.pdfMode.title')}
</label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPdfMode('layout')}
className={cn(
'flex flex-col items-start rounded-lg border-2 p-3 text-start transition-all',
pdfMode === 'layout'
? 'border-primary bg-primary/5 ring-1 ring-primary/20'
: 'border-border hover:border-primary/40'
)}
>
<div className="flex items-center gap-2 text-sm font-semibold">
<FileText className="size-4 text-primary" />
{t('dashboard.translate.pdfMode.preserveLayout')}
</div>
<p className="mt-1 text-xs text-muted-foreground leading-relaxed">
{t('dashboard.translate.pdfMode.preserveLayoutDesc')}
</p>
</button>
<button
type="button"
onClick={() => setPdfMode('text_only')}
className={cn(
'flex flex-col items-start rounded-lg border-2 p-3 text-start transition-all',
pdfMode === 'text_only'
? 'border-primary bg-primary/5 ring-1 ring-primary/20'
: 'border-border hover:border-primary/40'
)}
>
<div className="flex items-center gap-2 text-sm font-semibold">
<Languages className="size-4 text-primary" />
{t('dashboard.translate.pdfMode.textOnly')}
</div>
<p className="mt-1 text-xs text-muted-foreground leading-relaxed">
{t('dashboard.translate.pdfMode.textOnlyDesc')}
</p>
</button>
{/* ── FAILED STATE ───────────────────────────────────── */}
{showFailed && (
<div className="editorial-card p-10 bg-white border-none shadow-editorial dark:bg-[#141414]">
<div className="rounded-[24px] bg-destructive/10 border border-destructive/20 p-6" role="alert">
<div className="flex items-start gap-3">
<AlertTriangle className="size-5 text-destructive shrink-0 mt-0.5" />
<div>
<p className="text-sm font-semibold text-destructive mb-1">{t('dashboard.translate.progress.failedTitle')}</p>
<p className="text-sm text-destructive/80">{submit.error}</p>
</div>
</div>
</div>
{(submit.fileName || upload.file?.name) && upload.file && (
<div className="mt-6">
<FileStrip file={upload.file} onRemove={upload.removeFile} onReplace={() => replaceInputRef.current?.click()} t={t} />
<input ref={replaceInputRef} type="file" accept=".xlsx,.docx,.pptx,.pdf" className="hidden" onChange={upload.handleFileSelect} />
</div>
)}
</div>
)}
</div>
{/* Footer with button */}
<div className="px-6 py-4 border-t border-border/50">
<Button
size="lg" className="h-12 w-full gap-2 text-base font-semibold"
disabled={!config.isConfigValid || submit.isSubmitting || !upload.file}
onClick={handleTranslate}
>
{submit.isSubmitting ? (
<><Loader2 className="size-5 animate-spin" />{t('dashboard.translate.actions.uploading')}</>
) : (
<>{t('dashboard.translate.actions.translate')}<ArrowRight className="size-5 rtl:rotate-180" /></>
)}
</Button>
<div className="mt-3 flex items-center justify-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1.5"><ShieldCheck className="size-3.5" />{t('dashboard.translate.trust.zeroRetention')}</span>
<span className="h-3 w-px bg-border" aria-hidden />
<span className="flex items-center gap-1.5"><Clock className="size-3.5" />{t('dashboard.translate.trust.deletedAfter')}</span>
{/* ═══════════════════════════════════════════════════════ */}
{/* RIGHT (4 cols) — Config / Monitor / Summary */}
{/* ═══════════════════════════════════════════════════════ */}
<div className="lg:col-span-4">
{/* ── CONFIG (upload / configuring / failed) ──────────── */}
{(showUpload || showConfiguring || showFailed) && (
<div className="space-y-8">
<div className="editorial-card p-10 bg-white border-none shadow-editorial dark:bg-[#141414]">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] mb-10 text-brand-dark/30 border-b border-black/5 pb-6 dark:text-white/30 dark:border-white/5">
{t('landing.translate.configuration')}
</h4>
<div className="space-y-8">
<LanguageSelector
sourceLang={config.sourceLang} targetLang={config.targetLang}
languages={config.languages} isLoading={config.isLoadingLanguages}
error={config.languagesError} onSourceChange={config.setSourceLang}
onTargetChange={config.setTargetLang}
/>
<ProviderSelector
provider={config.provider} onProviderChange={config.setProvider}
availableProviders={config.availableProviders} isLoadingProviders={config.isLoadingProviders}
isPro={config.isPro}
/>
{/* PDF mode selector */}
{isPdf && (
<div className="space-y-2">
<label className="text-[9px] font-black text-brand-dark/40 uppercase tracking-[0.2em] block mb-3 dark:text-white/40">
{t('dashboard.translate.pdfMode.title')}
</label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPdfMode('layout')}
className={cn(
'flex flex-col items-start rounded-2xl border-2 p-3 text-start transition-all',
pdfMode === 'layout'
? 'border-brand-accent bg-brand-accent/5'
: 'border-black/5 bg-brand-muted/30 hover:border-brand-accent/20 dark:border-white/10 dark:bg-white/5'
)}
>
<div className="flex items-center gap-2 text-[11px] font-black uppercase tracking-tight text-brand-dark dark:text-white">
<FileText className="size-4 text-brand-accent" />
{t('dashboard.translate.pdfMode.preserveLayout')}
</div>
<p className="mt-1 text-[9px] text-brand-dark/50 font-bold uppercase tracking-widest leading-relaxed dark:text-white/40">
{t('dashboard.translate.pdfMode.preserveLayoutDesc')}
</p>
</button>
<button
type="button"
onClick={() => setPdfMode('text_only')}
className={cn(
'flex flex-col items-start rounded-2xl border-2 p-3 text-start transition-all',
pdfMode === 'text_only'
? 'border-brand-accent bg-brand-accent/5'
: 'border-black/5 bg-brand-muted/30 hover:border-brand-accent/20 dark:border-white/10 dark:bg-white/5'
)}
>
<div className="flex items-center gap-2 text-[11px] font-black uppercase tracking-tight text-brand-dark dark:text-white">
<Languages className="size-4 text-brand-accent" />
{t('dashboard.translate.pdfMode.textOnly')}
</div>
<p className="mt-1 text-[9px] text-brand-dark/50 font-bold uppercase tracking-widest leading-relaxed dark:text-white/40">
{t('dashboard.translate.pdfMode.textOnlyDesc')}
</p>
</button>
</div>
</div>
)}
<button
disabled={!config.isConfigValid || submit.isSubmitting || !upload.file}
onClick={handleTranslate}
className={cn(
'premium-button w-full py-6 text-[11px] uppercase tracking-[0.3em] flex items-center justify-center gap-3 !rounded-2xl',
(!config.isConfigValid || submit.isSubmitting || !upload.file) && 'opacity-40 cursor-not-allowed'
)}
>
{submit.isSubmitting ? (
<><Loader2 className="size-5 animate-spin" />{t('dashboard.translate.actions.uploading')}</>
) : (
<>{t('landing.translate.startTranslation')}<ArrowRight size={18} className="text-brand-accent" /></>
)}
</button>
</div>
</div>
<div className="flex justify-between px-6 text-[9px] font-black uppercase tracking-[0.2em] text-brand-dark/20 dark:text-white/20">
<span className="flex items-center gap-2">
<ShieldCheck size={12} /> {t('landing.translate.zeroRetention')}
</span>
<span className="flex items-center gap-2">
<Clock size={12} /> {t('landing.translate.filesDeleted')}
</span>
</div>
</div>
</>
)}
)}
{/* ── MONITOR (processing) ────────────────────────────── */}
{showProcessing && (
<>
<div className="flex items-center gap-2 border-b border-border px-6 py-4">
<div className="size-2 animate-pulse rounded-full bg-primary" />
<h3 className="text-sm font-semibold text-foreground">{t('dashboard.translate.liveMonitor')}</h3>
</div>
{/* ── MONITOR (processing) ────────────────────────────── */}
{showProcessing && (
<div className="editorial-card p-10 bg-white border-none shadow-editorial h-full dark:bg-[#141414]">
<h4 className="text-[10px] font-black uppercase tracking-[0.4em] mb-12 flex items-center gap-3 text-brand-dark/30 dark:text-white/30">
<div className="w-2 h-2 bg-brand-accent rounded-full animate-ping" />
{t('dashboard.translate.liveMonitor')}
</h4>
<div className="flex flex-1 flex-col gap-4 p-6">
{/* File summary */}
{(submit.fileName || upload.file?.name) && (
<div className="flex items-center gap-3 rounded-xl bg-primary/5 border border-primary/10 p-3">
{(() => {
const name = submit.fileName || upload.file?.name || '';
const ext = name.split('.').pop()?.toLowerCase() ?? '';
const FileIcon = FILE_ICONS[ext] ?? FileText;
const color = FILE_COLORS[ext] ?? 'text-muted-foreground';
return <FileIcon className={`size-6 shrink-0 ${color}`} />;
})()}
<div className="flex-1 min-w-0">
<p className="truncate text-sm font-semibold text-foreground">{submit.fileName || upload.file?.name}</p>
{upload.file && <p className="text-[11px] text-muted-foreground">{fmt(upload.file.size)}</p>}
<div className="p-6 bg-brand-muted rounded-[32px] mb-10 flex items-center gap-5 border border-black/5 dark:bg-white/5 dark:border-white/5">
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-brand-accent shadow-sm dark:bg-[#1a1a1a]">
{(() => {
const name = submit.fileName || upload.file?.name || '';
const ext = name.split('.').pop()?.toLowerCase() ?? '';
const FileIcon = FILE_ICONS[ext] ?? FileText;
return <FileIcon size={24} />;
})()}
</div>
<div className="overflow-hidden">
<p className="text-[11px] font-black uppercase tracking-tight truncate text-brand-dark dark:text-white">
{submit.fileName || upload.file?.name}
</p>
<p className="text-[9px] text-brand-dark/40 font-bold uppercase tracking-widest mt-1 dark:text-white/40">
{upload.file ? `${fmt(upload.file.size)} ` : ''}{(submit.fileName || upload.file?.name || '').split('.').pop()?.toUpperCase()}
</p>
</div>
</div>
)}
{/* Config summary */}
<div className="space-y-2.5">
<div className="flex items-center justify-between py-1.5 border-b border-border/50">
<span className="text-xs text-muted-foreground">{t('dashboard.translate.language.source')}</span>
<span className="text-xs font-semibold text-foreground bg-muted px-2 py-0.5 rounded">{srcLangName}</span>
<div className="space-y-8 mb-16 px-2">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<span>{t('dashboard.translate.language.source')}</span>
<span className="text-brand-dark dark:text-white">{srcLangName.toUpperCase()}</span>
</div>
<div className="flex items-center justify-between py-1.5 border-b border-border/50">
<span className="text-xs text-muted-foreground">{t('dashboard.translate.language.target')}</span>
<span className="text-xs font-semibold text-primary bg-primary/10 px-2 py-0.5 rounded">{tgtLangName}</span>
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<span>{t('dashboard.translate.language.target')}</span>
<span className="text-brand-accent">{tgtLangName.toUpperCase()}</span>
</div>
{currentProvider && (
<div className="flex items-center justify-between py-1.5 border-b border-border/50">
<span className="text-xs text-muted-foreground">{t('dashboard.translate.engine')}</span>
<span className="text-xs font-semibold text-foreground bg-muted px-2 py-0.5 rounded">{currentProvider.label}</span>
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<span>{t('dashboard.translate.engine')}</span>
<span className="text-brand-dark dark:text-white">{currentProvider.label.toUpperCase()}</span>
</div>
)}
</div>
{/* Quality indicator */}
<div className="rounded-xl border border-border bg-muted/20 p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">{t('dashboard.translate.quality')}</span>
<span className="text-xs font-bold text-emerald-600">{qualityLabel}</span>
{/* Quality progress */}
<div className="pt-10 border-t border-black/10 dark:border-white/10">
<div className="flex justify-between text-[10px] font-black uppercase tracking-[0.4em] mb-4">
<span className="text-brand-dark/30 dark:text-white/30">{t('dashboard.translate.quality')}</span>
<span className="text-brand-accent">{qualityLabel}</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-emerald-100 dark:bg-emerald-900/30">
<div className="h-2 bg-brand-muted rounded-full overflow-hidden p-0.5 dark:bg-white/10">
<div
className="h-full rounded-full bg-gradient-to-r from-emerald-400 to-emerald-600 transition-all duration-700"
className="h-full bg-brand-accent rounded-full transition-all duration-700"
style={{ width: `${Math.min(95, 40 + submit.progress * 0.55)}%` }}
/>
</div>
</div>
</div>
{/* Cancel */}
<div className="px-6 py-4 border-t border-border/50">
<Button
variant="outline"
className="w-full gap-2 border-destructive/20 text-destructive hover:bg-destructive hover:text-white hover:border-destructive"
<button
onClick={handleNewTranslation}
className="w-full mt-16 py-5 border border-red-50 text-red-500 rounded-2xl text-[10px] font-black uppercase tracking-[0.3em] flex items-center justify-center gap-3 hover:bg-red-50 transition-all dark:border-red-900/30 dark:text-red-400 dark:hover:bg-red-950/30"
>
<RotateCcw className="size-4" />{t('dashboard.translate.cancel')}
</Button>
<RotateCcw size={16} /> {t('dashboard.translate.cancel')}
</button>
</div>
</>
)}
)}
{/* ── SUMMARY (complete) ──────────────────────────────── */}
{showComplete && (
<>
<div className="px-6 py-4 border-b border-border">
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
<CheckCircle2 className="size-4 text-emerald-500" />{t('dashboard.translate.summary')}
</h3>
</div>
{/* ── SUMMARY (complete) ──────────────────────────────── */}
{showComplete && (
<div className="editorial-card p-10 bg-white border-none shadow-editorial h-full dark:bg-[#141414]">
<h4 className="text-[10px] font-black uppercase tracking-[0.4em] mb-12 flex items-center gap-3 text-brand-dark/30 dark:text-white/30">
<CheckCircle2 size={14} className="text-emerald-500" />
{t('dashboard.translate.summary')}
</h4>
<div className="flex-1 p-6 space-y-2.5">
<div className="flex items-center justify-between py-1.5 border-b border-border/50">
<span className="text-xs text-muted-foreground">{t('dashboard.translate.language.source')}</span>
<span className="text-xs font-semibold text-foreground bg-muted px-2 py-0.5 rounded">{srcLangName}</span>
</div>
<div className="flex items-center justify-between py-1.5 border-b border-border/50">
<span className="text-xs text-muted-foreground">{t('dashboard.translate.language.target')}</span>
<span className="text-xs font-semibold text-primary bg-primary/10 px-2 py-0.5 rounded">{tgtLangName}</span>
</div>
{currentProvider && (
<div className="flex items-center justify-between py-1.5 border-b border-border/50">
<span className="text-xs text-muted-foreground">{t('dashboard.translate.engine')}</span>
<span className="text-xs font-semibold text-foreground bg-muted px-2 py-0.5 rounded">{currentProvider.label}</span>
<div className="space-y-8 mb-16 px-2">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<span>{t('dashboard.translate.language.source')}</span>
<span className="text-brand-dark dark:text-white">{srcLangName.toUpperCase()}</span>
</div>
)}
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground">{t('dashboard.translate.quality')}</span>
<span className="text-xs font-bold text-emerald-600">{qualityLabel}</span>
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<span>{t('dashboard.translate.language.target')}</span>
<span className="text-brand-accent">{tgtLangName.toUpperCase()}</span>
</div>
{currentProvider && (
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<span>{t('dashboard.translate.engine')}</span>
<span className="text-brand-dark dark:text-white">{currentProvider.label.toUpperCase()}</span>
</div>
)}
</div>
</div>
{/* Quality bar */}
<div className="px-6 pb-6">
<div className="rounded-xl border border-border bg-muted/20 p-3">
<div className="h-2 w-full overflow-hidden rounded-full bg-emerald-100 dark:bg-emerald-900/30">
<div className="h-full rounded-full bg-gradient-to-r from-emerald-400 to-emerald-600" style={{ width: '95%' }} />
<div className="pt-10 border-t border-black/10 dark:border-white/10">
<div className="flex justify-between text-[10px] font-black uppercase tracking-[0.4em] mb-4">
<span className="text-brand-dark/30 dark:text-white/30">{t('dashboard.translate.quality')}</span>
<span className="text-brand-accent">{qualityLabel}</span>
</div>
<div className="h-2 bg-brand-muted rounded-full overflow-hidden p-0.5 dark:bg-white/10">
<div className="h-full bg-brand-accent rounded-full" style={{ width: '95%' }} />
</div>
</div>
</div>
</>
)}
)}
</div>
</div>
{/* ── MOMENTO PROMO BANNER ──────────────────────────────── */}
{(showUpload || showConfiguring || showFailed) && (
<div className="mt-20 editorial-card p-12 bg-white border-none shadow-editorial flex flex-col md:flex-row items-center gap-10 group overflow-hidden relative dark:bg-[#141414]">
<div className="absolute -right-20 -top-20 w-64 h-64 bg-brand-accent/5 rounded-full blur-3xl group-hover:bg-brand-accent/10 transition-colors" />
<div className="w-24 h-24 bg-brand-dark rounded-[32px] flex items-center justify-center text-white text-5xl font-black shadow-2xl shrink-0 group-hover:rotate-12 transition-transform duration-500 dark:bg-brand-accent dark:text-brand-dark">
M
</div>
<div className="flex-1">
<div className="flex items-center gap-4 mb-4">
<span className="accent-pill !px-3 !py-1 text-[9px] italic">{t('memento.title')}</span>
<h3 className="text-2xl font-black uppercase tracking-tight text-brand-dark dark:text-white">{t('memento.title')}</h3>
</div>
<p className="text-sm text-brand-dark/40 font-medium leading-relaxed max-w-2xl dark:text-white/40">
{t('memento.slogan')}
</p>
</div>
<div className="flex flex-col gap-4 shrink-0 w-full md:w-auto">
<button className="premium-button px-10 py-4 text-[11px] uppercase tracking-widest !rounded-2xl">
{t('memento.ctaFree')}
</button>
<button className="px-10 py-4 border border-black/5 bg-brand-muted text-brand-dark/40 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:text-brand-dark transition-all dark:border-white/10 dark:bg-white/5 dark:text-white/40 dark:hover:text-white">
{t('memento.ctaMore')}
</button>
</div>
</div>
)}
</div>
</div>
);
@@ -540,16 +628,16 @@ function FileStrip({ file, onRemove, onReplace, t }: { file: File; onRemove: ()
const FileIcon = FILE_ICONS[ext] ?? FileText;
const color = FILE_COLORS[ext] ?? 'text-muted-foreground';
return (
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="flex items-center gap-3 rounded-[24px] border border-black/5 bg-brand-muted/30 px-4 py-3 dark:border-white/5 dark:bg-white/5">
<FileIcon className={`size-5 shrink-0 ${color}`} />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-semibold text-foreground">{file.name}</span>
<span className="text-xs text-muted-foreground">{fmt(file.size)} · .{ext.toUpperCase()}</span>
<span className="truncate text-[11px] font-black uppercase tracking-tight text-brand-dark dark:text-white">{file.name}</span>
<span className="text-[9px] text-brand-dark/40 font-bold uppercase tracking-widest dark:text-white/40">{fmt(file.size)} .{ext.toUpperCase()}</span>
</div>
<button type="button" onClick={onReplace} className="flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-xs text-muted-foreground transition hover:bg-secondary hover:text-foreground">
<button type="button" onClick={onReplace} className="flex shrink-0 items-center gap-1 rounded-xl px-2 py-1 text-[10px] font-black uppercase tracking-widest text-brand-dark/30 transition hover:bg-brand-muted hover:text-brand-dark dark:text-white/30 dark:hover:bg-white/10 dark:hover:text-white">
<Upload className="size-3.5" />{t('dashboard.translate.replace')}
</button>
<button type="button" aria-label="Remove" onClick={onRemove} className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground">
<button type="button" aria-label="Remove" onClick={onRemove} className="flex size-7 shrink-0 items-center justify-center rounded-xl text-brand-dark/20 transition hover:bg-brand-muted hover:text-brand-dark dark:text-white/20 dark:hover:bg-white/10 dark:hover:text-white">
<X className="size-4" />
</button>
</div>
@@ -594,10 +682,10 @@ function PipelineStepper({ activeIdx, t }: { activeIdx: number; t: (key: string)
/** Small stat box */
function StatBox({ icon, value, label }: { icon: React.ReactNode; value: string; label: string }) {
return (
<div className="flex flex-col items-center gap-1 rounded-xl border border-border bg-muted/20 p-2.5 text-center">
<div className="text-primary">{icon}</div>
<p className="text-sm font-bold tabular-nums text-foreground leading-none">{value}</p>
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">{label}</p>
<div className="p-6 bg-brand-muted/30 rounded-3xl text-center border border-transparent hover:border-brand-accent/10 transition-all dark:bg-white/5 dark:border-white/5">
<div className="text-brand-accent flex justify-center mb-4">{icon}</div>
<p className="text-[12px] font-black text-brand-dark mb-1 uppercase tracking-tight dark:text-white">{value}</p>
<p className="text-[8px] font-black text-brand-dark/30 uppercase tracking-[0.2em] dark:text-white/30">{label}</p>
</div>
);
}

View File

@@ -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">&times;</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'}`}>&minus;{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} &euro;</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} &euro;</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>
);
}

View File

@@ -154,6 +154,12 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.pricing.business.f5": "Webhooks & automation",
"landing.pricing.business.f6": "5 team seats",
"landing.pricing.business.cta": "Contact Us",
"landing.pricing.free.name": "Free",
"landing.pricing.free.desc": "Perfect to discover the app",
"landing.pricing.free.cta": "Choose this plan",
"landing.pricing.enterprise.name": "Enterprise",
"landing.pricing.enterprise.desc": "Custom solutions for large organizations",
"landing.pricing.enterprise.cta": "Contact us",
"landing.cta.title": "Start translating in 30 seconds",
"landing.cta.subtitle": "No credit card required. Try for free now and bring your multilingual documents back to life.",
"landing.cta.button": "Create Free Account",
@@ -746,6 +752,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Live Analysis",
"landing.hero.termsDetected": "terms detected",
"landing.steps.process": "PROCESS",
"landing.translate.newProject": "New Project",
"landing.translate.title": "Translate a document",
"landing.translate.subtitle": "Import a file and choose the target language",
"landing.translate.sourceDocument": "Source Document",
"landing.translate.configuration": "Configuration",
"landing.translate.sourceLang": "Source Language",
"landing.translate.targetLang": "Target Language",
"landing.translate.provider": "Provider",
"landing.translate.startTranslation": "Start Translation",
"landing.translate.zeroRetention": "Zero retention",
"landing.translate.filesDeleted": "Files deleted after processing",
"landing.translate.dropHere": "Drag & drop here",
"landing.translate.supportedFormats": "DOCX, XLSX, PPTX or PDF files supported",
"landing.translate.aiAnalysis": "Active AI Analysis",
"landing.translate.processing": "Processing",
"landing.translate.preservingLayout": "Your layout is being preserved",
},
fr: {
@@ -1483,6 +1505,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Analyse en direct",
"landing.hero.termsDetected": "termes détectés",
"landing.steps.process": "PROCESSUS",
"landing.translate.newProject": "Nouveau projet",
"landing.translate.title": "Traduire un document",
"landing.translate.subtitle": "Importez un fichier et choisissez la langue cible",
"landing.translate.sourceDocument": "Document source",
"landing.translate.configuration": "Configuration",
"landing.translate.sourceLang": "Langue source",
"landing.translate.targetLang": "Langue cible",
"landing.translate.provider": "Fournisseur",
"landing.translate.startTranslation": "Lancer la traduction",
"landing.translate.zeroRetention": "Rétention zéro",
"landing.translate.filesDeleted": "Fichiers supprimés après traitement",
"landing.translate.dropHere": "Glissez-déposez ici",
"landing.translate.supportedFormats": "Fichiers DOCX, XLSX, PPTX ou PDF supportés",
"landing.translate.aiAnalysis": "Analyse IA Active",
"landing.translate.processing": "Traitement en cours",
"landing.translate.preservingLayout": "Votre mise en page est en cours de préservation",
},
// ═══════════════════════════════════════════════════════════════
// SPANISH (es)
@@ -2164,6 +2202,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Análisis en vivo",
"landing.hero.termsDetected": "términos detectados",
"landing.steps.process": "PROCESO",
"landing.translate.newProject": "Nuevo proyecto",
"landing.translate.title": "Traducir un documento",
"landing.translate.subtitle": "Importa un archivo y elige el idioma de destino",
"landing.translate.sourceDocument": "Documento fuente",
"landing.translate.configuration": "Configuración",
"landing.translate.sourceLang": "Idioma de origen",
"landing.translate.targetLang": "Idioma de destino",
"landing.translate.provider": "Proveedor",
"landing.translate.startTranslation": "Iniciar traducción",
"landing.translate.zeroRetention": "Retención cero",
"landing.translate.filesDeleted": "Archivos eliminados tras el procesamiento",
"landing.translate.dropHere": "Arrastra y suelta aquí",
"landing.translate.supportedFormats": "Archivos DOCX, XLSX, PPTX o PDF compatibles",
"landing.translate.aiAnalysis": "Análisis IA Activo",
"landing.translate.processing": "Procesando",
"landing.translate.preservingLayout": "Tu diseño se está preservando",
},
// ═══════════════════════════════════════════════════════════════
// GERMAN (de)
@@ -2845,6 +2899,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Live-Analyse",
"landing.hero.termsDetected": "Begriffe erkannt",
"landing.steps.process": "PROZESS",
"landing.translate.newProject": "Neues Projekt",
"landing.translate.title": "Dokument übersetzen",
"landing.translate.subtitle": "Datei importieren und Zielsprache wählen",
"landing.translate.sourceDocument": "Quelldokument",
"landing.translate.configuration": "Konfiguration",
"landing.translate.sourceLang": "Quellsprache",
"landing.translate.targetLang": "Zielsprache",
"landing.translate.provider": "Anbieter",
"landing.translate.startTranslation": "Übersetzung starten",
"landing.translate.zeroRetention": "Keine Speicherung",
"landing.translate.filesDeleted": "Dateien nach Verarbeitung gelöscht",
"landing.translate.dropHere": "Hier ablegen",
"landing.translate.supportedFormats": "DOCX, XLSX, PPTX oder PDF Dateien unterstützt",
"landing.translate.aiAnalysis": "KI-Analyse aktiv",
"landing.translate.processing": "Verarbeitung läuft",
"landing.translate.preservingLayout": "Ihr Layout wird beibehalten",
},
// ═══════════════════════════════════════════════════════════════
// PORTUGUESE BRAZILIAN (pt)
@@ -3526,6 +3596,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Análise em tempo real",
"landing.hero.termsDetected": "termos detectados",
"landing.steps.process": "PROCESSO",
"landing.translate.newProject": "Novo projeto",
"landing.translate.title": "Traduzir um documento",
"landing.translate.subtitle": "Importe um arquivo e escolha o idioma de destino",
"landing.translate.sourceDocument": "Documento fonte",
"landing.translate.configuration": "Configuração",
"landing.translate.sourceLang": "Idioma de origem",
"landing.translate.targetLang": "Idioma de destino",
"landing.translate.provider": "Fornecedor",
"landing.translate.startTranslation": "Iniciar tradução",
"landing.translate.zeroRetention": "Retenção zero",
"landing.translate.filesDeleted": "Ficheiros eliminados após processamento",
"landing.translate.dropHere": "Arraste e solte aqui",
"landing.translate.supportedFormats": "Ficheiros DOCX, XLSX, PPTX ou PDF suportados",
"landing.translate.aiAnalysis": "Análise IA Ativa",
"landing.translate.processing": "Em processamento",
"landing.translate.preservingLayout": "O seu layout está a ser preservado",
},
// ═══════════════════════════════════════════════════════════════
// ITALIAN (it)
@@ -4207,6 +4293,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Analisi in tempo reale",
"landing.hero.termsDetected": "termini rilevati",
"landing.steps.process": "PROCESSO",
"landing.translate.newProject": "Nuovo progetto",
"landing.translate.title": "Traduci un documento",
"landing.translate.subtitle": "Importa un file e scegli la lingua di destinazione",
"landing.translate.sourceDocument": "Documento sorgente",
"landing.translate.configuration": "Configurazione",
"landing.translate.sourceLang": "Lingua sorgente",
"landing.translate.targetLang": "Lingua di destinazione",
"landing.translate.provider": "Fornitore",
"landing.translate.startTranslation": "Avvia traduzione",
"landing.translate.zeroRetention": "Zero conservazione",
"landing.translate.filesDeleted": "File eliminati dopo l'elaborazione",
"landing.translate.dropHere": "Trascina e rilascia qui",
"landing.translate.supportedFormats": "File DOCX, XLSX, PPTX o PDF supportati",
"landing.translate.aiAnalysis": "Analisi IA Attiva",
"landing.translate.processing": "Elaborazione in corso",
"landing.translate.preservingLayout": "Il tuo layout viene preservato",
},
// ═══════════════════════════════════════════════════════════════
// DUTCH (nl)
@@ -4888,6 +4990,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Live-analyse",
"landing.hero.termsDetected": "termen gedetecteerd",
"landing.steps.process": "PROCES",
"landing.translate.newProject": "Nieuw project",
"landing.translate.title": "Vertaal een document",
"landing.translate.subtitle": "Importeer een bestand en kies de doeltaal",
"landing.translate.sourceDocument": "Brondocument",
"landing.translate.configuration": "Configuratie",
"landing.translate.sourceLang": "Brontaal",
"landing.translate.targetLang": "Doeltaal",
"landing.translate.provider": "Provider",
"landing.translate.startTranslation": "Vertaling starten",
"landing.translate.zeroRetention": "Nul retentie",
"landing.translate.filesDeleted": "Bestanden verwijderd na verwerking",
"landing.translate.dropHere": "Sleep en zet hier neer",
"landing.translate.supportedFormats": "DOCX, XLSX, PPTX of PDF bestanden ondersteund",
"landing.translate.aiAnalysis": "Actieve AI-analyse",
"landing.translate.processing": "Verwerking bezig",
"landing.translate.preservingLayout": "Uw opmaak wordt behouden",
},
// ═══════════════════════════════════════════════════════════════
// RUSSIAN (ru)
@@ -5571,6 +5689,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "Анализ в реальном времени",
"landing.hero.termsDetected": "терминов обнаружено",
"landing.steps.process": "ПРОЦЕСС",
"landing.translate.newProject": "Новый проект",
"landing.translate.title": "Перевести документ",
"landing.translate.subtitle": "Импортируйте файл и выберите целевой язык",
"landing.translate.sourceDocument": "Исходный документ",
"landing.translate.configuration": "Настройки",
"landing.translate.sourceLang": "Исходный язык",
"landing.translate.targetLang": "Целевой язык",
"landing.translate.provider": "Провайдер",
"landing.translate.startTranslation": "Начать перевод",
"landing.translate.zeroRetention": "Нулевое хранение",
"landing.translate.filesDeleted": "Файлы удаляются после обработки",
"landing.translate.dropHere": "Перетащите сюда",
"landing.translate.supportedFormats": "Поддерживаются файлы DOCX, XLSX, PPTX или PDF",
"landing.translate.aiAnalysis": "Активный ИИ-анализ",
"landing.translate.processing": "Обработка",
"landing.translate.preservingLayout": "Ваше форматирование сохраняется",
},
// ═══════════════════════════════════════════════════════════════
// JAPANESE (ja)
@@ -6251,6 +6385,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "リアルタイム分析",
"landing.hero.termsDetected": "件の用語を検出",
"landing.steps.process": "プロセス",
"landing.translate.newProject": "新規プロジェクト",
"landing.translate.title": "ドキュメントを翻訳",
"landing.translate.subtitle": "ファイルをインポートして翻訳先言語を選択",
"landing.translate.sourceDocument": "ソースドキュメント",
"landing.translate.configuration": "設定",
"landing.translate.sourceLang": "翻訳元言語",
"landing.translate.targetLang": "翻訳先言語",
"landing.translate.provider": "プロバイダー",
"landing.translate.startTranslation": "翻訳を開始",
"landing.translate.zeroRetention": "ゼロ保持",
"landing.translate.filesDeleted": "処理後にファイルを削除",
"landing.translate.dropHere": "ここにドラッグ&ドロップ",
"landing.translate.supportedFormats": "DOCX, XLSX, PPTX, PDFファイル対応",
"landing.translate.aiAnalysis": "AI分析アクティブ",
"landing.translate.processing": "処理中",
"landing.translate.preservingLayout": "レイアウトを保持しています",
},
// ═══════════════════════════════════════════════════════════════
// KOREAN (ko)
@@ -6931,6 +7081,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "실시간 분석",
"landing.hero.termsDetected": "개 용어 감지됨",
"landing.steps.process": "프로세스",
"landing.translate.newProject": "새 프로젝트",
"landing.translate.title": "문서 번역",
"landing.translate.subtitle": "파일을 가져오고 대상 언어를 선택하세요",
"landing.translate.sourceDocument": "원본 문서",
"landing.translate.configuration": "설정",
"landing.translate.sourceLang": "원본 언어",
"landing.translate.targetLang": "대상 언어",
"landing.translate.provider": "제공자",
"landing.translate.startTranslation": "번역 시작",
"landing.translate.zeroRetention": "제로 보관",
"landing.translate.filesDeleted": "처리 후 파일 삭제됨",
"landing.translate.dropHere": "여기에 드래그 앤 드롭",
"landing.translate.supportedFormats": "DOCX, XLSX, PPTX 또는 PDF 파일 지원",
"landing.translate.aiAnalysis": "AI 분석 활성",
"landing.translate.processing": "처리 중",
"landing.translate.preservingLayout": "레이아웃이 보존되고 있습니다",
},
// ═══════════════════════════════════════════════════════════════
// CHINESE SIMPLIFIED (zh)
@@ -7569,6 +7735,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "实时分析",
"landing.hero.termsDetected": "个术语已检测",
"landing.steps.process": "流程",
"landing.translate.newProject": "新项目",
"landing.translate.title": "翻译文档",
"landing.translate.subtitle": "导入文件并选择目标语言",
"landing.translate.sourceDocument": "源文档",
"landing.translate.configuration": "配置",
"landing.translate.sourceLang": "源语言",
"landing.translate.targetLang": "目标语言",
"landing.translate.provider": "提供商",
"landing.translate.startTranslation": "开始翻译",
"landing.translate.zeroRetention": "零保留",
"landing.translate.filesDeleted": "处理后删除文件",
"landing.translate.dropHere": "拖放到此处",
"landing.translate.supportedFormats": "支持 DOCX, XLSX, PPTX 或 PDF 文件",
"landing.translate.aiAnalysis": "AI 分析中",
"landing.translate.processing": "正在处理",
"landing.translate.preservingLayout": "正在保留您的排版",
},
// ARABIC (ar)
@@ -8207,6 +8389,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "تحليل مباشر",
"landing.hero.termsDetected": "مصطلحات مكتشفة",
"landing.steps.process": "العملية",
"landing.translate.newProject": "مشروع جديد",
"landing.translate.title": "ترجمة مستند",
"landing.translate.subtitle": "استورد ملفًا واختر اللغة المستهدفة",
"landing.translate.sourceDocument": "المستند المصدر",
"landing.translate.configuration": "الإعدادات",
"landing.translate.sourceLang": "لغة المصدر",
"landing.translate.targetLang": "اللغة المستهدفة",
"landing.translate.provider": "المزود",
"landing.translate.startTranslation": "بدء الترجمة",
"landing.translate.zeroRetention": "صفر احتفاظ",
"landing.translate.filesDeleted": "الملفات محذوفة بعد المعالجة",
"landing.translate.dropHere": "اسحب وأفلت هنا",
"landing.translate.supportedFormats": "ملفات DOCX, XLSX, PPTX أو PDF مدعومة",
"landing.translate.aiAnalysis": "تحليل AI نشط",
"landing.translate.processing": "جاري المعالجة",
"landing.translate.preservingLayout": "جاري الحفاظ على التنسيق",
},
// PERSIAN (fa)
@@ -8881,6 +9079,22 @@ const messages: Record<Locale, Record<string, string>> = {
"landing.hero.liveAnalysis": "تحلیل زنده",
"landing.hero.termsDetected": "اصطلاح شناسایی شد",
"landing.steps.process": "فرآیند",
"landing.translate.newProject": "پروژه جدید",
"landing.translate.title": "ترجمه سند",
"landing.translate.subtitle": "فایل را وارد کنید و زبان مقصد را انتخاب کنید",
"landing.translate.sourceDocument": "سند منبع",
"landing.translate.configuration": "پیکربندی",
"landing.translate.sourceLang": "زبان منبع",
"landing.translate.targetLang": "زبان مقصد",
"landing.translate.provider": "ارائه‌دهنده",
"landing.translate.startTranslation": "شروع ترجمه",
"landing.translate.zeroRetention": "صفر نگهداری",
"landing.translate.filesDeleted": "فایل‌ها پس از پردازش حذف می‌شوند",
"landing.translate.dropHere": "اینجا بکشید و رها کنید",
"landing.translate.supportedFormats": "فایل‌های DOCX, XLSX, PPTX یا PDF پشتیبانی می‌شوند",
"landing.translate.aiAnalysis": "تحلیل AI فعال",
"landing.translate.processing": "در حال پردازش",
"landing.translate.preservingLayout": "طرح‌بندی شما حفظ می‌شود",
},
};