243 lines
11 KiB
TypeScript
243 lines
11 KiB
TypeScript
'use client';
|
||
|
||
import type { RefObject } from 'react';
|
||
import { useMemo } from 'react';
|
||
import { useTranslation } from '../../../i18n/useTranslation';
|
||
|
||
type LanguageEntry = {
|
||
code: string;
|
||
name: string;
|
||
};
|
||
|
||
type Props = {
|
||
headerRef: RefObject<HTMLDivElement | null>;
|
||
totalKeys: number;
|
||
onScan: () => void;
|
||
isScanning: boolean;
|
||
isAutoFixing: boolean;
|
||
onBackToAdmin: () => void;
|
||
isDirty: boolean;
|
||
onSave: () => void;
|
||
saved: boolean;
|
||
saveError: string;
|
||
allLanguages: LanguageEntry[];
|
||
activeLang: string;
|
||
setActiveLang: (code: string) => void;
|
||
isBuiltin: (code: string) => boolean;
|
||
onDeleteLanguageRequest: (code: string) => void;
|
||
onOpenAddLanguage: () => void;
|
||
allTabStats: { total: number; translated: number; missing: number };
|
||
translationProgressPercent: number;
|
||
wizardMissingKeysCount: number;
|
||
onOpenTranslationWizard: () => void;
|
||
};
|
||
|
||
export default function LanguageManagementTopSection({
|
||
headerRef,
|
||
totalKeys,
|
||
onScan,
|
||
isScanning,
|
||
isAutoFixing,
|
||
onBackToAdmin,
|
||
isDirty,
|
||
onSave,
|
||
saved,
|
||
saveError,
|
||
allLanguages,
|
||
activeLang,
|
||
setActiveLang,
|
||
isBuiltin,
|
||
onDeleteLanguageRequest,
|
||
onOpenAddLanguage,
|
||
allTabStats,
|
||
translationProgressPercent,
|
||
wizardMissingKeysCount,
|
||
onOpenTranslationWizard,
|
||
}: Props) {
|
||
const { t } = useTranslation();
|
||
|
||
const prioritizedLanguages = useMemo(() => {
|
||
const byCode = new Map(allLanguages.map((lang) => [lang.code, lang]));
|
||
const english = byCode.get('en');
|
||
const german = byCode.get('de');
|
||
const rest = allLanguages
|
||
.filter((lang) => lang.code !== 'en' && lang.code !== 'de')
|
||
.sort((a, b) => a.name.localeCompare(b.name));
|
||
return [english, german, ...rest].filter((lang): lang is LanguageEntry => Boolean(lang));
|
||
}, [allLanguages]);
|
||
|
||
const englishLanguage = prioritizedLanguages.find((lang) => lang.code === 'en');
|
||
const germanLanguage = prioritizedLanguages.find((lang) => lang.code === 'de');
|
||
const otherLanguages = prioritizedLanguages.filter((lang) => lang.code !== 'en' && lang.code !== 'de');
|
||
|
||
const renderLanguageButton = (lang: LanguageEntry) => (
|
||
<button
|
||
key={lang.code}
|
||
onClick={() => setActiveLang(lang.code)}
|
||
className={`flex shrink-0 items-center gap-2 px-4 py-2.5 rounded-2xl text-sm font-medium transition whitespace-nowrap ${
|
||
activeLang === lang.code
|
||
? 'bg-slate-900 text-white shadow-[0_18px_40px_-24px_rgba(15,23,42,0.85)]'
|
||
: 'bg-transparent text-slate-700 hover:bg-slate-100 hover:text-slate-900 border border-transparent hover:border-slate-200'
|
||
}`}
|
||
>
|
||
{lang.name}
|
||
<span className={`text-xs ${activeLang === lang.code ? 'opacity-50' : 'opacity-40'}`}>
|
||
{lang.code}
|
||
</span>
|
||
{!isBuiltin(lang.code) && (
|
||
<span
|
||
role="button"
|
||
tabIndex={0}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onDeleteLanguageRequest(lang.code);
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
onDeleteLanguageRequest(lang.code);
|
||
}
|
||
}}
|
||
title={t('autofix.k5fcc9b0e')}
|
||
className={`ml-0.5 inline-flex items-center justify-center rounded-full w-4 h-4 text-xs leading-none cursor-pointer transition ${
|
||
activeLang === lang.code
|
||
? 'bg-white/20 hover:bg-white/40 text-white'
|
||
: 'bg-slate-200 hover:bg-red-100 text-slate-500 hover:text-red-600'
|
||
}`}
|
||
>
|
||
×
|
||
</span>
|
||
)}
|
||
</button>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
{/* ── Hero header card ─────────────────────────────────── */}
|
||
<div
|
||
ref={headerRef}
|
||
className="rounded-[30px] border border-white/80 bg-white/85 px-5 py-6 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.38)] backdrop-blur md:px-8 md:py-8"
|
||
>
|
||
<div className="space-y-4">
|
||
<div className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-500">
|
||
{t('autofix.ka8c928ac')}
|
||
</div>
|
||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||
<div>
|
||
<h1 className="text-3xl font-black tracking-tight text-slate-950 md:text-4xl">
|
||
{t('autofix.k346a2c64')}
|
||
</h1>
|
||
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-600">
|
||
{t('autofix.k7227f13d')} {totalKeys} {t('autofix.k511d7fab')}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2 flex-wrap shrink-0">
|
||
<button
|
||
onClick={onScan}
|
||
disabled={isScanning || isAutoFixing}
|
||
className="flex items-center gap-2 rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 transition disabled:opacity-50"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||
</svg>
|
||
{isScanning ? t('autofix.kf191f6df5') : t('autofix.k9863fa5')}
|
||
</button>
|
||
<button
|
||
onClick={onBackToAdmin}
|
||
className="rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 transition"
|
||
>
|
||
{t('autofix.kea7cde7a')}
|
||
</button>
|
||
{saved && !isDirty && (
|
||
<span className="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-700">{t('autofix.kac6aab53')}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* stat pills */}
|
||
<div className="flex flex-wrap gap-2 text-xs text-slate-600">
|
||
<span className="inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 shadow-sm">
|
||
{allLanguages.length} {allLanguages.length === 1 ? t('autofix.k20eb1f87') : t('autofix.ka6cf3286')}
|
||
</span>
|
||
<span className="inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 shadow-sm">
|
||
{totalKeys} {t('autofix.k3931709b')}
|
||
</span>
|
||
{activeLang !== 'en' && (
|
||
<span className={`inline-flex items-center rounded-full border px-3 py-1.5 font-medium shadow-sm ${
|
||
allTabStats.missing > 0
|
||
? 'border-red-200 bg-red-50 text-red-700'
|
||
: 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
||
}`}>
|
||
{allTabStats.missing > 0 ? `${allTabStats.missing} ${t('autofix.k571ffd91')}` : t('autofix.kdcc78d97')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Save error banner ────────────────────────────────── */}
|
||
{saveError && (
|
||
<div className="rounded-2xl border border-red-200 bg-red-50/80 backdrop-blur px-5 py-3 text-sm text-red-700 shadow-sm">
|
||
{saveError}
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Language tabs card ───────────────────────────────── */}
|
||
<div className="rounded-[28px] border border-white/80 bg-white/85 px-4 py-3 shadow-[0_22px_60px_-34px_rgba(15,23,42,0.28)] backdrop-blur md:px-5">
|
||
<div className="flex items-center gap-1.5 flex-wrap">
|
||
{englishLanguage && renderLanguageButton(englishLanguage)}
|
||
{germanLanguage && renderLanguageButton(germanLanguage)}
|
||
{otherLanguages.map((lang) => renderLanguageButton(lang))}
|
||
<button
|
||
onClick={onOpenAddLanguage}
|
||
className="flex shrink-0 items-center gap-1.5 px-4 py-2.5 rounded-2xl text-sm font-medium text-slate-400 border border-dashed border-slate-300 hover:border-slate-400 hover:text-slate-600 transition whitespace-nowrap"
|
||
>
|
||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>{t('autofix.k7a515516')}</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Translation progress card ────────────────────────── */}
|
||
{activeLang !== 'en' && (
|
||
<div className="rounded-[28px] border border-white/80 bg-white/85 px-5 py-4 shadow-[0_22px_60px_-34px_rgba(15,23,42,0.28)] backdrop-blur flex items-center gap-5">
|
||
<div className="flex-1 space-y-1.5">
|
||
<div className="flex justify-between text-xs text-slate-500">
|
||
<span>{t('autofix.kb8f33873')}</span>
|
||
<span className="font-medium text-slate-700">{allTabStats.translated} / {allTabStats.total} {t('autofix.k33f55455')}</span>
|
||
</div>
|
||
<div className="h-2 rounded-full bg-slate-100 overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full bg-slate-900 transition-all duration-500"
|
||
style={{ width: `${translationProgressPercent}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<span className="text-2xl font-black tracking-tight text-slate-950 tabular-nums">
|
||
{translationProgressPercent}%
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Wizard nudge card ────────────────────────────────── */}
|
||
{activeLang !== 'en' && allTabStats.missing > 0 && wizardMissingKeysCount > 0 && (
|
||
<div className="rounded-[28px] border border-indigo-200/80 bg-indigo-50/80 backdrop-blur px-5 py-4 shadow-[0_22px_60px_-34px_rgba(99,102,241,0.3)] flex items-start justify-between gap-4">
|
||
<div>
|
||
<p className="text-sm font-semibold text-indigo-950">{t('autofix.k5e5e8744')}</p>
|
||
<p className="text-xs text-indigo-800/80 mt-1">
|
||
{allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang} still has{' '}
|
||
<span className="font-semibold">{wizardMissingKeysCount}</span>{t('autofix.k0a50d234')}</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onOpenTranslationWizard}
|
||
className="shrink-0 rounded-2xl bg-slate-900 text-white px-4 py-2 text-xs font-semibold shadow-[0_18px_40px_-24px_rgba(15,23,42,0.85)] hover:bg-slate-800 transition"
|
||
>
|
||
{t('autofix.k725dd1d6')}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
} |