296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useTransition } from "react";
|
||
import { Globe, ChevronDown, Save, Loader2, CheckCircle2, Plus, X } from "lucide-react";
|
||
|
||
interface Localization {
|
||
id: string;
|
||
attributes: {
|
||
locale: string;
|
||
description: string;
|
||
keywords: string;
|
||
whatsNew: string;
|
||
promotionalText: string;
|
||
marketingUrl: string;
|
||
supportUrl: string;
|
||
};
|
||
}
|
||
|
||
const LOCALE_LABELS: Record<string, string> = {
|
||
"en-US": "English (US)",
|
||
"tr-TR": "Türkçe",
|
||
"de-DE": "Deutsch",
|
||
"fr-FR": "Français",
|
||
"es-ES": "Español",
|
||
"it-IT": "Italiano",
|
||
"pt-PT": "Português (PT)",
|
||
"pt-BR": "Português (BR)",
|
||
"ja": "日本語",
|
||
"ko": "한국어",
|
||
"zh-Hans": "简体中文",
|
||
"zh-Hant": "繁體中文",
|
||
"ar-SA": "العربية",
|
||
"ru": "Русский",
|
||
"nl-NL": "Nederlands",
|
||
"sv-SE": "Svenska",
|
||
"pl-PL": "Polski",
|
||
"da": "Dansk",
|
||
"fi": "Suomi",
|
||
"nb": "Norsk",
|
||
"el": "Ελληνικά",
|
||
"cs": "Čeština",
|
||
"hu": "Magyar",
|
||
"ro": "Română",
|
||
"sk": "Slovenčina",
|
||
"uk": "Українська",
|
||
"hr": "Hrvatski",
|
||
"id": "Bahasa Indonesia",
|
||
"ms": "Bahasa Malaysia",
|
||
"th": "ภาษาไทย",
|
||
"vi": "Tiếng Việt",
|
||
"hi": "हिन्दी",
|
||
};
|
||
|
||
const FIELDS: {
|
||
key: keyof Localization["attributes"];
|
||
label: string;
|
||
multiline?: boolean;
|
||
maxLen?: number;
|
||
}[] = [
|
||
{ key: "description", label: "Açıklama", multiline: true, maxLen: 4000 },
|
||
{ key: "keywords", label: "Anahtar Kelimeler", maxLen: 100 },
|
||
{ key: "whatsNew", label: "Bu Sürümdeki Yenilikler", multiline: true, maxLen: 4000 },
|
||
{ key: "promotionalText", label: "Tanıtım Metni", multiline: true, maxLen: 170 },
|
||
{ key: "marketingUrl", label: "Pazarlama URL" },
|
||
{ key: "supportUrl", label: "Destek URL" },
|
||
];
|
||
|
||
export default function LocalizationEditor({
|
||
versionId,
|
||
versionString,
|
||
}: {
|
||
versionId: string;
|
||
versionString: string;
|
||
}) {
|
||
const [localizations, setLocalizations] = useState<Localization[]>([]);
|
||
const [selectedLocale, setSelectedLocale] = useState<string>("");
|
||
const [form, setForm] = useState<Partial<Localization["attributes"]>>({});
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, startSaveTransition] = useTransition();
|
||
const [adding, startAddTransition] = useTransition();
|
||
const [saved, setSaved] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [showAddLocale, setShowAddLocale] = useState(false);
|
||
|
||
function fetchLocalizations(selectId?: string) {
|
||
setLoading(true);
|
||
fetch(`/api/asc/localizations?versionId=${versionId}`)
|
||
.then((r) => r.json())
|
||
.then((d) => {
|
||
const locs: Localization[] = d.data ?? [];
|
||
setLocalizations(locs);
|
||
if (locs.length > 0) {
|
||
const target = selectId ? locs.find((l) => l.id === selectId) ?? locs[0] : locs[0];
|
||
setSelectedLocale(target.id);
|
||
setForm(target.attributes);
|
||
}
|
||
})
|
||
.finally(() => setLoading(false));
|
||
}
|
||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
useEffect(() => { fetchLocalizations(); }, [versionId]);
|
||
|
||
function selectLocale(id: string) {
|
||
const loc = localizations.find((l) => l.id === id);
|
||
if (!loc) return;
|
||
setSelectedLocale(id);
|
||
setForm(loc.attributes);
|
||
setSaved(false);
|
||
setError(null);
|
||
}
|
||
|
||
function handleChange(key: keyof Localization["attributes"], value: string) {
|
||
setForm((prev) => ({ ...prev, [key]: value }));
|
||
setSaved(false);
|
||
}
|
||
|
||
function handleSave() {
|
||
setError(null);
|
||
startSaveTransition(async () => {
|
||
const res = await fetch(`/api/asc/localizations`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ localizationId: selectedLocale, payload: form }),
|
||
});
|
||
if (res.ok) {
|
||
setSaved(true);
|
||
setTimeout(() => setSaved(false), 3000);
|
||
} else {
|
||
const d = await res.json();
|
||
setError(d.error ?? "Kaydedilemedi");
|
||
}
|
||
});
|
||
}
|
||
|
||
function handleAddLocale(localeCode: string) {
|
||
setShowAddLocale(false);
|
||
setError(null);
|
||
startAddTransition(async () => {
|
||
const res = await fetch(`/api/asc/localizations`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ versionId, locale: localeCode }),
|
||
});
|
||
const d = await res.json();
|
||
if (res.ok) {
|
||
fetchLocalizations(d.data?.id);
|
||
} else {
|
||
setError(d.error ?? "Dil eklenemedi");
|
||
}
|
||
});
|
||
}
|
||
|
||
const existingCodes = new Set(localizations.map((l) => l.attributes.locale));
|
||
const availableToAdd = Object.entries(LOCALE_LABELS).filter(([code]) => !existingCodes.has(code));
|
||
|
||
return (
|
||
<section className="rounded-2xl border border-white/5 bg-white/[0.03] overflow-hidden">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/5">
|
||
<h2 className="font-bold text-sm flex items-center gap-2">
|
||
<Globe size={15} className="text-emerald-400" />
|
||
Metadata Düzenle
|
||
<span className="text-[10px] text-slate-500 font-normal">v{versionString}</span>
|
||
</h2>
|
||
|
||
<div className="flex items-center gap-2">
|
||
{/* Locale selector */}
|
||
{localizations.length > 0 && (
|
||
<div className="relative">
|
||
<select
|
||
value={selectedLocale}
|
||
onChange={(e) => selectLocale(e.target.value)}
|
||
className="appearance-none bg-white/5 border border-white/10 rounded-lg text-xs font-semibold pl-3 pr-8 py-2 text-slate-300 focus:outline-none focus:border-violet-500 cursor-pointer"
|
||
>
|
||
{localizations.map((l) => (
|
||
<option key={l.id} value={l.id}>
|
||
{LOCALE_LABELS[l.attributes.locale] ?? l.attributes.locale}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<ChevronDown size={12} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||
</div>
|
||
)}
|
||
|
||
{/* Add locale */}
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setShowAddLocale((v) => !v)}
|
||
disabled={adding}
|
||
className="flex items-center gap-1 px-2.5 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-semibold text-slate-300 hover:text-white transition-colors disabled:opacity-50"
|
||
>
|
||
{adding ? <Loader2 size={12} className="animate-spin" /> : <Plus size={12} />}
|
||
Dil Ekle
|
||
</button>
|
||
|
||
{showAddLocale && (
|
||
<div className="absolute right-0 top-full mt-1 z-50 w-52 rounded-xl border border-white/10 bg-[#1a1a2e] shadow-2xl overflow-hidden">
|
||
<div className="flex items-center justify-between px-3 py-2 border-b border-white/5">
|
||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Dil Seç</span>
|
||
<button onClick={() => setShowAddLocale(false)} className="text-slate-500 hover:text-slate-300">
|
||
<X size={12} />
|
||
</button>
|
||
</div>
|
||
<div className="max-h-60 overflow-y-auto">
|
||
{availableToAdd.length === 0 ? (
|
||
<p className="text-xs text-slate-500 px-3 py-3">Tüm diller eklenmiş.</p>
|
||
) : (
|
||
availableToAdd.map(([code, label]) => (
|
||
<button
|
||
key={code}
|
||
onClick={() => handleAddLocale(code)}
|
||
className="w-full text-left px-3 py-2 text-xs text-slate-300 hover:bg-white/5 hover:text-white transition-colors flex items-center justify-between group"
|
||
>
|
||
<span>{label}</span>
|
||
<span className="text-[10px] text-slate-600 group-hover:text-slate-400">{code}</span>
|
||
</button>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
{loading ? (
|
||
<div className="p-8 flex items-center justify-center">
|
||
<Loader2 size={20} className="animate-spin text-slate-500" />
|
||
</div>
|
||
) : localizations.length === 0 ? (
|
||
<div className="p-6 text-center text-slate-500 text-sm">
|
||
Bu sürüm için lokalizasyon verisi bulunamadı.
|
||
</div>
|
||
) : (
|
||
<div className="p-6 space-y-5">
|
||
{FIELDS.map((f) => (
|
||
<div key={f.key} className="space-y-1.5">
|
||
<div className="flex items-center justify-between">
|
||
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-400">
|
||
{f.label}
|
||
</label>
|
||
{f.maxLen && (
|
||
<span
|
||
className={`text-[10px] font-mono ${
|
||
((form[f.key] as string)?.length ?? 0) > f.maxLen
|
||
? "text-red-400"
|
||
: "text-slate-600"
|
||
}`}
|
||
>
|
||
{(form[f.key] as string)?.length ?? 0}/{f.maxLen}
|
||
</span>
|
||
)}
|
||
</div>
|
||
{f.multiline ? (
|
||
<textarea
|
||
value={(form[f.key] as string) ?? ""}
|
||
onChange={(e) => handleChange(f.key, e.target.value)}
|
||
rows={f.key === "description" ? 6 : 3}
|
||
className="w-full bg-black/30 border border-white/8 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-violet-500/50 resize-none font-mono leading-relaxed"
|
||
/>
|
||
) : (
|
||
<input
|
||
type="text"
|
||
value={(form[f.key] as string) ?? ""}
|
||
onChange={(e) => handleChange(f.key, e.target.value)}
|
||
className="w-full bg-black/30 border border-white/8 rounded-xl px-4 py-2.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-violet-500/50"
|
||
/>
|
||
)}
|
||
</div>
|
||
))}
|
||
|
||
{error && (
|
||
<p className="text-xs text-red-400 bg-red-400/10 rounded-lg px-4 py-2">{error}</p>
|
||
)}
|
||
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={saving}
|
||
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-violet-600 hover:bg-violet-500 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-bold transition-colors"
|
||
>
|
||
{saving ? (
|
||
<><Loader2 size={14} className="animate-spin" /> Kaydediliyor…</>
|
||
) : saved ? (
|
||
<><CheckCircle2 size={14} className="text-emerald-300" /> Kaydedildi</>
|
||
) : (
|
||
<><Save size={14} /> Kaydet</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|