İ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
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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue