Files
app-admin/app/apps/[id]/store/LocalizationEditor.tsx
2026-03-24 15:46:27 +03:00

296 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 { 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>
);
}