stepstead-web/assets/index.html
jts dea01dd6a8 İlk commit: Stepstead web (statik panel + db)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:59 +03:00

469 lines
22 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<title>Stepstead — Asset Takip</title>
<style>
:root{
--bg:#0f1419; --panel:#1a2230; --panel2:#222d3d; --line:#2c3a4f;
--txt:#e6edf3; --muted:#8b99a8; --gold:#e8b923;
--eksik:#e5534b; --yapiliyor:#d9a521; --tamam:#2ea043;
--d2:#4a9eff; --d3:#c77dff;
}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
background:var(--bg);color:var(--txt);line-height:1.5}
header{padding:20px 16px;border-bottom:1px solid var(--line);
position:sticky;top:0;background:var(--bg);z-index:10}
h1{margin:0;font-size:20px;color:var(--gold)}
.sub{color:var(--muted);font-size:13px;margin-top:4px}
.wrap{max-width:1040px;margin:0 auto;padding:16px}
.bar{height:10px;background:var(--panel);border-radius:6px;overflow:hidden;margin:10px 0 4px}
.bar > i{display:block;height:100%;background:var(--tamam);width:0;transition:width .3s}
.barlbl{font-size:12px;color:var(--muted)}
.cat{background:var(--panel);border:1px solid var(--line);border-radius:10px;
margin:12px 0;overflow:hidden}
.cat > summary{cursor:pointer;list-style:none;padding:14px 16px;
display:flex;align-items:center;gap:10px;font-weight:600;user-select:none}
.cat > summary::-webkit-details-marker{display:none}
.cat > summary .chev{transition:transform .2s;color:var(--muted)}
.cat[open] > summary .chev{transform:rotate(90deg)}
.cat .count{margin-left:auto;font-size:12px;color:var(--muted);font-weight:400;display:flex;gap:5px;flex-wrap:wrap}
table{width:100%;border-collapse:collapse}
th,td{text-align:left;padding:10px 16px;border-top:1px solid var(--line);font-size:14px;vertical-align:top}
th{color:var(--muted);font-weight:500;font-size:12px;text-transform:uppercase;letter-spacing:.04em}
td.name{font-weight:600}
td.desc{color:var(--muted)}
select.st,select.dm{background:var(--panel2);color:var(--txt);border:1px solid var(--line);
border-radius:6px;padding:5px 8px;font-size:13px;cursor:pointer}
select.st.eksik{color:var(--eksik)} select.st.yapiliyor{color:var(--yapiliyor)} select.st.tamam{color:var(--tamam)}
select.dm.d2{color:var(--d2)} select.dm.d3{color:var(--d3)} select.dm.dx{color:var(--muted)}
tr.done td.name{text-decoration:line-through;opacity:.6}
.del{background:none;border:none;color:var(--muted);cursor:pointer;font-size:16px;opacity:.5}
.del:hover{color:var(--eksik);opacity:1}
.addrow td{padding:8px 16px 14px}
.addrow input{background:var(--panel2);color:var(--txt);border:1px solid var(--line);
border-radius:6px;padding:7px 9px;font-size:13px;margin-right:6px}
.addrow input.n{width:180px} .addrow input.d{width:240px}
.addrow select{background:var(--panel2);color:var(--txt);border:1px solid var(--line);border-radius:6px;padding:7px;font-size:13px;margin-right:6px}
.btn{background:var(--gold);color:#1a1300;border:none;border-radius:6px;
padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer}
.btn.ghost{background:transparent;color:var(--muted);border:1px solid var(--line)}
.btn.ghost.on{border-color:var(--gold);color:var(--gold)}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;align-items:center}
.filt{display:flex;gap:6px;margin-right:auto}
.pill{font-size:11px;padding:1px 7px;border-radius:10px}
.pill.eksik{background:rgba(229,83,75,.15);color:var(--eksik)}
.pill.yapiliyor{background:rgba(217,165,33,.15);color:var(--yapiliyor)}
.pill.tamam{background:rgba(46,160,67,.15);color:var(--tamam)}
.pill.d2{background:rgba(74,158,255,.15);color:var(--d2)}
.pill.d3{background:rgba(199,125,255,.15);color:var(--d3)}
.hide{display:none !important}
.logincard{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px}
.logincard input{background:#222d3d;color:var(--txt,#e6edf3);border:1px solid #2c3a4f;border-radius:6px;padding:6px 9px;font-size:13px}
.logincard .msg{font-size:12px;color:#e5534b}
button.up{background:none;border:1px solid #2c3a4f;border-radius:6px;cursor:pointer;font-size:13px;padding:2px 7px}
button.up:hover{border-color:#e8b923}
#toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:#1a2230;border:1px solid #e8b923;color:#e6edf3;padding:9px 16px;border-radius:8px;font-size:13px;opacity:0;pointer-events:none;transition:.3s;max-width:90vw;z-index:9}
#toast.on{opacity:1}
</style>
<script src="/nav.js?v=8" data-theme="dark" defer></script>
</head>
<body>
<header>
<div class="wrap" style="padding-bottom:0">
<h1>🎨 Stepstead — Asset Takip</h1>
<div class="sub">Eksik görselleri gör, 2D/3D ayır, tamamlandıkça işaretle. Veriler bu tarayıcıda saklanır.</div>
<div class="bar"><i id="pbar"></i></div>
<div class="barlbl" id="plbl"></div>
<div class="toolbar">
<div class="filt">
<button class="btn ghost on" data-filt="all">Tümü</button>
<button class="btn ghost" data-filt="2d">🟦 2D</button>
<button class="btn ghost" data-filt="3d">🟪 3D</button>
</div>
<a class="btn ghost" href="/assets/upload.html" style="text-decoration:none">📤 Dosya Yükle</a>
<button class="btn ghost" id="expBtn">⬇ Dışa aktar</button>
<button class="btn ghost" id="impBtn">⬆ İçe aktar</button>
<input type="file" id="impFile" accept="application/json" hidden>
<button class="btn ghost" id="authBtn">🔑 Giriş</button>
</div>
<div class="logincard hide" id="loginCard">
<input type="email" id="lgEmail" placeholder="E-posta" autocomplete="username">
<input type="password" id="lgPass" placeholder="Şifre" autocomplete="current-password">
<button class="btn ghost" id="lgBtn">Giriş yap</button>
<span class="msg" id="lgMsg"></span>
</div>
</div>
</header>
<div id="toast"></div>
<div class="wrap" id="root"></div>
<script>
const STATES=["eksik","yapiliyor","tamam"];
const STLBL={eksik:"Eksik",yapiliyor:"Yapılıyor",tamam:"Tamam"};
const DIMS=["2d","3d","yok"];
const DMLBL={"2d":"2D","3d":"3D","yok":"—"};
const DMCLS={"2d":"d2","3d":"d3","yok":"dx"};
const KEY="stepstead_assets_v3";
// [ad, açıklama, boyut] — 2026-07-04: lokasyon-graf pivotuna göre yenilendi (tile/3D söküldü)
const SEED=[
{cat:"🗺 Dünya Haritası (EN KRİTİK)", items:[
["Ana harita illüstrasyonu","Statik dünya haritası, portre 720×1280; 15 lokasyon pin koordinatları 0-1000 grid'e oturacak (world.gd TextureRect)","2d"],
["Pin: köy","Başlangıç köyü işareti","2d"],
["Pin: pazar","Ticaret/market işareti","2d"],
["Pin: liman","Liman işareti","2d"],
["Pin: orman (5 tier)","Fısıltı→Karanlık Orman; tier tonlaması","2d"],
["Pin: maden (3 tier)","Taş Ocağı / Derin Maden / Ejder Dağı","2d"],
["Pin: su (göl/kıyı)","Aynalı Göl, Fırtına Kıyısı","2d"],
["Pin: çayır & av","Çayırlar, Av Toprakları","2d"],
["Yol dokusu","Lokasyonlar arası patika çizgi stili (şimdilik Line2D)","2d"],
["Oyuncu konumu işareti","'Buradasın' halkası/bayrak + diğer oyuncu 👤N rozeti","2d"],
]},
{cat:"📍 Lokasyon Kartları (15)", items:[
["Köy","Panel üst görseli / vignette","2d"],
["Pazar","Tezgâhlar, vendor","2d"],
["Liman","Balıı iskeleleri","2d"],
["Fısıltı Ormanı","Tier 1 orman","2d"],
["Gümüş Çamlık","Tier 2 orman","2d"],
["Akça Koru","Tier 3 orman","2d"],
["Kadim Orman","Tier 4 orman","2d"],
["Karanlık Orman","Tier 5 orman","2d"],
["Taş Ocağı","Başlangıç madeni","2d"],
["Derin Maden","Orta seviye maden","2d"],
["Ejder Dağı","Üst seviye maden","2d"],
["Aynalı Göl","Tatlı su balıılığı","2d"],
["Fırtına Kıyısı","Deniz balıılığı","2d"],
["Çayırlar","Ot toplama","2d"],
["Av Toprakları","Avcılık","2d"],
]},
{cat:"🎴 Olay Kartı İllüstrasyonları (20)", items:[
["Gezgin Tacir","Genel yol olayı","2d"],
["Haydut Pususu","Genel yol olayı","2d"],
["Yaralı Gezgin","Genel yol olayı","2d"],
["Kayıp Sandık","Genel yol olayı","2d"],
["Dev Ayı","Genel yol olayı","2d"],
["Mantar Halkası","Genel yol olayı","2d"],
["Eski Harabe","Genel yol olayı","2d"],
["Nehir Geçidi","Genel yol olayı","2d"],
["Gizemli Yaşlı","Genel yol olayı","2d"],
["Kırık Araba","Genel yol olayı","2d"],
["Sağanak","Genel yol olayı","2d"],
["Şanslı Buluş","Genel yol olayı","2d"],
["Kurt Sürüsü","Av Toprakları / Karanlık Orman","2d"],
["Vahşi Domuz","Ormanlar / av","2d"],
["Taze Geyik İzi","Çayır / av","2d"],
["Mağara Örümceği","Madenler","2d"],
["Korsan Artığı","Liman / kıyı (gold ödüllü)","2d"],
["Yaşlı Avcı","Av Toprakları / Çayırlar","2d"],
["Terk Edilmiş Ağ","Göl / kıyı / liman","2d"],
["Ayı İni","Kadim / Karanlık Orman","2d"],
["Olay kartı çerçevesi","Altın kenarlı kart şablonu + karartma","2d"],
["d20 zar görseli","Skill check sonucu (başarılı/başarısız durumları)","2d"],
]},
{cat:"🎒 Item İkonları (~318, aile bazlı)", items:[
["Odunlar (13)","Meşe→Abanoz tier serisi","2d"],
["Cevher + külçe (6+6)","Bakır→Altın","2d"],
["Otlar (12)","Nane, adaçayı, lavanta...","2d"],
["Balıklar (15)","Sazan→Mersin","2d"],
["Et + deri (5+5)","Tavşan/geyik/domuz/kurt/ayı","2d"],
["Bootstrap malzemeleri","Küçük taş, sopa, sarmaşık","2d"],
["Tool'lar","Taş/bakır/demir balta+kazma, 3 olta","2d"],
["Consumable'lar","Çaylar, iksirler, yemekler (adım veren)","2d"],
["Marangozluk ürünleri","Mobilya/yapı ~80 ikon","2d"],
["Okçuluk ürünleri","Yay/ok ~26 ikon","2d"],
["Kuyumculuk ürünleri","Takılar ~6 ikon","2d"],
["Dericilik ürünleri","Giyim ~20 ikon","2d"],
["Nadirlik çerçeveleri","Common→Legendary renk/çerçeve","2d"],
]},
{cat:"⛺ Sistem Görselleri", items:[
["Kamp görseli","Lokasyonda kamp kurulu göstergesi (⛺ yerine)","2d"],
["Adım ikonu","👣 yerine özel ikon","2d"],
["Gold ikonu","🪙 yerine özel ikon","2d"],
["Günlük ödül / streak","🎁 + 🔥 streak görselleri","2d"],
["Liderlik rozetleri","🥇🥈🥉 + sıra çerçeveleri","2d"],
["Skill ikonları (10)","Madencilik, balıılık, toplama, ağaç kesme, avcılık, savaş, demircilik, simya, aşçılık, inşaat","2d"],
]},
{cat:"🧑 Karakter / NPC Portreleri", items:[
["Oyuncu avatarı","2D portre (profil/HUD)","2d"],
["Vendor NPC","Pazar tüccarı portresi","2d"],
["Olay NPC'leri","Tacir, yaşlı avcı, gizemli yaşlı, haydutlar (kartlarda kullanılır)","2d"],
]},
{cat:"🖼 UI / Arayüz", items:[
["Buton seti","Normal / basılı / pasif durumlar","2d"],
["Panel & çerçeve","Envanter, craft, market, lokasyon, chat panelleri","2d"],
["Menü ikonları","💬🏪🎁📊🏆🎒🔨 buton ikonları","2d"],
["Popup / modal","Onay, ödül, uyarı kutusu çerçevesi","2d"],
["Progress bar","Adım / XP / günlük hedef çubukları","2d"],
["Sekme & navigasyon","Panel içi sekmeler (Genel/Burada, İlanlar/Sat/Vendor)","2d"],
]},
{cat:"🏷 Marka", items:[
["Logo","Oyun logosu (yatay + kare)","2d"],
["Splash ekranı","Açılış görseli","2d"],
["App ikonu","Android/iOS launcher ikonu","2d"],
["Store görselleri","Mağaza banner / ekran görüntüleri","2d"],
]},
{cat:"✨ Efekt / Geri bildirim", items:[
["Toplama efekti","Kaynak toplandı parçacık/animasyon","2d"],
["Craft tamam efekti","Üretim başarılı ışıma","2d"],
["Level-up efekti","Seviye atlama kutlaması","2d"],
["Günlük ödül konfetisi","Claim anı","2d"],
]},
{cat:"🔊 Ses", items:[
["Tık sesi","UI buton sesi","yok"],
["Toplama sesi","Kaynak toplama","yok"],
["Craft sesi","Üretim sesi","yok"],
["Zar sesi","Olay kartı d20 atışı","yok"],
["Olay kartı sesi","Kart açılma","yok"],
["Seyahat sesi","Yola çıkma / varış","yok"],
["Level-up sesi","Seviye atlama","yok"],
["Ödül sesi","Günlük ödül / satış","yok"],
["Ambient","Arka plan müzik / ortam (lokasyon türüne göre?)","yok"],
]},
];
function freshSeed(){
return SEED.map(c=>({cat:c.cat,items:c.items.map(i=>({name:i[0],desc:i[1],dim:i[2],state:"eksik"}))}));
}
function load(){
let data;
try{ data=JSON.parse(localStorage.getItem(KEY)); }catch(e){}
if(!data || !Array.isArray(data)){
// v3: lokasyon-graf pivotu sonrası taze liste; v2'deki durumlar isim eşleşmesiyle korunur
data=freshSeed();
let old;
try{ old=JSON.parse(localStorage.getItem("stepstead_assets_v2")); }catch(e){}
if(Array.isArray(old)){
const seedNames=new Set();
data.forEach(c=>c.items.forEach(i=>seedNames.add(i.name)));
const states={};
const extras=[];
old.forEach(c=>c.items.forEach(i=>{
states[i.name]=i.state;
if(!seedNames.has(i.name) && i.state!=="tamam")
extras.push({name:i.name,desc:i.desc||"",dim:i.dim||"2d",state:i.state||"eksik"});
}));
data.forEach(c=>c.items.forEach(i=>{ if(states[i.name]) i.state=states[i.name]; }));
if(extras.length) data.push({cat:"📦 Eski listeden taşınanlar",items:extras});
}
save(data);
}
data.forEach(c=>c.items.forEach(i=>{ if(!i.dim) i.dim="2d"; }));
// Sunucu gerçeği: oyuna bağlanmış asset'ler her tarayıcıda "tamam" görünür (2026-07-06)
const SERVER_DONE=new Set([
"Ana harita illüstrasyonu","Pin: köy","Pin: pazar","Pin: liman",
"Pin: orman (5 tier)","Pin: maden (3 tier)","Pin: su (göl/kıyı)","Pin: çayır & av",
"Oyuncu konumu işareti",
"Köy","Pazar","Liman","Fısıltı Ormanı","Gümüş Çamlık","Akça Koru",
"Kadim Orman","Karanlık Orman","Taş Ocağı","Derin Maden","Ejder Dağı",
"Aynalı Göl","Fırtına Kıyısı","Çayırlar","Av Toprakları",
"Haydut Pususu","Yaralı Gezgin","Kayıp Sandık",
]);
// Kısmen bağlananlar: her tarayıcıda "yapılıyor" görünür (tamam'ı ezmez)
const SERVER_WIP=new Set([]);
data.forEach(c=>c.items.forEach(i=>{
if(SERVER_DONE.has(i.name)) i.state="tamam";
else if(SERVER_WIP.has(i.name) && i.state!=="tamam") i.state="yapiliyor";
}));
save(data);
return data;
}
function save(d){ localStorage.setItem(KEY,JSON.stringify(d)); }
let DATA=load();
let FILT="all";
// ---- Giriş + satırdan upload (up_<asset adı>.<ext> olarak kaydedilir) ----
const API="https://api.stepstead.com";
let TOKEN=localStorage.getItem("ss_upload_token")||"";
let ADMIN=false;
let PENDING=null; // {c,i} — hangi asset için dosya seçiliyor
function toast(msg,ms=3500){
const t=document.getElementById("toast");
t.textContent=msg; t.classList.add("on");
clearTimeout(t._tm); t._tm=setTimeout(()=>t.classList.remove("on"),ms);
}
function updateAuthUI(){
document.getElementById("authBtn").textContent=ADMIN?"🚪 Çıkış":"🔑 Giriş";
if(window.ssNavRefresh)window.ssNavRefresh();
}
async function checkAdmin(){
if(!TOKEN){ADMIN=false;updateAuthUI();return;}
try{
const r=await fetch(API+"/admin/uploads",{headers:{Authorization:"Bearer "+TOKEN}});
ADMIN=r.ok;
if(!r.ok&&(r.status===401||r.status===403)){TOKEN="";localStorage.removeItem("ss_upload_token");}
}catch(e){ADMIN=false;}
updateAuthUI(); render();
}
async function doLogin(){
const m=document.getElementById("lgMsg"); m.textContent="…";
try{
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({email:document.getElementById("lgEmail").value.trim(),
password:document.getElementById("lgPass").value})});
const b=await r.json().catch(()=>({}));
if(r.ok&&b.access_token){
TOKEN=b.access_token; localStorage.setItem("ss_upload_token",TOKEN);
m.textContent=""; document.getElementById("loginCard").classList.add("hide");
await checkAdmin();
if(!ADMIN) toast("⚠ Bu hesabın yükleme yetkisi yok");
}else m.textContent=b.detail||"Giriş başarısız";
}catch(e){ m.textContent="Bağlantı hatası"; }
}
async function uploadFor(c,i,file){
if(!file) return;
const it=DATA[c].items[i];
toast("⏳ Yükleniyor: "+it.name);
const fd=new FormData();
fd.append("file",file);
fd.append("target_name",it.name);
try{
const r=await fetch(API+"/admin/upload",{method:"POST",
headers:{Authorization:"Bearer "+TOKEN},body:fd});
const b=await r.json().catch(()=>({}));
if(r.ok){
if(it.state!=="tamam") it.state="yapiliyor"; // kontrol bekliyor
save(DATA); render();
toast("✅ Yüklendi: "+b.filename+" — kontrol edilecek");
}else toast("⚠ "+(b.detail||("Hata "+r.status)));
}catch(e){ toast("⚠ Bağlantı hatası"); }
}
document.getElementById("authBtn").onclick=()=>{
if(ADMIN){
TOKEN=""; localStorage.removeItem("ss_upload_token");
ADMIN=false; updateAuthUI(); render(); toast(ıkış yapıldı");
}else document.getElementById("loginCard").classList.toggle("hide");
};
document.getElementById("lgBtn").onclick=doLogin;
document.getElementById("lgPass").addEventListener("keydown",e=>{if(e.key==="Enter")doLogin();});
const upInput=document.createElement("input");
upInput.type="file"; upInput.accept=".png,.jpg,.jpeg,.webp,.svg,.json,.zip"; upInput.hidden=true;
document.body.appendChild(upInput);
upInput.onchange=e=>{
if(PENDING) uploadFor(PENDING.c,PENDING.i,e.target.files[0]);
PENDING=null; upInput.value="";
};
function visible(it){ return FILT==="all" || it.dim===FILT; }
function render(){
const root=document.getElementById("root");
root.innerHTML="";
let total=0,done=0;
DATA.forEach((cat,ci)=>{
const shown=cat.items.filter(visible);
const cTotal=shown.length;
const cDone=shown.filter(i=>i.state==="tamam").length;
const cWip=shown.filter(i=>i.state==="yapiliyor").length;
const cEksik=cTotal-cDone-cWip;
const c2=cat.items.filter(i=>i.dim==="2d").length;
const c3=cat.items.filter(i=>i.dim==="3d").length;
total+=cTotal; done+=cDone;
const det=document.createElement("details");
det.className="cat";
if(FILT!=="all" && cTotal===0){ det.classList.add("hide"); }
const sum=document.createElement("summary");
sum.innerHTML=`<span class="chev">▶</span>${cat.cat}
<span class="count">
${c2?`<span class="pill d2">${c2} 2D</span>`:""}
${c3?`<span class="pill d3">${c3} 3D</span>`:""}
<span class="pill tamam">${cDone} ✓</span>
<span class="pill eksik">${cEksik} eksik</span>
</span>`;
det.appendChild(sum);
const tbl=document.createElement("table");
tbl.innerHTML=`<thead><tr><th>Asset</th><th>Açıklama</th><th>Boyut</th><th>Durum</th><th></th></tr></thead>`;
const tb=document.createElement("tbody");
cat.items.forEach((it,ii)=>{
if(!visible(it)) return;
const tr=document.createElement("tr");
if(it.state==="tamam") tr.className="done";
tr.innerHTML=`<td class="name">${esc(it.name)}</td>
<td class="desc">${esc(it.desc||"")}</td>
<td><select class="dm ${DMCLS[it.dim]}" data-c="${ci}" data-i="${ii}">
${DIMS.map(d=>`<option value="${d}" ${d===it.dim?"selected":""}>${DMLBL[d]}</option>`).join("")}
</select></td>
<td><select class="st ${it.state}" data-c="${ci}" data-i="${ii}">
${STATES.map(s=>`<option value="${s}" ${s===it.state?"selected":""}>${STLBL[s]}</option>`).join("")}
</select></td>
<td style="white-space:nowrap">${ADMIN?`<button class="up" data-c="${ci}" data-i="${ii}" title="Bu asset için görsel yükle">📤</button> `:""}<button class="del" data-c="${ci}" data-i="${ii}" title="Sil">✕</button></td>`;
tb.appendChild(tr);
});
const addtr=document.createElement("tr");
addtr.className="addrow";
addtr.innerHTML=`<td colspan="5">
<input class="n" placeholder="Yeni asset adı" data-add-n="${ci}">
<input class="d" placeholder="Açıklama (ops.)" data-add-d="${ci}">
<select data-add-dim="${ci}">${DIMS.map(d=>`<option value="${d}">${DMLBL[d]}</option>`).join("")}</select>
<button class="btn" data-add="${ci}">+ Ekle</button></td>`;
tb.appendChild(addtr);
tbl.appendChild(tb);
det.appendChild(tbl);
root.appendChild(det);
});
const pct=total?Math.round(done/total*100):0;
document.getElementById("pbar").style.width=pct+"%";
const fl=FILT==="all"?"":` (${DMLBL[FILT]})`;
document.getElementById("plbl").textContent=`${done}/${total} tamamlandı${fl} — %${pct}`;
}
function esc(s){return (s+"").replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));}
document.addEventListener("change",e=>{
const c=+e.target.dataset.c, i=+e.target.dataset.i;
if(e.target.classList.contains("st")){
DATA[c].items[i].state=e.target.value; save(DATA); render();
}
if(e.target.classList.contains("dm")){
DATA[c].items[i].dim=e.target.value; save(DATA); render();
}
});
document.addEventListener("click",e=>{
if(e.target.dataset.filt){
FILT=e.target.dataset.filt;
document.querySelectorAll("[data-filt]").forEach(b=>b.classList.toggle("on",b.dataset.filt===FILT));
render(); return;
}
if(e.target.classList.contains("up")){
PENDING={c:+e.target.dataset.c,i:+e.target.dataset.i};
upInput.click(); return;
}
if(e.target.classList.contains("del")){
const c=+e.target.dataset.c, i=+e.target.dataset.i;
if(confirm("Bu asset satırı silinsin mi?")){ DATA[c].items.splice(i,1); save(DATA); render(); }
}
if(e.target.dataset.add!==undefined){
const c=+e.target.dataset.add;
const ni=document.querySelector(`[data-add-n="${c}"]`);
const di=document.querySelector(`[data-add-d="${c}"]`);
const dm=document.querySelector(`[data-add-dim="${c}"]`);
const name=ni.value.trim(); if(!name) return;
DATA[c].items.push({name,desc:di.value.trim(),dim:dm.value,state:"eksik"});
save(DATA); render();
document.querySelectorAll(".cat")[c].open=true;
}
});
document.getElementById("expBtn").onclick=()=>{
const blob=new Blob([JSON.stringify(DATA,null,2)],{type:"application/json"});
const a=document.createElement("a");
a.href=URL.createObjectURL(blob);
a.download="stepstead-assets.json"; a.click();
};
document.getElementById("impBtn").onclick=()=>document.getElementById("impFile").click();
document.getElementById("impFile").onchange=ev=>{
const f=ev.target.files[0]; if(!f) return;
const r=new FileReader();
r.onload=()=>{ try{ const d=JSON.parse(r.result);
if(Array.isArray(d)){ DATA=d; load.call&&0; DATA.forEach(c=>c.items.forEach(i=>{if(!i.dim)i.dim="2d";})); save(DATA); render(); } else alert("Geçersiz dosya"); }
catch(e){ alert("Okunamadı"); } };
r.readAsText(f);
};
render();
checkAdmin();
</script>
</body>
</html>