Hashtags eingeführt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-11 01:14:33 +02:00
parent ec1409820b
commit e2a71ab096
57 changed files with 2365 additions and 740 deletions

View File

@@ -0,0 +1,371 @@
<!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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 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>