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

@@ -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>
);
}