Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
372 lines
17 KiB
HTML
372 lines
17 KiB
HTML
<!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>
|
||
.inv-tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 1.5rem;
|
||
border-bottom: 1px solid var(--color-secondary);
|
||
}
|
||
.inv-tab {
|
||
background: none;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
border-radius: 0;
|
||
padding: 0.5rem 1.1rem;
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
color: var(--color-muted);
|
||
cursor: pointer;
|
||
margin: 0 0 -1px;
|
||
width: auto;
|
||
transition: color 0.15s, border-color 0.15s;
|
||
}
|
||
.inv-tab.active { color: var(--color-primary); border-bottom-color: var(--color-primary); }
|
||
.inv-tab:hover:not(.active) { color: var(--color-text); background: none; }
|
||
|
||
.inv-section-label {
|
||
font-size: 0.8rem; font-weight: 600; color: var(--color-muted);
|
||
text-transform: uppercase; letter-spacing: 0.05em;
|
||
margin: 1.5rem 0 0.65rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid var(--color-secondary);
|
||
}
|
||
.inv-card {
|
||
display: flex; gap: 0.85rem; align-items: center;
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 12px;
|
||
padding: 0.85rem 1rem;
|
||
margin-bottom: 0.6rem;
|
||
text-decoration: none;
|
||
color: inherit;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.inv-card:hover { border-color: var(--color-primary); }
|
||
.inv-avatar {
|
||
width: 44px; height: 44px; border-radius: 50%;
|
||
background: var(--color-secondary);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.2rem; overflow: hidden; flex-shrink: 0;
|
||
}
|
||
.inv-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
||
.inv-body { flex: 1; min-width: 0; }
|
||
.inv-from { font-weight: 600; font-size: 0.95rem;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.inv-type { font-size: 0.78rem; color: var(--color-muted); margin-top: 0.15rem; }
|
||
.inv-actions { display: flex; gap: 0.5rem; flex-shrink: 0; flex-wrap: wrap; align-items: center; }
|
||
.inv-btn {
|
||
background: var(--color-primary);
|
||
color: #fff; border: none; border-radius: 8px;
|
||
padding: 0.38rem 0.9rem; font-size: 0.85rem; font-weight: 600;
|
||
cursor: pointer; margin: 0; width: auto;
|
||
text-decoration: none; display: inline-flex; align-items: center;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.inv-btn:hover { opacity: 0.85; }
|
||
.inv-btn.outline {
|
||
background: none; border: 1px solid var(--color-secondary);
|
||
color: var(--color-muted);
|
||
}
|
||
.inv-btn.outline:hover { border-color: var(--color-primary); color: var(--color-primary); opacity: 1; }
|
||
.inv-arrow { font-size: 0.85rem; color: var(--color-primary); font-weight: 600; flex-shrink: 0; }
|
||
.inv-status { font-size: 0.8rem; color: var(--color-muted); }
|
||
.inv-empty { text-align: center; color: var(--color-muted); padding: 3rem 1rem; font-size: 0.95rem; }
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
<div class="main">
|
||
<div class="content">
|
||
<h1 style="margin:0 0 1.25rem;">Einladungen</h1>
|
||
|
||
<div class="inv-tabs">
|
||
<button class="inv-tab" id="tabBtnErhalten" onclick="switchTab('erhalten')">Erhalten</button>
|
||
<button class="inv-tab" id="tabBtnGesendet" onclick="switchTab('gesendet')">Gesendet</button>
|
||
</div>
|
||
|
||
<!-- ── Erhalten ─────────────────────────────────────────────────── -->
|
||
<div id="panelErhalten">
|
||
<div id="loadingErhalten" style="text-align:center;color:var(--color-muted);padding:2rem 0;">Wird geladen…</div>
|
||
|
||
<div id="secVanilla" style="display:none;">
|
||
<div class="inv-section-label">🎭 Vanilla Game</div>
|
||
<div id="listVanilla"></div>
|
||
</div>
|
||
|
||
<div id="secBdsm" style="display:none;">
|
||
<div class="inv-section-label">⛓ BDSM Game</div>
|
||
<div id="listBdsm"></div>
|
||
</div>
|
||
|
||
<div id="secChastity" style="display:none;">
|
||
<div class="inv-section-label">🔒 Chastity Game</div>
|
||
<div id="listChastity"></div>
|
||
</div>
|
||
|
||
<div id="emptyErhalten" style="display:none;">
|
||
<div class="inv-empty">Du hast aktuell keine offenen Einladungen.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Gesendet ─────────────────────────────────────────────────── -->
|
||
<div id="panelGesendet" style="display:none;">
|
||
<div id="loadingGesendet" style="text-align:center;color:var(--color-muted);padding:2rem 0;">Wird geladen…</div>
|
||
|
||
<div id="secSentVanilla" style="display:none;">
|
||
<div class="inv-section-label">🎭 Vanilla Game</div>
|
||
<div id="listSentVanilla"></div>
|
||
</div>
|
||
|
||
<div id="secSentBdsm" style="display:none;">
|
||
<div class="inv-section-label">⛓ BDSM Game</div>
|
||
<div id="listSentBdsm"></div>
|
||
</div>
|
||
|
||
<div id="secSentChastity" style="display:none;">
|
||
<div class="inv-section-label">🔒 Chastity Game</div>
|
||
<div id="listSentChastity"></div>
|
||
</div>
|
||
|
||
<div id="emptySent" style="display:none;">
|
||
<div class="inv-empty">Du hast aktuell keine gesendeten Einladungen.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/nav.js"></script>
|
||
<script>
|
||
function esc(s) {
|
||
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ── Tab-Switching ────────────────────────────────────────────────────────
|
||
function switchTab(tab) {
|
||
const erhalten = tab === 'erhalten';
|
||
document.getElementById('panelErhalten').style.display = erhalten ? '' : 'none';
|
||
document.getElementById('panelGesendet').style.display = erhalten ? 'none' : '';
|
||
document.getElementById('tabBtnErhalten').classList.toggle('active', erhalten);
|
||
document.getElementById('tabBtnGesendet').classList.toggle('active', !erhalten);
|
||
const url = new URL(location.href);
|
||
erhalten ? url.searchParams.delete('tab') : url.searchParams.set('tab', tab);
|
||
history.replaceState(null, '', url);
|
||
}
|
||
|
||
const initTab = new URLSearchParams(location.search).get('tab') || 'erhalten';
|
||
switchTab(initTab);
|
||
|
||
// ── Erhalten ──────────────────────────────────────────────────────────────
|
||
async function loadErhalten() {
|
||
let shown = 0;
|
||
try {
|
||
const [vRes, bRes, cRes] = await Promise.all([
|
||
fetch('/vanilla/einladung/pending'),
|
||
fetch('/bdsm/einladung/pending'),
|
||
fetch('/lockee/invitations/mine'),
|
||
]);
|
||
|
||
// Vanilla
|
||
if (vRes.ok) {
|
||
const list = await vRes.json();
|
||
if (list.length) {
|
||
document.getElementById('secVanilla').style.display = '';
|
||
document.getElementById('listVanilla').innerHTML = list.map(e => `
|
||
<div class="inv-card" id="vcard-${esc(e.id)}">
|
||
<div class="inv-avatar">
|
||
${e.inviterAvatar
|
||
? `<img src="data:image/png;base64,${e.inviterAvatar}" alt="">`
|
||
: '🎭'}
|
||
</div>
|
||
<div class="inv-body">
|
||
<div class="inv-from">${esc(e.inviterName || 'Jemand')}</div>
|
||
<div class="inv-type">Vanilla-Spieleinladung</div>
|
||
</div>
|
||
<div class="inv-actions">
|
||
<button class="inv-btn" onclick="acceptVanilla(${esc(e.id)})">Annehmen</button>
|
||
<button class="inv-btn outline" onclick="declineVanilla(${esc(e.id)})">Ablehnen</button>
|
||
</div>
|
||
</div>`).join('');
|
||
shown += list.length;
|
||
}
|
||
}
|
||
|
||
// BDSM
|
||
if (bRes.ok) {
|
||
const list = await bRes.json();
|
||
if (list.length) {
|
||
document.getElementById('secBdsm').style.display = '';
|
||
document.getElementById('listBdsm').innerHTML = list.map(e => `
|
||
<a class="inv-card" href="/games/bdsm/bdsm-einladung.html?id=${esc(e.id)}">
|
||
<div class="inv-avatar">
|
||
${e.inviterAvatar
|
||
? `<img src="data:image/png;base64,${e.inviterAvatar}" alt="">`
|
||
: '⛓'}
|
||
</div>
|
||
<div class="inv-body">
|
||
<div class="inv-from">${esc(e.inviterName || 'Jemand')}</div>
|
||
<div class="inv-type">BDSM-Spieleinladung</div>
|
||
</div>
|
||
<span class="inv-arrow">Ansehen →</span>
|
||
</a>`).join('');
|
||
shown += list.length;
|
||
}
|
||
}
|
||
|
||
// Chastity
|
||
if (cRes.ok) {
|
||
const list = await cRes.json();
|
||
if (list.length) {
|
||
document.getElementById('secChastity').style.display = '';
|
||
document.getElementById('listChastity').innerHTML = list.map(e => `
|
||
<a class="inv-card" href="/games/chastity/joinlock.html?token=${esc(e.token)}">
|
||
<div class="inv-avatar">
|
||
${e.keyholderProfilePic
|
||
? `<img src="data:image/png;base64,${e.keyholderProfilePic}" alt="">`
|
||
: '🔒'}
|
||
</div>
|
||
<div class="inv-body">
|
||
<div class="inv-from">${esc(e.keyholderName || 'Jemand')}</div>
|
||
<div class="inv-type">Keuschheitslock: ${esc(e.lockName || '')}</div>
|
||
</div>
|
||
<span class="inv-arrow">Ansehen →</span>
|
||
</a>`).join('');
|
||
shown += list.length;
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
|
||
document.getElementById('loadingErhalten').style.display = 'none';
|
||
if (!shown) document.getElementById('emptyErhalten').style.display = '';
|
||
}
|
||
|
||
// Vanilla inline annehmen / ablehnen
|
||
async function acceptVanilla(id) {
|
||
const card = document.getElementById('vcard-' + id);
|
||
if (card) card.querySelector('.inv-actions').innerHTML = '<span class="inv-status">Wird gespeichert…</span>';
|
||
try {
|
||
const res = await fetch(`/vanilla/einladung/${id}/antwort`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ accepted: true, mode: 'OWN_DEVICE' }),
|
||
});
|
||
if (res.ok) {
|
||
window.location.href = '/games/vanilla/neuvanilla.html';
|
||
} else {
|
||
if (card) card.querySelector('.inv-actions').innerHTML = '<span class="inv-status" style="color:var(--color-primary)">Fehler beim Annehmen.</span>';
|
||
}
|
||
} catch (_) {
|
||
if (card) card.querySelector('.inv-actions').innerHTML = '<span class="inv-status" style="color:var(--color-primary)">Fehler.</span>';
|
||
}
|
||
}
|
||
|
||
async function declineVanilla(id) {
|
||
const card = document.getElementById('vcard-' + id);
|
||
if (card) card.querySelector('.inv-actions').innerHTML = '<span class="inv-status">Wird gespeichert…</span>';
|
||
try {
|
||
await fetch(`/vanilla/einladung/${id}/antwort`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ accepted: false, mode: null }),
|
||
});
|
||
if (card) card.remove();
|
||
if (!document.getElementById('listVanilla').children.length) {
|
||
document.getElementById('secVanilla').style.display = 'none';
|
||
}
|
||
checkEmptyErhalten();
|
||
} catch (_) {}
|
||
}
|
||
|
||
function checkEmptyErhalten() {
|
||
const anyVisible = ['secVanilla','secBdsm','secChastity']
|
||
.some(id => document.getElementById(id).style.display !== 'none');
|
||
document.getElementById('emptyErhalten').style.display = anyVisible ? 'none' : '';
|
||
}
|
||
|
||
// ── Gesendet ──────────────────────────────────────────────────────────────
|
||
async function loadGesendet() {
|
||
let shown = 0;
|
||
try {
|
||
const [bRes, vRes, cRes] = await Promise.all([
|
||
fetch('/bdsm/einladung/meine-aktive'),
|
||
fetch('/vanilla/einladung/meine-aktive'),
|
||
fetch('/keyholder/invitations/mine'),
|
||
]);
|
||
|
||
// Vanilla gesendete Einladung
|
||
if (vRes.ok) {
|
||
const e = await vRes.json();
|
||
if (e) {
|
||
document.getElementById('secSentVanilla').style.display = '';
|
||
document.getElementById('listSentVanilla').innerHTML = `
|
||
<div class="inv-card">
|
||
<div class="inv-avatar">🎭</div>
|
||
<div class="inv-body">
|
||
<div class="inv-from">Vanilla-Einladung gesendet</div>
|
||
<div class="inv-type">Warte auf Antwort…</div>
|
||
</div>
|
||
<a class="inv-btn" href="/games/vanilla/neuvanilla.html">Zum Spiel →</a>
|
||
</div>`;
|
||
shown++;
|
||
}
|
||
}
|
||
|
||
// BDSM gesendete Einladung
|
||
if (bRes.ok) {
|
||
const e = await bRes.json();
|
||
if (e) {
|
||
document.getElementById('secSentBdsm').style.display = '';
|
||
document.getElementById('listSentBdsm').innerHTML = `
|
||
<div class="inv-card">
|
||
<div class="inv-avatar">⛓</div>
|
||
<div class="inv-body">
|
||
<div class="inv-from">BDSM-Einladung gesendet</div>
|
||
<div class="inv-type">Warte auf Antwort…</div>
|
||
</div>
|
||
<a class="inv-btn" href="/games/bdsm/neubdsm.html">Zum Spiel →</a>
|
||
</div>`;
|
||
shown++;
|
||
}
|
||
}
|
||
|
||
// Chastity gesendete Einladungen (als Keyholder)
|
||
if (cRes.ok) {
|
||
const list = await cRes.json();
|
||
if (Array.isArray(list) && list.length) {
|
||
document.getElementById('secSentChastity').style.display = '';
|
||
document.getElementById('listSentChastity').innerHTML = list.map(e => `
|
||
<div class="inv-card">
|
||
<div class="inv-avatar">🔒</div>
|
||
<div class="inv-body">
|
||
<div class="inv-from">${esc(e.lockeeName || e.lockeeEmail || 'Eingeladene Person')}</div>
|
||
<div class="inv-type">Lock: ${esc(e.lockName || '')} · ${esc(e.status || 'Ausstehend')}</div>
|
||
</div>
|
||
<a class="inv-btn outline" href="/games/chastity/keyholder.html">Keyholder →</a>
|
||
</div>`).join('');
|
||
shown += list.length;
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
|
||
document.getElementById('loadingGesendet').style.display = 'none';
|
||
if (!shown) document.getElementById('emptySent').style.display = '';
|
||
}
|
||
|
||
// Auth-Check & Start
|
||
fetch('/login/me')
|
||
.then(r => { if (r.status === 401) window.location.href = '/login.html'; return r.ok ? r.json() : null; })
|
||
.then(user => {
|
||
if (user) { loadErhalten(); loadGesendet(); }
|
||
})
|
||
.catch(() => { window.location.href = '/login.html'; });
|
||
</script>
|
||
</body>
|
||
</html>
|