Files
SaaS-PDF/frontend/src/pages/AccountPage.tsx
Your Name 7e9edc2992 feat: Enhance Pricing Page with Enterprise Plan and Billing Toggle
- Added Enterprise plan with features and pricing.
- Introduced billing toggle for monthly and yearly subscriptions.
- Updated feature list to include enterprise-specific features.
- Improved UI for plan cards and added new styles for better visual appeal.
- Adjusted SEO metadata to reflect new pricing structure.
- Enhanced global styles for marketing elements.
2026-04-04 20:01:03 +02:00

853 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState, type FormEvent } from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import {
AlertTriangle,
BarChart3,
BadgeCheck,
Check,
Copy,
Download,
FolderClock,
KeyRound,
LogOut,
PartyPopper,
ShieldCheck,
Sparkles,
Trash2,
UserRound,
Zap,
} from 'lucide-react';
import {
getHistory,
getUsage,
getApiKeys,
createApiKey,
revokeApiKey,
type HistoryEntry,
type UsageSummary,
type ApiKey,
} from '@/services/api';
import { useAuthStore } from '@/stores/authStore';
type AuthMode = 'login' | 'register';
const toolKeyMap: Record<string, string> = {
'pdf-to-word': 'tools.pdfToWord.title',
'word-to-pdf': 'tools.wordToPdf.title',
'compress-pdf': 'tools.compressPdf.title',
'compress-image': 'tools.compressImage.title',
'crop-pdf': 'tools.cropPdf.title',
'crop-image': 'tools.imageCrop.title',
'edit-metadata': 'tools.pdfMetadata.title',
'excel-to-pdf': 'tools.excelToPdf.title',
'extract-pages': 'tools.extractPages.title',
'extract-tables': 'tools.tableExtractor.title',
'flatten-pdf': 'tools.flattenPdf.title',
'html-to-pdf': 'tools.htmlToPdf.title',
'image-convert': 'tools.imageConvert.title',
'image-converter': 'tools.imageConvert.title',
'image-crop': 'tools.imageCrop.title',
'image-resize': 'tools.imageConvert.title',
'image-rotate-flip': 'tools.imageRotateFlip.title',
'video-to-gif': 'tools.videoToGif.title',
'merge-pdf': 'tools.mergePdf.title',
'ocr': 'tools.ocr.title',
'split-pdf': 'tools.splitPdf.title',
'pdf-metadata': 'tools.pdfMetadata.title',
'pdf-to-excel': 'tools.pdfToExcel.title',
'pdf-to-pptx': 'tools.pdfToPptx.title',
'rotate-pdf': 'tools.rotatePdf.title',
'page-numbers': 'tools.pageNumbers.title',
'pdf-to-images': 'tools.pdfToImages.title',
'images-to-pdf': 'tools.imagesToPdf.title',
'watermark-pdf': 'tools.watermarkPdf.title',
'protect-pdf': 'tools.protectPdf.title',
'unlock-pdf': 'tools.unlockPdf.title',
'repair-pdf': 'tools.repairPdf.title',
'remove-background': 'tools.removeBg.title',
'remove-bg': 'tools.removeBg.title',
'remove-watermark-pdf': 'tools.removeWatermark.title',
'reorder-pdf': 'tools.reorderPdf.title',
'sign-pdf': 'tools.signPdf.title',
'summarize-pdf': 'tools.summarizePdf.title',
'translate-pdf': 'tools.translatePdf.title',
'chat-pdf': 'tools.chatPdf.title',
'barcode': 'tools.barcode.title',
'barcode-generator': 'tools.barcode.title',
'pptx-to-pdf': 'tools.pptxToPdf.title',
'pdf-flowchart': 'tools.pdfFlowchart.title',
'pdf-flowchart-sample': 'tools.pdfFlowchart.title',
'qr-code': 'tools.qrCode.title',
};
function formatHistoryTool(tool: string, t: (key: string) => string) {
const translationKey = toolKeyMap[tool];
return translationKey ? t(translationKey) : tool;
}
export default function AccountPage() {
const { t, i18n } = useTranslation();
const user = useAuthStore((state) => state.user);
const authLoading = useAuthStore((state) => state.isLoading);
const initialized = useAuthStore((state) => state.initialized);
const login = useAuthStore((state) => state.login);
const register = useAuthStore((state) => state.register);
const logout = useAuthStore((state) => state.logout);
const isNewAccount = useAuthStore((state) => state.isNewAccount);
const clearNewAccount = useAuthStore((state) => state.clearNewAccount);
const credits = useAuthStore((state) => state.credits);
// Welcome celebration for new registrations
useEffect(() => {
if (isNewAccount && user) {
toast(t('account.welcomeTitle'), {
description: t('account.welcomeMessage'),
icon: <PartyPopper className="h-5 w-5 text-amber-500" />,
duration: 6000,
});
clearNewAccount();
}
}, [isNewAccount, user, t, clearNewAccount]);
const [mode, setMode] = useState<AuthMode>('login');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [submitError, setSubmitError] = useState<string | null>(null);
const [historyItems, setHistoryItems] = useState<HistoryEntry[]>([]);
const [historyLoading, setHistoryLoading] = useState(false);
const [historyError, setHistoryError] = useState<string | null>(null);
// Usage summary state
const [usage, setUsage] = useState<UsageSummary | null>(null);
// API Keys state (pro only)
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [apiKeysLoading, setApiKeysLoading] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [newKeyCreating, setNewKeyCreating] = useState(false);
const [newKeyError, setNewKeyError] = useState<string | null>(null);
const [revealedKey, setRevealedKey] = useState<string | null>(null);
const [copiedKey, setCopiedKey] = useState(false);
const dateFormatter = useMemo(
() =>
new Intl.DateTimeFormat(i18n.language, {
dateStyle: 'medium',
timeStyle: 'short',
}),
[i18n.language]
);
const dashboardMetrics = useMemo(() => {
const completedItems = historyItems.filter((item) => item.status === 'completed');
const failedItems = historyItems.filter((item) => item.status !== 'completed');
const toolCounts = historyItems.reduce<Record<string, number>>((acc, item) => {
acc[item.tool] = (acc[item.tool] || 0) + 1;
return acc;
}, {});
const favoriteToolSlug = Object.entries(toolCounts)
.sort((left, right) => right[1] - left[1])[0]?.[0] || null;
return {
totalProcessed: historyItems.length,
completedCount: completedItems.length,
failedCount: failedItems.length,
favoriteToolSlug,
successRate: historyItems.length ? Math.round((completedItems.length / historyItems.length) * 100) : 0,
topTools: Object.entries(toolCounts)
.sort((left, right) => right[1] - left[1])
.slice(0, 4),
recentFailures: failedItems.slice(0, 3),
onboardingItems: [
{
key: 'firstTask',
done: historyItems.length > 0,
title: t('account.onboardingFirstTaskTitle'),
description: t('account.onboardingFirstTaskDesc'),
},
{
key: 'upgrade',
done: user?.plan === 'pro',
title: t('account.onboardingUpgradeTitle'),
description: t('account.onboardingUpgradeDesc'),
},
{
key: 'apiKey',
done: user?.plan !== 'pro' ? false : apiKeys.some((key) => !key.revoked_at),
title: t('account.onboardingApiTitle'),
description: t('account.onboardingApiDesc'),
},
],
};
}, [apiKeys, historyItems, t, user?.plan]);
useEffect(() => {
if (!user) {
setHistoryItems([]);
setHistoryError(null);
setUsage(null);
setApiKeys([]);
return;
}
const loadHistory = async () => {
setHistoryLoading(true);
setHistoryError(null);
try {
const items = await getHistory();
setHistoryItems(items);
} catch (error) {
setHistoryError(error instanceof Error ? error.message : t('account.loadFailed'));
} finally {
setHistoryLoading(false);
}
};
const loadUsage = async () => {
try {
const data = await getUsage();
setUsage(data);
} catch {
// non-critical, ignore
}
};
const loadApiKeys = async () => {
if (user.plan !== 'pro') return;
setApiKeysLoading(true);
try {
const keys = await getApiKeys();
setApiKeys(keys);
} catch {
// non-critical
} finally {
setApiKeysLoading(false);
}
};
void loadHistory();
void loadUsage();
void loadApiKeys();
}, [t, user]);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmitError(null);
if (mode === 'register' && password !== confirmPassword) {
const msg = t('account.passwordMismatch');
setSubmitError(msg);
toast.error(msg);
return;
}
try {
if (mode === 'login') {
await login(email, password);
} else {
await register(email, password);
}
setPassword('');
setConfirmPassword('');
} catch (error) {
const msg = error instanceof Error ? error.message : t('account.loadFailed');
setSubmitError(msg);
toast.error(msg);
}
};
const handleLogout = async () => {
setSubmitError(null);
try {
await logout();
setHistoryItems([]);
setUsage(null);
setApiKeys([]);
} catch (error) {
const msg = error instanceof Error ? error.message : t('account.loadFailed');
setSubmitError(msg);
toast.error(msg);
}
};
const handleCreateApiKey = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setNewKeyError(null);
const name = newKeyName.trim();
if (!name) return;
setNewKeyCreating(true);
try {
const key = await createApiKey(name);
setApiKeys((prev) => [key, ...prev]);
setRevealedKey(key.raw_key ?? null);
setNewKeyName('');
} catch (error) {
const msg = error instanceof Error ? error.message : t('account.loadFailed');
setNewKeyError(msg);
toast.error(msg);
} finally {
setNewKeyCreating(false);
}
};
const handleRevokeApiKey = async (keyId: number) => {
try {
await revokeApiKey(keyId);
setApiKeys((prev) =>
prev.map((k) =>
k.id === keyId ? { ...k, revoked_at: new Date().toISOString() } : k
)
);
} catch {
// ignore
}
};
const handleCopyKey = async () => {
if (!revealedKey) return;
await navigator.clipboard.writeText(revealedKey);
setCopiedKey(true);
setTimeout(() => setCopiedKey(false), 2000);
};
return (
<>
<Helmet>
<title>{t('account.metaTitle')} {t('common.appName')}</title>
<meta name="description" content={t('account.heroSubtitle')} />
</Helmet>
{!initialized && authLoading ? (
<div className="flex min-h-[40vh] items-center justify-center">
<div className="h-10 w-10 animate-spin rounded-full border-4 border-primary-200 border-t-primary-600 dark:border-primary-800 dark:border-t-primary-400" />
</div>
) : user ? (
<div className="space-y-8">
<section className="overflow-hidden rounded-[2rem] bg-gradient-to-br from-amber-100 via-orange-50 to-white p-8 shadow-sm ring-1 ring-amber-200 dark:from-amber-950/60 dark:via-slate-900 dark:to-slate-950 dark:ring-amber-900/50">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="max-w-2xl space-y-4">
<div className="inline-flex items-center gap-2 rounded-full bg-white/80 px-4 py-2 text-sm font-semibold text-amber-900 ring-1 ring-amber-200 dark:bg-amber-400/10 dark:text-amber-200 dark:ring-amber-700/40">
{user.plan === 'pro' ? <Zap className="h-4 w-4" /> : <BadgeCheck className="h-4 w-4" />}
{user.plan === 'pro' ? t('account.proPlanBadge') : t('account.freePlanBadge')}
</div>
<h1 className="text-3xl font-black tracking-tight text-slate-900 dark:text-white sm:text-4xl">
{t('account.heroTitle')}
</h1>
<p className="max-w-xl text-base leading-7 text-slate-600 dark:text-slate-300">
{t('account.heroSubtitle')}
</p>
{user.plan === 'free' && (
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
{t('account.upgradeNotice')}
</p>
)}
</div>
<div className="rounded-[1.5rem] bg-white/90 p-5 shadow-sm ring-1 ring-slate-200 dark:bg-slate-900/90 dark:ring-slate-800">
<div className="space-y-3">
<div className="flex items-center gap-3 text-slate-800 dark:text-slate-100">
<UserRound className="h-5 w-5 text-primary-600 dark:text-primary-400" />
<span className="text-sm font-medium">{t('account.signedInAs')}</span>
</div>
<p className="max-w-xs break-all text-lg font-semibold text-slate-900 dark:text-white">
{user.email}
</p>
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
<Sparkles className="h-4 w-4" />
<span>
{t('account.currentPlan')}: {user.plan === 'pro' ? t('account.plans.pro') : t('account.plans.free')}
</span>
</div>
<button type="button" onClick={handleLogout} className="btn-secondary w-full">
<LogOut className="h-4 w-4" />
{t('account.logoutCta')}
</button>
</div>
</div>
</div>
</section>
{/* Credit Balance Cards */}
{usage && usage.credits && (
<section className="grid gap-4 sm:grid-cols-2">
<div className="card rounded-[1.5rem] p-5">
<p className="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">
{t('account.creditBalanceTitle')}
</p>
<p className="mt-1 text-2xl font-bold text-slate-900 dark:text-white">
{usage.credits.credits_remaining}
<span className="text-base font-normal text-slate-400"> / {usage.credits.credits_allocated}</span>
</p>
<div className="mt-3 h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div
className="h-full rounded-full bg-primary-500 transition-all"
style={{ width: `${Math.min(100, (usage.credits.credits_used / usage.credits.credits_allocated) * 100)}%` }}
/>
</div>
{usage.credits.window_end && (
<p className="mt-2 text-xs text-slate-400">
{t('account.creditWindowResets')}: {new Date(usage.credits.window_end).toLocaleDateString()}
</p>
)}
</div>
{usage.api_quota?.limit != null && (
<div className="card rounded-[1.5rem] p-5">
<p className="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">
{t('account.apiQuotaTitle')}
</p>
<p className="mt-1 text-2xl font-bold text-slate-900 dark:text-white">
{usage.api_quota.used}
<span className="text-base font-normal text-slate-400"> / {usage.api_quota.limit}</span>
</p>
<div className="mt-3 h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div
className="h-full rounded-full bg-emerald-500 transition-all"
style={{ width: `${Math.min(100, (usage.api_quota.used / usage.api_quota.limit) * 100)}%` }}
/>
</div>
</div>
)}
</section>
)}
<section className="card rounded-[2rem] p-0">
<div className="border-b border-slate-200 px-6 py-5 dark:border-slate-700">
<div className="flex items-center gap-3">
<BarChart3 className="h-5 w-5 text-primary-600 dark:text-primary-400" />
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('account.dashboardTitle')}
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
{t('account.dashboardSubtitle')}
</p>
</div>
</div>
</div>
<div className="space-y-6 p-6">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-[1.5rem] bg-slate-50 p-5 dark:bg-slate-800/80">
<p className="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">{t('account.metricProcessed')}</p>
<p className="mt-2 text-3xl font-bold text-slate-900 dark:text-white">{dashboardMetrics.totalProcessed}</p>
</div>
<div className="rounded-[1.5rem] bg-slate-50 p-5 dark:bg-slate-800/80">
<p className="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">{t('account.metricSuccessRate')}</p>
<p className="mt-2 text-3xl font-bold text-slate-900 dark:text-white">{dashboardMetrics.successRate}%</p>
</div>
<div className="rounded-[1.5rem] bg-slate-50 p-5 dark:bg-slate-800/80">
<p className="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">{t('account.metricFavoriteTool')}</p>
<p className="mt-2 text-lg font-semibold text-slate-900 dark:text-white">
{dashboardMetrics.favoriteToolSlug
? formatHistoryTool(dashboardMetrics.favoriteToolSlug, t)
: t('account.metricFavoriteToolEmpty')}
</p>
</div>
<div className="rounded-[1.5rem] bg-slate-50 p-5 dark:bg-slate-800/80">
<p className="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">{t('account.metricFailures')}</p>
<p className="mt-2 text-3xl font-bold text-slate-900 dark:text-white">{dashboardMetrics.failedCount}</p>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[1fr_1fr_1.1fr]">
<div className="rounded-[1.5rem] border border-slate-200 p-5 dark:border-slate-700">
<h3 className="text-base font-semibold text-slate-900 dark:text-white">{t('account.topToolsTitle')}</h3>
<div className="mt-4 space-y-3">
{dashboardMetrics.topTools.length === 0 ? (
<p className="text-sm text-slate-500 dark:text-slate-400">{t('account.historyEmpty')}</p>
) : (
dashboardMetrics.topTools.map(([tool, count]) => (
<div key={tool} className="flex items-center justify-between rounded-xl bg-slate-50 px-4 py-3 dark:bg-slate-800/70">
<span className="text-sm font-medium text-slate-800 dark:text-slate-100">{formatHistoryTool(tool, t)}</span>
<span className="text-sm font-semibold text-primary-600 dark:text-primary-400">{count}</span>
</div>
))
)}
</div>
</div>
<div className="rounded-[1.5rem] border border-slate-200 p-5 dark:border-slate-700">
<h3 className="text-base font-semibold text-slate-900 dark:text-white">{t('account.issuesTitle')}</h3>
<div className="mt-4 space-y-3">
{dashboardMetrics.recentFailures.length === 0 ? (
<p className="text-sm text-slate-500 dark:text-slate-400">{t('account.issuesEmpty')}</p>
) : (
dashboardMetrics.recentFailures.map((item) => (
<div key={item.id} className="rounded-xl bg-red-50 px-4 py-3 dark:bg-red-950/30">
<div className="flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-4 w-4 text-red-500" />
<div>
<p className="text-sm font-semibold text-red-800 dark:text-red-300">{formatHistoryTool(item.tool, t)}</p>
<p className="mt-1 text-xs text-red-700 dark:text-red-400">{typeof item.metadata?.error === 'string' ? item.metadata.error : t('account.statusFailed')}</p>
</div>
</div>
</div>
))
)}
</div>
</div>
<div className="rounded-[1.5rem] border border-slate-200 p-5 dark:border-slate-700">
<h3 className="text-base font-semibold text-slate-900 dark:text-white">{t('account.onboardingTitle')}</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{t('account.onboardingSubtitle')}</p>
<div className="mt-4 space-y-3">
{dashboardMetrics.onboardingItems.map((item) => (
<div key={item.key} className="rounded-xl bg-slate-50 px-4 py-3 dark:bg-slate-800/70">
<div className="flex items-start gap-3">
<span className={`mt-0.5 inline-flex h-5 w-5 items-center justify-center rounded-full ${item.done ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' : 'bg-slate-200 text-slate-600 dark:bg-slate-700 dark:text-slate-300'}`}>
{item.done ? <Check className="h-3.5 w-3.5" /> : <span className="text-[10px] font-bold"></span>}
</span>
<div>
<p className="text-sm font-semibold text-slate-900 dark:text-white">{item.title}</p>
<p className="mt-1 text-xs leading-5 text-slate-500 dark:text-slate-400">{item.description}</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
{/* API Key Management — Pro only */}
{user.plan === 'pro' && (
<section className="card rounded-[2rem] p-0">
<div className="border-b border-slate-200 px-6 py-5 dark:border-slate-700">
<div className="flex items-center gap-3">
<KeyRound className="h-5 w-5 text-primary-600 dark:text-primary-400" />
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('account.apiKeysTitle')}
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
{t('account.apiKeysSubtitle')}
</p>
</div>
</div>
</div>
<div className="space-y-4 p-6">
{/* Create key form */}
<form onSubmit={handleCreateApiKey} className="flex gap-2">
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder={t('account.apiKeyNamePlaceholder')}
maxLength={100}
className="input flex-1"
/>
<button type="submit" className="btn-primary" disabled={newKeyCreating || !newKeyName.trim()}>
{newKeyCreating ? '…' : t('account.apiKeyCreate')}
</button>
</form>
{newKeyError && (
<p className="text-sm text-red-600 dark:text-red-400">{newKeyError}</p>
)}
{/* Revealed key — shown once after creation */}
{revealedKey && (
<div className="flex items-center gap-3 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 dark:border-emerald-800/60 dark:bg-emerald-950/30">
<code className="flex-1 break-all font-mono text-xs text-emerald-800 dark:text-emerald-200">
{revealedKey}
</code>
<button type="button" onClick={handleCopyKey} className="shrink-0 text-emerald-700 dark:text-emerald-300">
{copiedKey ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</button>
<button type="button" onClick={() => setRevealedKey(null)} className="shrink-0 text-slate-400 hover:text-slate-600">
×
</button>
</div>
)}
{revealedKey && (
<p className="text-xs text-amber-600 dark:text-amber-400">{t('account.apiKeyCopyWarning')}</p>
)}
{/* Key list */}
{apiKeysLoading ? (
<p className="text-sm text-slate-500">{t('account.historyLoading')}</p>
) : apiKeys.length === 0 ? (
<p className="text-sm text-slate-500 dark:text-slate-400">{t('account.apiKeysEmpty')}</p>
) : (
<ul className="space-y-2">
{apiKeys.map((key) => (
<li
key={key.id}
className={`flex items-center justify-between rounded-xl border px-4 py-3 ${
key.revoked_at
? 'border-slate-200 bg-slate-50 opacity-50 dark:border-slate-700 dark:bg-slate-900/40'
: 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900/70'
}`}
>
<div className="space-y-0.5">
<p className="text-sm font-semibold text-slate-900 dark:text-white">{key.name}</p>
<p className="font-mono text-xs text-slate-400">{key.key_prefix}</p>
{key.revoked_at && (
<p className="text-xs text-red-500">{t('account.apiKeyRevoked')}</p>
)}
</div>
{!key.revoked_at && (
<button
type="button"
onClick={() => handleRevokeApiKey(key.id)}
className="ml-4 text-slate-400 hover:text-red-500 dark:hover:text-red-400"
title={t('account.apiKeyRevoke')}
>
<Trash2 className="h-4 w-4" />
</button>
)}
</li>
))}
</ul>
)}
</div>
</section>
)}
<section id="history" className="card rounded-[2rem] p-0">
<div className="border-b border-slate-200 px-6 py-5 dark:border-slate-700">
<div className="flex items-center gap-3">
<FolderClock className="h-5 w-5 text-primary-600 dark:text-primary-400" />
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('account.historyTitle')}
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
{t('account.historySubtitle')}
</p>
</div>
</div>
</div>
<div className="space-y-4 p-6">
{historyLoading ? (
<p className="text-sm text-slate-500 dark:text-slate-400">{t('account.historyLoading')}</p>
) : historyError ? (
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-300">
{historyError}
</div>
) : historyItems.length === 0 ? (
<div className="rounded-[1.5rem] border border-dashed border-slate-300 bg-slate-50 px-6 py-10 text-center dark:border-slate-700 dark:bg-slate-900/60">
<p className="text-base font-medium text-slate-700 dark:text-slate-200">{t('account.historyEmpty')}</p>
</div>
) : (
historyItems.map((item) => {
const metadataError =
typeof item.metadata?.error === 'string' ? item.metadata.error : null;
return (
<article
key={item.id}
className="rounded-[1.5rem] border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-700 dark:bg-slate-900/70"
>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">
{formatHistoryTool(item.tool, t)}
</p>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">
{item.output_filename || item.original_filename || formatHistoryTool(item.tool, t)}
</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">
{t('account.createdAt')}: {dateFormatter.format(new Date(item.created_at))}
</p>
</div>
<span
className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold ${
item.status === 'completed'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
}`}
>
{item.status === 'completed'
? t('account.statusCompleted')
: t('account.statusFailed')}
</span>
</div>
<div className="mt-4 grid gap-3 text-sm text-slate-600 dark:text-slate-300 sm:grid-cols-2">
<div className="rounded-xl bg-slate-50 px-4 py-3 dark:bg-slate-800/80">
<p className="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">
{t('account.originalFile')}
</p>
<p className="mt-1 break-all font-medium text-slate-800 dark:text-slate-100">
{item.original_filename || '—'}
</p>
</div>
<div className="rounded-xl bg-slate-50 px-4 py-3 dark:bg-slate-800/80">
<p className="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">
{t('account.outputFile')}
</p>
<p className="mt-1 break-all font-medium text-slate-800 dark:text-slate-100">
{item.output_filename || '—'}
</p>
</div>
</div>
{metadataError ? (
<p className="mt-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700 dark:bg-red-950/40 dark:text-red-300">
{metadataError}
</p>
) : null}
{item.download_url && item.status === 'completed' ? (
<a href={item.download_url} className="btn-primary mt-4 inline-flex">
<Download className="h-4 w-4" />
{t('account.downloadResult')}
</a>
) : null}
</article>
);
})
)}
</div>
</section>
</div>
) : (
<div className="grid gap-8 lg:grid-cols-[1.1fr_0.9fr]">
<section className="overflow-hidden rounded-[2rem] bg-gradient-to-br from-cyan-100 via-white to-amber-50 p-8 shadow-sm ring-1 ring-cyan-200 dark:from-cyan-950/50 dark:via-slate-950 dark:to-amber-950/30 dark:ring-cyan-900/40">
<div className="max-w-xl space-y-5">
<div className="inline-flex items-center gap-2 rounded-full bg-white/80 px-4 py-2 text-sm font-semibold text-cyan-900 ring-1 ring-cyan-200 dark:bg-cyan-400/10 dark:text-cyan-200 dark:ring-cyan-700/40">
<ShieldCheck className="h-4 w-4" />
{t('account.benefitsTitle')}
</div>
<h1 className="text-3xl font-black tracking-tight text-slate-900 dark:text-white sm:text-4xl">
{t('account.heroTitle')}
</h1>
<p className="text-base leading-7 text-slate-600 dark:text-slate-300">
{t('account.heroSubtitle')}
</p>
</div>
<div className="mt-8 grid gap-4">
{[t('account.benefit1'), t('account.benefit2'), t('account.benefit3')].map((benefit) => (
<div
key={benefit}
className="flex items-start gap-3 rounded-[1.25rem] bg-white/80 px-4 py-4 shadow-sm ring-1 ring-white dark:bg-slate-900/80 dark:ring-slate-800"
>
<KeyRound className="mt-0.5 h-5 w-5 text-primary-600 dark:text-primary-400" />
<p className="text-sm font-medium leading-6 text-slate-700 dark:text-slate-200">{benefit}</p>
</div>
))}
</div>
</section>
<section className="overflow-hidden rounded-[2rem] bg-white shadow-sm ring-1 ring-slate-200 dark:bg-slate-900 dark:ring-slate-800">
<div className="grid grid-cols-2 border-b border-slate-200 dark:border-slate-800">
<button
type="button"
onClick={() => {
setMode('login');
setSubmitError(null);
}}
className={`px-5 py-4 text-sm font-semibold transition-colors ${
mode === 'login'
? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900'
: 'text-slate-500 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/70'
}`}
>
{t('common.signIn')}
</button>
<button
type="button"
onClick={() => {
setMode('register');
setSubmitError(null);
}}
className={`px-5 py-4 text-sm font-semibold transition-colors ${
mode === 'register'
? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900'
: 'text-slate-500 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/70'
}`}
>
{t('account.createAccount')}
</button>
</div>
<div className="p-6 sm:p-8">
<div className="mb-6">
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">
{mode === 'login' ? t('account.signInTitle') : t('account.registerTitle')}
</h2>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
{t('account.formSubtitle')}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<label className="block">
<span className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-200">
{t('common.email')}
</span>
<input
type="email"
required
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder={t('account.emailPlaceholder')}
className="input-field"
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-200">
{t('common.password')}
</span>
<input
type="password"
required
minLength={8}
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder={t('account.passwordPlaceholder')}
className="input-field"
/>
</label>
{mode === 'register' ? (
<label className="block">
<span className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-200">
{t('account.confirmPassword')}
</span>
<input
type="password"
required
minLength={8}
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
placeholder={t('account.confirmPasswordPlaceholder')}
className="input-field"
/>
</label>
) : null}
{submitError ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-300">
{submitError}
</div>
) : null}
<button type="submit" className="btn-primary w-full" disabled={authLoading}>
{mode === 'login' ? t('account.submitLogin') : t('account.submitRegister')}
</button>
{mode === 'login' && (
<p className="text-center text-sm">
<a href="/forgot-password" className="text-primary-600 hover:underline dark:text-primary-400">
{t('auth.forgotPassword.link')}
</a>
</p>
)}
</form>
</div>
</section>
</div>
)}
</>
);
}