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
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:
@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Segment {
|
||||
@@ -77,6 +78,7 @@ export default function ReviewPage() {
|
||||
const [draft, setDraft] = useState('');
|
||||
const [savingId, setSavingId] = useState<string | null>(null);
|
||||
const [isRebuilding, setIsRebuilding] = useState(false);
|
||||
const [approveArmed, setApproveArmed] = useState(false);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -136,6 +138,13 @@ export default function ReviewPage() {
|
||||
};
|
||||
|
||||
const approveAll = async () => {
|
||||
// Two-step: the first click arms, the second confirms — no accidental bulk-approve.
|
||||
if (!approveArmed) {
|
||||
setApproveArmed(true);
|
||||
setTimeout(() => setApproveArmed(false), 4000);
|
||||
return;
|
||||
}
|
||||
setApproveArmed(false);
|
||||
const pending = segments.filter((s) => s.status === 'pending');
|
||||
for (const seg of pending) {
|
||||
// sequential is fine — server-side each is a tiny PATCH
|
||||
@@ -232,10 +241,13 @@ export default function ReviewPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-foreground">{t('reviews.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground truncate max-w-xl">
|
||||
<span className="accent-pill mb-3 block w-fit">{t('reviews.pill')}</span>
|
||||
<h1 className="mb-2 text-3xl font-serif font-medium leading-tight tracking-tight text-brand-dark dark:text-white">
|
||||
{t('reviews.titleBase')} <span className="italic">{t('reviews.titleAccent')}</span>
|
||||
</h1>
|
||||
<p className="max-w-xl truncate text-sm text-brand-dark/60 dark:text-white/60">
|
||||
{t('reviews.subtitle', {
|
||||
file: fileName ?? jobId,
|
||||
total: counts.total,
|
||||
@@ -246,8 +258,11 @@ export default function ReviewPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={approveAll} disabled={counts.pending === 0}>
|
||||
<CheckCheck className="size-3.5" /> {t('reviews.approveAll')}
|
||||
<Button variant={approveArmed ? 'default' : 'outline'} size="sm" onClick={approveAll} disabled={counts.pending === 0}>
|
||||
<CheckCheck className="size-3.5" />
|
||||
{approveArmed
|
||||
? t('reviews.approveAllConfirm', { count: counts.pending })
|
||||
: t('reviews.approveAll')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={exportXliff} disabled={!segments.length}>
|
||||
<FileDown className="size-3.5" /> XLIFF
|
||||
@@ -360,38 +375,35 @@ export default function ReviewPage() {
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
{statusBadge(seg.status)}
|
||||
{!isEditing && (
|
||||
<div className="flex gap-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 px-2"
|
||||
title={t('reviews.action.edit')}
|
||||
className="h-7 gap-1 px-2 text-[11px]"
|
||||
onClick={() => {
|
||||
setEditingId(seg.id);
|
||||
setDraft(finalText);
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-3" />
|
||||
<Pencil className="size-3" /> {t('reviews.action.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 px-2"
|
||||
title={t('reviews.action.approve')}
|
||||
className="h-7 gap-1 px-2 text-[11px]"
|
||||
disabled={seg.status === 'approved' || savingId === seg.id}
|
||||
onClick={() => patchSegment(seg.id, { status: 'approved' })}
|
||||
>
|
||||
<Check className="size-3" />
|
||||
<Check className="size-3" /> {t('reviews.action.approveShort')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 px-2"
|
||||
title={t('reviews.action.reset')}
|
||||
className="h-7 gap-1 px-2 text-[11px]"
|
||||
disabled={seg.status === 'pending'}
|
||||
onClick={() => patchSegment(seg.id, { status: 'pending' })}
|
||||
>
|
||||
<Undo2 className="size-3" />
|
||||
<Undo2 className="size-3" /> {t('reviews.action.resetShort')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user