profit-planet-frontend/src/app/admin/language-management/components/ScanResultsModal.tsx
DeathKaioken e19164a471 Cringe
Co-authored-by: Copilot <copilot@github.com>
2026-05-03 23:46:38 +02:00

348 lines
17 KiB
TypeScript

'use client';
import ScanFixPanel from './ScanFixPanel';
import { useTranslation } from '../../../i18n/useTranslation';
import type { WorkspaceScanResult } from '../hooks/useI18nScanWorkflow';
import { useModalAnimation } from '../hooks/useModalAnimation';
type LanguageEntry = {
code: string;
name: string;
};
type NamespaceScanResult = {
ns: string;
total: number;
translated: number;
missing: number;
};
type Props = {
isOpen: boolean;
onClose: () => void;
lastScanTime: Date | null;
workspaceScan: WorkspaceScanResult | null;
totalKeys: number;
namespacesCount: number;
allLanguages: LanguageEntry[];
activeLang: string;
scanResults: NamespaceScanResult[];
scanError: string | null;
isScanning: boolean;
isAutoFixing: boolean;
fixableFiles: string[];
selectedFiles: string[];
forceConvertToClient: boolean;
onToggleFile: (file: string) => void;
onSelectAll: () => void;
onClear: () => void;
onToggleForceConvertToClient: () => void;
onRunFixSelected: () => void;
};
export default function ScanResultsModal({
isOpen,
onClose,
lastScanTime,
workspaceScan,
totalKeys,
namespacesCount,
allLanguages,
activeLang,
scanResults,
scanError,
isScanning,
isAutoFixing,
fixableFiles,
selectedFiles,
forceConvertToClient,
onToggleFile,
onSelectAll,
onClear,
onToggleForceConvertToClient,
onRunFixSelected,
}: Props) {
const { t } = useTranslation();
const { isRendered, isVisible } = useModalAnimation(isOpen);
if (!isRendered) return null;
const totalTranslated = scanResults.reduce((sum, row) => sum + row.translated, 0);
const coveragePercent = totalKeys === 0
? 100
: Math.round((totalTranslated / totalKeys) * 100);
return (
<div className={`fixed inset-0 z-[9999] flex items-start justify-center pt-14 pb-6 bg-black/50 backdrop-blur-sm transition-opacity duration-200 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}>
<div className={`mx-3 w-full max-w-[1400px] rounded-3xl border border-slate-200 bg-white shadow-2xl flex flex-col max-h-[calc(100vh-5rem)] overflow-hidden transform transition-all duration-200 ${
isVisible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-2 scale-[0.98]'
}`}>
<div className="px-8 pt-6 pb-5 border-b border-slate-200 bg-gradient-to-b from-slate-50 to-white">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-[#1C2B4A]">{t('autofix.kb2217bdf')}</h2>
<p className="text-sm text-slate-600 mt-1">
{workspaceScan
? `${workspaceScan.scannedFiles} files across ${workspaceScan.scannedDirectories} directories scanned`
: `${totalKeys} keys across ${namespacesCount} namespaces`}
{lastScanTime && (
<span className="ml-2 text-xs text-slate-500">
· Scanned {lastScanTime.toLocaleTimeString()}
</span>
)}
</p>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-700 ml-4 text-xl leading-none"
>
</button>
</div>
<div className="mt-4">
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="font-medium">Overall coverage ({allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang})</span>
<span>
{activeLang === 'en'
? `${totalKeys} / ${totalKeys}`
: `${totalTranslated} / ${totalKeys}`
}
</span>
</div>
<div className="h-2.5 rounded-full bg-slate-100 overflow-hidden">
<div
className="h-full rounded-full bg-[#1C2B4A] transition-all"
style={{ width: activeLang === 'en' ? '100%' : `${coveragePercent}%` }}
/>
</div>
</div>
{workspaceScan && (
<div className="mt-5 grid grid-cols-2 lg:grid-cols-6 gap-2.5">
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-slate-500">{t('autofix.k91052e3f')}</p>
<p className="text-sm font-semibold text-[#1C2B4A]">{workspaceScan.translationCallCount}</p>
</div>
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-slate-500">{t('autofix.k90a6e795')}</p>
<p className="text-sm font-semibold text-[#1C2B4A]">{workspaceScan.uniqueKeyCount}</p>
</div>
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-red-600">{t('autofix.k8cf40180')}</p>
<p className="text-sm font-semibold text-red-600">{workspaceScan.missingKeys.length}</p>
</div>
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-amber-700">{t('autofix.k1bf4ffa4')}</p>
<p className="text-sm font-semibold text-amber-700">{workspaceScan.untranslatedLiterals.length}</p>
</div>
<div className="rounded-xl border border-green-200 bg-green-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-green-700">{t('autofix.k9b173204')}</p>
<p className="text-sm font-semibold text-green-700">{workspaceScan.changedFileCount ?? 0}</p>
</div>
<div className="rounded-xl border border-indigo-200 bg-indigo-50 px-3 py-2.5">
<p className="text-[11px] uppercase tracking-wide text-indigo-700">{t('autofix.k60874ea3')}</p>
<p className="text-sm font-semibold text-indigo-700">{workspaceScan.createdKeyCount ?? 0}</p>
</div>
</div>
)}
</div>
<div className="overflow-y-auto flex-1 px-8 py-5">
{scanError && (
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{scanError}
</div>
)}
{isScanning && (
<div className="mb-4 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-700">{t('autofix.k0d9c63c5')}</div>
)}
{isAutoFixing && (
<div className="mb-4 rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-2 text-sm text-indigo-700">{t('autofix.ka802064d')}</div>
)}
<div className="grid grid-cols-1 xl:grid-cols-12 gap-5 items-start">
<div className="xl:col-span-5 space-y-4">
<ScanFixPanel
fixableFiles={fixableFiles}
selectedFiles={selectedFiles}
isAutoFixing={isAutoFixing}
forceConvertToClient={forceConvertToClient}
onToggleFile={onToggleFile}
onSelectAll={onSelectAll}
onClear={onClear}
onToggleForceConvertToClient={onToggleForceConvertToClient}
onRunFixSelected={onRunFixSelected}
/>
{workspaceScan && (workspaceScan.changedFileCount ?? 0) > 0 && (
<div className="rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">
Auto-fix updated {(workspaceScan.changedFileCount ?? 0)} files and created {(workspaceScan.createdKeyCount ?? 0)} translation keys.
</div>
)}
{workspaceScan && Array.isArray(workspaceScan.changedFiles) && workspaceScan.changedFiles.length > 0 && (
<div className="rounded-xl border border-green-200 bg-green-50/50 p-4">
<h3 className="text-sm font-semibold text-green-700 mb-2">{t('autofix.k5ad4d864')}</h3>
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{workspaceScan.changedFiles.map((entry) => (
<div key={entry.file} className="rounded-md border border-green-100 bg-white px-3 py-2">
<p className="font-mono text-xs text-green-800">{entry.file}</p>
<p className="text-[11px] text-gray-600 mt-1">
{entry.replacements} replacements
{entry.addedImport ? ' · import added' : ''}
{entry.addedHook ? ' · hook added' : ''}
</p>
</div>
))}
</div>
</div>
)}
{workspaceScan && Array.isArray(workspaceScan.skippedFiles) && workspaceScan.skippedFiles.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
<h3 className="text-sm font-semibold text-gray-700 mb-2">{t('autofix.k56a52520')}</h3>
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{workspaceScan.skippedFiles.map((entry) => (
<div key={entry.file} className="rounded-md border border-gray-100 bg-white px-3 py-2">
<p className="font-mono text-xs text-gray-700">{entry.file}</p>
<p className="text-[11px] text-gray-500 mt-1">{entry.reason}</p>
</div>
))}
</div>
</div>
)}
{workspaceScan && Array.isArray(workspaceScan.autoFixDebug) && workspaceScan.autoFixDebug.length > 0 && (
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
<h3 className="text-sm font-semibold text-slate-700 mb-2">{t('autofix.kc8034db6')}</h3>
<p className="text-xs text-slate-600 mb-3">{t('autofix.k9bd0812b')}</p>
<div className="space-y-2 max-h-64 overflow-y-auto pr-1">
{workspaceScan.autoFixDebug.slice(0, 200).map((entry) => (
<div key={`${entry.file}-${entry.status}-${entry.beforeLiteralCount}-${entry.afterLiteralCount}`} className="rounded-md border border-slate-200 bg-white px-3 py-2">
<div className="flex items-center justify-between gap-2">
<p className="font-mono text-xs text-slate-800 break-all">{entry.file}</p>
<span className={`text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-full ${
entry.status === 'changed'
? 'bg-green-100 text-green-700'
: entry.status === 'skipped'
? 'bg-amber-100 text-amber-700'
: 'bg-slate-100 text-slate-700'
}`}>
{entry.status}
</span>
</div>
<p className="text-[11px] text-slate-600 mt-1">
before: {entry.beforeLiteralCount} · text: {entry.textReplacements} · attr: {entry.attrReplacements} · after: {entry.afterLiteralCount}
</p>
{entry.reason && <p className="text-[11px] text-slate-500 mt-1">{entry.reason}</p>}
</div>
))}
</div>
</div>
)}
</div>
<div className="xl:col-span-7 space-y-4">
<div className="rounded-xl border border-slate-200 bg-slate-50/40 p-4">
<h3 className="text-sm font-semibold text-[#1C2B4A] mb-3">{t('autofix.kbe30c353')}</h3>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100">
<th className="pb-2 text-left font-medium text-gray-500">Namespace</th>
<th className="pb-2 text-right font-medium text-gray-500">Keys</th>
<th className="pb-2 text-right font-medium text-gray-500">Translated</th>
<th className="pb-2 text-right font-medium text-gray-500">Missing</th>
<th className="pb-2 text-left pl-4 font-medium text-gray-500">Coverage</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{scanResults.map(({ ns, total, translated, missing }) => {
const pct = total === 0 ? 100 : Math.round((translated / total) * 100);
return (
<tr key={ns} className="hover:bg-gray-50">
<td className="py-2 font-mono text-xs text-[#1C2B4A]">{ns}</td>
<td className="py-2 text-right text-gray-500">{total}</td>
<td className="py-2 text-right text-green-600 font-medium">{translated}</td>
<td className={`py-2 text-right font-medium ${missing > 0 ? 'text-red-500' : 'text-gray-400'}`}>
{missing}
</td>
<td className="py-2 pl-4">
<div className="flex items-center gap-2">
<div className="w-24 h-1.5 rounded-full bg-gray-100 overflow-hidden">
<div
className={`h-full rounded-full ${
pct === 100 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-400' : 'bg-red-400'
}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-xs text-gray-500">{pct}%</span>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{workspaceScan && workspaceScan.missingKeys.length > 0 && (
<div className="rounded-xl border border-red-200 bg-red-50/50 p-4">
<h3 className="text-sm font-semibold text-red-700 mb-2">{t('autofix.kae63e46a')}</h3>
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{workspaceScan.missingKeys.map((entry) => (
<div key={entry.key} className="rounded-md border border-red-100 bg-white px-3 py-2">
<p className="font-mono text-xs text-red-700">{entry.key}</p>
<p className="text-[11px] text-gray-500 mt-1">
{entry.files.slice(0, 3).join(', ')}
{entry.files.length > 3 ? ` (+${entry.files.length - 3} more)` : ''}
</p>
</div>
))}
</div>
</div>
)}
{workspaceScan && workspaceScan.untranslatedLiterals.length > 0 && (
<div className="rounded-xl border border-amber-200 bg-amber-50/50 p-4">
<h3 className="text-sm font-semibold text-amber-700 mb-2">{t('autofix.k14eb468b')}</h3>
<p className="text-xs text-amber-700/80 mb-3">
These literals appear directly in JSX. Replace them with t('...') to make the page translatable.
</p>
<div className="space-y-2 max-h-56 overflow-y-auto pr-1">
{workspaceScan.untranslatedLiterals.slice(0, 80).map((entry) => (
<div key={entry.text} className="rounded-md border border-amber-100 bg-white px-3 py-2">
<p className="text-xs font-medium text-amber-800">{entry.text}</p>
<p className="text-[11px] text-gray-500 mt-1">
{entry.files.slice(0, 3).join(', ')}
{entry.files.length > 3 ? ` (+${entry.files.length - 3} more)` : ''}
</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
<div className="px-8 py-4 border-t border-slate-200 flex justify-between items-center bg-white">
<p className="text-xs text-slate-500">
Scan now checks workspace files (pages, components, hooks, utils) and compares used keys against en.ts.
</p>
<button
onClick={onClose}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
>
Close
</button>
</div>
</div>
</div>
);
}