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