.
This commit is contained in:
parent
9ee465dd59
commit
95fc830017
@ -1913,19 +1913,20 @@
|
||||
<div class="login-logo">BLAIR</div>
|
||||
<div class="login-sub">Ghost Investigation Dashboard</div>
|
||||
<div class="login-desc">
|
||||
Login with discord to submit <Strong>Tips</strong> and check the status of your submissions.<br><br>
|
||||
<strong>Admin access</strong> will only be granted to unlocked Discord accounts.
|
||||
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>
|
||||
Login with Discord
|
||||
Mit Discord anmelden
|
||||
</a>
|
||||
<span class="login-or">or</span>
|
||||
<button class="btn-sm" id="guest-btn">Login with discord</button>
|
||||
<div class="login-guest-note">Guests can only view Ghost Cards, Cheatsheet and Hunt Guide.</div>
|
||||
<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>
|
||||
|
||||
@ -1941,14 +1942,14 @@
|
||||
<div class="header-inner">
|
||||
<div>
|
||||
<div class="site-title">BLAIR</div>
|
||||
<div class="site-sub">Ghost Investigation Dashboard · Blair Cheatsheet</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">Logout</button>
|
||||
<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>
|
||||
@ -1973,7 +1974,7 @@
|
||||
</nav>
|
||||
<div class="panel active" id="vtab-cards">
|
||||
<div class="filter-row" id="ev-filter">
|
||||
<div class="filter-label-sm">Evidence Filter — single click: found · double click: excluded</div>
|
||||
<div class="filter-label-sm">Evidence Filter — einmal: gefunden · zweimal: ausgeschlossen</div>
|
||||
</div>
|
||||
<div class="ghost-grid" id="ghost-grid"></div>
|
||||
</div>
|
||||
@ -2151,35 +2152,35 @@
|
||||
<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;">
|
||||
submit tips · report errors · add information</div>
|
||||
Tips einreichen · Fehler melden · Infos ergänzen</div>
|
||||
<div class="form-section" style="margin-bottom:28px;">
|
||||
<div class="form-title">Submit something</div>
|
||||
<div class="form-title">Etwas einreichen</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label class="form-label">Ghost</label><select class="form-select" id="usr-ghost">
|
||||
<option value="">— select ghost —</option>
|
||||
<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">Type</label>
|
||||
<div class="form-group"><label class="form-label">Art</label>
|
||||
<select class="form-select" id="usr-type">
|
||||
<option value="tip">💡 New tip / trick</option>
|
||||
<option value="edit">✏️ Error / correction</option>
|
||||
<option value="new">🆕 Ghost missing</option>
|
||||
<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">Description</label>
|
||||
<div class="form-group"><label class="form-label">Beschreibung</label>
|
||||
<textarea class="form-textarea" id="usr-content"
|
||||
placeholder="Describe your tip as detailed as possible..." rows="4"></textarea>
|
||||
<span class="form-hint">The more detailed, the better for the admin!</span>
|
||||
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">Submit →</button></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">Your submissions</div>
|
||||
<div class="divider-text">Deine Einreichungen</div>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
<div id="user-sub-list"></div>
|
||||
@ -2201,7 +2202,7 @@
|
||||
<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">Edit Ghost</div>
|
||||
<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">
|
||||
@ -2222,27 +2223,27 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">Sanity Note</label><input type="text" class="form-input"
|
||||
id="ef-ss" placeholder="e.g. Target's individual sanity"></div>
|
||||
id="ef-ss" placeholder="z.B. Target's individual sanity"></div>
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">Evidence (select exactly 3)</label>
|
||||
<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">Short Tip (Ghost Card)</label><textarea
|
||||
<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">Description (Wiki)</label><textarea class="form-textarea"
|
||||
<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">+ Add tell</button>
|
||||
<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">Cancel</button>
|
||||
<button class="btn-accent" id="btn-save-ghost">Save</button>
|
||||
<button class="btn-sm" id="btn-cancel-edit">Abbrechen</button>
|
||||
<button class="btn-accent" id="btn-save-ghost">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2255,7 +2256,7 @@
|
||||
<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">Add Entry</div>
|
||||
<div class="modal-title" id="cm-title">Eintrag hinzufügen</div>
|
||||
<div class="modal-sub">Admin · Credits Editor</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@ -2263,10 +2264,10 @@
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-input" id="cf-name" placeholder="e.g. GhostHunter99">
|
||||
<input type="text" class="form-input" id="cf-name" placeholder="z.B. GhostHunter99">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Type</label>
|
||||
<label class="form-label">Typ</label>
|
||||
<select class="form-select" id="cf-type">
|
||||
<option value="credit">✦ Credit</option>
|
||||
<option value="partner">⭐ Partner</option>
|
||||
@ -2275,8 +2276,8 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Role / Title</label>
|
||||
<input type="text" class="form-input" id="cf-role" placeholder="e.g. Wiki-Author, Community Manager">
|
||||
<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>
|
||||
@ -2285,20 +2286,20 @@
|
||||
</div>
|
||||
<div class="form-row full">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Emoji / Icon (if no Avatar)</label>
|
||||
<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">Description</label>
|
||||
<textarea class="form-textarea" id="cf-desc" rows="2" placeholder="Short description..."></textarea>
|
||||
<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 or server link">
|
||||
<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>
|
||||
@ -2307,13 +2308,13 @@
|
||||
</div>
|
||||
<div class="form-row full">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Roblox Profile URL (optional)</label>
|
||||
<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')">Cancel</button>
|
||||
<button class="btn-accent" id="btn-save-credit" onclick="saveCreditEntry()">Save</button>
|
||||
<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>
|
||||
@ -2322,10 +2323,10 @@
|
||||
|
||||
<div class="confirm-bg" id="confirm-bg">
|
||||
<div class="confirm-box">
|
||||
<div class="confirm-title" id="confirm-title">Sure?</div>
|
||||
<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">Cancel</button><button class="btn-danger"
|
||||
id="confirm-ok">Confirm</button></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>
|
||||
@ -2396,7 +2397,6 @@
|
||||
if (u.isAdmin) document.getElementById('pill-admin').style.display = 'block';
|
||||
buildCards(); buildCheat();
|
||||
populateGhostSelect();
|
||||
loadCredits();
|
||||
if (u.isAdmin) updateAdminBadge();
|
||||
// Credits-Tab sofort vorbereiten falls aktiv
|
||||
if (document.getElementById('vtab-credits')?.classList.contains('active')) renderCredits();
|
||||
@ -2405,7 +2405,6 @@
|
||||
function setupGuestUI() {
|
||||
document.getElementById('guest-badge').style.display = 'block';
|
||||
buildCards(); buildCheat();
|
||||
loadCredits();
|
||||
}
|
||||
|
||||
/* ── AUTH ── */
|
||||
@ -2595,18 +2594,18 @@
|
||||
}
|
||||
|
||||
async function approveSub(id) {
|
||||
try { await api('PATCH', `/admin/submissions/${id}`, { status: 'approved' }); toast("Accepted ✓", "success"); renderAdmin(); }
|
||||
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("Rejected ✕", "danger"); renderAdmin(); }
|
||||
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("💡 Show submission info: check the tip!", "info"), 300);
|
||||
setTimeout(() => toast("💡 Submission-Info anzeigen: prüfe den Tip!", "info"), 300);
|
||||
renderAdmin();
|
||||
}
|
||||
|
||||
@ -2614,8 +2613,8 @@
|
||||
function openGhostForm(id) {
|
||||
const g = id ? ghosts.find(x => x.id === id) : null;
|
||||
editingId = id || null;
|
||||
document.getElementById('em-title').textContent = g ? `Edit: ${g.name}` : 'New Ghost';
|
||||
document.getElementById('em-sub').textContent = g ? `Admin · Ghost Editor · ID: ${g.id}` : 'Admin · Ghost Editor · New';
|
||||
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';
|
||||
@ -2692,19 +2691,19 @@
|
||||
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">📭 You have not submitted anything yet.</div>'; return; }
|
||||
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' ? '✓ Accepted' : sub.status === 'rejected' ? '✕ Rejected' : '⏳ Waiting';
|
||||
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">Failed to load.</div>'; }
|
||||
} catch { ul.innerHTML = '<div class="empty-state">Fehler beim Laden.</div>'; }
|
||||
}
|
||||
|
||||
/* ── EXPORT ── */
|
||||
@ -2736,17 +2735,17 @@
|
||||
let credits = [];
|
||||
let editingCreditId = null;
|
||||
|
||||
// Load from localStorage (no separate API needed — admin manages locally)
|
||||
function loadCredits() {
|
||||
try { credits = JSON.parse(localStorage.getItem('blair_credits') || '[]'); }
|
||||
catch { credits = []; }
|
||||
}
|
||||
function saveCredits() {
|
||||
localStorage.setItem('blair_credits', JSON.stringify(credits));
|
||||
async function loadCredits() {
|
||||
try {
|
||||
credits = await api('GET', '/api/credits');
|
||||
} catch (e) {
|
||||
console.error('Credits laden fehlgeschlagen:', e);
|
||||
credits = [];
|
||||
}
|
||||
}
|
||||
|
||||
function renderCredits() {
|
||||
loadCredits();
|
||||
async function renderCredits() {
|
||||
await loadCredits();
|
||||
const partners = credits.filter(c => c.type === 'partner');
|
||||
const crds = credits.filter(c => c.type === 'credit');
|
||||
|
||||
@ -2809,7 +2808,7 @@
|
||||
</div>
|
||||
${c.type === 'partner' ? '<span class="partner-badge">Partner</span>' : ''}
|
||||
</div>
|
||||
${c.desc ? `<div class="credit-desc">${escH(c.desc)}</div>` : ''}
|
||||
${(c.desc || c.description) ? `<div class="credit-desc">${escH(c.desc || c.description)}</div>` : ''}
|
||||
${linksHtml ? `<div class="credit-links">${linksHtml}</div>` : ''}
|
||||
${adminBtns}`;
|
||||
|
||||
@ -2825,14 +2824,14 @@
|
||||
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 || '';
|
||||
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');
|
||||
}
|
||||
|
||||
function saveCreditEntry() {
|
||||
async function saveCreditEntry() {
|
||||
const name = document.getElementById('cf-name').value.trim();
|
||||
if (!name) { toast("Name darf nicht leer sein!", "danger"); return; }
|
||||
|
||||
@ -2843,33 +2842,41 @@
|
||||
role: document.getElementById('cf-role').value.trim(),
|
||||
avatar: document.getElementById('cf-avatar').value.trim(),
|
||||
emoji: document.getElementById('cf-emoji').value.trim() || '👤',
|
||||
desc: document.getElementById('cf-desc').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(),
|
||||
};
|
||||
|
||||
if (editingCreditId) {
|
||||
const idx = credits.findIndex(c => c.id === editingCreditId);
|
||||
if (idx > -1) credits[idx] = entry;
|
||||
} else {
|
||||
credits.push(entry);
|
||||
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");
|
||||
}
|
||||
|
||||
saveCredits();
|
||||
closeModal('credits-modal');
|
||||
renderCredits();
|
||||
toast(`✓ ${name} gespeichert!`, "success");
|
||||
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.', () => {
|
||||
credits = credits.filter(x => x.id !== id);
|
||||
saveCredits();
|
||||
renderCredits();
|
||||
toast(`${c.name} entfernt.`, "danger");
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// Alle Admin-Routen erfordern Admin-Zugriff
|
||||
@ -10,11 +10,11 @@ router.use(requireAdmin);
|
||||
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const [[{ ghosts }]] = await db.execute('SELECT COUNT(*) as ghosts FROM ghosts');
|
||||
const [[{ total }]] = await db.execute('SELECT COUNT(*) as total FROM submissions');
|
||||
const [[{ ghosts }]] = await db.execute('SELECT COUNT(*) as ghosts FROM ghosts');
|
||||
const [[{ total }]] = await db.execute('SELECT COUNT(*) as total FROM submissions');
|
||||
const [[{ pending }]] = await db.execute("SELECT COUNT(*) as pending FROM submissions WHERE status='pending'");
|
||||
const [[{ approved }]]= await db.execute("SELECT COUNT(*) as approved FROM submissions WHERE status='approved'");
|
||||
const [[{ rejected }]]= await db.execute("SELECT COUNT(*) as rejected FROM submissions WHERE status='rejected'");
|
||||
const [[{ approved }]] = await db.execute("SELECT COUNT(*) as approved FROM submissions WHERE status='approved'");
|
||||
const [[{ rejected }]] = await db.execute("SELECT COUNT(*) as rejected FROM submissions WHERE status='rejected'");
|
||||
res.json({ ghosts, total, pending, approved, rejected });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'DB-Fehler' });
|
||||
@ -33,9 +33,9 @@ router.post('/ghosts', async (req, res) => {
|
||||
await db.query(
|
||||
`INSERT INTO ghosts (id, name, evidence, sanity, hunt, sanity_special, tip, description, tells, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[id, name, JSON.stringify(evidence), sanity||50, hunt||'mid',
|
||||
sanity_special||'', tip||'', description||'', JSON.stringify(tells||[]),
|
||||
req.user.username]
|
||||
[id, name, JSON.stringify(evidence), sanity || 50, hunt || 'mid',
|
||||
sanity_special || '', tip || '', description || '', JSON.stringify(tells || []),
|
||||
req.user.username]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
@ -55,9 +55,9 @@ router.put('/ghosts/:id', async (req, res) => {
|
||||
const result = await db.query(
|
||||
`UPDATE ghosts SET name=?, evidence=?, sanity=?, hunt=?, sanity_special=?,
|
||||
tip=?, description=?, tells=?, updated_by=? WHERE id=?`,
|
||||
[name, JSON.stringify(evidence), sanity||50, hunt||'mid',
|
||||
sanity_special||'', tip||'', description||'', JSON.stringify(tells||[]),
|
||||
req.user.username, req.params.id]
|
||||
[name, JSON.stringify(evidence), sanity || 50, hunt || 'mid',
|
||||
sanity_special || '', tip || '', description || '', JSON.stringify(tells || []),
|
||||
req.user.username, req.params.id]
|
||||
);
|
||||
if (result.affectedRows === 0) return res.status(404).json({ error: 'Geist nicht gefunden' });
|
||||
res.json({ ok: true });
|
||||
@ -96,11 +96,11 @@ router.get('/submissions', async (req, res) => {
|
||||
// Submission reviewen (approve/reject)
|
||||
router.patch('/submissions/:id', async (req, res) => {
|
||||
const { status, admin_note } = req.body;
|
||||
if (!['approved','rejected'].includes(status)) return res.status(400).json({ error: 'Status muss approved oder rejected sein' });
|
||||
if (!['approved', 'rejected'].includes(status)) return res.status(400).json({ error: 'Status muss approved oder rejected sein' });
|
||||
try {
|
||||
await db.query(
|
||||
`UPDATE submissions SET status=?, admin_note=?, reviewed_at=NOW(), reviewed_by=? WHERE id=?`,
|
||||
[status, admin_note||'', req.user.username, req.params.id]
|
||||
[status, admin_note || '', req.user.username, req.params.id]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
@ -127,7 +127,7 @@ router.post('/whitelist', async (req, res) => {
|
||||
try {
|
||||
await db.query(
|
||||
'INSERT INTO admin_whitelist (discord_id, discord_username, added_by) VALUES (?,?,?)',
|
||||
[discord_id.trim(), discord_username||discord_id, req.user.username]
|
||||
[discord_id.trim(), discord_username || discord_id, req.user.username]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
@ -150,3 +150,53 @@ router.delete('/whitelist/:discord_id', async (req, res) => {
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
// ── Credits CRUD (admin only) ─────────────────────────
|
||||
|
||||
// Erstellen
|
||||
router.post('/credits', async (req, res) => {
|
||||
const { id, name, type, role, avatar, emoji, description, discord, website, roblox, sort_order } = req.body;
|
||||
if (!id || !name) return res.status(400).json({ error: 'id und name erforderlich' });
|
||||
try {
|
||||
await db.query(
|
||||
`INSERT INTO credits (id, name, type, role, avatar, emoji, description, discord, website, roblox, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[id, name, type || 'credit', role || '', avatar || '', emoji || '👤',
|
||||
description || '', discord || '', website || '', roblox || '', sort_order || 0]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
if (e.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'ID bereits vorhanden' });
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'DB-Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
// Bearbeiten
|
||||
router.put('/credits/:id', async (req, res) => {
|
||||
const { name, type, role, avatar, emoji, description, discord, website, roblox, sort_order } = req.body;
|
||||
if (!name) return res.status(400).json({ error: 'name erforderlich' });
|
||||
try {
|
||||
const result = await db.query(
|
||||
`UPDATE credits SET name=?, type=?, role=?, avatar=?, emoji=?, description=?,
|
||||
discord=?, website=?, roblox=?, sort_order=? WHERE id=?`,
|
||||
[name, type || 'credit', role || '', avatar || '', emoji || '👤',
|
||||
description || '', discord || '', website || '', roblox || '', sort_order || 0, req.params.id]
|
||||
);
|
||||
if (result.affectedRows === 0) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'DB-Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
// Löschen
|
||||
router.delete('/credits/:id', async (req, res) => {
|
||||
try {
|
||||
await db.query('DELETE FROM credits WHERE id=?', [req.params.id]);
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'DB-Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
|
||||
// ── Ghosts (public read) ──────────────────────────────
|
||||
@ -12,7 +12,7 @@ router.get('/ghosts', async (req, res) => {
|
||||
const ghosts = rows.map(row => ({
|
||||
...row,
|
||||
evidence: typeof row.evidence === 'string' ? JSON.parse(row.evidence) : row.evidence,
|
||||
tells: typeof row.tells === 'string' ? JSON.parse(row.tells) : row.tells,
|
||||
tells: typeof row.tells === 'string' ? JSON.parse(row.tells) : row.tells,
|
||||
}));
|
||||
res.json(ghosts);
|
||||
} catch (e) {
|
||||
@ -40,7 +40,7 @@ router.get('/submissions/mine', requireAuth, async (req, res) => {
|
||||
router.post('/submissions', requireAuth, async (req, res) => {
|
||||
const { ghost_name, type, content } = req.body;
|
||||
if (!content?.trim()) return res.status(400).json({ error: 'Inhalt fehlt' });
|
||||
if (!['tip','edit','new'].includes(type)) return res.status(400).json({ error: 'Ungültiger Typ' });
|
||||
if (!['tip', 'edit', 'new'].includes(type)) return res.status(400).json({ error: 'Ungültiger Typ' });
|
||||
|
||||
try {
|
||||
await db.query(
|
||||
@ -56,3 +56,17 @@ router.post('/submissions', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
// ── Credits (public read) ─────────────────────────────
|
||||
|
||||
router.get('/credits', async (req, res) => {
|
||||
try {
|
||||
const rows = await db.query(
|
||||
'SELECT * FROM credits ORDER BY type DESC, sort_order ASC, created_at ASC'
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'DB-Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
20
schema.sql
20
schema.sql
@ -74,6 +74,26 @@ CREATE TABLE IF NOT EXISTS submissions (
|
||||
INDEX idx_discord_id (discord_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
-- ─── Credits & Partner ──────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS credits (
|
||||
id VARCHAR(80) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'credit',
|
||||
role VARCHAR(150) NOT NULL DEFAULT '',
|
||||
avatar VARCHAR(255) NOT NULL DEFAULT '',
|
||||
emoji VARCHAR(10) NOT NULL DEFAULT '👤',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
discord VARCHAR(255) NOT NULL DEFAULT '',
|
||||
website VARCHAR(255) NOT NULL DEFAULT '',
|
||||
roblox VARCHAR(255) NOT NULL DEFAULT '',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_sort (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Default Ghosts ─────────────────────────────────────
|
||||
INSERT IGNORE INTO ghosts (id, name, evidence, sanity, hunt, sanity_special, tip, description, tells) VALUES
|
||||
('banshee','Banshee','["EMF","Freezing","SLS"]',50,'mid','Target individual sanity — not team average','Special Scream via Parabolic Mic · höhere Wahrscheinlichkeit Weinen','The Banshee chooses a single target and only chases that player until they die, then re-rolls.','[{"icon":"🎙️","text":"<strong>Parabolic scream</strong> audible within ~28 studs"},{"icon":"😢","text":"<strong>Crying Event</strong> more common than most ghosts"},{"icon":"🎯","text":"Ignores all others — only chases its chosen target"},{"icon":"📻","text":"Music Box: ignores non-target if target is inside"}]'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user