stepstead-web/nav.js
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

214 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

/* 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();
})();