İlk commit: Stepstead web (statik panel + db)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jts 2026-07-11 11:30:59 +03:00
commit dea01dd6a8
18 changed files with 3764 additions and 0 deletions

214
nav.js Normal file
View 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();
})();