1208 lines
76 KiB
HTML
1208 lines
76 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Blair — Ghost Dashboard</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{--bg:#0a0a0f;--bg2:#111118;--bg3:#16161f;--bg4:#1d1d2a;--bg5:#22222e;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--border3:rgba(255,255,255,.22);--text:#e8e4d8;--muted:#7a7568;--muted2:#a09890;--accent:#c8a96e;--accent2:#e8c98e;--red:#c04040;--red2:#e05050;--green:#4a9c6a;--green2:#5ab87e;--blue:#4a7ac0;--blue2:#6a9add;--purple:#8a5ac0;--purple2:#a87add;--discord:#5865F2;--orange2:#e09860;}
|
|
*{box-sizing:border-box;margin:0;padding:0;}
|
|
body{background:var(--bg);color:var(--text);font-family:'Crimson Pro',Georgia,serif;font-size:16px;line-height:1.6;min-height:100vh;}
|
|
body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(ellipse 80% 60% at 20% 10%,rgba(100,40,140,.06) 0%,transparent 60%),radial-gradient(ellipse 60% 80% at 80% 90%,rgba(40,80,160,.05) 0%,transparent 60%);pointer-events:none;z-index:0;}
|
|
.wrap{position:relative;z-index:1;max-width:1440px;margin:0 auto;padding:0 24px 80px;}
|
|
|
|
/* HEADER */
|
|
.site-header{padding:34px 0 24px;border-bottom:1px solid var(--border);margin-bottom:0;}
|
|
.header-inner{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap;}
|
|
.site-title{font-family:'Cinzel',serif;font-size:40px;font-weight:700;letter-spacing:8px;color:var(--accent);text-transform:uppercase;line-height:1;}
|
|
.site-sub{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:4px;color:var(--muted);text-transform:uppercase;margin-top:5px;}
|
|
.header-right{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
.auth-chip{display:flex;align-items:center;gap:8px;background:var(--bg3);border:1px solid var(--border2);border-radius:3px;padding:7px 13px;}
|
|
.auth-avatar{width:26px;height:26px;border-radius:50%;object-fit:cover;}
|
|
.auth-name{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:1.5px;color:var(--text);}
|
|
.auth-role{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;padding:2px 7px;border-radius:2px;margin-left:4px;}
|
|
.role-admin{background:rgba(192,64,64,.15);color:var(--red2);border:1px solid rgba(192,64,64,.3);}
|
|
.role-user{background:rgba(74,122,192,.12);color:var(--blue2);border:1px solid rgba(74,122,192,.3);}
|
|
.mode-pills{display:flex;gap:4px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;padding:4px;}
|
|
.mode-pill{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;background:none;border:none;color:var(--muted);padding:7px 14px;border-radius:3px;cursor:pointer;transition:all .18s;position:relative;}
|
|
.mode-pill:hover{color:var(--muted2);}
|
|
.mode-pill.active{background:var(--bg5);color:var(--accent);border:1px solid var(--border2);}
|
|
.mode-pill.ap.active{background:rgba(192,64,64,.12);color:var(--red2);border-color:rgba(192,64,64,.3);}
|
|
.mode-pill.up.active{background:rgba(74,122,192,.12);color:var(--blue2);border-color:rgba(74,122,192,.3);}
|
|
.pill-badge{position:absolute;top:-6px;right:-6px;background:var(--red);color:#fff;font-family:'Share Tech Mono',monospace;font-size:9px;border-radius:10px;padding:1px 5px;min-width:16px;text-align:center;display:none;}
|
|
.pill-badge.show{display:block;}
|
|
|
|
/* TABS */
|
|
.top-nav{display:flex;gap:2px;border-bottom:1px solid var(--border);margin-bottom:28px;padding-top:6px;}
|
|
.nav-btn{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:3px;text-transform:uppercase;background:none;border:none;border-bottom:2px solid transparent;color:var(--muted);padding:10px 18px 12px;cursor:pointer;transition:all .18s;margin-bottom:-1px;}
|
|
.nav-btn:hover{color:var(--muted2);}
|
|
.nav-btn.active{color:var(--accent);border-bottom-color:var(--accent);}
|
|
.panel{display:none;}.panel.active{display:block;}
|
|
|
|
/* EV FILTER */
|
|
.filter-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:24px;}
|
|
.filter-label-sm{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;flex-basis:100%;margin-bottom:4px;}
|
|
.ev-btn{display:flex;align-items:center;gap:7px;padding:7px 14px;background:var(--bg3);border:1px solid var(--border2);border-radius:3px;cursor:pointer;font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:1.5px;color:var(--muted2);text-transform:uppercase;transition:all .18s;user-select:none;}
|
|
.ev-btn:hover{border-color:var(--border3);color:var(--text);}
|
|
.ev-btn.ef{background:rgba(200,169,110,.12);border-color:var(--accent);color:var(--accent2);}
|
|
.ev-btn.ex{background:rgba(192,64,64,.12);border-color:var(--red);color:var(--red2);text-decoration:line-through;}
|
|
.ev-num{background:var(--bg4);border-radius:10px;padding:1px 7px;font-size:9px;color:var(--muted);}
|
|
.ev-btn.ef .ev-num{background:rgba(200,169,110,.2);color:var(--accent);}
|
|
.result-bar{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--muted);flex-basis:100%;display:none;}
|
|
.result-bar span{color:var(--accent);}
|
|
|
|
/* GHOST GRID */
|
|
.ghost-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(295px,1fr));gap:14px;}
|
|
.ghost-card{background:var(--bg2);border:1px solid var(--border);border-radius:4px;overflow:hidden;cursor:pointer;transition:all .22s;}
|
|
.ghost-card:hover{border-color:var(--border2);transform:translateY(-2px);}
|
|
.ghost-card.hl{border-color:rgba(200,169,110,.45);box-shadow:0 0 0 1px rgba(200,169,110,.12) inset;}
|
|
.ghost-card.dm{opacity:.2;pointer-events:none;}
|
|
.card-hd{padding:15px 17px 11px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;gap:10px;}
|
|
.card-name{font-family:'Cinzel',serif;font-size:16px;font-weight:600;color:var(--accent2);letter-spacing:.8px;line-height:1.2;}
|
|
.card-num{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted);margin-top:3px;}
|
|
.sb{font-family:'Share Tech Mono',monospace;font-size:10px;padding:3px 8px;border-radius:2px;flex-shrink:0;white-space:nowrap;}
|
|
.sb-lo{background:rgba(74,156,106,.15);color:var(--green2);border:1px solid rgba(74,156,106,.3);}
|
|
.sb-mi{background:rgba(200,169,110,.12);color:var(--accent2);border:1px solid rgba(200,169,110,.25);}
|
|
.sb-hi{background:rgba(192,64,64,.15);color:var(--red2);border:1px solid rgba(192,64,64,.3);}
|
|
.sb-va{background:rgba(138,90,192,.12);color:var(--purple2);border:1px solid rgba(138,90,192,.3);}
|
|
.card-evs{padding:11px 17px;display:flex;flex-wrap:wrap;gap:5px;border-bottom:1px solid var(--border);}
|
|
.ev-pill{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1.5px;text-transform:uppercase;padding:3px 8px;border-radius:2px;background:var(--bg4);border:1px solid var(--border2);color:var(--muted2);transition:all .18s;}
|
|
.ev-pill.pf{background:rgba(200,169,110,.15);border-color:rgba(200,169,110,.4);color:var(--accent2);}
|
|
.ev-pill.px{background:rgba(192,64,64,.12);border-color:rgba(192,64,64,.35);color:var(--red2);text-decoration:line-through;}
|
|
.card-tip{padding:11px 17px 13px;font-size:13.5px;color:var(--muted2);line-height:1.5;font-style:italic;}
|
|
|
|
/* CHEAT TABLE */
|
|
.cheat-wrap{overflow-x:auto;}
|
|
.cheat-table{width:100%;border-collapse:collapse;font-size:13.5px;}
|
|
.cheat-table th{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);padding:10px 11px;border-bottom:1px solid var(--border2);text-align:center;white-space:nowrap;position:sticky;top:0;background:var(--bg);}
|
|
.cheat-table th:first-child{text-align:left;min-width:105px;}
|
|
.cheat-table td{padding:8px 11px;border-bottom:1px solid var(--border);text-align:center;vertical-align:middle;}
|
|
.cheat-table td:first-child{text-align:left;font-family:'Cinzel',serif;font-size:13px;color:var(--accent2);cursor:pointer;letter-spacing:.5px;}
|
|
.cheat-table td:first-child:hover{color:var(--accent);text-decoration:underline;}
|
|
.cheat-table tr:hover td{background:rgba(255,255,255,.02);}
|
|
.chk{color:var(--accent);}
|
|
.tip-cell{text-align:left;font-size:12px;color:var(--muted2);font-style:italic;min-width:200px;max-width:360px;}
|
|
.scc{font-family:'Share Tech Mono',monospace;font-size:10px;padding:2px 7px;border-radius:2px;display:inline-block;}
|
|
|
|
/* GUIDE */
|
|
.guide-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:24px;}
|
|
.guide-card{background:var(--bg3);border:1px solid var(--border2);border-radius:4px;padding:15px 17px;}
|
|
.guide-card-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin-bottom:8px;}
|
|
.guide-card-body{font-size:13.5px;color:var(--muted2);line-height:1.75;}
|
|
.guide-card-body strong{color:var(--text);}
|
|
.divider{display:flex;align-items:center;gap:14px;margin:28px 0 18px;}
|
|
.divider-line{flex:1;height:1px;background:var(--border);}
|
|
.divider-text{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:4px;color:var(--muted);text-transform:uppercase;white-space:nowrap;}
|
|
.acc-l{border-left:2px solid var(--accent);}
|
|
.red-l{border-left:2px solid var(--red);}
|
|
.blue-l{border-left:2px solid var(--blue);}
|
|
.purple-l{border-left:2px solid var(--purple);}
|
|
|
|
/* MODALS */
|
|
.modal-bg{display:none;position:fixed;inset:0;background:rgba(5,5,10,.88);z-index:200;overflow-y:auto;padding:36px 18px;pointer-events:none;}
|
|
.modal-bg.open{display:flex;align-items:flex-start;justify-content:center;pointer-events:all;}
|
|
.modal-box{background:var(--bg2);border:1px solid var(--border2);border-radius:6px;max-width:760px;width:100%;position:relative;animation:mIn .2s ease;}
|
|
.modal-box.wide{max-width:920px;}
|
|
@keyframes mIn{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
|
|
.modal-close{position:absolute;top:14px;right:16px;background:none;border:none;color:var(--muted);cursor:pointer;font-size:20px;padding:4px 8px;transition:color .15s;}
|
|
.modal-close:hover{color:var(--text);}
|
|
.modal-hd{padding:26px 30px 18px;border-bottom:1px solid var(--border);}
|
|
.modal-title{font-family:'Cinzel',serif;font-size:26px;font-weight:700;color:var(--accent);letter-spacing:3px;}
|
|
.modal-sub{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin-top:5px;}
|
|
.modal-body{padding:22px 30px 30px;}
|
|
.wiki-sec{margin-bottom:22px;}.wiki-sec:last-child{margin-bottom:0;}
|
|
.wiki-hd{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border);}
|
|
.wiki-text{font-size:14.5px;color:var(--muted2);line-height:1.7;}
|
|
.wiki-text strong{color:var(--text);}
|
|
.wiki-ev-row{display:flex;flex-wrap:wrap;gap:7px;}
|
|
.wiki-ev-p{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:1.5px;text-transform:uppercase;padding:5px 13px;border-radius:3px;background:var(--bg3);border:1px solid var(--border2);color:var(--muted2);}
|
|
.tells-list{display:flex;flex-direction:column;gap:7px;}
|
|
.tell{display:flex;gap:11px;align-items:flex-start;padding:9px 13px;background:var(--bg3);border-radius:3px;border-left:2px solid var(--accent);}
|
|
.tell-ico{flex-shrink:0;font-size:13px;margin-top:2px;}
|
|
.tell-text{font-size:13.5px;color:var(--muted2);}
|
|
.tell-text strong{color:var(--text);}
|
|
.sanity-blk{display:inline-flex;align-items:center;gap:11px;background:var(--bg3);border-radius:3px;padding:9px 15px;border:1px solid var(--border2);}
|
|
.sanity-pct{font-family:'Cinzel',serif;font-size:22px;}
|
|
.sanity-note{font-size:13px;color:var(--muted2);}
|
|
|
|
/* ADMIN */
|
|
.admin-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:24px;flex-wrap:wrap;}
|
|
.admin-title{font-family:'Cinzel',serif;font-size:22px;font-weight:600;color:var(--red2);letter-spacing:2px;}
|
|
.admin-sub{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin-top:3px;}
|
|
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:10px;margin-bottom:24px;}
|
|
.stat-card{background:var(--bg3);border:1px solid var(--border2);border-radius:4px;padding:12px 15px;}
|
|
.stat-val{font-family:'Cinzel',serif;font-size:22px;color:var(--accent2);line-height:1;}
|
|
.stat-label{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-top:4px;}
|
|
.admin-ghost-list{display:flex;flex-direction:column;gap:8px;}
|
|
.agr{display:flex;align-items:center;gap:12px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;padding:12px 16px;transition:border-color .18s;flex-wrap:wrap;}
|
|
.agr:hover{border-color:var(--border3);}
|
|
.agr-name{font-family:'Cinzel',serif;font-size:14px;color:var(--accent2);letter-spacing:.5px;min-width:100px;}
|
|
.agr-evs{display:flex;gap:5px;flex-wrap:wrap;flex:1;}
|
|
.agr-ev{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1px;text-transform:uppercase;padding:2px 7px;background:var(--bg4);border:1px solid var(--border2);border-radius:2px;color:var(--muted2);}
|
|
.agr-meta{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted);white-space:nowrap;}
|
|
.agr-btns{display:flex;gap:6px;flex-shrink:0;margin-left:auto;}
|
|
.icon-btn{background:none;border:1px solid var(--border2);border-radius:3px;padding:5px 10px;cursor:pointer;font-size:13px;color:var(--muted);transition:all .18s;}
|
|
.icon-btn.ibe:hover{color:var(--accent2);border-color:rgba(200,169,110,.4);}
|
|
.icon-btn.ibd:hover{color:var(--red2);border-color:rgba(192,64,64,.4);}
|
|
.sub-list{display:flex;flex-direction:column;gap:10px;}
|
|
.sub-item{background:var(--bg3);border:1px solid var(--border2);border-radius:4px;padding:15px 18px;}
|
|
.sub-meta{display:flex;align-items:center;gap:10px;margin-bottom:8px;flex-wrap:wrap;}
|
|
.sub-ghost{font-family:'Cinzel',serif;font-size:13px;color:var(--accent2);}
|
|
.sub-type{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;padding:2px 8px;border-radius:2px;border:1px solid;}
|
|
.stt{color:var(--blue2);border-color:rgba(74,122,192,.35);background:rgba(74,122,192,.1);}
|
|
.ste{color:var(--orange2);border-color:rgba(192,120,64,.35);background:rgba(192,120,64,.1);}
|
|
.stn{color:var(--green2);border-color:rgba(74,156,106,.35);background:rgba(74,156,106,.1);}
|
|
.sub-time{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted);margin-left:auto;}
|
|
.sub-content{font-size:13.5px;color:var(--muted2);line-height:1.6;margin-bottom:10px;font-style:italic;}
|
|
.sub-actions{display:flex;gap:7px;flex-wrap:wrap;}
|
|
.badge-count{display:inline-flex;align-items:center;justify-content:center;background:var(--red);color:#fff;font-family:'Share Tech Mono',monospace;font-size:9px;border-radius:10px;padding:1px 6px;margin-left:6px;min-width:18px;}
|
|
.empty-state{text-align:center;padding:40px 24px;color:var(--muted);font-style:italic;font-size:14px;}
|
|
.wl-section{background:var(--bg3);border:1px solid rgba(192,64,64,.25);border-radius:4px;padding:18px 20px;margin-bottom:20px;}
|
|
.wl-title{font-family:'Cinzel',serif;font-size:14px;color:var(--red2);letter-spacing:1px;margin-bottom:12px;}
|
|
.wl-row{display:flex;align-items:center;gap:10px;background:var(--bg4);border-radius:3px;padding:8px 12px;border:1px solid var(--border2);margin-bottom:6px;}
|
|
.wl-name{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--accent2);}
|
|
.wl-id{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--muted2);}
|
|
.wl-date{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted);margin-left:auto;}
|
|
|
|
/* FORMS */
|
|
.form-section{background:var(--bg3);border:1px solid var(--border2);border-radius:4px;padding:20px 22px;}
|
|
.form-title{font-family:'Cinzel',serif;font-size:15px;color:var(--accent2);letter-spacing:1px;margin-bottom:16px;}
|
|
.form-grid{display:flex;flex-direction:column;gap:14px;}
|
|
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
|
|
.form-row.full{grid-template-columns:1fr;}
|
|
.form-group{display:flex;flex-direction:column;gap:5px;}
|
|
.form-label{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);}
|
|
.form-input,.form-select,.form-textarea{background:var(--bg4);border:1px solid var(--border2);border-radius:3px;color:var(--text);font-family:'Crimson Pro',Georgia,serif;font-size:14px;padding:8px 12px;transition:border-color .18s;width:100%;}
|
|
.form-input:focus,.form-select:focus,.form-textarea:focus{outline:none;border-color:var(--border3);}
|
|
.form-textarea{resize:vertical;min-height:80px;line-height:1.55;}
|
|
.form-select option{background:var(--bg3);}
|
|
.form-hint{font-size:12px;color:var(--muted);font-style:italic;}
|
|
.ev-checks{display:flex;flex-wrap:wrap;gap:8px;}
|
|
.ev-check-lbl{display:flex;align-items:center;gap:6px;font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted2);cursor:pointer;padding:5px 10px;background:var(--bg4);border:1px solid var(--border2);border-radius:2px;transition:all .18s;user-select:none;}
|
|
.ev-check-lbl:hover{border-color:var(--border3);color:var(--text);}
|
|
.ev-check-lbl input{display:none;}
|
|
.ev-check-lbl.ck{background:rgba(200,169,110,.12);border-color:rgba(200,169,110,.4);color:var(--accent2);}
|
|
.tells-editor{display:flex;flex-direction:column;gap:8px;}
|
|
.tell-row{display:flex;gap:8px;align-items:center;}
|
|
.tell-row .form-input{flex:1;}
|
|
.tell-ico-in{width:58px;text-align:center;flex-shrink:0;}
|
|
.remove-btn{background:none;border:1px solid rgba(192,64,64,.3);border-radius:3px;color:var(--red2);cursor:pointer;padding:6px 10px;font-size:12px;transition:all .18s;flex-shrink:0;}
|
|
.remove-btn:hover{background:rgba(192,64,64,.12);}
|
|
.add-tell-btn{background:none;border:1px dashed var(--border2);border-radius:3px;color:var(--muted);cursor:pointer;padding:7px;font-size:11px;font-family:'Share Tech Mono',monospace;letter-spacing:2px;text-transform:uppercase;transition:all .18s;width:100%;}
|
|
.add-tell-btn:hover{border-color:var(--border3);color:var(--muted2);}
|
|
.form-footer{display:flex;gap:8px;justify-content:flex-end;margin-top:4px;flex-wrap:wrap;}
|
|
|
|
/* BUTTONS */
|
|
.btn-sm{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;background:none;border:1px solid var(--border2);border-radius:3px;padding:7px 14px;color:var(--muted);cursor:pointer;transition:all .18s;}
|
|
.btn-sm:hover{color:var(--text);border-color:var(--border3);}
|
|
.btn-accent{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;background:rgba(200,169,110,.12);border:1px solid rgba(200,169,110,.35);border-radius:3px;padding:8px 16px;color:var(--accent2);cursor:pointer;transition:all .18s;}
|
|
.btn-accent:hover{background:rgba(200,169,110,.22);}
|
|
.btn-danger{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;background:rgba(192,64,64,.12);border:1px solid rgba(192,64,64,.35);border-radius:3px;padding:8px 16px;color:var(--red2);cursor:pointer;transition:all .18s;}
|
|
.btn-danger:hover{background:rgba(192,64,64,.22);}
|
|
.btn-success{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;background:rgba(74,156,106,.12);border:1px solid rgba(74,156,106,.35);border-radius:3px;padding:8px 16px;color:var(--green2);cursor:pointer;transition:all .18s;}
|
|
.btn-success:hover{background:rgba(74,156,106,.22);}
|
|
.btn-blue{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;background:rgba(74,122,192,.12);border:1px solid rgba(74,122,192,.35);border-radius:3px;padding:8px 16px;color:var(--blue2);cursor:pointer;transition:all .18s;}
|
|
.btn-blue:hover{background:rgba(74,122,192,.22);}
|
|
.btn-discord{font-family:'Share Tech Mono',monospace;font-size:11px;letter-spacing:2px;text-transform:uppercase;background:var(--discord);border:none;border-radius:3px;padding:11px 22px;color:#fff;cursor:pointer;transition:opacity .18s;display:inline-flex;align-items:center;gap:10px;}
|
|
.btn-discord:hover{opacity:.88;}
|
|
.btn-discord svg{width:18px;height:18px;}
|
|
.btn-logout{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;background:none;border:1px solid var(--border2);border-radius:3px;padding:5px 10px;color:var(--muted);cursor:pointer;transition:all .18s;}
|
|
.btn-logout:hover{color:var(--red2);border-color:rgba(192,64,64,.4);}
|
|
|
|
/* LOGIN */
|
|
.login-screen{display:none;position:fixed;inset:0;background:var(--bg);z-index:500;flex-direction:column;align-items:center;justify-content:center;padding:24px;pointer-events:none;}
|
|
.login-screen.show{display:flex;pointer-events:all;}
|
|
.login-box{background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:48px 52px;max-width:440px;width:100%;text-align:center;}
|
|
.login-logo{font-family:'Cinzel',serif;font-size:48px;font-weight:700;letter-spacing:10px;color:var(--accent);line-height:1;margin-bottom:8px;}
|
|
.login-sub{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:4px;color:var(--muted);text-transform:uppercase;margin-bottom:36px;}
|
|
.login-desc{font-size:14px;color:var(--muted2);line-height:1.7;margin-bottom:28px;}
|
|
.login-desc strong{color:var(--text);}
|
|
.login-or{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:3px;color:var(--muted);margin:18px 0;display:block;}
|
|
.login-guest-note{font-size:12px;color:var(--muted);margin-top:18px;font-style:italic;}
|
|
|
|
/* COMMUNITY */
|
|
.user-title{font-family:'Cinzel',serif;font-size:22px;font-weight:600;color:var(--blue2);letter-spacing:2px;margin-bottom:3px;}
|
|
.usi{background:var(--bg3);border:1px solid var(--border2);border-radius:4px;padding:13px 16px;margin-bottom:10px;}
|
|
.usi-meta{display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;}
|
|
.usi-st{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;padding:2px 8px;border-radius:2px;border:1px solid;}
|
|
.ssp{color:var(--accent2);border-color:rgba(200,169,110,.35);background:rgba(200,169,110,.1);}
|
|
.ssa{color:var(--green2);border-color:rgba(74,156,106,.35);background:rgba(74,156,106,.1);}
|
|
.ssr{color:var(--red2);border-color:rgba(192,64,64,.35);background:rgba(192,64,64,.1);}
|
|
|
|
/* LOADING */
|
|
.loading{display:none;position:fixed;inset:0;background:rgba(5,5,10,.7);z-index:400;align-items:center;justify-content:center;flex-direction:column;gap:16px;pointer-events:none;}
|
|
.loading.show{display:flex;pointer-events:all;}
|
|
.spinner{width:36px;height:36px;border:2px solid var(--border2);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;}
|
|
@keyframes spin{to{transform:rotate(360deg);}}
|
|
.loading-txt{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;}
|
|
|
|
/* CONFIRM */
|
|
.confirm-bg{display:none;position:fixed;inset:0;background:rgba(5,5,10,.88);z-index:300;align-items:center;justify-content:center;pointer-events:none;}
|
|
.confirm-bg.open{display:flex;pointer-events:all;}
|
|
.confirm-box{background:var(--bg2);border:1px solid var(--border2);border-radius:6px;padding:28px 32px;max-width:380px;width:100%;text-align:center;}
|
|
.confirm-title{font-family:'Cinzel',serif;font-size:16px;color:var(--red2);letter-spacing:1px;margin-bottom:10px;}
|
|
.confirm-text{font-size:14px;color:var(--muted2);margin-bottom:22px;line-height:1.6;}
|
|
.confirm-btns{display:flex;gap:10px;justify-content:center;}
|
|
|
|
/* TOAST */
|
|
.toast-wrap{position:fixed;bottom:24px;right:24px;z-index:999;display:flex;flex-direction:column;gap:8px;pointer-events:none;}
|
|
.toast{font-family:'Share Tech Mono',monospace;font-size:11px;letter-spacing:1px;padding:11px 18px;border-radius:4px;border-left:3px solid;animation:tIn .25s ease;pointer-events:all;max-width:340px;}
|
|
@keyframes tIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
|
|
.toast-success{background:rgba(74,156,106,.18);border-color:var(--green2);color:var(--green2);}
|
|
.toast-danger{background:rgba(192,64,64,.18);border-color:var(--red2);color:var(--red2);}
|
|
.toast-info{background:rgba(74,122,192,.18);border-color:var(--blue2);color:var(--blue2);}
|
|
|
|
|
|
/* ═══════════ CREDITS PAGE ═══════════ */
|
|
.credits-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-bottom:32px;}
|
|
.credit-card{background:var(--bg2);border:1px solid var(--border2);border-radius:6px;padding:22px 24px;transition:all .22s;}
|
|
.credit-card:hover{border-color:var(--border3);transform:translateY(-2px);}
|
|
.credit-card.partner{border-color:rgba(200,169,110,.3);background:linear-gradient(135deg,var(--bg2) 0%,rgba(200,169,110,.04) 100%);}
|
|
.credit-avatar{width:56px;height:56px;border-radius:50%;object-fit:cover;background:var(--bg4);border:2px solid var(--border2);flex-shrink:0;}
|
|
.credit-avatar-placeholder{width:56px;height:56px;border-radius:50%;background:var(--bg4);border:2px solid var(--border2);display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;}
|
|
.credit-header{display:flex;align-items:center;gap:14px;margin-bottom:12px;}
|
|
.credit-name{font-family:'Cinzel',serif;font-size:16px;font-weight:600;color:var(--accent2);letter-spacing:.5px;}
|
|
.credit-role{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin-top:3px;}
|
|
.credit-role.partner-role{color:var(--accent);}
|
|
.credit-desc{font-size:13.5px;color:var(--muted2);line-height:1.65;}
|
|
.credit-links{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;}
|
|
.credit-link{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1.5px;text-transform:uppercase;padding:4px 10px;border-radius:2px;background:var(--bg4);border:1px solid var(--border2);color:var(--muted2);text-decoration:none;transition:all .18s;}
|
|
.credit-link:hover{color:var(--text);border-color:var(--border3);}
|
|
.credit-link.discord{background:rgba(88,101,242,.12);border-color:rgba(88,101,242,.35);color:#7289da;}
|
|
.credit-link.discord:hover{background:rgba(88,101,242,.22);}
|
|
.credit-link.roblox{background:rgba(192,64,64,.1);border-color:rgba(192,64,64,.3);color:var(--red2);}
|
|
.credit-link.roblox:hover{background:rgba(192,64,64,.2);}
|
|
.credit-link.website{background:rgba(74,122,192,.1);border-color:rgba(74,122,192,.3);color:var(--blue2);}
|
|
.credit-link.website:hover{background:rgba(74,122,192,.2);}
|
|
.partner-badge{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;padding:2px 8px;border-radius:2px;background:rgba(200,169,110,.15);border:1px solid rgba(200,169,110,.35);color:var(--accent2);margin-left:auto;flex-shrink:0;align-self:flex-start;}
|
|
.credits-section-title{font-family:'Cinzel',serif;font-size:14px;color:var(--accent);letter-spacing:3px;text-transform:uppercase;margin-bottom:16px;}
|
|
.credits-empty{text-align:center;padding:40px 24px;color:var(--muted);font-style:italic;font-size:14px;background:var(--bg3);border:1px dashed var(--border2);border-radius:6px;}
|
|
.add-credit-form{background:var(--bg3);border:1px solid var(--border2);border-radius:6px;padding:22px 24px;margin-bottom:28px;}
|
|
|
|
@media(max-width:640px){.site-title{font-size:28px;letter-spacing:4px;}.form-row{grid-template-columns:1fr;}.ghost-grid{grid-template-columns:1fr;}.modal-hd,.modal-body{padding-left:18px;padding-right:18px;}.login-box{padding:32px 24px;}.login-logo{font-size:34px;letter-spacing:6px;}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- LOGIN -->
|
|
<div class="login-screen show" id="login-screen">
|
|
<div class="login-box">
|
|
<div class="login-logo">BLAIR</div>
|
|
<div class="login-sub">Ghost Investigation Dashboard</div>
|
|
<div class="login-desc">
|
|
Melde dich mit Discord an, um <strong>Tips einzureichen</strong> und den Status deiner Einsendungen zu sehen.<br><br>
|
|
<strong>Admin-Zugriff</strong> wird nur für freigeschaltete Discord-Accounts gewährt.
|
|
</div>
|
|
<a class="btn-discord" href="/auth/discord" style="text-decoration:none;">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057.101 18.08.114 18.1.134 18.114a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/></svg>
|
|
Mit Discord anmelden
|
|
</a>
|
|
<span class="login-or">oder</span>
|
|
<button class="btn-sm" id="guest-btn">Als Gast fortfahren (nur lesen)</button>
|
|
<div class="login-guest-note">Gäste können nur Ghost Cards, Cheatsheet und Hunt Guide sehen.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LOADING -->
|
|
<div class="loading" id="loading"><div class="spinner"></div><div class="loading-txt" id="loading-txt">Lade...</div></div>
|
|
|
|
<!-- APP -->
|
|
<div class="wrap" id="app" style="display:none;">
|
|
<header class="site-header">
|
|
<div class="header-inner">
|
|
<div>
|
|
<div class="site-title">BLAIR</div>
|
|
<div class="site-sub">Ghost Investigation Dashboard · Roblox</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="auth-chip" id="auth-chip" style="display:none;">
|
|
<img class="auth-avatar" id="auth-av" src="" alt="">
|
|
<span class="auth-name" id="auth-nm"></span>
|
|
<span class="auth-role" id="auth-rl"></span>
|
|
<button class="btn-logout">Abmelden</button>
|
|
</div>
|
|
<span id="guest-badge" style="display:none;font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:var(--muted);">GAST</span>
|
|
<div class="mode-pills">
|
|
<button class="mode-pill active" id="pill-viewer">👁 Viewer</button>
|
|
<button class="mode-pill up" id="pill-user" style="display:none;">✦ Community<span class="pill-badge" id="badge-user"></span></button>
|
|
<button class="mode-pill ap" id="pill-admin" style="display:none;">⚙ Admin<span class="pill-badge" id="badge-admin"></span></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- VIEWER -->
|
|
<div id="mode-viewer">
|
|
<nav class="top-nav" id="vnav">
|
|
<button class="nav-btn active">Ghost Cards</button>
|
|
<button class="nav-btn">Cheatsheet</button>
|
|
<button class="nav-btn">Hunt Guide</button>
|
|
<button class="nav-btn">Credits</button>
|
|
</nav>
|
|
<div class="panel active" id="vtab-cards">
|
|
<div class="filter-row" id="ev-filter"><div class="filter-label-sm">Evidence Filter — einmal: gefunden · zweimal: ausgeschlossen</div></div>
|
|
<div class="ghost-grid" id="ghost-grid"></div>
|
|
</div>
|
|
<div class="panel" id="vtab-cheat">
|
|
<div class="cheat-wrap"><table class="cheat-table"><thead><tr>
|
|
<th>Ghost</th><th>EMF</th><th>❄</th><th>✍</th><th>UV</th><th>◎</th><th>📻</th><th>SLS</th><th>Sanity</th><th>Tips</th>
|
|
</tr></thead><tbody id="cheat-tbody"></tbody></table></div>
|
|
</div>
|
|
<div class="panel" id="vtab-guide">
|
|
<div style="max-width:800px;">
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text">Hunt Mechanics</div><div class="divider-line"></div></div>
|
|
<div class="guide-grid">
|
|
<div class="guide-card"><div class="guide-card-title">Grace Period</div><div class="guide-card-body">Easy — 5s<br>Medium — 4s<br>Hard — 3s<br><strong style="color:var(--red2)">Nightmare — 2s</strong></div></div>
|
|
<div class="guide-card"><div class="guide-card-title">Sanity Drain/min</div><div class="guide-card-body">Easy — 2%<br>Medium — 2%<br>Hard — 3%<br><strong style="color:var(--red2)">Nightmare — ~5%</strong></div></div>
|
|
<div class="guide-card"><div class="guide-card-title">Hunt Triggers</div><div class="guide-card-body">Sanity ≤ ghost threshold<br>Cursed objects bypass sanity<br>Spirit Board — no goodbye = hunt<br>Strigoi tap = 1/8 hunt chance</div></div>
|
|
<div class="guide-card"><div class="guide-card-title">Survival Priority</div><div class="guide-card-body">1. Crucifix in ghost room<br>2. Incense (stun/cleanse)<br>3. Break line of sight<br>4. Hide in closet/vehicle</div></div>
|
|
</div>
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text">Salt Tips</div><div class="divider-line"></div></div>
|
|
<div class="guide-card" style="margin-bottom:24px;"><div class="guide-card-body" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px 20px;">
|
|
<div><strong>Wraith</strong> — NEVER steps in salt.</div><div><strong>Harrow</strong> — Salt outside room = not Harrow.</div>
|
|
<div><strong>Revenant</strong> — Normal steps = no Revenant.</div><div><strong>Jiangshi</strong> — Skips the 2nd footstep.</div>
|
|
<div><strong>Yurei</strong> — Takes extra time to step on salt.</div><div><strong>Demon</strong> — Fast, accelerating steps.</div>
|
|
<div><strong>Strigoi</strong> — 4-finger UV handprint.</div><div><strong>Oni</strong> — Fast salt steps; opens/closes doors.</div>
|
|
</div></div>
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text">Cursed Objects</div><div class="divider-line"></div></div>
|
|
<div class="guide-grid">
|
|
<div class="guide-card red-l"><div class="guide-card-title">Spirit Board</div><div class="guide-card-body">No "Goodbye" = forced hunt. ZoZo overrides 50%.</div></div>
|
|
<div class="guide-card purple-l"><div class="guide-card-title">Tarot Cards</div><div class="guide-card-body">Death (hunt 10%), Moon (→0% sanity), Sun (→100%), Fool (negates prev).</div></div>
|
|
<div class="guide-card acc-l"><div class="guide-card-title">Summoning Circle</div><div class="guide-card-body">Light all 5 candles → ghost manifests → hunt after 5s.</div></div>
|
|
<div class="guide-card blue-l"><div class="guide-card-title">Music Box</div><div class="guide-card-body">Triggers cursed hunt once/match. Can lure ghost during hunts.</div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel" id="vtab-credits">
|
|
<div style="max-width:1000px;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:28px;flex-wrap:wrap;">
|
|
<div>
|
|
<div style="font-family:'Cinzel',serif;font-size:26px;font-weight:700;color:var(--accent);letter-spacing:3px;">Credits & Partner</div>
|
|
<div style="font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin-top:5px;">Danksagungen · Partner · Community</div>
|
|
</div>
|
|
<div id="credits-admin-btn" style="display:none;">
|
|
<button class="btn-accent" id="btn-add-credit" onclick="openCreditForm(null)">+ Eintrag hinzufügen</button>
|
|
</div>
|
|
</div>
|
|
<div id="credits-partners-section">
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text">Partner</div><div class="divider-line"></div></div>
|
|
<div class="credits-grid" id="credits-partners-grid"></div>
|
|
</div>
|
|
<div id="credits-credits-section">
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text">Credits</div><div class="divider-line"></div></div>
|
|
<div class="credits-grid" id="credits-credits-grid"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ADMIN -->
|
|
<div id="mode-admin" style="display:none;margin-top:24px;">
|
|
<div class="admin-header">
|
|
<div><div class="admin-title">⚙ Admin Dashboard</div><div class="admin-sub">Ghost Database Management · Nur für Whitelist-Admins</div></div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
|
<button class="btn-accent" id="btn-new-ghost">+ New Ghost</button>
|
|
<button class="btn-sm" id="btn-export">↓ Export</button>
|
|
</div>
|
|
</div>
|
|
<div class="stats-row" id="admin-stats"></div>
|
|
<div class="wl-section">
|
|
<div class="wl-title">🔐 Admin Whitelist</div>
|
|
<div id="wl-list" style="margin-bottom:12px;"></div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
|
<input type="text" class="form-input" id="wl-id" placeholder="Discord ID (18-stellig)" style="width:210px;">
|
|
<input type="text" class="form-input" id="wl-name" placeholder="Username (optional)" style="width:160px;">
|
|
<button class="btn-success" id="btn-add-admin">+ Hinzufügen</button>
|
|
</div>
|
|
<div class="form-hint" style="margin-top:8px;">Discord → Einstellungen → Erweitert → Entwicklermodus → Rechtsklick auf Nutzer → ID kopieren</div>
|
|
</div>
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text" id="sub-title">Community Submissions</div><div class="divider-line"></div></div>
|
|
<div class="sub-list" id="admin-sub-list" style="margin-bottom:28px;"></div>
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text">Ghost Database</div><div class="divider-line"></div></div>
|
|
<div class="admin-ghost-list" id="admin-ghost-list"></div>
|
|
</div>
|
|
|
|
<!-- COMMUNITY -->
|
|
<div id="mode-user" style="display:none;margin-top:24px;max-width:820px;">
|
|
<div class="user-title">✦ Community Hub</div>
|
|
<div style="font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin:4px 0 22px;">Tips einreichen · Fehler melden · Infos ergänzen</div>
|
|
<div class="form-section" style="margin-bottom:28px;">
|
|
<div class="form-title">Etwas einreichen</div>
|
|
<div class="form-grid">
|
|
<div class="form-row">
|
|
<div class="form-group"><label class="form-label">Geist</label><select class="form-select" id="usr-ghost"><option value="">— Geist auswählen —</option></select></div>
|
|
<div class="form-group"><label class="form-label">Art</label>
|
|
<select class="form-select" id="usr-type">
|
|
<option value="tip">💡 Neuer Tip / Trick</option>
|
|
<option value="edit">✏️ Fehler / Korrektur</option>
|
|
<option value="new">🆕 Neuer Geist fehlt</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group"><label class="form-label">Beschreibung</label>
|
|
<textarea class="form-textarea" id="usr-content" placeholder="Beschreib deinen Tip so genau wie möglich..." rows="4"></textarea>
|
|
<span class="form-hint">Je detaillierter, desto besser für den Admin!</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-footer"><button class="btn-blue" id="btn-submit-tip">Einreichen →</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="divider"><div class="divider-line"></div><div class="divider-text">Deine Einreichungen</div><div class="divider-line"></div></div>
|
|
<div id="user-sub-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ghost View Modal -->
|
|
<div class="modal-bg" id="ghost-modal">
|
|
<div class="modal-box"><button class="modal-close" id="close-ghost-modal">✕</button>
|
|
<div class="modal-hd"><div class="modal-title" id="gm-title"></div><div class="modal-sub" id="gm-sub"></div></div>
|
|
<div class="modal-body" id="gm-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ghost Editor Modal -->
|
|
<div class="modal-bg" id="edit-modal">
|
|
<div class="modal-box wide"><button class="modal-close" id="close-edit-modal">✕</button>
|
|
<div class="modal-hd"><div class="modal-title" id="em-title">Geist bearbeiten</div><div class="modal-sub" id="em-sub">Admin · Ghost Editor</div></div>
|
|
<div class="modal-body">
|
|
<div class="form-grid">
|
|
<div class="form-row">
|
|
<div class="form-group"><label class="form-label">Name</label><input type="text" class="form-input" id="ef-name"></div>
|
|
<div class="form-group"><label class="form-label">Sanity Threshold (%)</label><input type="number" class="form-input" id="ef-sanity" min="0" max="100"></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group"><label class="form-label">Hunt Type</label>
|
|
<select class="form-select" id="ef-hunt"><option value="mid">Mid (50%)</option><option value="low">Low (≤35%)</option><option value="high">High (≥80%)</option><option value="variable">Variable</option></select>
|
|
</div>
|
|
<div class="form-group"><label class="form-label">Sanity Note</label><input type="text" class="form-input" id="ef-ss" placeholder="z.B. Target's individual sanity"></div>
|
|
</div>
|
|
<div class="form-group"><label class="form-label">Evidence (genau 3 auswählen)</label><div class="ev-checks" id="ef-evs"></div></div>
|
|
<div class="form-row full"><div class="form-group"><label class="form-label">Kurz-Tip (Ghost Card)</label><textarea class="form-textarea" id="ef-tip" rows="2"></textarea></div></div>
|
|
<div class="form-row full"><div class="form-group"><label class="form-label">Beschreibung (Wiki)</label><textarea class="form-textarea" id="ef-desc" rows="3"></textarea></div></div>
|
|
<div class="form-group">
|
|
<label class="form-label">Identification Tells</label>
|
|
<div class="tells-editor" id="ef-tells"></div>
|
|
<button class="add-tell-btn" id="btn-add-tell">+ Tell hinzufügen</button>
|
|
</div>
|
|
<div class="form-footer">
|
|
<button class="btn-sm" id="btn-cancel-edit">Abbrechen</button>
|
|
<button class="btn-accent" id="btn-save-ghost">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Credits Editor Modal -->
|
|
<div class="modal-bg" id="credits-modal">
|
|
<div class="modal-box" style="max-width:600px;">
|
|
<button class="modal-close" id="close-credits-modal" onclick="closeModal('credits-modal')">✕</button>
|
|
<div class="modal-hd">
|
|
<div class="modal-title" id="cm-title">Eintrag hinzufügen</div>
|
|
<div class="modal-sub">Admin · Credits Editor</div>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-grid">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-input" id="cf-name" placeholder="z.B. GhostHunter99">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Typ</label>
|
|
<select class="form-select" id="cf-type">
|
|
<option value="credit">✦ Credit</option>
|
|
<option value="partner">⭐ Partner</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Rolle / Titel</label>
|
|
<input type="text" class="form-input" id="cf-role" placeholder="z.B. Wiki-Autor, Community Manager">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Avatar URL (optional)</label>
|
|
<input type="text" class="form-input" id="cf-avatar" placeholder="https://...">
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group">
|
|
<label class="form-label">Emoji / Icon (falls kein Avatar)</label>
|
|
<input type="text" class="form-input" id="cf-emoji" placeholder="👻" maxlength="4" style="width:80px;">
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group">
|
|
<label class="form-label">Beschreibung</label>
|
|
<textarea class="form-textarea" id="cf-desc" rows="2" placeholder="Kurze Beschreibung..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Discord (optional)</label>
|
|
<input type="text" class="form-input" id="cf-discord" placeholder="Username oder Server-Link">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Website (optional)</label>
|
|
<input type="text" class="form-input" id="cf-website" placeholder="https://...">
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group">
|
|
<label class="form-label">Roblox Profil URL (optional)</label>
|
|
<input type="text" class="form-input" id="cf-roblox" placeholder="https://www.roblox.com/users/...">
|
|
</div>
|
|
</div>
|
|
<div class="form-footer">
|
|
<button class="btn-sm" id="btn-cancel-credits" onclick="closeModal('credits-modal')">Abbrechen</button>
|
|
<button class="btn-accent" id="btn-save-credit" onclick="saveCreditEntry()">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="confirm-bg" id="confirm-bg">
|
|
<div class="confirm-box">
|
|
<div class="confirm-title" id="confirm-title">Sicher?</div>
|
|
<div class="confirm-text" id="confirm-text"></div>
|
|
<div class="confirm-btns"><button class="btn-sm">Abbrechen</button><button class="btn-danger" id="confirm-ok">Bestätigen</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="toast-wrap" id="toast-wrap"></div>
|
|
|
|
<script>
|
|
/* ── CONSTANTS ── */
|
|
const EV_KEYS = ["EMF","Freezing","Writing","UV","Orbs","SpiritBox","SLS"];
|
|
const EV_LABELS = {EMF:"EMF 5",Freezing:"Freezing",Writing:"Writing",UV:"UV",Orbs:"Orbs",SpiritBox:"Spirit Box",SLS:"SLS"};
|
|
|
|
/* ── STATE ── */
|
|
let ghosts=[], currentUser=null, ev_state={}, editingId=null, confirmCb=null;
|
|
EV_KEYS.forEach(k=>ev_state[k]=0);
|
|
|
|
/* ── INIT ── */
|
|
async function init() {
|
|
// Gast-Button Event hier setzen (kein inline onclick nötig)
|
|
document.getElementById('guest-btn').addEventListener('click', guestMode);
|
|
|
|
showLoading("Lade...");
|
|
try {
|
|
const r = await fetch('/auth/me');
|
|
if (!r.ok) throw new Error('auth failed');
|
|
const { user } = await r.json();
|
|
currentUser = user;
|
|
|
|
if (!user) {
|
|
hideLoading();
|
|
return; // login-screen ist schon sichtbar (show class per default)
|
|
}
|
|
|
|
await loadGhosts();
|
|
setupUI();
|
|
hideLoading();
|
|
hideLogin(); // erst jetzt verstecken
|
|
document.getElementById('app').style.display='block';
|
|
} catch(e) {
|
|
hideLoading();
|
|
console.error('Init error:', e);
|
|
// Login screen bleibt sichtbar
|
|
}
|
|
}
|
|
|
|
function guestMode() {
|
|
hideLogin();
|
|
showLoading("Lade...");
|
|
loadGhosts().then(()=>{
|
|
hideLoading();
|
|
setupGuestUI();
|
|
document.getElementById('app').style.display='block';
|
|
}).catch(e=>{
|
|
hideLoading();
|
|
console.error(e);
|
|
});
|
|
}
|
|
|
|
/* ── UI SETUP ── */
|
|
function setupUI() {
|
|
const u = currentUser;
|
|
const chip = document.getElementById('auth-chip');
|
|
chip.style.display='flex';
|
|
const av = document.getElementById('auth-av');
|
|
if(u.avatar){av.src=u.avatar;av.style.display='block';}else{av.style.display='none';}
|
|
document.getElementById('auth-nm').textContent=u.username;
|
|
const rl = document.getElementById('auth-rl');
|
|
if(u.isAdmin){rl.textContent='Admin';rl.className='auth-role role-admin';}
|
|
else{rl.textContent='User';rl.className='auth-role role-user';}
|
|
document.getElementById('pill-user').style.display='block';
|
|
if(u.isAdmin) document.getElementById('pill-admin').style.display='block';
|
|
buildCards(); buildCheat();
|
|
populateGhostSelect();
|
|
if(u.isAdmin) updateAdminBadge();
|
|
// Credits-Tab sofort vorbereiten falls aktiv
|
|
if(document.getElementById('vtab-credits')?.classList.contains('active')) renderCredits();
|
|
}
|
|
|
|
function setupGuestUI() {
|
|
document.getElementById('guest-badge').style.display='block';
|
|
buildCards(); buildCheat();
|
|
}
|
|
|
|
/* ── AUTH ── */
|
|
async function doLogout() {
|
|
await fetch('/auth/logout', {method:'POST'});
|
|
location.reload();
|
|
}
|
|
|
|
/* ── FETCH HELPERS ── */
|
|
async function api(method, path, body) {
|
|
const opts = { method, headers:{'Content-Type':'application/json'} };
|
|
if(body) opts.body=JSON.stringify(body);
|
|
const r = await fetch(path, opts);
|
|
const d = await r.json();
|
|
if(!r.ok) throw new Error(d.error||'Fehler');
|
|
return d;
|
|
}
|
|
|
|
/* ── GHOSTS ── */
|
|
async function loadGhosts() {
|
|
const data = await api('GET','/api/ghosts');
|
|
ghosts = data.map(row=>({...row,
|
|
evidence: Array.isArray(row.evidence)?row.evidence:(typeof row.evidence==='string'?JSON.parse(row.evidence):[]),
|
|
tells: Array.isArray(row.tells)?row.tells:(typeof row.tells==='string'?JSON.parse(row.tells):[]),
|
|
}));
|
|
}
|
|
|
|
function sbC(g){return g.hunt==='high'?'sb-hi':g.hunt==='low'?'sb-lo':g.hunt==='variable'?'sb-va':'sb-mi';}
|
|
|
|
function buildFilter() {
|
|
const bar=document.getElementById('ev-filter'), lbl=bar.querySelector('.filter-label-sm');
|
|
bar.innerHTML=''; bar.appendChild(lbl);
|
|
EV_KEYS.forEach(ev=>{
|
|
const cnt=ghosts.filter(g=>g.evidence.includes(ev)).length;
|
|
const b=document.createElement('button'); b.className='ev-btn'; b.dataset.ev=ev;
|
|
b.innerHTML=`${EV_LABELS[ev]} <span class="ev-num">${cnt}</span>`;
|
|
b.onclick=()=>{ev_state[ev]=(ev_state[ev]+1)%3;b.classList.toggle('ef',ev_state[ev]===1);b.classList.toggle('ex',ev_state[ev]===2);applyFilter();};
|
|
bar.appendChild(b);
|
|
});
|
|
const rb=document.createElement('button'); rb.className='btn-sm'; rb.textContent='Reset';
|
|
rb.onclick=()=>{EV_KEYS.forEach(k=>ev_state[k]=0);document.querySelectorAll('.ev-btn').forEach(b=>b.classList.remove('ef','ex'));applyFilter();};
|
|
bar.appendChild(rb);
|
|
const ri=document.createElement('div'); ri.className='result-bar'; ri.id='result-bar'; bar.appendChild(ri);
|
|
}
|
|
|
|
function applyFilter() {
|
|
const found=EV_KEYS.filter(k=>ev_state[k]===1),excl=EV_KEYS.filter(k=>ev_state[k]===2);
|
|
let m=0;
|
|
document.querySelectorAll('.ghost-card').forEach(c=>{
|
|
const g=ghosts[c.dataset.idx];if(!g)return;
|
|
const ok=!found.some(e=>!g.evidence.includes(e))&&!excl.some(e=>g.evidence.includes(e));
|
|
const act=found.length||excl.length;
|
|
c.classList.toggle('hl',act&&ok); c.classList.toggle('dm',act&&!ok);
|
|
if(ok&&act)m++;
|
|
c.querySelectorAll('.ev-pill').forEach(p=>{p.classList.toggle('pf',ev_state[p.dataset.ev]===1);p.classList.toggle('px',ev_state[p.dataset.ev]===2);});
|
|
});
|
|
const ri=document.getElementById('result-bar');
|
|
if(ri){ri.style.display=(found.length||excl.length)?'block':'none';ri.innerHTML=`<span>${m}</span> possible ghost${m!==1?'s':''} matching evidence`;}
|
|
}
|
|
|
|
function buildCards() {
|
|
buildFilter();
|
|
const grid=document.getElementById('ghost-grid'); grid.innerHTML='';
|
|
ghosts.forEach((g,i)=>{
|
|
const d=document.createElement('div'); d.className='ghost-card'; d.dataset.idx=i;
|
|
d.innerHTML=`<div class="card-hd"><div><div class="card-name">${g.name}</div><div class="card-num">Ghost #${String(i+1).padStart(2,'0')}</div></div><div class="sb ${sbC(g)}">${g.sanity}%${g.hunt==='variable'?'↕':''}</div></div><div class="card-evs">${g.evidence.map(e=>`<span class="ev-pill" data-ev="${e}">${EV_LABELS[e]||e}</span>`).join('')}</div><div class="card-tip">${g.tip||''}</div>`;
|
|
d.onclick=()=>openViewModal(i); grid.appendChild(d);
|
|
});
|
|
}
|
|
|
|
function buildCheat() {
|
|
const tb=document.getElementById('cheat-tbody'); tb.innerHTML='';
|
|
ghosts.forEach((g,i)=>{
|
|
const tr=document.createElement('tr');
|
|
tr.innerHTML=`<td onclick="openViewModal(${i})">${g.name}</td>${EV_KEYS.map(e=>`<td>${g.evidence.includes(e)?'<span class="chk">✓</span>':''}</td>`).join('')}<td><span class="scc ${sbC(g)}">${g.sanity}%${g.hunt==='variable'?'↕':''}</span></td><td class="tip-cell">${(g.tip||'').split(' · ').map(t=>`<div>· ${t}</div>`).join('')}</td>`;
|
|
tb.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function populateGhostSelect() {
|
|
const sel=document.getElementById('usr-ghost');
|
|
sel.innerHTML='<option value="">— Geist auswählen —</option>'+ghosts.map(g=>`<option value="${g.name}">${g.name}</option>`).join('');
|
|
}
|
|
|
|
function rebuildAll() { buildCards(); buildCheat(); populateGhostSelect(); }
|
|
|
|
/* ── VIEW MODAL ── */
|
|
function openViewModal(idx) {
|
|
const g=ghosts[idx]; if(!g)return;
|
|
const sc=g.sanity>=80?'var(--red2)':g.sanity<=35?'var(--green2)':'var(--accent2)';
|
|
document.getElementById('gm-title').textContent=g.name.toUpperCase();
|
|
document.getElementById('gm-sub').textContent='Evidence · '+g.evidence.map(e=>EV_LABELS[e]||e).join(' · ');
|
|
document.getElementById('gm-body').innerHTML=`
|
|
<div class="wiki-sec"><div class="wiki-hd">Overview</div><div class="wiki-text">${g.description||'No description yet.'}</div></div>
|
|
<div class="wiki-sec"><div class="wiki-hd">Evidence</div><div class="wiki-ev-row">${g.evidence.map(e=>`<span class="wiki-ev-p">${EV_LABELS[e]||e}</span>`).join('')}</div></div>
|
|
<div class="wiki-sec"><div class="wiki-hd">Hunt Threshold</div><div class="sanity-blk"><span class="sanity-pct" style="color:${sc}">${g.sanity}%${g.hunt==='variable'?' ↕':''}</span><span class="sanity-note">${g.sanity_special||''}</span></div></div>
|
|
<div class="wiki-sec"><div class="wiki-hd">Identification Tells</div><div class="tells-list">${(g.tells||[]).map(t=>`<div class="tell"><span class="tell-ico">${t.icon}</span><span class="tell-text">${t.text}</span></div>`).join('')}</div></div>`;
|
|
openModal('ghost-modal');
|
|
}
|
|
|
|
/* ── MODE ── */
|
|
function setMode(m) {
|
|
['viewer','admin','user'].forEach(x=>document.getElementById('mode-'+x).style.display=x===m?'block':'none');
|
|
document.querySelectorAll('.mode-pill').forEach(p=>p.classList.remove('active'));
|
|
const mp={viewer:document.querySelectorAll('.mode-pill')[0],user:document.getElementById('pill-user'),admin:document.getElementById('pill-admin')};
|
|
if(mp[m])mp[m].classList.add('active');
|
|
if(m==='admin')renderAdmin();
|
|
if(m==='user'){populateGhostSelect();renderUserSubs();}
|
|
}
|
|
|
|
function switchTab(t) {
|
|
['cards','cheat','guide','credits'].forEach((x,i)=>{
|
|
const el = document.getElementById('vtab-'+x);
|
|
if(el) el.classList.toggle('active',x===t);
|
|
const btn = document.querySelectorAll('#vnav .nav-btn')[i];
|
|
if(btn) btn.classList.toggle('active',x===t);
|
|
});
|
|
if(t==='credits') renderCredits();
|
|
}
|
|
|
|
/* ── ADMIN ── */
|
|
async function renderAdmin() {
|
|
// Stats
|
|
try {
|
|
const s=await api('GET','/admin/stats');
|
|
document.getElementById('admin-stats').innerHTML=`
|
|
<div class="stat-card"><div class="stat-val">${s.ghosts}</div><div class="stat-label">Ghosts</div></div>
|
|
<div class="stat-card"><div class="stat-val">${s.total}</div><div class="stat-label">Submissions</div></div>
|
|
<div class="stat-card"><div class="stat-val" style="color:${s.pending?'var(--accent2)':'var(--muted2)'}">${s.pending}</div><div class="stat-label">Pending</div></div>
|
|
<div class="stat-card"><div class="stat-val">${s.approved}</div><div class="stat-label">Approved</div></div>
|
|
<div class="stat-card"><div class="stat-val">${s.rejected}</div><div class="stat-label">Rejected</div></div>`;
|
|
// badge
|
|
const b=document.getElementById('badge-admin');b.textContent=s.pending;b.classList.toggle('show',s.pending>0);
|
|
} catch{}
|
|
|
|
// Whitelist
|
|
try {
|
|
const wl=await api('GET','/admin/whitelist');
|
|
const wlEl=document.getElementById('wl-list');
|
|
if(!wl.length){wlEl.innerHTML='<div style="font-size:13px;color:var(--muted);font-style:italic;">Noch keine weiteren Admins.</div>';}
|
|
else{wlEl.innerHTML=wl.map(w=>`<div class="wl-row"><span style="font-size:16px">⚙</span><span class="wl-name">${escH(w.discord_username||'—')}</span><span class="wl-id">${w.discord_id}</span><span class="wl-date">${fmtTime(w.added_at)}</span><button class="icon-btn ibd" onclick="removeAdmin('${w.discord_id}')" title="Entfernen">🗑</button></div>`).join('');}
|
|
} catch{}
|
|
|
|
// Submissions (pending)
|
|
try {
|
|
const subs=await api('GET','/admin/submissions?status=pending');
|
|
const sl=document.getElementById('admin-sub-list');
|
|
document.getElementById('sub-title').innerHTML=`Community Submissions${subs.length?`<span class="badge-count">${subs.length}</span>`:''}`;
|
|
if(!subs.length){sl.innerHTML='<div class="empty-state">Keine ausstehenden Einreichungen 📭</div>';}
|
|
else{sl.innerHTML='';subs.forEach(sub=>{
|
|
const tl=sub.type==='tip'?'💡 Tip':sub.type==='edit'?'✏️ Korrektur':'🆕 Neuer Geist';
|
|
const tc=sub.type==='tip'?'stt':sub.type==='edit'?'ste':'stn';
|
|
const d=document.createElement('div');d.className='sub-item';
|
|
d.innerHTML=`<div class="sub-meta"><span class="sub-ghost">${escH(sub.ghost_name||'Allgemein')}</span><span class="sub-type ${tc}">${tl}</span><span style="font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted)">von ${escH(sub.username||'Anonym')}</span><span class="sub-time">${fmtTime(sub.created_at)}</span></div><div class="sub-content">${escH(sub.content)}</div><div class="sub-actions"><button class="btn-success" onclick="approveSub(${sub.id})">✓ Annehmen</button><button class="btn-danger" onclick="rejectSub(${sub.id})">✕ Ablehnen</button><button class="btn-accent" onclick="injectSub(${sub.id},'${escH(sub.ghost_name)}')">→ Bearbeiten & Einfügen</button></div>`;
|
|
sl.appendChild(d);
|
|
});}
|
|
} catch{}
|
|
|
|
// Ghost list
|
|
const gl=document.getElementById('admin-ghost-list');gl.innerHTML='';
|
|
ghosts.forEach(g=>{
|
|
const row=document.createElement('div');row.className='agr';
|
|
row.innerHTML=`<div class="agr-name">${g.name}</div><div class="agr-evs">${g.evidence.map(e=>`<span class="agr-ev">${EV_LABELS[e]||e}</span>`).join('')}</div><div class="agr-meta">von ${g.updated_by||'—'}</div><div class="agr-btns"><button class="icon-btn ibe" onclick="openGhostForm('${g.id}')" title="Bearbeiten">✏</button><button class="icon-btn ibd" onclick="confirmDel('${g.id}')" title="Löschen">🗑</button></div>`;
|
|
gl.appendChild(row);
|
|
});
|
|
}
|
|
|
|
async function updateAdminBadge() {
|
|
try{const s=await api('GET','/admin/stats');const b=document.getElementById('badge-admin');b.textContent=s.pending;b.classList.toggle('show',s.pending>0);}catch{}
|
|
}
|
|
|
|
async function addAdmin() {
|
|
const id=document.getElementById('wl-id').value.trim(),name=document.getElementById('wl-name').value.trim();
|
|
if(!id||id.length<10){toast("Gültige Discord ID eingeben!","danger");return;}
|
|
try{await api('POST','/admin/whitelist',{discord_id:id,discord_username:name||id});toast("Admin hinzugefügt ✓","success");document.getElementById('wl-id').value='';document.getElementById('wl-name').value='';renderAdmin();}
|
|
catch(e){toast(e.message,"danger");}
|
|
}
|
|
|
|
async function removeAdmin(id) {
|
|
showConfirm("Admin entfernen?","Diese Person verliert sofort den Admin-Zugriff.",async()=>{
|
|
try{await api('DELETE',`/admin/whitelist/${id}`);toast("Admin entfernt.","danger");renderAdmin();}
|
|
catch(e){toast(e.message,"danger");}
|
|
});
|
|
}
|
|
|
|
async function approveSub(id){
|
|
try{await api('PATCH',`/admin/submissions/${id}`,{status:'approved'});toast("Angenommen ✓","success");renderAdmin();}
|
|
catch(e){toast(e.message,"danger");}
|
|
}
|
|
async function rejectSub(id){
|
|
try{await api('PATCH',`/admin/submissions/${id}`,{status:'rejected'});toast("Abgelehnt.","danger");renderAdmin();}
|
|
catch(e){toast(e.message,"danger");}
|
|
}
|
|
async function injectSub(id,ghostName){
|
|
try{await api('PATCH',`/admin/submissions/${id}`,{status:'approved'});}catch{}
|
|
const g=ghosts.find(x=>x.name===ghostName);
|
|
openGhostForm(g?.id||null);
|
|
setTimeout(()=>toast("💡 Submission-Info anzeigen: prüfe den Tip!","info"),300);
|
|
renderAdmin();
|
|
}
|
|
|
|
/* ── GHOST EDITOR ── */
|
|
function openGhostForm(id) {
|
|
const g=id?ghosts.find(x=>x.id===id):null;
|
|
editingId=id||null;
|
|
document.getElementById('em-title').textContent=g?`Bearbeiten: ${g.name}`:'Neuen Geist erstellen';
|
|
document.getElementById('em-sub').textContent=g?`Admin · Ghost Editor · ID: ${g.id}`:'Admin · Ghost Editor · Neu';
|
|
document.getElementById('ef-name').value=g?.name||'';
|
|
document.getElementById('ef-sanity').value=g?.sanity??50;
|
|
document.getElementById('ef-hunt').value=g?.hunt||'mid';
|
|
document.getElementById('ef-ss').value=g?.sanity_special||'';
|
|
document.getElementById('ef-tip').value=g?.tip||'';
|
|
document.getElementById('ef-desc').value=g?.description||'';
|
|
const evDiv=document.getElementById('ef-evs');evDiv.innerHTML='';
|
|
EV_KEYS.forEach(ev=>{
|
|
const ck=g?.evidence.includes(ev);
|
|
const lbl=document.createElement('label');lbl.className='ev-check-lbl'+(ck?' ck':'');
|
|
lbl.innerHTML=`<input type="checkbox" value="${ev}"${ck?' checked':''}>${EV_LABELS[ev]}`;
|
|
lbl.querySelector('input').onchange=e=>lbl.classList.toggle('ck',e.target.checked);
|
|
evDiv.appendChild(lbl);
|
|
});
|
|
const td=document.getElementById('ef-tells');td.innerHTML='';
|
|
(g?.tells||[{icon:'💡',text:''}]).forEach(t=>addTellRow(t.icon,t.text));
|
|
openModal('edit-modal');
|
|
}
|
|
|
|
function addTellRow(icon='💡',text=''){
|
|
const td=document.getElementById('ef-tells');
|
|
const row=document.createElement('div');row.className='tell-row';
|
|
row.innerHTML=`<input type="text" class="form-input tell-ico-in" value="${escH(icon)}" placeholder="🔥" maxlength="4"><input type="text" class="form-input" value="${escH(text)}" placeholder="Tell text... use <strong>bold</strong>"><button class="remove-btn" onclick="this.parentElement.remove()">✕</button>`;
|
|
td.appendChild(row);
|
|
}
|
|
|
|
async function saveGhost() {
|
|
const name=document.getElementById('ef-name').value.trim();
|
|
if(!name){toast("Name darf nicht leer sein!","danger");return;}
|
|
const evs=Array.from(document.querySelectorAll('#ef-evs input:checked')).map(i=>i.value);
|
|
if(evs.length!==3){toast(`Genau 3 Evidenzen auswählen (aktuell: ${evs.length})!`,"danger");return;}
|
|
const tells=Array.from(document.querySelectorAll('.tell-row')).map(row=>{const ins=row.querySelectorAll('input');return{icon:ins[0].value||'💡',text:ins[1].value};}).filter(t=>t.text.trim());
|
|
const data={
|
|
id:editingId||name.toLowerCase().replace(/[^a-z0-9]/g,'')+Date.now().toString(36),
|
|
name,evidence:evs,sanity:parseInt(document.getElementById('ef-sanity').value)||50,
|
|
hunt:document.getElementById('ef-hunt').value,
|
|
sanity_special:document.getElementById('ef-ss').value,
|
|
tip:document.getElementById('ef-tip').value,
|
|
description:document.getElementById('ef-desc').value,
|
|
tells
|
|
};
|
|
showLoading("Speichere...");
|
|
try{
|
|
if(editingId){await api('PUT',`/admin/ghosts/${editingId}`,data);}
|
|
else{await api('POST','/admin/ghosts',data);}
|
|
toast(`✓ ${name} gespeichert!`,"success");
|
|
closeModal('edit-modal');
|
|
await loadGhosts(); rebuildAll(); renderAdmin();
|
|
}catch(e){toast(e.message,"danger");}
|
|
hideLoading();
|
|
}
|
|
|
|
async function confirmDel(id){
|
|
const g=ghosts.find(x=>x.id===id);if(!g)return;
|
|
showConfirm(`${g.name} löschen?`,`Dauerhaft aus der Datenbank entfernen.`,async()=>{
|
|
showLoading("Lösche...");
|
|
try{await api('DELETE',`/admin/ghosts/${id}`);toast(`${g.name} gelöscht.`,"danger");await loadGhosts();rebuildAll();renderAdmin();}
|
|
catch(e){toast(e.message,"danger");}
|
|
hideLoading();
|
|
});
|
|
}
|
|
|
|
/* ── COMMUNITY ── */
|
|
async function submitTip(){
|
|
const ghost=document.getElementById('usr-ghost').value,type=document.getElementById('usr-type').value,content=document.getElementById('usr-content').value.trim();
|
|
if(!content){toast("Bitte Inhalt eingeben!","danger");return;}
|
|
showLoading("Sende...");
|
|
try{await api('POST','/api/submissions',{ghost_name:ghost,type,content});toast("Einreichung gesendet ✓","success");document.getElementById('usr-content').value='';renderUserSubs();}
|
|
catch(e){toast(e.message,"danger");}
|
|
hideLoading();
|
|
}
|
|
|
|
async function renderUserSubs(){
|
|
const ul=document.getElementById('user-sub-list');
|
|
try{
|
|
const subs=await api('GET','/api/submissions/mine');
|
|
if(!subs.length){ul.innerHTML='<div class="empty-state">📭 Du hast noch nichts eingereicht.</div>';return;}
|
|
ul.innerHTML='';
|
|
subs.forEach(sub=>{
|
|
const tl=sub.type==='tip'?'💡 Tip':sub.type==='edit'?'✏️ Korrektur':'🆕 Neuer Geist';
|
|
const sc=sub.status==='approved'?'ssa':sub.status==='rejected'?'ssr':'ssp';
|
|
const sl=sub.status==='approved'?'✓ Angenommen':sub.status==='rejected'?'✕ Abgelehnt':'⏳ Ausstehend';
|
|
const d=document.createElement('div');d.className='usi';
|
|
d.innerHTML=`<div class="usi-meta"><span style="font-family:'Cinzel',serif;font-size:13px;color:var(--accent2)">${escH(sub.ghost_name||'Allgemein')}</span><span style="font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted2)">${tl}</span><span class="usi-st ${sc}">${sl}</span><span class="sub-time">${fmtTime(sub.created_at)}</span></div><div style="font-size:13.5px;color:var(--muted2);line-height:1.55;font-style:italic">${escH(sub.content)}</div>${sub.admin_note?`<div style="font-size:12px;color:var(--accent2);margin-top:6px">Admin: ${escH(sub.admin_note)}</div>`:''}`;
|
|
ul.appendChild(d);
|
|
});
|
|
const approved=subs.filter(s=>s.status==='approved').length;
|
|
const ub=document.getElementById('badge-user');ub.textContent=approved;ub.classList.toggle('show',approved>0);
|
|
}catch{ul.innerHTML='<div class="empty-state">Fehler beim Laden.</div>';}
|
|
}
|
|
|
|
/* ── EXPORT ── */
|
|
function exportGhosts(){const b=new Blob([JSON.stringify({ghosts},null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='blair-ghosts.json';a.click();toast("Exportiert ↓","info");}
|
|
|
|
/* ── MODAL / CONFIRM / TOAST ── */
|
|
function openModal(id){document.getElementById(id).classList.add('open');document.body.style.overflow='hidden';}
|
|
function closeModal(id){document.getElementById(id).classList.remove('open');document.body.style.overflow='';}
|
|
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeModal('ghost-modal');closeModal('edit-modal');closeConfirm();}});
|
|
function showConfirm(title,text,cb){document.getElementById('confirm-title').textContent=title;document.getElementById('confirm-text').textContent=text;confirmCb=cb;document.getElementById('confirm-bg').classList.add('open');}
|
|
function closeConfirm(){document.getElementById('confirm-bg').classList.remove('open');confirmCb=null;}
|
|
document.getElementById('confirm-ok').onclick=()=>{if(confirmCb)confirmCb();closeConfirm();};
|
|
function toast(msg,type='info'){const w=document.getElementById('toast-wrap'),t=document.createElement('div');t.className=`toast toast-${type}`;t.textContent=msg;w.appendChild(t);setTimeout(()=>{t.style.cssText='opacity:0;transition:opacity .3s';setTimeout(()=>t.remove(),350);},3400);}
|
|
|
|
/* ── LOADING / LOGIN ── */
|
|
function showLoading(msg='Lade...'){document.getElementById('loading-txt').textContent=msg;document.getElementById('loading').classList.add('show');}
|
|
function hideLoading(){document.getElementById('loading').classList.remove('show');}
|
|
function showLogin(){document.getElementById('login-screen').classList.add('show');}
|
|
function hideLogin(){document.getElementById('login-screen').classList.remove('show');}
|
|
|
|
/* ── UTILS ── */
|
|
function fmtTime(ts){if(!ts)return'—';const d=new Date(ts);return d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'})+' '+d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});}
|
|
function escH(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
CREDITS
|
|
═══════════════════════════════════════════════════ */
|
|
let credits = [];
|
|
let editingCreditId = null;
|
|
|
|
async function loadCredits() {
|
|
try {
|
|
credits = await api('GET', '/api/credits');
|
|
} catch (e) {
|
|
console.error('Credits laden fehlgeschlagen:', e);
|
|
credits = [];
|
|
}
|
|
}
|
|
|
|
async function renderCredits() {
|
|
await loadCredits();
|
|
const partners = credits.filter(c => c.type === 'partner');
|
|
const crds = credits.filter(c => c.type === 'credit');
|
|
|
|
const partnerGrid = document.getElementById('credits-partners-grid');
|
|
const creditsGrid = document.getElementById('credits-credits-grid');
|
|
const partnerSec = document.getElementById('credits-partners-section');
|
|
const creditsSec = document.getElementById('credits-credits-section');
|
|
|
|
// Partners
|
|
if (!partners.length) {
|
|
partnerGrid.innerHTML = `<div class="credits-empty">${currentUser?.isAdmin ? 'Noch keine Partner eingetragen. Nutze "+ Eintrag hinzufügen" oben rechts.' : 'Noch keine Partner eingetragen.'}</div>`;
|
|
} else {
|
|
partnerGrid.innerHTML = '';
|
|
partners.forEach(c => partnerGrid.appendChild(buildCreditCard(c)));
|
|
}
|
|
|
|
// Credits
|
|
if (!crds.length) {
|
|
creditsGrid.innerHTML = `<div class="credits-empty">${currentUser?.isAdmin ? 'Noch keine Credits eingetragen. Nutze "+ Eintrag hinzufügen" oben rechts.' : 'Noch keine Credits eingetragen.'}</div>`;
|
|
} else {
|
|
creditsGrid.innerHTML = '';
|
|
crds.forEach(c => creditsGrid.appendChild(buildCreditCard(c)));
|
|
}
|
|
|
|
// Show add button for admins — immer neu verdrahten nach jedem render
|
|
const adminBtn = document.getElementById('credits-admin-btn');
|
|
if (adminBtn) {
|
|
adminBtn.style.display = currentUser?.isAdmin ? 'block' : 'none';
|
|
// onclick is set directly in HTML
|
|
}
|
|
}
|
|
|
|
function buildCreditCard(c) {
|
|
const card = document.createElement('div');
|
|
card.className = 'credit-card' + (c.type === 'partner' ? ' partner' : '');
|
|
|
|
const avatarHtml = c.avatar
|
|
? `<img class="credit-avatar" src="${escH(c.avatar)}" alt="${escH(c.name)}" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">`
|
|
+ `<div class="credit-avatar-placeholder" style="display:none">${c.emoji || '👤'}</div>`
|
|
: `<div class="credit-avatar-placeholder">${c.emoji || '👤'}</div>`;
|
|
|
|
const linksHtml = [
|
|
c.discord ? `<a class="credit-link discord" href="${c.discord.startsWith('http') ? escH(c.discord) : '#'}" target="_blank" rel="noopener">Discord</a>` : '',
|
|
c.website ? `<a class="credit-link website" href="${escH(c.website)}" target="_blank" rel="noopener">Website</a>` : '',
|
|
c.roblox ? `<a class="credit-link roblox" href="${escH(c.roblox)}" target="_blank" rel="noopener">Roblox</a>` : '',
|
|
].filter(Boolean).join('');
|
|
|
|
const adminBtns = currentUser?.isAdmin
|
|
? `<div style="display:flex;gap:6px;margin-top:10px;">
|
|
<button class="icon-btn ibe" onclick="openCreditForm('${c.id}')" title="Bearbeiten">✏</button>
|
|
<button class="icon-btn ibd" onclick="confirmDeleteCredit('${c.id}')" title="Löschen">🗑</button>
|
|
</div>` : '';
|
|
|
|
card.innerHTML = `
|
|
<div class="credit-header">
|
|
${avatarHtml}
|
|
<div style="flex:1;min-width:0;">
|
|
<div class="credit-name">${escH(c.name)}</div>
|
|
<div class="credit-role ${c.type === 'partner' ? 'partner-role' : ''}">${escH(c.role || '')}</div>
|
|
</div>
|
|
${c.type === 'partner' ? '<span class="partner-badge">Partner</span>' : ''}
|
|
</div>
|
|
${(c.desc||c.description) ? `<div class="credit-desc">${escH(c.desc||c.description)}</div>` : ''}
|
|
${linksHtml ? `<div class="credit-links">${linksHtml}</div>` : ''}
|
|
${adminBtns}`;
|
|
|
|
return card;
|
|
}
|
|
|
|
function openCreditForm(id) {
|
|
const c = id ? credits.find(x => x.id === id) : null;
|
|
editingCreditId = id || null;
|
|
document.getElementById('cm-title').textContent = c ? `Bearbeiten: ${c.name}` : 'Eintrag hinzufügen';
|
|
document.getElementById('cf-name').value = c?.name || '';
|
|
document.getElementById('cf-type').value = c?.type || 'credit';
|
|
document.getElementById('cf-role').value = c?.role || '';
|
|
document.getElementById('cf-avatar').value = c?.avatar || '';
|
|
document.getElementById('cf-emoji').value = c?.emoji || '';
|
|
document.getElementById('cf-desc').value = c?.desc || c?.description || '';
|
|
document.getElementById('cf-discord').value = c?.discord || '';
|
|
document.getElementById('cf-website').value = c?.website || '';
|
|
document.getElementById('cf-roblox').value = c?.roblox || '';
|
|
openModal('credits-modal');
|
|
}
|
|
|
|
async function saveCreditEntry() {
|
|
const name = document.getElementById('cf-name').value.trim();
|
|
if (!name) { toast("Name darf nicht leer sein!", "danger"); return; }
|
|
|
|
const entry = {
|
|
id: editingCreditId || 'c_' + Date.now().toString(36),
|
|
name,
|
|
type: document.getElementById('cf-type').value,
|
|
role: document.getElementById('cf-role').value.trim(),
|
|
avatar: document.getElementById('cf-avatar').value.trim(),
|
|
emoji: document.getElementById('cf-emoji').value.trim() || '👤',
|
|
description: document.getElementById('cf-desc').value.trim(),
|
|
discord: document.getElementById('cf-discord').value.trim(),
|
|
website: document.getElementById('cf-website').value.trim(),
|
|
roblox: document.getElementById('cf-roblox').value.trim(),
|
|
};
|
|
|
|
showLoading("Speichere...");
|
|
try {
|
|
if (editingCreditId) {
|
|
await api('PUT', `/admin/credits/${editingCreditId}`, entry);
|
|
} else {
|
|
await api('POST', '/admin/credits', entry);
|
|
}
|
|
closeModal('credits-modal');
|
|
await renderCredits();
|
|
toast(`✓ ${name} gespeichert!`, "success");
|
|
} catch (e) {
|
|
toast(e.message, "danger");
|
|
}
|
|
hideLoading();
|
|
}
|
|
|
|
function confirmDeleteCredit(id) {
|
|
const c = credits.find(x => x.id === id);
|
|
if (!c) return;
|
|
showConfirm(`${c.name} entfernen?`, 'Dieser Eintrag wird aus Credits & Partner entfernt.', async () => {
|
|
showLoading("Lösche...");
|
|
try {
|
|
await api('DELETE', `/admin/credits/${id}`);
|
|
await renderCredits();
|
|
toast(`${c.name} entfernt.`, "danger");
|
|
} catch (e) {
|
|
toast(e.message, "danger");
|
|
}
|
|
hideLoading();
|
|
});
|
|
}
|
|
|
|
/* ── BOOT ── */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
// ── Mode pills ──
|
|
document.getElementById('pill-viewer').addEventListener('click', () => setMode('viewer'));
|
|
const pu = document.getElementById('pill-user');
|
|
const pa = document.getElementById('pill-admin');
|
|
if (pu) pu.addEventListener('click', () => setMode('user'));
|
|
if (pa) pa.addEventListener('click', () => setMode('admin'));
|
|
|
|
// ── Viewer tabs ──
|
|
document.querySelectorAll('#vnav .nav-btn').forEach((btn, i) => {
|
|
const tabs = ['cards', 'cheat', 'guide', 'credits'];
|
|
btn.addEventListener('click', () => switchTab(tabs[i]));
|
|
});
|
|
|
|
// ── Auth ──
|
|
const logoutBtn = document.querySelector('.btn-logout');
|
|
if (logoutBtn) logoutBtn.addEventListener('click', doLogout);
|
|
|
|
// ── Admin actions ──
|
|
const newGhost = document.getElementById('btn-new-ghost');
|
|
const exportBtn = document.getElementById('btn-export');
|
|
const addAdminBtn = document.getElementById('btn-add-admin');
|
|
if (newGhost) newGhost.addEventListener('click', () => openGhostForm(null));
|
|
if (exportBtn) exportBtn.addEventListener('click', exportGhosts);
|
|
if (addAdminBtn) addAdminBtn.addEventListener('click', addAdmin);
|
|
|
|
// ── Community ──
|
|
const submitBtn = document.getElementById('btn-submit-tip');
|
|
if (submitBtn) submitBtn.addEventListener('click', submitTip);
|
|
|
|
// ── Ghost view modal ──
|
|
document.getElementById('ghost-modal').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('ghost-modal')) closeModal('ghost-modal');
|
|
});
|
|
document.getElementById('close-ghost-modal').addEventListener('click', () => closeModal('ghost-modal'));
|
|
|
|
// ── Edit modal ──
|
|
document.getElementById('edit-modal').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('edit-modal')) closeModal('edit-modal');
|
|
});
|
|
document.getElementById('close-edit-modal').addEventListener('click', () => closeModal('edit-modal'));
|
|
document.getElementById('btn-cancel-edit').addEventListener('click', () => closeModal('edit-modal'));
|
|
document.getElementById('btn-add-tell').addEventListener('click', addTellRow);
|
|
document.getElementById('btn-save-ghost').addEventListener('click', saveGhost);
|
|
|
|
// ── Confirm dialog ──
|
|
document.querySelector('.confirm-bg .btn-sm').addEventListener('click', closeConfirm);
|
|
document.getElementById('confirm-ok').addEventListener('click', () => {
|
|
if (confirmCb) confirmCb();
|
|
closeConfirm();
|
|
});
|
|
|
|
// ── Keyboard shortcuts ──
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
closeModal('ghost-modal');
|
|
closeModal('edit-modal');
|
|
closeConfirm();
|
|
}
|
|
});
|
|
|
|
// ── Guest button ──
|
|
const guestBtn = document.getElementById('guest-btn');
|
|
if (guestBtn) guestBtn.addEventListener('click', guestMode);
|
|
|
|
// ── Credits ──
|
|
document.getElementById('close-credits-modal').addEventListener('click', () => closeModal('credits-modal'));
|
|
document.getElementById('btn-cancel-credits').addEventListener('click', () => closeModal('credits-modal'));
|
|
document.getElementById('btn-save-credit').addEventListener('click', saveCreditEntry);
|
|
document.getElementById('credits-modal').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('credits-modal')) closeModal('credits-modal');
|
|
});
|
|
const addCreditBtn = document.getElementById('btn-add-credit');
|
|
if (addCreditBtn) addCreditBtn.addEventListener('click', () => openCreditForm(null));
|
|
|
|
init();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|