Weiter am Chastity Game gearbeitet und Interaktionen zwischen Keyholder und Lockee hinzugefügt

This commit is contained in:
2026-03-16 23:16:45 +01:00
parent 57a7c78037
commit 97c6f0a131
399 changed files with 34194 additions and 2272 deletions

View File

@@ -0,0 +1,664 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einladungen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* Tabs */
.tabs-bar {
display: flex;
gap: 0;
border-bottom: 2px solid var(--color-secondary);
margin-bottom: 1.5rem;
}
.tab-btn {
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
padding: 0.6rem 1.25rem;
font-size: 0.92rem;
font-weight: 600;
color: var(--color-muted);
cursor: pointer;
width: auto;
border-radius: 0;
transition: color 0.15s, border-color 0.15s;
}
.tab-btn:hover { color: var(--color-text); background: none; }
.tab-btn.active { color: var(--color-primary); border-bottom-color: var(--color-primary); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* Sektionen */
.inv-section { margin-bottom: 2rem; }
.inv-section-title {
font-size: 0.8rem; font-weight: 700; color: var(--color-primary);
text-transform: uppercase; letter-spacing: 0.06em;
margin-bottom: 0.75rem;
}
.inv-list { display: flex; flex-direction: column; gap: 0.5rem; }
.inv-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
display: flex; align-items: center; gap: 0.9rem;
padding: 0.75rem 1rem;
}
.inv-avatar {
width: 52px; height: 52px;
border-radius: 50%;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; flex-shrink: 0; overflow: hidden;
border: 1px solid rgba(255,255,255,0.08);
}
.inv-avatar img { width: 100%; height: 100%; object-fit: cover; }
.inv-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.inv-line1 { font-size: 0.78rem; color: var(--color-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.inv-line2 { font-weight: 700; font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.inv-line3 { font-size: 0.78rem; color: var(--color-muted); }
.empty-hint { color: var(--color-muted); font-size: 0.9rem; margin-top: 0.25rem; }
/* Lockee-Einladungs-Dialog */
.lockee-dialog-bg {
display: none; position: fixed; inset: 0; z-index: 400;
align-items: center; justify-content: center;
}
.lockee-dialog-bg.open { display: flex; }
.lockee-dialog-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); }
.lockee-dialog-box {
position: relative; background: var(--color-card);
border: 1px solid var(--color-secondary); border-radius: 12px;
padding: 1.75rem 1.5rem 1.5rem; max-width: 420px; width: 92%; z-index: 1;
display: flex; flex-direction: column; gap: 1rem;
max-height: 90vh; overflow-y: auto;
}
.lockee-dialog-header { display: flex; align-items: center; gap: 0.75rem; }
.lockee-dialog-avatar {
width: 48px; height: 48px; border-radius: 50%;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem; flex-shrink: 0; overflow: hidden;
border: 1px solid rgba(255,255,255,0.08);
}
.lockee-dialog-avatar img { width: 100%; height: 100%; object-fit: cover; }
.lockee-dialog-title { font-weight: 700; font-size: 1rem; }
.lockee-dialog-sub { font-size: 0.82rem; color: var(--color-muted); margin-top: 0.1rem; }
.lockee-dialog-detail {
background: var(--color-secondary); border-radius: 8px;
padding: 0.75rem 1rem; font-size: 0.88rem;
}
.lockee-dialog-detail dt { color: var(--color-muted); font-size: 0.75rem; margin-bottom: 0.1rem; }
.lockee-dialog-detail dd { font-weight: 600; margin: 0 0 0.5rem 0; }
.lockee-dialog-detail dd:last-child { margin-bottom: 0; }
.lockee-dialog-codelines { display: flex; align-items: center; gap: 0.6rem; }
.lockee-dialog-codelines label { font-size: 0.88rem; font-weight: 600; white-space: nowrap; }
.lockee-dialog-codelines input { width: 72px; text-align: center; }
.lockee-dialog-codelines span { font-size: 0.88rem; color: var(--color-muted); }
.lockee-dialog-actions { display: flex; gap: 0.6rem; justify-content: flex-end; }
.lockee-dialog-actions button { width: auto; padding: 0.6rem 1.3rem; font-size: 0.9rem; }
.btn-accept { background: var(--color-success, #27ae60) !important; }
.btn-accept:hover { background: #219150 !important; }
.btn-decline { background: #c0392b !important; }
.btn-decline:hover { background: #a93226 !important; }
.lockee-dialog-error { color: #e74c3c; font-size: 0.82rem; display: none; }
/* Lock-Details im Dialog */
.lock-details-section { display: flex; flex-direction: column; gap: 0.5rem; }
.lock-details-cards {
display: grid; grid-template-columns: repeat(auto-fill, minmax(68px, 1fr)); gap: 0.4rem;
}
.lock-details-card-item {
background: var(--color-secondary); border-radius: 6px;
padding: 0.4rem 0.3rem;
display: flex; flex-direction: column; align-items: center; gap: 0.2rem; text-align: center;
}
.lock-details-card-item img { width: 36px; height: auto; border-radius: 3px; }
.lock-details-card-item .ldc-count { font-weight: 700; font-size: 0.9rem; }
.lock-details-card-item .ldc-name { font-size: 0.65rem; color: var(--color-muted); line-height: 1.2; }
.lock-details-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.lock-details-badge {
background: var(--color-secondary); border-radius: 20px;
padding: 0.2rem 0.6rem; font-size: 0.75rem; color: var(--color-muted);
}
.blind-hint {
background: var(--color-secondary); border-radius: 8px; padding: 0.9rem 1rem;
display: flex; gap: 0.6rem; align-items: flex-start;
font-size: 0.85rem; color: var(--color-muted); line-height: 1.5;
}
.blind-hint-icon { font-size: 1.4rem; flex-shrink: 0; }
/* Entsperrcode-Modal */
.unlock-modal-bg {
display: none; position: fixed; inset: 0; z-index: 500;
align-items: center; justify-content: center;
}
.unlock-modal-bg.open { display: flex; }
.unlock-modal-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
.unlock-modal-box {
position: relative; background: var(--color-card);
border: 1px solid var(--color-secondary); border-radius: 12px;
padding: 1.5rem 1.5rem 1.25rem; max-width: 380px; width: 90%; z-index: 1;
display: flex; flex-direction: column; align-items: center; gap: 0.75rem; text-align: center;
}
.unlock-code-display {
font-family: monospace; font-size: 2rem; letter-spacing: 0.3em;
background: var(--color-secondary); border-radius: 8px;
padding: 1rem 1.5rem; color: var(--color-primary);
line-height: 1.8; word-break: break-all; width: 100%; box-sizing: border-box;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1 style="margin-bottom:1.25rem;">Einladungen</h1>
<div class="tabs-bar">
<button class="tab-btn active" data-tab="empfangen" onclick="switchTab('empfangen')">Empfangen</button>
<button class="tab-btn" data-tab="gesendet" onclick="switchTab('gesendet')">Gesendet</button>
</div>
<!-- Tab: Empfangen -->
<div id="tab-empfangen" class="tab-panel active">
<div class="inv-section">
<div class="inv-section-title">Lockee-Einladungen</div>
<div class="inv-list" id="lockeeInvGrid"></div>
<p class="empty-hint" id="lockeeInvEmpty" style="display:none;">Keine ausstehenden Lockee-Einladungen.</p>
</div>
<div class="inv-section">
<div class="inv-section-title">Keyholder-Einladungen</div>
<div class="inv-list" id="khInvGrid"></div>
<p class="empty-hint" id="khInvEmpty" style="display:none;">Keine ausstehenden Keyholder-Einladungen.</p>
</div>
</div>
<!-- Tab: Gesendet -->
<div id="tab-gesendet" class="tab-panel">
<div class="inv-section">
<div class="inv-section-title">Gesendete Lockee-Einladungen</div>
<div class="inv-list" id="sentLockeeGrid"></div>
<p class="empty-hint" id="sentLockeeEmpty" style="display:none;">Keine ausstehenden gesendeten Lockee-Einladungen.</p>
</div>
<div class="inv-section">
<div class="inv-section-title">Gesendete Keyholder-Einladungen</div>
<div class="inv-list" id="sentKhGrid"></div>
<p class="empty-hint" id="sentKhEmpty" style="display:none;">Keine ausstehenden gesendeten Keyholder-Einladungen.</p>
</div>
</div>
</div>
</div>
<!-- Lockee-Einladungs-Dialog -->
<div class="lockee-dialog-bg" id="lockeeInviteDialog">
<div class="lockee-dialog-overlay" onclick="closeLockeeInviteDialog()"></div>
<div class="lockee-dialog-box">
<div class="lockee-dialog-header">
<div class="lockee-dialog-avatar" id="dialogAvatar">🔒</div>
<div>
<div class="lockee-dialog-title" id="dialogTitle"></div>
<div class="lockee-dialog-sub" id="dialogSub"></div>
</div>
</div>
<dl class="lockee-dialog-detail" id="dialogDetail"></dl>
<div id="dialogDetailsArea"></div>
<div>
<div class="lockee-dialog-codelines">
<label for="dialogCodeLines">Ziffern des Entsperrcodes:</label>
<input type="number" id="dialogCodeLines" min="1" max="20" value="5">
<span>Ziffern</span>
</div>
</div>
<div class="lockee-dialog-error" id="dialogError"></div>
<div class="lockee-dialog-actions">
<button class="btn-decline" onclick="declineLockeeInviteDialog()">✕ Ablehnen</button>
<button class="btn-accept" onclick="acceptLockeeInviteDialog()">✓ Annehmen</button>
</div>
</div>
</div>
<!-- Entsperrcode-Modal -->
<div class="unlock-modal-bg" id="unlockModal">
<div class="unlock-modal-overlay"></div>
<div class="unlock-modal-box">
<div style="font-size:2rem;">🔒</div>
<h3 id="unlockModalTitle" style="margin:0;">Dein Entsperrcode</h3>
<p id="unlockModalHint" style="color:var(--color-muted);font-size:0.85rem;margin:0;">
Stelle die Kombination deines Tresors auf den folgenden Code ein und verschließe deinen Schlüssel in diesem.
</p>
<div class="unlock-code-display" id="unlockCodeDisplay"></div>
<div id="unlockModalCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
<button id="unlockModalBtn" style="width:100%;margin-top:0.25rem;">Weiter</button>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
// ── Tabs ──
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === 'tab-' + name));
history.replaceState(null, '', '?tab=' + name);
}
// Activate tab from URL param
const urlTab = new URLSearchParams(window.location.search).get('tab');
if (urlTab === 'gesendet') switchTab('gesendet');
// ── Empfangen: Lockee-Einladungen ──
async function loadLockeeInvitations() {
try {
const res = await fetch('/lockee/invitations/mine');
if (!res.ok) return;
const invs = await res.json();
const grid = document.getElementById('lockeeInvGrid');
const empty = document.getElementById('lockeeInvEmpty');
grid.innerHTML = '';
if (invs.length === 0) { empty.style.display = ''; return; }
empty.style.display = 'none';
invs.forEach(inv => {
const av = inv.keyholderProfilePic
? `<div class="inv-avatar"><img src="data:image/jpeg;base64,${inv.keyholderProfilePic}" alt=""></div>`
: `<div class="inv-avatar">👤</div>`;
const dt = new Date(inv.createdAt);
const createdStr = dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
const card = document.createElement('div');
card.className = 'inv-card';
card.id = 'lockeeinv-' + inv.token;
card.dataset.detailsVisible = inv.detailsVisible ? '1' : '0';
card.innerHTML = `
${av}
<div class="inv-body">
<div class="inv-line1">${esc(inv.keyholderName)}</div>
<div class="inv-line2">${esc(inv.lockName)}</div>
<div class="inv-line3">Eingeladen am ${createdStr}</div>
</div>
<div style="display:flex;flex-direction:column;gap:0.4rem;flex-shrink:0;">
<button onclick="declineLockeeInvitation('${esc(inv.token)}', this)" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:#c0392b;border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✕ Ablehnen</button>
<button onclick="openLockeeInviteDialog('${esc(inv.token)}')" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:var(--color-success,#27ae60);border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✓ Details</button>
</div>`;
grid.appendChild(card);
});
} catch(e) { console.error(e); }
}
async function declineLockeeInvitation(token, btn) {
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
btn.disabled = true;
try {
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token), { method: 'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('lockeeinv-' + token)?.remove();
const grid = document.getElementById('lockeeInvGrid');
if (grid.children.length === 0) document.getElementById('lockeeInvEmpty').style.display = '';
} else { btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
// ── Empfangen: Keyholder-Einladungen ──
async function loadKeyholderInvitations() {
try {
const res = await fetch('/keyholder/invitations/mine');
if (!res.ok) return;
const invs = await res.json();
const grid = document.getElementById('khInvGrid');
const empty = document.getElementById('khInvEmpty');
grid.innerHTML = '';
if (invs.length === 0) { empty.style.display = ''; return; }
empty.style.display = 'none';
invs.forEach(inv => {
const av = inv.lockeeProfilePic
? `<div class="inv-avatar"><img src="data:image/jpeg;base64,${inv.lockeeProfilePic}" alt=""></div>`
: `<div class="inv-avatar">👤</div>`;
const dt = new Date(inv.createdAt);
const createdStr = dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
const card = document.createElement('div');
card.className = 'inv-card';
card.id = 'khinv-' + inv.token;
card.innerHTML = `
${av}
<div class="inv-body">
<div class="inv-line1">${esc(inv.lockeeName)}</div>
<div class="inv-line2">${esc(inv.lockName)}</div>
<div class="inv-line3">Eingeladen am ${createdStr}</div>
</div>
<div style="display:flex;flex-direction:column;gap:0.4rem;flex-shrink:0;">
<button onclick="declineKhInvitation('${esc(inv.token)}', this)" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:#c0392b;border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✕ Ablehnen</button>
<a href="/keyholder/invitation/${esc(inv.token)}" style="display:block;text-align:center;padding:0.45rem 1rem;font-size:0.85rem;background:var(--color-success);color:#fff;border-radius:6px;text-decoration:none;font-weight:600;">✓ Annehmen</a>
</div>`;
grid.appendChild(card);
});
} catch(e) { console.error(e); }
}
async function declineKhInvitation(token, btn) {
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
btn.disabled = true;
try {
const res = await fetch('/keyholder/invitations/mine/' + encodeURIComponent(token), { method: 'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('khinv-' + token)?.remove();
const grid = document.getElementById('khInvGrid');
if (grid.children.length === 0) document.getElementById('khInvEmpty').style.display = '';
} else { btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
// ── Gesendet: Lockee-Einladungen ──
async function loadSentLockeeInvitations() {
try {
const res = await fetch('/lockee/invitations/sent');
if (!res.ok) return;
const invs = await res.json();
const grid = document.getElementById('sentLockeeGrid');
const empty = document.getElementById('sentLockeeEmpty');
grid.innerHTML = '';
if (invs.length === 0) { empty.style.display = ''; return; }
empty.style.display = 'none';
invs.forEach(inv => {
const av = inv.lockeeProfilePic
? `<div class="inv-avatar"><img src="data:image/jpeg;base64,${inv.lockeeProfilePic}" alt=""></div>`
: `<div class="inv-avatar">👤</div>`;
const dt = new Date(inv.createdAt);
const createdStr = dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
const badge = inv.detailsVisible
? '<span style="font-size:0.72rem;">👁 Details sichtbar</span>'
: '<span style="font-size:0.72rem;">🙈 Details verborgen</span>';
const card = document.createElement('div');
card.className = 'inv-card';
card.id = 'sentlockeeinv-' + inv.token;
card.innerHTML = `
${av}
<div class="inv-body">
<div class="inv-line1">${esc(inv.lockeeName)}</div>
<div class="inv-line2">${esc(inv.lockName)}</div>
<div class="inv-line3">Gesendet am ${createdStr} &nbsp;${badge}</div>
</div>
<div style="flex-shrink:0;">
<button onclick="cancelSentLockeeInvitation('${esc(inv.token)}', this)" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:#c0392b;border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✕ Zurückziehen</button>
</div>`;
grid.appendChild(card);
});
} catch(e) { console.error(e); }
}
async function cancelSentLockeeInvitation(token, btn) {
if (!confirm('Einladung zurückziehen? Das Lock wird gelöscht und der Lockee wird benachrichtigt.')) return;
btn.disabled = true;
try {
const res = await fetch('/lockee/invitations/sent/' + encodeURIComponent(token), { method: 'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('sentlockeeinv-' + token)?.remove();
const grid = document.getElementById('sentLockeeGrid');
if (grid.children.length === 0) document.getElementById('sentLockeeEmpty').style.display = '';
} else { btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
// ── Gesendet: Keyholder-Einladungen ──
async function loadSentKeyholderInvitations() {
try {
const res = await fetch('/keyholder/invitations/sent');
if (!res.ok) return;
const invs = await res.json();
const grid = document.getElementById('sentKhGrid');
const empty = document.getElementById('sentKhEmpty');
grid.innerHTML = '';
if (invs.length === 0) { empty.style.display = ''; return; }
empty.style.display = 'none';
invs.forEach(inv => {
const av = inv.keyholderProfilePic
? `<div class="inv-avatar"><img src="data:image/jpeg;base64,${inv.keyholderProfilePic}" alt=""></div>`
: `<div class="inv-avatar">👤</div>`;
const dt = new Date(inv.createdAt);
const createdStr = dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
const card = document.createElement('div');
card.className = 'inv-card';
card.id = 'sentkhinv-' + inv.token;
card.innerHTML = `
${av}
<div class="inv-body">
<div class="inv-line1">${esc(inv.keyholderName)}</div>
<div class="inv-line2">${esc(inv.lockName)}</div>
<div class="inv-line3">Gesendet am ${createdStr}</div>
</div>
<div style="flex-shrink:0;">
<button onclick="cancelSentKhInvitation('${esc(inv.token)}', this)" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:#c0392b;border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✕ Zurückziehen</button>
</div>`;
grid.appendChild(card);
});
} catch(e) { console.error(e); }
}
async function cancelSentKhInvitation(token, btn) {
if (!confirm('Keyholder-Einladung zurückziehen? Der Keyholder wird benachrichtigt.')) return;
btn.disabled = true;
try {
const res = await fetch('/keyholder/invitations/sent/' + encodeURIComponent(token), { method: 'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('sentkhinv-' + token)?.remove();
const grid = document.getElementById('sentKhGrid');
if (grid.children.length === 0) document.getElementById('sentKhEmpty').style.display = '';
} else { btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
// ── Lockee-Einladungs-Dialog ──
const CARD_DEFS_DIALOG = [
{ id: 'RED', img: '/img/card_red.png', name: 'Rot' },
{ id: 'GREEN', img: '/img/card_green.png', name: 'Grün' },
{ id: 'YELLOW', img: '/img/card_yellow.png', name: 'Gelb' },
{ id: 'TASK', img: '/img/card_task.png', name: 'Aufgabe' },
{ id: 'FREEZE', img: '/img/card_freeze.png', name: 'Freeze' },
{ id: 'RESET', img: '/img/card_reset.png', name: 'Reset' },
{ id: 'DOUBLE_UP', img: '/img/card_doubleup.png', name: 'Double Up' },
];
function fmtMinutes(min) {
if (!min) return '';
const d = Math.floor(min / (24 * 60));
const h = Math.floor((min % (24 * 60)) / 60);
const m = min % 60;
const parts = [];
if (d) parts.push(d + 'd');
if (h) parts.push(h + 'h');
if (m) parts.push(m + 'min');
return parts.join(' ') || '';
}
function renderLockDetails(inv) {
if (!inv.detailsVisible) {
return `<div class="blind-hint">
<span class="blind-hint-icon">🙈</span>
<span>Der Keyholder hat die Lock-Details nicht freigegeben. Du weißt nicht, worauf du dich einlässt.</span>
</div>`;
}
const cardCounts = inv.cardCounts || {};
const totalCards = Object.values(cardCounts).reduce((a, b) => a + b, 0);
const cardsHtml = CARD_DEFS_DIALOG
.filter(c => cardCounts[c.id] > 0)
.map(c => `<div class="lock-details-card-item">
<img src="${c.img}" alt="${c.name}">
<span class="ldc-count">${cardCounts[c.id]}×</span>
<span class="ldc-name">${c.name}</span>
</div>`).join('');
const badges = [];
badges.push(`🃏 ${totalCards} Karten`);
badges.push(`⏱ Ziehen alle ${fmtMinutes(inv.pickEveryMinute)}`);
if (inv.accumulatePicks) badges.push('📦 Picks akkumulieren');
if (inv.showRemainingCards) badges.push('👁 Karten sichtbar');
if (inv.hygineOpeningEveryMinites) badges.push(`🚿 Hygiene alle ${fmtMinutes(inv.hygineOpeningEveryMinites)} (${fmtMinutes(inv.hygineOpeningDurationMinutes)})`);
if (inv.taskCount > 0) badges.push(`${inv.taskCount} Aufgabe${inv.taskCount !== 1 ? 'n' : ''}`);
if (inv.requiresVerification) badges.push('🔍 Verifikation erforderlich');
return `<div class="lock-details-section">
<div class="lock-details-cards">${cardsHtml}</div>
<div class="lock-details-meta">${badges.map(b => `<span class="lock-details-badge">${b}</span>`).join('')}</div>
</div>`;
}
let activeDialogToken = null;
async function openLockeeInviteDialog(token) {
activeDialogToken = token;
document.getElementById('dialogError').style.display = 'none';
document.getElementById('dialogCodeLines').value = '5';
document.getElementById('dialogDetailsArea').innerHTML = '<div style="color:var(--color-muted);font-size:0.85rem;">Lade Details…</div>';
document.getElementById('lockeeInviteDialog').classList.add('open');
const card = document.getElementById('lockeeinv-' + token);
const line1 = card?.querySelector('.inv-line1')?.textContent || '';
const line2 = card?.querySelector('.inv-line2')?.textContent || '';
const line3 = card?.querySelector('.inv-line3')?.textContent || '';
const imgEl = card?.querySelector('.inv-avatar img');
const avatarEl = document.getElementById('dialogAvatar');
avatarEl.innerHTML = imgEl ? `<img src="${imgEl.src}" alt="">` : '👤';
document.getElementById('dialogTitle').textContent = line2;
document.getElementById('dialogSub').textContent = line1 + ' lädt dich als Lockee ein';
document.getElementById('dialogDetail').innerHTML =
`<dt>Keyholder</dt><dd>${esc(line1)}</dd>` +
`<dt>Lock-Name</dt><dd>${esc(line2)}</dd>` +
`<dt>Datum</dt><dd>${esc(line3)}</dd>`;
try {
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token));
if (res.ok) {
document.getElementById('dialogDetailsArea').innerHTML = renderLockDetails(await res.json());
} else {
document.getElementById('dialogDetailsArea').innerHTML = '';
}
} catch(e) { document.getElementById('dialogDetailsArea').innerHTML = ''; }
}
function closeLockeeInviteDialog() {
document.getElementById('lockeeInviteDialog').classList.remove('open');
activeDialogToken = null;
}
async function acceptLockeeInviteDialog() {
if (!activeDialogToken) return;
const lines = parseInt(document.getElementById('dialogCodeLines').value);
if (!lines || lines < 1) { showDialogError('Bitte eine Ziffernanzahl eingeben.'); return; }
const acceptBtn = document.querySelector('.btn-accept');
acceptBtn.disabled = true;
document.getElementById('dialogError').style.display = 'none';
try {
const res = await fetch('/lockee/invitation/' + encodeURIComponent(activeDialogToken) + '/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unlockCodeLines: lines })
});
if (!res.ok) {
acceptBtn.disabled = false;
if (res.status === 409) {
const data = await res.json().catch(() => ({}));
showDialogError(data.error === 'active_lock_exists'
? 'Du hast bereits ein aktives Lock als Lockee. Erst das bestehende Lock beenden, bevor ein neues angenommen werden kann.'
: 'Diese Einladung wurde bereits angenommen.');
} else {
showDialogError('Fehler beim Annehmen der Einladung.');
}
return;
}
const data = await res.json();
document.getElementById('lockeeInviteDialog').classList.remove('open');
document.getElementById('lockeeinv-' + activeDialogToken)?.remove();
const grid = document.getElementById('lockeeInvGrid');
if (grid.children.length === 0) document.getElementById('lockeeInvEmpty').style.display = '';
showUnlockCodeModal(data.unlockCode, data.lockId);
} catch(e) {
acceptBtn.disabled = false;
showDialogError('Fehler beim Annehmen der Einladung.');
}
}
async function declineLockeeInviteDialog() {
if (!activeDialogToken) return;
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest? Das Lock wird gelöscht.')) return;
const declineBtn = document.querySelector('.btn-decline');
declineBtn.disabled = true;
try {
const res = await fetch('/lockee/invitation/' + encodeURIComponent(activeDialogToken), { method: 'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('lockeeinv-' + activeDialogToken)?.remove();
const grid = document.getElementById('lockeeInvGrid');
if (grid.children.length === 0) document.getElementById('lockeeInvEmpty').style.display = '';
closeLockeeInviteDialog();
} else {
declineBtn.disabled = false;
showDialogError('Fehler beim Ablehnen der Einladung.');
}
} catch(e) { declineBtn.disabled = false; showDialogError('Fehler beim Ablehnen der Einladung.'); }
}
function showDialogError(msg) {
const el = document.getElementById('dialogError');
el.textContent = msg;
el.style.display = '';
}
// ── Entsperrcode-Modal ──
function showUnlockCodeModal(code, lockId) {
document.getElementById('unlockCodeDisplay').textContent = code;
const url = '/sessionchastityingame.html?lockId=' + lockId;
const btn = document.getElementById('unlockModalBtn');
btn.onclick = () => startCodeScramble(code, url);
document.getElementById('unlockModal').classList.add('open');
}
function startCodeScramble(realCode, url) {
const display = document.getElementById('unlockCodeDisplay');
const btn = document.getElementById('unlockModalBtn');
const hint = document.getElementById('unlockModalHint');
const countdown = document.getElementById('unlockModalCountdown');
const len = realCode.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
function finish() {
stopped = true;
clearInterval(scrambleInterval);
clearInterval(countdownInterval);
window.location.href = url;
}
if (hint) hint.style.display = 'none';
countdown.style.display = '';
document.getElementById('unlockModalTitle').textContent = 'Nun vergessen wir den Code…';
btn.textContent = 'Abbrechen';
btn.onclick = finish;
function updateCountdown() {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
countdown.textContent = `${m}:${String(s).padStart(2, '0')}`;
}
updateCountdown();
const scrambleInterval = setInterval(() => { if (!stopped) display.textContent = randomCode(); }, 1000);
const countdownInterval = setInterval(() => {
if (stopped) return;
remaining--;
updateCountdown();
if (remaining <= 0) finish();
}, 1000);
}
// ── Alles laden ──
loadLockeeInvitations();
loadKeyholderInvitations();
loadSentLockeeInvitations();
loadSentKeyholderInvitations();
</script>
</body>
</html>