Weiter an der Oberfläche getüftelt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -577,6 +577,8 @@ body.app {
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.55rem 1rem;
|
||||
position: relative;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
/* Linker Bereich – Banner, gleiche Breite wie Sidebar */
|
||||
@@ -596,6 +598,7 @@ body.app {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.topbar-banner {
|
||||
height: 3.5rem;
|
||||
@@ -638,20 +641,31 @@ body.app {
|
||||
|
||||
.topbar-search-overlay {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
z-index: 600;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
|
||||
}
|
||||
|
||||
.topbar-search-overlay.open { display: block; }
|
||||
.topbar-search-overlay.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.topbar-search-hint {
|
||||
padding: 0.75rem 1rem;
|
||||
@@ -794,17 +808,29 @@ body.app {
|
||||
position: fixed;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
|
||||
z-index: 550;
|
||||
width: 360px;
|
||||
max-height: 500px;
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0.22s;
|
||||
}
|
||||
|
||||
.topbar-panel.open { display: flex; }
|
||||
.topbar-panel.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.topbar-panel-header {
|
||||
display: flex;
|
||||
@@ -996,7 +1022,12 @@ body.app {
|
||||
.topbar-profile-link:hover { background: var(--color-secondary); }
|
||||
.topbar-profile-link--danger { color: var(--color-primary); }
|
||||
|
||||
/* ── Mobile: Topbar ausblenden ── */
|
||||
/* ── Mobile: Topbar ausblenden, Content-Rahmen entfernen ── */
|
||||
@media (max-width: 768px) {
|
||||
.topbar { display: none; }
|
||||
.main {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
1776
bin/main/static/games/bdsm/aufgaben.html
Normal file
1776
bin/main/static/games/bdsm/aufgaben.html
Normal file
File diff suppressed because it is too large
Load Diff
485
bin/main/static/games/bdsm/entdecken.html
Normal file
485
bin/main/static/games/bdsm/entdecken.html
Normal file
@@ -0,0 +1,485 @@
|
||||
<!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>Entdecken – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* ── Search ── */
|
||||
.search-bar {
|
||||
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
|
||||
}
|
||||
.search-bar input[type="text"] {
|
||||
flex: 1; padding: 0.55rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
background: var(--color-card); color: var(--color-text);
|
||||
font-size: 0.95rem; outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.search-bar input[type="text"]:focus { border-color: var(--color-primary); }
|
||||
.search-bar input[type="text"]::placeholder { color: var(--color-muted); }
|
||||
.btn-search {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.btn-search:hover { background: var(--color-primary); color: #fff; }
|
||||
|
||||
/* ── Paging ── */
|
||||
.paging {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; margin-top: 1rem;
|
||||
}
|
||||
.paging button {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.paging button:hover:not(:disabled) { background: var(--color-primary); }
|
||||
.paging button:disabled { opacity: 0.35; cursor: default; }
|
||||
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
|
||||
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Gruppe card ── */
|
||||
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.gruppe-card {
|
||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
|
||||
}
|
||||
.gruppe-card.open { border-color: rgba(233,69,96,0.35); }
|
||||
.gruppe-header {
|
||||
display: flex; align-items: center; gap: 0.9rem;
|
||||
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
|
||||
}
|
||||
.gruppe-img {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
object-fit: cover; flex-shrink: 0;
|
||||
}
|
||||
.gruppe-img-placeholder {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
|
||||
}
|
||||
.gruppe-meta { flex: 1; min-width: 0; }
|
||||
.gruppe-name {
|
||||
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
|
||||
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
|
||||
.gruppe-badge {
|
||||
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-muted);
|
||||
}
|
||||
.gruppe-badge-sub { background: rgba(46,204,113,0.15); color: var(--color-success); }
|
||||
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
|
||||
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||
|
||||
/* ── Subscribe button ── */
|
||||
.btn-sub {
|
||||
background: none; border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
color: var(--color-muted); font-size: 0.8rem; padding: 0.3rem 0.75rem;
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
flex-shrink: 0; white-space: nowrap;
|
||||
}
|
||||
.btn-sub:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.btn-sub.subscribed {
|
||||
border-color: rgba(46,204,113,0.5); color: var(--color-success);
|
||||
}
|
||||
.btn-sub.subscribed:hover {
|
||||
border-color: var(--color-primary); color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.08);
|
||||
}
|
||||
.btn-sub:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* ── Gruppe body ── */
|
||||
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
|
||||
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
|
||||
|
||||
.sub-section + .sub-section { margin-top: 0.85rem; }
|
||||
.sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
|
||||
.sub-section-title {
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
|
||||
text-transform: uppercase; color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── Items ── */
|
||||
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.item { border-radius: 6px; background: var(--color-secondary); overflow: hidden; }
|
||||
.item-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.35rem 0.6rem;
|
||||
cursor: pointer; user-select: none; transition: background 0.12s;
|
||||
}
|
||||
.item-row:hover { background: rgba(255,255,255,0.04); }
|
||||
.item.open .item-row { background: rgba(233,69,96,0.08); }
|
||||
.item-text {
|
||||
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
|
||||
.badge {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
|
||||
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
|
||||
}
|
||||
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
|
||||
|
||||
/* ── Item detail ── */
|
||||
.item-detail {
|
||||
display: none; padding: 0.5rem 0.6rem 0.6rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
|
||||
}
|
||||
.item.open .item-detail { display: block; }
|
||||
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
|
||||
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
|
||||
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
|
||||
.item-detail-chip {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-text);
|
||||
}
|
||||
.item-detail-chip-toy { background: rgba(233,69,96,0.12); color: var(--color-primary); }
|
||||
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
|
||||
<button class="btn-search" id="searchBtn">Suchen</button>
|
||||
</div>
|
||||
<div id="loading" class="loading">Wird geladen…</div>
|
||||
<div id="groupList" class="gruppe-list"></div>
|
||||
<div class="paging" id="paging" style="display:none;">
|
||||
<button id="prevBtn">‹ Zurück</button>
|
||||
<span class="page-info" id="pageInfo"></span>
|
||||
<button id="nextBtn">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 0, totalPages = 1;
|
||||
let currentName = '';
|
||||
|
||||
// ── XSS ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── Auth ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.then(user => { if (!user) return; loadGroups(); })
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load ──
|
||||
function loadGroups() {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('groupList').innerHTML = '';
|
||||
document.getElementById('paging').style.display = 'none';
|
||||
const nameParam = currentName ? `&name=${encodeURIComponent(currentName)}` : '';
|
||||
fetch(`/abo/discover?page=${currentPage}&size=${PAGE_SIZE}${nameParam}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
totalPages = data.totalPages || 1;
|
||||
renderGroups(data.content || []);
|
||||
updatePaging(currentPage, totalPages);
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('loading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
const WERKZEUG_LABEL = {
|
||||
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
|
||||
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
|
||||
};
|
||||
|
||||
function werkzeugChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
|
||||
}
|
||||
function toyChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
|
||||
}
|
||||
function formatSek(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} s`;
|
||||
if (von != null) return `ab ${von} s`;
|
||||
if (bis != null) return `bis ${bis} s`;
|
||||
return '';
|
||||
}
|
||||
function formatMin(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} min`;
|
||||
if (von != null) return `ab ${von} min`;
|
||||
if (bis != null) return `bis ${bis} min`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Track which group card is open
|
||||
let openGroupId = null;
|
||||
// Track which item detail is open
|
||||
let openItemId = null;
|
||||
|
||||
function renderGroups(groups) {
|
||||
const list = document.getElementById('groupList');
|
||||
if (!groups || groups.length === 0) {
|
||||
list.innerHTML = '<p class="empty">Keine Gruppen gefunden.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = groups.map(g => {
|
||||
const aufgabenCount = (g.aufgaben || []).length;
|
||||
const strafeCount = (g.strafen || []).length;
|
||||
const sperreCount = (g.sperren || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
const subLabel = g.subscribed
|
||||
? `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`
|
||||
: '';
|
||||
const subCount = g.subscriberCount > 0
|
||||
? `<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`
|
||||
: '';
|
||||
|
||||
const subBtnClass = g.subscribed ? 'btn-sub subscribed' : 'btn-sub';
|
||||
const subBtnText = g.subscribed ? '♥ Abonniert' : '♥ Abonnieren';
|
||||
|
||||
return `
|
||||
<div class="gruppe-card" id="dgroup-${esc(g.gruppenId)}">
|
||||
<div class="gruppe-header">
|
||||
<div style="cursor:pointer; display:flex; align-items:center; gap:0.9rem; flex:1; min-width:0;"
|
||||
onclick="toggleGroup('${esc(g.gruppenId)}')">
|
||||
${g.bild
|
||||
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
|
||||
: `<div class="gruppe-img-placeholder">⊙</div>`}
|
||||
<div class="gruppe-meta">
|
||||
<div class="gruppe-name">${esc(g.name)}</div>
|
||||
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
|
||||
${(subLabel || subCount) ? `<div class="gruppe-badges">${subCount}${subLabel}</div>` : ''}
|
||||
</div>
|
||||
<span class="gruppe-toggle">▶</span>
|
||||
</div>
|
||||
<button class="${subBtnClass}" id="subbtn-${esc(g.gruppenId)}"
|
||||
onclick="toggleSubscribe('${esc(g.gruppenId)}', this)">
|
||||
${subBtnText}
|
||||
</button>
|
||||
</div>
|
||||
<div class="gruppe-body" id="dbody-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), renderAufgabe)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), renderStrafe)}
|
||||
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), renderZeitstrafe)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
openItemId = null;
|
||||
}
|
||||
|
||||
function renderSubSection(title, items, renderFn) {
|
||||
return `<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<span class="sub-section-title">${esc(title)} (${items.length})</span>
|
||||
</div>
|
||||
${items.length === 0
|
||||
? '<div class="sub-empty">Keine Einträge</div>'
|
||||
: `<div class="item-list">${items.map(item => renderFn(item)).join('')}</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderAufgabe(a) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
|
||||
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
|
||||
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
|
||||
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(a.aufgabeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
|
||||
<span class="item-text">${esc(a.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStrafe(s) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
|
||||
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
|
||||
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
|
||||
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(s.strafeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
|
||||
<span class="item-text">${esc(s.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderZeitstrafe(z) {
|
||||
const badges = [];
|
||||
const zeit = formatMin(z.minutenVon, z.minutenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
||||
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
|
||||
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
|
||||
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(z.sperreId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
|
||||
<span class="item-text">${esc(z.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Sort ──
|
||||
function sortByLevelThenName(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const la = a.level ?? 999, lb = b.level ?? 999;
|
||||
if (la !== lb) return la - lb;
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
function sortByName(items) {
|
||||
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
|
||||
}
|
||||
|
||||
// ── Group toggle ──
|
||||
function toggleGroup(gruppenId) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
const body = document.getElementById('dbody-' + gruppenId);
|
||||
if (!card) return;
|
||||
if (card.classList.contains('open')) {
|
||||
card.classList.remove('open');
|
||||
body.style.display = 'none';
|
||||
if (openGroupId === gruppenId) openGroupId = null;
|
||||
} else {
|
||||
if (openGroupId) {
|
||||
const prev = document.getElementById('dgroup-' + openGroupId);
|
||||
const prevBody = document.getElementById('dbody-' + openGroupId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
if (prevBody) prevBody.style.display = 'none';
|
||||
}
|
||||
card.classList.add('open');
|
||||
body.style.display = 'block';
|
||||
openGroupId = gruppenId;
|
||||
openItemId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Item toggle ──
|
||||
function toggleItem(itemId) {
|
||||
if (openItemId === itemId) {
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.remove('open');
|
||||
openItemId = null;
|
||||
return;
|
||||
}
|
||||
if (openItemId) {
|
||||
const prev = document.getElementById('ditem-' + openItemId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
}
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.add('open');
|
||||
openItemId = itemId;
|
||||
}
|
||||
|
||||
// ── Subscribe / Unsubscribe ──
|
||||
function toggleSubscribe(gruppenId, btn) {
|
||||
btn.disabled = true;
|
||||
const isSubscribed = btn.classList.contains('subscribed');
|
||||
const method = isSubscribed ? 'DELETE' : 'POST';
|
||||
fetch(`/abo/${gruppenId}`, { method })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201 || r.status === 202) {
|
||||
if (isSubscribed) {
|
||||
btn.classList.remove('subscribed');
|
||||
btn.textContent = '♥ Abonnieren';
|
||||
updateBadge(gruppenId, false);
|
||||
} else {
|
||||
btn.classList.add('subscribed');
|
||||
btn.textContent = '♥ Abonniert';
|
||||
updateBadge(gruppenId, true);
|
||||
}
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function updateBadge(gruppenId, subscribed) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
if (!card) return;
|
||||
const badgesEl = card.querySelector('.gruppe-badges');
|
||||
if (!badgesEl) return;
|
||||
const subBadge = badgesEl.querySelector('.gruppe-badge-sub');
|
||||
if (subscribed && !subBadge) {
|
||||
badgesEl.insertAdjacentHTML('beforeend', `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`);
|
||||
} else if (!subscribed && subBadge) {
|
||||
subBadge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search ──
|
||||
document.getElementById('searchBtn').addEventListener('click', () => {
|
||||
currentName = document.getElementById('searchInput').value.trim();
|
||||
currentPage = 0;
|
||||
loadGroups();
|
||||
});
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('searchBtn').click();
|
||||
});
|
||||
|
||||
// ── Paging ──
|
||||
function updatePaging(current, total) {
|
||||
const el = document.getElementById('paging');
|
||||
if (total <= 1) { el.style.display = 'none'; return; }
|
||||
el.style.display = 'flex';
|
||||
document.getElementById('prevBtn').disabled = current === 0;
|
||||
document.getElementById('nextBtn').disabled = current >= total - 1;
|
||||
document.getElementById('pageInfo').textContent = `Seite ${current + 1} von ${total}`;
|
||||
}
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => {
|
||||
if (currentPage > 0) { currentPage--; loadGroups(); }
|
||||
});
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
if (currentPage < totalPages - 1) { currentPage++; loadGroups(); }
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,982 +0,0 @@
|
||||
<!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/nav.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
|
||||
? ' <span style="font-size:0.72rem;">👁 Details sichtbar</span>'
|
||||
: ' <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>
|
||||
@@ -4,7 +4,7 @@
|
||||
<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 id="pageTitle">Aufgaben – xXx Sphere</title>
|
||||
<title>Aufgaben – Vanilla – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
@@ -609,21 +609,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Mode detection ──
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const IS_BDSM_MODE = urlParams.get('mode') === 'bdsm';
|
||||
|
||||
// ── API-URL-Hilfsfunktion ──
|
||||
function apiUrl(path) {
|
||||
if (IS_BDSM_MODE) return path;
|
||||
return '/vanilla' + path;
|
||||
}
|
||||
|
||||
// ── Seitenüberschrift ──
|
||||
document.getElementById('pageTitle').textContent = IS_BDSM_MODE
|
||||
? 'Aufgaben – BDSM – xXx Sphere'
|
||||
: 'Aufgaben – Vanilla – xXx Sphere';
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
let userPage = 0, userTotalPages = 1;
|
||||
let aboPage = 0, aboTotalPages = 1;
|
||||
@@ -715,8 +705,6 @@
|
||||
const finisherCount = (g.finisher || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
IS_BDSM_MODE && strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
IS_BDSM_MODE && sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : '',
|
||||
finisherCount ? `${finisherCount} Finisher` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
@@ -742,8 +730,6 @@
|
||||
<div class="gruppe-body" id="body-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${IS_BDSM_MODE ? renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type) : ''}
|
||||
${IS_BDSM_MODE ? renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type) : ''}
|
||||
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -755,7 +741,7 @@
|
||||
|
||||
// ── Sub-sections ──
|
||||
function renderSubSection(title, items, kind, renderFn, gruppenId, type) {
|
||||
const showAdd = type === 'user' && (IS_BDSM_MODE || (kind !== 'strafe' && kind !== 'zeitstrafe'));
|
||||
const showAdd = type === 'user' && (kind !== 'strafe' && kind !== 'zeitstrafe');
|
||||
const addBtn = showAdd
|
||||
? `<button class="btn-sub-add" onclick="openItemModal('${esc(gruppenId)}','${kind}')">+ ${title.replace('en','').replace('fen','fe')}</button>`
|
||||
: '';
|
||||
@@ -917,8 +903,8 @@
|
||||
// ── Item löschen ──
|
||||
const ITEM_DELETE_URL = {
|
||||
aufgabe: apiUrl('/aufgabe'),
|
||||
strafe: IS_BDSM_MODE ? '/strafe' : null,
|
||||
zeitstrafe: IS_BDSM_MODE ? '/sperre' : null,
|
||||
strafe: null,
|
||||
zeitstrafe: null,
|
||||
finisher: apiUrl('/finisher')
|
||||
};
|
||||
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
|
||||
485
bin/main/static/games/vanilla/entdecken.html
Normal file
485
bin/main/static/games/vanilla/entdecken.html
Normal file
@@ -0,0 +1,485 @@
|
||||
<!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>Entdecken – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* ── Search ── */
|
||||
.search-bar {
|
||||
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
|
||||
}
|
||||
.search-bar input[type="text"] {
|
||||
flex: 1; padding: 0.55rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
background: var(--color-card); color: var(--color-text);
|
||||
font-size: 0.95rem; outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.search-bar input[type="text"]:focus { border-color: var(--color-primary); }
|
||||
.search-bar input[type="text"]::placeholder { color: var(--color-muted); }
|
||||
.btn-search {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.btn-search:hover { background: var(--color-primary); color: #fff; }
|
||||
|
||||
/* ── Paging ── */
|
||||
.paging {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; margin-top: 1rem;
|
||||
}
|
||||
.paging button {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.paging button:hover:not(:disabled) { background: var(--color-primary); }
|
||||
.paging button:disabled { opacity: 0.35; cursor: default; }
|
||||
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
|
||||
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Gruppe card ── */
|
||||
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.gruppe-card {
|
||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
|
||||
}
|
||||
.gruppe-card.open { border-color: rgba(233,69,96,0.35); }
|
||||
.gruppe-header {
|
||||
display: flex; align-items: center; gap: 0.9rem;
|
||||
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
|
||||
}
|
||||
.gruppe-img {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
object-fit: cover; flex-shrink: 0;
|
||||
}
|
||||
.gruppe-img-placeholder {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
|
||||
}
|
||||
.gruppe-meta { flex: 1; min-width: 0; }
|
||||
.gruppe-name {
|
||||
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
|
||||
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
|
||||
.gruppe-badge {
|
||||
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-muted);
|
||||
}
|
||||
.gruppe-badge-sub { background: rgba(46,204,113,0.15); color: var(--color-success); }
|
||||
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
|
||||
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||
|
||||
/* ── Subscribe button ── */
|
||||
.btn-sub {
|
||||
background: none; border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
color: var(--color-muted); font-size: 0.8rem; padding: 0.3rem 0.75rem;
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
flex-shrink: 0; white-space: nowrap;
|
||||
}
|
||||
.btn-sub:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.btn-sub.subscribed {
|
||||
border-color: rgba(46,204,113,0.5); color: var(--color-success);
|
||||
}
|
||||
.btn-sub.subscribed:hover {
|
||||
border-color: var(--color-primary); color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.08);
|
||||
}
|
||||
.btn-sub:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* ── Gruppe body ── */
|
||||
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
|
||||
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
|
||||
|
||||
.sub-section + .sub-section { margin-top: 0.85rem; }
|
||||
.sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
|
||||
.sub-section-title {
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
|
||||
text-transform: uppercase; color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── Items ── */
|
||||
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.item { border-radius: 6px; background: var(--color-secondary); overflow: hidden; }
|
||||
.item-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.35rem 0.6rem;
|
||||
cursor: pointer; user-select: none; transition: background 0.12s;
|
||||
}
|
||||
.item-row:hover { background: rgba(255,255,255,0.04); }
|
||||
.item.open .item-row { background: rgba(233,69,96,0.08); }
|
||||
.item-text {
|
||||
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
|
||||
.badge {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
|
||||
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
|
||||
}
|
||||
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
|
||||
|
||||
/* ── Item detail ── */
|
||||
.item-detail {
|
||||
display: none; padding: 0.5rem 0.6rem 0.6rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
|
||||
}
|
||||
.item.open .item-detail { display: block; }
|
||||
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
|
||||
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
|
||||
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
|
||||
.item-detail-chip {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-text);
|
||||
}
|
||||
.item-detail-chip-toy { background: rgba(233,69,96,0.12); color: var(--color-primary); }
|
||||
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
|
||||
<button class="btn-search" id="searchBtn">Suchen</button>
|
||||
</div>
|
||||
<div id="loading" class="loading">Wird geladen…</div>
|
||||
<div id="groupList" class="gruppe-list"></div>
|
||||
<div class="paging" id="paging" style="display:none;">
|
||||
<button id="prevBtn">‹ Zurück</button>
|
||||
<span class="page-info" id="pageInfo"></span>
|
||||
<button id="nextBtn">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 0, totalPages = 1;
|
||||
let currentName = '';
|
||||
|
||||
// ── XSS ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── Auth ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.then(user => { if (!user) return; loadGroups(); })
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load ──
|
||||
function loadGroups() {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('groupList').innerHTML = '';
|
||||
document.getElementById('paging').style.display = 'none';
|
||||
const nameParam = currentName ? `&name=${encodeURIComponent(currentName)}` : '';
|
||||
fetch(`/abo/discover?page=${currentPage}&size=${PAGE_SIZE}${nameParam}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
totalPages = data.totalPages || 1;
|
||||
renderGroups(data.content || []);
|
||||
updatePaging(currentPage, totalPages);
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('loading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
const WERKZEUG_LABEL = {
|
||||
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
|
||||
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
|
||||
};
|
||||
|
||||
function werkzeugChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
|
||||
}
|
||||
function toyChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
|
||||
}
|
||||
function formatSek(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} s`;
|
||||
if (von != null) return `ab ${von} s`;
|
||||
if (bis != null) return `bis ${bis} s`;
|
||||
return '';
|
||||
}
|
||||
function formatMin(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} min`;
|
||||
if (von != null) return `ab ${von} min`;
|
||||
if (bis != null) return `bis ${bis} min`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Track which group card is open
|
||||
let openGroupId = null;
|
||||
// Track which item detail is open
|
||||
let openItemId = null;
|
||||
|
||||
function renderGroups(groups) {
|
||||
const list = document.getElementById('groupList');
|
||||
if (!groups || groups.length === 0) {
|
||||
list.innerHTML = '<p class="empty">Keine Gruppen gefunden.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = groups.map(g => {
|
||||
const aufgabenCount = (g.aufgaben || []).length;
|
||||
const strafeCount = (g.strafen || []).length;
|
||||
const sperreCount = (g.sperren || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
const subLabel = g.subscribed
|
||||
? `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`
|
||||
: '';
|
||||
const subCount = g.subscriberCount > 0
|
||||
? `<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`
|
||||
: '';
|
||||
|
||||
const subBtnClass = g.subscribed ? 'btn-sub subscribed' : 'btn-sub';
|
||||
const subBtnText = g.subscribed ? '♥ Abonniert' : '♥ Abonnieren';
|
||||
|
||||
return `
|
||||
<div class="gruppe-card" id="dgroup-${esc(g.gruppenId)}">
|
||||
<div class="gruppe-header">
|
||||
<div style="cursor:pointer; display:flex; align-items:center; gap:0.9rem; flex:1; min-width:0;"
|
||||
onclick="toggleGroup('${esc(g.gruppenId)}')">
|
||||
${g.bild
|
||||
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
|
||||
: `<div class="gruppe-img-placeholder">⊙</div>`}
|
||||
<div class="gruppe-meta">
|
||||
<div class="gruppe-name">${esc(g.name)}</div>
|
||||
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
|
||||
${(subLabel || subCount) ? `<div class="gruppe-badges">${subCount}${subLabel}</div>` : ''}
|
||||
</div>
|
||||
<span class="gruppe-toggle">▶</span>
|
||||
</div>
|
||||
<button class="${subBtnClass}" id="subbtn-${esc(g.gruppenId)}"
|
||||
onclick="toggleSubscribe('${esc(g.gruppenId)}', this)">
|
||||
${subBtnText}
|
||||
</button>
|
||||
</div>
|
||||
<div class="gruppe-body" id="dbody-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), renderAufgabe)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), renderStrafe)}
|
||||
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), renderZeitstrafe)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
openItemId = null;
|
||||
}
|
||||
|
||||
function renderSubSection(title, items, renderFn) {
|
||||
return `<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<span class="sub-section-title">${esc(title)} (${items.length})</span>
|
||||
</div>
|
||||
${items.length === 0
|
||||
? '<div class="sub-empty">Keine Einträge</div>'
|
||||
: `<div class="item-list">${items.map(item => renderFn(item)).join('')}</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderAufgabe(a) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
|
||||
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
|
||||
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
|
||||
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(a.aufgabeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
|
||||
<span class="item-text">${esc(a.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStrafe(s) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
|
||||
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
|
||||
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
|
||||
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(s.strafeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
|
||||
<span class="item-text">${esc(s.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderZeitstrafe(z) {
|
||||
const badges = [];
|
||||
const zeit = formatMin(z.minutenVon, z.minutenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
||||
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
|
||||
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
|
||||
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(z.sperreId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
|
||||
<span class="item-text">${esc(z.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Sort ──
|
||||
function sortByLevelThenName(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const la = a.level ?? 999, lb = b.level ?? 999;
|
||||
if (la !== lb) return la - lb;
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
function sortByName(items) {
|
||||
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
|
||||
}
|
||||
|
||||
// ── Group toggle ──
|
||||
function toggleGroup(gruppenId) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
const body = document.getElementById('dbody-' + gruppenId);
|
||||
if (!card) return;
|
||||
if (card.classList.contains('open')) {
|
||||
card.classList.remove('open');
|
||||
body.style.display = 'none';
|
||||
if (openGroupId === gruppenId) openGroupId = null;
|
||||
} else {
|
||||
if (openGroupId) {
|
||||
const prev = document.getElementById('dgroup-' + openGroupId);
|
||||
const prevBody = document.getElementById('dbody-' + openGroupId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
if (prevBody) prevBody.style.display = 'none';
|
||||
}
|
||||
card.classList.add('open');
|
||||
body.style.display = 'block';
|
||||
openGroupId = gruppenId;
|
||||
openItemId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Item toggle ──
|
||||
function toggleItem(itemId) {
|
||||
if (openItemId === itemId) {
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.remove('open');
|
||||
openItemId = null;
|
||||
return;
|
||||
}
|
||||
if (openItemId) {
|
||||
const prev = document.getElementById('ditem-' + openItemId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
}
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.add('open');
|
||||
openItemId = itemId;
|
||||
}
|
||||
|
||||
// ── Subscribe / Unsubscribe ──
|
||||
function toggleSubscribe(gruppenId, btn) {
|
||||
btn.disabled = true;
|
||||
const isSubscribed = btn.classList.contains('subscribed');
|
||||
const method = isSubscribed ? 'DELETE' : 'POST';
|
||||
fetch(`/abo/${gruppenId}`, { method })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201 || r.status === 202) {
|
||||
if (isSubscribed) {
|
||||
btn.classList.remove('subscribed');
|
||||
btn.textContent = '♥ Abonnieren';
|
||||
updateBadge(gruppenId, false);
|
||||
} else {
|
||||
btn.classList.add('subscribed');
|
||||
btn.textContent = '♥ Abonniert';
|
||||
updateBadge(gruppenId, true);
|
||||
}
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function updateBadge(gruppenId, subscribed) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
if (!card) return;
|
||||
const badgesEl = card.querySelector('.gruppe-badges');
|
||||
if (!badgesEl) return;
|
||||
const subBadge = badgesEl.querySelector('.gruppe-badge-sub');
|
||||
if (subscribed && !subBadge) {
|
||||
badgesEl.insertAdjacentHTML('beforeend', `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`);
|
||||
} else if (!subscribed && subBadge) {
|
||||
subBadge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search ──
|
||||
document.getElementById('searchBtn').addEventListener('click', () => {
|
||||
currentName = document.getElementById('searchInput').value.trim();
|
||||
currentPage = 0;
|
||||
loadGroups();
|
||||
});
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('searchBtn').click();
|
||||
});
|
||||
|
||||
// ── Paging ──
|
||||
function updatePaging(current, total) {
|
||||
const el = document.getElementById('paging');
|
||||
if (total <= 1) { el.style.display = 'none'; return; }
|
||||
el.style.display = 'flex';
|
||||
document.getElementById('prevBtn').disabled = current === 0;
|
||||
document.getElementById('nextBtn').disabled = current >= total - 1;
|
||||
document.getElementById('pageInfo').textContent = `Seite ${current + 1} von ${total}`;
|
||||
}
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => {
|
||||
if (currentPage > 0) { currentPage--; loadGroups(); }
|
||||
});
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
if (currentPage < totalPages - 1) { currentPage++; loadGroups(); }
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -191,7 +191,7 @@
|
||||
<div class="acc-body" id="acc-aufgaben-body">
|
||||
<div id="guestAufgabenHint" class="guest-hint" style="display:none;">Aufgaben werden vom Host festgelegt – nur zur Ansicht.</div>
|
||||
<p style="font-size:0.85rem;color:var(--color-muted);margin-bottom:0.75rem;">
|
||||
Gruppen verwalten: <a href="/games/common/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
|
||||
Gruppen verwalten: <a href="/games/vanilla/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
|
||||
</p>
|
||||
<div id="sectionOwn">
|
||||
<div class="aufgaben-section-label"><label class="select-all-label"><input type="checkbox" class="select-all-cb" data-list="listOwn"> Eigene Gruppen</label></div>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
459
bin/main/static/js/mobile-nav.js
Normal file
459
bin/main/static/js/mobile-nav.js
Normal file
@@ -0,0 +1,459 @@
|
||||
(function () {
|
||||
if (window.__mobileNavLoaded) return;
|
||||
window.__mobileNavLoaded = true;
|
||||
|
||||
const path = window.location.pathname;
|
||||
const I = window.IC || function () { return ''; };
|
||||
const TOPBAR_H = '4.875rem';
|
||||
|
||||
// ── CSS ──────────────────────────────────────────────────────────────────
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.mobile-topbar {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: ${TOPBAR_H};
|
||||
background: var(--color-card);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
.mobile-topbar-logo {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.mobile-topbar-logo img {
|
||||
height: 3rem;
|
||||
width: auto;
|
||||
}
|
||||
.mobile-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
.mobile-tb-btn {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
font-size: 1.725rem;
|
||||
line-height: 1;
|
||||
padding: 0.75rem 0.675rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
transition: background 0.12s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-tb-btn:hover { background: var(--color-secondary); }
|
||||
.mobile-tb-badge {
|
||||
position: absolute;
|
||||
top: 3px; right: 3px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
font-size: 0.87rem;
|
||||
font-weight: 700;
|
||||
min-width: 1.65em;
|
||||
padding: 0.15em 0.375em;
|
||||
display: none;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-tb-avatar {
|
||||
width: 2.7rem;
|
||||
height: 2.7rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ── Mobile Menu Backdrop ── */
|
||||
.mob-menu-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: ${TOPBAR_H};
|
||||
left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
z-index: 998;
|
||||
}
|
||||
.mob-menu-backdrop.open { display: block; }
|
||||
|
||||
/* ── Mobile Menu Panel ── */
|
||||
.mob-menu-panel {
|
||||
position: fixed;
|
||||
top: ${TOPBAR_H};
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(80%, 360px);
|
||||
background: var(--color-card);
|
||||
border-left: 1px solid var(--color-secondary);
|
||||
z-index: 999;
|
||||
overflow-y: auto;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.mob-menu-panel.open { transform: translateX(0); }
|
||||
|
||||
/* ── Accordion ── */
|
||||
.mnav-section { border-bottom: 1px solid var(--color-secondary); }
|
||||
.mnav-section:last-child { border-bottom: none; }
|
||||
|
||||
.mnav-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.85rem 1.1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.mnav-section-header:hover { background: var(--color-secondary); }
|
||||
|
||||
.mnav-section-arrow {
|
||||
font-size: 0.65rem;
|
||||
transition: transform 0.2s;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.mnav-section.open .mnav-section-arrow { transform: rotate(90deg); }
|
||||
|
||||
.mnav-section-body { display: none; }
|
||||
.mnav-section.open .mnav-section-body { display: block; }
|
||||
|
||||
/* ── Menu Links ── */
|
||||
.mnav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.6rem 1.1rem 0.6rem 1.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.mnav-link:hover { background: var(--color-secondary); color: var(--color-primary); }
|
||||
.mnav-link.active { color: var(--color-primary); background: rgba(var(--color-primary-rgb,233,69,96),0.08); }
|
||||
.mnav-icon { width: 1.3rem; text-align: center; font-size: 1rem; flex-shrink: 0; }
|
||||
.mnav-badge {
|
||||
margin-left: auto;
|
||||
background: var(--color-primary); color: #fff;
|
||||
border-radius: 10px; font-size: 0.68rem; font-weight: 700;
|
||||
min-width: 1.2em; padding: 0.1em 0.3em; display: none;
|
||||
}
|
||||
.mnav-link--danger { color: var(--color-primary); }
|
||||
.mnav-link--danger:hover { background: rgba(var(--color-primary-rgb,233,69,96),0.1); color: var(--color-primary); }
|
||||
|
||||
/* ── Show only on mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-topbar { display: flex; }
|
||||
body.app { padding-top: ${TOPBAR_H}; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// ── Helper ───────────────────────────────────────────────────────────────
|
||||
function lnk(href, iconKey, label, id, badgeId) {
|
||||
const cls = path === href ? ' active' : '';
|
||||
const idAttr = id ? ` id="${id}"` : '';
|
||||
const badge = badgeId ? `<span class="mnav-badge" id="${badgeId}"></span>` : '';
|
||||
const icon = iconKey ? `<span class="mnav-icon">${I(iconKey) || ''}</span>` : `<span class="mnav-icon"></span>`;
|
||||
return `<a href="${href}" class="mnav-link${cls}"${idAttr}>${icon}<span>${label}</span>${badge}</a>`;
|
||||
}
|
||||
|
||||
// ── Menu-Sektionen ────────────────────────────────────────────────────────
|
||||
const SECTIONS = [
|
||||
{
|
||||
label: 'Allgemein',
|
||||
prefixes: ['/userhome.html', '/search.html', '/community/nachrichten.html',
|
||||
'/community/benachrichtigungen.html', '/games/common/einladungen.html'],
|
||||
html: `
|
||||
${lnk('/userhome.html', 'HOME', 'Home' )}
|
||||
${lnk('/search.html', 'SEARCH', 'Suche' )}
|
||||
<a href="/admin/admin.html" class="mnav-link${path === '/admin/admin.html' ? ' active' : ''}"
|
||||
id="mnavAdminLink" style="display:none">
|
||||
<span class="mnav-icon">${I('ADMIN') || '⚙'}</span><span>Administration</span></a>
|
||||
${lnk('/community/nachrichten.html', 'MESSAGES', 'Nachrichten', null, 'mnavBadgeMsg' )}
|
||||
${lnk('/community/benachrichtigungen.html', 'NOTIFICATIONS', 'Benachrichtigungen', null, 'mnavBadgeNotif' )}
|
||||
${lnk('/games/common/einladungen.html', 'INVITATIONS', 'Einladungen', null, 'mnavBadgeInv' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
prefixes: ['/community/'],
|
||||
html: `
|
||||
${lnk('/community/feed.html', 'FEED', 'Feed' )}
|
||||
${lnk('/community/freunde.html', 'FRIENDS', 'Freunde', null, 'mnavBadgeFriends' )}
|
||||
${lnk('/community/gruppen.html', 'GROUPS', 'Gruppen', null, 'mnavBadgeGruppen' )}
|
||||
${lnk('/community/locations.html', 'LOCATION', 'Locations' )}
|
||||
${lnk('/community/events.html', 'EVENT', 'Veranstaltungen' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Dating',
|
||||
prefixes: ['/dating/'],
|
||||
html: `
|
||||
<a href="/dating/dating.html" class="mnav-link${path === '/dating/dating.html' ? ' active' : ''}" id="mnavDatingLink">
|
||||
<span class="mnav-icon">${I('DATING') || '♥'}</span><span>Dating</span></a>
|
||||
${lnk('/dating/besucher.html', '', 'Besucher')}
|
||||
${lnk('/dating/likes.html', '', 'Likes' )}
|
||||
${lnk('/dating/matches.html', '', 'Matches' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Vanilla Game',
|
||||
prefixes: ['/games/vanilla/'],
|
||||
html: `
|
||||
${lnk('/games/vanilla/neuvanilla.html', 'PLAY_NEW', 'Neue Session', 'mnavVanillaNeu' )}
|
||||
<a href="#" class="mnav-link" id="mnavVanillaAktiv" style="display:none">
|
||||
<span class="mnav-icon">${I('WAITING') || ''}</span><span>Aktive Session</span></a>
|
||||
<a href="/games/vanilla/vanillaingame.html" class="mnav-link${path === '/games/vanilla/vanillaingame.html' ? ' active' : ''}"
|
||||
id="mnavVanillaImSpiel" style="display:none">
|
||||
<span class="mnav-icon">${I('PLAY_ACTIVE') || ''}</span><span>Im Spiel</span></a>
|
||||
${lnk('/games/vanilla/aufgaben.html', 'CHECK', 'Aufgaben' )}
|
||||
${lnk('/games/vanilla/toys.html', 'TOYS', 'Toys' )}
|
||||
${lnk('/games/vanilla/entdecken.html', 'DISCOVER', 'Entdecken' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'BDSM Game',
|
||||
prefixes: ['/games/bdsm/'],
|
||||
html: `
|
||||
${lnk('/games/bdsm/neubdsm.html', 'PLAY_NEW', 'Neue Session', 'mnavBdsmNeu' )}
|
||||
<a href="#" class="mnav-link" id="mnavBdsmAktiv" style="display:none">
|
||||
<span class="mnav-icon">${I('WAITING') || ''}</span><span>Aktive Session</span></a>
|
||||
<a href="/games/bdsm/bdsmingame.html" class="mnav-link${path === '/games/bdsm/bdsmingame.html' ? ' active' : ''}"
|
||||
id="mnavBdsmImSpiel" style="display:none">
|
||||
<span class="mnav-icon">${I('PLAY_ACTIVE') || ''}</span><span>Im Spiel</span></a>
|
||||
${lnk('/games/bdsm/aufgaben.html', 'CHECK', 'Aufgaben' )}
|
||||
${lnk('/games/bdsm/toys.html', 'TOYS', 'Toys' )}
|
||||
${lnk('/games/bdsm/entdecken.html', 'DISCOVER', 'Entdecken' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Chastity Game',
|
||||
prefixes: ['/games/chastity/'],
|
||||
html: `
|
||||
${lnk('/games/chastity/neulock.html', 'NEW_LOCK', 'Neues Lock', 'mnavChastityNeu' )}
|
||||
<a href="#" class="mnav-link" id="mnavChastityAktiv" style="display:none">
|
||||
<span class="mnav-icon">${I('ACTIVE_LOCK') || ''}</span><span>Aktives Lock</span></a>
|
||||
${lnk('/games/chastity/communityvotes.html', 'VOTES', 'Community Votes' )}
|
||||
${lnk('/games/chastity/meine-locks.html', 'LOCK', 'Meine Vorlagen' )}
|
||||
${lnk('/games/chastity/entdecken-vorlagen.html', 'DISCOVER', 'Entdecken' )}
|
||||
${lnk('/games/chastity/keyholder-finden.html', 'FRIENDS', 'Keyholder finden' )}
|
||||
${lnk('/games/chastity/keyholder.html', 'KEY', 'Keyholder' )}
|
||||
${lnk('/games/chastity/unlock-history.html', 'HISTORY', 'Code-Historie' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Konto',
|
||||
prefixes: ['/konto/', '/help/'],
|
||||
html: `
|
||||
${lnk('/konto/einstellungen.html', 'SETTINGS', 'Einstellungen')}
|
||||
${lnk('/help/overview.html', 'HELP', 'Hilfe' )}
|
||||
<a href="/login/logout" class="mnav-link mnav-link--danger">
|
||||
<span class="mnav-icon">${I('LOGOUT') || ''}</span><span>Abmelden</span></a>
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mobile Topbar ─────────────────────────────────────────────────────────
|
||||
const topbarEl = document.createElement('div');
|
||||
topbarEl.className = 'mobile-topbar';
|
||||
topbarEl.id = 'mobileTopbar';
|
||||
topbarEl.innerHTML = `
|
||||
<a href="/userhome.html" class="mobile-topbar-logo">
|
||||
<img src="/img/icon.png" alt="xXx Sphere">
|
||||
</a>
|
||||
<div class="mobile-topbar-actions">
|
||||
<a href="/community/nachrichten.html" class="mobile-tb-btn" title="Nachrichten">
|
||||
${I('MESSAGES')}
|
||||
<span class="mobile-tb-badge" id="mobTbMsgBadge"></span>
|
||||
</a>
|
||||
<a href="/community/benachrichtigungen.html" class="mobile-tb-btn" title="Benachrichtigungen">
|
||||
${I('NOTIFICATIONS')}
|
||||
<span class="mobile-tb-badge" id="mobTbNotifBadge"></span>
|
||||
</a>
|
||||
<a href="/games/common/einladungen.html" class="mobile-tb-btn" title="Einladungen">
|
||||
${I('INVITATIONS')}
|
||||
<span class="mobile-tb-badge" id="mobTbInvBadge"></span>
|
||||
</a>
|
||||
<a href="/community/benutzer.html" class="mobile-tb-btn" id="mobTbProfileBtn" title="Profil">
|
||||
${I('PROFILE')}
|
||||
</a>
|
||||
<button class="mobile-tb-btn" id="mobMenuToggle" aria-label="Menü">
|
||||
${I('MENU') || '☰'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentElement('afterbegin', topbarEl);
|
||||
|
||||
// ── Overlay ───────────────────────────────────────────────────────────────
|
||||
const backdropEl = document.createElement('div');
|
||||
backdropEl.className = 'mob-menu-backdrop';
|
||||
backdropEl.id = 'mobMenuBackdrop';
|
||||
|
||||
const panelEl = document.createElement('div');
|
||||
panelEl.className = 'mob-menu-panel';
|
||||
panelEl.id = 'mobMenuPanel';
|
||||
panelEl.innerHTML = SECTIONS.map(s => {
|
||||
const isOpen = s.prefixes.some(p => path.startsWith(p) || path === p);
|
||||
return `
|
||||
<div class="mnav-section${isOpen ? ' open' : ''}">
|
||||
<div class="mnav-section-header">
|
||||
<span>${s.label}</span>
|
||||
<span class="mnav-section-arrow">▶</span>
|
||||
</div>
|
||||
<div class="mnav-section-body">${s.html}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
document.body.appendChild(backdropEl);
|
||||
document.body.appendChild(panelEl);
|
||||
|
||||
// ── Accordion (nur eine Sektion gleichzeitig offen) ──────────────────────
|
||||
panelEl.querySelectorAll('.mnav-section-header').forEach(h => {
|
||||
h.addEventListener('click', () => {
|
||||
const section = h.closest('.mnav-section');
|
||||
const isOpen = section.classList.contains('open');
|
||||
panelEl.querySelectorAll('.mnav-section').forEach(s => s.classList.remove('open'));
|
||||
if (!isOpen) section.classList.add('open');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Open / Close ──────────────────────────────────────────────────────────
|
||||
function openMenu() {
|
||||
panelEl.classList.add('open');
|
||||
backdropEl.classList.add('open');
|
||||
}
|
||||
function closeMenu() {
|
||||
panelEl.classList.remove('open');
|
||||
backdropEl.classList.remove('open');
|
||||
}
|
||||
|
||||
document.getElementById('mobMenuToggle').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
panelEl.classList.contains('open') ? closeMenu() : openMenu();
|
||||
});
|
||||
backdropEl.addEventListener('click', closeMenu);
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMenu(); });
|
||||
panelEl.querySelectorAll('.mnav-link').forEach(l => {
|
||||
l.addEventListener('click', () => { if (l.getAttribute('href') !== '#') closeMenu(); });
|
||||
});
|
||||
|
||||
// ── Badges ────────────────────────────────────────────────────────────────
|
||||
function setBadge(id, n) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = n > 99 ? '99+' : n;
|
||||
el.style.display = n > 0 ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
fetch('/social/messages/unread/count').then(r => r.ok ? r.json() : 0).then(n => {
|
||||
setBadge('mobTbMsgBadge', n); setBadge('mnavBadgeMsg', n);
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/notifications/unread/count').then(r => r.ok ? r.json() : 0).then(n => {
|
||||
setBadge('mobTbNotifBadge', n); setBadge('mnavBadgeNotif', n);
|
||||
}).catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/lockee/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/keyholder/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/bdsm/einladung/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/vanilla/einladung/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
]).then(([l, k, b, v]) => {
|
||||
const n = l + k + b + v;
|
||||
setBadge('mobTbInvBadge', n); setBadge('mnavBadgeInv', n);
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/social/friends/pending/count').then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge('mnavBadgeFriends', n)).catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/gruppen/requests/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/gruppen/reports/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
]).then(([j, rep]) => setBadge('mnavBadgeGruppen', j + rep)).catch(() => {});
|
||||
|
||||
// ── User / Dynamische Links ───────────────────────────────────────────────
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||
if (!user) return;
|
||||
|
||||
// Profilbild
|
||||
const profileBtn = document.getElementById('mobTbProfileBtn');
|
||||
if (profileBtn) {
|
||||
profileBtn.href = '/community/benutzer.html?userId=' + user.userId;
|
||||
if (user.profilePicture) {
|
||||
profileBtn.innerHTML =
|
||||
`<img src="data:image/png;base64,${user.profilePicture}" class="mobile-tb-avatar" alt="">`;
|
||||
}
|
||||
}
|
||||
|
||||
// Admin
|
||||
if (user.admin) {
|
||||
const el = document.getElementById('mnavAdminLink');
|
||||
if (el) el.style.display = '';
|
||||
}
|
||||
|
||||
// Dating
|
||||
const datingLink = document.getElementById('mnavDatingLink');
|
||||
if (datingLink) {
|
||||
datingLink.href = user.datingAktiv
|
||||
? '/dating/dating.html'
|
||||
: '/konto/einstellungen.html#sec-dating';
|
||||
}
|
||||
|
||||
const hide = id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; };
|
||||
const show = id => { const el = document.getElementById(id); if (el) el.style.display = ''; };
|
||||
const setHref = (id, h) => { const el = document.getElementById(id); if (el) el.href = h; };
|
||||
|
||||
// BDSM
|
||||
try {
|
||||
const r = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('mnavBdsmNeu'); hide('mnavBdsmImSpiel'); show('mnavBdsmAktiv');
|
||||
setHref('mnavBdsmAktiv', aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html');
|
||||
} else {
|
||||
const sr = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('mnavBdsmNeu'); show('mnavBdsmImSpiel'); }
|
||||
else show('mnavBdsmNeu');
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Vanilla
|
||||
try {
|
||||
const r = await fetch('/vanilla/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('mnavVanillaNeu'); hide('mnavVanillaImSpiel'); show('mnavVanillaAktiv');
|
||||
setHref('mnavVanillaAktiv', aktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html');
|
||||
} else {
|
||||
const sr = await fetch(`/vanilla?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('mnavVanillaNeu'); show('mnavVanillaImSpiel'); }
|
||||
else show('mnavVanillaNeu');
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Chastity
|
||||
try {
|
||||
const r = await fetch('/keyholder/mylock');
|
||||
if (r.ok) {
|
||||
const lock = await r.json();
|
||||
show('mnavChastityAktiv');
|
||||
setHref('mnavChastityAktiv', '/games/chastity/activelock.html?lockId=' + lock.lockId);
|
||||
}
|
||||
} catch (_) {}
|
||||
}).catch(() => {});
|
||||
})();
|
||||
@@ -8,23 +8,48 @@
|
||||
/* ── Burger-Button ── */
|
||||
.nav-burger {
|
||||
display: inline-flex; align-items: center; gap: 0.45rem;
|
||||
padding: 0.35rem 0.8rem 0.35rem 0.6rem;
|
||||
padding: 0.46rem 0.8rem 0.46rem 0.6rem;
|
||||
background: none; border: 1px solid var(--color-secondary);
|
||||
border-radius: 8px; cursor: pointer;
|
||||
color: var(--color-text); font-size: 0.88rem; font-weight: 600;
|
||||
flex-shrink: 0; transition: border-color 0.15s, color 0.15s;
|
||||
margin-right: 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.nav-burger:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.nav-burger-icon { font-size: 1.05rem; line-height: 1; }
|
||||
.nav-burger-icon {
|
||||
font-size: 1.05rem; line-height: 1;
|
||||
position: relative;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 1.2em; height: 1.2em;
|
||||
}
|
||||
.nav-burger-icon-menu,
|
||||
.nav-burger-icon-close {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.nav-burger-icon-close { opacity: 0; transform: rotate(-45deg); }
|
||||
#topbar.nav-menu-open .nav-burger .nav-burger-icon-menu { opacity: 0; transform: rotate(45deg); }
|
||||
#topbar.nav-menu-open .nav-burger .nav-burger-icon-close { opacity: 1; transform: rotate(0deg); }
|
||||
|
||||
/* ── Backdrop ── */
|
||||
.nav-backdrop {
|
||||
display: none; position: fixed; inset: 0; z-index: 498;
|
||||
position: fixed; inset: 0; z-index: 498;
|
||||
background: rgba(0,0,0,0);
|
||||
pointer-events: none;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.nav-backdrop.open {
|
||||
background: rgba(0,0,0,0.55);
|
||||
pointer-events: auto;
|
||||
}
|
||||
.nav-backdrop.open { display: block; }
|
||||
|
||||
/* ── Topbar: untere Rundung entfernen wenn Menü offen ── */
|
||||
#topbar {
|
||||
transition: border-radius 0.22s ease, border-bottom-color 0.22s ease;
|
||||
}
|
||||
#topbar.nav-menu-open {
|
||||
border-radius: 12px 12px 0 0;
|
||||
border-bottom-color: transparent;
|
||||
@@ -38,11 +63,21 @@
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.55);
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 80px);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0.22s;
|
||||
}
|
||||
.nav-dropdown.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0s;
|
||||
}
|
||||
.nav-dropdown.open { display: block; }
|
||||
|
||||
/* ── 4-Spalten-Layout ── */
|
||||
.nav-columns {
|
||||
@@ -226,17 +261,17 @@
|
||||
{ href: '/games/vanilla/neuvanilla.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navVanillaNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navVanillaAktiv' },
|
||||
{ href: '/games/vanilla/vanillaingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navVanillaImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
{ href: '/games/vanilla/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/vanilla/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/vanilla/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
])}
|
||||
${gameGroup('BDSM', 'BDSM Game', [
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html?mode=bdsm', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/games/bdsm/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/bdsm/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/bdsm/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
])}
|
||||
${gameGroup('CHASTITY', 'Chastity Game', [
|
||||
{ href: '/games/chastity/neulock.html', icon: 'NEW_LOCK', label: 'Neues Lock', id: 'navChastityNeu' },
|
||||
@@ -330,8 +365,8 @@
|
||||
btn.className = 'nav-burger';
|
||||
btn.id = 'navBurgerBtn';
|
||||
btn.setAttribute('aria-label', 'Menü öffnen');
|
||||
btn.innerHTML = `<span class="nav-burger-icon">${I('MENU') || '☰'}</span><span class="nav-burger-text">Menü</span>`;
|
||||
topbarLeft.prepend(btn);
|
||||
btn.innerHTML = `<span class="nav-burger-icon"><span class="nav-burger-icon-menu">${I('MENU') || '≡'}</span><span class="nav-burger-icon-close">${I('CLOSE') || 'x'}</span></span><span class="nav-burger-text">Menü</span>`;
|
||||
topbarLeft.append(btn);
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const dd = document.getElementById('navDropdown');
|
||||
@@ -460,4 +495,6 @@
|
||||
}
|
||||
loadScript('/js/topbar.js');
|
||||
loadScript('/js/social-sidebar.js');
|
||||
loadScript('/js/section-nav.js');
|
||||
loadScript('/js/mobile-nav.js');
|
||||
})();
|
||||
|
||||
243
bin/main/static/js/section-nav.js
Normal file
243
bin/main/static/js/section-nav.js
Normal file
@@ -0,0 +1,243 @@
|
||||
(function () {
|
||||
const path = window.location.pathname;
|
||||
const search = window.location.search;
|
||||
const I = window.IC || function () { return ''; };
|
||||
|
||||
// ── Bereichs-Definitionen ────────────────────────────────────────────────
|
||||
const SECTIONS = {
|
||||
social: {
|
||||
prefixes: ['/community/'],
|
||||
exclude: [
|
||||
'/community/nachrichten.html',
|
||||
'/community/benachrichtigungen.html',
|
||||
'/community/einladungen.html',
|
||||
],
|
||||
items: [
|
||||
{ href: '/community/feed.html', icon: 'FEED', label: 'Feed' },
|
||||
{ href: '/community/freunde.html', icon: 'FRIENDS', label: 'Freunde' },
|
||||
{ href: '/community/gruppen.html', icon: 'GROUPS', label: 'Gruppen' },
|
||||
{ href: '/community/locations.html', icon: 'LOCATION', label: 'Locations' },
|
||||
{ href: '/community/events.html', icon: 'EVENT', label: 'Veranstaltungen' },
|
||||
],
|
||||
},
|
||||
dating: {
|
||||
prefixes: ['/dating/'],
|
||||
items: [
|
||||
{ href: '/dating/dating.html', icon: 'DATING', label: 'Dating', id: 'snavDatingLink' },
|
||||
{ href: '/dating/besucher.html', icon: '', label: 'Besucher' },
|
||||
{ href: '/dating/likes.html', icon: '', label: 'Likes' },
|
||||
{ href: '/dating/matches.html', icon: '', label: 'Matches' },
|
||||
],
|
||||
},
|
||||
vanilla: {
|
||||
prefixes: ['/games/vanilla/'],
|
||||
items: [
|
||||
{ href: '/games/vanilla/neuvanilla.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'snavVanillaNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'snavVanillaAktiv', hidden: true },
|
||||
{ href: '/games/vanilla/vanillaingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'snavVanillaImSpiel', hidden: true },
|
||||
{ href: '/games/vanilla/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/vanilla/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/vanilla/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
],
|
||||
},
|
||||
bdsm: {
|
||||
prefixes: ['/games/bdsm/'],
|
||||
items: [
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'snavBdsmNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'snavBdsmAktiv', hidden: true },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'snavBdsmImSpiel', hidden: true },
|
||||
{ href: '/games/bdsm/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/bdsm/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/bdsm/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
],
|
||||
},
|
||||
chastity: {
|
||||
prefixes: ['/games/chastity/'],
|
||||
items: [
|
||||
{ href: '/games/chastity/neulock.html', icon: 'NEW_LOCK', label: 'Neues Lock', id: 'snavChastityNeu' },
|
||||
{ href: '#', icon: 'ACTIVE_LOCK', label: 'Aktives Lock', id: 'snavChastityAktiv', hidden: true },
|
||||
{ href: '/games/chastity/communityvotes.html', icon: 'VOTES', label: 'Community Votes' },
|
||||
{ href: '/games/chastity/meine-locks.html', icon: 'LOCK', label: 'Meine Vorlagen' },
|
||||
{ href: '/games/chastity/entdecken-vorlagen.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
{ href: '/games/chastity/keyholder-finden.html', icon: 'FRIENDS', label: 'Keyholder finden' },
|
||||
{ href: '/games/chastity/keyholder.html', icon: 'KEY', label: 'Keyholder' },
|
||||
{ href: '/games/chastity/unlock-history.html', icon: 'HISTORY', label: 'Code-Historie' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ── Aktiven Bereich ermitteln ────────────────────────────────────────────
|
||||
const sectionKey = Object.keys(SECTIONS).find(k => {
|
||||
const s = SECTIONS[k];
|
||||
if (!s.prefixes.some(p => path.startsWith(p))) return false;
|
||||
if (s.exclude && s.exclude.includes(path)) return false;
|
||||
return true;
|
||||
});
|
||||
if (!sectionKey) return;
|
||||
const section = SECTIONS[sectionKey];
|
||||
|
||||
// ── CSS ──────────────────────────────────────────────────────────────────
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.section-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 0 0.6rem 0;
|
||||
}
|
||||
.section-nav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 7px;
|
||||
text-decoration: none;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.section-nav-link:hover {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.section-nav-link.active {
|
||||
color: var(--color-primary);
|
||||
background: rgba(var(--color-primary-rgb, 233,69,96), 0.09);
|
||||
font-weight: 600;
|
||||
}
|
||||
.section-nav-icon {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.section-nav--icons-only .section-nav-label { display: none; }
|
||||
.section-nav--icons-only .section-nav-link { gap: 0; padding: 0.3rem 0.55rem; }
|
||||
.section-nav-sep {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
margin: 0 0 1.25rem 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// ── Aktiv-Erkennung ──────────────────────────────────────────────────────
|
||||
function isActive(item) {
|
||||
if (item.href === '#') return false;
|
||||
const [itemPath, itemQuery] = item.href.split('?');
|
||||
if (itemQuery) return path === itemPath && search === '?' + itemQuery;
|
||||
return path === itemPath;
|
||||
}
|
||||
|
||||
// ── Nav bauen ────────────────────────────────────────────────────────────
|
||||
const navEl = document.createElement('nav');
|
||||
navEl.className = 'section-nav';
|
||||
|
||||
section.items.forEach(item => {
|
||||
const a = document.createElement('a');
|
||||
a.href = item.href;
|
||||
a.className = 'section-nav-link' + (isActive(item) ? ' active' : '');
|
||||
if (item.id) a.id = item.id;
|
||||
if (item.hidden) a.style.display = 'none';
|
||||
a.title = item.label;
|
||||
if (item.icon) a.innerHTML += `<span class="section-nav-icon">${I(item.icon) || ''}</span>`;
|
||||
a.innerHTML += `<span class="section-nav-label">${item.label}</span>`;
|
||||
navEl.appendChild(a);
|
||||
});
|
||||
|
||||
const sep = document.createElement('hr');
|
||||
sep.className = 'section-nav-sep';
|
||||
|
||||
// ── Einfügen in .main ────────────────────────────────────────────────────
|
||||
function checkOverflow() {
|
||||
// Immer zuerst Labels einblenden und ohne wrap messen —
|
||||
// so gibt es keine Feedback-Schleife durch Größenänderung nach dem Umschalten
|
||||
navEl.classList.remove('section-nav--icons-only');
|
||||
navEl.style.flexWrap = 'nowrap';
|
||||
const overflows = navEl.scrollWidth > navEl.clientWidth;
|
||||
navEl.style.flexWrap = '';
|
||||
if (overflows) navEl.classList.add('section-nav--icons-only');
|
||||
}
|
||||
|
||||
function inject() {
|
||||
const main = document.querySelector('.main');
|
||||
if (!main) { setTimeout(inject, 30); return; }
|
||||
main.insertBefore(sep, main.firstChild);
|
||||
main.insertBefore(navEl, sep);
|
||||
loadDynamic();
|
||||
// Overflow-Erkennung beim Laden und bei Größenänderung
|
||||
requestAnimationFrame(checkOverflow);
|
||||
new ResizeObserver(checkOverflow).observe(navEl);
|
||||
}
|
||||
inject();
|
||||
|
||||
// ── Dynamische Elemente (analog nav.js) ──────────────────────────────────
|
||||
function hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
||||
function show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
||||
function setHref(id, h) { const el = document.getElementById(id); if (el) el.href = h; }
|
||||
|
||||
async function loadDynamic() {
|
||||
try {
|
||||
const res = await fetch('/login/me');
|
||||
if (!res.ok) return;
|
||||
const user = await res.json();
|
||||
if (!user) return;
|
||||
|
||||
// Dating-Link
|
||||
const datingLink = document.getElementById('snavDatingLink');
|
||||
if (datingLink) {
|
||||
datingLink.href = user.datingAktiv
|
||||
? '/dating/dating.html'
|
||||
: '/konto/einstellungen.html#sec-dating';
|
||||
}
|
||||
|
||||
// BDSM
|
||||
if (sectionKey === 'bdsm') {
|
||||
try {
|
||||
const r = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('snavBdsmNeu'); hide('snavBdsmImSpiel');
|
||||
show('snavBdsmAktiv');
|
||||
setHref('snavBdsmAktiv', aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html');
|
||||
} else {
|
||||
const sr = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('snavBdsmNeu'); show('snavBdsmImSpiel'); }
|
||||
else show('snavBdsmNeu');
|
||||
}
|
||||
} catch (_) { show('snavBdsmNeu'); }
|
||||
}
|
||||
|
||||
// Vanilla
|
||||
if (sectionKey === 'vanilla') {
|
||||
try {
|
||||
const r = await fetch('/vanilla/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('snavVanillaNeu'); hide('snavVanillaImSpiel');
|
||||
show('snavVanillaAktiv');
|
||||
setHref('snavVanillaAktiv', aktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html');
|
||||
} else {
|
||||
const sr = await fetch(`/vanilla?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('snavVanillaNeu'); show('snavVanillaImSpiel'); }
|
||||
else show('snavVanillaNeu');
|
||||
}
|
||||
} catch (_) { show('snavVanillaNeu'); }
|
||||
}
|
||||
|
||||
// Chastity
|
||||
if (sectionKey === 'chastity') {
|
||||
try {
|
||||
const r = await fetch('/keyholder/mylock');
|
||||
if (r.ok) {
|
||||
const lock = await r.json();
|
||||
show('snavChastityAktiv');
|
||||
setHref('snavChastityAktiv', '/games/chastity/activelock.html?lockId=' + lock.lockId);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
})();
|
||||
@@ -10,9 +10,9 @@
|
||||
{ href: '/games/vanilla/neuvanilla.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navVanillaNeu' },
|
||||
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navVanillaAktiv' },
|
||||
{ href: '/games/vanilla/vanillaingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navVanillaImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
{ href: '/games/vanilla/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/vanilla/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/vanilla/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -22,9 +22,9 @@
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html?mode=bdsm', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
{ href: '/games/bdsm/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/bdsm/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/bdsm/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
.search-load-more:hover { border-color: var(--color-primary); color: var(--color-primary); background: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1 style="margin-bottom:1rem;">Suche</h1>
|
||||
|
||||
@@ -577,6 +577,8 @@ body.app {
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.55rem 1rem;
|
||||
position: relative;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
/* Linker Bereich – Banner, gleiche Breite wie Sidebar */
|
||||
@@ -596,6 +598,7 @@ body.app {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.topbar-banner {
|
||||
height: 3.5rem;
|
||||
@@ -638,20 +641,31 @@ body.app {
|
||||
|
||||
.topbar-search-overlay {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
z-index: 600;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
|
||||
}
|
||||
|
||||
.topbar-search-overlay.open { display: block; }
|
||||
.topbar-search-overlay.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.topbar-search-hint {
|
||||
padding: 0.75rem 1rem;
|
||||
@@ -794,17 +808,29 @@ body.app {
|
||||
position: fixed;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
|
||||
z-index: 550;
|
||||
width: 360px;
|
||||
max-height: 500px;
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0.22s;
|
||||
}
|
||||
|
||||
.topbar-panel.open { display: flex; }
|
||||
.topbar-panel.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.topbar-panel-header {
|
||||
display: flex;
|
||||
@@ -996,7 +1022,12 @@ body.app {
|
||||
.topbar-profile-link:hover { background: var(--color-secondary); }
|
||||
.topbar-profile-link--danger { color: var(--color-primary); }
|
||||
|
||||
/* ── Mobile: Topbar ausblenden ── */
|
||||
/* ── Mobile: Topbar ausblenden, Content-Rahmen entfernen ── */
|
||||
@media (max-width: 768px) {
|
||||
.topbar { display: none; }
|
||||
.main {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
1776
src/main/resources/static/games/bdsm/aufgaben.html
Normal file
1776
src/main/resources/static/games/bdsm/aufgaben.html
Normal file
File diff suppressed because it is too large
Load Diff
485
src/main/resources/static/games/bdsm/entdecken.html
Normal file
485
src/main/resources/static/games/bdsm/entdecken.html
Normal file
@@ -0,0 +1,485 @@
|
||||
<!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>Entdecken – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* ── Search ── */
|
||||
.search-bar {
|
||||
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
|
||||
}
|
||||
.search-bar input[type="text"] {
|
||||
flex: 1; padding: 0.55rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
background: var(--color-card); color: var(--color-text);
|
||||
font-size: 0.95rem; outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.search-bar input[type="text"]:focus { border-color: var(--color-primary); }
|
||||
.search-bar input[type="text"]::placeholder { color: var(--color-muted); }
|
||||
.btn-search {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.btn-search:hover { background: var(--color-primary); color: #fff; }
|
||||
|
||||
/* ── Paging ── */
|
||||
.paging {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; margin-top: 1rem;
|
||||
}
|
||||
.paging button {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.paging button:hover:not(:disabled) { background: var(--color-primary); }
|
||||
.paging button:disabled { opacity: 0.35; cursor: default; }
|
||||
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
|
||||
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Gruppe card ── */
|
||||
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.gruppe-card {
|
||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
|
||||
}
|
||||
.gruppe-card.open { border-color: rgba(233,69,96,0.35); }
|
||||
.gruppe-header {
|
||||
display: flex; align-items: center; gap: 0.9rem;
|
||||
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
|
||||
}
|
||||
.gruppe-img {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
object-fit: cover; flex-shrink: 0;
|
||||
}
|
||||
.gruppe-img-placeholder {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
|
||||
}
|
||||
.gruppe-meta { flex: 1; min-width: 0; }
|
||||
.gruppe-name {
|
||||
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
|
||||
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
|
||||
.gruppe-badge {
|
||||
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-muted);
|
||||
}
|
||||
.gruppe-badge-sub { background: rgba(46,204,113,0.15); color: var(--color-success); }
|
||||
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
|
||||
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||
|
||||
/* ── Subscribe button ── */
|
||||
.btn-sub {
|
||||
background: none; border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
color: var(--color-muted); font-size: 0.8rem; padding: 0.3rem 0.75rem;
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
flex-shrink: 0; white-space: nowrap;
|
||||
}
|
||||
.btn-sub:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.btn-sub.subscribed {
|
||||
border-color: rgba(46,204,113,0.5); color: var(--color-success);
|
||||
}
|
||||
.btn-sub.subscribed:hover {
|
||||
border-color: var(--color-primary); color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.08);
|
||||
}
|
||||
.btn-sub:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* ── Gruppe body ── */
|
||||
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
|
||||
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
|
||||
|
||||
.sub-section + .sub-section { margin-top: 0.85rem; }
|
||||
.sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
|
||||
.sub-section-title {
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
|
||||
text-transform: uppercase; color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── Items ── */
|
||||
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.item { border-radius: 6px; background: var(--color-secondary); overflow: hidden; }
|
||||
.item-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.35rem 0.6rem;
|
||||
cursor: pointer; user-select: none; transition: background 0.12s;
|
||||
}
|
||||
.item-row:hover { background: rgba(255,255,255,0.04); }
|
||||
.item.open .item-row { background: rgba(233,69,96,0.08); }
|
||||
.item-text {
|
||||
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
|
||||
.badge {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
|
||||
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
|
||||
}
|
||||
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
|
||||
|
||||
/* ── Item detail ── */
|
||||
.item-detail {
|
||||
display: none; padding: 0.5rem 0.6rem 0.6rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
|
||||
}
|
||||
.item.open .item-detail { display: block; }
|
||||
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
|
||||
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
|
||||
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
|
||||
.item-detail-chip {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-text);
|
||||
}
|
||||
.item-detail-chip-toy { background: rgba(233,69,96,0.12); color: var(--color-primary); }
|
||||
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
|
||||
<button class="btn-search" id="searchBtn">Suchen</button>
|
||||
</div>
|
||||
<div id="loading" class="loading">Wird geladen…</div>
|
||||
<div id="groupList" class="gruppe-list"></div>
|
||||
<div class="paging" id="paging" style="display:none;">
|
||||
<button id="prevBtn">‹ Zurück</button>
|
||||
<span class="page-info" id="pageInfo"></span>
|
||||
<button id="nextBtn">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 0, totalPages = 1;
|
||||
let currentName = '';
|
||||
|
||||
// ── XSS ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── Auth ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.then(user => { if (!user) return; loadGroups(); })
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load ──
|
||||
function loadGroups() {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('groupList').innerHTML = '';
|
||||
document.getElementById('paging').style.display = 'none';
|
||||
const nameParam = currentName ? `&name=${encodeURIComponent(currentName)}` : '';
|
||||
fetch(`/abo/discover?page=${currentPage}&size=${PAGE_SIZE}${nameParam}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
totalPages = data.totalPages || 1;
|
||||
renderGroups(data.content || []);
|
||||
updatePaging(currentPage, totalPages);
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('loading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
const WERKZEUG_LABEL = {
|
||||
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
|
||||
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
|
||||
};
|
||||
|
||||
function werkzeugChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
|
||||
}
|
||||
function toyChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
|
||||
}
|
||||
function formatSek(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} s`;
|
||||
if (von != null) return `ab ${von} s`;
|
||||
if (bis != null) return `bis ${bis} s`;
|
||||
return '';
|
||||
}
|
||||
function formatMin(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} min`;
|
||||
if (von != null) return `ab ${von} min`;
|
||||
if (bis != null) return `bis ${bis} min`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Track which group card is open
|
||||
let openGroupId = null;
|
||||
// Track which item detail is open
|
||||
let openItemId = null;
|
||||
|
||||
function renderGroups(groups) {
|
||||
const list = document.getElementById('groupList');
|
||||
if (!groups || groups.length === 0) {
|
||||
list.innerHTML = '<p class="empty">Keine Gruppen gefunden.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = groups.map(g => {
|
||||
const aufgabenCount = (g.aufgaben || []).length;
|
||||
const strafeCount = (g.strafen || []).length;
|
||||
const sperreCount = (g.sperren || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
const subLabel = g.subscribed
|
||||
? `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`
|
||||
: '';
|
||||
const subCount = g.subscriberCount > 0
|
||||
? `<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`
|
||||
: '';
|
||||
|
||||
const subBtnClass = g.subscribed ? 'btn-sub subscribed' : 'btn-sub';
|
||||
const subBtnText = g.subscribed ? '♥ Abonniert' : '♥ Abonnieren';
|
||||
|
||||
return `
|
||||
<div class="gruppe-card" id="dgroup-${esc(g.gruppenId)}">
|
||||
<div class="gruppe-header">
|
||||
<div style="cursor:pointer; display:flex; align-items:center; gap:0.9rem; flex:1; min-width:0;"
|
||||
onclick="toggleGroup('${esc(g.gruppenId)}')">
|
||||
${g.bild
|
||||
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
|
||||
: `<div class="gruppe-img-placeholder">⊙</div>`}
|
||||
<div class="gruppe-meta">
|
||||
<div class="gruppe-name">${esc(g.name)}</div>
|
||||
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
|
||||
${(subLabel || subCount) ? `<div class="gruppe-badges">${subCount}${subLabel}</div>` : ''}
|
||||
</div>
|
||||
<span class="gruppe-toggle">▶</span>
|
||||
</div>
|
||||
<button class="${subBtnClass}" id="subbtn-${esc(g.gruppenId)}"
|
||||
onclick="toggleSubscribe('${esc(g.gruppenId)}', this)">
|
||||
${subBtnText}
|
||||
</button>
|
||||
</div>
|
||||
<div class="gruppe-body" id="dbody-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), renderAufgabe)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), renderStrafe)}
|
||||
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), renderZeitstrafe)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
openItemId = null;
|
||||
}
|
||||
|
||||
function renderSubSection(title, items, renderFn) {
|
||||
return `<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<span class="sub-section-title">${esc(title)} (${items.length})</span>
|
||||
</div>
|
||||
${items.length === 0
|
||||
? '<div class="sub-empty">Keine Einträge</div>'
|
||||
: `<div class="item-list">${items.map(item => renderFn(item)).join('')}</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderAufgabe(a) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
|
||||
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
|
||||
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
|
||||
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(a.aufgabeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
|
||||
<span class="item-text">${esc(a.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStrafe(s) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
|
||||
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
|
||||
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
|
||||
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(s.strafeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
|
||||
<span class="item-text">${esc(s.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderZeitstrafe(z) {
|
||||
const badges = [];
|
||||
const zeit = formatMin(z.minutenVon, z.minutenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
||||
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
|
||||
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
|
||||
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(z.sperreId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
|
||||
<span class="item-text">${esc(z.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Sort ──
|
||||
function sortByLevelThenName(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const la = a.level ?? 999, lb = b.level ?? 999;
|
||||
if (la !== lb) return la - lb;
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
function sortByName(items) {
|
||||
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
|
||||
}
|
||||
|
||||
// ── Group toggle ──
|
||||
function toggleGroup(gruppenId) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
const body = document.getElementById('dbody-' + gruppenId);
|
||||
if (!card) return;
|
||||
if (card.classList.contains('open')) {
|
||||
card.classList.remove('open');
|
||||
body.style.display = 'none';
|
||||
if (openGroupId === gruppenId) openGroupId = null;
|
||||
} else {
|
||||
if (openGroupId) {
|
||||
const prev = document.getElementById('dgroup-' + openGroupId);
|
||||
const prevBody = document.getElementById('dbody-' + openGroupId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
if (prevBody) prevBody.style.display = 'none';
|
||||
}
|
||||
card.classList.add('open');
|
||||
body.style.display = 'block';
|
||||
openGroupId = gruppenId;
|
||||
openItemId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Item toggle ──
|
||||
function toggleItem(itemId) {
|
||||
if (openItemId === itemId) {
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.remove('open');
|
||||
openItemId = null;
|
||||
return;
|
||||
}
|
||||
if (openItemId) {
|
||||
const prev = document.getElementById('ditem-' + openItemId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
}
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.add('open');
|
||||
openItemId = itemId;
|
||||
}
|
||||
|
||||
// ── Subscribe / Unsubscribe ──
|
||||
function toggleSubscribe(gruppenId, btn) {
|
||||
btn.disabled = true;
|
||||
const isSubscribed = btn.classList.contains('subscribed');
|
||||
const method = isSubscribed ? 'DELETE' : 'POST';
|
||||
fetch(`/abo/${gruppenId}`, { method })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201 || r.status === 202) {
|
||||
if (isSubscribed) {
|
||||
btn.classList.remove('subscribed');
|
||||
btn.textContent = '♥ Abonnieren';
|
||||
updateBadge(gruppenId, false);
|
||||
} else {
|
||||
btn.classList.add('subscribed');
|
||||
btn.textContent = '♥ Abonniert';
|
||||
updateBadge(gruppenId, true);
|
||||
}
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function updateBadge(gruppenId, subscribed) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
if (!card) return;
|
||||
const badgesEl = card.querySelector('.gruppe-badges');
|
||||
if (!badgesEl) return;
|
||||
const subBadge = badgesEl.querySelector('.gruppe-badge-sub');
|
||||
if (subscribed && !subBadge) {
|
||||
badgesEl.insertAdjacentHTML('beforeend', `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`);
|
||||
} else if (!subscribed && subBadge) {
|
||||
subBadge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search ──
|
||||
document.getElementById('searchBtn').addEventListener('click', () => {
|
||||
currentName = document.getElementById('searchInput').value.trim();
|
||||
currentPage = 0;
|
||||
loadGroups();
|
||||
});
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('searchBtn').click();
|
||||
});
|
||||
|
||||
// ── Paging ──
|
||||
function updatePaging(current, total) {
|
||||
const el = document.getElementById('paging');
|
||||
if (total <= 1) { el.style.display = 'none'; return; }
|
||||
el.style.display = 'flex';
|
||||
document.getElementById('prevBtn').disabled = current === 0;
|
||||
document.getElementById('nextBtn').disabled = current >= total - 1;
|
||||
document.getElementById('pageInfo').textContent = `Seite ${current + 1} von ${total}`;
|
||||
}
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => {
|
||||
if (currentPage > 0) { currentPage--; loadGroups(); }
|
||||
});
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
if (currentPage < totalPages - 1) { currentPage++; loadGroups(); }
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
642
src/main/resources/static/games/bdsm/toys.html
Normal file
642
src/main/resources/static/games/bdsm/toys.html
Normal file
@@ -0,0 +1,642 @@
|
||||
<!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>Toys – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* ── Section ── */
|
||||
.section + .section { margin-top: 2.5rem; }
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
}
|
||||
.btn-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-add:hover { background: #c73652; }
|
||||
|
||||
/* ── Toy grid ── */
|
||||
.toy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Toy card ── */
|
||||
.toy-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 0.8rem 0.9rem;
|
||||
transition: border-color 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
.toy-card { cursor: pointer; }
|
||||
.toy-card:hover { border-color: var(--color-primary); }
|
||||
.toy-card.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.06);
|
||||
}
|
||||
|
||||
.toy-img {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toy-img-placeholder {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.toy-info { flex: 1; min-width: 0; }
|
||||
.toy-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.toy-desc {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.2rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Section action buttons ── */
|
||||
.section-actions { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.btn-action {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.btn-action:disabled { opacity: 0.35; cursor: default; }
|
||||
.btn-action:not(:disabled):hover { background: var(--color-primary); color: #fff; }
|
||||
.btn-action-danger:not(:disabled):hover { background: rgba(233,69,96,0.18); color: var(--color-primary); }
|
||||
.action-error {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-primary);
|
||||
min-height: 1.1em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* ── Empty / Loading ── */
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Inline-Fehler im Grid ── */
|
||||
.grid-error {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-primary);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 200;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-backdrop.open { display: flex; }
|
||||
.modal {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.6);
|
||||
}
|
||||
.modal h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.modal label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.modal input[type="text"],
|
||||
.modal textarea {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
resize: vertical;
|
||||
}
|
||||
.modal input[type="text"]:focus,
|
||||
.modal textarea:focus { border-color: var(--color-primary); }
|
||||
.modal input[type="file"] {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.modal-actions .btn-cancel {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-cancel:hover { background: #1a4a8a; }
|
||||
.modal-actions .btn-save {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-save:hover { background: #c73652; }
|
||||
.modal-actions .btn-save:disabled { opacity: 0.5; cursor: default; }
|
||||
.modal-error {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.75rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toy-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<!-- Erstell-/Bearbeitungs-Modal -->
|
||||
<div class="modal-backdrop" id="createModal">
|
||||
<div class="modal">
|
||||
<h2 id="modalTitle">Neues Toy</h2>
|
||||
<label for="toyName">Name *</label>
|
||||
<input type="text" id="toyName" placeholder="z.B. Vibrator" maxlength="100">
|
||||
<label for="toyDesc">Beschreibung</label>
|
||||
<textarea id="toyDesc" rows="3" placeholder="Kurze Beschreibung…" maxlength="500"></textarea>
|
||||
<label>Bild (optional)</label>
|
||||
<div id="currentImageWrap" style="display:none; align-items:center; gap:0.5rem; margin-bottom:0.4rem;">
|
||||
<img id="currentImage" style="max-width:64px; max-height:64px; border-radius:6px;" src="" alt="">
|
||||
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues Bild wählen zum Ersetzen</span>
|
||||
</div>
|
||||
<input type="file" id="toyBild" accept="image/*">
|
||||
<div class="modal-error" id="modalError"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
||||
<button class="btn-save" id="saveBtn">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<!-- Meine Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Meine Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="editBtn" disabled>✎ Bearbeiten</button>
|
||||
<button class="btn-action btn-action-danger" id="deleteBtn" disabled>✕ Löschen</button>
|
||||
<button class="btn-add" id="openCreateBtn">+ Neu</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="actionError"></div>
|
||||
<div class="toy-grid" id="userGrid"></div>
|
||||
<div id="userLoading" class="loading" style="display:none;"></div>
|
||||
<div id="userSentinel"></div>
|
||||
</div>
|
||||
|
||||
<!-- System-Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">System-Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="copyBtn" disabled>⊕ In meine Toys kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="systemActionError"></div>
|
||||
<div class="toy-grid" id="systemGrid"></div>
|
||||
<div id="systemLoading" class="loading" style="display:none;"></div>
|
||||
<div id="systemSentinel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 12;
|
||||
let userPage = 0, userTotalPages = 1, userLoading = false;
|
||||
let systemPage = 0, systemTotalPages = 1, systemLoading = false;
|
||||
|
||||
// ── Infinite-scroll observers ──
|
||||
const userObserver = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) loadUserPage();
|
||||
}, { rootMargin: '200px' });
|
||||
|
||||
const systemObserver = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) loadSystemPage();
|
||||
}, { rootMargin: '200px' });
|
||||
|
||||
// ── Auth + initial load ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.then(user => {
|
||||
if (!user) return;
|
||||
userObserver.observe(document.getElementById('userSentinel'));
|
||||
systemObserver.observe(document.getElementById('systemSentinel'));
|
||||
})
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load user toys (append, füllt Viewport automatisch auf) ──
|
||||
async function loadUserPage() {
|
||||
if (userLoading || userPage >= userTotalPages) return;
|
||||
userLoading = true;
|
||||
const loadEl = document.getElementById('userLoading');
|
||||
try {
|
||||
do {
|
||||
loadEl.textContent = 'Wird geladen…';
|
||||
loadEl.style.display = 'block';
|
||||
const r = await fetch(`/toy/list/user?page=${userPage}&size=${PAGE_SIZE}`);
|
||||
const data = await r.json();
|
||||
userTotalPages = data.totalPages || 1;
|
||||
appendGrid('userGrid', data.content, 'selectToy');
|
||||
userPage++;
|
||||
loadEl.style.display = 'none';
|
||||
} while (userPage < userTotalPages && sentinelVisible('userSentinel'));
|
||||
} catch (_) {
|
||||
loadEl.textContent = 'Fehler beim Laden.';
|
||||
} finally {
|
||||
userLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadUserToys() {
|
||||
userPage = 0;
|
||||
userTotalPages = 1;
|
||||
resetSelection();
|
||||
document.getElementById('userGrid').innerHTML = '';
|
||||
loadUserPage();
|
||||
}
|
||||
|
||||
// ── Load system toys (append, füllt Viewport automatisch auf) ──
|
||||
async function loadSystemPage() {
|
||||
if (systemLoading || systemPage >= systemTotalPages) return;
|
||||
systemLoading = true;
|
||||
const loadEl = document.getElementById('systemLoading');
|
||||
try {
|
||||
do {
|
||||
loadEl.textContent = 'Wird geladen…';
|
||||
loadEl.style.display = 'block';
|
||||
const r = await fetch(`/toy/list/system?page=${systemPage}&size=${PAGE_SIZE}`);
|
||||
const data = await r.json();
|
||||
systemTotalPages = data.totalPages || 1;
|
||||
appendGrid('systemGrid', data.content, 'selectSystemToy');
|
||||
systemPage++;
|
||||
loadEl.style.display = 'none';
|
||||
} while (systemPage < systemTotalPages && sentinelVisible('systemSentinel'));
|
||||
} catch (_) {
|
||||
loadEl.textContent = 'Fehler beim Laden.';
|
||||
} finally {
|
||||
systemLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadSystemToys() {
|
||||
systemPage = 0;
|
||||
systemTotalPages = 1;
|
||||
resetSystemSelection();
|
||||
document.getElementById('systemGrid').innerHTML = '';
|
||||
loadSystemPage();
|
||||
}
|
||||
|
||||
// ── Prüft ob ein Sentinel noch im (erweiterten) Viewport liegt ──
|
||||
function sentinelVisible(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.getBoundingClientRect().top <= window.innerHeight + 200 : false;
|
||||
}
|
||||
|
||||
// ── Append items to a grid ──
|
||||
function appendGrid(gridId, toys, selectFn) {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!toys || toys.length === 0) {
|
||||
if (!grid.querySelector('.toy-card')) {
|
||||
grid.innerHTML = '<p class="empty">Keine Einträge vorhanden.</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
const emptyEl = grid.querySelector('.empty');
|
||||
if (emptyEl) emptyEl.remove();
|
||||
grid.insertAdjacentHTML('beforeend', toys.map(toy => `
|
||||
<div class="toy-card" data-id="${esc(toy.toyId)}"
|
||||
${selectFn ? `onclick="${selectFn}('${esc(toy.toyId)}')"` : ''}>
|
||||
${toy.bild
|
||||
? `<img class="toy-img" src="data:image/png;base64,${toy.bild}" alt="${esc(toy.name)}">`
|
||||
: `<div class="toy-img-placeholder">◈</div>`}
|
||||
<div class="toy-info">
|
||||
<div class="toy-name">${esc(toy.name)}</div>
|
||||
${toy.beschreibung ? `<div class="toy-desc">${esc(toy.beschreibung)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join(''));
|
||||
}
|
||||
|
||||
// ── Selection ──
|
||||
let selectedUserToyId = null;
|
||||
|
||||
function selectToy(toyId) {
|
||||
const prev = document.querySelector('#userGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedUserToyId === toyId) {
|
||||
selectedUserToyId = null;
|
||||
} else {
|
||||
selectedUserToyId = toyId;
|
||||
document.querySelector(`#userGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
const has = selectedUserToyId != null;
|
||||
document.getElementById('editBtn').disabled = !has;
|
||||
document.getElementById('deleteBtn').disabled = !has;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
selectedUserToyId = null;
|
||||
document.getElementById('editBtn').disabled = true;
|
||||
document.getElementById('deleteBtn').disabled = true;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── System-Toy selection ──
|
||||
let selectedSystemToyId = null;
|
||||
|
||||
function selectSystemToy(toyId) {
|
||||
const prev = document.querySelector('#systemGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedSystemToyId === toyId) {
|
||||
selectedSystemToyId = null;
|
||||
} else {
|
||||
selectedSystemToyId = toyId;
|
||||
document.querySelector(`#systemGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
document.getElementById('copyBtn').disabled = selectedSystemToyId == null;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSystemSelection() {
|
||||
selectedSystemToyId = null;
|
||||
document.getElementById('copyBtn').disabled = true;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── Copy system toy ──
|
||||
document.getElementById('copyBtn').addEventListener('click', () => {
|
||||
if (!selectedSystemToyId) return;
|
||||
const btn = document.getElementById('copyBtn');
|
||||
btn.disabled = true;
|
||||
fetch(`/toy/copy/${selectedSystemToyId}`, { method: 'POST' })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
reloadUserToys();
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Du hast bereits ein Toy mit diesem Namen.';
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Fehler beim Kopieren (HTTP ' + r.status + ').';
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('systemActionError').textContent = 'Verbindungsfehler.';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Header action buttons ──
|
||||
document.getElementById('editBtn').addEventListener('click', () => {
|
||||
if (selectedUserToyId) openModal(selectedUserToyId);
|
||||
});
|
||||
|
||||
document.getElementById('deleteBtn').addEventListener('click', () => {
|
||||
if (!selectedUserToyId) return;
|
||||
if (!confirm('Toy wirklich löschen?')) return;
|
||||
const btn = document.getElementById('deleteBtn');
|
||||
btn.disabled = true;
|
||||
const toyId = selectedUserToyId;
|
||||
fetch(`/toy/${toyId}`, { method: 'DELETE' })
|
||||
.then(r => {
|
||||
if (r.status === 409) {
|
||||
showActionError('Wird in Aufgaben verwendet – nicht löschbar.');
|
||||
btn.disabled = false;
|
||||
} else if (r.status === 403) {
|
||||
showActionError('Keine Berechtigung.');
|
||||
btn.disabled = false;
|
||||
} else if (r.ok || r.status === 202) {
|
||||
reloadUserToys();
|
||||
} else {
|
||||
showActionError('Fehler beim Löschen.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { showActionError('Verbindungsfehler.'); btn.disabled = false; });
|
||||
});
|
||||
|
||||
function showActionError(msg) {
|
||||
const el = document.getElementById('actionError');
|
||||
el.textContent = msg;
|
||||
setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 4000);
|
||||
}
|
||||
|
||||
// ── Create / Edit modal ──
|
||||
const modal = document.getElementById('createModal');
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
let currentEditId = null;
|
||||
|
||||
function openModal(editId) {
|
||||
currentEditId = editId || null;
|
||||
document.getElementById('modalError').style.display = 'none';
|
||||
document.getElementById('toyBild').value = '';
|
||||
if (currentEditId) {
|
||||
fetch(`/toy/${currentEditId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(toy => {
|
||||
if (!toy) return;
|
||||
document.getElementById('modalTitle').textContent = 'Toy bearbeiten';
|
||||
document.getElementById('toyName').value = toy.name || '';
|
||||
document.getElementById('toyDesc').value = toy.beschreibung || '';
|
||||
const imgWrap = document.getElementById('currentImageWrap');
|
||||
if (toy.bild) {
|
||||
document.getElementById('currentImage').src = 'data:image/png;base64,' + toy.bild;
|
||||
imgWrap.style.display = 'flex';
|
||||
} else {
|
||||
imgWrap.style.display = 'none';
|
||||
}
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
})
|
||||
.catch(() => alert('Fehler beim Laden des Toys.'));
|
||||
} else {
|
||||
document.getElementById('modalTitle').textContent = 'Neues Toy';
|
||||
document.getElementById('toyName').value = '';
|
||||
document.getElementById('toyDesc').value = '';
|
||||
document.getElementById('currentImageWrap').style.display = 'none';
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
|
||||
|
||||
function closeModal() { modal.classList.remove('open'); }
|
||||
|
||||
function editToy(toyId) { openModal(toyId); }
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const name = document.getElementById('toyName').value.trim();
|
||||
if (!name) {
|
||||
showModalError('Bitte einen Namen eingeben.');
|
||||
return;
|
||||
}
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichert…';
|
||||
|
||||
let bildBase64 = null;
|
||||
const fileInput = document.getElementById('toyBild');
|
||||
if (fileInput.files.length > 0) {
|
||||
bildBase64 = await toBase64(fileInput.files[0]);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
beschreibung: document.getElementById('toyDesc').value.trim() || null,
|
||||
bild: bildBase64
|
||||
};
|
||||
|
||||
const isEdit = currentEditId != null;
|
||||
fetch(isEdit ? `/toy/${currentEditId}` : '/toy', {
|
||||
method: isEdit ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
closeModal();
|
||||
reloadUserToys();
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
showModalError('Ein Toy mit diesem Namen existiert bereits.');
|
||||
} else {
|
||||
showModalError('Fehler beim Speichern (HTTP ' + r.status + ').');
|
||||
}
|
||||
})
|
||||
.catch(() => showModalError('Verbindungsfehler.'))
|
||||
.finally(() => { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; });
|
||||
});
|
||||
|
||||
function showModalError(msg) {
|
||||
const el = document.getElementById('modalError');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function toBase64(file) {
|
||||
const MAX = 128;
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(file);
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > MAX || h > MAX) {
|
||||
if (w >= h) { h = Math.max(1, Math.round(MAX * h / w)); w = MAX; }
|
||||
else { w = Math.max(1, Math.round(MAX * w / h)); h = MAX; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
resolve(canvas.toDataURL('image/png').split(',')[1]);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
// ── XSS-Schutz ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,982 +0,0 @@
|
||||
<!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/nav.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
|
||||
? ' <span style="font-size:0.72rem;">👁 Details sichtbar</span>'
|
||||
: ' <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>
|
||||
@@ -4,7 +4,7 @@
|
||||
<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 id="pageTitle">Aufgaben – xXx Sphere</title>
|
||||
<title>Aufgaben – Vanilla – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
@@ -609,21 +609,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Mode detection ──
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const IS_BDSM_MODE = urlParams.get('mode') === 'bdsm';
|
||||
|
||||
// ── API-URL-Hilfsfunktion ──
|
||||
function apiUrl(path) {
|
||||
if (IS_BDSM_MODE) return path;
|
||||
return '/vanilla' + path;
|
||||
}
|
||||
|
||||
// ── Seitenüberschrift ──
|
||||
document.getElementById('pageTitle').textContent = IS_BDSM_MODE
|
||||
? 'Aufgaben – BDSM – xXx Sphere'
|
||||
: 'Aufgaben – Vanilla – xXx Sphere';
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
let userPage = 0, userTotalPages = 1;
|
||||
let aboPage = 0, aboTotalPages = 1;
|
||||
@@ -715,8 +705,6 @@
|
||||
const finisherCount = (g.finisher || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
IS_BDSM_MODE && strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
IS_BDSM_MODE && sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : '',
|
||||
finisherCount ? `${finisherCount} Finisher` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
@@ -742,8 +730,6 @@
|
||||
<div class="gruppe-body" id="body-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${IS_BDSM_MODE ? renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type) : ''}
|
||||
${IS_BDSM_MODE ? renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type) : ''}
|
||||
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -755,7 +741,7 @@
|
||||
|
||||
// ── Sub-sections ──
|
||||
function renderSubSection(title, items, kind, renderFn, gruppenId, type) {
|
||||
const showAdd = type === 'user' && (IS_BDSM_MODE || (kind !== 'strafe' && kind !== 'zeitstrafe'));
|
||||
const showAdd = type === 'user' && (kind !== 'strafe' && kind !== 'zeitstrafe');
|
||||
const addBtn = showAdd
|
||||
? `<button class="btn-sub-add" onclick="openItemModal('${esc(gruppenId)}','${kind}')">+ ${title.replace('en','').replace('fen','fe')}</button>`
|
||||
: '';
|
||||
@@ -917,8 +903,8 @@
|
||||
// ── Item löschen ──
|
||||
const ITEM_DELETE_URL = {
|
||||
aufgabe: apiUrl('/aufgabe'),
|
||||
strafe: IS_BDSM_MODE ? '/strafe' : null,
|
||||
zeitstrafe: IS_BDSM_MODE ? '/sperre' : null,
|
||||
strafe: null,
|
||||
zeitstrafe: null,
|
||||
finisher: apiUrl('/finisher')
|
||||
};
|
||||
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
|
||||
485
src/main/resources/static/games/vanilla/entdecken.html
Normal file
485
src/main/resources/static/games/vanilla/entdecken.html
Normal file
@@ -0,0 +1,485 @@
|
||||
<!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>Entdecken – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* ── Search ── */
|
||||
.search-bar {
|
||||
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
|
||||
}
|
||||
.search-bar input[type="text"] {
|
||||
flex: 1; padding: 0.55rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
background: var(--color-card); color: var(--color-text);
|
||||
font-size: 0.95rem; outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.search-bar input[type="text"]:focus { border-color: var(--color-primary); }
|
||||
.search-bar input[type="text"]::placeholder { color: var(--color-muted); }
|
||||
.btn-search {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.btn-search:hover { background: var(--color-primary); color: #fff; }
|
||||
|
||||
/* ── Paging ── */
|
||||
.paging {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; margin-top: 1rem;
|
||||
}
|
||||
.paging button {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.paging button:hover:not(:disabled) { background: var(--color-primary); }
|
||||
.paging button:disabled { opacity: 0.35; cursor: default; }
|
||||
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
|
||||
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Gruppe card ── */
|
||||
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.gruppe-card {
|
||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
|
||||
}
|
||||
.gruppe-card.open { border-color: rgba(233,69,96,0.35); }
|
||||
.gruppe-header {
|
||||
display: flex; align-items: center; gap: 0.9rem;
|
||||
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
|
||||
}
|
||||
.gruppe-img {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
object-fit: cover; flex-shrink: 0;
|
||||
}
|
||||
.gruppe-img-placeholder {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
|
||||
}
|
||||
.gruppe-meta { flex: 1; min-width: 0; }
|
||||
.gruppe-name {
|
||||
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
|
||||
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
|
||||
.gruppe-badge {
|
||||
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-muted);
|
||||
}
|
||||
.gruppe-badge-sub { background: rgba(46,204,113,0.15); color: var(--color-success); }
|
||||
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
|
||||
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||
|
||||
/* ── Subscribe button ── */
|
||||
.btn-sub {
|
||||
background: none; border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
color: var(--color-muted); font-size: 0.8rem; padding: 0.3rem 0.75rem;
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
flex-shrink: 0; white-space: nowrap;
|
||||
}
|
||||
.btn-sub:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.btn-sub.subscribed {
|
||||
border-color: rgba(46,204,113,0.5); color: var(--color-success);
|
||||
}
|
||||
.btn-sub.subscribed:hover {
|
||||
border-color: var(--color-primary); color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.08);
|
||||
}
|
||||
.btn-sub:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* ── Gruppe body ── */
|
||||
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
|
||||
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
|
||||
|
||||
.sub-section + .sub-section { margin-top: 0.85rem; }
|
||||
.sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
|
||||
.sub-section-title {
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
|
||||
text-transform: uppercase; color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── Items ── */
|
||||
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.item { border-radius: 6px; background: var(--color-secondary); overflow: hidden; }
|
||||
.item-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.35rem 0.6rem;
|
||||
cursor: pointer; user-select: none; transition: background 0.12s;
|
||||
}
|
||||
.item-row:hover { background: rgba(255,255,255,0.04); }
|
||||
.item.open .item-row { background: rgba(233,69,96,0.08); }
|
||||
.item-text {
|
||||
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
|
||||
.badge {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
|
||||
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
|
||||
}
|
||||
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
|
||||
|
||||
/* ── Item detail ── */
|
||||
.item-detail {
|
||||
display: none; padding: 0.5rem 0.6rem 0.6rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
|
||||
}
|
||||
.item.open .item-detail { display: block; }
|
||||
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
|
||||
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
|
||||
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
|
||||
.item-detail-chip {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-text);
|
||||
}
|
||||
.item-detail-chip-toy { background: rgba(233,69,96,0.12); color: var(--color-primary); }
|
||||
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
|
||||
<button class="btn-search" id="searchBtn">Suchen</button>
|
||||
</div>
|
||||
<div id="loading" class="loading">Wird geladen…</div>
|
||||
<div id="groupList" class="gruppe-list"></div>
|
||||
<div class="paging" id="paging" style="display:none;">
|
||||
<button id="prevBtn">‹ Zurück</button>
|
||||
<span class="page-info" id="pageInfo"></span>
|
||||
<button id="nextBtn">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 0, totalPages = 1;
|
||||
let currentName = '';
|
||||
|
||||
// ── XSS ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── Auth ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.then(user => { if (!user) return; loadGroups(); })
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load ──
|
||||
function loadGroups() {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('groupList').innerHTML = '';
|
||||
document.getElementById('paging').style.display = 'none';
|
||||
const nameParam = currentName ? `&name=${encodeURIComponent(currentName)}` : '';
|
||||
fetch(`/abo/discover?page=${currentPage}&size=${PAGE_SIZE}${nameParam}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
totalPages = data.totalPages || 1;
|
||||
renderGroups(data.content || []);
|
||||
updatePaging(currentPage, totalPages);
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('loading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
const WERKZEUG_LABEL = {
|
||||
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
|
||||
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
|
||||
};
|
||||
|
||||
function werkzeugChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
|
||||
}
|
||||
function toyChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
|
||||
}
|
||||
function formatSek(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} s`;
|
||||
if (von != null) return `ab ${von} s`;
|
||||
if (bis != null) return `bis ${bis} s`;
|
||||
return '';
|
||||
}
|
||||
function formatMin(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} min`;
|
||||
if (von != null) return `ab ${von} min`;
|
||||
if (bis != null) return `bis ${bis} min`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Track which group card is open
|
||||
let openGroupId = null;
|
||||
// Track which item detail is open
|
||||
let openItemId = null;
|
||||
|
||||
function renderGroups(groups) {
|
||||
const list = document.getElementById('groupList');
|
||||
if (!groups || groups.length === 0) {
|
||||
list.innerHTML = '<p class="empty">Keine Gruppen gefunden.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = groups.map(g => {
|
||||
const aufgabenCount = (g.aufgaben || []).length;
|
||||
const strafeCount = (g.strafen || []).length;
|
||||
const sperreCount = (g.sperren || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
const subLabel = g.subscribed
|
||||
? `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`
|
||||
: '';
|
||||
const subCount = g.subscriberCount > 0
|
||||
? `<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`
|
||||
: '';
|
||||
|
||||
const subBtnClass = g.subscribed ? 'btn-sub subscribed' : 'btn-sub';
|
||||
const subBtnText = g.subscribed ? '♥ Abonniert' : '♥ Abonnieren';
|
||||
|
||||
return `
|
||||
<div class="gruppe-card" id="dgroup-${esc(g.gruppenId)}">
|
||||
<div class="gruppe-header">
|
||||
<div style="cursor:pointer; display:flex; align-items:center; gap:0.9rem; flex:1; min-width:0;"
|
||||
onclick="toggleGroup('${esc(g.gruppenId)}')">
|
||||
${g.bild
|
||||
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
|
||||
: `<div class="gruppe-img-placeholder">⊙</div>`}
|
||||
<div class="gruppe-meta">
|
||||
<div class="gruppe-name">${esc(g.name)}</div>
|
||||
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
|
||||
${(subLabel || subCount) ? `<div class="gruppe-badges">${subCount}${subLabel}</div>` : ''}
|
||||
</div>
|
||||
<span class="gruppe-toggle">▶</span>
|
||||
</div>
|
||||
<button class="${subBtnClass}" id="subbtn-${esc(g.gruppenId)}"
|
||||
onclick="toggleSubscribe('${esc(g.gruppenId)}', this)">
|
||||
${subBtnText}
|
||||
</button>
|
||||
</div>
|
||||
<div class="gruppe-body" id="dbody-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), renderAufgabe)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), renderStrafe)}
|
||||
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), renderZeitstrafe)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
openItemId = null;
|
||||
}
|
||||
|
||||
function renderSubSection(title, items, renderFn) {
|
||||
return `<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<span class="sub-section-title">${esc(title)} (${items.length})</span>
|
||||
</div>
|
||||
${items.length === 0
|
||||
? '<div class="sub-empty">Keine Einträge</div>'
|
||||
: `<div class="item-list">${items.map(item => renderFn(item)).join('')}</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderAufgabe(a) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
|
||||
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
|
||||
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
|
||||
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(a.aufgabeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
|
||||
<span class="item-text">${esc(a.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStrafe(s) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
|
||||
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
|
||||
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
|
||||
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(s.strafeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
|
||||
<span class="item-text">${esc(s.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderZeitstrafe(z) {
|
||||
const badges = [];
|
||||
const zeit = formatMin(z.minutenVon, z.minutenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
||||
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
|
||||
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
|
||||
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(z.sperreId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
|
||||
<span class="item-text">${esc(z.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Sort ──
|
||||
function sortByLevelThenName(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const la = a.level ?? 999, lb = b.level ?? 999;
|
||||
if (la !== lb) return la - lb;
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
function sortByName(items) {
|
||||
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
|
||||
}
|
||||
|
||||
// ── Group toggle ──
|
||||
function toggleGroup(gruppenId) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
const body = document.getElementById('dbody-' + gruppenId);
|
||||
if (!card) return;
|
||||
if (card.classList.contains('open')) {
|
||||
card.classList.remove('open');
|
||||
body.style.display = 'none';
|
||||
if (openGroupId === gruppenId) openGroupId = null;
|
||||
} else {
|
||||
if (openGroupId) {
|
||||
const prev = document.getElementById('dgroup-' + openGroupId);
|
||||
const prevBody = document.getElementById('dbody-' + openGroupId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
if (prevBody) prevBody.style.display = 'none';
|
||||
}
|
||||
card.classList.add('open');
|
||||
body.style.display = 'block';
|
||||
openGroupId = gruppenId;
|
||||
openItemId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Item toggle ──
|
||||
function toggleItem(itemId) {
|
||||
if (openItemId === itemId) {
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.remove('open');
|
||||
openItemId = null;
|
||||
return;
|
||||
}
|
||||
if (openItemId) {
|
||||
const prev = document.getElementById('ditem-' + openItemId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
}
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.add('open');
|
||||
openItemId = itemId;
|
||||
}
|
||||
|
||||
// ── Subscribe / Unsubscribe ──
|
||||
function toggleSubscribe(gruppenId, btn) {
|
||||
btn.disabled = true;
|
||||
const isSubscribed = btn.classList.contains('subscribed');
|
||||
const method = isSubscribed ? 'DELETE' : 'POST';
|
||||
fetch(`/abo/${gruppenId}`, { method })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201 || r.status === 202) {
|
||||
if (isSubscribed) {
|
||||
btn.classList.remove('subscribed');
|
||||
btn.textContent = '♥ Abonnieren';
|
||||
updateBadge(gruppenId, false);
|
||||
} else {
|
||||
btn.classList.add('subscribed');
|
||||
btn.textContent = '♥ Abonniert';
|
||||
updateBadge(gruppenId, true);
|
||||
}
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function updateBadge(gruppenId, subscribed) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
if (!card) return;
|
||||
const badgesEl = card.querySelector('.gruppe-badges');
|
||||
if (!badgesEl) return;
|
||||
const subBadge = badgesEl.querySelector('.gruppe-badge-sub');
|
||||
if (subscribed && !subBadge) {
|
||||
badgesEl.insertAdjacentHTML('beforeend', `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`);
|
||||
} else if (!subscribed && subBadge) {
|
||||
subBadge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search ──
|
||||
document.getElementById('searchBtn').addEventListener('click', () => {
|
||||
currentName = document.getElementById('searchInput').value.trim();
|
||||
currentPage = 0;
|
||||
loadGroups();
|
||||
});
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('searchBtn').click();
|
||||
});
|
||||
|
||||
// ── Paging ──
|
||||
function updatePaging(current, total) {
|
||||
const el = document.getElementById('paging');
|
||||
if (total <= 1) { el.style.display = 'none'; return; }
|
||||
el.style.display = 'flex';
|
||||
document.getElementById('prevBtn').disabled = current === 0;
|
||||
document.getElementById('nextBtn').disabled = current >= total - 1;
|
||||
document.getElementById('pageInfo').textContent = `Seite ${current + 1} von ${total}`;
|
||||
}
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => {
|
||||
if (currentPage > 0) { currentPage--; loadGroups(); }
|
||||
});
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
if (currentPage < totalPages - 1) { currentPage++; loadGroups(); }
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -191,7 +191,7 @@
|
||||
<div class="acc-body" id="acc-aufgaben-body">
|
||||
<div id="guestAufgabenHint" class="guest-hint" style="display:none;">Aufgaben werden vom Host festgelegt – nur zur Ansicht.</div>
|
||||
<p style="font-size:0.85rem;color:var(--color-muted);margin-bottom:0.75rem;">
|
||||
Gruppen verwalten: <a href="/games/common/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
|
||||
Gruppen verwalten: <a href="/games/vanilla/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
|
||||
</p>
|
||||
<div id="sectionOwn">
|
||||
<div class="aufgaben-section-label"><label class="select-all-label"><input type="checkbox" class="select-all-cb" data-list="listOwn"> Eigene Gruppen</label></div>
|
||||
|
||||
642
src/main/resources/static/games/vanilla/toys.html
Normal file
642
src/main/resources/static/games/vanilla/toys.html
Normal file
@@ -0,0 +1,642 @@
|
||||
<!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>Toys – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* ── Section ── */
|
||||
.section + .section { margin-top: 2.5rem; }
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
}
|
||||
.btn-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-add:hover { background: #c73652; }
|
||||
|
||||
/* ── Toy grid ── */
|
||||
.toy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Toy card ── */
|
||||
.toy-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 0.8rem 0.9rem;
|
||||
transition: border-color 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
.toy-card { cursor: pointer; }
|
||||
.toy-card:hover { border-color: var(--color-primary); }
|
||||
.toy-card.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.06);
|
||||
}
|
||||
|
||||
.toy-img {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toy-img-placeholder {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.toy-info { flex: 1; min-width: 0; }
|
||||
.toy-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.toy-desc {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.2rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Section action buttons ── */
|
||||
.section-actions { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.btn-action {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.btn-action:disabled { opacity: 0.35; cursor: default; }
|
||||
.btn-action:not(:disabled):hover { background: var(--color-primary); color: #fff; }
|
||||
.btn-action-danger:not(:disabled):hover { background: rgba(233,69,96,0.18); color: var(--color-primary); }
|
||||
.action-error {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-primary);
|
||||
min-height: 1.1em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* ── Empty / Loading ── */
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Inline-Fehler im Grid ── */
|
||||
.grid-error {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-primary);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 200;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-backdrop.open { display: flex; }
|
||||
.modal {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.6);
|
||||
}
|
||||
.modal h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.modal label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.modal input[type="text"],
|
||||
.modal textarea {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
resize: vertical;
|
||||
}
|
||||
.modal input[type="text"]:focus,
|
||||
.modal textarea:focus { border-color: var(--color-primary); }
|
||||
.modal input[type="file"] {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.modal-actions .btn-cancel {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-cancel:hover { background: #1a4a8a; }
|
||||
.modal-actions .btn-save {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-save:hover { background: #c73652; }
|
||||
.modal-actions .btn-save:disabled { opacity: 0.5; cursor: default; }
|
||||
.modal-error {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.75rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toy-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<!-- Erstell-/Bearbeitungs-Modal -->
|
||||
<div class="modal-backdrop" id="createModal">
|
||||
<div class="modal">
|
||||
<h2 id="modalTitle">Neues Toy</h2>
|
||||
<label for="toyName">Name *</label>
|
||||
<input type="text" id="toyName" placeholder="z.B. Vibrator" maxlength="100">
|
||||
<label for="toyDesc">Beschreibung</label>
|
||||
<textarea id="toyDesc" rows="3" placeholder="Kurze Beschreibung…" maxlength="500"></textarea>
|
||||
<label>Bild (optional)</label>
|
||||
<div id="currentImageWrap" style="display:none; align-items:center; gap:0.5rem; margin-bottom:0.4rem;">
|
||||
<img id="currentImage" style="max-width:64px; max-height:64px; border-radius:6px;" src="" alt="">
|
||||
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues Bild wählen zum Ersetzen</span>
|
||||
</div>
|
||||
<input type="file" id="toyBild" accept="image/*">
|
||||
<div class="modal-error" id="modalError"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
||||
<button class="btn-save" id="saveBtn">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<!-- Meine Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Meine Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="editBtn" disabled>✎ Bearbeiten</button>
|
||||
<button class="btn-action btn-action-danger" id="deleteBtn" disabled>✕ Löschen</button>
|
||||
<button class="btn-add" id="openCreateBtn">+ Neu</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="actionError"></div>
|
||||
<div class="toy-grid" id="userGrid"></div>
|
||||
<div id="userLoading" class="loading" style="display:none;"></div>
|
||||
<div id="userSentinel"></div>
|
||||
</div>
|
||||
|
||||
<!-- System-Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">System-Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="copyBtn" disabled>⊕ In meine Toys kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="systemActionError"></div>
|
||||
<div class="toy-grid" id="systemGrid"></div>
|
||||
<div id="systemLoading" class="loading" style="display:none;"></div>
|
||||
<div id="systemSentinel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 12;
|
||||
let userPage = 0, userTotalPages = 1, userLoading = false;
|
||||
let systemPage = 0, systemTotalPages = 1, systemLoading = false;
|
||||
|
||||
// ── Infinite-scroll observers ──
|
||||
const userObserver = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) loadUserPage();
|
||||
}, { rootMargin: '200px' });
|
||||
|
||||
const systemObserver = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) loadSystemPage();
|
||||
}, { rootMargin: '200px' });
|
||||
|
||||
// ── Auth + initial load ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.then(user => {
|
||||
if (!user) return;
|
||||
userObserver.observe(document.getElementById('userSentinel'));
|
||||
systemObserver.observe(document.getElementById('systemSentinel'));
|
||||
})
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load user toys (append, füllt Viewport automatisch auf) ──
|
||||
async function loadUserPage() {
|
||||
if (userLoading || userPage >= userTotalPages) return;
|
||||
userLoading = true;
|
||||
const loadEl = document.getElementById('userLoading');
|
||||
try {
|
||||
do {
|
||||
loadEl.textContent = 'Wird geladen…';
|
||||
loadEl.style.display = 'block';
|
||||
const r = await fetch(`/toy/list/user?page=${userPage}&size=${PAGE_SIZE}`);
|
||||
const data = await r.json();
|
||||
userTotalPages = data.totalPages || 1;
|
||||
appendGrid('userGrid', data.content, 'selectToy');
|
||||
userPage++;
|
||||
loadEl.style.display = 'none';
|
||||
} while (userPage < userTotalPages && sentinelVisible('userSentinel'));
|
||||
} catch (_) {
|
||||
loadEl.textContent = 'Fehler beim Laden.';
|
||||
} finally {
|
||||
userLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadUserToys() {
|
||||
userPage = 0;
|
||||
userTotalPages = 1;
|
||||
resetSelection();
|
||||
document.getElementById('userGrid').innerHTML = '';
|
||||
loadUserPage();
|
||||
}
|
||||
|
||||
// ── Load system toys (append, füllt Viewport automatisch auf) ──
|
||||
async function loadSystemPage() {
|
||||
if (systemLoading || systemPage >= systemTotalPages) return;
|
||||
systemLoading = true;
|
||||
const loadEl = document.getElementById('systemLoading');
|
||||
try {
|
||||
do {
|
||||
loadEl.textContent = 'Wird geladen…';
|
||||
loadEl.style.display = 'block';
|
||||
const r = await fetch(`/toy/list/system?page=${systemPage}&size=${PAGE_SIZE}`);
|
||||
const data = await r.json();
|
||||
systemTotalPages = data.totalPages || 1;
|
||||
appendGrid('systemGrid', data.content, 'selectSystemToy');
|
||||
systemPage++;
|
||||
loadEl.style.display = 'none';
|
||||
} while (systemPage < systemTotalPages && sentinelVisible('systemSentinel'));
|
||||
} catch (_) {
|
||||
loadEl.textContent = 'Fehler beim Laden.';
|
||||
} finally {
|
||||
systemLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadSystemToys() {
|
||||
systemPage = 0;
|
||||
systemTotalPages = 1;
|
||||
resetSystemSelection();
|
||||
document.getElementById('systemGrid').innerHTML = '';
|
||||
loadSystemPage();
|
||||
}
|
||||
|
||||
// ── Prüft ob ein Sentinel noch im (erweiterten) Viewport liegt ──
|
||||
function sentinelVisible(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.getBoundingClientRect().top <= window.innerHeight + 200 : false;
|
||||
}
|
||||
|
||||
// ── Append items to a grid ──
|
||||
function appendGrid(gridId, toys, selectFn) {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!toys || toys.length === 0) {
|
||||
if (!grid.querySelector('.toy-card')) {
|
||||
grid.innerHTML = '<p class="empty">Keine Einträge vorhanden.</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
const emptyEl = grid.querySelector('.empty');
|
||||
if (emptyEl) emptyEl.remove();
|
||||
grid.insertAdjacentHTML('beforeend', toys.map(toy => `
|
||||
<div class="toy-card" data-id="${esc(toy.toyId)}"
|
||||
${selectFn ? `onclick="${selectFn}('${esc(toy.toyId)}')"` : ''}>
|
||||
${toy.bild
|
||||
? `<img class="toy-img" src="data:image/png;base64,${toy.bild}" alt="${esc(toy.name)}">`
|
||||
: `<div class="toy-img-placeholder">◈</div>`}
|
||||
<div class="toy-info">
|
||||
<div class="toy-name">${esc(toy.name)}</div>
|
||||
${toy.beschreibung ? `<div class="toy-desc">${esc(toy.beschreibung)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join(''));
|
||||
}
|
||||
|
||||
// ── Selection ──
|
||||
let selectedUserToyId = null;
|
||||
|
||||
function selectToy(toyId) {
|
||||
const prev = document.querySelector('#userGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedUserToyId === toyId) {
|
||||
selectedUserToyId = null;
|
||||
} else {
|
||||
selectedUserToyId = toyId;
|
||||
document.querySelector(`#userGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
const has = selectedUserToyId != null;
|
||||
document.getElementById('editBtn').disabled = !has;
|
||||
document.getElementById('deleteBtn').disabled = !has;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
selectedUserToyId = null;
|
||||
document.getElementById('editBtn').disabled = true;
|
||||
document.getElementById('deleteBtn').disabled = true;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── System-Toy selection ──
|
||||
let selectedSystemToyId = null;
|
||||
|
||||
function selectSystemToy(toyId) {
|
||||
const prev = document.querySelector('#systemGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedSystemToyId === toyId) {
|
||||
selectedSystemToyId = null;
|
||||
} else {
|
||||
selectedSystemToyId = toyId;
|
||||
document.querySelector(`#systemGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
document.getElementById('copyBtn').disabled = selectedSystemToyId == null;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSystemSelection() {
|
||||
selectedSystemToyId = null;
|
||||
document.getElementById('copyBtn').disabled = true;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── Copy system toy ──
|
||||
document.getElementById('copyBtn').addEventListener('click', () => {
|
||||
if (!selectedSystemToyId) return;
|
||||
const btn = document.getElementById('copyBtn');
|
||||
btn.disabled = true;
|
||||
fetch(`/toy/copy/${selectedSystemToyId}`, { method: 'POST' })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
reloadUserToys();
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Du hast bereits ein Toy mit diesem Namen.';
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Fehler beim Kopieren (HTTP ' + r.status + ').';
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('systemActionError').textContent = 'Verbindungsfehler.';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Header action buttons ──
|
||||
document.getElementById('editBtn').addEventListener('click', () => {
|
||||
if (selectedUserToyId) openModal(selectedUserToyId);
|
||||
});
|
||||
|
||||
document.getElementById('deleteBtn').addEventListener('click', () => {
|
||||
if (!selectedUserToyId) return;
|
||||
if (!confirm('Toy wirklich löschen?')) return;
|
||||
const btn = document.getElementById('deleteBtn');
|
||||
btn.disabled = true;
|
||||
const toyId = selectedUserToyId;
|
||||
fetch(`/toy/${toyId}`, { method: 'DELETE' })
|
||||
.then(r => {
|
||||
if (r.status === 409) {
|
||||
showActionError('Wird in Aufgaben verwendet – nicht löschbar.');
|
||||
btn.disabled = false;
|
||||
} else if (r.status === 403) {
|
||||
showActionError('Keine Berechtigung.');
|
||||
btn.disabled = false;
|
||||
} else if (r.ok || r.status === 202) {
|
||||
reloadUserToys();
|
||||
} else {
|
||||
showActionError('Fehler beim Löschen.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { showActionError('Verbindungsfehler.'); btn.disabled = false; });
|
||||
});
|
||||
|
||||
function showActionError(msg) {
|
||||
const el = document.getElementById('actionError');
|
||||
el.textContent = msg;
|
||||
setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 4000);
|
||||
}
|
||||
|
||||
// ── Create / Edit modal ──
|
||||
const modal = document.getElementById('createModal');
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
let currentEditId = null;
|
||||
|
||||
function openModal(editId) {
|
||||
currentEditId = editId || null;
|
||||
document.getElementById('modalError').style.display = 'none';
|
||||
document.getElementById('toyBild').value = '';
|
||||
if (currentEditId) {
|
||||
fetch(`/toy/${currentEditId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(toy => {
|
||||
if (!toy) return;
|
||||
document.getElementById('modalTitle').textContent = 'Toy bearbeiten';
|
||||
document.getElementById('toyName').value = toy.name || '';
|
||||
document.getElementById('toyDesc').value = toy.beschreibung || '';
|
||||
const imgWrap = document.getElementById('currentImageWrap');
|
||||
if (toy.bild) {
|
||||
document.getElementById('currentImage').src = 'data:image/png;base64,' + toy.bild;
|
||||
imgWrap.style.display = 'flex';
|
||||
} else {
|
||||
imgWrap.style.display = 'none';
|
||||
}
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
})
|
||||
.catch(() => alert('Fehler beim Laden des Toys.'));
|
||||
} else {
|
||||
document.getElementById('modalTitle').textContent = 'Neues Toy';
|
||||
document.getElementById('toyName').value = '';
|
||||
document.getElementById('toyDesc').value = '';
|
||||
document.getElementById('currentImageWrap').style.display = 'none';
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
|
||||
|
||||
function closeModal() { modal.classList.remove('open'); }
|
||||
|
||||
function editToy(toyId) { openModal(toyId); }
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const name = document.getElementById('toyName').value.trim();
|
||||
if (!name) {
|
||||
showModalError('Bitte einen Namen eingeben.');
|
||||
return;
|
||||
}
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichert…';
|
||||
|
||||
let bildBase64 = null;
|
||||
const fileInput = document.getElementById('toyBild');
|
||||
if (fileInput.files.length > 0) {
|
||||
bildBase64 = await toBase64(fileInput.files[0]);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
beschreibung: document.getElementById('toyDesc').value.trim() || null,
|
||||
bild: bildBase64
|
||||
};
|
||||
|
||||
const isEdit = currentEditId != null;
|
||||
fetch(isEdit ? `/toy/${currentEditId}` : '/toy', {
|
||||
method: isEdit ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
closeModal();
|
||||
reloadUserToys();
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
showModalError('Ein Toy mit diesem Namen existiert bereits.');
|
||||
} else {
|
||||
showModalError('Fehler beim Speichern (HTTP ' + r.status + ').');
|
||||
}
|
||||
})
|
||||
.catch(() => showModalError('Verbindungsfehler.'))
|
||||
.finally(() => { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; });
|
||||
});
|
||||
|
||||
function showModalError(msg) {
|
||||
const el = document.getElementById('modalError');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function toBase64(file) {
|
||||
const MAX = 128;
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(file);
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > MAX || h > MAX) {
|
||||
if (w >= h) { h = Math.max(1, Math.round(MAX * h / w)); w = MAX; }
|
||||
else { w = Math.max(1, Math.round(MAX * w / h)); h = MAX; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
resolve(canvas.toDataURL('image/png').split(',')[1]);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
// ── XSS-Schutz ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
459
src/main/resources/static/js/mobile-nav.js
Normal file
459
src/main/resources/static/js/mobile-nav.js
Normal file
@@ -0,0 +1,459 @@
|
||||
(function () {
|
||||
if (window.__mobileNavLoaded) return;
|
||||
window.__mobileNavLoaded = true;
|
||||
|
||||
const path = window.location.pathname;
|
||||
const I = window.IC || function () { return ''; };
|
||||
const TOPBAR_H = '4.875rem';
|
||||
|
||||
// ── CSS ──────────────────────────────────────────────────────────────────
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.mobile-topbar {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: ${TOPBAR_H};
|
||||
background: var(--color-card);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
.mobile-topbar-logo {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.mobile-topbar-logo img {
|
||||
height: 3rem;
|
||||
width: auto;
|
||||
}
|
||||
.mobile-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
.mobile-tb-btn {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
font-size: 1.725rem;
|
||||
line-height: 1;
|
||||
padding: 0.75rem 0.675rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
transition: background 0.12s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-tb-btn:hover { background: var(--color-secondary); }
|
||||
.mobile-tb-badge {
|
||||
position: absolute;
|
||||
top: 3px; right: 3px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
font-size: 0.87rem;
|
||||
font-weight: 700;
|
||||
min-width: 1.65em;
|
||||
padding: 0.15em 0.375em;
|
||||
display: none;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-tb-avatar {
|
||||
width: 2.7rem;
|
||||
height: 2.7rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ── Mobile Menu Backdrop ── */
|
||||
.mob-menu-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: ${TOPBAR_H};
|
||||
left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
z-index: 998;
|
||||
}
|
||||
.mob-menu-backdrop.open { display: block; }
|
||||
|
||||
/* ── Mobile Menu Panel ── */
|
||||
.mob-menu-panel {
|
||||
position: fixed;
|
||||
top: ${TOPBAR_H};
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(80%, 360px);
|
||||
background: var(--color-card);
|
||||
border-left: 1px solid var(--color-secondary);
|
||||
z-index: 999;
|
||||
overflow-y: auto;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.mob-menu-panel.open { transform: translateX(0); }
|
||||
|
||||
/* ── Accordion ── */
|
||||
.mnav-section { border-bottom: 1px solid var(--color-secondary); }
|
||||
.mnav-section:last-child { border-bottom: none; }
|
||||
|
||||
.mnav-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.85rem 1.1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.mnav-section-header:hover { background: var(--color-secondary); }
|
||||
|
||||
.mnav-section-arrow {
|
||||
font-size: 0.65rem;
|
||||
transition: transform 0.2s;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.mnav-section.open .mnav-section-arrow { transform: rotate(90deg); }
|
||||
|
||||
.mnav-section-body { display: none; }
|
||||
.mnav-section.open .mnav-section-body { display: block; }
|
||||
|
||||
/* ── Menu Links ── */
|
||||
.mnav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.6rem 1.1rem 0.6rem 1.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.mnav-link:hover { background: var(--color-secondary); color: var(--color-primary); }
|
||||
.mnav-link.active { color: var(--color-primary); background: rgba(var(--color-primary-rgb,233,69,96),0.08); }
|
||||
.mnav-icon { width: 1.3rem; text-align: center; font-size: 1rem; flex-shrink: 0; }
|
||||
.mnav-badge {
|
||||
margin-left: auto;
|
||||
background: var(--color-primary); color: #fff;
|
||||
border-radius: 10px; font-size: 0.68rem; font-weight: 700;
|
||||
min-width: 1.2em; padding: 0.1em 0.3em; display: none;
|
||||
}
|
||||
.mnav-link--danger { color: var(--color-primary); }
|
||||
.mnav-link--danger:hover { background: rgba(var(--color-primary-rgb,233,69,96),0.1); color: var(--color-primary); }
|
||||
|
||||
/* ── Show only on mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-topbar { display: flex; }
|
||||
body.app { padding-top: ${TOPBAR_H}; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// ── Helper ───────────────────────────────────────────────────────────────
|
||||
function lnk(href, iconKey, label, id, badgeId) {
|
||||
const cls = path === href ? ' active' : '';
|
||||
const idAttr = id ? ` id="${id}"` : '';
|
||||
const badge = badgeId ? `<span class="mnav-badge" id="${badgeId}"></span>` : '';
|
||||
const icon = iconKey ? `<span class="mnav-icon">${I(iconKey) || ''}</span>` : `<span class="mnav-icon"></span>`;
|
||||
return `<a href="${href}" class="mnav-link${cls}"${idAttr}>${icon}<span>${label}</span>${badge}</a>`;
|
||||
}
|
||||
|
||||
// ── Menu-Sektionen ────────────────────────────────────────────────────────
|
||||
const SECTIONS = [
|
||||
{
|
||||
label: 'Allgemein',
|
||||
prefixes: ['/userhome.html', '/search.html', '/community/nachrichten.html',
|
||||
'/community/benachrichtigungen.html', '/games/common/einladungen.html'],
|
||||
html: `
|
||||
${lnk('/userhome.html', 'HOME', 'Home' )}
|
||||
${lnk('/search.html', 'SEARCH', 'Suche' )}
|
||||
<a href="/admin/admin.html" class="mnav-link${path === '/admin/admin.html' ? ' active' : ''}"
|
||||
id="mnavAdminLink" style="display:none">
|
||||
<span class="mnav-icon">${I('ADMIN') || '⚙'}</span><span>Administration</span></a>
|
||||
${lnk('/community/nachrichten.html', 'MESSAGES', 'Nachrichten', null, 'mnavBadgeMsg' )}
|
||||
${lnk('/community/benachrichtigungen.html', 'NOTIFICATIONS', 'Benachrichtigungen', null, 'mnavBadgeNotif' )}
|
||||
${lnk('/games/common/einladungen.html', 'INVITATIONS', 'Einladungen', null, 'mnavBadgeInv' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
prefixes: ['/community/'],
|
||||
html: `
|
||||
${lnk('/community/feed.html', 'FEED', 'Feed' )}
|
||||
${lnk('/community/freunde.html', 'FRIENDS', 'Freunde', null, 'mnavBadgeFriends' )}
|
||||
${lnk('/community/gruppen.html', 'GROUPS', 'Gruppen', null, 'mnavBadgeGruppen' )}
|
||||
${lnk('/community/locations.html', 'LOCATION', 'Locations' )}
|
||||
${lnk('/community/events.html', 'EVENT', 'Veranstaltungen' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Dating',
|
||||
prefixes: ['/dating/'],
|
||||
html: `
|
||||
<a href="/dating/dating.html" class="mnav-link${path === '/dating/dating.html' ? ' active' : ''}" id="mnavDatingLink">
|
||||
<span class="mnav-icon">${I('DATING') || '♥'}</span><span>Dating</span></a>
|
||||
${lnk('/dating/besucher.html', '', 'Besucher')}
|
||||
${lnk('/dating/likes.html', '', 'Likes' )}
|
||||
${lnk('/dating/matches.html', '', 'Matches' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Vanilla Game',
|
||||
prefixes: ['/games/vanilla/'],
|
||||
html: `
|
||||
${lnk('/games/vanilla/neuvanilla.html', 'PLAY_NEW', 'Neue Session', 'mnavVanillaNeu' )}
|
||||
<a href="#" class="mnav-link" id="mnavVanillaAktiv" style="display:none">
|
||||
<span class="mnav-icon">${I('WAITING') || ''}</span><span>Aktive Session</span></a>
|
||||
<a href="/games/vanilla/vanillaingame.html" class="mnav-link${path === '/games/vanilla/vanillaingame.html' ? ' active' : ''}"
|
||||
id="mnavVanillaImSpiel" style="display:none">
|
||||
<span class="mnav-icon">${I('PLAY_ACTIVE') || ''}</span><span>Im Spiel</span></a>
|
||||
${lnk('/games/vanilla/aufgaben.html', 'CHECK', 'Aufgaben' )}
|
||||
${lnk('/games/vanilla/toys.html', 'TOYS', 'Toys' )}
|
||||
${lnk('/games/vanilla/entdecken.html', 'DISCOVER', 'Entdecken' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'BDSM Game',
|
||||
prefixes: ['/games/bdsm/'],
|
||||
html: `
|
||||
${lnk('/games/bdsm/neubdsm.html', 'PLAY_NEW', 'Neue Session', 'mnavBdsmNeu' )}
|
||||
<a href="#" class="mnav-link" id="mnavBdsmAktiv" style="display:none">
|
||||
<span class="mnav-icon">${I('WAITING') || ''}</span><span>Aktive Session</span></a>
|
||||
<a href="/games/bdsm/bdsmingame.html" class="mnav-link${path === '/games/bdsm/bdsmingame.html' ? ' active' : ''}"
|
||||
id="mnavBdsmImSpiel" style="display:none">
|
||||
<span class="mnav-icon">${I('PLAY_ACTIVE') || ''}</span><span>Im Spiel</span></a>
|
||||
${lnk('/games/bdsm/aufgaben.html', 'CHECK', 'Aufgaben' )}
|
||||
${lnk('/games/bdsm/toys.html', 'TOYS', 'Toys' )}
|
||||
${lnk('/games/bdsm/entdecken.html', 'DISCOVER', 'Entdecken' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Chastity Game',
|
||||
prefixes: ['/games/chastity/'],
|
||||
html: `
|
||||
${lnk('/games/chastity/neulock.html', 'NEW_LOCK', 'Neues Lock', 'mnavChastityNeu' )}
|
||||
<a href="#" class="mnav-link" id="mnavChastityAktiv" style="display:none">
|
||||
<span class="mnav-icon">${I('ACTIVE_LOCK') || ''}</span><span>Aktives Lock</span></a>
|
||||
${lnk('/games/chastity/communityvotes.html', 'VOTES', 'Community Votes' )}
|
||||
${lnk('/games/chastity/meine-locks.html', 'LOCK', 'Meine Vorlagen' )}
|
||||
${lnk('/games/chastity/entdecken-vorlagen.html', 'DISCOVER', 'Entdecken' )}
|
||||
${lnk('/games/chastity/keyholder-finden.html', 'FRIENDS', 'Keyholder finden' )}
|
||||
${lnk('/games/chastity/keyholder.html', 'KEY', 'Keyholder' )}
|
||||
${lnk('/games/chastity/unlock-history.html', 'HISTORY', 'Code-Historie' )}
|
||||
`,
|
||||
},
|
||||
{
|
||||
label: 'Konto',
|
||||
prefixes: ['/konto/', '/help/'],
|
||||
html: `
|
||||
${lnk('/konto/einstellungen.html', 'SETTINGS', 'Einstellungen')}
|
||||
${lnk('/help/overview.html', 'HELP', 'Hilfe' )}
|
||||
<a href="/login/logout" class="mnav-link mnav-link--danger">
|
||||
<span class="mnav-icon">${I('LOGOUT') || ''}</span><span>Abmelden</span></a>
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mobile Topbar ─────────────────────────────────────────────────────────
|
||||
const topbarEl = document.createElement('div');
|
||||
topbarEl.className = 'mobile-topbar';
|
||||
topbarEl.id = 'mobileTopbar';
|
||||
topbarEl.innerHTML = `
|
||||
<a href="/userhome.html" class="mobile-topbar-logo">
|
||||
<img src="/img/icon.png" alt="xXx Sphere">
|
||||
</a>
|
||||
<div class="mobile-topbar-actions">
|
||||
<a href="/community/nachrichten.html" class="mobile-tb-btn" title="Nachrichten">
|
||||
${I('MESSAGES')}
|
||||
<span class="mobile-tb-badge" id="mobTbMsgBadge"></span>
|
||||
</a>
|
||||
<a href="/community/benachrichtigungen.html" class="mobile-tb-btn" title="Benachrichtigungen">
|
||||
${I('NOTIFICATIONS')}
|
||||
<span class="mobile-tb-badge" id="mobTbNotifBadge"></span>
|
||||
</a>
|
||||
<a href="/games/common/einladungen.html" class="mobile-tb-btn" title="Einladungen">
|
||||
${I('INVITATIONS')}
|
||||
<span class="mobile-tb-badge" id="mobTbInvBadge"></span>
|
||||
</a>
|
||||
<a href="/community/benutzer.html" class="mobile-tb-btn" id="mobTbProfileBtn" title="Profil">
|
||||
${I('PROFILE')}
|
||||
</a>
|
||||
<button class="mobile-tb-btn" id="mobMenuToggle" aria-label="Menü">
|
||||
${I('MENU') || '☰'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentElement('afterbegin', topbarEl);
|
||||
|
||||
// ── Overlay ───────────────────────────────────────────────────────────────
|
||||
const backdropEl = document.createElement('div');
|
||||
backdropEl.className = 'mob-menu-backdrop';
|
||||
backdropEl.id = 'mobMenuBackdrop';
|
||||
|
||||
const panelEl = document.createElement('div');
|
||||
panelEl.className = 'mob-menu-panel';
|
||||
panelEl.id = 'mobMenuPanel';
|
||||
panelEl.innerHTML = SECTIONS.map(s => {
|
||||
const isOpen = s.prefixes.some(p => path.startsWith(p) || path === p);
|
||||
return `
|
||||
<div class="mnav-section${isOpen ? ' open' : ''}">
|
||||
<div class="mnav-section-header">
|
||||
<span>${s.label}</span>
|
||||
<span class="mnav-section-arrow">▶</span>
|
||||
</div>
|
||||
<div class="mnav-section-body">${s.html}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
document.body.appendChild(backdropEl);
|
||||
document.body.appendChild(panelEl);
|
||||
|
||||
// ── Accordion (nur eine Sektion gleichzeitig offen) ──────────────────────
|
||||
panelEl.querySelectorAll('.mnav-section-header').forEach(h => {
|
||||
h.addEventListener('click', () => {
|
||||
const section = h.closest('.mnav-section');
|
||||
const isOpen = section.classList.contains('open');
|
||||
panelEl.querySelectorAll('.mnav-section').forEach(s => s.classList.remove('open'));
|
||||
if (!isOpen) section.classList.add('open');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Open / Close ──────────────────────────────────────────────────────────
|
||||
function openMenu() {
|
||||
panelEl.classList.add('open');
|
||||
backdropEl.classList.add('open');
|
||||
}
|
||||
function closeMenu() {
|
||||
panelEl.classList.remove('open');
|
||||
backdropEl.classList.remove('open');
|
||||
}
|
||||
|
||||
document.getElementById('mobMenuToggle').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
panelEl.classList.contains('open') ? closeMenu() : openMenu();
|
||||
});
|
||||
backdropEl.addEventListener('click', closeMenu);
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMenu(); });
|
||||
panelEl.querySelectorAll('.mnav-link').forEach(l => {
|
||||
l.addEventListener('click', () => { if (l.getAttribute('href') !== '#') closeMenu(); });
|
||||
});
|
||||
|
||||
// ── Badges ────────────────────────────────────────────────────────────────
|
||||
function setBadge(id, n) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = n > 99 ? '99+' : n;
|
||||
el.style.display = n > 0 ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
fetch('/social/messages/unread/count').then(r => r.ok ? r.json() : 0).then(n => {
|
||||
setBadge('mobTbMsgBadge', n); setBadge('mnavBadgeMsg', n);
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/notifications/unread/count').then(r => r.ok ? r.json() : 0).then(n => {
|
||||
setBadge('mobTbNotifBadge', n); setBadge('mnavBadgeNotif', n);
|
||||
}).catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/lockee/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/keyholder/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/bdsm/einladung/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/vanilla/einladung/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
]).then(([l, k, b, v]) => {
|
||||
const n = l + k + b + v;
|
||||
setBadge('mobTbInvBadge', n); setBadge('mnavBadgeInv', n);
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/social/friends/pending/count').then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge('mnavBadgeFriends', n)).catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/gruppen/requests/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/gruppen/reports/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
]).then(([j, rep]) => setBadge('mnavBadgeGruppen', j + rep)).catch(() => {});
|
||||
|
||||
// ── User / Dynamische Links ───────────────────────────────────────────────
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||
if (!user) return;
|
||||
|
||||
// Profilbild
|
||||
const profileBtn = document.getElementById('mobTbProfileBtn');
|
||||
if (profileBtn) {
|
||||
profileBtn.href = '/community/benutzer.html?userId=' + user.userId;
|
||||
if (user.profilePicture) {
|
||||
profileBtn.innerHTML =
|
||||
`<img src="data:image/png;base64,${user.profilePicture}" class="mobile-tb-avatar" alt="">`;
|
||||
}
|
||||
}
|
||||
|
||||
// Admin
|
||||
if (user.admin) {
|
||||
const el = document.getElementById('mnavAdminLink');
|
||||
if (el) el.style.display = '';
|
||||
}
|
||||
|
||||
// Dating
|
||||
const datingLink = document.getElementById('mnavDatingLink');
|
||||
if (datingLink) {
|
||||
datingLink.href = user.datingAktiv
|
||||
? '/dating/dating.html'
|
||||
: '/konto/einstellungen.html#sec-dating';
|
||||
}
|
||||
|
||||
const hide = id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; };
|
||||
const show = id => { const el = document.getElementById(id); if (el) el.style.display = ''; };
|
||||
const setHref = (id, h) => { const el = document.getElementById(id); if (el) el.href = h; };
|
||||
|
||||
// BDSM
|
||||
try {
|
||||
const r = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('mnavBdsmNeu'); hide('mnavBdsmImSpiel'); show('mnavBdsmAktiv');
|
||||
setHref('mnavBdsmAktiv', aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html');
|
||||
} else {
|
||||
const sr = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('mnavBdsmNeu'); show('mnavBdsmImSpiel'); }
|
||||
else show('mnavBdsmNeu');
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Vanilla
|
||||
try {
|
||||
const r = await fetch('/vanilla/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('mnavVanillaNeu'); hide('mnavVanillaImSpiel'); show('mnavVanillaAktiv');
|
||||
setHref('mnavVanillaAktiv', aktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html');
|
||||
} else {
|
||||
const sr = await fetch(`/vanilla?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('mnavVanillaNeu'); show('mnavVanillaImSpiel'); }
|
||||
else show('mnavVanillaNeu');
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Chastity
|
||||
try {
|
||||
const r = await fetch('/keyholder/mylock');
|
||||
if (r.ok) {
|
||||
const lock = await r.json();
|
||||
show('mnavChastityAktiv');
|
||||
setHref('mnavChastityAktiv', '/games/chastity/activelock.html?lockId=' + lock.lockId);
|
||||
}
|
||||
} catch (_) {}
|
||||
}).catch(() => {});
|
||||
})();
|
||||
@@ -8,23 +8,48 @@
|
||||
/* ── Burger-Button ── */
|
||||
.nav-burger {
|
||||
display: inline-flex; align-items: center; gap: 0.45rem;
|
||||
padding: 0.35rem 0.8rem 0.35rem 0.6rem;
|
||||
padding: 0.46rem 0.8rem 0.46rem 0.6rem;
|
||||
background: none; border: 1px solid var(--color-secondary);
|
||||
border-radius: 8px; cursor: pointer;
|
||||
color: var(--color-text); font-size: 0.88rem; font-weight: 600;
|
||||
flex-shrink: 0; transition: border-color 0.15s, color 0.15s;
|
||||
margin-right: 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.nav-burger:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.nav-burger-icon { font-size: 1.05rem; line-height: 1; }
|
||||
.nav-burger-icon {
|
||||
font-size: 1.05rem; line-height: 1;
|
||||
position: relative;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 1.2em; height: 1.2em;
|
||||
}
|
||||
.nav-burger-icon-menu,
|
||||
.nav-burger-icon-close {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.nav-burger-icon-close { opacity: 0; transform: rotate(-45deg); }
|
||||
#topbar.nav-menu-open .nav-burger .nav-burger-icon-menu { opacity: 0; transform: rotate(45deg); }
|
||||
#topbar.nav-menu-open .nav-burger .nav-burger-icon-close { opacity: 1; transform: rotate(0deg); }
|
||||
|
||||
/* ── Backdrop ── */
|
||||
.nav-backdrop {
|
||||
display: none; position: fixed; inset: 0; z-index: 498;
|
||||
position: fixed; inset: 0; z-index: 498;
|
||||
background: rgba(0,0,0,0);
|
||||
pointer-events: none;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.nav-backdrop.open {
|
||||
background: rgba(0,0,0,0.55);
|
||||
pointer-events: auto;
|
||||
}
|
||||
.nav-backdrop.open { display: block; }
|
||||
|
||||
/* ── Topbar: untere Rundung entfernen wenn Menü offen ── */
|
||||
#topbar {
|
||||
transition: border-radius 0.22s ease, border-bottom-color 0.22s ease;
|
||||
}
|
||||
#topbar.nav-menu-open {
|
||||
border-radius: 12px 12px 0 0;
|
||||
border-bottom-color: transparent;
|
||||
@@ -38,11 +63,21 @@
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.55);
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 80px);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0.22s;
|
||||
}
|
||||
.nav-dropdown.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0s;
|
||||
}
|
||||
.nav-dropdown.open { display: block; }
|
||||
|
||||
/* ── 4-Spalten-Layout ── */
|
||||
.nav-columns {
|
||||
@@ -226,17 +261,17 @@
|
||||
{ href: '/games/vanilla/neuvanilla.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navVanillaNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navVanillaAktiv' },
|
||||
{ href: '/games/vanilla/vanillaingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navVanillaImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
{ href: '/games/vanilla/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/vanilla/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/vanilla/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
])}
|
||||
${gameGroup('BDSM', 'BDSM Game', [
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html?mode=bdsm', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/games/bdsm/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/bdsm/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/bdsm/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
])}
|
||||
${gameGroup('CHASTITY', 'Chastity Game', [
|
||||
{ href: '/games/chastity/neulock.html', icon: 'NEW_LOCK', label: 'Neues Lock', id: 'navChastityNeu' },
|
||||
@@ -330,8 +365,8 @@
|
||||
btn.className = 'nav-burger';
|
||||
btn.id = 'navBurgerBtn';
|
||||
btn.setAttribute('aria-label', 'Menü öffnen');
|
||||
btn.innerHTML = `<span class="nav-burger-icon">${I('MENU') || '☰'}</span><span class="nav-burger-text">Menü</span>`;
|
||||
topbarLeft.prepend(btn);
|
||||
btn.innerHTML = `<span class="nav-burger-icon"><span class="nav-burger-icon-menu">${I('MENU') || '≡'}</span><span class="nav-burger-icon-close">${I('CLOSE') || 'x'}</span></span><span class="nav-burger-text">Menü</span>`;
|
||||
topbarLeft.append(btn);
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const dd = document.getElementById('navDropdown');
|
||||
@@ -460,4 +495,6 @@
|
||||
}
|
||||
loadScript('/js/topbar.js');
|
||||
loadScript('/js/social-sidebar.js');
|
||||
loadScript('/js/section-nav.js');
|
||||
loadScript('/js/mobile-nav.js');
|
||||
})();
|
||||
|
||||
243
src/main/resources/static/js/section-nav.js
Normal file
243
src/main/resources/static/js/section-nav.js
Normal file
@@ -0,0 +1,243 @@
|
||||
(function () {
|
||||
const path = window.location.pathname;
|
||||
const search = window.location.search;
|
||||
const I = window.IC || function () { return ''; };
|
||||
|
||||
// ── Bereichs-Definitionen ────────────────────────────────────────────────
|
||||
const SECTIONS = {
|
||||
social: {
|
||||
prefixes: ['/community/'],
|
||||
exclude: [
|
||||
'/community/nachrichten.html',
|
||||
'/community/benachrichtigungen.html',
|
||||
'/community/einladungen.html',
|
||||
],
|
||||
items: [
|
||||
{ href: '/community/feed.html', icon: 'FEED', label: 'Feed' },
|
||||
{ href: '/community/freunde.html', icon: 'FRIENDS', label: 'Freunde' },
|
||||
{ href: '/community/gruppen.html', icon: 'GROUPS', label: 'Gruppen' },
|
||||
{ href: '/community/locations.html', icon: 'LOCATION', label: 'Locations' },
|
||||
{ href: '/community/events.html', icon: 'EVENT', label: 'Veranstaltungen' },
|
||||
],
|
||||
},
|
||||
dating: {
|
||||
prefixes: ['/dating/'],
|
||||
items: [
|
||||
{ href: '/dating/dating.html', icon: 'DATING', label: 'Dating', id: 'snavDatingLink' },
|
||||
{ href: '/dating/besucher.html', icon: '', label: 'Besucher' },
|
||||
{ href: '/dating/likes.html', icon: '', label: 'Likes' },
|
||||
{ href: '/dating/matches.html', icon: '', label: 'Matches' },
|
||||
],
|
||||
},
|
||||
vanilla: {
|
||||
prefixes: ['/games/vanilla/'],
|
||||
items: [
|
||||
{ href: '/games/vanilla/neuvanilla.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'snavVanillaNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'snavVanillaAktiv', hidden: true },
|
||||
{ href: '/games/vanilla/vanillaingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'snavVanillaImSpiel', hidden: true },
|
||||
{ href: '/games/vanilla/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/vanilla/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/vanilla/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
],
|
||||
},
|
||||
bdsm: {
|
||||
prefixes: ['/games/bdsm/'],
|
||||
items: [
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'snavBdsmNeu' },
|
||||
{ href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'snavBdsmAktiv', hidden: true },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'snavBdsmImSpiel', hidden: true },
|
||||
{ href: '/games/bdsm/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
|
||||
{ href: '/games/bdsm/toys.html', icon: 'TOYS', label: 'Toys' },
|
||||
{ href: '/games/bdsm/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
],
|
||||
},
|
||||
chastity: {
|
||||
prefixes: ['/games/chastity/'],
|
||||
items: [
|
||||
{ href: '/games/chastity/neulock.html', icon: 'NEW_LOCK', label: 'Neues Lock', id: 'snavChastityNeu' },
|
||||
{ href: '#', icon: 'ACTIVE_LOCK', label: 'Aktives Lock', id: 'snavChastityAktiv', hidden: true },
|
||||
{ href: '/games/chastity/communityvotes.html', icon: 'VOTES', label: 'Community Votes' },
|
||||
{ href: '/games/chastity/meine-locks.html', icon: 'LOCK', label: 'Meine Vorlagen' },
|
||||
{ href: '/games/chastity/entdecken-vorlagen.html', icon: 'DISCOVER', label: 'Entdecken' },
|
||||
{ href: '/games/chastity/keyholder-finden.html', icon: 'FRIENDS', label: 'Keyholder finden' },
|
||||
{ href: '/games/chastity/keyholder.html', icon: 'KEY', label: 'Keyholder' },
|
||||
{ href: '/games/chastity/unlock-history.html', icon: 'HISTORY', label: 'Code-Historie' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ── Aktiven Bereich ermitteln ────────────────────────────────────────────
|
||||
const sectionKey = Object.keys(SECTIONS).find(k => {
|
||||
const s = SECTIONS[k];
|
||||
if (!s.prefixes.some(p => path.startsWith(p))) return false;
|
||||
if (s.exclude && s.exclude.includes(path)) return false;
|
||||
return true;
|
||||
});
|
||||
if (!sectionKey) return;
|
||||
const section = SECTIONS[sectionKey];
|
||||
|
||||
// ── CSS ──────────────────────────────────────────────────────────────────
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.section-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 0 0.6rem 0;
|
||||
}
|
||||
.section-nav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 7px;
|
||||
text-decoration: none;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.section-nav-link:hover {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.section-nav-link.active {
|
||||
color: var(--color-primary);
|
||||
background: rgba(var(--color-primary-rgb, 233,69,96), 0.09);
|
||||
font-weight: 600;
|
||||
}
|
||||
.section-nav-icon {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.section-nav--icons-only .section-nav-label { display: none; }
|
||||
.section-nav--icons-only .section-nav-link { gap: 0; padding: 0.3rem 0.55rem; }
|
||||
.section-nav-sep {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
margin: 0 0 1.25rem 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// ── Aktiv-Erkennung ──────────────────────────────────────────────────────
|
||||
function isActive(item) {
|
||||
if (item.href === '#') return false;
|
||||
const [itemPath, itemQuery] = item.href.split('?');
|
||||
if (itemQuery) return path === itemPath && search === '?' + itemQuery;
|
||||
return path === itemPath;
|
||||
}
|
||||
|
||||
// ── Nav bauen ────────────────────────────────────────────────────────────
|
||||
const navEl = document.createElement('nav');
|
||||
navEl.className = 'section-nav';
|
||||
|
||||
section.items.forEach(item => {
|
||||
const a = document.createElement('a');
|
||||
a.href = item.href;
|
||||
a.className = 'section-nav-link' + (isActive(item) ? ' active' : '');
|
||||
if (item.id) a.id = item.id;
|
||||
if (item.hidden) a.style.display = 'none';
|
||||
a.title = item.label;
|
||||
if (item.icon) a.innerHTML += `<span class="section-nav-icon">${I(item.icon) || ''}</span>`;
|
||||
a.innerHTML += `<span class="section-nav-label">${item.label}</span>`;
|
||||
navEl.appendChild(a);
|
||||
});
|
||||
|
||||
const sep = document.createElement('hr');
|
||||
sep.className = 'section-nav-sep';
|
||||
|
||||
// ── Einfügen in .main ────────────────────────────────────────────────────
|
||||
function checkOverflow() {
|
||||
// Immer zuerst Labels einblenden und ohne wrap messen —
|
||||
// so gibt es keine Feedback-Schleife durch Größenänderung nach dem Umschalten
|
||||
navEl.classList.remove('section-nav--icons-only');
|
||||
navEl.style.flexWrap = 'nowrap';
|
||||
const overflows = navEl.scrollWidth > navEl.clientWidth;
|
||||
navEl.style.flexWrap = '';
|
||||
if (overflows) navEl.classList.add('section-nav--icons-only');
|
||||
}
|
||||
|
||||
function inject() {
|
||||
const main = document.querySelector('.main');
|
||||
if (!main) { setTimeout(inject, 30); return; }
|
||||
main.insertBefore(sep, main.firstChild);
|
||||
main.insertBefore(navEl, sep);
|
||||
loadDynamic();
|
||||
// Overflow-Erkennung beim Laden und bei Größenänderung
|
||||
requestAnimationFrame(checkOverflow);
|
||||
new ResizeObserver(checkOverflow).observe(navEl);
|
||||
}
|
||||
inject();
|
||||
|
||||
// ── Dynamische Elemente (analog nav.js) ──────────────────────────────────
|
||||
function hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
||||
function show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
||||
function setHref(id, h) { const el = document.getElementById(id); if (el) el.href = h; }
|
||||
|
||||
async function loadDynamic() {
|
||||
try {
|
||||
const res = await fetch('/login/me');
|
||||
if (!res.ok) return;
|
||||
const user = await res.json();
|
||||
if (!user) return;
|
||||
|
||||
// Dating-Link
|
||||
const datingLink = document.getElementById('snavDatingLink');
|
||||
if (datingLink) {
|
||||
datingLink.href = user.datingAktiv
|
||||
? '/dating/dating.html'
|
||||
: '/konto/einstellungen.html#sec-dating';
|
||||
}
|
||||
|
||||
// BDSM
|
||||
if (sectionKey === 'bdsm') {
|
||||
try {
|
||||
const r = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('snavBdsmNeu'); hide('snavBdsmImSpiel');
|
||||
show('snavBdsmAktiv');
|
||||
setHref('snavBdsmAktiv', aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html');
|
||||
} else {
|
||||
const sr = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('snavBdsmNeu'); show('snavBdsmImSpiel'); }
|
||||
else show('snavBdsmNeu');
|
||||
}
|
||||
} catch (_) { show('snavBdsmNeu'); }
|
||||
}
|
||||
|
||||
// Vanilla
|
||||
if (sectionKey === 'vanilla') {
|
||||
try {
|
||||
const r = await fetch('/vanilla/einladung/meine-aktive');
|
||||
if (r.ok) {
|
||||
const aktiv = await r.json();
|
||||
hide('snavVanillaNeu'); hide('snavVanillaImSpiel');
|
||||
show('snavVanillaAktiv');
|
||||
setHref('snavVanillaAktiv', aktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html');
|
||||
} else {
|
||||
const sr = await fetch(`/vanilla?userId=${user.userId}`);
|
||||
if (sr.status === 200) { hide('snavVanillaNeu'); show('snavVanillaImSpiel'); }
|
||||
else show('snavVanillaNeu');
|
||||
}
|
||||
} catch (_) { show('snavVanillaNeu'); }
|
||||
}
|
||||
|
||||
// Chastity
|
||||
if (sectionKey === 'chastity') {
|
||||
try {
|
||||
const r = await fetch('/keyholder/mylock');
|
||||
if (r.ok) {
|
||||
const lock = await r.json();
|
||||
show('snavChastityAktiv');
|
||||
setHref('snavChastityAktiv', '/games/chastity/activelock.html?lockId=' + lock.lockId);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
})();
|
||||
@@ -10,9 +10,9 @@
|
||||
{ href: '/games/vanilla/neuvanilla.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navVanillaNeu' },
|
||||
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navVanillaAktiv' },
|
||||
{ href: '/games/vanilla/vanillaingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navVanillaImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
{ href: '/games/vanilla/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/vanilla/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/vanilla/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -22,9 +22,9 @@
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/games/common/aufgaben.html?mode=bdsm', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
{ href: '/games/bdsm/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/bdsm/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/bdsm/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
.search-load-more:hover { border-color: var(--color-primary); color: var(--color-primary); background: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1 style="margin-bottom:1rem;">Suche</h1>
|
||||
|
||||
Reference in New Issue
Block a user