fix(ui): wave 2 from critique re-run — AA contrast, unified PageHeader, checkout confirm
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m46s

Nav: teams/settings/services back out of the nav until product-ready (user
decision); teams page was already half-finished (UUID member display is a
backend limitation).

Payment: explicit confirmation dialog (plan, amount, billing period, Stripe
note) before any redirect; ?plan= URL now pre-opens the dialog instead of
triggering a silent checkout.

Contrast AA sweep (71 replacements): functional micro-labels raised to
>=60-65% opacity and >=10px across sidebar, header, pricing, landing;
decorative all-caps 'interface' label removed.

Design unification: new PageHeader component (accent pill + serif
base/accent title) applied to settings, services, reviews, teams; pricing
header returned to the editorial voice (serif + accent pill); dead
GlossaryCard deleted.

i18n residuals: suggestion chips, 'Standard' provider label, notification
close, model-combobox strings extracted (+broken bg-surface/border-border-
subtle tokens fixed); ~45 new keys EN+FR; 13 dead keys removed; zero
missing keys verified.

Reviews: icon-only row actions now carry visible text; 'Approve all' is
two-step armed-confirm.

Accelerators: Ctrl/Cmd+Enter submits; arrow-key navigation in the language
combobox; recent-jobs history cap 8->20 with per-job download; source=target
config rejected; empty 'Master Quality' badge removed.

Cookie consent: reopenable via footer link, emoji replaced with drawn icon.

Verified: build exit 0, vitest 9/9, eslint 64 errors = previous level,
0 missing i18n keys.
This commit is contained in:
2026-08-30 22:02:35 +02:00
parent 50047ea8a2
commit 111f3cb69d
41 changed files with 470 additions and 280 deletions

View File

@@ -142,13 +142,13 @@ export default function TranslatePage() {
};
const handleNewTranslation = () => { submit.reset(); upload.removeFile(); setElapsed(0); setRecentJobs(getRecentJobs()); };
const handleDownload = async () => {
if (!submit.jobId) return;
const handleDownload = async (jobId: string = submit.jobId ?? '') => {
if (!jobId) return;
const token = localStorage.getItem('token');
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
try {
const response = await fetch(`${API_BASE}/api/v1/download/${submit.jobId}`, { headers });
const response = await fetch(`${API_BASE}/api/v1/download/${jobId}`, { headers });
if (!response.ok) {
showError({ title: t('dashboard.translate.error.title'), description: t('translate.downloadFailed') });
return;
@@ -180,6 +180,19 @@ export default function TranslatePage() {
const isCompleted = submit.status === 'completed';
const isFailed = submit.status === 'failed';
// Ctrl/Cmd+Enter launches the translation from anywhere on the page
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && upload.file && config.isConfigValid && !submit.isSubmitting && !isProcessing) {
e.preventDefault();
handleTranslate();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [upload.file, config.isConfigValid, submit.isSubmitting, isProcessing]);
const showUpload = !upload.file && !isProcessing && !isCompleted && !isFailed;
const showConfiguring = isConfiguring;
const showProcessing = isProcessing;
@@ -311,18 +324,27 @@ export default function TranslatePage() {
{t('translate.recent.title')}
</h4>
<ul className="divide-y divide-black/[0.03] dark:divide-white/[0.03]">
{recentJobs.slice(0, 4).map((job) => (
{recentJobs.slice(0, 6).map((job) => (
<li key={job.jobId} className="flex items-center justify-between gap-3 py-3">
<span className="flex min-w-0 items-center gap-2.5 text-xs font-semibold text-brand-dark dark:text-white">
<FileText className="size-3.5 shrink-0 text-brand-accent" />
<span className="truncate">{job.fileName || job.jobId}</span>
</span>
<Link
href={`/dashboard/reviews/${job.jobId}`}
className="shrink-0 rounded-lg px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-brand-accent border border-brand-accent/25 hover:bg-brand-accent/10 transition-colors"
>
{t('translate.recent.review')}
</Link>
<span className="flex shrink-0 items-center gap-1.5">
<button
type="button"
onClick={() => handleDownload(job.jobId)}
className="rounded-lg px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-brand-dark/60 dark:text-white/60 border border-black/10 dark:border-white/10 hover:bg-brand-muted/50 transition-colors"
>
<Download size={11} className="inline" />
</button>
<Link
href={`/dashboard/reviews/${job.jobId}`}
className="rounded-lg px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-brand-accent border border-brand-accent/25 hover:bg-brand-accent/10 transition-colors"
>
{t('translate.recent.review')}
</Link>
</span>
</li>
))}
</ul>
@@ -446,21 +468,18 @@ export default function TranslatePage() {
</div>
<div className="text-left">
<p className="text-[13px] font-bold uppercase tracking-[0.1em] text-brand-dark dark:text-white">
{t('translate.header.completedTitle')}
{t('translate.header.completedTitleBase')} {t('translate.header.completedTitleAccent')}
</p>
<p className="text-[11px] text-brand-dark/50 dark:text-white/50 font-semibold uppercase mt-1 tracking-wider max-w-[300px] truncate">
{submit.fileName}
</p>
</div>
</div>
<span className="px-5 py-2 bg-white dark:bg-[#1a1a1a] rounded-full text-[10px] font-bold uppercase tracking-wider text-brand-accent border border-brand-accent/20 shadow-sm shrink-0">
{t('translate.complete.masterQuality')}
</span>
</div>
<div className="flex-1 flex flex-col items-center justify-center py-16 bg-brand-muted/20 dark:bg-white/5 rounded-[40px] border border-black/5 dark:border-white/5">
<button
onClick={handleDownload}
onClick={() => handleDownload()}
className="premium-button px-24 py-6 text-xl !rounded-full flex items-center gap-6 mb-8 group cursor-pointer hover:scale-[1.02] active:scale-95"
>
<Download size={28} className="group-hover:translate-y-1 transition-transform" />