first commit

This commit is contained in:
mstfyldz
2026-03-24 15:46:27 +03:00
parent 095d830279
commit 34b6a46604
33 changed files with 5212 additions and 81 deletions

View File

@@ -0,0 +1,295 @@
"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>
);
}