profit-planet-frontend/src/app/admin/language-management/components/LanguageManagementTopSection.tsx
DeathKaioken e769132f84 fml
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 04:52:11 +02:00

243 lines
11 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.

'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>
)}
</>
);
}