Files
office_translator/frontend/src/app/dashboard/profile/page.tsx
2026-06-14 11:15:09 +02:00

622 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { API_BASE } from '@/lib/config';
import { useI18n, type Locale, formatDate } from '@/lib/i18n';
import {
User, Mail, Calendar, Crown, Zap, Sparkles, Building2, Rocket,
FileText, Layers, CreditCard, TrendingUp, AlertTriangle,
CheckCircle2, XCircle, RefreshCw, ExternalLink, ArrowRight,
BadgeCheck, ShieldAlert, Info, Globe, Settings, Palette, Trash2, Loader2,
Activity, ChevronDown,
} from 'lucide-react';
import { ThemeToggle } from '@/components/ui/theme-toggle';
import { languages } from '@/lib/api';
import { useTranslationStore } from '@/lib/store';
import { cn } from '@/lib/utils';
import { useQueryClient } from '@tanstack/react-query';
/* ── helpers ──────────────────────────────────────────────────── */
const PLAN_ICONS: Record<string, React.ElementType> = {
free: Sparkles, starter: Zap, pro: Crown, business: Building2, enterprise: Rocket,
};
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',
};
const PLAN_PRICES: Record<string, number> = { free: 0, starter: 9, pro: 19, business: 49 };
function getInitials(name?: string) {
if (!name) return '??';
return name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase();
}
function pct(used: number, limit: number) {
if (limit === -1 || limit === 0) return 0;
return Math.min(100, Math.round((used / limit) * 100));
}
function fmtLimit(val: number) { return val === -1 ? '∞' : String(val); }
function nextResetDate(locale: Locale) {
const now = new Date();
const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
return formatDate(next, locale, { day: 'numeric', month: 'long' });
}
const UI_LANGUAGES: { value: Locale; label: string; flag: string }[] = [
{ value: 'en', label: 'English', flag: '🇬🇧' },
{ value: 'fr', label: 'Français', flag: '🇫🇷' },
{ value: 'es', label: 'Español', flag: '🇪🇸' },
{ value: 'de', label: 'Deutsch', flag: '🇩🇪' },
{ value: 'pt', label: 'Português', flag: '🇧🇷' },
{ value: 'it', label: 'Italiano', flag: '🇮🇹' },
{ value: 'nl', label: 'Nederlands', flag: '🇳🇱' },
{ value: 'ru', label: 'Русский', flag: '🇷🇺' },
{ value: 'ja', label: '日本語', flag: '🇯🇵' },
{ value: 'ko', label: '한국어', flag: '🇰🇷' },
{ value: 'zh', label: '中文', flag: '🇨🇳' },
{ value: 'ar', label: 'العربية', flag: '🇸🇦' },
{ value: 'fa', label: 'فارسی', flag: '🇮🇷' },
];
function formatTitleWithItalic(title: string) {
const words = title.split(' ');
if (words.length > 1) {
return (
<>
{words.slice(0, -1).join(' ')} <span className="italic">{words[words.length - 1]}</span>
</>
);
}
return title;
}
/* ── Main component ───────────────────────────────────────────── */
export default function ProfilePage() {
const router = useRouter();
const { locale, setLocale, t } = useI18n();
const { settings, updateSettings } = useTranslationStore();
const [user, setUser] = useState<any>(null);
const [usage, setUsage] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [cancelConfirm, setCancelConfirm] = useState(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [loadingPortal, setLoadingPortal] = useState(false);
const [loadingCancel, setLoadingCancel] = useState(false);
const [isClearing, setIsClearing] = useState(false);
const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null;
const [activeTab, setActiveTab] = useState(searchParams?.get('tab') ?? 'account');
const [defaultLanguage, setDefaultLanguage] = useState(settings.defaultTargetLanguage);
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
const authHeaders = { Authorization: `Bearer ${token}` };
const queryClient = useQueryClient();
const fetchData = useCallback(async () => {
if (!token) { router.push('/auth/login?redirect=/dashboard/profile'); return; }
try {
const [meRes, usageRes] = await Promise.all([
fetch(`${API_BASE}/api/v1/auth/me`, { headers: authHeaders }),
fetch(`${API_BASE}/api/v1/auth/usage`, { headers: authHeaders }),
]);
if (meRes.ok) {
const j = await meRes.json();
const userData = j.data ?? j;
setUser(userData);
queryClient.setQueryData(['user', 'me'], userData);
}
if (usageRes.ok) { const j = await usageRes.json(); setUsage(j.data ?? j); }
} catch { /* ignore */ } finally { setLoading(false); }
}, [token, queryClient]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { fetchData(); }, [fetchData]);
useEffect(() => { setDefaultLanguage(settings.defaultTargetLanguage); }, [settings.defaultTargetLanguage]);
const handleBillingPortal = async () => {
setLoadingPortal(true);
try {
const res = await fetch(`${API_BASE}/api/v1/auth/billing-portal`, { headers: authHeaders });
const j = await res.json();
const url = j.data?.url ?? j.url;
if (url) window.location.href = url; // same tab so return_url brings back to app
else setStatusMsg({ type: 'err', text: t('profile.subscription.billingUnavailable') });
} catch { setStatusMsg({ type: 'err', text: t('profile.subscription.billingError') }); }
finally { setLoadingPortal(false); }
};
const handleCancel = async () => {
if (!cancelConfirm) { setCancelConfirm(true); return; }
setLoadingCancel(true);
try {
const res = await fetch(`${API_BASE}/api/v1/auth/cancel-subscription`, { method: 'POST', headers: authHeaders });
if (res.ok) {
setStatusMsg({ type: 'ok', text: t('profile.subscription.cancelSuccess') });
setCancelConfirm(false);
fetchData();
} else { setStatusMsg({ type: 'err', text: t('profile.subscription.cancelError') }); }
} catch { setStatusMsg({ type: 'err', text: t('profile.subscription.networkError') }); }
finally { setLoadingCancel(false); }
};
const handleSavePrefs = () => {
updateSettings({ defaultTargetLanguage: defaultLanguage });
};
const handleClearCache = async () => {
setIsClearing(true);
try {
localStorage.removeItem('translation-settings');
sessionStorage.clear();
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
}
await new Promise(resolve => setTimeout(resolve, 500));
window.location.reload();
} catch (error) {
console.error('Error clearing cache:', error);
setIsClearing(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<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 PlanIcon = PLAN_ICONS[planId] ?? Sparkles;
const planPrice = PLAN_PRICES[planId];
const isFreePlan = planId === 'free';
const isCanceling = user?.cancel_at_period_end === true;
const docsUsed = usage?.docs_used ?? user?.docs_translated_this_month ?? 0;
const docsLimit = usage?.docs_limit ?? user?.plan_limits?.docs_per_month ?? 5;
const pagesUsed = usage?.pages_used ?? user?.pages_translated_this_month ?? 0;
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);
return (
<div className="flex h-full flex-col overflow-y-auto">
<div className="flex flex-1 flex-col gap-8 p-6 lg:p-8 max-w-4xl mx-auto w-full">
{/* ── Editorial Header ───────────────────────────────────── */}
<div className="mb-4">
<span className="accent-pill mb-4 block w-fit">{t('profile.header.title')}</span>
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
{formatTitleWithItalic(t('profile.header.title'))}
</h1>
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">{t('profile.header.subtitle')}</p>
</div>
{/* Status message */}
{statusMsg && (
<div className={cn(
'flex items-start gap-3 p-4 rounded-xl border text-sm',
statusMsg.type === 'ok'
? 'bg-success/10 border-success/30 text-success'
: 'bg-destructive/10 border-destructive/30 text-destructive'
)}>
{statusMsg.type === 'ok' ? <CheckCircle2 className="w-4 h-4 shrink-0 mt-0.5" /> : <XCircle className="w-4 h-4 shrink-0 mt-0.5" />}
<span className="flex-1">{statusMsg.text}</span>
<button onClick={() => setStatusMsg(null)} className="text-muted-foreground hover:text-foreground"></button>
</div>
)}
{/* ── 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 ────────────────────────────────────── */}
{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-serif font-medium text-brand-dark dark:text-white tracking-tight">
{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>
<div>
<div className="flex items-center gap-3 flex-wrap">
<h3 className="text-2xl font-serif font-medium text-white tracking-tight">
{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') : isFreePlan ? t('profile.subscription.active') : user?.subscription_status ?? t('profile.subscription.unknown')}
</span>
</div>
<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>
</div>
</div>
{!isFreePlan && (
<div className="relative z-10 flex flex-col sm:flex-row gap-3">
{/* Change plan → pricing page */}
<Link href="/pricing">
<button className="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.changePlan')}
</button>
</Link>
{/* Billing portal → invoices / cancel / payment method */}
<button
onClick={handleBillingPortal}
disabled={loadingPortal}
className="px-10 py-4 bg-white/10 border border-white/20 text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:bg-white/20 transition-all shrink-0 disabled:opacity-50"
>
{loadingPortal ? <RefreshCw className="w-4 h-4 animate-spin inline" /> : t('profile.subscription.manageBilling')}
</button>
</div>
)}
{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>
)}
{/* 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>
<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(
'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)]'
)}
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>
</div>
)}
{/* Danger zone — show if user has a paid plan (even if sub ID not yet synced) */}
{!isFreePlan && !isCanceling && (
<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-serif font-medium text-brand-dark dark:text-white tracking-tight">
{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 */}
<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>
<div>
<h3 className="text-2xl font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{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}
onChange={(e) => setDefaultLanguage(e.target.value)}
className="appearance-none px-8 py-4 pr-12 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-[220px] outline-none focus:ring-2 focus:ring-brand-accent/20 cursor-pointer"
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.flag} {lang.name}
</option>
))}
</select>
<ChevronDown size={14} className="absolute right-4 top-1/2 -translate-y-1/2 text-brand-accent pointer-events-none" />
</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 */}
<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>
<div>
<h3 className="text-2xl font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{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 */}
<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-serif font-medium text-brand-dark dark:text-white tracking-tight">
{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>
);
}