'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 = { free: Sparkles, starter: Zap, pro: Crown, business: Building2, enterprise: Rocket, }; const PLAN_LABELS: Record = { free: 'profile.plan.free', starter: 'profile.plan.starter', pro: 'profile.plan.pro', business: 'profile.plan.business', enterprise: 'profile.plan.enterprise', }; const PLAN_PRICES: Record = { 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(' ')} {words[words.length - 1]} ); } return title; } /* ── Main component ───────────────────────────────────────────── */ export default function ProfilePage() { const router = useRouter(); const { locale, setLocale, t } = useI18n(); const { settings, updateSettings } = useTranslationStore(); const [user, setUser] = useState(null); const [usage, setUsage] = useState(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 (
); } 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 (
{/* ── Editorial Header ───────────────────────────────────── */}
{t('profile.header.title')}

{formatTitleWithItalic(t('profile.header.title'))}

{t('profile.header.subtitle')}

{/* Status message */} {statusMsg && (
{statusMsg.type === 'ok' ? : } {statusMsg.text}
)} {/* ── Editorial Pill Tabs ────────────────────────────────── */}
{[ { 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) => ( ))}
{/* ── Tab: Account ────────────────────────────────────── */} {activeTab === 'account' && (
{getInitials(user?.name)}

{user?.name || t('profile.account.user')}

{planLabel}

{user?.email}

{user?.created_at && (

{t('profile.account.memberSince')} {formatDate(new Date(user.created_at), locale)}

)}
)} {/* ── Tab: Subscription ───────────────────────────────── */} {activeTab === 'subscription' && (
{/* Plan card - dark editorial */}
{/* Decorative element */}

{t('profile.plan.label')} {planLabel}

{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')}

{isFreePlan ? t('profile.plan.free') : t('profile.plan.pricePerMonth', { price: planPrice })}

{!isFreePlan && (
{/* Change plan → pricing page */} {/* Billing portal → invoices / cancel / payment method */}
)} {isFreePlan && ( )}
{/* Subscription ends info */} {!isFreePlan && user?.subscription_ends_at && (
{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)}`}
)} {/* Usage metrics */}
{t('profile.usage.title')}
{t('profile.usage.resetOn')} {nextResetDate(locale)}
{/* Documents bar */}
{t('profile.usage.documents')} {docsUsed} / {fmtLimit(docsLimit)}
= 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}%` }} />
{/* Pages bar */}
{t('profile.usage.pages')} {pagesUsed} / {fmtLimit(pagesLimit)}
= 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}%` }} />
{/* Extra credits */} {extraCredits > 0 && (
{extraCredits} {extraCredits > 1 ? t('profile.usage.extraCreditsPlural') : t('profile.usage.extraCredits')}
)} {/* Quota reached warning */} {usage?.upgrade_required && (

{t('profile.usage.quotaReached')}

{t('profile.usage.quotaReachedDesc')}

)} {/* Upgrade CTA */} {isFreePlan && (

{t('profile.usage.unlockMore')}

)}
{/* Features grid */} {user?.plan_limits?.features?.length > 0 && (
{t('profile.usage.includedInPlan')}
{user.plan_limits.features.map((f: string, i: number) => (
{f.includes('.') ? t(f) : f}
))}
)} {/* Danger zone — show if user has a paid plan (even if sub ID not yet synced) */} {!isFreePlan && !isCanceling && (
{t('profile.danger.title')}

{t('profile.danger.description')}

{cancelConfirm && (
⚠️ {t('profile.danger.confirm')}
)}
{cancelConfirm && ( )}
)}
)} {/* ── Tab: Preferences ────────────────────────────────── */} {activeTab === 'preferences' && (
{/* Interface language */}

{t('profile.prefs.interfaceLang')}

{UI_LANGUAGES.map((lang) => ( ))}

{t('profile.prefs.interfaceLangDesc')}

{/* Default target language */}

{t('profile.prefs.defaultTargetLang')}

{t('profile.prefs.defaultTargetLangDesc')}

{/* Theme */}

{t('profile.prefs.theme')}

{t('profile.prefs.themeDesc')}

{/* Cache */}

{t('profile.prefs.cache')}

{t('profile.prefs.cacheDesc')}

)}
); }