İlk commit: Stepstead web (statik panel + db)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
dea01dd6a8
18 changed files with 3764 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Kullanıcı yüklemeleri (büyük binary, canlıda kalır ama versiyonlanmaz)
|
||||||
|
assets/uploads/
|
||||||
432
assets/chars.html
Normal file
432
assets/chars.html
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
<!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 — Karakterler</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
|
||||||
|
*{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}
|
||||||
|
.wrap{max-width:1100px;margin:0 auto;padding:20px 16px}
|
||||||
|
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
|
||||||
|
h2{font-size:15px;color:var(--gold);margin:0 0 10px}
|
||||||
|
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:16px}
|
||||||
|
input,button,select,textarea{font-size:13px;border-radius:6px;font-family:inherit}
|
||||||
|
input,select,textarea{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:7px 9px}
|
||||||
|
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
|
||||||
|
textarea{width:100%;min-height:70px;resize:vertical;line-height:1.5}
|
||||||
|
.btn{background:var(--gold);color:#1a1300;border:none;padding:8px 16px;font-weight:600;cursor:pointer}
|
||||||
|
.btn.sec{background:#2a3648;color:var(--txt)}
|
||||||
|
.btn.sm{padding:5px 10px;font-size:12px}
|
||||||
|
.btn.link{background:none;border:none;color:var(--muted);cursor:pointer;padding:4px 6px;font-size:12px}
|
||||||
|
.btn.link:hover{color:var(--gold)}
|
||||||
|
.btn.link.del:hover{color:var(--err)}
|
||||||
|
.msg{font-size:13px;margin-top:8px;min-height:16px}
|
||||||
|
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
|
||||||
|
.hide{display:none}
|
||||||
|
.tabs{display:flex;gap:6px;margin-bottom:16px;border-bottom:1px solid var(--line)}
|
||||||
|
.tab{background:none;border:none;color:var(--muted);cursor:pointer;padding:9px 14px;font-size:14px;font-weight:600;border-bottom:2px solid transparent;margin-bottom:-1px}
|
||||||
|
.tab.on{color:var(--gold);border-bottom-color:var(--gold)}
|
||||||
|
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
|
||||||
|
.toolbar input[type=search]{flex:1 1 200px}
|
||||||
|
.count{color:var(--muted);font-size:12px}
|
||||||
|
.row{border-top:1px solid var(--line);padding:10px 2px}
|
||||||
|
.row:first-child{border-top:none}
|
||||||
|
.row .head{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.row .head b{font-size:14px}
|
||||||
|
.pill{font-size:11px;padding:1px 8px;border-radius:10px;background:#243247;color:var(--gold);font-weight:600}
|
||||||
|
.pill.max{background:#2a2438;color:#c9a0ff}
|
||||||
|
.pill.cls{background:#233a2c;color:#8fd6a0}
|
||||||
|
.row .acts{margin-left:auto;display:flex;gap:2px}
|
||||||
|
.row .desc{white-space:pre-wrap;word-wrap:break-word;font-size:13px;margin-top:4px}
|
||||||
|
.row .eff{white-space:pre-wrap;word-wrap:break-word;font-size:13px;margin-top:4px;border-left:2px solid var(--gold);padding-left:8px}
|
||||||
|
.row .notes{white-space:pre-wrap;word-wrap:break-word;font-size:12px;color:var(--muted);margin-top:4px;border-left:2px solid var(--line);padding-left:8px}
|
||||||
|
.frm{display:grid;grid-template-columns:2fr 1fr 1fr;gap:8px;margin-bottom:8px}
|
||||||
|
@media(max-width:700px){.frm{grid-template-columns:1fr}}
|
||||||
|
label{font-size:11px;color:var(--muted);display:block;margin-bottom:3px}
|
||||||
|
.frm input,.frm select{width:100%}
|
||||||
|
#toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:#1a2230;border:1px solid var(--gold);color:var(--txt);padding:9px 16px;border-radius:8px;font-size:13px;opacity:0;pointer-events:none;transition:.3s;z-index:9}
|
||||||
|
#toast.on{opacity:1}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="dark" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>⚔ Karakterler</h1>
|
||||||
|
<div class="sub">Sınıf ve skill tanımları. Sınıf = karakterin rolü / ne yaptığı. Skill = max level + etkisi. Skiller istersen bir sınıfa bağlanabilir.</div>
|
||||||
|
|
||||||
|
<div class="card" id="loginCard">
|
||||||
|
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button class="btn" id="loginBtn">Giriş</button>
|
||||||
|
<div class="msg" id="loginMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app" class="hide">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab on" data-tab="classes">🛡 Sınıflar</button>
|
||||||
|
<button class="tab" data-tab="skills">✨ Skiller</button>
|
||||||
|
<button class="tab" data-tab="races">🧬 Irklar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SINIFLAR ============ -->
|
||||||
|
<div id="tab-classes">
|
||||||
|
<div class="card">
|
||||||
|
<div class="toolbar">
|
||||||
|
<input type="search" id="qC" placeholder="Sınıf ara…">
|
||||||
|
<span class="count" id="cntC"></span>
|
||||||
|
</div>
|
||||||
|
<div id="listC"></div>
|
||||||
|
<div class="count" id="emptyC">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="formCardC">
|
||||||
|
<h2 id="formTitleC">+ Yeni Sınıf</h2>
|
||||||
|
<div class="frm">
|
||||||
|
<div><label>İsim *</label><input type="text" id="cName" placeholder="örn. Madenci"></div>
|
||||||
|
<div><label>Özet (kısa)</label><input type="text" id="cSummary" placeholder="tek satır tanım"></div>
|
||||||
|
<div><label>Sıra</label><input type="number" id="cSort" value="0"></div>
|
||||||
|
</div>
|
||||||
|
<label>Açıklama (ne yapar, rolü, oynanış)</label>
|
||||||
|
<textarea id="cDesc"></textarea>
|
||||||
|
<label style="margin-top:8px">Not (iç not — denge fikirleri, TODO)</label>
|
||||||
|
<textarea id="cNotes" style="min-height:50px"></textarea>
|
||||||
|
<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn" id="saveBtnC">Kaydet</button>
|
||||||
|
<button class="btn sec hide" id="cancelBtnC">Vazgeç (düzenleme)</button>
|
||||||
|
<div class="msg" id="fMsgC" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SKILLER ============ -->
|
||||||
|
<div id="tab-skills" class="hide">
|
||||||
|
<div class="card">
|
||||||
|
<div class="toolbar">
|
||||||
|
<input type="search" id="qS" placeholder="Skill, etki veya sınıf ara…">
|
||||||
|
<select id="fCls"><option value="">Tüm sınıflar</option></select>
|
||||||
|
<span class="count" id="cntS"></span>
|
||||||
|
</div>
|
||||||
|
<div id="listS"></div>
|
||||||
|
<div class="count" id="emptyS">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="formCardS">
|
||||||
|
<h2 id="formTitleS">+ Yeni Skill</h2>
|
||||||
|
<div class="frm">
|
||||||
|
<div><label>İsim *</label><input type="text" id="sName" placeholder="örn. Mining"></div>
|
||||||
|
<div><label>Max Level</label><input type="number" id="sMax" placeholder="örn. 50" min="0"></div>
|
||||||
|
<div><label>Bağlı Sınıf</label><select id="sCls"><option value="">— bağımsız —</option></select></div>
|
||||||
|
</div>
|
||||||
|
<label>Ne yapar (etki)</label>
|
||||||
|
<textarea id="sEff"></textarea>
|
||||||
|
<label style="margin-top:8px">Detay / seviye eğrisi / açıklama</label>
|
||||||
|
<textarea id="sDesc" style="min-height:50px"></textarea>
|
||||||
|
<label style="margin-top:8px">Not (iç not)</label>
|
||||||
|
<textarea id="sNotes" style="min-height:44px"></textarea>
|
||||||
|
<label style="margin-top:8px">Sıra</label>
|
||||||
|
<input type="number" id="sSort" value="0" style="width:100px">
|
||||||
|
<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn" id="saveBtnS">Kaydet</button>
|
||||||
|
<button class="btn sec hide" id="cancelBtnS">Vazgeç (düzenleme)</button>
|
||||||
|
<div class="msg" id="fMsgS" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ IRKLAR ============ -->
|
||||||
|
<div id="tab-races" class="hide">
|
||||||
|
<div class="card">
|
||||||
|
<div class="toolbar">
|
||||||
|
<input type="search" id="qR" placeholder="Irk ara…">
|
||||||
|
<span class="count" id="cntR"></span>
|
||||||
|
</div>
|
||||||
|
<div id="listR"></div>
|
||||||
|
<div class="count" id="emptyR">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="formCardR">
|
||||||
|
<h2 id="formTitleR">+ Yeni Irk</h2>
|
||||||
|
<div class="frm">
|
||||||
|
<div><label>İsim *</label><input type="text" id="rName" placeholder="örn. Zirler"></div>
|
||||||
|
<div><label>Özet (kısa)</label><input type="text" id="rSummary" placeholder="tek satır"></div>
|
||||||
|
<div><label>Sıra</label><input type="number" id="rSort" value="0"></div>
|
||||||
|
</div>
|
||||||
|
<label>Genel özellikler (madde madde, · ile ayır)</label>
|
||||||
|
<textarea id="rTraits" style="min-height:50px" placeholder="Karanlıkta görüş · Yeraltı · Doğa büyüsü"></textarea>
|
||||||
|
<label style="margin-top:8px">Açıklama (köken, kültür, oyundaki yeri)</label>
|
||||||
|
<textarea id="rDesc"></textarea>
|
||||||
|
<label style="margin-top:8px">Not (iç not / çapraz referans)</label>
|
||||||
|
<textarea id="rNotes" style="min-height:44px"></textarea>
|
||||||
|
<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn" id="saveBtnR">Kaydet</button>
|
||||||
|
<button class="btn sec hide" id="cancelBtnR">Vazgeç (düzenleme)</button>
|
||||||
|
<div class="msg" id="fMsgR" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
let TOKEN=localStorage.getItem("ss_upload_token")||"";
|
||||||
|
let CLASSES=[],SKILLS=[],RACES=[],editC=null,editS=null,editR=null;
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
const norm=s=>(s||"").toLocaleLowerCase("tr");
|
||||||
|
function esc(s){return (s??"").toString().replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));}
|
||||||
|
function toast(m,ms=2500){const t=$("toast");t.textContent=m;t.classList.add("on");clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove("on"),ms);}
|
||||||
|
|
||||||
|
async function api(path,opts={}){
|
||||||
|
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
|
||||||
|
const r=await fetch(API+path,opts);
|
||||||
|
let body={};try{body=await r.json();}catch(e){}
|
||||||
|
return {ok:r.ok,status:r.status,body};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(){
|
||||||
|
$("loginMsg").textContent="…";
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);
|
||||||
|
if(window.ssNavRefresh)ssNavRefresh();showApp();}
|
||||||
|
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showApp(){
|
||||||
|
const c=await api("/chars/classes");
|
||||||
|
if(!c.ok){
|
||||||
|
if(c.status===401||c.status===403){
|
||||||
|
$("loginMsg").className="msg err";
|
||||||
|
$("loginMsg").textContent=c.status===403?"Bu hesabın karakter yetkisi yok":"Giriş gerekli / oturum bitti";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CLASSES=c.body;
|
||||||
|
const [s,r]=await Promise.all([api("/chars/skills"),api("/chars/races")]);
|
||||||
|
SKILLS=s.ok?s.body:[];RACES=r.ok?r.body:[];
|
||||||
|
$("loginCard").classList.add("hide");$("app").classList.remove("hide");
|
||||||
|
refreshClassOptions();
|
||||||
|
renderC();renderS();renderR();
|
||||||
|
}
|
||||||
|
async function reloadR(){const r=await api("/chars/races");RACES=r.ok?r.body:[];renderR();}
|
||||||
|
async function reloadC(){const r=await api("/chars/classes");CLASSES=r.ok?r.body:[];refreshClassOptions();renderC();renderS();}
|
||||||
|
async function reloadS(){const r=await api("/chars/skills");SKILLS=r.ok?r.body:[];renderS();}
|
||||||
|
|
||||||
|
function refreshClassOptions(){
|
||||||
|
const opts=CLASSES.map(c=>`<option value="${c.id}">${esc(c.name)}</option>`).join("");
|
||||||
|
$("sCls").innerHTML='<option value="">— bağımsız —</option>'+opts;
|
||||||
|
$("fCls").innerHTML='<option value="">Tüm sınıflar</option>'+opts+'<option value="none">— sınıfsız —</option>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Sınıflar ---- */
|
||||||
|
function renderC(){
|
||||||
|
const q=norm($("qC").value.trim());
|
||||||
|
const rows=CLASSES.filter(c=>!q||norm(c.name).includes(q)||norm(c.summary).includes(q)||norm(c.description).includes(q));
|
||||||
|
$("cntC").textContent=`${rows.length} / ${CLASSES.length} sınıf`;
|
||||||
|
$("emptyC").style.display=rows.length?"none":"block";
|
||||||
|
$("emptyC").textContent=CLASSES.length?"Eşleşen sınıf yok.":"Henüz sınıf yok — aşağıdan ekle.";
|
||||||
|
let html="";
|
||||||
|
for(const c of rows){
|
||||||
|
const nSk=SKILLS.filter(s=>s.class_id===c.id).length;
|
||||||
|
html+=`<div class="row">
|
||||||
|
<div class="head"><b>${esc(c.name)}</b>
|
||||||
|
${c.summary?`<span class="pill">${esc(c.summary)}</span>`:""}
|
||||||
|
${nSk?`<span class="pill cls">${nSk} skill</span>`:""}
|
||||||
|
<span class="acts">
|
||||||
|
<button class="btn link" data-editc="${c.id}">düzenle</button>
|
||||||
|
<button class="btn link del" data-delc="${c.id}">sil</button>
|
||||||
|
</span></div>
|
||||||
|
${c.description?`<div class="desc">${esc(c.description)}</div>`:""}
|
||||||
|
${c.notes?`<div class="notes">${esc(c.notes)}</div>`:""}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
$("listC").innerHTML=html;
|
||||||
|
}
|
||||||
|
function fillC(c){
|
||||||
|
editC=c?c.id:null;
|
||||||
|
$("formTitleC").textContent=c?("✏️ Düzenle: "+c.name):"+ Yeni Sınıf";
|
||||||
|
$("cName").value=c?c.name:"";
|
||||||
|
$("cSummary").value=c?(c.summary||""):"";
|
||||||
|
$("cSort").value=c?c.sort_order:0;
|
||||||
|
$("cDesc").value=c?(c.description||""):"";
|
||||||
|
$("cNotes").value=c?(c.notes||""):"";
|
||||||
|
$("cancelBtnC").classList.toggle("hide",!c);
|
||||||
|
if(c)$("formCardC").scrollIntoView({behavior:"smooth",block:"nearest"});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Skiller ---- */
|
||||||
|
function renderS(){
|
||||||
|
const q=norm($("qS").value.trim()),fc=$("fCls").value;
|
||||||
|
const rows=SKILLS.filter(s=>{
|
||||||
|
if(fc==="none"&&s.class_id)return false;
|
||||||
|
if(fc&&fc!=="none"&&s.class_id!==+fc)return false;
|
||||||
|
if(!q)return true;
|
||||||
|
return norm(s.name).includes(q)||norm(s.effect).includes(q)||norm(s.description).includes(q)||norm(s.class_name).includes(q);
|
||||||
|
});
|
||||||
|
$("cntS").textContent=`${rows.length} / ${SKILLS.length} skill`;
|
||||||
|
$("emptyS").style.display=rows.length?"none":"block";
|
||||||
|
$("emptyS").textContent=SKILLS.length?"Eşleşen skill yok.":"Henüz skill yok — aşağıdan ekle.";
|
||||||
|
let html="";
|
||||||
|
for(const s of rows){
|
||||||
|
html+=`<div class="row">
|
||||||
|
<div class="head"><b>${esc(s.name)}</b>
|
||||||
|
${s.max_level!=null?`<span class="pill max">max ${s.max_level}</span>`:""}
|
||||||
|
${s.class_name?`<span class="pill cls">${esc(s.class_name)}</span>`:""}
|
||||||
|
<span class="acts">
|
||||||
|
<button class="btn link" data-edits="${s.id}">düzenle</button>
|
||||||
|
<button class="btn link del" data-dels="${s.id}">sil</button>
|
||||||
|
</span></div>
|
||||||
|
${s.effect?`<div class="eff">${esc(s.effect)}</div>`:""}
|
||||||
|
${s.description?`<div class="desc">${esc(s.description)}</div>`:""}
|
||||||
|
${s.notes?`<div class="notes">${esc(s.notes)}</div>`:""}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
$("listS").innerHTML=html;
|
||||||
|
}
|
||||||
|
function fillS(s){
|
||||||
|
editS=s?s.id:null;
|
||||||
|
$("formTitleS").textContent=s?("✏️ Düzenle: "+s.name):"+ Yeni Skill";
|
||||||
|
$("sName").value=s?s.name:"";
|
||||||
|
$("sMax").value=s&&s.max_level!=null?s.max_level:"";
|
||||||
|
$("sCls").value=s&&s.class_id?s.class_id:"";
|
||||||
|
$("sEff").value=s?(s.effect||""):"";
|
||||||
|
$("sDesc").value=s?(s.description||""):"";
|
||||||
|
$("sNotes").value=s?(s.notes||""):"";
|
||||||
|
$("sSort").value=s?s.sort_order:0;
|
||||||
|
$("cancelBtnS").classList.toggle("hide",!s);
|
||||||
|
if(s)$("formCardS").scrollIntoView({behavior:"smooth",block:"nearest"});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Irklar ---- */
|
||||||
|
function renderR(){
|
||||||
|
const q=norm($("qR").value.trim());
|
||||||
|
const rows=RACES.filter(r=>!q||norm(r.name).includes(q)||norm(r.summary).includes(q)||norm(r.traits).includes(q)||norm(r.description).includes(q));
|
||||||
|
$("cntR").textContent=`${rows.length} / ${RACES.length} ırk`;
|
||||||
|
$("emptyR").style.display=rows.length?"none":"block";
|
||||||
|
$("emptyR").textContent=RACES.length?"Eşleşen ırk yok.":"Henüz ırk yok — aşağıdan ekle.";
|
||||||
|
let html="";
|
||||||
|
for(const r of rows){
|
||||||
|
html+=`<div class="row">
|
||||||
|
<div class="head"><b>${esc(r.name)}</b>
|
||||||
|
${r.summary?`<span class="pill">${esc(r.summary)}</span>`:""}
|
||||||
|
<span class="acts">
|
||||||
|
<button class="btn link" data-editr="${r.id}">düzenle</button>
|
||||||
|
<button class="btn link del" data-delr="${r.id}">sil</button>
|
||||||
|
</span></div>
|
||||||
|
${r.traits?`<div class="eff">${esc(r.traits)}</div>`:""}
|
||||||
|
${r.description?`<div class="desc">${esc(r.description)}</div>`:""}
|
||||||
|
${r.notes?`<div class="notes">${esc(r.notes)}</div>`:""}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
$("listR").innerHTML=html;
|
||||||
|
}
|
||||||
|
function fillR(r){
|
||||||
|
editR=r?r.id:null;
|
||||||
|
$("formTitleR").textContent=r?("✏️ Düzenle: "+r.name):"+ Yeni Irk";
|
||||||
|
$("rName").value=r?r.name:"";
|
||||||
|
$("rSummary").value=r?(r.summary||""):"";
|
||||||
|
$("rSort").value=r?r.sort_order:0;
|
||||||
|
$("rTraits").value=r?(r.traits||""):"";
|
||||||
|
$("rDesc").value=r?(r.description||""):"";
|
||||||
|
$("rNotes").value=r?(r.notes||""):"";
|
||||||
|
$("cancelBtnR").classList.toggle("hide",!r);
|
||||||
|
if(r)$("formCardR").scrollIntoView({behavior:"smooth",block:"nearest"});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Olaylar ---- */
|
||||||
|
document.addEventListener("click",async e=>{
|
||||||
|
const t=e.target;
|
||||||
|
if(t.dataset.editc){const c=CLASSES.find(x=>x.id===+t.dataset.editc);if(c)fillC(c);}
|
||||||
|
if(t.dataset.dels)return dels(t);
|
||||||
|
if(t.dataset.edits){const s=SKILLS.find(x=>x.id===+t.dataset.edits);if(s)fillS(s);}
|
||||||
|
if(t.dataset.editr){const r=RACES.find(x=>x.id===+t.dataset.editr);if(r)fillR(r);}
|
||||||
|
if(t.dataset.delc){
|
||||||
|
const c=CLASSES.find(x=>x.id===+t.dataset.delc);
|
||||||
|
if(!c||!confirm(`"${c.name}" sınıfı silinsin mi? (Bağlı skiller sınıfsız kalır)`))return;
|
||||||
|
const r=await api("/chars/classes/"+c.id+"/delete",{method:"POST"});
|
||||||
|
if(r.ok){toast("Silindi");if(editC===c.id)fillC(null);await reloadC();}
|
||||||
|
else toast("⚠ "+(r.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
if(t.dataset.delr){
|
||||||
|
const r=RACES.find(x=>x.id===+t.dataset.delr);
|
||||||
|
if(!r||!confirm(`"${r.name}" ırkı silinsin mi?`))return;
|
||||||
|
const res=await api("/chars/races/"+r.id+"/delete",{method:"POST"});
|
||||||
|
if(res.ok){toast("Silindi");if(editR===r.id)fillR(null);await reloadR();}
|
||||||
|
else toast("⚠ "+(res.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
if(t.classList.contains("tab")){
|
||||||
|
document.querySelectorAll(".tab").forEach(x=>x.classList.toggle("on",x===t));
|
||||||
|
for(const k of ["classes","skills","races"])$("tab-"+k).classList.toggle("hide",t.dataset.tab!==k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
async function dels(t){
|
||||||
|
const s=SKILLS.find(x=>x.id===+t.dataset.dels);
|
||||||
|
if(!s||!confirm(`"${s.name}" skill silinsin mi?`))return;
|
||||||
|
const r=await api("/chars/skills/"+s.id+"/delete",{method:"POST"});
|
||||||
|
if(r.ok){toast("Silindi");if(editS===s.id)fillS(null);await reloadS();}
|
||||||
|
else toast("⚠ "+(r.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$("saveBtnC").onclick=async()=>{
|
||||||
|
const name=$("cName").value.trim();
|
||||||
|
if(!name){$("fMsgC").className="msg err";$("fMsgC").textContent="İsim gerekli";return;}
|
||||||
|
$("fMsgC").className="msg";$("fMsgC").textContent="…";
|
||||||
|
const payload={name,summary:$("cSummary").value.trim()||null,
|
||||||
|
description:$("cDesc").value.trim()||null,notes:$("cNotes").value.trim()||null,
|
||||||
|
sort_order:parseInt($("cSort").value)||0};
|
||||||
|
const r=editC?await api("/chars/classes/"+editC,{method:"POST",body:JSON.stringify(payload)})
|
||||||
|
:await api("/chars/classes",{method:"POST",body:JSON.stringify(payload)});
|
||||||
|
if(!r.ok){$("fMsgC").className="msg err";$("fMsgC").textContent=r.body.detail||"Hata";return;}
|
||||||
|
$("fMsgC").className="msg ok";$("fMsgC").textContent=editC?"Güncellendi":"Eklendi";
|
||||||
|
setTimeout(()=>$("fMsgC").textContent="",1500);
|
||||||
|
fillC(null);await reloadC();
|
||||||
|
};
|
||||||
|
$("cancelBtnC").onclick=()=>fillC(null);
|
||||||
|
|
||||||
|
$("saveBtnS").onclick=async()=>{
|
||||||
|
const name=$("sName").value.trim();
|
||||||
|
if(!name){$("fMsgS").className="msg err";$("fMsgS").textContent="İsim gerekli";return;}
|
||||||
|
$("fMsgS").className="msg";$("fMsgS").textContent="…";
|
||||||
|
const mx=$("sMax").value.trim();
|
||||||
|
const payload={name,max_level:mx===""?null:parseInt(mx),
|
||||||
|
effect:$("sEff").value.trim()||null,description:$("sDesc").value.trim()||null,
|
||||||
|
class_id:$("sCls").value?+$("sCls").value:null,
|
||||||
|
notes:$("sNotes").value.trim()||null,sort_order:parseInt($("sSort").value)||0};
|
||||||
|
const r=editS?await api("/chars/skills/"+editS,{method:"POST",body:JSON.stringify(payload)})
|
||||||
|
:await api("/chars/skills",{method:"POST",body:JSON.stringify(payload)});
|
||||||
|
if(!r.ok){$("fMsgS").className="msg err";$("fMsgS").textContent=r.body.detail||"Hata";return;}
|
||||||
|
$("fMsgS").className="msg ok";$("fMsgS").textContent=editS?"Güncellendi":"Eklendi";
|
||||||
|
setTimeout(()=>$("fMsgS").textContent="",1500);
|
||||||
|
fillS(null);await reloadS();
|
||||||
|
};
|
||||||
|
$("cancelBtnS").onclick=()=>fillS(null);
|
||||||
|
|
||||||
|
$("saveBtnR").onclick=async()=>{
|
||||||
|
const name=$("rName").value.trim();
|
||||||
|
if(!name){$("fMsgR").className="msg err";$("fMsgR").textContent="İsim gerekli";return;}
|
||||||
|
$("fMsgR").className="msg";$("fMsgR").textContent="…";
|
||||||
|
const payload={name,summary:$("rSummary").value.trim()||null,
|
||||||
|
traits:$("rTraits").value.trim()||null,description:$("rDesc").value.trim()||null,
|
||||||
|
notes:$("rNotes").value.trim()||null,sort_order:parseInt($("rSort").value)||0};
|
||||||
|
const r=editR?await api("/chars/races/"+editR,{method:"POST",body:JSON.stringify(payload)})
|
||||||
|
:await api("/chars/races",{method:"POST",body:JSON.stringify(payload)});
|
||||||
|
if(!r.ok){$("fMsgR").className="msg err";$("fMsgR").textContent=r.body.detail||"Hata";return;}
|
||||||
|
$("fMsgR").className="msg ok";$("fMsgR").textContent=editR?"Güncellendi":"Eklendi";
|
||||||
|
setTimeout(()=>$("fMsgR").textContent="",1500);
|
||||||
|
fillR(null);await reloadR();
|
||||||
|
};
|
||||||
|
$("cancelBtnR").onclick=()=>fillR(null);
|
||||||
|
|
||||||
|
$("qC").addEventListener("input",renderC);
|
||||||
|
$("qS").addEventListener("input",renderS);
|
||||||
|
$("qR").addEventListener("input",renderR);
|
||||||
|
$("fCls").addEventListener("change",renderS);
|
||||||
|
$("loginBtn").onclick=login;
|
||||||
|
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
|
||||||
|
if(TOKEN)showApp();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
469
assets/index.html
Normal file
469
assets/index.html
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
<!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ıkçı 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ıkçılığı","2d"],
|
||||||
|
["Fırtına Kıyısı","Deniz balıkçı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ıkçı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=>({"&":"&","<":"<",">":">",'"':"""}[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>
|
||||||
451
assets/lore.html
Normal file
451
assets/lore.html
Normal file
|
|
@ -0,0 +1,451 @@
|
||||||
|
<!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 — Lore Notları</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
|
||||||
|
*{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.6}
|
||||||
|
.wrap{max-width:1240px;margin:0 auto;padding:20px 16px}
|
||||||
|
.cols{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
|
||||||
|
@media(max-width:900px){.cols{grid-template-columns:1fr}}
|
||||||
|
.summary{white-space:pre-wrap;word-wrap:break-word;background:#141c28;border:1px solid var(--line);border-radius:8px;padding:14px;min-height:200px;font-size:14px}
|
||||||
|
.summary h3{color:var(--gold);font-size:15px;margin:16px 0 6px;border-bottom:1px solid var(--line);padding-bottom:4px}
|
||||||
|
.summary h3:first-child{margin-top:0}
|
||||||
|
.summary p{margin:0 0 8px}
|
||||||
|
.colhead{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
||||||
|
.colhead h2{font-size:15px;color:var(--gold);margin:0}
|
||||||
|
.colhead .count{margin-left:auto}
|
||||||
|
h1{font-size:20px;color:var(--gold);margin:0 0 2px}
|
||||||
|
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:14px}
|
||||||
|
input,button,textarea{font-size:14px;border-radius:6px;font-family:inherit}
|
||||||
|
input,textarea,select{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:9px 10px}
|
||||||
|
select{border-radius:6px;font-size:13px;font-family:inherit;max-width:220px}
|
||||||
|
.note .subj{background:#243247;color:#7fb3e8;border-radius:4px;padding:1px 7px;font-weight:600;cursor:pointer}
|
||||||
|
.note .subj:hover{color:var(--gold)}
|
||||||
|
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
|
||||||
|
textarea{width:100%;min-height:90px;resize:vertical;line-height:1.5}
|
||||||
|
.btn{background:var(--gold);color:#1a1300;border:none;padding:9px 18px;font-weight:600;cursor:pointer}
|
||||||
|
.btn.sec{background:#2a3648;color:var(--txt)}
|
||||||
|
.btn.sm{padding:5px 10px;font-size:12px}
|
||||||
|
.btn.link{background:none;border:none;color:var(--muted);cursor:pointer;padding:4px 6px;font-size:12px}
|
||||||
|
.btn.link:hover{color:var(--gold)}
|
||||||
|
.btn.link.del:hover{color:var(--err)}
|
||||||
|
.msg{font-size:13px;margin-top:8px}
|
||||||
|
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
|
||||||
|
.hide{display:none}
|
||||||
|
.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:10px}
|
||||||
|
.toolbar input{flex:0 0 180px}
|
||||||
|
.note{border-top:1px solid var(--line);padding:12px 2px}
|
||||||
|
.note:first-child{border-top:none}
|
||||||
|
.note .meta{display:flex;gap:10px;align-items:center;font-size:11px;color:var(--muted);margin-bottom:6px}
|
||||||
|
.note .tag{background:#243247;color:var(--gold);border-radius:4px;padding:1px 7px;font-weight:600}
|
||||||
|
.author{color:#7fd0a0;font-weight:600}
|
||||||
|
.note .body{white-space:pre-wrap;word-wrap:break-word}
|
||||||
|
.bilingual{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||||
|
@media(max-width:640px){.bilingual{grid-template-columns:1fr}}
|
||||||
|
.bilingual .lang{border:1px solid var(--line);border-radius:6px;padding:8px 10px;background:#141c28}
|
||||||
|
.bilingual .lang .lbl{font-size:10px;color:var(--muted);font-weight:600;letter-spacing:.5px;margin-bottom:4px}
|
||||||
|
.bilingual .lang .txt{white-space:pre-wrap;word-wrap:break-word;font-size:13px}
|
||||||
|
.bilingual .lang.en{opacity:.9}
|
||||||
|
.note .acts{margin-left:auto;display:flex;gap:2px}
|
||||||
|
.count{color:var(--muted);font-size:12px}
|
||||||
|
a{color:var(--gold)}
|
||||||
|
.story{border-top:1px solid var(--line);padding:14px 2px}
|
||||||
|
.story:first-of-type{border-top:none}
|
||||||
|
.story h3{color:var(--gold);font-size:16px;margin:0 0 4px}
|
||||||
|
.story .meta{font-size:11px;color:var(--muted);margin-bottom:8px;display:flex;gap:10px;align-items:center}
|
||||||
|
.story .meta .acts{margin-left:auto;display:flex;gap:2px}
|
||||||
|
.story .body{white-space:pre-wrap;word-wrap:break-word;line-height:1.7}
|
||||||
|
.story textarea{min-height:260px}
|
||||||
|
#stBody{min-height:260px}
|
||||||
|
#stTitle{width:100%;margin-bottom:8px}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="dark" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>📜 Lore Notları</h1>
|
||||||
|
<div class="sub">Özel — sadece admin. Küçük notlar al, sonra hikaye olarak derlenir.</div>
|
||||||
|
|
||||||
|
<div class="card" id="loginCard">
|
||||||
|
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button class="btn" id="loginBtn">Giriş</button>
|
||||||
|
<div class="msg" id="loginMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app" class="hide">
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-bottom:10px">
|
||||||
|
<button class="btn sec sm" id="langBtn">🌐 English</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="toolbar">
|
||||||
|
<select id="newSubj" title="Bir kişi veya yerle ilişkilendir"></select>
|
||||||
|
<input type="text" id="newTag" placeholder="etiket (ops.) örn. Eldoria">
|
||||||
|
<span class="count" id="cnt"></span>
|
||||||
|
</div>
|
||||||
|
<textarea id="newBody" placeholder="Not yaz… (kısa fikir, olay, karakter, cümle — ne olursa)"></textarea>
|
||||||
|
<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn" id="addBtn">+ Not Ekle</button>
|
||||||
|
<span style="color:var(--muted);font-size:12px">Ctrl/Cmd + Enter · yazınca otomatik çevrilir</span>
|
||||||
|
<div class="msg" id="addMsg" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="listCard">
|
||||||
|
<div class="colhead"><h2>📝 Notlar</h2>
|
||||||
|
<select id="filterSubj" title="Konuya göre filtrele" style="margin-left:auto"></select>
|
||||||
|
</div>
|
||||||
|
<div id="notes"></div>
|
||||||
|
<div class="count" id="empty">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="colhead"><h2>📖 Temel Hikaye</h2><span class="count" id="stCnt"></span></div>
|
||||||
|
<div id="stories"></div>
|
||||||
|
<div class="count" id="stEmpty">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 style="font-size:15px;color:var(--gold);margin:0 0 10px">+ Yeni Bölüm</h2>
|
||||||
|
<input type="text" id="stTitle" placeholder="Bölüm başlığı (ops.) örn. Eldoria'nın Doğuşu">
|
||||||
|
<textarea id="stBody" placeholder="Uzun hikaye metnini buraya yaz…"></textarea>
|
||||||
|
<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn" id="stAddBtn">+ Bölüm Ekle</button>
|
||||||
|
<span style="color:var(--muted);font-size:12px">yazınca otomatik çevrilir</span>
|
||||||
|
<div class="msg" id="stMsg" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
let TOKEN=localStorage.getItem("ss_upload_token")||"";
|
||||||
|
let notes=[];
|
||||||
|
let editId=null;
|
||||||
|
let ME={id:null,admin:false};
|
||||||
|
function canEdit(item){return ME.admin || (item.author_id!=null && item.author_id===ME.id);}
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
|
||||||
|
/* ---- Konu (kişi/yer) bağlama ---- */
|
||||||
|
let SUBJECTS={npc:[],place:[]};
|
||||||
|
let filterKey=""; // "", "npc:12", "place:3"
|
||||||
|
const KIND_TR={npc:"Kişiler",place:"Yerler"};
|
||||||
|
function subjKey(kind,id){return (kind&&id)?kind+":"+id:"";}
|
||||||
|
function subjLabel(kind,id){
|
||||||
|
const arr=SUBJECTS[kind]||[];const m=arr.find(x=>x.id===id);return m?m.label:null;
|
||||||
|
}
|
||||||
|
async function loadSubjects(){
|
||||||
|
const r=await api("/lore/subjects");
|
||||||
|
if(r.ok)SUBJECTS={npc:r.body.npc||[],place:r.body.place||[]};
|
||||||
|
buildSubjSelect($("newSubj"),"","",true,"— konu yok —");
|
||||||
|
buildFilterSelect();
|
||||||
|
}
|
||||||
|
// select'i optgroup'larla doldur. cur = "kind:id" seçili.
|
||||||
|
function buildSubjSelect(sel,curKind,curId,withNone,noneLabel){
|
||||||
|
if(!sel)return;
|
||||||
|
const cur=subjKey(curKind,curId);
|
||||||
|
sel.innerHTML="";
|
||||||
|
if(withNone){const o=document.createElement("option");o.value="";o.textContent=noneLabel||"— konu yok —";sel.appendChild(o);}
|
||||||
|
for(const kind of ["npc","place"]){
|
||||||
|
if(!SUBJECTS[kind].length)continue;
|
||||||
|
const og=document.createElement("optgroup");og.label=KIND_TR[kind];
|
||||||
|
for(const s of SUBJECTS[kind]){
|
||||||
|
const o=document.createElement("option");o.value=kind+":"+s.id;
|
||||||
|
o.textContent=s.label+(s.sub?` · ${s.sub}`:"");
|
||||||
|
if(o.value===cur)o.selected=true;
|
||||||
|
og.appendChild(o);
|
||||||
|
}
|
||||||
|
sel.appendChild(og);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildFilterSelect(){
|
||||||
|
const sel=$("filterSubj");if(!sel)return;
|
||||||
|
const prev=filterKey;sel.innerHTML="";
|
||||||
|
const o0=document.createElement("option");o0.value="";o0.textContent="Tüm konular";sel.appendChild(o0);
|
||||||
|
for(const kind of ["npc","place"]){
|
||||||
|
if(!SUBJECTS[kind].length)continue;
|
||||||
|
const og=document.createElement("optgroup");og.label=KIND_TR[kind];
|
||||||
|
for(const s of SUBJECTS[kind]){
|
||||||
|
const o=document.createElement("option");o.value=kind+":"+s.id;o.textContent=s.label;
|
||||||
|
if(o.value===prev)o.selected=true;og.appendChild(o);
|
||||||
|
}
|
||||||
|
sel.appendChild(og);
|
||||||
|
}
|
||||||
|
sel.onchange=()=>{filterKey=sel.value;render();};
|
||||||
|
}
|
||||||
|
function readSubj(sel){
|
||||||
|
const v=sel&&sel.value?sel.value:"";
|
||||||
|
if(!v)return {subject_kind:null,subject_id:null,subject_label:null};
|
||||||
|
const [kind,idStr]=v.split(":");const id=parseInt(idStr,10);
|
||||||
|
return {subject_kind:kind,subject_id:id,subject_label:subjLabel(kind,id)};
|
||||||
|
}
|
||||||
|
let LANG=localStorage.getItem("ss_lore_lang")||"tr";
|
||||||
|
function pickLang(item){return LANG==="en"?(item.body_en||item.body):item.body;}
|
||||||
|
function setLang(l){LANG=l;localStorage.setItem("ss_lore_lang",l);
|
||||||
|
$("langBtn").textContent=LANG==="tr"?"🌐 English":"🌐 Türkçe";
|
||||||
|
render();renderStories();}
|
||||||
|
|
||||||
|
async function api(path,opts={}){
|
||||||
|
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
|
||||||
|
const r=await fetch(API+path,opts);
|
||||||
|
let body={};try{body=await r.json();}catch(e){}
|
||||||
|
return {ok:r.ok,status:r.status,body};
|
||||||
|
}
|
||||||
|
function esc(s){return (s||"").replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c]));}
|
||||||
|
function fmtDate(iso){const d=new Date(iso);return d.toLocaleString("tr-TR",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});}
|
||||||
|
|
||||||
|
async function login(){
|
||||||
|
$("loginMsg").textContent="…";
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);showApp();}
|
||||||
|
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
|
||||||
|
}
|
||||||
|
async function showApp(){
|
||||||
|
const r=await api("/lore");
|
||||||
|
if(r.status===401||r.status===403){$("loginMsg").className="msg err";$("loginMsg").textContent="Yetki yok / oturum bitti";return;}
|
||||||
|
$("loginCard").classList.add("hide");$("app").classList.remove("hide");
|
||||||
|
$("langBtn").textContent=LANG==="tr"?"🌐 English":"🌐 Türkçe";
|
||||||
|
const me=await api("/auth/me");
|
||||||
|
if(me.ok){ME.id=me.body.id;ME.admin=me.body.is_admin||(me.body.permissions||[]).includes("*");}
|
||||||
|
await loadSubjects();
|
||||||
|
notes=r.ok?r.body:[];render();
|
||||||
|
await loadStories();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Temel Hikaye ---- */
|
||||||
|
let stories=[],stEditId=null;
|
||||||
|
async function loadStories(){const r=await api("/lore/story");stories=r.ok?r.body:[];renderStories();}
|
||||||
|
function renderStories(){
|
||||||
|
const box=$("stories");box.innerHTML="";
|
||||||
|
$("stCnt").textContent=stories.length+" bölüm";
|
||||||
|
$("stEmpty").style.display=stories.length?"none":"block";
|
||||||
|
$("stEmpty").textContent="Henüz bölüm yok — aşağıdan ilk bölümü ekle.";
|
||||||
|
for(const s of stories){
|
||||||
|
const div=document.createElement("div");div.className="story";
|
||||||
|
if(stEditId===s.id){
|
||||||
|
div.innerHTML=`<input type="text" class="etitle" value="${esc(s.title||"")}" placeholder="Bölüm başlığı" style="width:100%;margin-bottom:8px">
|
||||||
|
<div class="lbl" style="font-size:10px;color:var(--muted);margin-bottom:3px">TÜRKÇE</div>
|
||||||
|
<textarea class="ebody">${esc(s.body)}</textarea>
|
||||||
|
<div class="lbl" style="font-size:10px;color:var(--muted);margin:8px 0 3px">ENGLISH</div>
|
||||||
|
<textarea class="ebodyen">${esc(s.body_en||"")}</textarea>
|
||||||
|
<div style="margin-top:8px;display:flex;gap:6px;align-items:center">
|
||||||
|
<button class="btn sm save">Kaydet</button>
|
||||||
|
<button class="btn sec sm retr">🌐 Yeniden Çevir</button>
|
||||||
|
<button class="btn sec sm cancel">Vazgeç</button>
|
||||||
|
<span class="msg emsg" style="margin:0"></span>
|
||||||
|
</div>`;
|
||||||
|
div.querySelector(".save").onclick=async()=>{
|
||||||
|
const body=div.querySelector(".ebody").value.trim();
|
||||||
|
if(!body)return;
|
||||||
|
const body_en=div.querySelector(".ebodyen").value.trim()||null;
|
||||||
|
const r=await api("/lore/story/"+s.id,{method:"POST",
|
||||||
|
body:JSON.stringify({title:div.querySelector(".etitle").value.trim()||null,body,body_en})});
|
||||||
|
if(!r.ok){alert("Kaydedilemedi");return;}
|
||||||
|
stEditId=null;await loadStories();
|
||||||
|
};
|
||||||
|
div.querySelector(".retr").onclick=async()=>{
|
||||||
|
const em=div.querySelector(".emsg");em.className="msg";em.textContent="Çeviriliyor…";
|
||||||
|
try{const p=await translatePair(div.querySelector(".ebody").value.trim());
|
||||||
|
div.querySelector(".ebody").value=p.tr;div.querySelector(".ebodyen").value=p.en;
|
||||||
|
em.className="msg ok";em.textContent="Çevrildi";}
|
||||||
|
catch(err){em.className="msg err";em.textContent="Olmadı: "+err.message;}
|
||||||
|
};
|
||||||
|
div.querySelector(".cancel").onclick=()=>{stEditId=null;renderStories();};
|
||||||
|
}else{
|
||||||
|
div.innerHTML=`${s.title?`<h3>${esc(s.title)}</h3>`:""}
|
||||||
|
<div class="meta"><span>${fmtDate(s.updated_at)}</span>
|
||||||
|
${s.author?`<span class="author">✍ ${esc(s.author)}</span>`:""}
|
||||||
|
${canEdit(s)?`<span class="acts">
|
||||||
|
<button class="btn link edit">düzenle</button>
|
||||||
|
<button class="btn link del delx">sil</button>
|
||||||
|
</span>`:""}</div>
|
||||||
|
<div class="body">${esc(pickLang(s))}</div>`;
|
||||||
|
if(canEdit(s)){
|
||||||
|
div.querySelector(".edit").onclick=()=>{stEditId=s.id;renderStories();};
|
||||||
|
div.querySelector(".delx").onclick=async()=>{
|
||||||
|
if(!confirm("Bölüm silinsin mi?"))return;
|
||||||
|
const r=await api("/lore/story/"+s.id+"/delete",{method:"POST"});
|
||||||
|
if(!r.ok){alert("Silinemedi");return;}
|
||||||
|
await loadStories();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
box.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadNotes(){const r=await api("/lore");notes=r.ok?r.body:[];render();}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
const box=$("notes");box.innerHTML="";
|
||||||
|
const shown=filterKey?notes.filter(n=>subjKey(n.subject_kind,n.subject_id)===filterKey):notes;
|
||||||
|
$("cnt").textContent=filterKey?`${shown.length}/${notes.length} not`:notes.length+" not";
|
||||||
|
$("empty").style.display=shown.length?"none":"block";
|
||||||
|
$("empty").textContent=filterKey?"Bu konuda not yok.":"Henüz not yok.";
|
||||||
|
for(const n of shown){
|
||||||
|
const div=document.createElement("div");div.className="note";
|
||||||
|
if(editId===n.id){
|
||||||
|
div.innerHTML=`<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
|
||||||
|
<select class="esubj"></select>
|
||||||
|
<input type="text" class="etag" value="${esc(n.tag||"")}" placeholder="etiket" style="width:160px">
|
||||||
|
</div>
|
||||||
|
<div class="bilingual">
|
||||||
|
<div><div class="lbl" style="font-size:10px;color:var(--muted);margin-bottom:3px">TÜRKÇE</div>
|
||||||
|
<textarea class="ebody">${esc(n.body)}</textarea></div>
|
||||||
|
<div><div class="lbl" style="font-size:10px;color:var(--muted);margin-bottom:3px">ENGLISH</div>
|
||||||
|
<textarea class="ebodyen">${esc(n.body_en||"")}</textarea></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;display:flex;gap:6px;align-items:center">
|
||||||
|
<button class="btn sm save">Kaydet</button>
|
||||||
|
<button class="btn sec sm retr">🌐 Yeniden Çevir</button>
|
||||||
|
<button class="btn sec sm cancel">Vazgeç</button>
|
||||||
|
<span class="msg emsg" style="margin:0"></span>
|
||||||
|
</div>`;
|
||||||
|
buildSubjSelect(div.querySelector(".esubj"),n.subject_kind,n.subject_id,true,"— konu yok —");
|
||||||
|
div.querySelector(".save").onclick=()=>saveEdit(n.id,div);
|
||||||
|
div.querySelector(".cancel").onclick=()=>{editId=null;render();};
|
||||||
|
div.querySelector(".retr").onclick=async(e)=>{
|
||||||
|
const em=div.querySelector(".emsg");em.className="msg";em.textContent="Çeviriliyor…";
|
||||||
|
try{const p=await translatePair(div.querySelector(".ebody").value.trim());
|
||||||
|
div.querySelector(".ebody").value=p.tr;div.querySelector(".ebodyen").value=p.en;
|
||||||
|
em.className="msg ok";em.textContent="Çevrildi";}
|
||||||
|
catch(err){em.className="msg err";em.textContent="Olmadı: "+err.message;}
|
||||||
|
};
|
||||||
|
}else{
|
||||||
|
const subjChip=n.subject_kind&&n.subject_id
|
||||||
|
? `<span class="subj" data-k="${subjKey(n.subject_kind,n.subject_id)}">${n.subject_kind==="npc"?"👤":"📍"} ${esc(n.subject_label||subjLabel(n.subject_kind,n.subject_id)||"?")}</span>`:"";
|
||||||
|
div.innerHTML=`<div class="meta">
|
||||||
|
${subjChip}
|
||||||
|
${n.tag?`<span class="tag">${esc(n.tag)}</span>`:""}
|
||||||
|
<span>${fmtDate(n.created_at)}</span>
|
||||||
|
${n.author?`<span class="author">✍ ${esc(n.author)}</span>`:""}
|
||||||
|
${canEdit(n)?`<span class="acts">
|
||||||
|
<button class="btn link edit">düzenle</button>
|
||||||
|
<button class="btn link del delx">sil</button>
|
||||||
|
</span>`:""}</div>
|
||||||
|
<div class="body">${esc(pickLang(n))}</div>`;
|
||||||
|
const sc=div.querySelector(".subj");
|
||||||
|
if(sc)sc.onclick=()=>{filterKey=sc.dataset.k;$("filterSubj").value=filterKey;render();};
|
||||||
|
if(canEdit(n)){
|
||||||
|
div.querySelector(".edit").onclick=()=>{editId=n.id;render();};
|
||||||
|
div.querySelector(".delx").onclick=()=>delNote(n.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
box.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add(){
|
||||||
|
const input=$("newBody").value.trim();
|
||||||
|
if(!input){return;}
|
||||||
|
$("addMsg").className="msg";$("addMsg").textContent="Çeviriliyor…";
|
||||||
|
let tr,en;
|
||||||
|
try{const p=await translatePair(input);tr=p.tr;en=p.en;}
|
||||||
|
catch(e){$("addMsg").className="msg err";$("addMsg").textContent="Çeviri olmadı: "+e.message;return;}
|
||||||
|
$("addMsg").textContent="Kaydediliyor…";
|
||||||
|
const subj=readSubj($("newSubj"));
|
||||||
|
const r=await api("/lore",{method:"POST",body:JSON.stringify(Object.assign(
|
||||||
|
{body:tr,body_en:en,tag:$("newTag").value.trim()||null},subj))});
|
||||||
|
if(!r.ok){$("addMsg").className="msg err";$("addMsg").textContent=r.body.detail||"Hata";return;}
|
||||||
|
$("newBody").value="";$("newTag").value="";$("newSubj").value="";
|
||||||
|
$("addMsg").className="msg ok";$("addMsg").textContent="Eklendi (TR + EN)";
|
||||||
|
setTimeout(()=>$("addMsg").textContent="",1500);
|
||||||
|
await loadNotes();
|
||||||
|
}
|
||||||
|
async function saveEdit(id,div){
|
||||||
|
const body=div.querySelector(".ebody").value.trim();
|
||||||
|
const body_en=div.querySelector(".ebodyen").value.trim()||null;
|
||||||
|
if(!body)return;
|
||||||
|
const subj=readSubj(div.querySelector(".esubj"));
|
||||||
|
const r=await api("/lore/"+id,{method:"POST",body:JSON.stringify(Object.assign(
|
||||||
|
{body,body_en,tag:div.querySelector(".etag").value.trim()||null},subj))});
|
||||||
|
if(!r.ok){alert("Kaydedilemedi");return;}
|
||||||
|
editId=null;await loadNotes();
|
||||||
|
}
|
||||||
|
async function delNote(id){
|
||||||
|
if(!confirm("Not silinsin mi?"))return;
|
||||||
|
const r=await api("/lore/"+id+"/delete",{method:"POST"});
|
||||||
|
if(!r.ok){alert("Silinemedi");return;}
|
||||||
|
await loadNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Çeviri (TR↔EN, otomatik yön) ---- */
|
||||||
|
function detectLang(t){
|
||||||
|
if(/[çğıöşüÇĞİÖŞÜ]/.test(t))return "tr";
|
||||||
|
const w=(t.toLowerCase().match(/\b(ve|bir|bu|için|ile|çok|ama|değil|ben|sen|olan|gibi|daha|kadar|olarak|sonra)\b/g)||[]).length;
|
||||||
|
return w>=2?"tr":"en";
|
||||||
|
}
|
||||||
|
function chunk(t,max=450){
|
||||||
|
const parts=t.split(/(?<=[.!?\n])\s+/);const out=[];let cur="";
|
||||||
|
for(const p of parts){
|
||||||
|
if((cur+" "+p).length>max){if(cur)out.push(cur);cur=p.length>max?p:p;
|
||||||
|
while(cur.length>max){out.push(cur.slice(0,max));cur=cur.slice(max);}}
|
||||||
|
else cur=cur?cur+" "+p:p;
|
||||||
|
}
|
||||||
|
if(cur)out.push(cur);return out;
|
||||||
|
}
|
||||||
|
async function mmTranslate(text,src,tgt){
|
||||||
|
const r=await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${src}|${tgt}&de=joethesleeper84@gmail.com`);
|
||||||
|
const b=await r.json();
|
||||||
|
if(!b.responseData||!b.responseData.translatedText)throw new Error(b.responseDetails||"çeviri hatası");
|
||||||
|
return b.responseData.translatedText;
|
||||||
|
}
|
||||||
|
async function translateAll(text,src,tgt){
|
||||||
|
const outs=[];for(const c of chunk(text))outs.push(await mmTranslate(c,src,tgt));
|
||||||
|
return outs.join(" ");
|
||||||
|
}
|
||||||
|
// Girdiyi TR+EN çifte çevir; sol her zaman TR, sağ her zaman EN.
|
||||||
|
async function translatePair(input){
|
||||||
|
if(!input)throw new Error("boş metin");
|
||||||
|
const src=detectLang(input);
|
||||||
|
if(src==="tr")return {tr:input,en:await translateAll(input,"tr","en")};
|
||||||
|
return {en:input,tr:await translateAll(input,"en","tr")};
|
||||||
|
}
|
||||||
|
async function translateBox(taId,msgId){
|
||||||
|
const ta=$(taId),msg=$(msgId);
|
||||||
|
const text=ta.value.trim();
|
||||||
|
if(!text){msg.className="msg err";msg.textContent="Metin boş";return;}
|
||||||
|
const src=detectLang(text),tgt=src==="tr"?"en":"tr";
|
||||||
|
msg.className="msg";msg.textContent=`Çeviriliyor… (${src.toUpperCase()}→${tgt.toUpperCase()})`;
|
||||||
|
try{
|
||||||
|
const outs=[];
|
||||||
|
for(const c of chunk(text))outs.push(await mmTranslate(c,src,tgt));
|
||||||
|
ta.value=text+"\n\n--- "+tgt.toUpperCase()+" ---\n"+outs.join(" ");
|
||||||
|
msg.className="msg ok";msg.textContent=`Çevrildi (${src.toUpperCase()}→${tgt.toUpperCase()})`;
|
||||||
|
setTimeout(()=>msg.textContent="",2000);
|
||||||
|
}catch(e){msg.className="msg err";msg.textContent="Çeviri olmadı: "+e.message;}
|
||||||
|
}
|
||||||
|
$("langBtn").onclick=()=>setLang(LANG==="tr"?"en":"tr");
|
||||||
|
|
||||||
|
$("loginBtn").onclick=login;
|
||||||
|
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
|
||||||
|
$("addBtn").onclick=add;
|
||||||
|
$("newBody").addEventListener("keydown",e=>{if((e.ctrlKey||e.metaKey)&&e.key==="Enter")add();});
|
||||||
|
$("stAddBtn").onclick=async()=>{
|
||||||
|
const input=$("stBody").value.trim();
|
||||||
|
if(!input){$("stMsg").className="msg err";$("stMsg").textContent="Metin boş";return;}
|
||||||
|
$("stMsg").className="msg";$("stMsg").textContent="Çeviriliyor…";
|
||||||
|
let tr,en;
|
||||||
|
try{const p=await translatePair(input);tr=p.tr;en=p.en;}
|
||||||
|
catch(e){$("stMsg").className="msg err";$("stMsg").textContent="Çeviri olmadı: "+e.message;return;}
|
||||||
|
$("stMsg").textContent="Kaydediliyor…";
|
||||||
|
const r=await api("/lore/story",{method:"POST",
|
||||||
|
body:JSON.stringify({title:$("stTitle").value.trim()||null,body:tr,body_en:en})});
|
||||||
|
if(!r.ok){$("stMsg").className="msg err";$("stMsg").textContent=r.body.detail||"Hata";return;}
|
||||||
|
$("stTitle").value="";$("stBody").value="";
|
||||||
|
$("stMsg").className="msg ok";$("stMsg").textContent="Bölüm eklendi (TR + EN)";
|
||||||
|
setTimeout(()=>$("stMsg").textContent="",1500);
|
||||||
|
await loadStories();
|
||||||
|
};
|
||||||
|
if(TOKEN)showApp();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
758
assets/map-editor.html
Normal file
758
assets/map-editor.html
Normal file
|
|
@ -0,0 +1,758 @@
|
||||||
|
<!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 — Harita Editörü</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
|
||||||
|
*{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}
|
||||||
|
.wrap{max-width:1500px;margin:0 auto;padding:16px}
|
||||||
|
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
|
||||||
|
.sub{color:var(--muted);font-size:13px;margin-bottom:14px}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:14px}
|
||||||
|
input,button,select,textarea{font-size:14px;border-radius:6px}
|
||||||
|
input,select,textarea{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:8px 10px}
|
||||||
|
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
|
||||||
|
textarea{width:100%;min-height:54px;resize:vertical}
|
||||||
|
.btn{background:var(--gold);color:#1a1300;border:none;padding:8px 16px;font-weight:600;cursor:pointer}
|
||||||
|
.btn.sec{background:#2a3648;color:var(--txt)}
|
||||||
|
.btn.danger{background:var(--err);color:#fff}
|
||||||
|
.btn:disabled{opacity:.45;cursor:default}
|
||||||
|
.msg{font-size:13px;margin-top:8px}
|
||||||
|
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
|
||||||
|
.hide{display:none}
|
||||||
|
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
|
||||||
|
.toolbar .grp{display:flex;gap:6px;align-items:center;background:#141c28;border:1px solid var(--line);border-radius:8px;padding:6px 8px}
|
||||||
|
.modebtn{background:#2a3648;color:var(--txt);border:1px solid var(--line);padding:7px 12px;cursor:pointer;border-radius:6px}
|
||||||
|
.modebtn.on{background:var(--gold);color:#1a1300;border-color:var(--gold);font-weight:600}
|
||||||
|
.maparea{position:relative;width:100%;user-select:none;line-height:0;margin-bottom:14px}
|
||||||
|
#mapview{position:relative;width:100%}
|
||||||
|
#mapzoom{position:relative;width:100%;transform-origin:0 0}
|
||||||
|
#mapimg{width:100%;display:block;border-radius:8px;border:1px solid var(--line)}
|
||||||
|
#svg{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair}
|
||||||
|
/* tam ekran düzenleme modu */
|
||||||
|
.maparea.fs{position:fixed;inset:0;z-index:2000;width:100vw;height:100vh;height:100dvh;margin:0;overflow:hidden;background:#0b0f14;display:flex;flex-direction:column}
|
||||||
|
.maparea.fs #mapview{flex:1;min-height:0;overflow:hidden;touch-action:none}
|
||||||
|
.maparea.fs #mapimg{border-radius:0;border:none}
|
||||||
|
.toolbar.fsfloat{flex:0 0 auto;margin:0;background:#141c28;border:none;border-bottom:1px solid var(--line);border-radius:0;padding:6px 8px;max-height:42vh;overflow:auto}
|
||||||
|
.maparea.fs #fsOpenBtn{display:none}
|
||||||
|
.zoomctl{display:none}
|
||||||
|
.maparea.fs .zoomctl{display:flex;position:absolute;right:10px;bottom:10px;z-index:5;flex-direction:column;gap:6px}
|
||||||
|
.zoomctl button{width:44px;height:44px;font-size:20px;font-weight:700;background:rgba(20,28,40,.92);color:var(--txt);border:1px solid var(--line);border-radius:8px;cursor:pointer;line-height:1;display:flex;align-items:center;justify-content:center}
|
||||||
|
.zoomctl button:hover{border-color:var(--gold)}
|
||||||
|
#fsClose{display:none}
|
||||||
|
.maparea.fs #fsClose{display:inline-flex}
|
||||||
|
#formCard{max-width:560px;margin-bottom:14px}
|
||||||
|
.taggroups{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;align-items:start}
|
||||||
|
@media(max-width:900px){.taggroups{grid-template-columns:1fr}}
|
||||||
|
.taggroups .card{margin-bottom:0}
|
||||||
|
.grouphead{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;margin-bottom:6px}
|
||||||
|
.grouphead .cnt{color:var(--muted);font-size:12px;font-weight:400}
|
||||||
|
.taglist{max-height:340px;overflow:auto}
|
||||||
|
.gempty{color:var(--muted);font-size:12px;padding:6px 2px}
|
||||||
|
.tagrow{display:flex;align-items:center;gap:8px;padding:6px 4px;border-top:1px solid var(--line);font-size:13px;cursor:pointer}
|
||||||
|
.tagrow:hover{background:#141c28}
|
||||||
|
.tagrow.sel{background:#243247}
|
||||||
|
.swatch{width:12px;height:12px;border-radius:3px;flex:0 0 12px}
|
||||||
|
.knd{color:var(--muted);font-size:11px;margin-left:auto}
|
||||||
|
.delx{flex:0 0 auto;background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:0 4px;line-height:1}
|
||||||
|
.delx:hover{color:var(--err)}
|
||||||
|
.hint{color:var(--muted);font-size:12px;margin-top:8px}
|
||||||
|
label{font-size:12px;color:var(--muted);display:block;margin:8px 0 3px}
|
||||||
|
.row{display:flex;gap:8px}
|
||||||
|
.row>*{flex:1}
|
||||||
|
.regchecks{display:flex;flex-wrap:wrap;gap:6px;max-height:140px;overflow:auto;background:#141c28;border:1px solid var(--line);border-radius:6px;padding:8px}
|
||||||
|
.regchecks label{display:inline-flex;align-items:center;gap:5px;margin:0;font-size:12px;color:var(--txt);background:#222d3d;border:1px solid var(--line);border-radius:14px;padding:3px 9px;cursor:pointer}
|
||||||
|
.regchecks label:hover{border-color:var(--gold)}
|
||||||
|
.regchecks input{width:auto;margin:0}
|
||||||
|
.regchecks label.auto{border-color:var(--ok)}
|
||||||
|
.regchecks .gempty{color:var(--muted);font-size:12px}
|
||||||
|
polygon,polyline,circle,text{vector-effect:non-scaling-stroke}
|
||||||
|
#svg.draw *{pointer-events:none}
|
||||||
|
.veclabel{fill:#fff;paint-order:stroke;stroke:#000;stroke-width:3px;font-weight:600}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="dark" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>🗺 Harita Editörü</h1>
|
||||||
|
<div class="sub">Haritada kıta/bölge/nokta işaretle, lokasyonlar arası yol çiz → DB'ye kaydet. Kıtalar: Eldoria · Kaldün · Meridia</div>
|
||||||
|
|
||||||
|
<div class="card" id="loginCard">
|
||||||
|
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button class="btn" id="loginBtn">Giriş</button>
|
||||||
|
<div class="msg" id="loginMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editor" class="hide">
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="grp">
|
||||||
|
<span style="color:var(--muted);font-size:12px">Mod:</span>
|
||||||
|
<button class="modebtn on" data-mode="select">👆 Seç</button>
|
||||||
|
<button class="modebtn" data-mode="region">▱ Bölge çiz</button>
|
||||||
|
<button class="modebtn" data-mode="poi">📍 Nokta koy</button>
|
||||||
|
<button class="modebtn" data-mode="road">🛣 Yol çiz</button>
|
||||||
|
</div>
|
||||||
|
<div class="grp" id="drawCtl" style="display:none">
|
||||||
|
<span id="drawInfo" style="font-size:12px;color:var(--muted)">0 köşe</span>
|
||||||
|
<button class="modebtn" id="undoPt">↩︎ Geri al</button>
|
||||||
|
<button class="btn" id="finishBtn">✓ Bitir</button>
|
||||||
|
<button class="modebtn" id="cancelDraw">✕ İptal</button>
|
||||||
|
</div>
|
||||||
|
<div class="grp" id="roadCtl" style="display:none">
|
||||||
|
<span id="roadInfo" style="font-size:12px;color:var(--muted)">Başlangıç lokasyonuna tıkla</span>
|
||||||
|
<button class="modebtn" id="undoWp">↩︎ Geri al</button>
|
||||||
|
<button class="modebtn" id="cancelRoad">✕ İptal</button>
|
||||||
|
</div>
|
||||||
|
<div class="grp" style="gap:10px">
|
||||||
|
<span style="color:var(--muted);font-size:12px">Göster:</span>
|
||||||
|
<label style="margin:0"><input type="checkbox" id="showLabels" checked style="width:auto"> İsimler</label>
|
||||||
|
<label style="margin:0"><input type="checkbox" id="showRegions" checked style="width:auto"> Bölgeler</label>
|
||||||
|
<label style="margin:0"><input type="checkbox" id="showRoads" checked style="width:auto"> Yollar</label>
|
||||||
|
<label style="margin:0"><input type="checkbox" id="showPois" checked style="width:auto"> Noktalar</label>
|
||||||
|
</div>
|
||||||
|
<div class="grp">
|
||||||
|
<button class="btn" id="fsOpenBtn" title="Haritayı tam ekranda düzenle">🖊 Tam ekran düzenle</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="maparea" id="maparea">
|
||||||
|
<div id="mapview">
|
||||||
|
<div id="mapzoom">
|
||||||
|
<img id="mapimg" src="/assets/world_map@4x.webp?v=2" alt="Dünya haritası">
|
||||||
|
<svg id="svg" viewBox="0 0 1 1" preserveAspectRatio="none"></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="zoomctl">
|
||||||
|
<button type="button" id="fsClose" title="Tam ekranı kapat" style="background:var(--gold);color:#1a1300">✓</button>
|
||||||
|
<button type="button" id="zoomIn" title="Yakınlaştır">+</button>
|
||||||
|
<button type="button" id="zoomOut" title="Uzaklaştır">−</button>
|
||||||
|
<button type="button" id="zoomReset" title="Sığdır" style="font-size:15px">⤢</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card hide" id="formCard">
|
||||||
|
<b id="formTitle" style="font-size:14px">Yeni etiket</b>
|
||||||
|
<label>İsim</label>
|
||||||
|
<input type="text" id="fName" placeholder="örn. Fısıltı Ormanı">
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<label>Tür</label>
|
||||||
|
<select id="fKind">
|
||||||
|
<option value="region">Bölge</option>
|
||||||
|
<option value="continent">Kıta</option>
|
||||||
|
<option value="poi">Nokta (POI)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Kıta</label>
|
||||||
|
<select id="fCont">
|
||||||
|
<option value="">—</option>
|
||||||
|
<option>Eldoria</option>
|
||||||
|
<option>Kaldün</option>
|
||||||
|
<option>Meridia</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="catWrap">
|
||||||
|
<label>Kategori <span style="color:var(--muted)">(tip — ikon/renk otomatik gelir)</span></label>
|
||||||
|
<select id="fCategory"><option value="">— (belirsiz) —</option></select>
|
||||||
|
</div>
|
||||||
|
<div id="regionsWrap" class="hide">
|
||||||
|
<label>Bölge(ler) — nokta hangi bölgede? <span style="color:var(--muted)">(içinde olduğu bölgeler otomatik işaretlendi)</span></label>
|
||||||
|
<div id="fRegions" class="regchecks"></div>
|
||||||
|
</div>
|
||||||
|
<label>Not (opsiyonel)</label>
|
||||||
|
<textarea id="fNote" placeholder="lore / açıklama"></textarea>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:12px">
|
||||||
|
<button class="btn" id="saveBtn">Kaydet</button>
|
||||||
|
<button class="btn sec" id="closeForm">Vazgeç</button>
|
||||||
|
<button class="btn danger hide" id="delBtn" style="margin-left:auto">Sil</button>
|
||||||
|
</div>
|
||||||
|
<div class="msg" id="formMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card hide" id="roadForm" style="max-width:560px">
|
||||||
|
<b id="roadTitle" style="font-size:14px">Yeni yol</b>
|
||||||
|
<div style="font-size:13px;margin:8px 0" id="roadEnds">—</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<label>Adım maliyeti</label>
|
||||||
|
<input type="number" id="rCost" min="1" max="100000" value="300">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Olay şansı (0-1)</label>
|
||||||
|
<input type="number" id="rEvent" min="0" max="1" step="0.05" value="0.3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hint" id="rWpInfo">0 ara nokta</div>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:12px">
|
||||||
|
<button class="btn" id="saveRoad">Kaydet</button>
|
||||||
|
<button class="btn sec" id="closeRoad">Vazgeç</button>
|
||||||
|
<button class="btn danger hide" id="delRoadBtn" style="margin-left:auto">Sil</button>
|
||||||
|
</div>
|
||||||
|
<div class="msg" id="roadMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="taggroups">
|
||||||
|
<div class="card">
|
||||||
|
<div class="grouphead">🌍 Kıtalar <span class="cnt" id="cntContinent">0</span></div>
|
||||||
|
<div class="taglist" id="listContinent"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="grouphead">▱ Bölgeler <span class="cnt" id="cntRegion">0</span></div>
|
||||||
|
<div class="taglist" id="listRegion"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="grouphead">📍 Noktalar <span class="cnt" id="cntPoi">0</span></div>
|
||||||
|
<div class="taglist" id="listPoi"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="grouphead">🛣 Yollar <span class="cnt" id="cntRoad">0</span></div>
|
||||||
|
<div class="taglist" id="listRoad"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hint" id="listHint">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
const SVGNS="http://www.w3.org/2000/svg";
|
||||||
|
let TOKEN=localStorage.getItem("ss_upload_token")||"";
|
||||||
|
let CAN_EDIT=false; // map_edit izni — yoksa salt okunur (map_view)
|
||||||
|
let mode="select";
|
||||||
|
let tags=[]; // sunucudan gelen etiketler
|
||||||
|
let draft=[]; // çizilmekte olan köşeler [[x,y],...]
|
||||||
|
let editingId=null; // seçili/düzenlenen etiket id (null = yeni)
|
||||||
|
let draftPoint=null; // poi için tek nokta
|
||||||
|
let locs=[]; // lokasyonlar (yol uçları) — {id,code,name,type,x,y}
|
||||||
|
let roads=[]; // yollar — {id,a,b,step_cost,event_chance,waypoints}
|
||||||
|
let roadA=null,roadB=null;// çizilen yolun uç lokasyon code'ları
|
||||||
|
let roadWps=[]; // çizilen yolun ara noktaları [[x,y],...]
|
||||||
|
let roadEditId=null; // düzenlenen yol id (null = yeni)
|
||||||
|
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
const CONT_COLOR={Eldoria:"#3fa34d",["Kaldün"]:"#e0632a",Meridia:"#3a7bd5"};
|
||||||
|
let CATS={}; // kategori kataloğu {key:{kind,label,icon,color}}
|
||||||
|
let VENDOR_CATS=[],QUEST_CATS=[];
|
||||||
|
function cat(k){return k&&CATS[k]?CATS[k]:null;}
|
||||||
|
function catIcon(k){return cat(k)?cat(k).icon:"";}
|
||||||
|
function catLabel(k){return cat(k)?cat(k).label:"";}
|
||||||
|
function colorFor(t){return t.color||(cat(t.category)?cat(t.category).color:null)||CONT_COLOR[t.continent]||"#9aa7b5";}
|
||||||
|
function catOptions(kind,selected){
|
||||||
|
let html='<option value="">— (belirsiz) —</option>';
|
||||||
|
for(const k in CATS)if(CATS[k].kind===kind)
|
||||||
|
html+=`<option value="${k}"${k===selected?" selected":""}>${CATS[k].icon} ${esc(CATS[k].label)}</option>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
async function loadCats(){
|
||||||
|
const r=await api("/map-tags/categories");
|
||||||
|
if(r.ok){CATS=r.body.categories||{};VENDOR_CATS=r.body.vendor||[];QUEST_CATS=r.body.questgiver||[];}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(path,opts={}){
|
||||||
|
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
|
||||||
|
const r=await fetch(API+path,opts);
|
||||||
|
let body={};try{body=await r.json();}catch(e){}
|
||||||
|
return {ok:r.ok,status:r.status,body};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- auth ---- */
|
||||||
|
async function login(){
|
||||||
|
$("loginMsg").textContent="…";
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);showEditor();}
|
||||||
|
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
|
||||||
|
}
|
||||||
|
async function showEditor(){
|
||||||
|
// izin kontrolü: map_edit → tam yetki, map_view → salt okunur, yoksa red
|
||||||
|
try{
|
||||||
|
const r=await fetch(API+"/auth/me",{headers:{Authorization:"Bearer "+TOKEN}});
|
||||||
|
if(!r.ok){
|
||||||
|
if(r.status===401){TOKEN="";localStorage.removeItem("ss_upload_token");}
|
||||||
|
$("loginMsg").className="msg err";$("loginMsg").textContent="Oturum geçersiz — tekrar giriş yap";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const u=await r.json();
|
||||||
|
const perms=u.permissions||[];
|
||||||
|
const has=p=>perms.includes("*")||perms.includes(p);
|
||||||
|
CAN_EDIT=has("map_edit");
|
||||||
|
if(!CAN_EDIT&&!has("map_view")){
|
||||||
|
$("loginMsg").className="msg err";$("loginMsg").textContent="Bu hesabın harita editörü yetkisi yok";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}catch(e){$("loginMsg").className="msg err";$("loginMsg").textContent="Bağlantı hatası";return;}
|
||||||
|
$("loginCard").classList.add("hide");$("editor").classList.remove("hide");
|
||||||
|
if(!CAN_EDIT){
|
||||||
|
document.querySelectorAll('.modebtn[data-mode="region"],.modebtn[data-mode="poi"],.modebtn[data-mode="road"]').forEach(b=>b.style.display="none");
|
||||||
|
document.querySelector(".sub").textContent="Salt okunur görünüm — etiketleri görebilirsin, değiştiremezsin.";
|
||||||
|
}
|
||||||
|
await loadCats();
|
||||||
|
await loadTags();
|
||||||
|
await loadLocsRoads();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- veri ---- */
|
||||||
|
async function loadTags(){
|
||||||
|
const r=await api("/map-tags");
|
||||||
|
if(!r.ok){$("listHint").textContent="Yüklenemedi";return;}
|
||||||
|
tags=r.body;render();
|
||||||
|
}
|
||||||
|
async function loadLocsRoads(){
|
||||||
|
const [l,r]=await Promise.all([api("/map-tags/locations"),api("/map-tags/roads")]);
|
||||||
|
if(l.ok)locs=l.body;
|
||||||
|
if(r.ok)roads=r.body;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function locByCode(c){return locs.find(l=>l.code===c);}
|
||||||
|
// Yol ismi: "A'dan B'ye" (basit Türkçe ünlü uyumu — ayrılma + yönelme eki)
|
||||||
|
function _lastVowel(w){const m=(w||"").toLowerCase().match(/[aeıioöuü]/g);return m?m[m.length-1]:"e";}
|
||||||
|
function _back(v){return "aıou".includes(v);}
|
||||||
|
function _ablative(w){ // 'dan/'den/'tan/'ten
|
||||||
|
const b=_back(_lastVowel(w)); const d=/[çfhkpsşt]$/i.test((w||"").trim())?"t":"d";
|
||||||
|
return w+"'"+d+(b?"an":"en");
|
||||||
|
}
|
||||||
|
function _dative(w){ // 'a/'e/'ya/'ye
|
||||||
|
const b=_back(_lastVowel(w)); const y=/[aeıioöuü]$/i.test((w||"").trim())?"y":"";
|
||||||
|
return w+"'"+y+(b?"a":"e");
|
||||||
|
}
|
||||||
|
function roadLabel(rd){
|
||||||
|
const a=locByCode(rd.a),b=locByCode(rd.b);
|
||||||
|
return _ablative(a?a.name:rd.a)+" "+_dative(b?b.name:rd.b);
|
||||||
|
}
|
||||||
|
// yol tipine göre renk
|
||||||
|
const LOC_COLOR={village:"#e8b923",city:"#f0872a",forest:"#3fa34d",mine:"#9aa7b5",lake:"#3a7bd5",meadow:"#8fce5a",hunting:"#c06a3a"};
|
||||||
|
function locColor(l){return LOC_COLOR[l&&l.type]||"#c9d4e0";}
|
||||||
|
// verilen normalize noktaya en yakın lokasyonu bul (eşik içinde), yoksa null
|
||||||
|
function locAt(pt){
|
||||||
|
let best=null,bd=0.022;
|
||||||
|
for(const l of locs){const d=Math.hypot(l.x-pt[0],l.y-pt[1]);if(d<bd){bd=d;best=l;}}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- koordinat: tıklama -> normalize 0-1 ---- */
|
||||||
|
function evtToNorm(e){
|
||||||
|
const rect=$("svg").getBoundingClientRect();
|
||||||
|
let x=(e.clientX-rect.left)/rect.width, y=(e.clientY-rect.top)/rect.height;
|
||||||
|
return [Math.min(1,Math.max(0,+x.toFixed(4))),Math.min(1,Math.max(0,+y.toFixed(4)))];
|
||||||
|
}
|
||||||
|
function centroid(pts){
|
||||||
|
let x=0,y=0;for(const p of pts){x+=p[0];y+=p[1];}return [x/pts.length,y/pts.length];
|
||||||
|
}
|
||||||
|
function pointInPoly(pt,poly){
|
||||||
|
let inside=false,x=pt[0],y=pt[1];
|
||||||
|
for(let i=0,j=poly.length-1;i<poly.length;j=i++){
|
||||||
|
const xi=poly[i][0],yi=poly[i][1],xj=poly[j][0],yj=poly[j][1];
|
||||||
|
if(((yi>y)!==(yj>y))&&(x<(xj-xi)*(y-yi)/(yj-yi)+xi))inside=!inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
// verilen noktayı içeren kıta etiketinin kıta adını döndür (yoksa "")
|
||||||
|
function continentAt(pt){
|
||||||
|
for(const t of tags){
|
||||||
|
if(t.kind==="continent"&&t.points.length>=3&&pointInPoly(pt,t.points))
|
||||||
|
return t.continent||t.name||"";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// verilen noktayı içeren bölge (region) id'lerini döndür — üst üste binenlerin hepsi
|
||||||
|
function regionsAt(pt){
|
||||||
|
const out=[];
|
||||||
|
for(const t of tags){
|
||||||
|
if(t.kind==="region"&&t.points.length>=3&&pointInPoly(pt,t.points))out.push(t.id);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// bölge checkbox listesini çiz (checked = seçili, auto = otomatik tespit edilenler)
|
||||||
|
function renderRegionChecks(selected,autoIds){
|
||||||
|
const wrap=$("fRegions");wrap.innerHTML="";
|
||||||
|
const regs=tags.filter(t=>t.kind==="region");
|
||||||
|
if(!regs.length){wrap.innerHTML='<div class="gempty">Henüz bölge çizilmemiş.</div>';return;}
|
||||||
|
const sel=new Set(selected||[]),auto=new Set(autoIds||[]);
|
||||||
|
for(const r of regs){
|
||||||
|
const lb=document.createElement("label");
|
||||||
|
if(auto.has(r.id))lb.className="auto";
|
||||||
|
lb.innerHTML=`<input type="checkbox" value="${r.id}" ${sel.has(r.id)?"checked":""} ${CAN_EDIT?"":"disabled"}>
|
||||||
|
<span>${esc(r.name)}${r.continent?" · "+esc(r.continent):""}</span>`;
|
||||||
|
wrap.appendChild(lb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function selectedRegions(){
|
||||||
|
return [...$("fRegions").querySelectorAll("input:checked")].map(i=>+i.value);
|
||||||
|
}
|
||||||
|
function toggleRegionsWrap(kind){
|
||||||
|
$("regionsWrap").classList.toggle("hide",kind!=="poi");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- render ---- */
|
||||||
|
function render(){
|
||||||
|
const svg=$("svg");svg.innerHTML="";
|
||||||
|
const showLbl=$("showLabels").checked;
|
||||||
|
// kayıtlı yollar (en altta) — lokasyon uçları + ara noktalardan geçen polyline
|
||||||
|
if($("showRoads").checked)
|
||||||
|
for(const rd of roads){
|
||||||
|
const a=locByCode(rd.a),b=locByCode(rd.b);if(!a||!b)continue;
|
||||||
|
const pts=[[a.x,a.y],...(rd.waypoints||[]),[b.x,b.y]];
|
||||||
|
const sel=rd.id===roadEditId;
|
||||||
|
const ln=el("polyline",{points:pts.map(p=>p.join(",")).join(" "),fill:"none",
|
||||||
|
stroke:sel?"#fff":"#b98a3a","stroke-width":sel?3.2:2});
|
||||||
|
ln.setAttribute("pointer-events",mode==="select"?"auto":"none");
|
||||||
|
ln.style.cursor="pointer";ln.onclick=ev=>{ev.stopPropagation();selectRoad(rd);};
|
||||||
|
svg.appendChild(ln);
|
||||||
|
}
|
||||||
|
// kayıtlı etiketler
|
||||||
|
for(const t of tags){
|
||||||
|
if(t.kind==="poi" && !$("showPois").checked) continue;
|
||||||
|
if(t.kind!=="poi" && !$("showRegions").checked) continue;
|
||||||
|
const c=colorFor(t),sel=t.id===editingId;
|
||||||
|
if(t.kind==="poi"){
|
||||||
|
const [x,y]=t.points[0];
|
||||||
|
const dot=el("circle",{cx:x,cy:y,r:0.008,fill:c,stroke:sel?"#fff":"#000","stroke-width":sel?2:1});
|
||||||
|
dot.style.cursor="pointer";dot.onclick=ev=>{ev.stopPropagation();selectTag(t);};
|
||||||
|
svg.appendChild(dot);
|
||||||
|
if(showLbl)svg.appendChild(text(x+0.012,y+0.004,(catIcon(t.category)?catIcon(t.category)+" ":"")+t.name));
|
||||||
|
}else{
|
||||||
|
const poly=el("polygon",{points:t.points.map(p=>p.join(",")).join(" "),
|
||||||
|
fill:c+"33",stroke:c,"stroke-width":sel?3:1.6});
|
||||||
|
poly.style.cursor="pointer";poly.onclick=ev=>{ev.stopPropagation();selectTag(t);};
|
||||||
|
svg.appendChild(poly);
|
||||||
|
if(showLbl){const [cx,cy]=centroid(t.points);svg.appendChild(text(cx,cy,(catIcon(t.category)?catIcon(t.category)+" ":"")+t.name,"middle"));}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// çizim taslağı
|
||||||
|
if(draft.length){
|
||||||
|
svg.appendChild(el("polyline",{points:draft.map(p=>p.join(",")).join(" "),
|
||||||
|
fill:"none",stroke:"#e8b923","stroke-width":2,"stroke-dasharray":"4 3"}));
|
||||||
|
for(const p of draft)svg.appendChild(el("circle",{cx:p[0],cy:p[1],r:0.006,fill:"#e8b923"}));
|
||||||
|
}
|
||||||
|
if(draftPoint)svg.appendChild(el("circle",{cx:draftPoint[0],cy:draftPoint[1],r:0.008,fill:"#e8b923",stroke:"#000"}));
|
||||||
|
// lokasyon pinleri (yol uçları) — yol modunda tıklanabilir, diğerlerinde referans
|
||||||
|
for(const l of locs){
|
||||||
|
const isEnd=mode==="road"&&(l.code===roadA||l.code===roadB);
|
||||||
|
const c=el("circle",{cx:l.x,cy:l.y,r:0.0075,fill:locColor(l),
|
||||||
|
stroke:isEnd?"#fff":"#0a0f16","stroke-width":isEnd?2.6:1.4});
|
||||||
|
c.setAttribute("pointer-events",mode==="road"?"auto":"none");
|
||||||
|
if(mode==="road"){c.style.cursor="pointer";c.onclick=ev=>{ev.stopPropagation();pickRoadEnd(l);};}
|
||||||
|
svg.appendChild(c);
|
||||||
|
if(showLbl&&mode==="road")svg.appendChild(text(l.x,l.y-0.016,l.name,"middle"));
|
||||||
|
}
|
||||||
|
// çizilen yol taslağı
|
||||||
|
if(mode==="road"&&roadA){
|
||||||
|
const a=locByCode(roadA);
|
||||||
|
const pts=[[a.x,a.y],...roadWps];
|
||||||
|
if(roadB)pts.push([locByCode(roadB).x,locByCode(roadB).y]);
|
||||||
|
svg.appendChild(el("polyline",{points:pts.map(p=>p.join(",")).join(" "),fill:"none",
|
||||||
|
stroke:"#e8b923","stroke-width":2.5,"stroke-dasharray":"4 3"}));
|
||||||
|
for(const p of roadWps)svg.appendChild(el("circle",{cx:p[0],cy:p[1],r:0.005,fill:"#e8b923"}));
|
||||||
|
}
|
||||||
|
// liste — türe göre 3 grup
|
||||||
|
const lists={continent:$("listContinent"),region:$("listRegion"),poi:$("listPoi")};
|
||||||
|
const counts={continent:0,region:0,poi:0};
|
||||||
|
for(const k in lists)lists[k].innerHTML="";
|
||||||
|
$("listHint").textContent=tags.length?"":"Henüz etiket yok. Bir mod seç, haritaya çiz.";
|
||||||
|
// zone↔pin çapraz: her bölgenin içindeki pinler + her pin'in bölgesi
|
||||||
|
const pinsByRegion={},tagById={};
|
||||||
|
for(const t of tags)tagById[t.id]=t;
|
||||||
|
for(const t of tags)if(t.kind==="poi")for(const rid of (t.region_ids||[])){(pinsByRegion[rid]=pinsByRegion[rid]||[]).push(t);}
|
||||||
|
for(const t of tags){
|
||||||
|
const row=document.createElement("div");
|
||||||
|
row.className="tagrow"+(t.id===editingId?" sel":"");
|
||||||
|
let meta=t.category?(catIcon(t.category)+" "+catLabel(t.category)):"";
|
||||||
|
if(t.kind==="region"){const ps=pinsByRegion[t.id]||[];if(ps.length)meta+=` · içinde ${ps.map(p=>catIcon(p.category)||"📍").join("")} ${ps.length}`;}
|
||||||
|
if(t.kind==="poi"){const zs=(t.region_ids||[]).map(id=>tagById[id]).filter(Boolean);if(zs.length)meta+=" · ⤷ "+zs.map(z=>esc(z.name)).join(", ");}
|
||||||
|
if(t.continent)meta+=(meta?" · ":"")+esc(t.continent);
|
||||||
|
row.innerHTML=`<span class="swatch" style="background:${colorFor(t)}"></span>
|
||||||
|
<span>${catIcon(t.category)?catIcon(t.category)+" ":""}${esc(t.name)}</span><span class="knd">${meta}</span>
|
||||||
|
${CAN_EDIT?'<button class="delx" title="Sil">✕</button>':""}`;
|
||||||
|
const dx=row.querySelector(".delx");
|
||||||
|
if(dx)dx.onclick=ev=>{ev.stopPropagation();delTag(t.id,t.name);};
|
||||||
|
row.onclick=()=>selectTag(t);
|
||||||
|
const k=lists[t.kind]?t.kind:"region";
|
||||||
|
lists[k].appendChild(row);counts[k]++;
|
||||||
|
}
|
||||||
|
$("cntContinent").textContent=counts.continent;
|
||||||
|
$("cntRegion").textContent=counts.region;
|
||||||
|
$("cntPoi").textContent=counts.poi;
|
||||||
|
for(const k in lists)
|
||||||
|
if(!counts[k])lists[k].innerHTML='<div class="gempty">Boş</div>';
|
||||||
|
// Yollar listesi — "A'dan B'ye"
|
||||||
|
const lr=$("listRoad");lr.innerHTML="";
|
||||||
|
for(const rd of roads){
|
||||||
|
const wpn=(rd.waypoints||[]).length;
|
||||||
|
const row=document.createElement("div");
|
||||||
|
row.className="tagrow"+(rd.id===roadEditId?" sel":"");
|
||||||
|
row.innerHTML=`<span class="swatch" style="background:#b98a3a"></span>
|
||||||
|
<span>${esc(roadLabel(rd))}</span>
|
||||||
|
<span class="knd">${rd.step_cost}👣${wpn?" · "+wpn+" nokta":""}</span>
|
||||||
|
${CAN_EDIT?'<button class="delx" title="Sil">✕</button>':""}`;
|
||||||
|
const dx=row.querySelector(".delx");
|
||||||
|
if(dx)dx.onclick=ev=>{ev.stopPropagation();delRoad(rd.id);};
|
||||||
|
row.onclick=()=>selectRoad(rd);
|
||||||
|
lr.appendChild(row);
|
||||||
|
}
|
||||||
|
$("cntRoad").textContent=roads.length;
|
||||||
|
if(!roads.length)lr.innerHTML='<div class="gempty">Boş — 🛣 Yol çiz moduyla ekle</div>';
|
||||||
|
$("drawInfo").textContent=draft.length+" köşe";
|
||||||
|
$("finishBtn").disabled=draft.length<3;
|
||||||
|
}
|
||||||
|
function el(tag,attrs){const n=document.createElementNS(SVGNS,tag);for(const k in attrs)n.setAttribute(k,attrs[k]);return n;}
|
||||||
|
function text(x,y,s,anchor){const t=el("text",{x,y,class:"veclabel","text-anchor":anchor||"start"});t.setAttribute("font-size","0.02");t.textContent=s;return t;}
|
||||||
|
function esc(s){return (s||"").replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c]));}
|
||||||
|
|
||||||
|
/* ---- mod ---- */
|
||||||
|
function setMode(m){
|
||||||
|
mode=m;draft=[];draftPoint=null;
|
||||||
|
if(m!=="road"){roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");}
|
||||||
|
document.querySelectorAll(".modebtn[data-mode]").forEach(b=>b.classList.toggle("on",b.dataset.mode===m));
|
||||||
|
$("drawCtl").style.display=(m==="region")?"flex":"none";
|
||||||
|
$("roadCtl").style.display=(m==="road")?"flex":"none";
|
||||||
|
// 'draw' sınıfı çocukların pointer-event'lerini kapatır — yol modunda pinler tıklanabilir kalmalı
|
||||||
|
$("svg").classList.toggle("draw",m==="region"||m==="poi");
|
||||||
|
$("svg").style.cursor=(m==="select")?"pointer":"crosshair";
|
||||||
|
if(m==="road")updateRoadInfo();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function updateRoadInfo(){
|
||||||
|
let s;
|
||||||
|
if(!roadA)s="Başlangıç lokasyonuna tıkla";
|
||||||
|
else if(!roadB)s=`${nm(roadA)} → … (ara nokta koy, sonra varış lokasyonuna tıkla)`;
|
||||||
|
else s=`${nm(roadA)} → ${nm(roadB)}`;
|
||||||
|
$("roadInfo").textContent=s;
|
||||||
|
$("rWpInfo").textContent=roadWps.length+" ara nokta";
|
||||||
|
}
|
||||||
|
function nm(code){const l=locByCode(code);return l?l.name:code;}
|
||||||
|
// yol modunda lokasyon pinine tıklama: 1.=başlangıç, 2.=varış → form aç
|
||||||
|
function pickRoadEnd(l){
|
||||||
|
if(!roadA){roadA=l.code;roadWps=[];roadB=null;updateRoadInfo();render();return;}
|
||||||
|
if(l.code===roadA){return;} // aynı pine tekrar → yoksay
|
||||||
|
roadB=l.code;
|
||||||
|
openRoadForm(null);
|
||||||
|
updateRoadInfo();render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- çizim ---- */
|
||||||
|
$("svg").addEventListener("click",e=>{
|
||||||
|
if(mode==="region"){draft.push(evtToNorm(e));render();}
|
||||||
|
else if(mode==="poi"){draftPoint=evtToNorm(e);openForm(null,"poi");render();}
|
||||||
|
else if(mode==="road"){
|
||||||
|
const pt=evtToNorm(e);
|
||||||
|
const near=locAt(pt);
|
||||||
|
if(near){pickRoadEnd(near);return;}
|
||||||
|
if(roadA&&!roadB){roadWps.push(pt);updateRoadInfo();render();}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("undoPt").onclick=()=>{draft.pop();render();};
|
||||||
|
$("cancelDraw").onclick=()=>{draft=[];render();};
|
||||||
|
$("finishBtn").onclick=()=>{if(draft.length>=3)openForm(null,"region");};
|
||||||
|
|
||||||
|
/* ---- form ---- */
|
||||||
|
function openForm(tag,kind){
|
||||||
|
$("formCard").classList.remove("hide");$("formMsg").textContent="";
|
||||||
|
// salt okunurda kaydet/sil gizli, alanlar kilitli
|
||||||
|
$("saveBtn").style.display=CAN_EDIT?"":"none";
|
||||||
|
for(const id of ["fName","fKind","fCategory","fCont","fNote"])$(id).disabled=!CAN_EDIT;
|
||||||
|
if(tag){ // düzenleme
|
||||||
|
editingId=tag.id;$("formTitle").textContent="Düzenle: "+tag.name;
|
||||||
|
$("fName").value=tag.name;$("fKind").value=tag.kind;$("fCont").value=tag.continent||"";
|
||||||
|
$("fCategory").innerHTML=catOptions(tag.kind,tag.category||"");
|
||||||
|
$("fNote").value=tag.note||"";$("delBtn").classList.remove("hide");
|
||||||
|
// POI ise: mevcut bölge seçimini göster; noktanın altındaki bölgeleri "auto" vurgula
|
||||||
|
const autoR=tag.kind==="poi"&&tag.points[0]?regionsAt(tag.points[0]):[];
|
||||||
|
renderRegionChecks(tag.region_ids||[],autoR);
|
||||||
|
toggleRegionsWrap(tag.kind);
|
||||||
|
}else{ // yeni — çizilen alanın merkezi hangi kıtadaysa onu otomatik seç
|
||||||
|
editingId=null;$("formTitle").textContent="Yeni etiket";
|
||||||
|
const pt=(kind==="poi")?draftPoint:(draft.length?centroid(draft):null);
|
||||||
|
const cont=pt?continentAt(pt):"";
|
||||||
|
$("fName").value="";$("fKind").value=kind;$("fCont").value=cont;$("fNote").value="";
|
||||||
|
$("fCategory").innerHTML=catOptions(kind,"");
|
||||||
|
$("delBtn").classList.add("hide");
|
||||||
|
// POI: içinde olduğu bölgeleri otomatik seç
|
||||||
|
const autoR=(kind==="poi"&&pt)?regionsAt(pt):[];
|
||||||
|
renderRegionChecks(autoR,autoR);
|
||||||
|
toggleRegionsWrap(kind);
|
||||||
|
}
|
||||||
|
$("catWrap").classList.toggle("hide",$("fKind").value==="continent");
|
||||||
|
if(!CAN_EDIT)$("delBtn").classList.add("hide");
|
||||||
|
render();
|
||||||
|
$("formCard").scrollIntoView({behavior:"smooth",block:"nearest"});
|
||||||
|
$("fName").focus();
|
||||||
|
}
|
||||||
|
function selectTag(t){setMode("select");openForm(t,t.kind);}
|
||||||
|
$("closeForm").onclick=()=>{editingId=null;draft=[];draftPoint=null;$("formCard").classList.add("hide");render();};
|
||||||
|
|
||||||
|
$("saveBtn").onclick=async()=>{
|
||||||
|
const name=$("fName").value.trim();
|
||||||
|
if(!name){$("formMsg").className="msg err";$("formMsg").textContent="İsim gerekli";return;}
|
||||||
|
const kind=$("fKind").value;
|
||||||
|
let points;
|
||||||
|
if(editingId){ // mevcut geometriyi koru
|
||||||
|
const cur=tags.find(t=>t.id===editingId);points=cur.points;
|
||||||
|
}else if(kind==="poi"){
|
||||||
|
if(!draftPoint){$("formMsg").textContent="Haritada nokta seç";return;}points=[draftPoint];
|
||||||
|
}else{
|
||||||
|
if(draft.length<3){$("formMsg").textContent="En az 3 köşe";return;}points=draft.slice();
|
||||||
|
}
|
||||||
|
const category=$("fCategory").value||null;
|
||||||
|
const payload={name,kind,category,continent:$("fCont").value||null,points,
|
||||||
|
color:cat(category)?cat(category).color:null,
|
||||||
|
note:$("fNote").value.trim()||null,
|
||||||
|
region_ids:kind==="poi"?selectedRegions():[]};
|
||||||
|
$("formMsg").className="msg";$("formMsg").textContent="Kaydediliyor…";
|
||||||
|
const r=editingId?await api("/map-tags/"+editingId,{method:"POST",body:JSON.stringify(payload)})
|
||||||
|
:await api("/map-tags",{method:"POST",body:JSON.stringify(payload)});
|
||||||
|
if(!r.ok){$("formMsg").className="msg err";$("formMsg").textContent=r.body.detail||("Hata "+r.status);return;}
|
||||||
|
draft=[];draftPoint=null;editingId=null;$("formCard").classList.add("hide");
|
||||||
|
await loadTags();
|
||||||
|
};
|
||||||
|
async function delTag(id,name){
|
||||||
|
if(!confirm("Silinsin mi? — "+name))return;
|
||||||
|
const r=await api("/map-tags/"+id+"/delete",{method:"POST"});
|
||||||
|
if(!r.ok){alert("Silinemedi ("+r.status+")");return;}
|
||||||
|
if(editingId===id){editingId=null;$("formCard").classList.add("hide");}
|
||||||
|
await loadTags();
|
||||||
|
}
|
||||||
|
$("delBtn").onclick=()=>{if(editingId)delTag(editingId,$("fName").value);};
|
||||||
|
|
||||||
|
/* ---- yol formu ---- */
|
||||||
|
function openRoadForm(rd){
|
||||||
|
$("roadForm").classList.remove("hide");$("roadMsg").textContent="";
|
||||||
|
if(rd){ // mevcut yolu düzenle
|
||||||
|
roadEditId=rd.id;roadA=rd.a;roadB=rd.b;roadWps=(rd.waypoints||[]).map(p=>p.slice());
|
||||||
|
$("roadTitle").textContent="Yolu düzenle";
|
||||||
|
$("rCost").value=rd.step_cost;$("rEvent").value=rd.event_chance.toFixed(2);
|
||||||
|
$("delRoadBtn").classList.remove("hide");
|
||||||
|
}else{ // yeni yol (roadA/roadB zaten seçili)
|
||||||
|
roadEditId=null;$("roadTitle").textContent="Yeni yol";
|
||||||
|
$("delRoadBtn").classList.add("hide");
|
||||||
|
}
|
||||||
|
$("roadEnds").innerHTML=`🛣 <b>${esc(roadLabel({a:roadA,b:roadB}))}</b>`;
|
||||||
|
$("rWpInfo").textContent=roadWps.length+" ara nokta";
|
||||||
|
$("roadForm").scrollIntoView({behavior:"smooth",block:"nearest"});
|
||||||
|
}
|
||||||
|
function selectRoad(rd){setMode("road");openRoadForm(rd);render();}
|
||||||
|
$("saveRoad").onclick=async()=>{
|
||||||
|
if(!roadA||!roadB){$("roadMsg").className="msg err";$("roadMsg").textContent="İki uç lokasyon gerekli";return;}
|
||||||
|
const payload={a:roadA,b:roadB,step_cost:parseInt($("rCost").value)||0,
|
||||||
|
event_chance:parseFloat($("rEvent").value)||0,waypoints:roadWps};
|
||||||
|
$("roadMsg").className="msg";$("roadMsg").textContent="Kaydediliyor…";
|
||||||
|
const r=await api("/map-tags/roads",{method:"POST",body:JSON.stringify(payload)});
|
||||||
|
if(!r.ok){$("roadMsg").className="msg err";$("roadMsg").textContent=(r.body&&r.body.detail)||("Hata "+r.status);return;}
|
||||||
|
roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");
|
||||||
|
await loadLocsRoads();updateRoadInfo();
|
||||||
|
};
|
||||||
|
async function delRoad(id){
|
||||||
|
if(!confirm("Bu yol silinsin mi?"))return;
|
||||||
|
const r=await api("/map-tags/roads/"+id+"/delete",{method:"POST"});
|
||||||
|
if(!r.ok){alert("Silinemedi ("+r.status+")");return;}
|
||||||
|
roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");
|
||||||
|
await loadLocsRoads();updateRoadInfo();
|
||||||
|
}
|
||||||
|
$("delRoadBtn").onclick=()=>{if(roadEditId)delRoad(roadEditId);};
|
||||||
|
$("closeRoad").onclick=()=>{roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");updateRoadInfo();render();};
|
||||||
|
$("undoWp").onclick=()=>{roadWps.pop();updateRoadInfo();render();};
|
||||||
|
$("cancelRoad").onclick=()=>{roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");updateRoadInfo();render();};
|
||||||
|
|
||||||
|
/* ---- bağla ---- */
|
||||||
|
$("loginBtn").onclick=login;
|
||||||
|
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
|
||||||
|
document.querySelectorAll(".modebtn[data-mode]").forEach(b=>b.onclick=()=>setMode(b.dataset.mode));
|
||||||
|
$("fKind").addEventListener("change",()=>{
|
||||||
|
const k=$("fKind").value;
|
||||||
|
toggleRegionsWrap(k);
|
||||||
|
$("catWrap").classList.toggle("hide",k==="continent");
|
||||||
|
$("fCategory").innerHTML=catOptions(k,"");
|
||||||
|
});
|
||||||
|
// kategori seçilince isim boşsa otomatik öner: "Orman 3"
|
||||||
|
$("fCategory").addEventListener("change",()=>{
|
||||||
|
const c=$("fCategory").value;
|
||||||
|
if(!c||$("fName").value.trim())return;
|
||||||
|
const n=tags.filter(t=>t.category===c).length+1;
|
||||||
|
$("fName").value=catLabel(c)+" "+n;
|
||||||
|
});
|
||||||
|
$("showLabels").onchange=render;
|
||||||
|
$("showRegions").onchange=render;
|
||||||
|
$("showRoads").onchange=render;
|
||||||
|
$("showPois").onchange=render;
|
||||||
|
|
||||||
|
/* ---- tam ekran düzenleme + pan/zoom (yalnız fullscreen'de aktif) ---- */
|
||||||
|
(function(){
|
||||||
|
const area=$("maparea"), view=$("mapview"), zoom=$("mapzoom"), toolbar=document.querySelector(".toolbar");
|
||||||
|
let fsActive=false, scale=1, tx=0, ty=0, MIN=1; const MAX=8;
|
||||||
|
function apply(){ zoom.style.transform=`translate(${tx}px,${ty}px) scale(${scale})`; }
|
||||||
|
function clamp(){
|
||||||
|
const vw=view.clientWidth, vh=view.clientHeight;
|
||||||
|
const cw=vw*scale, ch=zoom.offsetHeight*scale;
|
||||||
|
tx = cw<=vw ? (vw-cw)/2 : Math.max(vw-cw, Math.min(0, tx)); // sığıyorsa ortala
|
||||||
|
ty = ch<=vh ? (vh-ch)/2 : Math.max(vh-ch, Math.min(0, ty));
|
||||||
|
}
|
||||||
|
// tüm haritayı görünüme sığdır (contain), ortala — taban zoom
|
||||||
|
function fit(){
|
||||||
|
const baseH=zoom.offsetHeight;
|
||||||
|
if(!baseH || !view.clientHeight) return;
|
||||||
|
MIN=Math.min(1, view.clientHeight/baseH); // her şeyi görecek kadar uzaklaş
|
||||||
|
scale=MIN; tx=0; ty=0; clamp(); apply();
|
||||||
|
}
|
||||||
|
function zoomAt(cx,cy,ns){
|
||||||
|
ns=Math.max(MIN,Math.min(MAX,ns));
|
||||||
|
const r=view.getBoundingClientRect(), px=cx-r.left, py=cy-r.top;
|
||||||
|
tx=px-(px-tx)*(ns/scale); ty=py-(py-ty)*(ns/scale);
|
||||||
|
scale=ns; clamp(); apply();
|
||||||
|
}
|
||||||
|
function centerZoom(f){ const r=view.getBoundingClientRect(); zoomAt(r.left+view.clientWidth/2, r.top+view.clientHeight/2, scale*f); }
|
||||||
|
|
||||||
|
function fsOpen(){
|
||||||
|
if(fsActive) return; fsActive=true;
|
||||||
|
area.classList.add("fs");
|
||||||
|
toolbar.classList.add("fsfloat"); area.prepend(toolbar); // araçları overlay içine taşı
|
||||||
|
document.body.style.overflow="hidden";
|
||||||
|
requestAnimationFrame(fit);
|
||||||
|
}
|
||||||
|
function fsClose(){
|
||||||
|
if(!fsActive) return; fsActive=false;
|
||||||
|
toolbar.classList.remove("fsfloat"); area.parentNode.insertBefore(toolbar, area); // geri koy
|
||||||
|
area.classList.remove("fs");
|
||||||
|
document.body.style.overflow="";
|
||||||
|
scale=1; tx=0; ty=0; zoom.style.transform=""; // normal görünüme dön
|
||||||
|
}
|
||||||
|
$("fsOpenBtn").onclick=fsOpen;
|
||||||
|
$("fsClose").onclick=fsClose;
|
||||||
|
$("zoomIn").onclick=()=>centerZoom(1.4);
|
||||||
|
$("zoomOut").onclick=()=>centerZoom(1/1.4);
|
||||||
|
$("zoomReset").onclick=fit;
|
||||||
|
document.addEventListener("keydown",e=>{ if(e.key==="Escape"&&fsActive) fsClose(); });
|
||||||
|
|
||||||
|
view.addEventListener("wheel",e=>{ if(!fsActive) return; e.preventDefault(); zoomAt(e.clientX,e.clientY,scale*(e.deltaY<0?1.15:1/1.15)); },{passive:false});
|
||||||
|
let pts=new Map(), startDist=0, startScale=1, lastMid=null;
|
||||||
|
view.addEventListener("touchstart",e=>{
|
||||||
|
if(!fsActive) return;
|
||||||
|
for(const t of e.changedTouches) pts.set(t.identifier,{x:t.clientX,y:t.clientY});
|
||||||
|
if(pts.size===2){ const [a,b]=[...pts.values()]; startDist=Math.hypot(a.x-b.x,a.y-b.y); startScale=scale; lastMid={x:(a.x+b.x)/2,y:(a.y+b.y)/2}; }
|
||||||
|
},{passive:false});
|
||||||
|
view.addEventListener("touchmove",e=>{
|
||||||
|
if(!fsActive) return;
|
||||||
|
for(const t of e.changedTouches) if(pts.has(t.identifier)) pts.set(t.identifier,{x:t.clientX,y:t.clientY});
|
||||||
|
if(pts.size>=2){
|
||||||
|
e.preventDefault();
|
||||||
|
const [a,b]=[...pts.values()];
|
||||||
|
const dist=Math.hypot(a.x-b.x,a.y-b.y), mid={x:(a.x+b.x)/2,y:(a.y+b.y)/2};
|
||||||
|
if(lastMid){ tx+=mid.x-lastMid.x; ty+=mid.y-lastMid.y; } lastMid=mid;
|
||||||
|
if(startDist>0) zoomAt(mid.x,mid.y,startScale*(dist/startDist)); else { clamp(); apply(); }
|
||||||
|
}
|
||||||
|
},{passive:false});
|
||||||
|
function end(e){ for(const t of e.changedTouches) pts.delete(t.identifier); if(pts.size<2){ startDist=0; lastMid=null; } }
|
||||||
|
view.addEventListener("touchend",end); view.addEventListener("touchcancel",end);
|
||||||
|
window.addEventListener("resize",()=>{ if(fsActive) fit(); });
|
||||||
|
})();
|
||||||
|
|
||||||
|
if(TOKEN)showEditor();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
307
assets/npcs.html
Normal file
307
assets/npcs.html
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
<!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 — NPC'ler</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
|
||||||
|
*{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}
|
||||||
|
.wrap{max-width:1100px;margin:0 auto;padding:20px 16px}
|
||||||
|
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
|
||||||
|
h2{font-size:15px;color:var(--gold);margin:0 0 10px}
|
||||||
|
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:16px}
|
||||||
|
input,button,select,textarea{font-size:13px;border-radius:6px;font-family:inherit}
|
||||||
|
input,select,textarea{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:7px 9px}
|
||||||
|
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
|
||||||
|
textarea{width:100%;min-height:70px;resize:vertical;line-height:1.5}
|
||||||
|
.btn{background:var(--gold);color:#1a1300;border:none;padding:8px 16px;font-weight:600;cursor:pointer}
|
||||||
|
.btn.sec{background:#2a3648;color:var(--txt)}
|
||||||
|
.btn.sm{padding:5px 10px;font-size:12px}
|
||||||
|
.btn.link{background:none;border:none;color:var(--muted);cursor:pointer;padding:4px 6px;font-size:12px}
|
||||||
|
.btn.link:hover{color:var(--gold)}
|
||||||
|
.btn.link.del:hover{color:var(--err)}
|
||||||
|
.msg{font-size:13px;margin-top:8px;min-height:16px}
|
||||||
|
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
|
||||||
|
.hide{display:none}
|
||||||
|
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
|
||||||
|
.toolbar input[type=search]{flex:1 1 200px}
|
||||||
|
.count{color:var(--muted);font-size:12px}
|
||||||
|
.locgroup{margin-bottom:6px}
|
||||||
|
.locgroup h3{font-size:14px;color:var(--gold);margin:18px 0 4px;border-bottom:1px solid var(--line);padding-bottom:4px}
|
||||||
|
.npc{border-top:1px solid var(--line);padding:10px 2px}
|
||||||
|
.npc:first-child{border-top:none}
|
||||||
|
.npc .head{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.npc .head b{font-size:14px}
|
||||||
|
.npc .rolepill{font-size:11px;padding:1px 8px;border-radius:10px;background:#243247;color:var(--gold);font-weight:600}
|
||||||
|
.npc .stancepill{font-size:11px;padding:1px 8px;border-radius:10px;font-weight:600}
|
||||||
|
.stance-friendly{background:#1c3326;color:#5fd08a} .stance-hostile{background:#3a1f22;color:#e5534b} .stance-neutral{background:#2a3140;color:#9aa7b6}
|
||||||
|
.npc .lnkline{margin-top:5px;font-size:12px}
|
||||||
|
.npc .lnkline .lbl{color:var(--muted)}
|
||||||
|
.npc .qitem{display:inline-block;background:#243247;color:#cbd5e1;border-radius:8px;padding:1px 7px;margin:2px 3px 0 0;font-size:11px}
|
||||||
|
.npc .qitem.q{background:#2c2440;color:#c9a0ff}
|
||||||
|
.links{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px}
|
||||||
|
@media(max-width:700px){.links{grid-template-columns:1fr}}
|
||||||
|
.links select{width:100%}
|
||||||
|
.hint{font-size:10px;color:var(--muted);font-weight:400}
|
||||||
|
.npc .acts{margin-left:auto;display:flex;gap:2px}
|
||||||
|
.npc .desc{white-space:pre-wrap;word-wrap:break-word;font-size:13px;margin-top:4px}
|
||||||
|
.npc .notes{white-space:pre-wrap;word-wrap:break-word;font-size:12px;color:var(--muted);margin-top:4px;border-left:2px solid var(--line);padding-left:8px}
|
||||||
|
.frm{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:8px}
|
||||||
|
@media(max-width:700px){.frm{grid-template-columns:1fr}}
|
||||||
|
label{font-size:11px;color:var(--muted);display:block;margin-bottom:3px}
|
||||||
|
.frm input,.frm select{width:100%}
|
||||||
|
#toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:#1a2230;border:1px solid var(--gold);color:var(--txt);padding:9px 16px;border-radius:8px;font-size:13px;opacity:0;pointer-events:none;transition:.3s;z-index:9}
|
||||||
|
#toast.on{opacity:1}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="dark" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>🧙 NPC'ler</h1>
|
||||||
|
<div class="sub">Oyun NPC listesi — konum ve temel bilgileriyle. Konumlar map editöründe çizdiğin alanlar (🗺 bölge) ve pinlerdir (📌).</div>
|
||||||
|
|
||||||
|
<div class="card" id="loginCard">
|
||||||
|
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button class="btn" id="loginBtn">Giriş</button>
|
||||||
|
<div class="msg" id="loginMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app" class="hide">
|
||||||
|
<div class="card">
|
||||||
|
<div class="toolbar">
|
||||||
|
<input type="search" id="q" placeholder="NPC, rol veya yer ara…">
|
||||||
|
<select id="fStanceF">
|
||||||
|
<option value="">Tüm taraflar</option>
|
||||||
|
<option value="friendly">🟢 Dost</option>
|
||||||
|
<option value="neutral">⚪ Tarafsız</option>
|
||||||
|
<option value="hostile">🔴 Düşman</option>
|
||||||
|
</select>
|
||||||
|
<select id="fLoc"><option value="">Tüm yerler</option></select>
|
||||||
|
<span class="count" id="cnt"></span>
|
||||||
|
</div>
|
||||||
|
<div id="list"></div>
|
||||||
|
<div class="count" id="empty">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="formCard">
|
||||||
|
<h2 id="formTitle">+ Yeni NPC</h2>
|
||||||
|
<div class="frm">
|
||||||
|
<div><label>İsim *</label><input type="text" id="fName" placeholder="örn. Tacir Baldor"></div>
|
||||||
|
<div><label>Rol</label><input type="text" id="fRole" placeholder="örn. Demirci, Görev veren, Haydut"></div>
|
||||||
|
<div><label>Taraf</label><select id="fStance">
|
||||||
|
<option value="friendly">🟢 Dost</option>
|
||||||
|
<option value="neutral" selected>⚪ Tarafsız</option>
|
||||||
|
<option value="hostile">🔴 Düşman</option>
|
||||||
|
</select></div>
|
||||||
|
<div><label>Yer (alan / pin)</label><select id="fLocSel"><option value="">— yok / gezgin —</option></select></div>
|
||||||
|
</div>
|
||||||
|
<label>Açıklama (görünüş, kişilik, oyundaki işlevi)</label>
|
||||||
|
<textarea id="fDesc"></textarea>
|
||||||
|
<label style="margin-top:8px">Not (iç not — diyalog fikirleri, TODO)</label>
|
||||||
|
<textarea id="fNotes" style="min-height:50px"></textarea>
|
||||||
|
<div class="links">
|
||||||
|
<div>
|
||||||
|
<label>Verdiği görevler <span class="hint">(bir görevi tek NPC verebilir)</span></label>
|
||||||
|
<select id="fQuests" multiple size="5"></select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Sattığı itemlar <span class="hint">(bir item birden çok NPC'de olabilir)</span></label>
|
||||||
|
<input type="search" id="qItemFilter" placeholder="item filtrele…" style="width:100%;margin-bottom:4px">
|
||||||
|
<select id="fItems" multiple size="6"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn" id="saveBtn">Kaydet</button>
|
||||||
|
<button class="btn sec hide" id="cancelBtn">Vazgeç (düzenleme)</button>
|
||||||
|
<div class="msg" id="fMsg" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
let TOKEN=localStorage.getItem("ss_upload_token")||"";
|
||||||
|
let NPCS=[],LOCS=[],CATALOG={quests:[],items:[]},editId=null;
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
const norm=s=>(s||"").toLocaleLowerCase("tr");
|
||||||
|
function esc(s){return (s??"").toString().replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));}
|
||||||
|
function n_ic(kind){return kind==="poi"?"📌":kind==="region"?"🗺":"🚶";}
|
||||||
|
const STANCE={friendly:{ic:"🟢",lbl:"Dost"},hostile:{ic:"🔴",lbl:"Düşman"},neutral:{ic:"⚪",lbl:"Tarafsız"}};
|
||||||
|
let CATS={},VENDOR_CATS=[];
|
||||||
|
function catIcon(k){return k&&CATS[k]?CATS[k].icon:"";}
|
||||||
|
function locLabel(x){return (catIcon(x.category)||(x.kind==="poi"?"📌":"🗺"))+" "+x.name+(x.category&&CATS[x.category]?" — "+CATS[x.category].label:"");}
|
||||||
|
function toast(m,ms=2500){const t=$("toast");t.textContent=m;t.classList.add("on");clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove("on"),ms);}
|
||||||
|
|
||||||
|
async function api(path,opts={}){
|
||||||
|
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
|
||||||
|
const r=await fetch(API+path,opts);
|
||||||
|
let body={};try{body=await r.json();}catch(e){}
|
||||||
|
return {ok:r.ok,status:r.status,body};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(){
|
||||||
|
$("loginMsg").textContent="…";
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);
|
||||||
|
if(window.ssNavRefresh)ssNavRefresh();showApp();}
|
||||||
|
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showApp(){
|
||||||
|
const [n,l,c,cat]=await Promise.all([api("/npcs"),api("/npcs/locations"),api("/npcs/catalog"),api("/map-tags/categories")]);
|
||||||
|
if(!n.ok){
|
||||||
|
if(n.status===401||n.status===403){
|
||||||
|
$("loginMsg").className="msg err";
|
||||||
|
$("loginMsg").textContent=n.status===403?"Bu hesabın NPC yetkisi yok":"Giriş gerekli / oturum bitti";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NPCS=n.body;LOCS=l.ok?l.body:[];CATALOG=c.ok?c.body:{quests:[],items:[]};
|
||||||
|
if(cat.ok){CATS=cat.body.categories||{};VENDOR_CATS=cat.body.vendor||[];}
|
||||||
|
$("loginCard").classList.add("hide");$("app").classList.remove("hide");
|
||||||
|
const opts=LOCS.map(x=>`<option value="${x.id}">${esc(locLabel(x))}${x.continent?" ("+esc(x.continent)+")":""}</option>`).join("");
|
||||||
|
$("fLoc").innerHTML='<option value="">Tüm yerler</option>'+opts+'<option value="none">— yersiz —</option>';
|
||||||
|
$("fLocSel").innerHTML='<option value="">— yok / gezgin —</option>'+opts;
|
||||||
|
buildItemOptions();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
async function reload(){
|
||||||
|
const [r,c]=await Promise.all([api("/npcs"),api("/npcs/catalog")]);
|
||||||
|
NPCS=r.ok?r.body:[];if(c.ok)CATALOG=c.body;render();
|
||||||
|
}
|
||||||
|
function buildItemOptions(){
|
||||||
|
$("fItems").innerHTML=CATALOG.items.map(i=>
|
||||||
|
`<option value="${i.id}" data-s="${norm(i.name)+' '+norm(i.type)+' '+norm(i.code)}">${esc(i.name)} — ${esc(i.type||"")}</option>`).join("");
|
||||||
|
}
|
||||||
|
function filterItemOptions(){
|
||||||
|
const q=norm($("qItemFilter").value.trim());
|
||||||
|
for(const o of $("fItems").options)o.hidden=q&&!o.dataset.s.includes(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
const q=norm($("q").value.trim()),fl=$("fLoc").value,fs=$("fStanceF").value;
|
||||||
|
const rows=NPCS.filter(n=>{
|
||||||
|
if(fs&&n.stance!==fs)return false;
|
||||||
|
if(fl==="none"&&n.map_tag_id)return false;
|
||||||
|
if(fl&&fl!=="none"&&n.map_tag_id!==+fl)return false;
|
||||||
|
if(!q)return true;
|
||||||
|
return norm(n.name).includes(q)||norm(n.role).includes(q)||norm(n.location_name).includes(q)||norm(n.description).includes(q);
|
||||||
|
});
|
||||||
|
$("cnt").textContent=`${rows.length} / ${NPCS.length} NPC`;
|
||||||
|
$("empty").style.display=rows.length?"none":"block";
|
||||||
|
$("empty").textContent=NPCS.length?"Eşleşen NPC yok.":"Henüz NPC yok — aşağıdan ekle.";
|
||||||
|
// yere göre grupla
|
||||||
|
const groups={},order=[],gkind={},gcat={};
|
||||||
|
for(const n of rows){
|
||||||
|
const k=n.location_name||"🚶 Yersiz / Gezgin";
|
||||||
|
if(!groups[k]){groups[k]=[];order.push(k);gkind[k]=n.location_kind;gcat[k]=n.location_category;}
|
||||||
|
groups[k].push(n);
|
||||||
|
}
|
||||||
|
let html="";
|
||||||
|
for(const k of order){
|
||||||
|
const ic=catIcon(gcat[k])||n_ic(gkind[k]);
|
||||||
|
html+=`<div class="locgroup"><h3>${ic} ${esc(k)} <span class="count">(${groups[k].length})</span></h3>`;
|
||||||
|
for(const n of groups[k]){
|
||||||
|
const st=STANCE[n.stance]||STANCE.neutral;
|
||||||
|
const qh=(n.quests||[]).map(q=>`<span class="qitem q">🗡 ${esc(q.title)}</span>`).join("");
|
||||||
|
const ih=(n.sells||[]).map(i=>`<span class="qitem">${esc(i.name)}</span>`).join("");
|
||||||
|
html+=`<div class="npc" data-id="${n.id}">
|
||||||
|
<div class="head"><b>${esc(n.name)}</b>
|
||||||
|
${n.role?`<span class="rolepill">${esc(n.role)}</span>`:""}
|
||||||
|
<span class="stancepill stance-${n.stance}">${st.ic} ${st.lbl}</span>
|
||||||
|
<span class="acts">
|
||||||
|
<button class="btn link edit" data-id="${n.id}">düzenle</button>
|
||||||
|
<button class="btn link del delx" data-id="${n.id}">sil</button>
|
||||||
|
</span></div>
|
||||||
|
${n.description?`<div class="desc">${esc(n.description)}</div>`:""}
|
||||||
|
${n.notes?`<div class="notes">${esc(n.notes)}</div>`:""}
|
||||||
|
${qh?`<div class="lnkline"><span class="lbl">Görevler:</span> ${qh}</div>`:""}
|
||||||
|
${ih?`<div class="lnkline"><span class="lbl">Satar (${n.sells.length}):</span> ${ih}</div>`:""}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
html+=`</div>`;
|
||||||
|
}
|
||||||
|
$("list").innerHTML=html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(n){
|
||||||
|
editId=n?n.id:null;
|
||||||
|
$("formTitle").textContent=n?("✏️ Düzenle: "+n.name):"+ Yeni NPC";
|
||||||
|
$("fName").value=n?n.name:"";
|
||||||
|
$("fRole").value=n?(n.role||""):"";
|
||||||
|
$("fStance").value=n?(n.stance||"neutral"):"neutral";
|
||||||
|
$("fLocSel").value=n&&n.map_tag_id?n.map_tag_id:"";
|
||||||
|
$("fDesc").value=n?(n.description||""):"";
|
||||||
|
$("fNotes").value=n?(n.notes||""):"";
|
||||||
|
// quest seçimi: başka NPC'ye bağlı görevler disabled; bu NPC'ninkiler seçili
|
||||||
|
const mine=new Set(n?(n.quests||[]).map(q=>q.id):[]);
|
||||||
|
$("fQuests").innerHTML=CATALOG.quests.map(q=>{
|
||||||
|
const takenByOther=q.giver_npc_id&&(!n||q.giver_npc_id!==n.id);
|
||||||
|
const sel=mine.has(q.id)?" selected":"";
|
||||||
|
const dis=takenByOther?" disabled":"";
|
||||||
|
const suf=takenByOther?" — başka NPC'de":"";
|
||||||
|
return `<option value="${q.id}"${sel}${dis}>${esc(q.title)}${suf}</option>`;
|
||||||
|
}).join("");
|
||||||
|
// item seçimi
|
||||||
|
const sells=new Set(n?(n.sells||[]).map(i=>i.id):[]);
|
||||||
|
for(const o of $("fItems").options)o.selected=sells.has(+o.value);
|
||||||
|
$("qItemFilter").value="";filterItemOptions();
|
||||||
|
$("cancelBtn").classList.toggle("hide",!n);
|
||||||
|
if(n)$("formCard").scrollIntoView({behavior:"smooth",block:"nearest"});
|
||||||
|
}
|
||||||
|
function selectedIds(sel){return Array.from($(sel).selectedOptions).map(o=>+o.value);}
|
||||||
|
|
||||||
|
document.addEventListener("click",async e=>{
|
||||||
|
if(e.target.classList.contains("edit")){
|
||||||
|
const n=NPCS.find(x=>x.id===+e.target.dataset.id);
|
||||||
|
if(n)fillForm(n);
|
||||||
|
}
|
||||||
|
if(e.target.classList.contains("delx")){
|
||||||
|
const n=NPCS.find(x=>x.id===+e.target.dataset.id);
|
||||||
|
if(!n||!confirm(`"${n.name}" silinsin mi?`))return;
|
||||||
|
const r=await api("/npcs/"+n.id+"/delete",{method:"POST"});
|
||||||
|
if(r.ok){toast("Silindi");if(editId===n.id)fillForm(null);await reload();}
|
||||||
|
else toast("⚠ "+(r.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("saveBtn").onclick=async()=>{
|
||||||
|
const name=$("fName").value.trim();
|
||||||
|
if(!name){$("fMsg").className="msg err";$("fMsg").textContent="İsim gerekli";return;}
|
||||||
|
$("fMsg").className="msg";$("fMsg").textContent="…";
|
||||||
|
const payload={name,role:$("fRole").value.trim()||null,stance:$("fStance").value,
|
||||||
|
map_tag_id:$("fLocSel").value?+$("fLocSel").value:null,
|
||||||
|
description:$("fDesc").value.trim()||null,notes:$("fNotes").value.trim()||null};
|
||||||
|
const r=editId?await api("/npcs/"+editId,{method:"POST",body:JSON.stringify(payload)})
|
||||||
|
:await api("/npcs",{method:"POST",body:JSON.stringify(payload)});
|
||||||
|
if(!r.ok){$("fMsg").className="msg err";$("fMsg").textContent=r.body.detail||"Hata";return;}
|
||||||
|
const npcId=r.body.id;
|
||||||
|
// quest + item bağlarını ayarla
|
||||||
|
const qr=await api("/npcs/"+npcId+"/quests",{method:"POST",body:JSON.stringify({ids:selectedIds("fQuests")})});
|
||||||
|
if(!qr.ok){$("fMsg").className="msg err";$("fMsg").textContent="Görev bağı: "+(qr.body.detail||"Hata");await reload();return;}
|
||||||
|
const ir=await api("/npcs/"+npcId+"/items",{method:"POST",body:JSON.stringify({ids:selectedIds("fItems")})});
|
||||||
|
if(!ir.ok){$("fMsg").className="msg err";$("fMsg").textContent="Item bağı: "+(ir.body.detail||"Hata");await reload();return;}
|
||||||
|
$("fMsg").className="msg ok";$("fMsg").textContent=editId?"Güncellendi":"Eklendi";
|
||||||
|
setTimeout(()=>$("fMsg").textContent="",1500);
|
||||||
|
fillForm(null);
|
||||||
|
await reload();
|
||||||
|
};
|
||||||
|
$("cancelBtn").onclick=()=>fillForm(null);
|
||||||
|
$("q").addEventListener("input",render);
|
||||||
|
$("fLoc").addEventListener("change",render);
|
||||||
|
$("fStanceF").addEventListener("change",render);
|
||||||
|
$("qItemFilter").addEventListener("input",filterItemOptions);
|
||||||
|
$("loginBtn").onclick=login;
|
||||||
|
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
|
||||||
|
if(TOKEN)showApp();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
168
assets/prompts.md
Normal file
168
assets/prompts.md
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
# Stepstead — Görsel Üretim Promptları
|
||||||
|
|
||||||
|
Güncelleme: 2026-07-06 (gece). **Tamamlananlar listeden çıkarıldı** (ana atlas,
|
||||||
|
15 lokasyon kartının TAMAMI, 15 pinin TAMAMI, oyuncu bayrağı, 3 olay kartı).
|
||||||
|
Her sahne promptuna **ana atlası referans görsel olarak ekle**.
|
||||||
|
Lokasyon kartları 16:9; pin/ikon/portre görselleri 1:1 ve **şeffaf arka plan** iste.
|
||||||
|
Hepsinde yazı/etiket YOK.
|
||||||
|
|
||||||
|
## Ortak ön ek — sahne görselleri (lokasyon/olay kartı)
|
||||||
|
|
||||||
|
```
|
||||||
|
Using the attached world atlas as the style and geography reference, render a detailed
|
||||||
|
zoomed-in regional illustration in the exact same hand-drawn parchment cartography style —
|
||||||
|
same aged parchment texture, fine ink linework, rich watercolor tints, muted earthy palette
|
||||||
|
with warm golden accents, same ornate border frame. No text, no labels, no letters. Region:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Olay Kartı İllüstrasyonları (17 kaldı + çerçeve + d20)
|
||||||
|
|
||||||
|
Ortak şablon (3:4 dikey):
|
||||||
|
|
||||||
|
```
|
||||||
|
Using the attached world atlas as the style reference, render a fantasy event card
|
||||||
|
illustration in the same ink-and-watercolor style, portrait 3:4, ornate golden card
|
||||||
|
frame: [SAHNE]. Dramatic but painterly, no text.
|
||||||
|
```
|
||||||
|
|
||||||
|
| Kart | [SAHNE] |
|
||||||
|
|---|---|
|
||||||
|
| Gezgin Tacir | a cheerful traveling merchant with an overloaded mule, holding up a trinket by a forest road |
|
||||||
|
| Dev Ayı | a huge brown bear rearing up on a forest trail, morning mist |
|
||||||
|
| Mantar Halkası | a perfect circle of glowing mushrooms in a dark clearing, faint fairy light |
|
||||||
|
| Eski Harabe | crumbling moss-covered stone ruins with a broken archway, ivy, low sun |
|
||||||
|
| Nehir Geçidi | a fast river crossing with slippery stepping stones and a frayed rope line |
|
||||||
|
| Gizemli Yaşlı | a hooded elder with a lantern staff standing at a foggy crossroads |
|
||||||
|
| Kırık Araba | a merchant cart with a shattered wheel, spilled crates, an anxious driver |
|
||||||
|
| Sağanak | a traveler bracing under a violent downpour, cloak whipping in wind |
|
||||||
|
| Şanslı Buluş | a coin pouch and a gleaming ring lying in the grass beside the road |
|
||||||
|
| Kurt Sürüsü | a wolf pack blocking a forest road at dusk, the alpha baring its teeth |
|
||||||
|
| Vahşi Domuz | a huge scarred wild boar charging through undergrowth |
|
||||||
|
| Taze Geyik İzi | fresh deer tracks in soft mud leading into golden meadow grass |
|
||||||
|
| Mağara Örümceği | a giant spider descending in a torch-lit mine tunnel, thick webs |
|
||||||
|
| Korsan Artığı | a washed-up pirate chest among seaweed and wreckage on a rocky shore |
|
||||||
|
| Yaşlı Avcı | a grizzled old hunter by a small campfire, offering advice, bow on his back |
|
||||||
|
| Terk Edilmiş Ağ | a torn fishing net tangled on lake reeds, a few fish still caught |
|
||||||
|
| Ayı İni | a dark bear den opening between giant tree roots, bones scattered |
|
||||||
|
|
||||||
|
**Kart çerçevesi (şeffaf, içi boş):**
|
||||||
|
```
|
||||||
|
An ornate fantasy trading card frame, portrait 3:4: aged gold filigree border with
|
||||||
|
carved corner medallions, inner window fully transparent/empty, parchment-toned outer
|
||||||
|
edge, painterly style matching the attached atlas. Transparent background inside and
|
||||||
|
outside the frame, no text.
|
||||||
|
```
|
||||||
|
|
||||||
|
**d20 zarı (2 durum):**
|
||||||
|
```
|
||||||
|
A polished d20 dice icon in painterly fantasy style, [emerald green glowing / cracked
|
||||||
|
dark red], slight 3/4 angle, soft glow, blank faces without numbers, transparent
|
||||||
|
background, no text, game UI icon, 1:1.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Karakter / NPC Görselleri
|
||||||
|
|
||||||
|
Ortak portre şablonu (1:1, omuz üstü):
|
||||||
|
|
||||||
|
```
|
||||||
|
A painterly fantasy character portrait, bust (head and shoulders), warm ink-and-watercolor
|
||||||
|
style matching the attached atlas, muted earthy palette with golden accents, soft parchment
|
||||||
|
vignette background, no text, 1:1. Character: [KARAKTER]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Portre | [KARAKTER] |
|
||||||
|
|---|---|
|
||||||
|
| Oyuncu avatarı (E) | a determined young wanderer in a hooded travel cloak with a leather satchel and walking staff, friendly eyes |
|
||||||
|
| Oyuncu avatarı (K) | a confident young female explorer with braided hair, travel cloak and a map case slung across her shoulder |
|
||||||
|
| Vendor NPC | a plump jovial market merchant with a magnificent mustache, fur-trimmed vest, holding a brass scale |
|
||||||
|
| Gezgin Tacir | a wiry traveling peddler with a wide-brimmed hat and a backpack full of pots and trinkets |
|
||||||
|
| Yaşlı Avcı | a weathered old hunter with a grey beard, fur hood, a scar over one eyebrow, longbow visible |
|
||||||
|
| Gizemli Yaşlı | a mysterious hooded elder, face half in shadow, glowing lantern light from below |
|
||||||
|
| Haydut | a masked highway bandit with a crooked grin, leather armor, dagger at the collar |
|
||||||
|
|
||||||
|
**Tam boy oyuncu figürü (harita/HUD token'ı):**
|
||||||
|
```
|
||||||
|
A full-body chibi-proportioned fantasy wanderer character, standing pose, hooded travel
|
||||||
|
cloak, satchel and walking staff, painterly style matching the attached atlas, clean
|
||||||
|
silhouette, transparent background, no text. Suitable as a small game map token.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skill İkonları (10)
|
||||||
|
|
||||||
|
Şablon:
|
||||||
|
```
|
||||||
|
A round fantasy skill icon, painterly ink-and-watercolor style matching the attached
|
||||||
|
atlas, gold ring border, parchment inner background, transparent outside the circle,
|
||||||
|
no text, 1:1. Icon subject: [KONU]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Skill | [KONU] |
|
||||||
|
|---|---|
|
||||||
|
| Madencilik | a crossed pickaxe and a glowing copper ore chunk |
|
||||||
|
| Balıkçılık | a fishing rod with a leaping fish |
|
||||||
|
| Toplama | a bundle of herbs and berries in a small basket |
|
||||||
|
| Ağaç Kesme | a woodcutter's axe embedded in an oak stump |
|
||||||
|
| Avcılık | a longbow with a nocked arrow and an antler |
|
||||||
|
| Savaş | two crossed swords over a small round shield |
|
||||||
|
| Demircilik | a blacksmith hammer striking a glowing anvil |
|
||||||
|
| Simya | a bubbling potion flask with drifting sparkles |
|
||||||
|
| Aşçılık | a steaming cooking pot over a small fire |
|
||||||
|
| İnşaat | a mallet, a saw and a wooden plank frame |
|
||||||
|
|
||||||
|
## Item İkonları (aile bazlı sprite sheet)
|
||||||
|
|
||||||
|
Şablon — her aile için tek prompt, grid iste:
|
||||||
|
```
|
||||||
|
A sprite sheet of [N] fantasy game inventory icons in a consistent painterly
|
||||||
|
ink-and-watercolor style matching the attached atlas, arranged in a neat grid on a
|
||||||
|
transparent background, equal spacing, each icon centered in its cell, no text, no frames.
|
||||||
|
Icons: [LİSTE]
|
||||||
|
```
|
||||||
|
|
||||||
|
Örnek aileler:
|
||||||
|
- **Odunlar:** `oak log, birch log, pine log, dark walnut log, ebony log — stacked cut logs, tier color darkening left to right`
|
||||||
|
- **Cevherler:** `copper ore, tin ore, iron ore, silver ore, gold ore — rough rock chunks with visible metal veins`
|
||||||
|
- **Külçeler:** `copper ingot, bronze ingot, iron ingot, silver ingot, gold ingot — stacked metal bars`
|
||||||
|
- **Otlar:** `mint sprig, sage bundle, lavender stems, chamomile flowers, wild garlic, nettle leaves`
|
||||||
|
- **Balıklar:** `carp, trout, perch, pike, salmon — side view, subtle scale detail`
|
||||||
|
- **Et & deri:** `rabbit meat, venison cut, boar meat, wolf pelt, bear hide`
|
||||||
|
- **Tool'lar:** `stone axe, copper axe, iron axe, stone pickaxe, copper pickaxe, iron pickaxe, simple fishing rod, sturdy fishing rod, masterwork fishing rod`
|
||||||
|
- **Bootstrap:** `small grey stone, wooden stick, coiled green vine`
|
||||||
|
- **Consumable:** `steaming mint tea cup, honey bread, dried meat strips, glowing stamina potion`
|
||||||
|
|
||||||
|
## Sistem / UI Görselleri
|
||||||
|
|
||||||
|
- **Kamp göstergesi:** `A small pitched canvas camp tent with a tiny campfire, painterly game icon, transparent background, no text, 1:1.`
|
||||||
|
- **Adım ikonu:** `A single boot footprint icon, gold on transparent background, painterly, clean silhouette, game UI, 1:1.`
|
||||||
|
- **Gold ikonu:** `A small pile of three gold coins with an embossed compass rose, painterly game icon, transparent background, 1:1.`
|
||||||
|
- **Günlük ödül:** `A wrapped gift chest with a glowing ribbon, painterly fantasy game icon, transparent background, 1:1.`
|
||||||
|
- **Streak alevi:** `A small stylized orange-gold flame icon, painterly, transparent background, 1:1.`
|
||||||
|
- **Liderlik rozetleri:** `Three round laurel-wreath rank medals side by side: gold, silver, bronze — same design, painterly, transparent background, no numbers, no text.`
|
||||||
|
- **Buton seti:** `A fantasy game UI button set on a transparent background: a wide rounded carved-wood button with gold trim in three states side by side — normal, pressed (darker, inset), disabled (desaturated). Painterly, no text.`
|
||||||
|
- **Panel çerçevesi (9-slice):** `A fantasy game UI panel frame suitable for 9-slice scaling: dark stained wood border with gold corner fittings, fully transparent empty center, painterly, no text.`
|
||||||
|
- **Progress bar:** `A fantasy game UI progress bar on a transparent background: ornate gold end caps, dark leather track, glowing amber fill shown at 60%, painterly, no text.`
|
||||||
|
|
||||||
|
## Marka
|
||||||
|
|
||||||
|
- **Logo amblemi:** `A fantasy game emblem WITHOUT any letters: a winding path of golden footprints crossing a stylized parchment map toward a distant castle silhouette, circular emblem, ink-and-watercolor, transparent background.` *(Yazı ayrıca tipografiyle bindirilecek.)*
|
||||||
|
- **Splash:** `A wide fantasy landscape: a lone wanderer with a walking staff on a hilltop path at sunrise, looking over a vast hand-drawn map world blending into real terrain — forests, a harbor, a distant volcano; painterly ink-and-watercolor, warm golden light, no text, 16:9.`
|
||||||
|
- **App ikonu:** `A single gold map pin with a tiny footprint symbol in its circular window, painterly fantasy style, bold and readable at small size, flat warm parchment background filling the square, no text, 1:1.`
|
||||||
|
|
||||||
|
## Şablonlar (sonrası için)
|
||||||
|
|
||||||
|
**Zindan iç haritası (üstten):**
|
||||||
|
```
|
||||||
|
Using the attached world atlas as the style reference, render a top-down dungeon floor map
|
||||||
|
in the same hand-drawn parchment style: stone corridors and chambers drawn as clean ink
|
||||||
|
outlines, cracked flagstone floors, [a spider nest chamber / a bear den / flooded tunnels],
|
||||||
|
torch sconces, rubble, a clearly visible entrance at the bottom and a treasure chamber at
|
||||||
|
the top. No text, no labels, no grid numbers.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Kasaba/şehir iç haritası (üstten):**
|
||||||
|
```
|
||||||
|
Using the attached world atlas as the style reference, render a top-down town map in the
|
||||||
|
same hand-drawn parchment style: individual buildings with detailed rooftops, a central
|
||||||
|
plaza, market street, [harbor district / temple hill / craftsmen quarter], garden plots,
|
||||||
|
perimeter wall with gates, winding streets. No text, no labels.
|
||||||
|
```
|
||||||
300
assets/quests.html
Normal file
300
assets/quests.html
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
<!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 — Görevler</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
|
||||||
|
*{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}
|
||||||
|
.wrap{max-width:1100px;margin:0 auto;padding:20px 16px}
|
||||||
|
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
|
||||||
|
h2{font-size:15px;color:var(--gold);margin:0 0 10px}
|
||||||
|
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:16px}
|
||||||
|
input,button,select,textarea{font-size:13px;border-radius:6px;font-family:inherit}
|
||||||
|
input,select,textarea{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:7px 9px}
|
||||||
|
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
|
||||||
|
textarea{width:100%;min-height:60px;resize:vertical;line-height:1.5}
|
||||||
|
.btn{background:var(--gold);color:#1a1300;border:none;padding:8px 16px;font-weight:600;cursor:pointer}
|
||||||
|
.btn.sec{background:#2a3648;color:var(--txt)}
|
||||||
|
.btn.sm{padding:5px 10px;font-size:12px}
|
||||||
|
.btn.link{background:none;border:none;color:var(--muted);cursor:pointer;padding:4px 6px;font-size:12px}
|
||||||
|
.btn.link:hover{color:var(--gold)}
|
||||||
|
.btn.link.del:hover{color:var(--err)}
|
||||||
|
.msg{font-size:13px;margin-top:8px;min-height:16px}
|
||||||
|
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
|
||||||
|
.hide{display:none}
|
||||||
|
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
|
||||||
|
.toolbar input[type=search]{flex:1 1 200px}
|
||||||
|
.count{color:var(--muted);font-size:12px}
|
||||||
|
.typegroup h3{font-size:14px;color:var(--gold);margin:18px 0 4px;border-bottom:1px solid var(--line);padding-bottom:4px}
|
||||||
|
.quest{border-top:1px solid var(--line);padding:10px 2px}
|
||||||
|
.quest:first-child{border-top:none}
|
||||||
|
.quest .head{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.quest .head b{font-size:14px}
|
||||||
|
.quest .pill{font-size:11px;padding:1px 8px;border-radius:10px;background:#243247;color:var(--gold);font-weight:600}
|
||||||
|
.quest .pill.main{background:#3a2d12;color:#f0c651}
|
||||||
|
.quest .pill.side{background:#22303f;color:#7fb3e8}
|
||||||
|
.quest .acts{margin-left:auto;display:flex;gap:2px}
|
||||||
|
.quest .summary{white-space:pre-wrap;word-wrap:break-word;font-size:13px;margin-top:4px}
|
||||||
|
.quest .geo{font-size:12px;color:var(--muted);margin-top:5px;display:flex;gap:6px;flex-wrap:wrap}
|
||||||
|
.quest .geo span{background:#182231;border:1px solid var(--line);border-radius:6px;padding:2px 8px}
|
||||||
|
.quest .objs{margin:6px 0 0;padding-left:18px;font-size:12.5px}
|
||||||
|
.quest .objs li{margin:1px 0}
|
||||||
|
.quest .author{color:#7fd0a0;font-weight:600;font-size:12px}
|
||||||
|
.frm{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:8px}
|
||||||
|
@media(max-width:700px){.frm{grid-template-columns:1fr}}
|
||||||
|
label{font-size:11px;color:var(--muted);display:block;margin-bottom:3px}
|
||||||
|
.frm input,.frm select{width:100%}
|
||||||
|
.objrow{display:grid;grid-template-columns:1fr 1fr auto;gap:6px;margin-bottom:6px;align-items:center}
|
||||||
|
@media(max-width:700px){.objrow{grid-template-columns:1fr}}
|
||||||
|
.objrow select,.objrow input{width:100%}
|
||||||
|
.objx{background:#2a3648;color:var(--err);border:none;border-radius:6px;padding:6px 10px;cursor:pointer;font-weight:600}
|
||||||
|
#toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:#1a2230;border:1px solid var(--gold);color:var(--txt);padding:9px 16px;border-radius:8px;font-size:13px;opacity:0;pointer-events:none;transition:.3s;z-index:9}
|
||||||
|
#toast.on{opacity:1}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="dark" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>🗡 Görevler</h1>
|
||||||
|
<div class="sub">Ana ve yan görevler — görevi veren NPC, kıta/bölge, veriliş noktası ve nerede yapılacağı (çok noktalı) ile.</div>
|
||||||
|
|
||||||
|
<div class="card" id="loginCard">
|
||||||
|
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button class="btn" id="loginBtn">Giriş</button>
|
||||||
|
<div class="msg" id="loginMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app" class="hide">
|
||||||
|
<div class="card">
|
||||||
|
<div class="toolbar">
|
||||||
|
<input type="search" id="q" placeholder="Görev, NPC, bölge ara…">
|
||||||
|
<select id="fType"><option value="">Tüm türler</option><option value="main">Ana görev</option><option value="side">Yan görev</option></select>
|
||||||
|
<span class="count" id="cnt"></span>
|
||||||
|
</div>
|
||||||
|
<div id="list"></div>
|
||||||
|
<div class="count" id="empty">Yükleniyor…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="formCard">
|
||||||
|
<h2 id="formTitle">+ Yeni Görev</h2>
|
||||||
|
<div class="frm">
|
||||||
|
<div><label>Başlık *</label><input type="text" id="fTitle" placeholder="örn. Kayıp Kervan"></div>
|
||||||
|
<div><label>Tür</label><select id="fTypeSel"><option value="side">Yan görev</option><option value="main">Ana görev</option></select></div>
|
||||||
|
<div><label>Görevi veren NPC</label><select id="fGiver"><option value="">— yok —</option></select></div>
|
||||||
|
</div>
|
||||||
|
<div class="frm">
|
||||||
|
<div><label>Kıta</label><select id="fCont"><option value="">— yok —</option></select></div>
|
||||||
|
<div><label>Bölge</label><select id="fRegion"><option value="">— yok —</option></select></div>
|
||||||
|
<div><label>Veriliş noktası (alan / pin)</label><select id="fLoc"><option value="">— yok —</option></select></div>
|
||||||
|
</div>
|
||||||
|
<label>Açıklama / özet</label>
|
||||||
|
<textarea id="fSummary" placeholder="Görevin konusu, hedefi…"></textarea>
|
||||||
|
|
||||||
|
<label style="margin-top:10px">Nerede yapılacak? (çok noktalı — istediğin kadar ekle)</label>
|
||||||
|
<div id="objList"></div>
|
||||||
|
<button class="btn sec sm" id="addObjBtn" type="button">+ Nokta ekle</button>
|
||||||
|
|
||||||
|
<div style="margin-top:12px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn" id="saveBtn">Kaydet</button>
|
||||||
|
<button class="btn sec hide" id="cancelBtn">Vazgeç</button>
|
||||||
|
<div class="msg" id="fMsg" style="margin:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
let TOKEN=localStorage.getItem("ss_upload_token")||"";
|
||||||
|
let QUESTS=[],META={npcs:[],places:[],regions:[],continents:[]},editId=null;
|
||||||
|
let ME={id:null,admin:false};
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
const norm=s=>(s||"").toLocaleLowerCase("tr");
|
||||||
|
function esc(s){return (s??"").toString().replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));}
|
||||||
|
function toast(m,ms=2500){const t=$("toast");t.textContent=m;t.classList.add("on");clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove("on"),ms);}
|
||||||
|
function canEdit(q){return ME.admin||(q.author_id!=null&&q.author_id===ME.id);}
|
||||||
|
|
||||||
|
async function api(path,opts={}){
|
||||||
|
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
|
||||||
|
const r=await fetch(API+path,opts);
|
||||||
|
let body={};try{body=await r.json();}catch(e){}
|
||||||
|
return {ok:r.ok,status:r.status,body};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(){
|
||||||
|
$("loginMsg").textContent="…";
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);
|
||||||
|
if(window.ssNavRefresh)ssNavRefresh();showApp();}
|
||||||
|
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showApp(){
|
||||||
|
const [q,m,me]=await Promise.all([api("/quests"),api("/quests/meta"),api("/auth/me")]);
|
||||||
|
if(!q.ok){
|
||||||
|
if(q.status===401||q.status===403){
|
||||||
|
$("loginMsg").className="msg err";
|
||||||
|
$("loginMsg").textContent=q.status===403?"Bu hesabın görev (lore) yetkisi yok":"Giriş gerekli / oturum bitti";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QUESTS=q.body;META=m.ok?m.body:META;
|
||||||
|
if(me.ok){ME.id=me.body.id;ME.admin=me.body.is_admin||(me.body.permissions||[]).includes("*");}
|
||||||
|
$("loginCard").classList.add("hide");$("app").classList.remove("hide");
|
||||||
|
fillMetaSelects();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
async function reload(){const r=await api("/quests");QUESTS=r.ok?r.body:[];render();}
|
||||||
|
|
||||||
|
function locOpts(sel){
|
||||||
|
return '<option value="">— yok —</option>'+
|
||||||
|
META.places.map(l=>`<option value="${l.id}"${sel==l.id?" selected":""}>${l.kind==="poi"?"📌":"🗺"} ${esc(l.name)}${l.continent?" ("+esc(l.continent)+")":""}</option>`).join("");
|
||||||
|
}
|
||||||
|
function fillMetaSelects(){
|
||||||
|
$("fGiver").innerHTML='<option value="">— yok —</option>'+
|
||||||
|
META.npcs.map(n=>`<option value="${n.id}">${esc(n.name)}${n.location_name?" · "+esc(n.location_name):""}</option>`).join("");
|
||||||
|
$("fCont").innerHTML='<option value="">— yok —</option>'+
|
||||||
|
META.continents.map(c=>`<option value="${esc(c)}">${esc(c)}</option>`).join("");
|
||||||
|
$("fLoc").innerHTML=locOpts("");
|
||||||
|
refreshRegionSel("");
|
||||||
|
}
|
||||||
|
function refreshRegionSel(cont,sel){
|
||||||
|
const list=META.regions.filter(r=>!cont||r.continent===cont);
|
||||||
|
$("fRegion").innerHTML='<option value="">— yok —</option>'+
|
||||||
|
list.map(r=>`<option value="${r.id}"${sel==r.id?" selected":""}>${esc(r.name)}${r.continent&&!cont?" ("+esc(r.continent)+")":""}</option>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addObjRow(loc,note){
|
||||||
|
const div=document.createElement("div");div.className="objrow";
|
||||||
|
div.innerHTML=`<select class="objLoc">${locOpts(loc||"")}</select>
|
||||||
|
<input type="text" class="objNote" placeholder="ne yapılacak? (ops.)" value="${esc(note||"")}">
|
||||||
|
<button type="button" class="objx">✕</button>`;
|
||||||
|
div.querySelector(".objx").onclick=()=>div.remove();
|
||||||
|
$("objList").appendChild(div);
|
||||||
|
}
|
||||||
|
function readObjs(){
|
||||||
|
return [...$("objList").querySelectorAll(".objrow")].map(r=>({
|
||||||
|
map_tag_id:r.querySelector(".objLoc").value?+r.querySelector(".objLoc").value:null,
|
||||||
|
note:r.querySelector(".objNote").value.trim()||null
|
||||||
|
})).filter(o=>o.map_tag_id!=null||o.note);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
const qq=norm($("q").value.trim()),ft=$("fType").value;
|
||||||
|
const rows=QUESTS.filter(x=>{
|
||||||
|
if(ft&&x.quest_type!==ft)return false;
|
||||||
|
if(!qq)return true;
|
||||||
|
return norm(x.title).includes(qq)||norm(x.summary).includes(qq)||norm(x.giver_npc_name).includes(qq)
|
||||||
|
||norm(x.region_name).includes(qq)||norm(x.continent).includes(qq)||norm(x.location_name).includes(qq);
|
||||||
|
});
|
||||||
|
$("cnt").textContent=`${rows.length} / ${QUESTS.length} görev`;
|
||||||
|
$("empty").style.display=rows.length?"none":"block";
|
||||||
|
$("empty").textContent=QUESTS.length?"Eşleşen görev yok.":"Henüz görev yok — aşağıdan ekle.";
|
||||||
|
const groups={main:[],side:[]};
|
||||||
|
for(const x of rows)(groups[x.quest_type]||groups.side).push(x);
|
||||||
|
const titles={main:"⭐ Ana Görevler",side:"🗡 Yan Görevler"};
|
||||||
|
let html="";
|
||||||
|
for(const t of ["main","side"]){
|
||||||
|
if(!groups[t].length)continue;
|
||||||
|
html+=`<div class="typegroup"><h3>${titles[t]} <span class="count">(${groups[t].length})</span></h3>`;
|
||||||
|
for(const x of groups[t])html+=questHtml(x);
|
||||||
|
html+=`</div>`;
|
||||||
|
}
|
||||||
|
$("list").innerHTML=html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function questHtml(x){
|
||||||
|
const geo=[];
|
||||||
|
if(x.giver_npc_name)geo.push(`🧙 ${esc(x.giver_npc_name)}`);
|
||||||
|
if(x.continent)geo.push(`🌍 ${esc(x.continent)}`);
|
||||||
|
if(x.region_name)geo.push(`🗺 ${esc(x.region_name)}`);
|
||||||
|
if(x.location_name)geo.push(`📍 Veriliş: ${esc(x.location_name)}`);
|
||||||
|
const objs=(x.objectives||[]).map(o=>{
|
||||||
|
const parts=[];
|
||||||
|
if(o.location_name)parts.push((o.location_kind==="poi"?"📌 ":"🗺 ")+esc(o.location_name));
|
||||||
|
if(o.note)parts.push(esc(o.note));
|
||||||
|
return parts.length?`<li>${parts.join(" — ")}</li>`:"";
|
||||||
|
}).join("");
|
||||||
|
return `<div class="quest" data-id="${x.id}">
|
||||||
|
<div class="head"><b>${esc(x.title)}</b>
|
||||||
|
<span class="pill ${x.quest_type}">${x.quest_type==="main"?"Ana":"Yan"}</span>
|
||||||
|
${x.author?`<span class="author">✍ ${esc(x.author)}</span>`:""}
|
||||||
|
${canEdit(x)?`<span class="acts">
|
||||||
|
<button class="btn link edit" data-id="${x.id}">düzenle</button>
|
||||||
|
<button class="btn link del delx" data-id="${x.id}">sil</button>
|
||||||
|
</span>`:""}</div>
|
||||||
|
${x.summary?`<div class="summary">${esc(x.summary)}</div>`:""}
|
||||||
|
${geo.length?`<div class="geo">${geo.map(g=>`<span>${g}</span>`).join("")}</div>`:""}
|
||||||
|
${objs?`<ul class="objs">${objs}</ul>`:""}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(x){
|
||||||
|
editId=x?x.id:null;
|
||||||
|
$("formTitle").textContent=x?("✏️ Düzenle: "+x.title):"+ Yeni Görev";
|
||||||
|
$("fTitle").value=x?x.title:"";
|
||||||
|
$("fTypeSel").value=x?x.quest_type:"side";
|
||||||
|
$("fGiver").value=x&&x.giver_npc_id?x.giver_npc_id:"";
|
||||||
|
$("fCont").value=x&&x.continent?x.continent:"";
|
||||||
|
refreshRegionSel(x&&x.continent?x.continent:"",x&&x.region_tag_id?x.region_tag_id:"");
|
||||||
|
$("fLoc").value=x&&x.location_tag_id?x.location_tag_id:"";
|
||||||
|
$("fSummary").value=x?(x.summary||""):"";
|
||||||
|
$("objList").innerHTML="";
|
||||||
|
if(x&&x.objectives)for(const o of x.objectives)addObjRow(o.map_tag_id,o.note);
|
||||||
|
$("cancelBtn").classList.toggle("hide",!x);
|
||||||
|
if(x)$("formCard").scrollIntoView({behavior:"smooth",block:"nearest"});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("fCont").addEventListener("change",()=>refreshRegionSel($("fCont").value,""));
|
||||||
|
$("addObjBtn").onclick=()=>addObjRow("","");
|
||||||
|
|
||||||
|
document.addEventListener("click",async e=>{
|
||||||
|
if(e.target.classList.contains("edit")){
|
||||||
|
const x=QUESTS.find(q=>q.id===+e.target.dataset.id);
|
||||||
|
if(x)fillForm(x);
|
||||||
|
}
|
||||||
|
if(e.target.classList.contains("delx")){
|
||||||
|
const x=QUESTS.find(q=>q.id===+e.target.dataset.id);
|
||||||
|
if(!x||!confirm(`"${x.title}" görevi silinsin mi?`))return;
|
||||||
|
const r=await api("/quests/"+x.id+"/delete",{method:"POST"});
|
||||||
|
if(r.ok){toast("Silindi");if(editId===x.id)fillForm(null);await reload();}
|
||||||
|
else toast("⚠ "+(r.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("saveBtn").onclick=async()=>{
|
||||||
|
const title=$("fTitle").value.trim();
|
||||||
|
if(!title){$("fMsg").className="msg err";$("fMsg").textContent="Başlık gerekli";return;}
|
||||||
|
$("fMsg").className="msg";$("fMsg").textContent="…";
|
||||||
|
const payload={title,quest_type:$("fTypeSel").value,
|
||||||
|
summary:$("fSummary").value.trim()||null,
|
||||||
|
giver_npc_id:$("fGiver").value?+$("fGiver").value:null,
|
||||||
|
continent:$("fCont").value||null,
|
||||||
|
region_tag_id:$("fRegion").value?+$("fRegion").value:null,
|
||||||
|
location_tag_id:$("fLoc").value?+$("fLoc").value:null,
|
||||||
|
objectives:readObjs()};
|
||||||
|
const r=editId?await api("/quests/"+editId,{method:"POST",body:JSON.stringify(payload)})
|
||||||
|
:await api("/quests",{method:"POST",body:JSON.stringify(payload)});
|
||||||
|
if(!r.ok){$("fMsg").className="msg err";$("fMsg").textContent=r.body.detail||"Hata";return;}
|
||||||
|
$("fMsg").className="msg ok";$("fMsg").textContent=editId?"Güncellendi":"Eklendi";
|
||||||
|
setTimeout(()=>$("fMsg").textContent="",1500);
|
||||||
|
fillForm(null);
|
||||||
|
await reload();
|
||||||
|
};
|
||||||
|
$("cancelBtn").onclick=()=>fillForm(null);
|
||||||
|
$("q").addEventListener("input",render);
|
||||||
|
$("fType").addEventListener("change",render);
|
||||||
|
$("loginBtn").onclick=login;
|
||||||
|
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
|
||||||
|
if(TOKEN)showApp();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
131
assets/upload.html
Normal file
131
assets/upload.html
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
<!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 Yükle</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
|
||||||
|
*{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}
|
||||||
|
.wrap{max-width:640px;margin:0 auto;padding:24px 16px}
|
||||||
|
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
|
||||||
|
.sub{color:var(--muted);font-size:13px;margin-bottom:20px}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:16px}
|
||||||
|
input,button{font-size:14px;border-radius:6px}
|
||||||
|
input[type=email],input[type=password]{width:100%;background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:9px 10px;margin-bottom:10px}
|
||||||
|
.btn{background:var(--gold);color:#1a1300;border:none;padding:9px 18px;font-weight:600;cursor:pointer}
|
||||||
|
.btn:disabled{opacity:.5;cursor:default}
|
||||||
|
.msg{font-size:13px;margin-top:10px}
|
||||||
|
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
|
||||||
|
.drop{border:2px dashed var(--line);border-radius:10px;padding:36px 16px;text-align:center;color:var(--muted);cursor:pointer;transition:.15s}
|
||||||
|
.drop.over{border-color:var(--gold);color:var(--gold)}
|
||||||
|
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||||
|
td,th{text-align:left;padding:7px 4px;border-top:1px solid var(--line)}
|
||||||
|
th{color:var(--muted);font-weight:500;font-size:12px}
|
||||||
|
a{color:var(--gold)}
|
||||||
|
.hide{display:none}
|
||||||
|
.size{color:var(--muted)}
|
||||||
|
img.thumb{max-height:40px;border-radius:4px;vertical-align:middle}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="dark" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>📤 Asset Yükle</h1>
|
||||||
|
<div class="sub">Harita ve görselleri buradan yükle → <code>https://stepstead.com/assets/uploads/…</code></div>
|
||||||
|
|
||||||
|
<div class="card" id="loginCard">
|
||||||
|
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button class="btn" id="loginBtn">Giriş</button>
|
||||||
|
<div class="msg" id="loginMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card hide" id="upCard">
|
||||||
|
<div class="drop" id="drop">Dosyayı buraya sürükle ya da tıkla<br><small>png / jpg / webp / svg / json / zip — max 30 MB</small></div>
|
||||||
|
<input type="file" id="file" hidden accept=".png,.jpg,.jpeg,.webp,.svg,.json,.zip">
|
||||||
|
<div class="msg" id="upMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card hide" id="listCard">
|
||||||
|
<table><thead><tr><th></th><th>Dosya</th><th>Boyut</th></tr></thead><tbody id="rows"></tbody></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
let TOKEN=localStorage.getItem("ss_upload_token")||"";
|
||||||
|
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
function fmt(n){return n>1048576?(n/1048576).toFixed(1)+" MB":Math.round(n/1024)+" KB";}
|
||||||
|
|
||||||
|
async function api(path,opts={}){
|
||||||
|
opts.headers=Object.assign({},opts.headers,{Authorization:"Bearer "+TOKEN});
|
||||||
|
const r=await fetch(API+path,opts);
|
||||||
|
let body={}; try{body=await r.json();}catch(e){}
|
||||||
|
return {ok:r.ok,status:r.status,body};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(){
|
||||||
|
$("loginMsg").textContent="…";
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(r.ok&&b.access_token){
|
||||||
|
TOKEN=b.access_token; localStorage.setItem("ss_upload_token",TOKEN);
|
||||||
|
show();
|
||||||
|
}else{
|
||||||
|
$("loginMsg").className="msg err";
|
||||||
|
$("loginMsg").textContent=b.detail||"Giriş başarısız";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function show(){
|
||||||
|
const r=await api("/admin/uploads");
|
||||||
|
if(!r.ok){
|
||||||
|
if(r.status===401||r.status===403){TOKEN="";localStorage.removeItem("ss_upload_token");}
|
||||||
|
$("loginCard").classList.remove("hide");
|
||||||
|
$("upCard").classList.add("hide"); $("listCard").classList.add("hide");
|
||||||
|
if(r.status===403){$("loginMsg").className="msg err";$("loginMsg").textContent="Bu hesabın yükleme yetkisi yok";}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$("loginCard").classList.add("hide");
|
||||||
|
$("upCard").classList.remove("hide"); $("listCard").classList.remove("hide");
|
||||||
|
const tb=$("rows"); tb.innerHTML="";
|
||||||
|
for(const f of r.body){
|
||||||
|
const tr=document.createElement("tr");
|
||||||
|
const img=/\.(png|jpe?g|webp|svg)$/i.test(f.filename)?`<img class="thumb" src="${f.url}" loading="lazy">`:"📄";
|
||||||
|
tr.innerHTML=`<td>${img}</td><td><a href="${f.url}" target="_blank">${f.filename}</a></td><td class="size">${fmt(f.size)}</td>`;
|
||||||
|
tb.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(file){
|
||||||
|
if(!file)return;
|
||||||
|
$("upMsg").className="msg"; $("upMsg").textContent="⏳ Yükleniyor: "+file.name;
|
||||||
|
const fd=new FormData(); fd.append("file",file);
|
||||||
|
const r=await api("/admin/upload",{method:"POST",body:fd});
|
||||||
|
if(r.ok){
|
||||||
|
$("upMsg").className="msg ok";
|
||||||
|
$("upMsg").innerHTML=`✅ Yüklendi: <a href="${r.body.url}" target="_blank">${r.body.url}</a>`;
|
||||||
|
show();
|
||||||
|
}else{
|
||||||
|
$("upMsg").className="msg err";
|
||||||
|
$("upMsg").textContent="⚠ "+(r.body.detail||("Hata "+r.status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$("loginBtn").onclick=login;
|
||||||
|
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
|
||||||
|
$("drop").onclick=()=>$("file").click();
|
||||||
|
$("file").onchange=e=>upload(e.target.files[0]);
|
||||||
|
$("drop").addEventListener("dragover",e=>{e.preventDefault();$("drop").classList.add("over");});
|
||||||
|
$("drop").addEventListener("dragleave",()=>$("drop").classList.remove("over"));
|
||||||
|
$("drop").addEventListener("drop",e=>{e.preventDefault();$("drop").classList.remove("over");upload(e.dataTransfer.files[0]);});
|
||||||
|
|
||||||
|
if(TOKEN)show();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
233
assets/users.html
Normal file
233
assets/users.html
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
<!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 — Kullanıcılar</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
|
||||||
|
*{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}
|
||||||
|
.wrap{max-width:960px;margin:0 auto;padding:20px 16px}
|
||||||
|
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
|
||||||
|
h2{font-size:15px;color:var(--gold);margin:0 0 10px}
|
||||||
|
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:16px}
|
||||||
|
input,button,select{font-size:13px;border-radius:6px;font-family:inherit}
|
||||||
|
input,select{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:7px 9px}
|
||||||
|
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
|
||||||
|
.btn{background:var(--gold);color:#1a1300;border:none;padding:8px 16px;font-weight:600;cursor:pointer}
|
||||||
|
.btn.sec{background:#2a3648;color:var(--txt)}
|
||||||
|
.btn.sm{padding:5px 10px;font-size:12px}
|
||||||
|
.btn.danger{background:none;border:1px solid var(--line);color:var(--muted)}
|
||||||
|
.btn.danger:hover{color:var(--err);border-color:var(--err)}
|
||||||
|
.msg{font-size:13px;margin-top:8px;min-height:16px}
|
||||||
|
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
|
||||||
|
.hide{display:none}
|
||||||
|
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||||
|
th,td{text-align:left;padding:8px 6px;border-top:1px solid var(--line);vertical-align:middle}
|
||||||
|
th{color:var(--muted);font-weight:500;font-size:12px;text-transform:uppercase;letter-spacing:.04em;border-top:none}
|
||||||
|
tr.inactive td{opacity:.45}
|
||||||
|
.pill{font-size:11px;padding:1px 8px;border-radius:10px;background:#243247;color:var(--gold);font-weight:600}
|
||||||
|
.permbox{display:flex;gap:10px;flex-wrap:wrap;margin:6px 0}
|
||||||
|
.permbox label{display:flex;gap:5px;align-items:center;font-size:12px;color:var(--muted);background:#141c28;border:1px solid var(--line);border-radius:6px;padding:5px 9px;cursor:pointer}
|
||||||
|
.permbox label:has(input:checked){color:var(--gold);border-color:var(--gold)}
|
||||||
|
.rolerow{border-top:1px solid var(--line);padding:12px 0}
|
||||||
|
.rolerow:first-of-type{border-top:none}
|
||||||
|
.rolehead{display:flex;gap:10px;align-items:center}
|
||||||
|
.rolehead b{font-size:14px}
|
||||||
|
.rolehead .cnt{color:var(--muted);font-size:12px}
|
||||||
|
.rolehead .acts{margin-left:auto;display:flex;gap:6px}
|
||||||
|
.newrow{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px}
|
||||||
|
.newrow input{flex:1 1 140px}
|
||||||
|
#toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:#1a2230;border:1px solid var(--gold);color:var(--txt);padding:9px 16px;border-radius:8px;font-size:13px;opacity:0;pointer-events:none;transition:.3s;z-index:9}
|
||||||
|
#toast.on{opacity:1}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="dark" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>👥 Kullanıcılar & Roller</h1>
|
||||||
|
<div class="sub">Kullanıcı oluştur, rol ata; rollerin hangi sayfalara/aksiyonlara erişeceğini seç.</div>
|
||||||
|
|
||||||
|
<div class="card" id="loginCard">
|
||||||
|
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button class="btn" id="loginBtn">Giriş</button>
|
||||||
|
<div class="msg" id="loginMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app" class="hide">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Kullanıcılar</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>E-posta</th><th>İsim</th><th>Rol</th><th>Durum</th><th></th></tr></thead>
|
||||||
|
<tbody id="userRows"></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="newrow">
|
||||||
|
<input type="email" id="nuEmail" placeholder="E-posta">
|
||||||
|
<input type="text" id="nuName" placeholder="İsim (ops.)">
|
||||||
|
<input type="password" id="nuPass" placeholder="Şifre (min 8)">
|
||||||
|
<select id="nuRole"></select>
|
||||||
|
<button class="btn" id="nuBtn">+ Kullanıcı Oluştur</button>
|
||||||
|
</div>
|
||||||
|
<div class="msg" id="uMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Roller</h2>
|
||||||
|
<div id="roleList"></div>
|
||||||
|
<div class="newrow">
|
||||||
|
<input type="text" id="nrName" placeholder="Yeni rol adı">
|
||||||
|
<button class="btn" id="nrBtn">+ Rol Oluştur</button>
|
||||||
|
</div>
|
||||||
|
<div class="msg" id="rMsg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
let TOKEN=localStorage.getItem("ss_upload_token")||"";
|
||||||
|
let ROLES=[],USERS=[],PERMS={};
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
function esc(s){return (s??"").toString().replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));}
|
||||||
|
function toast(m,ms=3000){const t=$("toast");t.textContent=m;t.classList.add("on");clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove("on"),ms);}
|
||||||
|
|
||||||
|
async function api(path,opts={}){
|
||||||
|
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
|
||||||
|
const r=await fetch(API+path,opts);
|
||||||
|
let body={};try{body=await r.json();}catch(e){}
|
||||||
|
return {ok:r.ok,status:r.status,body};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(){
|
||||||
|
$("loginMsg").textContent="…";
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);
|
||||||
|
if(window.ssNavRefresh)ssNavRefresh();showApp();}
|
||||||
|
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showApp(){
|
||||||
|
const p=await api("/admin/permissions");
|
||||||
|
if(!p.ok){
|
||||||
|
$("loginMsg").className="msg err";
|
||||||
|
$("loginMsg").textContent=p.status===403?"Bu hesabın kullanıcı yönetimi yetkisi yok":"Giriş gerekli / oturum bitti";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PERMS=p.body;
|
||||||
|
$("loginCard").classList.add("hide");$("app").classList.remove("hide");
|
||||||
|
await loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAll(){
|
||||||
|
const [r,u]=await Promise.all([api("/admin/roles"),api("/admin/users")]);
|
||||||
|
ROLES=r.ok?r.body:[];USERS=u.ok?u.body:[];
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function roleSelect(id,sel){
|
||||||
|
const opts=['<option value="">— rol yok (oyuncu) —</option>']
|
||||||
|
.concat(ROLES.map(r=>`<option value="${r.id}" ${r.id===sel?"selected":""}>${esc(r.name)}</option>`));
|
||||||
|
return `<select data-uid="${id}" class="uRole">${opts.join("")}</select>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
// kullanıcılar
|
||||||
|
const tb=$("userRows");tb.innerHTML="";
|
||||||
|
for(const u of USERS){
|
||||||
|
const tr=document.createElement("tr");
|
||||||
|
if(!u.is_active)tr.className="inactive";
|
||||||
|
tr.innerHTML=`<td>${esc(u.email)}</td><td>${esc(u.display_name||"")}</td>
|
||||||
|
<td>${roleSelect(u.id,u.role_id)}</td>
|
||||||
|
<td><button class="btn sm sec uAct" data-uid="${u.id}" data-on="${u.is_active?1:0}">${u.is_active?"Aktif":"Pasif"}</button></td>
|
||||||
|
<td></td>`;
|
||||||
|
tb.appendChild(tr);
|
||||||
|
}
|
||||||
|
// yeni kullanıcı rol seçimi
|
||||||
|
$("nuRole").innerHTML=['<option value="">— rol yok —</option>']
|
||||||
|
.concat(ROLES.map(r=>`<option value="${r.id}">${esc(r.name)}</option>`)).join("");
|
||||||
|
// roller
|
||||||
|
const box=$("roleList");box.innerHTML="";
|
||||||
|
for(const r of ROLES){
|
||||||
|
const div=document.createElement("div");div.className="rolerow";
|
||||||
|
const permHtml=r.is_system
|
||||||
|
? `<div class="permbox"><span class="pill">tüm izinler (*)</span></div>`
|
||||||
|
: `<div class="permbox">`+Object.entries(PERMS).map(([code,lbl])=>
|
||||||
|
`<label title="${esc(lbl)}"><input type="checkbox" data-rid="${r.id}" value="${code}" ${r.permissions.includes(code)?"checked":""}>${code}</label>`
|
||||||
|
).join("")+`</div>`;
|
||||||
|
div.innerHTML=`<div class="rolehead">
|
||||||
|
<b>${esc(r.name)}</b><span class="pill">${r.is_system?"sistem":"özel"}</span>
|
||||||
|
<span class="cnt">${r.user_count} kullanıcı</span>
|
||||||
|
<span class="acts">
|
||||||
|
${r.is_system?"":`<button class="btn sm rSave" data-rid="${r.id}">Kaydet</button>
|
||||||
|
<button class="btn sm danger rDel" data-rid="${r.id}">Sil</button>`}
|
||||||
|
</span>
|
||||||
|
</div>${permHtml}`;
|
||||||
|
box.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("change",async e=>{
|
||||||
|
if(e.target.classList.contains("uRole")){
|
||||||
|
const uid=+e.target.dataset.uid;
|
||||||
|
const val=e.target.value?+e.target.value:null;
|
||||||
|
const r=await api("/admin/users/"+uid,{method:"POST",body:JSON.stringify({role_id:val})});
|
||||||
|
if(r.ok){toast("✅ Rol güncellendi");await loadAll();}
|
||||||
|
else{toast("⚠ "+(r.body.detail||"Hata"));render();}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("click",async e=>{
|
||||||
|
if(e.target.classList.contains("uAct")){
|
||||||
|
const uid=+e.target.dataset.uid, on=e.target.dataset.on==="1";
|
||||||
|
const r=await api("/admin/users/"+uid,{method:"POST",body:JSON.stringify({is_active:!on})});
|
||||||
|
if(r.ok){toast(on?"Kullanıcı pasifleştirildi":"Kullanıcı aktifleştirildi");await loadAll();}
|
||||||
|
else toast("⚠ "+(r.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
if(e.target.classList.contains("rSave")){
|
||||||
|
const rid=+e.target.dataset.rid;
|
||||||
|
const role=ROLES.find(x=>x.id===rid);
|
||||||
|
const perms=[...document.querySelectorAll(`input[data-rid="${rid}"]:checked`)].map(c=>c.value);
|
||||||
|
const r=await api("/admin/roles/"+rid,{method:"POST",body:JSON.stringify({name:role.name,permissions:perms})});
|
||||||
|
if(r.ok){toast("✅ Rol kaydedildi");await loadAll();}
|
||||||
|
else toast("⚠ "+(r.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
if(e.target.classList.contains("rDel")){
|
||||||
|
const rid=+e.target.dataset.rid;
|
||||||
|
const role=ROLES.find(x=>x.id===rid);
|
||||||
|
if(!confirm(`"${role.name}" rolü silinsin mi?`))return;
|
||||||
|
const r=await api("/admin/roles/"+rid+"/delete",{method:"POST"});
|
||||||
|
if(r.ok){toast("Rol silindi");await loadAll();}
|
||||||
|
else toast("⚠ "+(r.body.detail||"Hata"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("nuBtn").onclick=async()=>{
|
||||||
|
const email=$("nuEmail").value.trim(),pass=$("nuPass").value;
|
||||||
|
if(!email||pass.length<8){$("uMsg").className="msg err";$("uMsg").textContent="E-posta + min 8 karakter şifre gerekli";return;}
|
||||||
|
$("uMsg").className="msg";$("uMsg").textContent="…";
|
||||||
|
const r=await api("/admin/users",{method:"POST",body:JSON.stringify({
|
||||||
|
email,password:pass,display_name:$("nuName").value.trim()||null,
|
||||||
|
role_id:$("nuRole").value?+$("nuRole").value:null})});
|
||||||
|
if(r.ok){$("uMsg").className="msg ok";$("uMsg").textContent="✅ Oluşturuldu: "+r.body.email;
|
||||||
|
$("nuEmail").value=$("nuName").value=$("nuPass").value="";await loadAll();}
|
||||||
|
else{$("uMsg").className="msg err";$("uMsg").textContent=r.body.detail||"Hata";}
|
||||||
|
};
|
||||||
|
$("nrBtn").onclick=async()=>{
|
||||||
|
const name=$("nrName").value.trim();
|
||||||
|
if(name.length<2){$("rMsg").className="msg err";$("rMsg").textContent="Rol adı gerekli";return;}
|
||||||
|
const r=await api("/admin/roles",{method:"POST",body:JSON.stringify({name,permissions:[]})});
|
||||||
|
if(r.ok){$("rMsg").className="msg ok";$("rMsg").textContent="✅ Rol oluşturuldu — izinlerini seçip Kaydet'e bas";
|
||||||
|
$("nrName").value="";await loadAll();}
|
||||||
|
else{$("rMsg").className="msg err";$("rMsg").textContent=r.body.detail||"Hata";}
|
||||||
|
};
|
||||||
|
$("loginBtn").onclick=login;
|
||||||
|
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
|
||||||
|
if(TOKEN)showApp();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
assets/world_map.png
Normal file
BIN
assets/world_map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 MiB |
BIN
assets/world_map@4x.webp
Normal file
BIN
assets/world_map@4x.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 MiB |
1
db/data/crafts.json
Normal file
1
db/data/crafts.json
Normal file
File diff suppressed because one or more lines are too long
119
db/index.html
Normal file
119
db/index.html
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="tr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Stepstead — Craft Veritabanı</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#f5f3ee; --card:#fff; --ink:#2d3a2e; --muted:#77806f; --accent:#3a5a40;
|
||||||
|
--line:#e4e0d6;
|
||||||
|
--common:#6b7280; --uncommon:#2e8b45; --rare:#2563c9; --epic:#8b3fc9; --legendary:#c9762e;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font-family:system-ui,sans-serif;background:var(--bg);color:var(--ink)}
|
||||||
|
header{background:var(--accent);color:#fff;padding:20px 16px;position:sticky;top:0;z-index:5;box-shadow:0 2px 12px rgba(0,0,0,.15)}
|
||||||
|
header .inner{max-width:1000px;margin:0 auto}
|
||||||
|
header h1{margin:0 0 2px;font-size:24px}
|
||||||
|
header h1 a{color:#fff;text-decoration:none}
|
||||||
|
header p{margin:0;font-size:13px;opacity:.85}
|
||||||
|
.controls{max-width:1000px;margin:16px auto 0;padding:0 16px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||||
|
#q{flex:1 1 220px;padding:10px 14px;border:1px solid var(--line);border-radius:8px;font-size:15px;background:var(--card)}
|
||||||
|
.chip{padding:7px 14px;border-radius:999px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-size:13px;white-space:nowrap}
|
||||||
|
.chip.on{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||||
|
main{max-width:1000px;margin:16px auto 60px;padding:0 16px}
|
||||||
|
.stats{color:var(--muted);font-size:13px;margin:8px 2px 16px}
|
||||||
|
h2.group{font-size:19px;margin:28px 0 4px;color:var(--accent);border-bottom:2px solid var(--line);padding-bottom:6px}
|
||||||
|
h3.family{font-size:14px;margin:16px 0 8px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}
|
||||||
|
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
||||||
|
.card .name{font-weight:600;font-size:15px}
|
||||||
|
.card .meta{font-size:12px;color:var(--muted);margin:4px 0 8px;display:flex;gap:10px;flex-wrap:wrap}
|
||||||
|
.card .ings{font-size:13px;line-height:1.5}
|
||||||
|
.card .ings b{color:var(--muted);font-weight:500}
|
||||||
|
.badge{display:inline-block;background:#e8f0e9;color:var(--accent);border-radius:6px;padding:1px 7px;font-size:12px;margin-left:6px}
|
||||||
|
.r-common{color:var(--common)} .r-uncommon{color:var(--uncommon)} .r-rare{color:var(--rare)}
|
||||||
|
.r-epic{color:var(--epic)} .r-legendary{color:var(--legendary)}
|
||||||
|
.empty{color:var(--muted);text-align:center;margin:48px 0}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="light" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><div class="inner">
|
||||||
|
<h1><a href="/">Stepstead</a> · Craft Veritabanı</h1>
|
||||||
|
<p>Tüm üretim tarifleri — malzemeler, skill gereksinimleri, adım maliyetleri</p>
|
||||||
|
</div></header>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<input id="q" type="search" placeholder="Tarif, malzeme veya skill ara… (örn. Meşe, Kılıç, Simya)">
|
||||||
|
<div id="chips"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="stats" id="stats"></div>
|
||||||
|
<div id="list"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let DATA=null, activeGroup="Tümü";
|
||||||
|
const norm=s=>s.toLocaleLowerCase("tr");
|
||||||
|
|
||||||
|
fetch("data/crafts.json").then(r=>r.json()).then(d=>{DATA=d;buildChips();render();});
|
||||||
|
|
||||||
|
function buildChips(){
|
||||||
|
const box=document.getElementById("chips");
|
||||||
|
const groups=["Tümü",...Object.keys(DATA.groups).sort((a,b)=>DATA.groups[b]-DATA.groups[a])];
|
||||||
|
box.style.display="flex";box.style.flexWrap="wrap";box.style.gap="8px";
|
||||||
|
for(const g of groups){
|
||||||
|
const c=document.createElement("button");
|
||||||
|
c.className="chip"+(g===activeGroup?" on":"");
|
||||||
|
c.textContent=g==="Tümü"?`Tümü (${DATA.total})`:`${g} (${DATA.groups[g]})`;
|
||||||
|
c.onclick=()=>{activeGroup=g;document.querySelectorAll(".chip").forEach(x=>x.classList.remove("on"));c.classList.add("on");render();};
|
||||||
|
box.appendChild(c);
|
||||||
|
}
|
||||||
|
document.getElementById("q").addEventListener("input",render);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matches(r,q){
|
||||||
|
if(!q)return true;
|
||||||
|
if(norm(r.name).includes(q)||norm(r.skill).includes(q)||norm(r.family).includes(q)||norm(r.group).includes(q))return true;
|
||||||
|
return r.ings.some(i=>norm(i.name).includes(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
function card(r){
|
||||||
|
const eff=r.effect&&r.effect.type==="steps"?`<span class="badge">+${r.effect.amount} adım</span>`:"";
|
||||||
|
const qty=r.out_qty>1?` ×${r.out_qty}`:"";
|
||||||
|
const ings=r.ings.map(i=>`${i.qty}× <span class="r-${i.rarity}">${i.name}</span>`).join(", ");
|
||||||
|
return `<div class="card">
|
||||||
|
<div class="name"><span class="r-${r.rarity}">${r.name}</span>${qty}${eff}</div>
|
||||||
|
<div class="meta"><span>⭐ ${r.skill} sv.${r.level}</span><span>👣 ${r.steps} adım</span><span>+${r.xp} XP</span></div>
|
||||||
|
<div class="ings"><b>Malzeme:</b> ${ings}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
const q=norm(document.getElementById("q").value.trim());
|
||||||
|
const list=document.getElementById("list");
|
||||||
|
const rows=DATA.recipes.filter(r=>(activeGroup==="Tümü"||r.group===activeGroup)&&matches(r,q));
|
||||||
|
document.getElementById("stats").textContent=`${rows.length} tarif gösteriliyor`;
|
||||||
|
if(!rows.length){list.innerHTML='<div class="empty">Sonuç yok.</div>';return;}
|
||||||
|
// grup → aile → kartlar
|
||||||
|
const byGroup={};
|
||||||
|
for(const r of rows){(byGroup[r.group]??={});(byGroup[r.group][r.family]??=[]).push(r);}
|
||||||
|
let html="";
|
||||||
|
for(const g of Object.keys(byGroup).sort()){
|
||||||
|
html+=`<h2 class="group">${g}</h2>`;
|
||||||
|
const fams=Object.keys(byGroup[g]).sort((a,b)=>{
|
||||||
|
const la=Math.min(...byGroup[g][a].map(r=>r.level)), lb=Math.min(...byGroup[g][b].map(r=>r.level));
|
||||||
|
return la-lb||a.localeCompare(b,"tr");
|
||||||
|
});
|
||||||
|
for(const f of fams){
|
||||||
|
if(f!=="Genel"||fams.length>1)html+=`<h3 class="family">${f} (${byGroup[g][f].length})</h3>`;
|
||||||
|
html+=`<div class="grid">${byGroup[g][f].sort((a,b)=>a.level-b.level).map(card).join("")}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.innerHTML=html;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
index.html
Normal file
20
index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="tr"><head><meta charset="utf-8"><title>Stepstead</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<style>body{font-family:system-ui,sans-serif;background:#f5f3ee;color:#2d3a2e;display:flex;flex-direction:column;min-height:100vh;margin:0;text-align:center}
|
||||||
|
main{flex:1;display:flex;align-items:center;justify-content:center;padding:24px 16px}
|
||||||
|
.box{max-width:480px;padding:48px 32px;background:#fff;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.08)}
|
||||||
|
h1{color:#3a5a40;margin:0 0 16px;font-size:48px;letter-spacing:-1px}
|
||||||
|
p{color:#666;line-height:1.6}
|
||||||
|
.links{margin-top:24px;display:flex;gap:12px;justify-content:center}
|
||||||
|
.links a{background:#3a5a40;color:#fff;text-decoration:none;padding:10px 20px;border-radius:8px;font-size:14px}</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="light" defer></script></head>
|
||||||
|
<body><main><div class="box">
|
||||||
|
<h1>Stepstead</h1>
|
||||||
|
<p><strong>Her adımın değer kazanır.</strong></p>
|
||||||
|
<p>Yakında iOS ve Android'de. Yürü, keşfet, topla, üret.</p>
|
||||||
|
<div class="links">
|
||||||
|
<a href="/db/">📖 Craft Veritabanı</a>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#999">v0.1 — kurulum aşamasında</p>
|
||||||
|
</div></main></body></html>
|
||||||
1
items/data/items.json
Normal file
1
items/data/items.json
Normal file
File diff suppressed because one or more lines are too long
158
items/index.html
Normal file
158
items/index.html
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="tr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Stepstead — Eşyalar</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#f5f3ee; --card:#fff; --ink:#2d3a2e; --muted:#77806f; --accent:#3a5a40;
|
||||||
|
--line:#e4e0d6;
|
||||||
|
--common:#6b7280; --uncommon:#2e8b45; --rare:#2563c9; --epic:#8b3fc9; --legendary:#c9762e;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font-family:system-ui,sans-serif;background:var(--bg);color:var(--ink)}
|
||||||
|
header{background:var(--accent);color:#fff;padding:20px 16px;position:sticky;top:0;z-index:5;box-shadow:0 2px 12px rgba(0,0,0,.15)}
|
||||||
|
header .inner{max-width:1000px;margin:0 auto}
|
||||||
|
header h1{margin:0 0 2px;font-size:24px}
|
||||||
|
header h1 a{color:#fff;text-decoration:none}
|
||||||
|
header p{margin:0;font-size:13px;opacity:.85}
|
||||||
|
.controls{max-width:1000px;margin:16px auto 0;padding:0 16px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||||
|
#q{flex:1 1 220px;padding:10px 14px;border:1px solid var(--line);border-radius:8px;font-size:15px;background:var(--card)}
|
||||||
|
.row{max-width:1000px;margin:8px auto 0;padding:0 16px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||||
|
.row .lbl{font-size:12px;color:var(--muted);margin-right:2px}
|
||||||
|
.chip{padding:7px 14px;border-radius:999px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-size:13px;white-space:nowrap}
|
||||||
|
.chip.on{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||||
|
.chip.cls.on{background:#2f4a6b;border-color:#2f4a6b}
|
||||||
|
main{max-width:1000px;margin:16px auto 60px;padding:0 16px}
|
||||||
|
.stats{color:var(--muted);font-size:13px;margin:8px 2px 16px}
|
||||||
|
h2.group{font-size:19px;margin:28px 0 4px;color:var(--accent);border-bottom:2px solid var(--line);padding-bottom:6px}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}
|
||||||
|
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
||||||
|
.card .name{font-weight:600;font-size:15px}
|
||||||
|
.card .meta{font-size:12px;color:var(--muted);margin:4px 0 6px;display:flex;gap:10px;flex-wrap:wrap}
|
||||||
|
.card .line{font-size:12.5px;line-height:1.5;margin-top:3px}
|
||||||
|
.card .line b{color:var(--muted);font-weight:500}
|
||||||
|
.tag{display:inline-block;border-radius:6px;padding:1px 7px;font-size:11.5px;margin:1px 3px 1px 0}
|
||||||
|
.tag.slot{background:#e8f0e9;color:var(--accent)}
|
||||||
|
.tag.cls{background:#e6ecf5;color:#2f4a6b}
|
||||||
|
.tag.all{background:#eef2ec;color:var(--muted)}
|
||||||
|
.tag.src{background:#f3ecdd;color:#8a6a2e}
|
||||||
|
.tag.eff{background:#e9f3f0;color:#2e7d6b}
|
||||||
|
.r-common{color:var(--common)} .r-uncommon{color:var(--uncommon)} .r-rare{color:var(--rare)}
|
||||||
|
.r-epic{color:var(--epic)} .r-legendary{color:var(--legendary)}
|
||||||
|
.empty{color:var(--muted);text-align:center;margin:48px 0}
|
||||||
|
</style>
|
||||||
|
<script src="/nav.js?v=8" data-theme="light" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><div class="inner">
|
||||||
|
<h1><a href="/">Stepstead</a> · Eşyalar</h1>
|
||||||
|
<p>Tüm silah, giysi, alet ve maddeler — hangi sınıf kullanır, nereden gelir</p>
|
||||||
|
</div></header>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<input id="q" type="search" placeholder="Eşya ara… (örn. Kılıç, Asa, Bakır, Çanta)">
|
||||||
|
</div>
|
||||||
|
<div class="row" id="typechips"></div>
|
||||||
|
<div class="row" id="clschips"><span class="lbl">Sınıf:</span></div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="stats" id="stats"></div>
|
||||||
|
<div id="list"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let DATA=null, activeType="Tümü", activeClass=null;
|
||||||
|
const norm=s=>s.toLocaleLowerCase("tr");
|
||||||
|
const RORDER={common:0,uncommon:1,rare:2,epic:3,legendary:4};
|
||||||
|
|
||||||
|
fetch("data/items.json").then(r=>r.json()).then(d=>{DATA=d;buildChips();render();});
|
||||||
|
|
||||||
|
function chip(text,on,cls,fn){
|
||||||
|
const c=document.createElement("button");
|
||||||
|
c.className="chip"+(cls?" cls":"")+(on?" on":"");
|
||||||
|
c.textContent=text; c.onclick=fn; return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChips(){
|
||||||
|
const tb=document.getElementById("typechips");
|
||||||
|
const types=["Tümü",...Object.keys(DATA.types).sort((a,b)=>DATA.types[b]-DATA.types[a])];
|
||||||
|
for(const t of types){
|
||||||
|
tb.appendChild(chip(t==="Tümü"?`Tümü (${DATA.total})`:`${t} (${DATA.types[t]})`,t===activeType,false,()=>{
|
||||||
|
activeType=t;document.querySelectorAll("#typechips .chip").forEach(x=>x.classList.remove("on"));
|
||||||
|
event.target.classList.add("on");render();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const cb=document.getElementById("clschips");
|
||||||
|
cb.appendChild(chip("Hepsi",true,true,()=>{
|
||||||
|
activeClass=null;document.querySelectorAll("#clschips .chip").forEach(x=>x.classList.remove("on"));
|
||||||
|
event.target.classList.add("on");render();
|
||||||
|
}));
|
||||||
|
for(const c of DATA.classes){
|
||||||
|
cb.appendChild(chip(c,false,true,()=>{
|
||||||
|
activeClass=c;document.querySelectorAll("#clschips .chip").forEach(x=>x.classList.remove("on"));
|
||||||
|
event.target.classList.add("on");render();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
document.getElementById("q").addEventListener("input",render);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sınıf filtresi: eşyayı verilen sınıf kullanabiliyor mu?
|
||||||
|
function usableBy(it,cls){
|
||||||
|
if(!cls)return true;
|
||||||
|
if(it.classes.length===0)return true; // herkese açık
|
||||||
|
return it.classes.includes(cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matches(it,q){
|
||||||
|
if(!q)return true;
|
||||||
|
return norm(it.name).includes(q)||norm(it.type).includes(q)||
|
||||||
|
(it.slot&&norm(it.slot).includes(q))||it.classes.some(c=>norm(c).includes(q))||
|
||||||
|
it.gather.some(g=>norm(g.loc).includes(q)||norm(g.skill).includes(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
function card(it){
|
||||||
|
const slot=it.slot?`<span class="tag slot">${it.slot}${it.two_handed?" (çift el)":""}</span>`:"";
|
||||||
|
let cls="";
|
||||||
|
if(it.slot_code&&it.slot_code!=="tool"){
|
||||||
|
cls=it.classes.length===0
|
||||||
|
? `<span class="tag all">Tüm sınıflar</span>`
|
||||||
|
: it.classes.map(c=>`<span class="tag cls">${c}</span>`).join("");
|
||||||
|
}
|
||||||
|
const src=[];
|
||||||
|
it.gather.forEach(g=>src.push(`<span class="tag src">📍 ${g.loc} · ${g.skill}</span>`));
|
||||||
|
if(it.craftable)src.push(`<span class="tag src">🔨 Üretilebilir</span>`);
|
||||||
|
const eff=it.effect&&it.effect.type==="steps"?`<span class="tag eff">+${it.effect.amount} adım</span>`:"";
|
||||||
|
const clsLine=(it.slot_code&&it.slot_code!=="tool")?`<div class="line"><b>Kullanabilen:</b> ${cls}</div>`:"";
|
||||||
|
const srcLine=src.length?`<div class="line"><b>Kaynak:</b> ${src.join(" ")}</div>`:"";
|
||||||
|
const descLine=it.desc?`<div class="line" style="color:var(--muted)">${it.desc}</div>`:"";
|
||||||
|
return `<div class="card">
|
||||||
|
<div class="name"><span class="r-${it.rarity}">${it.name}</span> ${slot} ${eff}</div>
|
||||||
|
<div class="meta"><span>${it.type}</span><span>🪙 ${it.value}</span></div>
|
||||||
|
${clsLine}${srcLine}${descLine}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
const q=norm(document.getElementById("q").value.trim());
|
||||||
|
const list=document.getElementById("list");
|
||||||
|
let rows=DATA.items.filter(it=>
|
||||||
|
(activeType==="Tümü"||it.type===activeType)&&usableBy(it,activeClass)&&matches(it,q));
|
||||||
|
document.getElementById("stats").textContent=
|
||||||
|
`${rows.length} eşya`+(activeClass?` · ${activeClass} kullanabilir`:"");
|
||||||
|
if(!rows.length){list.innerHTML='<div class="empty">Sonuç yok.</div>';return;}
|
||||||
|
const byType={};
|
||||||
|
for(const it of rows)(byType[it.type]??=[]).push(it);
|
||||||
|
let html="";
|
||||||
|
for(const t of Object.keys(byType).sort((a,b)=>byType[b].length-byType[a].length)){
|
||||||
|
html+=`<h2 class="group">${t} (${byType[t].length})</h2>`;
|
||||||
|
const sorted=byType[t].sort((a,b)=>
|
||||||
|
(RORDER[a.rarity]-RORDER[b.rarity])||a.name.localeCompare(b.name,"tr"));
|
||||||
|
html+=`<div class="grid">${sorted.map(card).join("")}</div>`;
|
||||||
|
}
|
||||||
|
list.innerHTML=html;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
214
nav.js
Normal file
214
nav.js
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
/* Stepstead ortak header + footer.
|
||||||
|
Kullanım: <script src="/nav.js?v=7" data-theme="light|dark" defer></script>
|
||||||
|
Panel linkleri role/izin bazlı görünür: GET /auth/me → permissions[].
|
||||||
|
İzinler: upload, lore, map_view, map_edit, users ("*" = admin). */
|
||||||
|
(function(){
|
||||||
|
"use strict";
|
||||||
|
const API="https://api.stepstead.com";
|
||||||
|
const PUBLIC_LINKS=[
|
||||||
|
{href:"/",label:"🏠 Ana Sayfa"},
|
||||||
|
{href:"/db/",label:"📖 Craft Veritabanı"},
|
||||||
|
{href:"/items/",label:"🎒 Eşyalar"},
|
||||||
|
];
|
||||||
|
const PANEL_LINKS=[
|
||||||
|
{href:"/assets/",label:"🎨 Asset Takip",perms:["upload"]},
|
||||||
|
{href:"/assets/upload.html",label:"📤 Dosya Yükle",perms:["upload"]},
|
||||||
|
{href:"/assets/lore.html",label:"📜 Lore",perms:["lore"]},
|
||||||
|
{href:"/assets/quests.html",label:"🗡 Görevler",perms:["lore"]},
|
||||||
|
{href:"/assets/npcs.html",label:"🧙 NPC'ler",perms:["npc"]},
|
||||||
|
{href:"/assets/chars.html",label:"⚔ Karakterler",perms:["chars"]},
|
||||||
|
{href:"/assets/map-editor.html",label:"🗺 Harita Editörü",perms:["map_view","map_edit"]},
|
||||||
|
{href:"/assets/users.html",label:"👥 Kullanıcılar",perms:["users"]},
|
||||||
|
];
|
||||||
|
const script=document.currentScript;
|
||||||
|
const theme=(script&&script.dataset.theme)==="light"?"light":"dark";
|
||||||
|
|
||||||
|
const css=`
|
||||||
|
.ss-nav{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.4;position:relative}
|
||||||
|
.ss-nav .ss-in{max-width:1040px;margin:0 auto;padding:0 16px;display:flex;align-items:center;gap:4px;flex-wrap:wrap;min-height:46px}
|
||||||
|
.ss-nav .ss-brand{font-weight:700;font-size:15px;text-decoration:none;margin-right:10px;letter-spacing:.2px}
|
||||||
|
.ss-nav a.ss-lnk{text-decoration:none;font-size:13px;padding:5px 10px;border-radius:6px;white-space:nowrap}
|
||||||
|
.ss-nav .ss-adm{display:none;align-items:center;gap:4px;flex-wrap:wrap}
|
||||||
|
.ss-nav .ss-adm.on{display:flex}
|
||||||
|
.ss-nav .ss-admtag{font-size:10px;text-transform:uppercase;letter-spacing:.08em;opacity:.6;margin:0 2px 0 8px}
|
||||||
|
.ss-nav .ss-authbtn{margin-left:auto;background:none;border:1px solid transparent;font-size:13px;padding:5px 10px;border-radius:6px;cursor:pointer;white-space:nowrap}
|
||||||
|
.ss-nav .ss-loginbox{display:none;position:absolute;right:16px;top:100%;margin-top:6px;z-index:50;padding:12px;border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.35);min-width:230px}
|
||||||
|
.ss-nav .ss-loginbox.on{display:block}
|
||||||
|
.ss-nav .ss-loginbox input{display:block;width:100%;box-sizing:border-box;margin-bottom:8px;padding:7px 9px;border-radius:6px;font-size:13px}
|
||||||
|
.ss-nav .ss-loginbox button{width:100%;padding:7px;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer}
|
||||||
|
.ss-nav .ss-loginmsg{font-size:12px;margin-top:6px;min-height:14px}
|
||||||
|
.ss-foot{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:12px;margin-top:48px}
|
||||||
|
.ss-foot .ss-in{max-width:1040px;margin:0 auto;padding:16px;display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
||||||
|
.ss-foot a{text-decoration:none}
|
||||||
|
.ss-foot .ss-sp{margin-left:auto}
|
||||||
|
/* dark */
|
||||||
|
.ss-dark.ss-nav{background:#0b1016;border-bottom:1px solid #2c3a4f}
|
||||||
|
.ss-dark.ss-nav .ss-brand{color:#e8b923}
|
||||||
|
.ss-dark.ss-nav a.ss-lnk{color:#8b99a8}
|
||||||
|
.ss-dark.ss-nav a.ss-lnk:hover{color:#e6edf3;background:#1a2230}
|
||||||
|
.ss-dark.ss-nav a.ss-lnk.on{color:#e8b923;background:#1a2230}
|
||||||
|
.ss-dark.ss-nav .ss-admtag{color:#8b99a8}
|
||||||
|
.ss-dark.ss-nav .ss-authbtn{color:#8b99a8;border-color:#2c3a4f;background:#0b1016}
|
||||||
|
.ss-dark.ss-nav .ss-authbtn:hover{color:#e8b923;border-color:#e8b923}
|
||||||
|
.ss-dark.ss-nav .ss-loginbox{background:#1a2230;border:1px solid #2c3a4f}
|
||||||
|
.ss-dark.ss-nav .ss-loginbox input{background:#222d3d;color:#e6edf3;border:1px solid #2c3a4f}
|
||||||
|
.ss-dark.ss-nav .ss-loginbox button{background:#e8b923;color:#1a1300}
|
||||||
|
.ss-dark.ss-nav .ss-loginmsg{color:#e5534b}
|
||||||
|
.ss-dark.ss-foot{background:#0b1016;border-top:1px solid #2c3a4f;color:#8b99a8}
|
||||||
|
.ss-dark.ss-foot a{color:#8b99a8}
|
||||||
|
.ss-dark.ss-foot a:hover{color:#e8b923}
|
||||||
|
/* light */
|
||||||
|
.ss-light.ss-nav{background:#2f4a35;border-bottom:1px solid #253b2a}
|
||||||
|
.ss-light.ss-nav .ss-brand{color:#fff}
|
||||||
|
.ss-light.ss-nav a.ss-lnk{color:rgba(255,255,255,.75)}
|
||||||
|
.ss-light.ss-nav a.ss-lnk:hover{color:#fff;background:rgba(255,255,255,.12)}
|
||||||
|
.ss-light.ss-nav a.ss-lnk.on{color:#fff;background:rgba(255,255,255,.18)}
|
||||||
|
.ss-light.ss-nav .ss-admtag{color:rgba(255,255,255,.6)}
|
||||||
|
.ss-light.ss-nav .ss-authbtn{color:rgba(255,255,255,.75);border-color:rgba(255,255,255,.3);background:transparent}
|
||||||
|
.ss-light.ss-nav .ss-authbtn:hover{color:#fff;border-color:#fff}
|
||||||
|
.ss-light.ss-nav .ss-loginbox{background:#fff;border:1px solid #e4e0d6}
|
||||||
|
.ss-light.ss-nav .ss-loginbox input{background:#f5f3ee;color:#2d3a2e;border:1px solid #e4e0d6}
|
||||||
|
.ss-light.ss-nav .ss-loginbox button{background:#3a5a40;color:#fff}
|
||||||
|
.ss-light.ss-nav .ss-loginmsg{color:#c0392b}
|
||||||
|
.ss-light.ss-foot{background:transparent;border-top:1px solid #e4e0d6;color:#77806f}
|
||||||
|
.ss-light.ss-foot a{color:#77806f}
|
||||||
|
.ss-light.ss-foot a:hover{color:#3a5a40}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let PERMS=[];
|
||||||
|
function tok(){return localStorage.getItem("ss_upload_token")||"";}
|
||||||
|
function hasPerm(p){return PERMS.includes("*")||PERMS.includes(p);}
|
||||||
|
function anyPerm(list){return list.some(hasPerm);}
|
||||||
|
|
||||||
|
function norm(p){return p.replace(/index\.html$/,"");}
|
||||||
|
function link(l){
|
||||||
|
const a=document.createElement("a");
|
||||||
|
a.className="ss-lnk"+(norm(location.pathname)===norm(l.href)?" on":"");
|
||||||
|
a.href=l.href;a.textContent=l.label;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPerms(role){
|
||||||
|
const adm=document.getElementById("ssAdmLinks");
|
||||||
|
adm.innerHTML="";
|
||||||
|
// Panel yetkisi olan (bir rolü/izni bulunan) herkes TÜM sekmeleri görür.
|
||||||
|
// İşlem yetkisi sayfa/backend tarafında izne göre ayrıca denetlenir.
|
||||||
|
const isPanel=PERMS.length>0;
|
||||||
|
if(isPanel){
|
||||||
|
const tag=document.createElement("span");
|
||||||
|
tag.className="ss-admtag";tag.textContent=role||"panel";
|
||||||
|
adm.appendChild(tag);
|
||||||
|
for(const l of PANEL_LINKS)adm.appendChild(link(l));
|
||||||
|
adm.classList.add("on");
|
||||||
|
}else adm.classList.remove("on");
|
||||||
|
document.getElementById("ssAuthBtn").textContent=PERMS.length?"🚪 Çıkış":"🔑 Giriş";
|
||||||
|
if(PERMS.length)sessionStorage.setItem("ss_nav_perms",JSON.stringify({p:PERMS,r:role||""}));
|
||||||
|
else sessionStorage.removeItem("ss_nav_perms");
|
||||||
|
}
|
||||||
|
|
||||||
|
function build(){
|
||||||
|
const st=document.createElement("style");st.textContent=css;document.head.appendChild(st);
|
||||||
|
|
||||||
|
const nav=document.createElement("nav");
|
||||||
|
nav.className="ss-nav ss-"+theme;
|
||||||
|
const inner=document.createElement("div");inner.className="ss-in";
|
||||||
|
const brand=document.createElement("a");
|
||||||
|
brand.className="ss-brand";brand.href="/";brand.textContent="👣 Stepstead";
|
||||||
|
inner.appendChild(brand);
|
||||||
|
for(const l of PUBLIC_LINKS)inner.appendChild(link(l));
|
||||||
|
const adm=document.createElement("span");adm.className="ss-adm";adm.id="ssAdmLinks";
|
||||||
|
inner.appendChild(adm);
|
||||||
|
|
||||||
|
const authBtn=document.createElement("button");
|
||||||
|
authBtn.className="ss-authbtn";authBtn.id="ssAuthBtn";authBtn.textContent="🔑 Giriş";
|
||||||
|
authBtn.onclick=()=>{
|
||||||
|
if(PERMS.length){
|
||||||
|
localStorage.removeItem("ss_upload_token");
|
||||||
|
PERMS=[];applyPerms(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("ssLoginBox").classList.toggle("on");
|
||||||
|
document.getElementById("ssLgEmail").focus();
|
||||||
|
};
|
||||||
|
inner.appendChild(authBtn);
|
||||||
|
|
||||||
|
const box=document.createElement("div");
|
||||||
|
box.className="ss-loginbox";box.id="ssLoginBox";
|
||||||
|
box.innerHTML=`
|
||||||
|
<input type="email" id="ssLgEmail" placeholder="E-posta" autocomplete="username">
|
||||||
|
<input type="password" id="ssLgPass" placeholder="Şifre" autocomplete="current-password">
|
||||||
|
<button id="ssLgBtn">Giriş yap</button>
|
||||||
|
<div class="ss-loginmsg" id="ssLgMsg"></div>`;
|
||||||
|
nav.appendChild(inner);
|
||||||
|
nav.appendChild(box);
|
||||||
|
document.body.prepend(nav);
|
||||||
|
|
||||||
|
document.getElementById("ssLgBtn").onclick=doLogin;
|
||||||
|
document.getElementById("ssLgPass").addEventListener("keydown",e=>{if(e.key==="Enter")doLogin();});
|
||||||
|
document.addEventListener("click",e=>{
|
||||||
|
if(!box.contains(e.target)&&e.target!==authBtn)box.classList.remove("on");
|
||||||
|
});
|
||||||
|
|
||||||
|
const foot=document.createElement("footer");
|
||||||
|
foot.className="ss-foot ss-"+theme;
|
||||||
|
foot.innerHTML=`<div class="ss-in">
|
||||||
|
<span>© ${new Date().getFullYear()} Stepstead — her adımın değer kazanır</span>
|
||||||
|
<span class="ss-sp"><a href="/">Ana Sayfa</a></span>
|
||||||
|
<span><a href="/db/">Craft Veritabanı</a></span>
|
||||||
|
</div>`;
|
||||||
|
document.body.appendChild(foot);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doLogin(){
|
||||||
|
const msg=document.getElementById("ssLgMsg");
|
||||||
|
msg.textContent="…";
|
||||||
|
try{
|
||||||
|
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({email:document.getElementById("ssLgEmail").value.trim(),
|
||||||
|
password:document.getElementById("ssLgPass").value})});
|
||||||
|
const b=await r.json().catch(()=>({}));
|
||||||
|
if(!r.ok||!b.access_token){msg.textContent=b.detail||"Giriş başarısız";return;}
|
||||||
|
localStorage.setItem("ss_upload_token",b.access_token);
|
||||||
|
msg.textContent="";
|
||||||
|
await refresh();
|
||||||
|
if(PERMS.length){
|
||||||
|
document.getElementById("ssLoginBox").classList.remove("on");
|
||||||
|
document.getElementById("ssLgPass").value="";
|
||||||
|
}else msg.textContent="Giriş tamam — ama bu hesabın panel yetkisi yok";
|
||||||
|
}catch(e){msg.textContent="Bağlantı hatası";}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(){
|
||||||
|
if(!tok()){PERMS=[];applyPerms(null);return;}
|
||||||
|
try{
|
||||||
|
const r=await fetch(API+"/auth/me",{headers:{Authorization:"Bearer "+tok()}});
|
||||||
|
if(r.ok){
|
||||||
|
const u=await r.json();
|
||||||
|
PERMS=u.permissions||[];
|
||||||
|
applyPerms(u.role);
|
||||||
|
}else{
|
||||||
|
if(r.status===401)localStorage.removeItem("ss_upload_token");
|
||||||
|
PERMS=[];applyPerms(null);
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
// geçici ağ hatası: cache'e güven
|
||||||
|
try{
|
||||||
|
const c=JSON.parse(sessionStorage.getItem("ss_nav_perms")||"null");
|
||||||
|
if(c){PERMS=c.p;applyPerms(c.r);return;}
|
||||||
|
}catch(_){}
|
||||||
|
PERMS=[];applyPerms(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.ssNavRefresh=refresh;
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
build();
|
||||||
|
try{
|
||||||
|
const c=JSON.parse(sessionStorage.getItem("ss_nav_perms")||"null");
|
||||||
|
if(c&&tok()){PERMS=c.p;applyPerms(c.r);}
|
||||||
|
}catch(_){}
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",init);
|
||||||
|
else init();
|
||||||
|
})();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue