Files
xxx-sphere-web/bin/main/static/games/common/einladungen.html

983 lines
47 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einladungen xXx Sphere</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; }
/* Liste */
.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;
}
/* Avatar mit Typ-Badge */
.inv-avatar-wrap {
position: relative;
flex-shrink: 0;
}
.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; overflow: hidden;
border: 1px solid rgba(255,255,255,0.08);
}
.inv-avatar img { width: 100%; height: 100%; object-fit: cover; }
.inv-type-badge {
position: absolute;
top: -6px; left: -6px;
width: 26px; height: 26px;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.08rem;
z-index: 1;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.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; }
/* Paging */
.paging-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-top: 1rem;
font-size: 0.88rem;
color: var(--color-muted);
}
.paging-bar button {
width: auto;
padding: 0.4rem 0.9rem;
font-size: 0.85rem;
}
.paging-bar button:disabled {
opacity: 0.35;
cursor: default;
}
/* 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; }
/* Bestätigungs-Modal */
.confirm-modal-bg {
display: none; position: fixed; inset: 0; z-index: 600;
align-items: center; justify-content: center;
}
.confirm-modal-bg.open { display: flex; }
.confirm-modal-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
.confirm-modal-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: 380px; width: 92%; z-index: 1;
display: flex; flex-direction: column; gap: 1rem;
}
.confirm-modal-title {
font-weight: 700; font-size: 1rem; padding-right: 1.5rem;
}
.confirm-modal-text {
font-size: 0.9rem; color: var(--color-muted); line-height: 1.5;
}
.confirm-modal-actions {
display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 0.25rem;
}
.confirm-modal-actions button { width: auto; padding: 0.6rem 1.3rem; font-size: 0.9rem; }
.confirm-modal-cancel { background: var(--color-secondary) !important; color: var(--color-text) !important; }
.confirm-modal-ok { background: #c0392b !important; }
.confirm-modal-ok:hover { background: #a93226 !important; }
/* 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-list" id="recvList"></div>
<p class="empty-hint" id="recvEmpty" style="display:none;">Keine ausstehenden Einladungen.</p>
<div class="paging-bar" id="recvPaging" style="display:none;"></div>
</div>
<!-- Tab: Gesendet -->
<div id="tab-gesendet" class="tab-panel">
<div class="inv-list" id="sentList"></div>
<p class="empty-hint" id="sentEmpty" style="display:none;">Keine ausstehenden gesendeten Einladungen.</p>
<div class="paging-bar" id="sentPaging" style="display:none;"></div>
</div>
</div>
</div>
<!-- Bestätigungs-Modal -->
<div class="confirm-modal-bg" id="confirmModal">
<div class="confirm-modal-overlay" onclick="confirmCancel()"></div>
<div class="confirm-modal-box">
<button onclick="confirmCancel()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;" title="Schließen"></button>
<div class="confirm-modal-title" id="confirmTitle"></div>
<div class="confirm-modal-text" id="confirmText"></div>
<div class="confirm-modal-actions">
<button class="confirm-modal-cancel" onclick="confirmCancel()">Abbrechen</button>
<button class="confirm-modal-ok" id="confirmOkBtn">Bestätigen</button>
</div>
</div>
</div>
<!-- Vanilla-Einladungs-Dialog -->
<div class="lockee-dialog-bg" id="vanillaInviteDialog">
<div class="lockee-dialog-overlay" onclick="closeVanillaInviteDialog()"></div>
<div class="lockee-dialog-box">
<button onclick="closeVanillaInviteDialog()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;" title="Schließen"></button>
<div class="lockee-dialog-header">
<div class="lockee-dialog-avatar">🎲</div>
<div>
<div class="lockee-dialog-title" id="vanillaDialogTitle"></div>
<div class="lockee-dialog-sub">Vanilla Game Einladung</div>
</div>
</div>
<p style="font-size:0.88rem;color:var(--color-muted);line-height:1.5;margin:0;">
Du wurdest zu einem Vanilla Game eingeladen. Wie möchtest du mitspielen?
</p>
<div class="lockee-dialog-error" id="vanillaDialogError"></div>
<div class="lockee-dialog-actions" style="flex-direction:column;gap:0.5rem;">
<button class="btn-accept" style="width:100%;" onclick="acceptVanillaOwnDevice()">Am eigenen Gerät mitspielen</button>
<button class="btn-accept" style="width:100%;background:#1a5c8a!important;" onclick="acceptVanillaHostDevice()">Am Gerät des Hosts mitspielen</button>
<button class="btn-decline" style="width:100%;" onclick="declineVanillaFromDialog()">Einladung ablehnen</button>
</div>
</div>
</div>
<!-- BDSM-Einladungs-Dialog -->
<div class="lockee-dialog-bg" id="bdsmInviteDialog">
<div class="lockee-dialog-overlay" onclick="closeBdsmInviteDialog()"></div>
<div class="lockee-dialog-box">
<button onclick="closeBdsmInviteDialog()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;" title="Schließen"></button>
<div class="lockee-dialog-header">
<div class="lockee-dialog-avatar" id="bdsmDialogAvatar">⛓️</div>
<div>
<div class="lockee-dialog-title" id="bdsmDialogTitle"></div>
<div class="lockee-dialog-sub">BDSM Game Einladung</div>
</div>
</div>
<p style="font-size:0.88rem;color:var(--color-muted);line-height:1.5;margin:0;">
Du wurdest zu einem BDSM Game eingeladen. Wie möchtest du mitspielen?
</p>
<div class="lockee-dialog-error" id="bdsmDialogError"></div>
<div class="lockee-dialog-actions" style="flex-direction:column;gap:0.5rem;">
<button class="btn-accept" style="width:100%;" onclick="acceptBdsmOwnDevice()">Am eigenen Gerät mitspielen</button>
<button class="btn-accept" style="width:100%;background:#1a5c8a!important;" onclick="acceptBdsmHostDevice()">Am Gerät des Hosts mitspielen</button>
<button class="btn-decline" style="width:100%;" onclick="declineBdsmFromDialog()">Einladung ablehnen</button>
</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">
<button onclick="closeLockeeInviteDialog()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;" title="Schließen"></button>
<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/card-defs.js"></script>
<script src="/js/icons.js"></script>
<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);
}
const urlTab = new URLSearchParams(window.location.search).get('tab');
if (urlTab === 'gesendet') switchTab('gesendet');
// ── Konstanten ──
const PAGE_SIZE = 10;
// ── State ──
let recvItems = [];
let sentItems = [];
let recvPage = 0;
let sentPage = 0;
// ── Hilfsfunktionen ──
function fmtDate(iso) {
const dt = new Date(iso);
return dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
}
function buildAvatarHtml(picBase64, type) {
const badge = type === 'keyholder' ? '🔑' : type === 'bdsm' ? '⛓️' : type === 'vanilla' ? '🎲' : '🔒';
const inner = picBase64
? `<div class="inv-avatar"><img src="data:image/jpeg;base64,${picBase64}" alt=""></div>`
: `<div class="inv-avatar">👤</div>`;
return `<div class="inv-avatar-wrap"><span class="inv-type-badge">${badge}</span>${inner}</div>`;
}
function renderPaging(barId, page, total, onNav) {
const bar = document.getElementById(barId);
if (total <= 1) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
bar.innerHTML = `
<button onclick="${onNav}(${page - 1})" ${page === 0 ? 'disabled' : ''}> Zurück</button>
<span>Seite ${page + 1} von ${total}</span>
<button onclick="${onNav}(${page + 1})" ${page >= total - 1 ? 'disabled' : ''}>Weiter </button>`;
}
// ── Empfangen laden ──
async function loadReceivedInvitations() {
try {
const [lockeeRes, khRes, bdsmRes, vanillaRes] = await Promise.all([
fetch('/lockee/invitations/mine'),
fetch('/keyholder/invitations/mine'),
fetch('/bdsm/einladung/pending'),
fetch('/vanilla/einladung/pending'),
]);
const lockeeInvs = lockeeRes.ok ? await lockeeRes.json() : [];
const khInvs = khRes.ok ? await khRes.json() : [];
const bdsmInvs = bdsmRes.ok ? await bdsmRes.json() : [];
const vanillaInvs = vanillaRes.ok ? await vanillaRes.json() : [];
lockeeInvs.forEach(inv => { inv._type = 'lockee'; inv._key = inv.token; inv._otherName = inv.keyholderName; inv._otherPic = inv.keyholderProfilePic; });
khInvs.forEach(inv => { inv._type = 'keyholder'; inv._key = inv.token; inv._otherName = inv.lockeeName; inv._otherPic = inv.lockeeProfilePic; });
bdsmInvs.forEach(inv => { inv._type = 'bdsm'; inv._key = inv.einladungId; inv._otherName = inv.inviterName; inv._otherPic = inv.inviterAvatar; });
vanillaInvs.forEach(inv => { inv._type = 'vanilla'; inv._key = inv.einladungId; inv._otherName = inv.inviterName; inv._otherPic = inv.inviterAvatar || ''; });
recvItems = [...lockeeInvs, ...khInvs, ...bdsmInvs, ...vanillaInvs].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
recvPage = 0;
renderRecvPage();
} catch(e) { console.error(e); }
}
function renderRecvPage() {
const list = document.getElementById('recvList');
const empty = document.getElementById('recvEmpty');
list.innerHTML = '';
if (recvItems.length === 0) {
empty.style.display = '';
document.getElementById('recvPaging').style.display = 'none';
return;
}
empty.style.display = 'none';
const totalPages = Math.ceil(recvItems.length / PAGE_SIZE);
const start = recvPage * PAGE_SIZE;
const pageItems = recvItems.slice(start, start + PAGE_SIZE);
pageItems.forEach(inv => {
const av = buildAvatarHtml(inv._otherPic, inv._type);
const card = document.createElement('div');
card.className = 'inv-card';
card.id = 'recvinv-' + inv._key;
if (inv._type === 'lockee') card.dataset.detailsVisible = inv.detailsVisible ? '1' : '0';
let typeLabel, line2, actions;
if (inv._type === 'lockee') {
typeLabel = 'Lockee-Einladung';
line2 = 'Lockee: ' + esc(inv.lockName);
actions = `
<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>`;
} else if (inv._type === 'keyholder') {
typeLabel = 'Keyholder-Einladung';
line2 = 'Keyholder: ' + esc(inv.lockName);
actions = `
<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>`;
} else if (inv._type === 'vanilla') {
typeLabel = 'Vanilla Game';
line2 = 'Spieleinladung';
actions = `
<div style="display:flex;flex-direction:column;gap:0.4rem;flex-shrink:0;">
<button onclick="openVanillaInviteDialog('${esc(inv.einladungId)}', '${esc(inv._otherName)}')" 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>`;
} else {
typeLabel = 'BDSM Game';
line2 = 'Spieleinladung';
actions = `
<div style="display:flex;flex-direction:column;gap:0.4rem;flex-shrink:0;">
<button onclick="openBdsmInviteDialog('${esc(inv.einladungId)}', '${esc(inv._otherName)}', '${esc(inv._otherPic || '')}')" 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>`;
}
card.innerHTML = `
${av}
<div class="inv-body">
<div class="inv-line1">${esc(inv._otherName)}</div>
<div class="inv-line2">${line2}</div>
<div class="inv-line3">${typeLabel} · ${fmtDate(inv.createdAt)}</div>
</div>
${actions}`;
list.appendChild(card);
});
renderPaging('recvPaging', recvPage, totalPages, 'goRecvPage');
}
function goRecvPage(page) {
const total = Math.ceil(recvItems.length / PAGE_SIZE);
if (page < 0 || page >= total) return;
recvPage = page;
renderRecvPage();
document.getElementById('recvList').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function removeRecvItem(key) {
recvItems = recvItems.filter(i => i._key !== key);
const total = Math.ceil(recvItems.length / PAGE_SIZE);
if (recvPage >= total && recvPage > 0) recvPage = total - 1;
renderRecvPage();
}
// ── Gesendet laden ──
async function loadSentInvitations() {
try {
const [lockeeRes, khRes, vanillaRes] = await Promise.all([
fetch('/lockee/invitations/sent'),
fetch('/keyholder/invitations/sent'),
fetch('/vanilla/einladung/sent'),
]);
const lockeeInvs = lockeeRes.ok ? await lockeeRes.json() : [];
const khInvs = khRes.ok ? await khRes.json() : [];
const vanillaInvs = vanillaRes.ok ? await vanillaRes.json() : [];
lockeeInvs.forEach(inv => { inv._type = 'lockee'; inv._key = inv.token; inv._otherName = inv.lockeeName; inv._otherPic = inv.lockeeProfilePic; });
khInvs.forEach(inv => { inv._type = 'keyholder'; inv._key = inv.token; inv._otherName = inv.keyholderName; inv._otherPic = inv.keyholderProfilePic; });
vanillaInvs.forEach(inv => { inv._type = 'vanilla'; inv._key = inv.einladungId; inv._otherName = inv.inviteeName; inv._otherPic = ''; });
sentItems = [...lockeeInvs, ...khInvs, ...vanillaInvs].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
sentPage = 0;
renderSentPage();
} catch(e) { console.error(e); }
}
function renderSentPage() {
const list = document.getElementById('sentList');
const empty = document.getElementById('sentEmpty');
list.innerHTML = '';
if (sentItems.length === 0) {
empty.style.display = '';
document.getElementById('sentPaging').style.display = 'none';
return;
}
empty.style.display = 'none';
const totalPages = Math.ceil(sentItems.length / PAGE_SIZE);
const start = sentPage * PAGE_SIZE;
const pageItems = sentItems.slice(start, start + PAGE_SIZE);
pageItems.forEach(inv => {
const av = buildAvatarHtml(inv._otherPic, inv._type);
const card = document.createElement('div');
card.className = 'inv-card';
card.id = 'sentinv-' + inv._key;
let typeLabel, line2sent, extra = '';
if (inv._type === 'lockee') {
typeLabel = 'Lockee-Einladung';
line2sent = 'Lockee: ' + esc(inv.lockName);
extra = inv.detailsVisible
? ' &nbsp;<span style="font-size:0.72rem;">👁 Details sichtbar</span>'
: ' &nbsp;<span style="font-size:0.72rem;">🙈 Details verborgen</span>';
} else if (inv._type === 'vanilla') {
typeLabel = 'Vanilla Game';
line2sent = 'Spieleinladung';
} else {
typeLabel = 'Keyholder-Einladung';
line2sent = 'Keyholder: ' + esc(inv.lockName);
}
card.innerHTML = `
${av}
<div class="inv-body">
<div class="inv-line1">${esc(inv._otherName)}</div>
<div class="inv-line2">${line2sent}</div>
<div class="inv-line3">${typeLabel} · ${fmtDate(inv.createdAt)}${extra}</div>
</div>
<div style="flex-shrink:0;">
<button onclick="cancelSentInvitation('${esc(inv._key)}', '${inv._type}', 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>`;
list.appendChild(card);
});
renderPaging('sentPaging', sentPage, totalPages, 'goSentPage');
}
function goSentPage(page) {
const total = Math.ceil(sentItems.length / PAGE_SIZE);
if (page < 0 || page >= total) return;
sentPage = page;
renderSentPage();
document.getElementById('sentList').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function removeSentItem(key) {
sentItems = sentItems.filter(i => i._key !== key);
const total = Math.ceil(sentItems.length / PAGE_SIZE);
if (sentPage >= total && sentPage > 0) sentPage = total - 1;
renderSentPage();
}
// ── Bestätigungs-Modal ──
let _confirmResolve = null;
function showConfirm(title, text) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmText').textContent = text;
document.getElementById('confirmModal').classList.add('open');
document.querySelector('#confirmModal .confirm-modal-cancel').style.display = '';
return new Promise(resolve => {
_confirmResolve = resolve;
document.getElementById('confirmOkBtn').onclick = () => { confirmClose(true); };
});
}
function showInfo(title, text) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmText').textContent = text;
document.getElementById('confirmModal').classList.add('open');
document.querySelector('#confirmModal .confirm-modal-cancel').style.display = 'none';
return new Promise(resolve => {
_confirmResolve = resolve;
document.getElementById('confirmOkBtn').onclick = () => { confirmClose(true); };
});
}
function confirmCancel() { confirmClose(false); }
function confirmClose(result) {
document.getElementById('confirmModal').classList.remove('open');
if (_confirmResolve) { _confirmResolve(result); _confirmResolve = null; }
}
// ── Aktionen: Empfangen ──
async function declineLockeeInvitation(token, btn) {
if (!await showConfirm('Einladung ablehnen', '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) { removeRecvItem(token); }
else { btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
async function declineKhInvitation(token, btn) {
if (!await showConfirm('Einladung ablehnen', '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) { removeRecvItem(token); }
else { btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
// ── Aktionen: Gesendet ──
async function cancelSentInvitation(key, type, btn) {
const title = 'Einladung zurückziehen';
const text = type === 'lockee'
? 'Das Lock wird gelöscht und der Lockee wird benachrichtigt.'
: type === 'vanilla'
? 'Der eingeladene Spieler wird benachrichtigt.'
: 'Der Keyholder wird benachrichtigt.';
if (!await showConfirm(title, text)) return;
btn.disabled = true;
const url = type === 'lockee'
? '/lockee/invitations/sent/' + encodeURIComponent(key)
: type === 'vanilla'
? '/vanilla/einladung/' + encodeURIComponent(key)
: '/keyholder/invitations/sent/' + encodeURIComponent(key);
try {
const res = await fetch(url, { method: 'DELETE' });
if (res.ok || res.status === 204) { removeSentItem(key); }
else { btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
// ── Lockee-Einladungs-Dialog ──
// CARD_DEFS wird von /js/card-defs.js bereitgestellt.
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
.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('recvinv-' + 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');
removeRecvItem(activeDialogToken);
showUnlockCodeModal(data.unlockCode, data.lockId);
} catch(e) {
acceptBtn.disabled = false;
showDialogError('Fehler beim Annehmen der Einladung.');
}
}
async function declineLockeeInviteDialog() {
if (!activeDialogToken) return;
if (!await showConfirm('Einladung ablehnen', '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) {
removeRecvItem(activeDialogToken);
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 = '/games/chastity/activelock.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);
}
// ── BDSM-Einladungs-Dialog ──
let activeBdsmEinladungId = null;
function openBdsmInviteDialog(einladungId, inviterName, inviterPic) {
activeBdsmEinladungId = einladungId;
document.getElementById('bdsmDialogTitle').textContent = inviterName + ' lädt dich ein';
document.getElementById('bdsmDialogError').style.display = 'none';
const avatarEl = document.getElementById('bdsmDialogAvatar');
avatarEl.innerHTML = inviterPic
? `<img src="data:image/jpeg;base64,${inviterPic}" alt="" style="width:100%;height:100%;object-fit:cover;">`
: '⛓️';
document.getElementById('bdsmInviteDialog').classList.add('open');
}
function closeBdsmInviteDialog() {
document.getElementById('bdsmInviteDialog').classList.remove('open');
activeBdsmEinladungId = null;
}
async function _bdsmAntworten(mode) {
if (!activeBdsmEinladungId) return;
const accepted = mode !== null;
const errEl = document.getElementById('bdsmDialogError');
errEl.style.display = 'none';
try {
const res = await fetch(`/bdsm/einladung/${activeBdsmEinladungId}/antwort`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accepted, mode }),
});
if (!res.ok) throw new Error();
const key = activeBdsmEinladungId;
closeBdsmInviteDialog();
removeRecvItem(key);
if (mode === 'OWN_DEVICE') {
window.location.href = `/games/bdsm/neubdsm.html`;
} else if (mode === 'HOST_DEVICE') {
await showInfo('Einladung angenommen', 'Das Spiel findet am Gerät des Hosts statt. Du wirst zur Startseite weitergeleitet.');
window.location.href = '/userhome.html';
}
} catch (_) {
errEl.textContent = 'Fehler beim Speichern der Antwort.';
errEl.style.display = '';
}
}
function acceptBdsmOwnDevice() { _bdsmAntworten('OWN_DEVICE'); }
function acceptBdsmHostDevice() { _bdsmAntworten('HOST_DEVICE'); }
async function declineBdsmFromDialog() {
if (!await showConfirm('Einladung ablehnen', 'Möchtest du diese BDSM-Game-Einladung wirklich ablehnen?')) return;
_bdsmAntworten(null);
}
// ── Vanilla-Einladungs-Dialog ──
let activeVanillaEinladungId = null;
function openVanillaInviteDialog(einladungId, inviterName) {
activeVanillaEinladungId = einladungId;
document.getElementById('vanillaDialogTitle').textContent = inviterName + ' lädt dich ein';
document.getElementById('vanillaDialogError').style.display = 'none';
document.getElementById('vanillaInviteDialog').classList.add('open');
}
function closeVanillaInviteDialog() {
document.getElementById('vanillaInviteDialog').classList.remove('open');
activeVanillaEinladungId = null;
}
async function _vanillaAntworten(mode) {
if (!activeVanillaEinladungId) return;
const accepted = mode !== null;
const errEl = document.getElementById('vanillaDialogError');
errEl.style.display = 'none';
try {
const res = await fetch(`/vanilla/einladung/${activeVanillaEinladungId}/antwort`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accepted, mode }),
});
if (!res.ok) throw new Error();
const key = activeVanillaEinladungId;
closeVanillaInviteDialog();
removeRecvItem(key);
if (mode === 'OWN_DEVICE') {
window.location.href = '/games/vanilla/neuvanilla.html';
} else if (mode === 'HOST_DEVICE') {
await showInfo('Einladung angenommen', 'Das Spiel findet am Gerät des Hosts statt. Du wirst zur Startseite weitergeleitet.');
window.location.href = '/userhome.html';
}
} catch (_) {
errEl.textContent = 'Fehler beim Speichern der Antwort.';
errEl.style.display = '';
}
}
function acceptVanillaOwnDevice() { _vanillaAntworten('OWN_DEVICE'); }
function acceptVanillaHostDevice() { _vanillaAntworten('HOST_DEVICE'); }
async function declineVanillaFromDialog() {
if (!await showConfirm('Einladung ablehnen', 'Möchtest du diese Vanilla-Game-Einladung wirklich ablehnen?')) return;
_vanillaAntworten(null);
}
// ── Esc schließt Dialog ──
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if (document.getElementById('vanillaInviteDialog').classList.contains('open')) closeVanillaInviteDialog();
if (document.getElementById('bdsmInviteDialog').classList.contains('open')) closeBdsmInviteDialog();
if (document.getElementById('lockeeInviteDialog').classList.contains('open')) closeLockeeInviteDialog();
}
});
// ── Alles laden ──
loadReceivedInvitations();
loadSentInvitations();
</script>
</body>
</html>