create credits page

This commit is contained in:
Jeremy Kirsch 2026-05-18 15:39:43 +02:00
parent bada4b312a
commit 05ed85bc2e

View File

@ -254,6 +254,33 @@ body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(
.toast-danger{background:rgba(192,64,64,.18);border-color:var(--red2);color:var(--red2);} .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);} .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;}} @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> </style>
</head> </head>
@ -312,6 +339,7 @@ body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(
<button class="nav-btn active">Ghost Cards</button> <button class="nav-btn active">Ghost Cards</button>
<button class="nav-btn">Cheatsheet</button> <button class="nav-btn">Cheatsheet</button>
<button class="nav-btn">Hunt Guide</button> <button class="nav-btn">Hunt Guide</button>
<button class="nav-btn">Credits</button>
</nav> </nav>
<div class="panel active" id="vtab-cards"> <div class="panel active" id="vtab-cards">
<div class="filter-row" id="ev-filter"><div class="filter-label-sm">Evidence Filter — einmal: gefunden &nbsp;·&nbsp; zweimal: ausgeschlossen</div></div> <div class="filter-row" id="ev-filter"><div class="filter-label-sm">Evidence Filter — einmal: gefunden &nbsp;·&nbsp; zweimal: ausgeschlossen</div></div>
@ -347,6 +375,28 @@ body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(
</div> </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 &amp; 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">+ 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> </div>
<!-- ADMIN --> <!-- ADMIN -->
@ -447,6 +497,77 @@ body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(
</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"></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">Abbrechen</button>
<button class="btn-accent" id="btn-save-credit">Speichern</button>
</div>
</div>
</div>
</div>
</div>
<div class="confirm-bg" id="confirm-bg"> <div class="confirm-bg" id="confirm-bg">
<div class="confirm-box"> <div class="confirm-box">
<div class="confirm-title" id="confirm-title">Sicher?</div> <div class="confirm-title" id="confirm-title">Sicher?</div>
@ -522,12 +643,14 @@ function setupUI() {
if(u.isAdmin) document.getElementById('pill-admin').style.display='block'; if(u.isAdmin) document.getElementById('pill-admin').style.display='block';
buildCards(); buildCheat(); buildCards(); buildCheat();
populateGhostSelect(); populateGhostSelect();
loadCredits();
if(u.isAdmin) updateAdminBadge(); if(u.isAdmin) updateAdminBadge();
} }
function setupGuestUI() { function setupGuestUI() {
document.getElementById('guest-badge').style.display='block'; document.getElementById('guest-badge').style.display='block';
buildCards(); buildCheat(); buildCards(); buildCheat();
loadCredits();
} }
/* ── AUTH ── */ /* ── AUTH ── */
@ -639,10 +762,13 @@ function setMode(m) {
} }
function switchTab(t) { function switchTab(t) {
['cards','cheat','guide'].forEach((x,i)=>{ ['cards','cheat','guide','credits'].forEach((x,i)=>{
document.getElementById('vtab-'+x).classList.toggle('active',x===t); const el = document.getElementById('vtab-'+x);
document.querySelectorAll('#vnav .nav-btn')[i].classList.toggle('active',x===t); 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 ── */ /* ── ADMIN ── */
@ -845,6 +971,147 @@ function hideLogin(){document.getElementById('login-screen').classList.remove('s
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 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');} function escH(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
/* ═══════════════════════════════════════════════════
CREDITS
═══════════════════════════════════════════════════ */
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));
}
function renderCredits() {
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">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">Noch keine Credits eingetragen.</div>';
} else {
creditsGrid.innerHTML = '';
crds.forEach(c => creditsGrid.appendChild(buildCreditCard(c)));
}
// Show add button for admins
const adminBtn = document.getElementById('credits-admin-btn');
if (adminBtn) adminBtn.style.display = (currentUser?.isAdmin) ? 'block' : 'none';
}
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 ? `<div class="credit-desc">${escH(c.desc)}</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 || '';
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() {
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() || '👤',
desc: 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);
}
saveCredits();
closeModal('credits-modal');
renderCredits();
toast(`✓ ${name} gespeichert!`, "success");
}
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");
});
}
/* ── BOOT ── */ /* ── BOOT ── */
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -912,6 +1179,16 @@ document.addEventListener('DOMContentLoaded', () => {
const guestBtn = document.getElementById('guest-btn'); const guestBtn = document.getElementById('guest-btn');
if (guestBtn) guestBtn.addEventListener('click', guestMode); 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(); init();
}); });
</script> </script>