feat(ui,api): wave 3 — editorial pricing, real cancel, server history, DeepL purge
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m36s

Pricing: full editorial redesign — serif card headers with accent pills
replace the colored font-black blocks, tone sweep across toggle/metrics/
features/CTAs, PLAN_COLORS removed; one design system app-wide.

Translate: decorative titles one step down (CTA hierarchy restored);
glossary and image-translation blocks hidden entirely for free users
(progressive disclosure — three controls for free).

Reviews: XLIFF hint line explains the exchange format; backend errors
routed through a friendly mapper (session/not-found/rate-limit/server).

Landing: fabricated hero UI cards (fake 'Context Engine' overlay)
removed — the photo no longer promises screens that don't exist.

Nav: single DashboardNavLinks component shared by sidebar and mobile
drawer (was duplicated markup).

API: GET /api/v1/translations (user job history, paginated; completed
jobs retained 24h) and POST /api/v1/translations/{id}/cancel —
cooperative cancellation with worker checkpoints before dispatch and
before finalisation, reserved quota released immediately. Translate
monitor now offers a real 'Cancel translation' next to 'Back to start';
recent-jobs list reads server history first, localStorage fallback.

DeepL purge (backend): provider module, registry registration, config
attrs/defaults, dispatch branch, admin settings schema + test branch,
legacy availability block, validation rules, plan provider lists,
error-code mappings, MCP enums, translator prompt mention, related
tests updated/removed. Fallback resolver skips unknown providers, so
stale chains containing 'deepl' degrade gracefully.

Verified: backend 110 tests passed; frontend build exit 0, vitest 9/9,
0 missing i18n keys, eslint 63 errors (vs 64 at HEAD).
This commit is contained in:
2026-08-30 22:42:29 +02:00
parent 52748ee653
commit 67365918ae
35 changed files with 398 additions and 1572 deletions

View File

@@ -215,14 +215,6 @@ const PLAN_ICONS: Record<string, any> = {
enterprise: Shield,
};
const PLAN_COLORS: Record<string, { header: string; iconColor: string; nameColor: string }> = {
free: { header: "bg-muted", iconColor: "text-foreground/40", 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 (
@@ -460,7 +452,7 @@ export default function PricingPage() {
<div className="flex justify-between items-center mb-20">
<button
onClick={() => router.back()}
className="flex items-center gap-3 text-[10px] font-black uppercase tracking-[0.4em] text-foreground/30 hover:text-foreground transition-all group"
className="flex items-center gap-3 text-[11px] font-bold uppercase tracking-[0.4em] text-foreground/30 hover:text-foreground transition-all group"
>
<ChevronLeft size={16} className="group-hover:-translate-x-1 transition-transform" />
{t('pricing.nav.back')}
@@ -476,7 +468,7 @@ export default function PricingPage() {
{isLoggedIn && (
<Link
href="/dashboard/profile"
className="px-6 py-2 bg-white rounded-full text-[9px] font-black uppercase tracking-widest text-foreground shadow-sm border border-black/5"
className="px-6 py-2 bg-white rounded-full text-[9px] font-bold uppercase tracking-wider text-foreground shadow-sm border border-black/5"
>
{t('pricing.nav.mySubscription')}
</Link>
@@ -562,13 +554,13 @@ export default function PricingPage() {
<div className="flex p-1 bg-muted rounded-full border border-black/5 shadow-inner px-2">
<button
onClick={() => setIsYearly(false)}
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'}`}
className={`px-8 py-3 rounded-full text-[11px] font-bold uppercase tracking-wider transition-all ${!isYearly ? 'bg-brand-dark text-white shadow-xl dark:bg-brand-accent dark:text-brand-dark' : 'text-foreground/60 hover:text-foreground'}`}
>
{t('pricing.billing.monthly')}
</button>
<button
onClick={() => setIsYearly(true)}
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'}`}
className={`px-8 py-3 rounded-full text-[11px] font-bold uppercase tracking-wider transition-all ${isYearly ? 'bg-brand-dark text-white shadow-xl dark:bg-brand-accent dark:text-brand-dark' : 'text-foreground/60 hover:text-foreground'}`}
>
{t('pricing.billing.yearly')}
<span className={`ml-2 transition-colors ${isYearly ? 'text-accent' : 'text-accent/60'}`}>&minus;{annualDiscountPercent} %</span>
@@ -585,7 +577,6 @@ export default function PricingPage() {
<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";
@@ -599,43 +590,34 @@ export default function PricingPage() {
plan.popular && "border-accent/30 ring-4 ring-accent/5"
)}
>
{/* ── 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">
{/* ── Header (editorial) ── */}
<div className="relative border-b border-black/[0.04] p-8 pb-6 dark:border-white/[0.06]">
{(plan.popular || isCurrent) && (
<div className="absolute top-5 right-5 flex gap-2">
{plan.badge && (
<span className="bg-foreground/20 backdrop-blur-md text-white text-[10px] font-black uppercase tracking-wider px-3 py-1 rounded-full border border-white/20 shadow-lg">
<span className="rounded-full border border-brand-accent/30 bg-brand-accent/10 px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-brand-accent">
{t(plan.badge)}
</span>
)}
{isCurrent && (
<span className="bg-foreground/40 backdrop-blur-md text-white text-[10px] font-black uppercase tracking-wider 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 className="flex items-center gap-1.5 rounded-full border border-black/10 bg-brand-muted px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-brand-dark/70 dark:border-white/10 dark:bg-white/10 dark:text-white/70">
<span className="h-1.5 w-1.5 rounded-full bg-brand-accent animate-pulse" /> {t('pricing.card.myPlan')}
</span>
)}
</div>
)}
{plan.badge && !plan.popular && (
<div className="absolute top-0 right-0 p-3">
<span className="bg-white/10 backdrop-blur-md text-white text-[10px] font-black uppercase tracking-wider 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-[10px] font-black uppercase tracking-wider 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>
{!plan.popular && !isCurrent && plan.badge && (
<span className="absolute top-5 right-5 rounded-full border border-black/[0.06] bg-brand-muted px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-brand-dark/60 dark:border-white/[0.08] dark:bg-white/5 dark:text-white/60">
{t(plan.badge)}
</span>
)}
{/* 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)}>
<div className="mb-4 flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-xl bg-brand-muted text-brand-accent dark:bg-white/10">
<Icon size={18} />
</div>
<span className="text-xs font-bold uppercase tracking-[0.15em] text-brand-dark/70 dark:text-white/70">
{t(plan.name)}
</span>
</div>
@@ -643,30 +625,30 @@ export default function PricingPage() {
{/* Price */}
<div className="flex items-baseline gap-2">
{isEnterprise ? (
<h3 className={cn("text-3xl font-black uppercase tracking-tighter", isFree ? "text-foreground" : "text-white")}>
<h3 className="text-3xl font-serif font-medium tracking-tight text-brand-dark dark:text-white">
{t('pricing.card.onRequest')}
</h3>
) : price === 0 ? (
<h3 className="text-3xl font-black uppercase tracking-tighter text-foreground">
<h3 className="text-3xl font-serif font-medium tracking-tight text-brand-dark dark:text-white">
{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/70">{t('pricing.card.perMonth')}</span>
<h3 className="text-4xl font-serif font-medium tracking-tight text-brand-dark dark:text-white">{price} &euro;</h3>
<span className="text-[11px] font-semibold uppercase tracking-wider text-brand-dark/55 dark:text-white/55">{t('pricing.card.perMonth')}</span>
</>
)}
</div>
{/* Yearly billing note */}
{isYearly && plan.price_yearly > 0 && (
<div className="text-white/70 text-[10px] mt-1">
<div className="mt-1 text-[11px] text-brand-dark/55 dark:text-white/55">
{t('pricing.card.billedYearly', { price: plan.price_yearly.toFixed(2) })}
</div>
)}
{/* Description */}
<p className={cn("text-[10px] font-medium uppercase mt-3 tracking-widest leading-relaxed", isFree ? "text-foreground/40" : "text-white/60")}>
<p className="mt-3 text-xs font-light leading-relaxed text-brand-dark/60 dark:text-white/60">
{t(plan.description || '')}
</p>
</div>
@@ -678,18 +660,18 @@ export default function PricingPage() {
<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/40" />
<span className="text-[11px] font-bold uppercase tracking-wider text-foreground/60">{t('pricing.card.documents')}</span>
<span className="text-[11px] font-semibold text-foreground/70">{t('pricing.card.documents')}</span>
</div>
<span className="text-[10px] font-black uppercase text-foreground">
<span className="text-[11px] font-bold 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/40" />
<span className="text-[11px] font-bold uppercase tracking-wider text-foreground/60">{t('pricing.card.pagesMax')}</span>
<span className="text-[11px] font-semibold text-foreground/70">{t('pricing.card.pagesMax')}</span>
</div>
<span className="text-[10px] font-black uppercase text-foreground">
<span className="text-[11px] font-bold text-foreground">
{plan.max_pages_per_doc === -1 ? t('pricing.card.unlimited') : `${plan.max_pages_per_doc} ${t('pricing.card.perDoc')}`}
</span>
</div>
@@ -706,13 +688,13 @@ export default function PricingPage() {
"text-foreground/40"
} />
<span className={cn(
"text-[9px] font-black uppercase tracking-widest",
"text-[10px] font-bold uppercase tracking-wider",
plan.ai_tier === "essential" ? "text-accent/60" :
"text-foreground/40"
)}>{t('pricing.card.aiTranslation')}</span>
</div>
<span className={cn(
"text-[9px] font-black uppercase",
"text-[10px] font-bold uppercase",
plan.ai_tier === "essential" ? "text-accent" :
plan.ai_tier === "premium" ? "text-foreground" :
"text-foreground"
@@ -732,7 +714,7 @@ export default function PricingPage() {
<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>
<span className="text-[11px] font-medium text-foreground/75 leading-normal">{t(feat)}</span>
</li>
))}
</ul>
@@ -741,7 +723,7 @@ export default function PricingPage() {
{isCurrent ? (
<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"
className="w-full py-4 rounded-2xl text-xs font-bold uppercase tracking-wider 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"
>
{t('pricing.card.managePlan')}
<ArrowRight size={14} className="opacity-40" />
@@ -749,7 +731,7 @@ export default function PricingPage() {
) : 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"
className="w-full py-4 rounded-2xl text-xs font-bold uppercase tracking-wider 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')}
<ArrowRight size={14} className="opacity-40" />
@@ -759,7 +741,7 @@ export default function PricingPage() {
onClick={() => setConfirmPlan(plan)}
disabled={loadingPlanId !== null}
className={cn(
"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",
"w-full py-4 rounded-2xl text-xs font-bold uppercase tracking-wider 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",