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

300 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>ı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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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>