Weiter gebaut
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-25 16:56:35 +02:00
parent e4b762f905
commit 4f2048bdc8
242 changed files with 14108 additions and 1770 deletions

View File

@@ -6,4 +6,4 @@ app.cookie.secure=false
# Klartext-Credentials für lokale DB (kein Umgebungsvariablen-Zwang)
spring.mail.username=local@dev.invalid
spring.mail.password=unused
jwt.keystore.password=XUR!Rv&f$j3UsqD&
jwt.keystore.password=${JWT_KEYSTORE_PASSWORD}

View File

@@ -6,6 +6,8 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.flyway.baseline-on-migrate=true
spring.flyway.locations=classpath:db/migration
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect

View File

@@ -0,0 +1,8 @@
-- Baseline marker für Flyway.
-- Diese Migration wird auf bestehenden Datenbanken nicht ausgeführt
-- (spring.flyway.baseline-on-migrate=true markiert sie als bereits angewendet).
-- Für neue Datenbanken: Schema wird von Hibernate (ddl-auto=update) angelegt,
-- da kein vollständiges CREATE-Skript vorhanden ist.
-- Sobald das Schema stabil ist, diesen Inhalt durch ein vollständiges
-- mysqldump --no-data xxx_sphere > V1__baseline.sql ersetzen
-- und ddl-auto auf validate umstellen.

View File

@@ -0,0 +1,34 @@
-- Migration: vanillaAvailable (boolean) → availableIn (VARCHAR enum)
--
-- BDSM_AND_VANILLA = ehemals vanilla_available = TRUE
-- BDSM_ONLY = ehemals vanilla_available = FALSE (Default)
-- CHASTITY_ONLY = neuer Wert
--
-- Die Prozedur prüft zuerst, ob vanilla_available noch existiert, bevor
-- sie etwas tut dadurch ist die Migration auf leeren Datenbanken ein No-op.
DROP PROCEDURE IF EXISTS proc_migrate_available_in;
CREATE PROCEDURE proc_migrate_available_in()
BEGIN
DECLARE col_exists INT DEFAULT 0;
SELECT COUNT(*) INTO col_exists
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'aufgaben_gruppe'
AND COLUMN_NAME = 'vanilla_available';
IF col_exists > 0 THEN
ALTER TABLE aufgaben_gruppe
ADD COLUMN available_in VARCHAR(50) NOT NULL DEFAULT 'BDSM_ONLY';
UPDATE aufgaben_gruppe
SET available_in = 'BDSM_AND_VANILLA'
WHERE vanilla_available = 1;
ALTER TABLE aufgaben_gruppe
DROP COLUMN vanilla_available;
END IF;
END;
CALL proc_migrate_available_in();
DROP PROCEDURE IF EXISTS proc_migrate_available_in;

View File

@@ -122,6 +122,7 @@
.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-public { background:rgba(46,204,113,0.15); color:var(--color-success); }
.gruppe-badge-vanilla { background:#e8f5e9; color:#2e7d32; border:1px solid #a5d6a7; }
.gruppe-badge-chastity { background:rgba(155,89,182,0.15); color:#9b59b6; border:1px solid rgba(155,89,182,0.4); }
.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); }
.gruppe-body { border-top:1px solid var(--color-secondary); padding:1rem 1rem 0.75rem; }
@@ -278,11 +279,12 @@
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild neues wählen zum Ersetzen</span>
</div>
<input type="file" id="gBild" accept="image/*">
<label style="margin-top:0.5rem;">
<span class="modal-check">
<input type="checkbox" id="gVanilla">
Auch für Vanilla-Game verfügbar
</span>
<label style="margin-top:0.5rem;display:block;font-size:0.85rem;color:var(--color-muted);">Verfügbar in
<select id="gAvailableIn" style="margin-top:0.3rem;width:100%;padding:0.5rem 0.75rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;">
<option value="BDSM_ONLY">Nur BDSM</option>
<option value="BDSM_AND_VANILLA">BDSM &amp; Vanilla</option>
<option value="CHASTITY_ONLY">Nur Chastity</option>
</select>
</label>
<div class="modal-error" id="gruppeModalError"></div>
<div class="modal-actions">
@@ -993,7 +995,7 @@ function renderAdminGruppen(gruppen) {
<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>
<div class="gruppe-badges"><span class="gruppe-badge gruppe-badge-public">Öffentlich</span>${g.vanillaAvailable ? '<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>' : ''}</div>
<div class="gruppe-badges"><span class="gruppe-badge gruppe-badge-public">Öffentlich</span>${g.availableIn === 'BDSM_AND_VANILLA' ? '<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>' : ''}${g.availableIn === 'CHASTITY_ONLY' ? '<span class="gruppe-badge gruppe-badge-chastity">Chastity</span>' : ''}</div>
</div>
<span class="gruppe-toggle">▶</span>
</div>
@@ -1269,7 +1271,7 @@ function openGruppeModal(editId) {
document.getElementById('gName').value = g.name || '';
document.getElementById('gVon').value = g.von || '';
document.getElementById('gDesc').value = g.beschreibung || '';
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
document.getElementById('gAvailableIn').value = g.availableIn || 'BDSM_ONLY';
const imgWrap = document.getElementById('gCurrentImgWrap');
if (g.bild) { document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild; imgWrap.style.display = 'flex'; }
else imgWrap.style.display = 'none';
@@ -1278,7 +1280,7 @@ function openGruppeModal(editId) {
document.getElementById('gName').value = '';
document.getElementById('gVon').value = '';
document.getElementById('gDesc').value = '';
document.getElementById('gVanilla').checked = false;
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
document.getElementById('gCurrentImgWrap').style.display = 'none';
}
gruppeModal.classList.add('open');
@@ -1357,7 +1359,7 @@ gruppeModalSave.addEventListener('click', async () => {
let bildBase64 = null;
const fi = document.getElementById('gBild');
if (fi.files.length > 0) bildBase64 = await toBase64(fi.files[0]);
const payload = { name, von: document.getElementById('gVon').value.trim() || null, beschreibung: document.getElementById('gDesc').value.trim() || null, vanillaAvailable: document.getElementById('gVanilla').checked, bild: bildBase64 };
const payload = { name, von: document.getElementById('gVon').value.trim() || null, beschreibung: document.getElementById('gDesc').value.trim() || null, availableIn: document.getElementById('gAvailableIn').value, bild: bildBase64 };
const isEdit = currentEditGruppeId != null;
fetch(isEdit ? `/admin/aufgabengruppen/${currentEditGruppeId}` : '/admin/aufgabengruppen', {
method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)

File diff suppressed because it is too large Load Diff

View 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 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>

View 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
</body>
</html>

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta http-equiv="refresh" content="0;url=/games/aufgaben/aufgaben.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aufgaben BDSM xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
@@ -96,6 +97,7 @@
.gruppe-badge-private { background: rgba(233,69,96,0.15); color: var(--color-primary); }
.gruppe-badge-public { background: rgba(46,204,113,0.15); color: var(--color-success); }
.gruppe-badge-vanilla { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
.gruppe-badge-chastity { background: rgba(155,89,182,0.15); color: #9b59b6; border: 1px solid rgba(155,89,182,0.4); }
.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); }
@@ -368,12 +370,14 @@
Gruppe veröffentlichen (für alle sichtbar)
</span>
</label>
<label>
<span class="modal-check">
<input type="checkbox" id="gVanilla">
Auch für Vanilla-Game verfügbar
</span>
</label>
<div style="margin-top:0.5rem;">
<label for="gAvailableIn" style="font-size:0.85rem;display:block;margin-bottom:0.3rem;">Verfügbar in</label>
<select id="gAvailableIn" style="width:100%;padding:0.5rem 0.75rem;border-radius:6px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.9rem;">
<option value="BDSM_ONLY">Nur BDSM</option>
<option value="BDSM_AND_VANILLA">BDSM &amp; Vanilla</option>
<option value="CHASTITY_ONLY">Nur Chastity</option>
</select>
</div>
<div class="modal-error" id="modalError"></div>
<div class="modal-actions">
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
@@ -636,8 +640,13 @@
.then(user => { if (!user) return; loadUserGruppen(); loadAboGruppen(); loadSystemGruppen(); })
.catch(() => { window.location.href = '/login.html'; });
// ── Cross-tab notification ──
let _notifyOnLoad = false;
const gruppenBc = new BroadcastChannel('bdsm-gruppen-updated');
// ── Load ──
function loadUserGruppen() {
if (_notifyOnLoad) { _notifyOnLoad = false; try { gruppenBc.postMessage(1); } catch (_) {} }
resetSelection();
document.getElementById('userLoading').style.display = 'block';
fetch(apiUrl(`/gruppe/list/user`) + `?page=${userPage}&size=${PAGE_SIZE}`)
@@ -722,7 +731,8 @@
if (g.privateGruppe) badges.push(`<span class="gruppe-badge gruppe-badge-private">Privat</span>`);
else badges.push(`<span class="gruppe-badge gruppe-badge-public">Öffentlich</span>`);
if (type === 'user' && g.subscriberCount > 0) badges.push(`<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`);
if (g.vanillaAvailable) badges.push(`<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>`);
if (g.availableIn === 'BDSM_AND_VANILLA') badges.push(`<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>`);
if (g.availableIn === 'CHASTITY_ONLY') badges.push(`<span class="gruppe-badge gruppe-badge-chastity">Chastity</span>`);
return `
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
@@ -936,7 +946,7 @@
openItemId = null;
pendingExpandId = gruppenId;
pendingExpandType = 'user';
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else {
document.getElementById('userActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').';
}
@@ -1077,7 +1087,7 @@
pubCb.checked = !g.privateGruppe;
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
document.getElementById('gPublicLabel').style.display = 'block';
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
document.getElementById('gAvailableIn').value = g.availableIn || 'BDSM_ONLY';
const imgWrap = document.getElementById('gCurrentImgWrap');
if (g.bild) {
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
@@ -1095,7 +1105,7 @@
document.getElementById('gDesc').value = '';
document.getElementById('gPublic').checked = false;
document.getElementById('gPublicLabel').style.display = 'none';
document.getElementById('gVanilla').checked = false;
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
document.getElementById('gCurrentImgWrap').style.display = 'none';
gruppeModal.classList.add('open');
document.getElementById('gName').focus();
@@ -1129,7 +1139,7 @@
name,
beschreibung: document.getElementById('gDesc').value.trim() || null,
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
vanillaAvailable: document.getElementById('gVanilla').checked,
availableIn: document.getElementById('gAvailableIn').value,
bild: bildBase64
};
@@ -1146,7 +1156,7 @@
pendingExpandType = 'user';
}
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else if (r.status === 409) {
showModalError('Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.');
} else {
@@ -1172,7 +1182,7 @@
.then(r => {
if (r.ok || r.status === 202) {
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else if (r.status === 403) {
document.getElementById('userActionError').textContent = 'Keine Berechtigung.';
btn.disabled = false;
@@ -1194,7 +1204,7 @@
.then(r => {
if (r.ok || r.status === 201) {
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
document.getElementById('systemActionError').textContent = '';
} else {
document.getElementById('systemActionError').textContent = 'Fehler beim Kopieren (HTTP ' + r.status + ').';
@@ -1213,7 +1223,7 @@
.then(r => {
if (r.ok || r.status === 201) {
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
document.getElementById('aboActionError').textContent = '';
} else if (r.status === 409) {
document.getElementById('aboActionError').textContent = 'Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.';
@@ -1643,7 +1653,7 @@
pendingExpandId = currentItemGruppeId;
pendingExpandType = 'user';
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else if (r.status === 409) {
showItemError('Limit erreicht: maximal 100 Einträge pro Gruppe möglich.');
} else {
@@ -1739,7 +1749,7 @@
pendingExpandId = selectedGruppeId;
pendingExpandType = 'user';
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else {
const errEl = document.getElementById('publishError');
errEl.textContent = 'Fehler beim Veröffentlichen (HTTP ' + r.status + ').';

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta http-equiv="refresh" content="0;url=/games/aufgaben/entdecken.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entdecken xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">

View File

@@ -67,13 +67,36 @@
.card-field:last-child { margin-bottom: 0; }
.card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; }
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
.check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; }
.check-group--two-col { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); }
.check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; position: relative; }
.check-item.is-checked { border-color: var(--color-primary); }
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; display: flex; align-items: center; gap: 0.2rem; flex-wrap: wrap; }
.check-item-desc { display: none; }
.check-item-tooltip {
display: none; position: absolute; bottom: calc(100% + 6px); left: 0;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 6px; padding: 0.4rem 0.65rem;
font-size: 0.78rem; color: var(--color-muted); line-height: 1.4;
width: max-content; max-width: 210px;
z-index: 50; pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.35);
}
.check-item:hover .check-item-tooltip { display: block; }
.check-item-info-btn {
display: none; background: none; border: 1px solid var(--color-muted);
border-radius: 50%; width: 1.1rem; height: 1.1rem; font-size: 0.62rem;
color: var(--color-muted); cursor: pointer; padding: 0; line-height: 1;
flex-shrink: 0; font-style: normal; font-weight: normal;
align-items: center; justify-content: center;
}
.check-item-info-btn.active { border-color: var(--color-primary); color: var(--color-primary); }
.check-item-desc-mobile { display: none; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.25rem; line-height: 1.4; }
@media (max-width: 679px) {
.check-item:hover .check-item-tooltip { display: none; }
.check-item-info-btn { display: inline-flex; }
}
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
.add-player-btn { width: 100%; background: transparent; border: 1px dashed var(--color-secondary); color: var(--color-muted); padding: 0.7rem; border-radius: 8px; font-size: 0.88rem; font-weight: normal; cursor: pointer; transition: border-color 0.15s, color 0.15s; margin-top: 0.5rem; }
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
@@ -160,8 +183,7 @@
<div class="main" id="setupView" style="display:none;">
<div class="content">
<h1>BDSM Game</h1>
<p id="pageSubtitle" style="margin-bottom:1.5rem;">Session einrichten</p>
<h1>BDSM Game - Session einrichten</h1>
<!-- Accordion 1: Grundeinstellungen -->
<div class="acc-item">
@@ -212,6 +234,9 @@
</button>
<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/bdsm/aufgaben.html" target="_blank" style="color:var(--color-primary);">Aufgaben-Verwaltung (BDSM)</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>
<ul class="gruppe-list" id="listOwn"><li class="empty-hint">Wird geladen…</li></ul>
@@ -281,11 +306,11 @@
DIVERS: ['MUND','ANUS','UMSCHNALLDILDO'],
};
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
{ value: 'MUND', label: 'Oral', desc: 'Ist für aktiven Oral-Verkehr' },
{ value: 'ANUS', label: 'Anal', desc: 'Ist Bereit passiv Anal-Verkehr zu haben ' },
];
const ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv', AUFGABE_PASSIV: 'Aufgabe Passiv',
@@ -417,10 +442,19 @@
return items.map(({ value, label, desc }) => `
<label class="check-item${disabled ? ' is-disabled' : ''}">
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
<span><span class="check-item-label">${label}</span>${desc ? `<span class="check-item-desc">${desc}</span>` : ''}</span>
<span><span class="check-item-label">${label}${desc ? `<button type="button" class="check-item-info-btn" onclick="event.stopPropagation();toggleCheckDesc(this);">ⓘ</button>` : ''}</span>${desc ? `<span class="check-item-tooltip">${desc}</span><span class="check-item-desc-mobile">${desc}</span>` : ''}</span>
</label>`).join('');
}
function toggleCheckDesc(btn) {
const mobile = btn.closest('.check-item')?.querySelector('.check-item-desc-mobile');
if (!mobile) return;
const isVisible = mobile.style.display === 'block';
document.querySelectorAll('.check-item-desc-mobile').forEach(el => { el.style.display = 'none'; });
document.querySelectorAll('.check-item-info-btn').forEach(el => el.classList.remove('active'));
if (!isVisible) { mobile.style.display = 'block'; btn.classList.add('active'); }
}
function buildPlayerBody(id, nameValue, nameReadOnly = false, genderDisabled = false, allDisabled = false) {
const globalDefault = document.getElementById('chkZeitstrafen')?.checked ?? true;
const isCheckedCls = globalDefault ? ' is-checked' : '';
@@ -650,15 +684,15 @@
const selectAllWrap = section?.querySelector('.select-all-label');
if (!gruppen.length) {
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden'; return;
if (selectAllWrap) { const cb = selectAllWrap.querySelector('input'); if (cb) cb.disabled = true; selectAllWrap.style.pointerEvents = 'none'; selectAllWrap.style.opacity = '0.4'; } return;
}
ul.innerHTML = gruppen.map(g => {
const checked = savedGruppen.has(g.gruppenId);
const vanillaBadge = g.vanillaAvailable ? '<span class="vanilla-badge">Vanilla</span>' : '';
const vanillaBadge = g.availableIn === 'BDSM_AND_VANILLA' ? '<span class="vanilla-badge">Vanilla</span>' : '';
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
<span><span class="gruppe-item-name">${g.name}${vanillaBadge}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
<span><span class="gruppe-item-name">${g.name}${vanillaBadge}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
</label></li>`;
}).join('');
updateSelectAll(ul);
@@ -1033,8 +1067,7 @@
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || !geschlecht.length || !spieltMit.length || !rollen.length || !werkzeuge.length) valid = false;
if (!name || !geschlecht.length || !spieltMit.length || !rollen.length) valid = false;
const sperre = document.getElementById(`p${id}-sperrenAufloesen`);
return { name, geschlecht: geschlecht[0] || null, spieltMit, rollen, werkzeuge,
userId: inv ? inv.inviteeId : (id === selfPlayerId ? myUserId : null),
@@ -1196,8 +1229,7 @@
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!geschlecht.length || !spieltMit.length || !rollen.length || !werkzeuge.length) {
if (!geschlecht.length || !spieltMit.length || !rollen.length) {
showMessage('Bitte alle Felder ausfüllen.', 'error'); return;
}
const sperre = document.getElementById(`p${id}-sperrenAufloesen`);
@@ -1482,6 +1514,13 @@
}
init();
const _sessBc = new BroadcastChannel('bdsm-gruppen-updated');
_sessBc.onmessage = () => {
document.querySelectorAll('.gruppe-list input[type="checkbox"]:checked').forEach(cb => savedGruppen.add(cb.value));
document.querySelectorAll('.gruppe-list input[type="checkbox"]:not(:checked)').forEach(cb => savedGruppen.delete(cb.value));
ladeGruppenListen();
};
</script>
</body>
</html>

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta http-equiv="refresh" content="0;url=/games/aufgaben/toys.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toys xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">

View File

@@ -57,27 +57,90 @@
}
.nextcard-cards {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
align-items: center;
gap: 0.5rem;
position: relative;
border-radius: 6px;
padding: 0.75rem 0.5rem 0.5rem;
overflow: visible;
padding: 0.75rem 0 1rem;
}
.nc-window {
flex: 1;
overflow-x: hidden;
overflow-y: visible;
min-width: 0;
padding-top: 14px;
position: relative;
}
.nc-slide-wrapper {
display: flex;
will-change: transform;
}
.nextcard-card-img {
width: calc((100% - 5 * 0.6rem) / 6);
flex-shrink: 0;
width: 130px;
height: auto;
border-radius: 6px;
position: relative;
z-index: 1;
margin-right: var(--nc-gap, 6px);
transition: transform 0.15s, box-shadow 0.15s;
}
.nextcard-card-img:last-child { margin-right: 0; }
.nextcard-panel.drawable .nextcard-card-img:hover {
transform: translateY(-8px) scale(1.06);
transform: translateY(-10px) scale(1.08);
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
z-index: 10;
cursor: pointer;
}
.nc-nav-btn {
flex-shrink: 0;
width: 48px;
background: var(--color-secondary);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 8px;
color: var(--color-text);
font-size: 0.72rem;
font-weight: 600;
padding: 0.5rem 0.2rem;
cursor: pointer;
text-align: center;
line-height: 1.4;
align-self: stretch;
display: none;
transition: background 0.15s;
}
.nc-nav-btn:hover { background: var(--color-primary); }
/* ── Touch-Karussell ── */
.nextcard-cards.carousel-mode {
overflow: hidden;
height: 210px;
padding: 1rem 0 1rem;
justify-content: center;
}
.carousel-card {
position: absolute;
width: 100px;
border-radius: 8px;
box-shadow: 2px 4px 14px rgba(0,0,0,0.55);
transition: transform 0.18s cubic-bezier(.4,0,.2,1), opacity 0.18s;
user-select: none;
-webkit-user-drag: none;
}
.carousel-card.pos-center { transform: translateX(0) scale(1.18); opacity: 1; z-index: 5; }
.carousel-card.pos-left { transform: translateX(-120px) scale(0.84); opacity: 0.72; z-index: 3; }
.carousel-card.pos-right { transform: translateX(120px) scale(0.84); opacity: 0.72; z-index: 3; }
.carousel-card.pos-far-left { transform: translateX(-210px) scale(0.68); opacity: 0.4; z-index: 1; }
.carousel-card.pos-far-right { transform: translateX(210px) scale(0.68); opacity: 0.4; z-index: 1; }
.carousel-card.pos-exit-left {
transform: translateX(-400px) scale(0.4) rotate(-15deg);
opacity: 0; z-index: 0;
transition: transform 0.32s ease-in, opacity 0.32s ease-in;
}
.carousel-card.pos-exit-right {
transform: translateX(400px) scale(0.4) rotate(15deg);
opacity: 0; z-index: 0;
transition: transform 0.32s ease-in, opacity 0.32s ease-in;
}
.nextcard-panel.drawable .carousel-card.pos-center { cursor: pointer; }
.nextcard-overlay {
position: absolute;
inset: 0;
@@ -595,6 +658,12 @@
<button class="btn-hygiene" id="hygieneBtn" style="display:none;" onclick="openHygieneModal()">🚿 Hygiene-Öffnung</button>
</div>
<!-- Speed-Effekt-Panel -->
<div id="speedPanel" style="display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;padding:0.85rem 1.1rem;gap:0.35rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--color-muted);" id="speedPanelTitle">Slow Motion aktiv</div>
<div style="font-size:0.9rem;font-weight:600;" id="speedPanelInfo"></div>
</div>
<!-- Verifikations-Panel -->
<div class="verification-panel" id="verificationPanel" style="display:none;">
<div class="verification-panel-title">Tägliche Verifikation</div>
@@ -724,6 +793,12 @@
<span id="drawTaskPendingText"></span>
</div>
<!-- Speed-Karte: Zeitpunkt wählen -->
<div id="drawSpeedPicker" style="display:none;margin-top:0.75rem;padding:0.75rem 1rem;border-radius:8px;background:rgba(100,149,237,0.10);border:1px solid rgba(100,149,237,0.3);gap:0.6rem;text-align:center;">
<div style="font-size:0.88rem;color:var(--color-text);">Wähle den Zeitpunkt, bis zu dem der Effekt aktiv sein soll:</div>
<input type="datetime-local" id="drawSpeedUntilInput" style="background:var(--color-secondary);border:1px solid var(--color-secondary);border-radius:7px;padding:0.45rem 0.75rem;color:var(--color-text);font-size:0.9rem;width:100%;box-sizing:border-box;">
</div>
<!-- Grüne Karte: Entscheidung -->
<div class="draw-green-choice" id="drawGreenChoice">
<p id="drawGreenText" style="text-align:center;font-size:0.88rem;color:var(--color-muted);margin:0;">
@@ -735,6 +810,8 @@
<div class="draw-modal-actions" id="drawModalActions" style="display:none;">
<!-- Non-green: OK -->
<button class="btn-draw-ok" id="btnDrawOk" onclick="closeDrawModal()">OK</button>
<!-- Speed-Karte: Bestätigen -->
<button class="btn-draw-ok" id="btnSpeedConfirm" style="display:none;" onclick="confirmSpeedCard()">✓ Bestätigen</button>
<!-- Green: zwei Optionen -->
<button class="btn-draw-unlock" id="btnDrawUnlock" style="display:none;" onclick="confirmUnlock()">🔓 Entsperren</button>
<button class="btn-draw-keep" id="btnDrawKeep" style="display:none;" onclick="keepGreenCard()">Zurücklegen</button>
@@ -833,6 +910,7 @@
renderAssignedTasks(lock);
renderNextCardPanel(lock);
renderHygienePanel(lock);
renderSpeedPanel(lock);
renderVerificationPanel(lock);
renderTempOpeningPanel(lock);
renderCardsPanel(lock);
@@ -1095,17 +1173,14 @@
panel.style.display = '';
panel.classList.remove('drawable');
// Karten-Bilder rendern (Overlay bleibt erhalten)
cardsDiv.querySelectorAll('.nextcard-card-img').forEach(el => el.remove());
const total = lock.totalCards || 0;
const show = Math.min(total, 36);
for (let i = 0; i < show; i++) {
const img = document.createElement('img');
img.className = 'nextcard-card-img';
img.src = '/img/card.png';
img.alt = 'Karte';
cardsDiv.insertBefore(img, overlay);
}
// Karten-Darstellung aufbauen
if (ncResizeObs) { ncResizeObs.disconnect(); ncResizeObs = null; }
cardsDiv.querySelectorAll('.nc-nav-btn, .nc-window, .carousel-card').forEach(el => el.remove());
cardsDiv.classList.remove('carousel-mode');
const total = lock.totalCards || 0;
const isTouch = window.matchMedia('(hover: none) and (pointer: coarse)').matches;
if (isTouch) initCarousel(cardsDiv, overlay, total);
else initCardWindow(cardsDiv, overlay, total);
// Overlay-Elemente
const timerBox = document.getElementById('nextcardTimerBox');
@@ -1314,21 +1389,254 @@
// ── Karte ziehen ──
let drawnUnlockCode = null;
function enableCardClick() {
document.querySelectorAll('.nextcard-card-img').forEach(img => {
img.addEventListener('click', onCardClick, { once: true });
// ── Touch-Karussell ──
const CAROUSEL_POS = ['pos-far-left', 'pos-left', 'pos-center', 'pos-right', 'pos-far-right'];
let carouselCards = [];
let carouselShifting = false;
let carouselTouchX = 0;
let carouselTouchT = 0;
let carouselAbort = null;
let carouselDrawable = false;
function initCarousel(cardsDiv, overlay, total) {
if (carouselAbort) carouselAbort.abort();
carouselAbort = new AbortController();
const signal = carouselAbort.signal;
carouselCards = [];
carouselShifting = false;
carouselDrawable = false;
cardsDiv.classList.add('carousel-mode');
for (let i = 0; i < 5; i++) {
const img = document.createElement('img');
img.className = 'carousel-card ' + CAROUSEL_POS[i];
img.src = '/img/card.png';
img.alt = 'Karte';
cardsDiv.insertBefore(img, overlay);
carouselCards.push(img);
}
cardsDiv.addEventListener('touchstart', e => {
carouselTouchX = e.touches[0].clientX;
carouselTouchT = Date.now();
}, { passive: true, signal });
cardsDiv.addEventListener('touchend', e => {
if (carouselShifting) return;
const touch = e.changedTouches[0];
const dx = touch.clientX - carouselTouchX;
const dt = Date.now() - carouselTouchT;
const absDx = Math.abs(dx);
if (absDx < 5) {
// Tap: Element unter dem Finger bestimmen
const el = document.elementFromPoint(touch.clientX, touch.clientY);
const idx = el ? carouselCards.indexOf(el) : -1;
if (idx < 0) return;
if (idx === 2 && carouselDrawable) {
// Mittlere Karte → Karte ziehen
e.preventDefault();
carouselDrawable = false;
carouselCards.forEach(c => c.style.pointerEvents = 'none');
openDrawModal();
} else if (idx < 2) {
_doCarouselShift('right', 1);
} else {
_doCarouselShift('left', 1);
}
return;
}
if (!carouselDrawable) return;
// Swipe erkannt: Geschwindigkeit bestimmt wie viele Karten wechseln
const velocity = absDx / dt;
const steps = velocity > 1.2 ? 3 : velocity > 0.6 ? 2 : 1;
dx < 0 ? _doCarouselShift('left', steps) : _doCarouselShift('right', steps);
}, { passive: false, signal });
}
function shiftCarouselLeft(steps = 1) { _doCarouselShift('left', steps); }
function shiftCarouselRight(steps = 1) { _doCarouselShift('right', steps); }
function _doCarouselShift(dir, remaining) {
if (carouselShifting && remaining === /* first call */ remaining) {/* skip guard for chained */}
carouselShifting = true;
if (dir === 'left') {
carouselCards[0].className = 'carousel-card pos-exit-left';
for (let i = 1; i < 5; i++) carouselCards[i].className = 'carousel-card ' + CAROUSEL_POS[i - 1];
} else {
carouselCards[4].className = 'carousel-card pos-exit-right';
for (let i = 3; i >= 0; i--) carouselCards[i].className = 'carousel-card ' + CAROUSEL_POS[i + 1];
}
setTimeout(() => {
if (dir === 'left') {
const ex = carouselCards.shift();
ex.style.transition = 'none';
ex.className = 'carousel-card pos-exit-right';
ex.getBoundingClientRect();
ex.style.transition = '';
ex.className = 'carousel-card pos-far-right';
carouselCards.push(ex);
} else {
const ex = carouselCards.pop();
ex.style.transition = 'none';
ex.className = 'carousel-card pos-exit-left';
ex.getBoundingClientRect();
ex.style.transition = '';
ex.className = 'carousel-card pos-far-left';
carouselCards.unshift(ex);
}
if (remaining > 1) _doCarouselShift(dir, remaining - 1);
else carouselShifting = false;
}, 180);
}
// ── Karten-Fenster ──
let ncTotal = 0;
let ncWindowStart = 0;
let ncVisibleCount = 0;
let ncDrawable = false;
let ncWindowEl = null;
let ncBtnLeft = null;
let ncBtnRight = null;
let ncResizeObs = null;
function initCardWindow(cardsDiv, overlay, total) {
ncTotal = total;
ncDrawable = false;
ncWindowStart = 0;
if (total === 0) return;
ncBtnLeft = document.createElement('button');
ncBtnRight = document.createElement('button');
ncWindowEl = document.createElement('div');
const wrapper = document.createElement('div');
ncBtnLeft.className = 'nc-nav-btn';
ncBtnRight.className = 'nc-nav-btn';
ncWindowEl.className = 'nc-window';
wrapper.className = 'nc-slide-wrapper';
ncBtnLeft.addEventListener('click', () => scrollCardWindow(-1));
ncBtnRight.addEventListener('click', () => scrollCardWindow(1));
ncWindowEl.appendChild(wrapper);
cardsDiv.insertBefore(ncBtnLeft, overlay);
cardsDiv.insertBefore(ncWindowEl, overlay);
cardsDiv.insertBefore(ncBtnRight, overlay);
// Klick auf beliebige Karte → Karte ziehen (per Delegation)
ncWindowEl.addEventListener('click', e => {
if (!ncDrawable) return;
if (e.target.classList.contains('nextcard-card-img')) {
ncDrawable = false;
ncWindowEl.style.pointerEvents = 'none';
openDrawModal();
}
});
if (ncResizeObs) ncResizeObs.disconnect();
ncResizeObs = new ResizeObserver(() => recalcCardWindow());
ncResizeObs.observe(ncWindowEl);
requestAnimationFrame(() => recalcCardWindow(true));
}
function recalcCardWindow(initialCenter = false) {
const slots = Math.round(ncWindowEl.offsetWidth / 50);
const newCount = Math.min(ncTotal, Math.min(20, Math.max(3, slots)));
if (newCount === ncVisibleCount && !initialCenter) {
// Breite hat sich nicht verändert genug → nur Gap neu berechnen
renderCardWindow(null);
return;
}
ncVisibleCount = newCount;
if (initialCenter) {
ncWindowStart = Math.max(0, Math.floor(ncTotal / 2) - Math.floor(ncVisibleCount / 2));
} else {
// Fensterstart so anpassen, dass die Mitte des sichtbaren Bereichs gleich bleibt
const mid = ncWindowStart + Math.floor(ncVisibleCount / 2);
ncWindowStart = Math.max(0, Math.min(ncTotal - ncVisibleCount, mid - Math.floor(ncVisibleCount / 2)));
}
renderCardWindow(null);
}
function renderCardWindow(slideDir) {
const wrapper = ncWindowEl.querySelector('.nc-slide-wrapper');
const cardW = 130;
// Karten neu befüllen
wrapper.innerHTML = '';
for (let i = 0; i < ncVisibleCount; i++) {
const img = document.createElement('img');
img.className = 'nextcard-card-img';
img.src = '/img/card.png';
img.alt = 'Karte';
wrapper.appendChild(img);
}
// Dynamischer Overlap: alle sichtbaren Karten füllen die Fensterbreite
const windowW = ncWindowEl.offsetWidth;
let gap = ncVisibleCount <= 1 ? 6 : (windowW - ncVisibleCount * cardW) / (ncVisibleCount - 1);
gap = Math.max(-105, Math.min(12, gap));
ncWindowEl.style.setProperty('--nc-gap', gap + 'px');
// Animation: alte Karten raus, neue rein
if (slideDir) {
const oldWrapper = wrapper.cloneNode(true);
oldWrapper.style.cssText = 'position:absolute;top:0;left:0;width:100%;pointer-events:none;';
ncWindowEl.appendChild(oldWrapper);
// Neue Karten von der Seite einblenden
wrapper.style.transition = 'none';
wrapper.style.transform = `translateX(${slideDir > 0 ? '100%' : '-100%'})`;
wrapper.getBoundingClientRect();
wrapper.style.transition = 'transform 0.28s ease';
wrapper.style.transform = 'translateX(0)';
// Alte Karten zur anderen Seite ausblenden
oldWrapper.style.transition = 'transform 0.28s ease';
oldWrapper.style.transform = `translateX(${slideDir > 0 ? '-100%' : '100%'})`;
setTimeout(() => oldWrapper.remove(), 300);
}
// Nav-Buttons aktualisieren
const leftCount = ncWindowStart;
const rightCount = ncTotal - ncWindowStart - ncVisibleCount;
ncBtnLeft.style.display = leftCount > 0 ? 'block' : 'none';
ncBtnRight.style.display = rightCount > 0 ? 'block' : 'none';
ncBtnLeft.textContent = `\n${leftCount}`;
ncBtnRight.textContent = `${rightCount}\n`;
ncWindowEl.style.pointerEvents = '';
}
function scrollCardWindow(dir) {
const step = Math.max(1, Math.floor(ncVisibleCount / 2));
if (dir < 0) ncWindowStart = Math.max(0, ncWindowStart - step);
else ncWindowStart = Math.min(ncTotal - ncVisibleCount, ncWindowStart + step);
renderCardWindow(dir);
}
function enableCardClick() {
if (carouselCards.length > 0) {
carouselDrawable = true;
} else if (ncWindowEl) {
ncDrawable = true;
}
}
async function onCardClick() {
// Alle weiteren Klicks blockieren
document.querySelectorAll('.nextcard-card-img').forEach(img => {
img.removeEventListener('click', onCardClick);
img.style.pointerEvents = 'none';
});
carouselDrawable = false;
carouselCards.forEach(c => c.style.pointerEvents = 'none');
if (ncWindowEl) ncWindowEl.style.pointerEvents = 'none';
document.querySelectorAll('.nextcard-card-img').forEach(img => img.style.pointerEvents = 'none');
openDrawModal();
}
let _pendingSpeedMode = null;
function openDrawModal() {
const modal = document.getElementById('drawModal');
const inner = document.getElementById('flipCardInner');
@@ -1343,11 +1651,14 @@
document.getElementById('drawGreenText').style.display = '';
document.getElementById('drawUnlockCode').style.display = 'none';
document.getElementById('drawTaskPendingHint').style.display = 'none';
document.getElementById('drawSpeedPicker').style.display = 'none';
document.getElementById('btnDrawOk').style.display = '';
document.getElementById('btnSpeedConfirm').style.display = 'none';
document.getElementById('btnDrawUnlock').style.display = 'none';
document.getElementById('btnDrawKeep').style.display = 'none';
actions.style.display = 'none';
drawnUnlockCode = null;
_pendingSpeedMode = null;
modal.classList.add('open');
// Karte serverseitig ziehen
@@ -1386,6 +1697,20 @@
document.getElementById('btnDrawUnlock').style.display = '';
document.getElementById('btnDrawKeep').style.display = '';
}
if (dto.card === 'SLOWMO_CARD' || dto.card === 'SPEEDUP_CARD') {
_pendingSpeedMode = dto.card === 'SLOWMO_CARD' ? 'SLOWMO' : 'SPEEDUP';
const picker = document.getElementById('drawSpeedPicker');
const input = document.getElementById('drawSpeedUntilInput');
// Minimum: jetzt + 1 Stunde, Standardwert: jetzt + 24 Stunden
const minDate = new Date(Date.now() + 60 * 60 * 1000);
const defDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
input.min = toLocalDatetimeInputValue(minDate);
input.value = toLocalDatetimeInputValue(defDate);
picker.style.display = 'flex';
document.getElementById('btnDrawOk').style.display = 'none';
document.getElementById('btnSpeedConfirm').style.display = '';
}
}, 700);
}, 1000);
})
@@ -1418,6 +1743,56 @@
closeDrawModal();
}
function toLocalDatetimeInputValue(date) {
const pad = n => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
async function confirmSpeedCard() {
if (!_pendingSpeedMode) return;
const input = document.getElementById('drawSpeedUntilInput');
if (!input.value) { alert('Bitte wähle einen Zeitpunkt.'); return; }
const until = new Date(input.value);
if (until <= new Date()) { alert('Der Zeitpunkt muss in der Zukunft liegen.'); return; }
const isoUntil = `${input.value}:00`; // datetime-local hat kein Sekunden-Teil
const res = await fetch('/keyholder/cardlock/' + lockId + '/speed/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: _pendingSpeedMode, until: isoUntil })
});
if (!res.ok) { alert('Fehler beim Aktivieren des Speed-Effekts.'); return; }
closeDrawModal();
}
let speedPanelTick = null;
function renderSpeedPanel(lock) {
if (speedPanelTick) { clearInterval(speedPanelTick); speedPanelTick = null; }
const panel = document.getElementById('speedPanel');
const now = new Date();
const slowmoUntil = lock.slowmoUntil ? new Date(lock.slowmoUntil) : null;
const speedupUntil = lock.speedupUntil ? new Date(lock.speedupUntil) : null;
const active = (slowmoUntil && slowmoUntil > now) ? { mode: 'slowmo', until: slowmoUntil }
: (speedupUntil && speedupUntil > now) ? { mode: 'speedup', until: speedupUntil }
: null;
if (!active) { panel.style.display = 'none'; return; }
panel.style.display = 'flex';
document.getElementById('speedPanelTitle').textContent =
active.mode === 'slowmo' ? '🐢 Slow Motion aktiv' : '⚡ Speed Up aktiv';
function tickSpeed() {
const diff = active.until - Date.now();
if (diff <= 0) {
panel.style.display = 'none';
clearInterval(speedPanelTick); speedPanelTick = null;
return;
}
document.getElementById('speedPanelInfo').textContent =
(active.mode === 'slowmo' ? 'Aktionen dauern 4× so lange noch ' : 'Aktionen dauern 4× so kurz noch ') + fmtCountdown(diff);
}
tickSpeed();
speedPanelTick = setInterval(tickSpeed, 1000);
}
// ── Hygiene-Öffnung ──
let hygieneTickInterval = null;

View File

@@ -76,7 +76,7 @@
/* ── Detail-Modal ── */
.detail-backdrop {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.65); z-index: 400;
background: rgba(0,0,0,0.65); z-index: 500;
align-items: flex-start; justify-content: center;
padding: 2rem 1rem; overflow-y: auto;
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta http-equiv="refresh" content="0;url=/games/aufgaben/aufgaben.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aufgaben Vanilla xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
@@ -629,8 +630,13 @@
.then(user => { if (!user) return; loadUserGruppen(); loadAboGruppen(); loadSystemGruppen(); })
.catch(() => { window.location.href = '/login.html'; });
// ── Cross-tab notification ──
let _notifyOnLoad = false;
const gruppenBc = new BroadcastChannel('vanilla-gruppen-updated');
// ── Load ──
function loadUserGruppen() {
if (_notifyOnLoad) { _notifyOnLoad = false; try { gruppenBc.postMessage(1); } catch (_) {} }
resetSelection();
document.getElementById('userLoading').style.display = 'block';
fetch(apiUrl(`/gruppe/list/user`) + `?page=${userPage}&size=${PAGE_SIZE}`)
@@ -924,7 +930,7 @@
openItemId = null;
pendingExpandId = gruppenId;
pendingExpandType = 'user';
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else {
document.getElementById('userActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').';
}
@@ -1131,7 +1137,7 @@
pendingExpandType = 'user';
}
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else if (r.status === 409) {
showModalError('Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.');
} else {
@@ -1157,7 +1163,7 @@
.then(r => {
if (r.ok || r.status === 202) {
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else if (r.status === 403) {
document.getElementById('userActionError').textContent = 'Keine Berechtigung.';
btn.disabled = false;
@@ -1179,7 +1185,7 @@
.then(r => {
if (r.ok || r.status === 201) {
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
document.getElementById('systemActionError').textContent = '';
} else {
document.getElementById('systemActionError').textContent = 'Fehler beim Kopieren (HTTP ' + r.status + ').';
@@ -1198,7 +1204,7 @@
.then(r => {
if (r.ok || r.status === 201) {
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
document.getElementById('aboActionError').textContent = '';
} else if (r.status === 409) {
document.getElementById('aboActionError').textContent = 'Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.';
@@ -1628,7 +1634,7 @@
pendingExpandId = currentItemGruppeId;
pendingExpandType = 'user';
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else if (r.status === 409) {
showItemError('Limit erreicht: maximal 100 Einträge pro Gruppe möglich.');
} else {
@@ -1724,7 +1730,7 @@
pendingExpandId = selectedGruppeId;
pendingExpandType = 'user';
userPage = 0;
loadUserGruppen();
_notifyOnLoad = true; loadUserGruppen();
} else {
const errEl = document.getElementById('publishError');
errEl.textContent = 'Fehler beim Veröffentlichen (HTTP ' + r.status + ').';

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta http-equiv="refresh" content="0;url=/games/aufgaben/entdecken.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entdecken xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">

View File

@@ -69,13 +69,36 @@
.card-field:last-child { margin-bottom: 0; }
.card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; }
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
.check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; }
.check-group--two-col { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); }
.check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; position: relative; }
.check-item.is-checked { border-color: var(--color-primary); }
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; display: flex; align-items: center; gap: 0.2rem; flex-wrap: wrap; }
.check-item-desc { display: none; }
.check-item-tooltip {
display: none; position: absolute; bottom: calc(100% + 6px); left: 0;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 6px; padding: 0.4rem 0.65rem;
font-size: 0.78rem; color: var(--color-muted); line-height: 1.4;
width: max-content; max-width: 210px;
z-index: 50; pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.35);
}
.check-item:hover .check-item-tooltip { display: block; }
.check-item-info-btn {
display: none; background: none; border: 1px solid var(--color-muted);
border-radius: 50%; width: 1.1rem; height: 1.1rem; font-size: 0.62rem;
color: var(--color-muted); cursor: pointer; padding: 0; line-height: 1;
flex-shrink: 0; font-style: normal; font-weight: normal;
align-items: center; justify-content: center;
}
.check-item-info-btn.active { border-color: var(--color-primary); color: var(--color-primary); }
.check-item-desc-mobile { display: none; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.25rem; line-height: 1.4; }
@media (max-width: 679px) {
.check-item:hover .check-item-tooltip { display: none; }
.check-item-info-btn { display: inline-flex; }
}
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
.add-player-btn { width: 100%; background: transparent; border: 1px dashed var(--color-secondary); color: var(--color-muted); padding: 0.7rem; border-radius: 8px; font-size: 0.88rem; font-weight: normal; cursor: pointer; transition: border-color 0.15s, color 0.15s; margin-top: 0.5rem; }
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
@@ -163,7 +186,6 @@
<div class="main" id="setupView" style="display:none;">
<div class="content">
<h1>Vanilla Game Session einrichten</h1>
<p id="pageSubtitle" style="margin-bottom:1.5rem;">Session einrichten</p>
<!-- Accordion 1: Grundeinstellungen -->
<div class="acc-item">
@@ -191,7 +213,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/vanilla/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
Gruppen verwalten: <a href="/games/vanilla/aufgaben.html" target="_blank" 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>
@@ -262,11 +284,11 @@
DIVERS: ['MUND','ANUS','UMSCHNALLDILDO'],
};
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
{ value: 'MUND', label: 'Oral', desc: 'Ist für aktiven Oral-Verkehr' },
{ value: 'ANUS', label: 'Anal', desc: 'Ist Bereit passiv Anal-Verkehr zu haben ' },
];
const ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv', AUFGABE_PASSIV: 'Aufgabe Passiv',
@@ -370,10 +392,19 @@
return items.map(({ value, label, desc }) => `
<label class="check-item${disabled ? ' is-disabled' : ''}">
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
<span><span class="check-item-label">${label}</span>${desc ? `<span class="check-item-desc">${desc}</span>` : ''}</span>
<span><span class="check-item-label">${label}${desc ? `<button type="button" class="check-item-info-btn" onclick="event.stopPropagation();toggleCheckDesc(this);">ⓘ</button>` : ''}</span>${desc ? `<span class="check-item-tooltip">${desc}</span><span class="check-item-desc-mobile">${desc}</span>` : ''}</span>
</label>`).join('');
}
function toggleCheckDesc(btn) {
const mobile = btn.closest('.check-item')?.querySelector('.check-item-desc-mobile');
if (!mobile) return;
const isVisible = mobile.style.display === 'block';
document.querySelectorAll('.check-item-desc-mobile').forEach(el => { el.style.display = 'none'; });
document.querySelectorAll('.check-item-info-btn').forEach(el => el.classList.remove('active'));
if (!isVisible) { mobile.style.display = 'block'; btn.classList.add('active'); }
}
function buildPlayerBody(id, nameValue, nameReadOnly = false, genderDisabled = false, allDisabled = false) {
const nameHtml = nameReadOnly
? `<input type="text" id="p${id}-name" value="${nameValue}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`
@@ -574,14 +605,14 @@
);
if (!filtered.length) {
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden'; return;
if (selectAllWrap) { const cb = selectAllWrap.querySelector('input'); if (cb) cb.disabled = true; selectAllWrap.style.pointerEvents = 'none'; selectAllWrap.style.opacity = '0.4'; } return;
}
ul.innerHTML = filtered.map(g => {
const checked = savedGruppen.has(g.gruppenId);
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
<span><span class="gruppe-item-name">${g.name}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
<span><span class="gruppe-item-name">${g.name}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
</label></li>`;
}).join('');
updateSelectAll(ul);
@@ -905,8 +936,7 @@
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-name-err`, !name);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || !werkzeuge.length) valid = false;
if (!name) valid = false;
return { name, geschlecht: null, spieltMit: [], rollen: [], werkzeuge,
userId: inv ? inv.inviteeId : (id === selfPlayerId ? myUserId : null),
eigenesGeraet: false };
@@ -1035,10 +1065,6 @@
async function bereitMachen() {
const id = guestOwnPlayerId; if (!id) return;
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!werkzeuge.length) {
showMessage('Bitte mindestens ein Werkzeug auswählen.', 'error'); return;
}
try {
const res = await fetch(`/vanilla/einladung/${guestEinladungId}/spielerdaten`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
@@ -1338,6 +1364,13 @@
}
init();
const _sessBc = new BroadcastChannel('vanilla-gruppen-updated');
_sessBc.onmessage = () => {
document.querySelectorAll('.gruppe-list input[type="checkbox"]:checked').forEach(cb => savedGruppen.add(cb.value));
document.querySelectorAll('.gruppe-list input[type="checkbox"]:not(:checked)').forEach(cb => savedGruppen.delete(cb.value));
ladeGruppenListen();
};
</script>
</body>
</html>

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta http-equiv="refresh" content="0;url=/games/aufgaben/toys.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toys xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">

View File

@@ -0,0 +1,181 @@
<!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>Hilfe Abonnements xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header { margin-bottom: 2rem; }
.hilfe-header h1 { font-size: 1.6rem; margin: 0 0 0.4rem 0; }
.hilfe-header p { color: var(--color-muted); font-size: 0.92rem; margin: 0; line-height: 1.6; }
.hilfe-section { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 10px; margin-bottom: 0.75rem; overflow: hidden; }
.hilfe-section-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; cursor: pointer; user-select: none; transition: background 0.15s; }
.hilfe-section-header:hover { background: rgba(255,255,255,0.03); }
.hilfe-section-title { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; font-weight: 600; }
.hilfe-section-arrow { font-size: 0.75rem; color: var(--color-muted); transition: transform 0.2s; }
.hilfe-section.open .hilfe-section-arrow { transform: rotate(90deg); }
.hilfe-section-body { display: none; padding: 0 1.25rem 1.25rem; border-top: 1px solid var(--color-secondary); }
.hilfe-section.open .hilfe-section-body { display: block; }
.hilfe-section-body p { font-size: 0.9rem; color: var(--color-muted); line-height: 1.7; margin: 0.9rem 0 0; }
.hilfe-section-body p:first-child { margin-top: 1rem; }
.hilfe-hint { background: rgba(var(--color-primary-rgb, 120,80,200), 0.08); border-left: 3px solid var(--color-primary); border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-hint strong { color: var(--color-text); }
.hilfe-warn { background: rgba(231,76,60,0.08); border-left: 3px solid #e74c3c; border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-warn strong { color: #e74c3c; }
.hilfe-info { background: var(--color-secondary); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-top: 1rem; }
@media (max-width: 520px) { .feature-grid { grid-template-columns: 1fr; } }
.feature-col { background: var(--color-secondary); border-radius: 8px; padding: 0.85rem 1rem; }
.feature-col-title { font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.6rem; }
.feature-col-title.free { color: var(--color-muted); }
.feature-col-title.premium { color: var(--color-primary); }
.feature-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.feature-list li { font-size: 0.85rem; color: var(--color-muted); display: flex; align-items: flex-start; gap: 0.5rem; }
.feature-list li::before { content: '•'; color: var(--color-primary); flex-shrink: 0; }
.hilfe-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 1rem; }
.hilfe-table th { text-align: left; color: var(--color-text); font-weight: 600; padding: 0.4rem 0.75rem 0.4rem 0; border-bottom: 1px solid var(--color-secondary); }
.hilfe-table td { color: var(--color-muted); padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; line-height: 1.5; }
.hilfe-table tr:last-child td { border-bottom: none; }
.hilfe-badge { display: inline-block; background: var(--color-secondary); border-radius: 4px; padding: 0.1rem 0.45rem; font-size: 0.78rem; font-weight: 600; color: var(--color-muted); vertical-align: middle; }
.back-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-muted); text-decoration: none; margin-bottom: 1.25rem; transition: color 0.15s; }
.back-link:hover { color: var(--color-text); }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a href="/help/overview.html" class="back-link"> Zurück zur Hilfe-Übersicht</a>
<div class="hilfe-header">
<h1>💳 Abonnements</h1>
<p>Informationen zu Premium-Funktionen und wie du dein Abonnement verwaltest.</p>
</div>
<div class="hilfe-section open" id="sec-philosophy">
<div class="hilfe-section-header" onclick="toggleSection('sec-philosophy')">
<span class="hilfe-section-title">💡 Unser Ansatz</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
xXx Sphere soll niemanden reich machen. Die Abonnements dienen ausschließlich dazu, laufende Kosten wie Server, Datenbanken und externe API-Dienste zu decken.
</p>
<p>
Die wesentlichen Funktionen der Plattform Community, Spiele, Feed und Profile sind kostenlos nutzbar. Premium-Funktionen betreffen vor allem Hardware-Integrationen und erweiterte Automatisierungen.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-vergleich">
<div class="hilfe-section-header" onclick="toggleSection('sec-vergleich')">
<span class="hilfe-section-title">📋 Free vs. Premium</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<div class="feature-grid">
<div class="feature-col">
<div class="feature-col-title free">Kostenlos</div>
<ul class="feature-list">
<li>Chastity, BDSM und Vanilla Game</li>
<li>Unbegrenzte Aufgaben und Karten</li>
<li>Community, Gruppen, Feed</li>
<li>Direktnachrichten</li>
<li>Dating-Funktionen</li>
<li>Profil und Freunde</li>
<li>Community Votes</li>
</ul>
</div>
<div class="feature-col">
<div class="feature-col-title premium">Premium</div>
<ul class="feature-list">
<li>TTLock-Integration (automatische Code-Verwaltung)</li>
<li>Automatische Lock-Öffnung via API</li>
<li>Erweiterte Spielstatistiken</li>
<li>Priorität beim Community-Support</li>
</ul>
</div>
</div>
<div class="hilfe-hint">
<strong>Hinweis:</strong> Der Funktionsumfang der Abonnements befindet sich noch im Aufbau und wird laufend erweitert.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-verwalten">
<div class="hilfe-section-header" onclick="toggleSection('sec-verwalten')">
<span class="hilfe-section-title">⚙️ Abonnement verwalten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Dein aktives Abonnement findest und verwaltest du unter <strong>Community → Abonnements</strong>.
</p>
<table class="hilfe-table">
<thead><tr><th>Aktion</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Abschließen</span></td><td>Neues Abonnement aktivieren.</td></tr>
<tr><td><span class="hilfe-badge">Verlängern</span></td><td>Laufzeit manuell verlängern.</td></tr>
<tr><td><span class="hilfe-badge">Kündigen</span></td><td>Abonnement zum Ende der aktuellen Laufzeit kündigen.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Was passiert nach einer Kündigung?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Nach einer Kündigung bleibt dein Premium-Zugang bis zum Ende der bezahlten Laufzeit aktiv. Danach fallen die Premium-Funktionen weg deine Daten und dein Account bleiben erhalten.
</p>
<div class="hilfe-warn">
<strong>Achtung:</strong> Aktive TTLock-Verbindungen werden nach Ablauf des Abonnements deaktiviert. Laufende Locks werden davon nicht sofort beeinflusst, können aber nach dem Ablauf nicht mehr automatisch geöffnet werden.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Gibt es eine Testphase?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Derzeit gibt es keine automatische Testphase. Wende dich über <a href="/help/kontakt.html">Kontakt &amp; Feedback</a> an uns, wenn du Premium-Funktionen testen möchtest wir helfen gerne weiter.
</p>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -0,0 +1,180 @@
<!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>Hilfe BDSM Game xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header { margin-bottom: 2rem; }
.hilfe-header h1 { font-size: 1.6rem; margin: 0 0 0.4rem 0; }
.hilfe-header p { color: var(--color-muted); font-size: 0.92rem; margin: 0; line-height: 1.6; }
.hilfe-section { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 10px; margin-bottom: 0.75rem; overflow: hidden; }
.hilfe-section-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; cursor: pointer; user-select: none; transition: background 0.15s; }
.hilfe-section-header:hover { background: rgba(255,255,255,0.03); }
.hilfe-section-title { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; font-weight: 600; }
.hilfe-section-arrow { font-size: 0.75rem; color: var(--color-muted); transition: transform 0.2s; }
.hilfe-section.open .hilfe-section-arrow { transform: rotate(90deg); }
.hilfe-section-body { display: none; padding: 0 1.25rem 1.25rem; border-top: 1px solid var(--color-secondary); }
.hilfe-section.open .hilfe-section-body { display: block; }
.hilfe-section-body p { font-size: 0.9rem; color: var(--color-muted); line-height: 1.7; margin: 0.9rem 0 0; }
.hilfe-section-body p:first-child { margin-top: 1rem; }
.hilfe-steps { list-style: none; padding: 0; margin: 1rem 0 0; display: flex; flex-direction: column; gap: 0.75rem; }
.hilfe-steps li { list-style: none; display: flex; align-items: flex-start; gap: 0.85rem; font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; }
.hilfe-steps li::before { display: none; }
.hilfe-steps li .step-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--color-primary); color: #fff; font-size: 0.75rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; }
.hilfe-hint { background: rgba(var(--color-primary-rgb, 120,80,200), 0.08); border-left: 3px solid var(--color-primary); border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-hint strong { color: var(--color-text); }
.hilfe-warn { background: rgba(231,76,60,0.08); border-left: 3px solid #e74c3c; border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-warn strong { color: #e74c3c; }
.hilfe-info { background: var(--color-secondary); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 1rem; }
.hilfe-table th { text-align: left; color: var(--color-text); font-weight: 600; padding: 0.4rem 0.75rem 0.4rem 0; border-bottom: 1px solid var(--color-secondary); }
.hilfe-table td { color: var(--color-muted); padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; line-height: 1.5; }
.hilfe-table tr:last-child td { border-bottom: none; }
.hilfe-badge { display: inline-block; background: var(--color-secondary); border-radius: 4px; padding: 0.1rem 0.45rem; font-size: 0.78rem; font-weight: 600; color: var(--color-muted); vertical-align: middle; }
.back-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-muted); text-decoration: none; margin-bottom: 1.25rem; transition: color 0.15s; }
.back-link:hover { color: var(--color-text); }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a href="/help/overview.html" class="back-link"> Zurück zur Hilfe-Übersicht</a>
<div class="hilfe-header">
<h1>⛓️ BDSM Game</h1>
<p>Sessions erstellen, Spieler einladen und Aufgaben verwalten.</p>
</div>
<div class="hilfe-section open" id="sec-intro">
<div class="hilfe-section-header" onclick="toggleSection('sec-intro')">
<span class="hilfe-section-title">📖 Was ist das BDSM Game?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Das BDSM Game ermöglicht strukturierte Spielsessions zwischen einem <strong>Dom</strong> (Dominant) und einem oder mehreren <strong>Subs</strong> (Submissive). Der Dom erstellt die Session, legt Regeln und Aufgaben fest und überwacht den Verlauf.
</p>
<p>
Aufgaben können zufällig aus dem Aufgaben-Pool gezogen oder manuell vergeben werden. Jede Session hat einen definierten Anfang und ein definiertes Ende.
</p>
<div class="hilfe-hint">
<strong>Safe Word:</strong> Lege vor jeder Session ein Safe Word fest. Das Safe Word beendet die Session sofort und unbedingt unabhängig vom Spielstand.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-session">
<div class="hilfe-section-header" onclick="toggleSection('sec-session')">
<span class="hilfe-section-title">🚀 Session starten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>Als Dom startest du eine neue Session so:</p>
<ol class="hilfe-steps">
<li><span class="step-num">1</span><span>Navigiere zu <strong>BDSM → Neue Session</strong>.</span></li>
<li><span class="step-num">2</span><span>Vergib einen Session-Namen und lege die Intensität fest (<span class="hilfe-badge">Leicht</span> / <span class="hilfe-badge">Mittel</span> / <span class="hilfe-badge">Intensiv</span>).</span></li>
<li><span class="step-num">3</span><span>Wähle die Aufgaben-Sets, die während der Session aktiv sein sollen.</span></li>
<li><span class="step-num">4</span><span>Lade Mitspieler per Nutzername oder Einladungslink ein (siehe unten).</span></li>
<li><span class="step-num">5</span><span>Starte die Session. Alle eingeladenen Subs erhalten eine Benachrichtigung.</span></li>
</ol>
<div class="hilfe-warn">
<strong>Achtung:</strong> Eine laufende Session kann nur vom Dom beendet werden oder wenn das Safe Word ausgelöst wird.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-einladen">
<div class="hilfe-section-header" onclick="toggleSection('sec-einladen')">
<span class="hilfe-section-title">👥 Spieler einladen</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Spieler können auf zwei Wegen in eine Session eingeladen werden:
</p>
<table class="hilfe-table">
<thead><tr><th>Methode</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Nutzername</span></td><td>Gib den Nutzernamen direkt ein. Nur für registrierte Nutzer, die dir folgen oder mit dir befreundet sind.</td></tr>
<tr><td><span class="hilfe-badge">Einladungslink</span></td><td>Generiere einen einmalig verwendbaren Link. Jede Person mit dem Link kann beitreten (auch Nicht-Freunde).</td></tr>
</tbody>
</table>
<div class="hilfe-hint">
<strong>Tipp:</strong> Teile Einladungslinks nur über sichere Kanäle. Ein Link kann nur einmal verwendet werden und verfällt nach 24 Stunden.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-aufgaben">
<div class="hilfe-section-header" onclick="toggleSection('sec-aufgaben')">
<span class="hilfe-section-title">📋 Aufgaben verwalten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Aufgaben sind das Herzstück des BDSM Games. Eigene Aufgaben kannst du unter <strong>BDSM → Aufgaben</strong> anlegen und bearbeiten.
</p>
<p>
Während einer aktiven Session kann der Dom jederzeit eine neue Aufgabe vergeben. Der Sub muss die Aufgabe bestätigen oder Einwände erheben. Nicht erfüllte Aufgaben werden protokolliert.
</p>
<div class="hilfe-info">
Aufgaben können öffentlich als Vorlagen geteilt werden. Andere Nutzer können diese Vorlagen in ihre eigenen Sammlungen übernehmen.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Was passiert, wenn ein Sub die Session verlässt?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Verlässt ein Sub die Session vorzeitig (ohne Safe Word), wird dies als Verstoß protokolliert. Der Dom kann die Session dennoch fortsetzen, wenn weitere Subs teilnehmen. Bei nur einem Sub endet die Session automatisch.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Kann ich Aufgaben aus dem Community-Pool nutzen?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Ja. Unter <strong>BDSM → Entdecken</strong> findest du öffentlich geteilte Aufgaben-Sets der Community. Du kannst diese direkt in deine eigene Sammlung übernehmen und für Sessions verwenden.
</p>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -0,0 +1,213 @@
<!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>Hilfe Chastity Game xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header { margin-bottom: 2rem; }
.hilfe-header h1 { font-size: 1.6rem; margin: 0 0 0.4rem 0; }
.hilfe-header p { color: var(--color-muted); font-size: 0.92rem; margin: 0; line-height: 1.6; }
.hilfe-section { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 10px; margin-bottom: 0.75rem; overflow: hidden; }
.hilfe-section-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; cursor: pointer; user-select: none; transition: background 0.15s; }
.hilfe-section-header:hover { background: rgba(255,255,255,0.03); }
.hilfe-section-title { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; font-weight: 600; }
.hilfe-section-arrow { font-size: 0.75rem; color: var(--color-muted); transition: transform 0.2s; }
.hilfe-section.open .hilfe-section-arrow { transform: rotate(90deg); }
.hilfe-section-body { display: none; padding: 0 1.25rem 1.25rem; border-top: 1px solid var(--color-secondary); }
.hilfe-section.open .hilfe-section-body { display: block; }
.hilfe-section-body p { font-size: 0.9rem; color: var(--color-muted); line-height: 1.7; margin: 0.9rem 0 0; }
.hilfe-section-body p:first-child { margin-top: 1rem; }
.hilfe-steps { list-style: none; padding: 0; margin: 1rem 0 0; display: flex; flex-direction: column; gap: 0.75rem; }
.hilfe-steps li { list-style: none; display: flex; align-items: flex-start; gap: 0.85rem; font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; }
.hilfe-steps li::before { display: none; }
.hilfe-steps li .step-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--color-primary); color: #fff; font-size: 0.75rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; }
.hilfe-hint { background: rgba(var(--color-primary-rgb, 120,80,200), 0.08); border-left: 3px solid var(--color-primary); border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-hint strong { color: var(--color-text); }
.hilfe-warn { background: rgba(231,76,60,0.08); border-left: 3px solid #e74c3c; border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-warn strong { color: #e74c3c; }
.hilfe-info { background: var(--color-secondary); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 1rem; }
.hilfe-table th { text-align: left; color: var(--color-text); font-weight: 600; padding: 0.4rem 0.75rem 0.4rem 0; border-bottom: 1px solid var(--color-secondary); }
.hilfe-table td { color: var(--color-muted); padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; line-height: 1.5; }
.hilfe-table tr:last-child td { border-bottom: none; }
.hilfe-badge { display: inline-block; background: var(--color-secondary); border-radius: 4px; padding: 0.1rem 0.45rem; font-size: 0.78rem; font-weight: 600; color: var(--color-muted); vertical-align: middle; }
.back-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-muted); text-decoration: none; margin-bottom: 1.25rem; transition: color 0.15s; }
.back-link:hover { color: var(--color-text); }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a href="/help/overview.html" class="back-link"> Zurück zur Hilfe-Übersicht</a>
<div class="hilfe-header">
<h1>🔒 Chastity Game</h1>
<p>Alles rund um Locks, Keyholder, Karten und Aufgaben im Chastity Game.</p>
</div>
<div class="hilfe-section open" id="sec-intro">
<div class="hilfe-section-header" onclick="toggleSection('sec-intro')">
<span class="hilfe-section-title">📖 Was ist das Chastity Game?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Das Chastity Game ist ein interaktives Rollenspiel für zwei Personen: eine Person übernimmt die Rolle des <strong>Wearers</strong> (trägt das Gerät), die andere die des <strong>Keyholders</strong> (verwaltet den Schlüssel).
</p>
<p>
Der Keyholder bestimmt, wann das Lock geöffnet wird entweder nach einem festen Datum, durch das Ziehen von Karten, oder durch Community-Votes. Der Wearer kann Aufgaben erhalten, die den Verlauf des Spiels beeinflussen.
</p>
<div class="hilfe-hint">
<strong>Hinweis:</strong> Für die automatische Steuerung einer physischen TTLock-Schlüsselbox ist ein Premium-Abonnement erforderlich. Mehr dazu unter <a href="/help/ttlock.html">TTLock-Hilfe</a>.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-newlock">
<div class="hilfe-section-header" onclick="toggleSection('sec-newlock')">
<span class="hilfe-section-title">🚀 Neues Lock starten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>So startest du als Wearer ein neues Lock:</p>
<ol class="hilfe-steps">
<li><span class="step-num">1</span><span>Navigiere zu <strong>Chastity → Neues Lock</strong>.</span></li>
<li><span class="step-num">2</span><span>Vergib einen Namen für dein Lock (z. B. „Mein erstes Lock").</span></li>
<li><span class="step-num">3</span><span>Wähle einen Modus: <span class="hilfe-badge">Keyholder</span>, <span class="hilfe-badge">TimeLock</span> oder <span class="hilfe-badge">Community</span>.</span></li>
<li><span class="step-num">4</span><span>Lege optional Aufgaben und Karten fest, die den Verlauf beeinflussen.</span></li>
<li><span class="step-num">5</span><span>Bestätige mit <em>Lock starten</em>. Das Lock ist jetzt aktiv.</span></li>
</ol>
<div class="hilfe-warn">
<strong>Achtung:</strong> Ein aktives Lock kann nicht einfach abgebrochen werden. Stelle sicher, dass du und dein Keyholder sich über die Bedingungen einig sind.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-keyholder">
<div class="hilfe-section-header" onclick="toggleSection('sec-keyholder')">
<span class="hilfe-section-title">🗝️ Die Rolle als Keyholder</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Als Keyholder verwaltest du das Lock einer anderen Person. Du wirst per Einladungslink oder direkt über den Nutzernamen des Wearers zugewiesen.
</p>
<table class="hilfe-table">
<thead><tr><th>Aktion</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Öffnen</span></td><td>Gibt den Schlüssel / Öffnungscode frei. Bei TTLock wird der Code automatisch an die Box übermittelt.</td></tr>
<tr><td><span class="hilfe-badge">Verlängern</span></td><td>Fügt dem Lock zusätzliche Zeit hinzu.</td></tr>
<tr><td><span class="hilfe-badge">Aufgabe vergeben</span></td><td>Schickt dem Wearer eine neue Aufgabe. Nichterfüllung kann Strafzeit bedeuten.</td></tr>
<tr><td><span class="hilfe-badge">Verstoß melden</span></td><td>Protokolliert einen Regelverstoß und kann Zeit hinzufügen.</td></tr>
</tbody>
</table>
<div class="hilfe-hint">
<strong>Tipp:</strong> Unter <strong>Keyholder → Übersicht</strong> siehst du alle Locks, für die du verantwortlich bist, inklusive Laufzeit und offener Aufgaben.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-karten">
<div class="hilfe-section-header" onclick="toggleSection('sec-karten')">
<span class="hilfe-section-title">🃏 Karten und Aufgaben</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Karten sind ein zufälliges Element im Chastity Game. Beim Einrichten eines Locks kann ein Kartenstapel aktiviert werden. Der Wearer zieht in festgelegten Abständen eine Karte das Ergebnis kann die Laufzeit verlängern, verkürzen oder eine Aufgabe auslösen.
</p>
<table class="hilfe-table">
<thead><tr><th>Kartentyp</th><th>Auswirkung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">+Zeit</span></td><td>Fügt dem Lock zusätzliche Zeit hinzu (z. B. +1 Tag).</td></tr>
<tr><td><span class="hilfe-badge">Zeit</span></td><td>Verkürzt die verbleibende Laufzeit.</td></tr>
<tr><td><span class="hilfe-badge">Aufgabe</span></td><td>Löst eine zufällige Aufgabe aus, die erfüllt werden muss.</td></tr>
<tr><td><span class="hilfe-badge">Freeze</span></td><td>Laufzeit wird für einen definierten Zeitraum eingefroren.</td></tr>
<tr><td><span class="hilfe-badge">Reset</span></td><td>Setzt die Laufzeit auf den ursprünglichen Wert zurück.</td></tr>
</tbody>
</table>
<p>
Eigene Karten und Aufgaben kannst du unter <strong>Chastity → Aufgaben</strong> verwalten und den Vorlagen-Pool nach Belieben erweitern.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-timelock">
<div class="hilfe-section-header" onclick="toggleSection('sec-timelock')">
<span class="hilfe-section-title">⏱️ TimeLock erklärt</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Ein TimeLock läuft ohne Keyholder die Öffnung erfolgt automatisch, sobald der eingestellte Endzeitpunkt erreicht ist. Es gibt keinen manuellen Eingriff.
</p>
<div class="hilfe-warn">
<strong>Achtung:</strong> Bei einem TimeLock ohne Keyholder gibt es keine manuelle Freigabe vor Ablauf der Zeit. Plane daher immer eine Notfalloption ein (z. B. physischer Ersatzschlüssel an vertrauenswürdiger Person).
</div>
<div class="hilfe-hint">
<strong>Hinweis:</strong> Für die automatische TTLock-Öffnung zum Ablaufzeitpunkt muss die TTLock-Integration korrekt eingerichtet sein. Weitere Informationen: <a href="/help/ttlock.html">TTLock-Hilfe</a>.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Kann ich ein Lock vorzeitig beenden?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Ein Lock kann nur dann vorzeitig beendet werden, wenn der Keyholder die Freigabe erteilt. Bei einem TimeLock ohne Keyholder ist dies nicht möglich außer über einen physischen Ersatzschlüssel.
</p>
<div class="hilfe-info">
Im Notfall steht die TTLock-Notfallöffnung zur Verfügung. Mehr dazu: <a href="/help/ttlock.html#sec-faq2">TTLock Notfall-Öffnung</a>.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Wie finde ich einen Keyholder?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Unter <strong>Chastity → Keyholder finden</strong> kannst du nach registrierten Keyholdern suchen und eine Anfrage stellen. Der Keyholder muss die Anfrage bestätigen, bevor das Lock aktiv wird.
</p>
<p>
Alternativ kannst du deinen Keyholder direkt über seinen Nutzernamen einladen, wenn ihr euch bereits kennt.
</p>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -0,0 +1,208 @@
<!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>Hilfe Community xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header { margin-bottom: 2rem; }
.hilfe-header h1 { font-size: 1.6rem; margin: 0 0 0.4rem 0; }
.hilfe-header p { color: var(--color-muted); font-size: 0.92rem; margin: 0; line-height: 1.6; }
.hilfe-section { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 10px; margin-bottom: 0.75rem; overflow: hidden; }
.hilfe-section-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; cursor: pointer; user-select: none; transition: background 0.15s; }
.hilfe-section-header:hover { background: rgba(255,255,255,0.03); }
.hilfe-section-title { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; font-weight: 600; }
.hilfe-section-arrow { font-size: 0.75rem; color: var(--color-muted); transition: transform 0.2s; }
.hilfe-section.open .hilfe-section-arrow { transform: rotate(90deg); }
.hilfe-section-body { display: none; padding: 0 1.25rem 1.25rem; border-top: 1px solid var(--color-secondary); }
.hilfe-section.open .hilfe-section-body { display: block; }
.hilfe-section-body p { font-size: 0.9rem; color: var(--color-muted); line-height: 1.7; margin: 0.9rem 0 0; }
.hilfe-section-body p:first-child { margin-top: 1rem; }
.hilfe-steps { list-style: none; padding: 0; margin: 1rem 0 0; display: flex; flex-direction: column; gap: 0.75rem; }
.hilfe-steps li { list-style: none; display: flex; align-items: flex-start; gap: 0.85rem; font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; }
.hilfe-steps li::before { display: none; }
.hilfe-steps li .step-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--color-primary); color: #fff; font-size: 0.75rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; }
.hilfe-hint { background: rgba(var(--color-primary-rgb, 120,80,200), 0.08); border-left: 3px solid var(--color-primary); border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-hint strong { color: var(--color-text); }
.hilfe-warn { background: rgba(231,76,60,0.08); border-left: 3px solid #e74c3c; border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-warn strong { color: #e74c3c; }
.hilfe-info { background: var(--color-secondary); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 1rem; }
.hilfe-table th { text-align: left; color: var(--color-text); font-weight: 600; padding: 0.4rem 0.75rem 0.4rem 0; border-bottom: 1px solid var(--color-secondary); }
.hilfe-table td { color: var(--color-muted); padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; line-height: 1.5; }
.hilfe-table tr:last-child td { border-bottom: none; }
.hilfe-badge { display: inline-block; background: var(--color-secondary); border-radius: 4px; padding: 0.1rem 0.45rem; font-size: 0.78rem; font-weight: 600; color: var(--color-muted); vertical-align: middle; }
.back-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-muted); text-decoration: none; margin-bottom: 1.25rem; transition: color 0.15s; }
.back-link:hover { color: var(--color-text); }
.hilfe-section-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-muted); margin: 1.75rem 0 0.75rem; }
.hilfe-section-label:first-of-type { margin-top: 0; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a href="/help/overview.html" class="back-link"> Zurück zur Hilfe-Übersicht</a>
<div class="hilfe-header">
<h1>🌐 Community</h1>
<p>Gruppen, Feed, Profile und Community-Votes alles, was die xXx-Sphere-Community zusammenhält.</p>
</div>
<div class="hilfe-section-label">Gruppen</div>
<div class="hilfe-section open" id="sec-gruppen-intro">
<div class="hilfe-section-header" onclick="toggleSection('sec-gruppen-intro')">
<span class="hilfe-section-title">👥 Was sind Gruppen?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Gruppen sind private oder öffentliche Räume für Gleichgesinnte. Mitglieder können Beiträge teilen, Abstimmungen starten und sich austauschen. Jede Gruppe wird von einem oder mehreren Admins verwaltet.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-gruppe-erstellen">
<div class="hilfe-section-header" onclick="toggleSection('sec-gruppe-erstellen')">
<span class="hilfe-section-title">🚀 Gruppe erstellen</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<ol class="hilfe-steps">
<li><span class="step-num">1</span><span>Navigiere zu <strong>Community → Gruppen</strong>.</span></li>
<li><span class="step-num">2</span><span>Klicke auf <em>Neue Gruppe</em> und vergib Name, Beschreibung und Bild.</span></li>
<li><span class="step-num">3</span><span>Wähle die Sichtbarkeit: <span class="hilfe-badge">Öffentlich</span> (jeder kann beitreten) oder <span class="hilfe-badge">Privat</span> (nur auf Einladung).</span></li>
<li><span class="step-num">4</span><span>Bestätige mit <em>Erstellen</em>. Du bist automatisch Admin der neuen Gruppe.</span></li>
</ol>
</div>
</div>
<div class="hilfe-section" id="sec-mitglieder">
<div class="hilfe-section-header" onclick="toggleSection('sec-mitglieder')">
<span class="hilfe-section-title">⚙️ Mitglieder verwalten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>Als Admin einer Gruppe kannst du:</p>
<table class="hilfe-table">
<thead><tr><th>Aktion</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Einladen</span></td><td>Nutzer per Nutzername direkt einladen.</td></tr>
<tr><td><span class="hilfe-badge">Entfernen</span></td><td>Mitglieder aus der Gruppe ausschließen.</td></tr>
<tr><td><span class="hilfe-badge">Moderator</span></td><td>Mitglieder zu Moderatoren befördern (können Beiträge entfernen).</td></tr>
<tr><td><span class="hilfe-badge">Admin</span></td><td>Admin-Rechte an ein anderes Mitglied übertragen oder teilen.</td></tr>
</tbody>
</table>
<div class="hilfe-warn">
<strong>Achtung:</strong> Wenn du als letzter Admin eine Gruppe verlässt, wird die Gruppe aufgelöst.
</div>
</div>
</div>
<div class="hilfe-section-label">Feed &amp; Profil</div>
<div class="hilfe-section" id="sec-feed">
<div class="hilfe-section-header" onclick="toggleSection('sec-feed')">
<span class="hilfe-section-title">📰 Feed nutzen</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Der Feed zeigt dir Beiträge von Personen, denen du folgst, sowie Aktivitäten aus deinen Gruppen. Du kannst Beiträge liken, kommentieren und teilen.
</p>
<p>
Eigene Beiträge erstellst du über das -Symbol im Feed. Du kannst Text, Bilder und Links teilen. Beiträge können öffentlich, für Freunde oder nur für Gruppenmitglieder sichtbar sein.
</p>
<div class="hilfe-hint">
<strong>Tipp:</strong> Über die Filter-Option kannst du den Feed auf bestimmte Gruppen oder Personen einschränken.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-profil">
<div class="hilfe-section-header" onclick="toggleSection('sec-profil')">
<span class="hilfe-section-title">👤 Profil gestalten &amp; Personen folgen</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Dein Profil ist deine öffentliche Visitenkarte in der Community. Du kannst ein Profilbild, einen Anzeigenamen, eine kurze Bio und deine Interessen hinterlegen.
</p>
<p>
Um jemandem zu folgen, besuche sein Profil und klicke auf <em>Folgen</em>. Du siehst dann seine öffentlichen Beiträge in deinem Feed. Alternativ kannst du eine Freundschaftsanfrage senden, um auch nicht-öffentliche Inhalte zu sehen.
</p>
<div class="hilfe-info">
Andere Nutzer findest du über die Suchfunktion oder unter <strong>Community → Nutzer entdecken</strong>.
</div>
</div>
</div>
<div class="hilfe-section-label">Community Votes</div>
<div class="hilfe-section" id="sec-votes">
<div class="hilfe-section-header" onclick="toggleSection('sec-votes')">
<span class="hilfe-section-title">🏆 Wie funktionieren Community Votes?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Community Votes sind eine besondere Spielmechanik im Chastity Game: Wenn ein Lock im Modus <span class="hilfe-badge">Community</span> läuft, entscheidet die gesamte Community mit, ob das Lock geöffnet wird oder die Zeit verlängert wird.
</p>
<p>
Andere Nutzer können für <em>Öffnen</em> oder <em>Verlängern</em> abstimmen. Nach Ablauf der Abstimmungszeit gewinnt die Mehrheit das Ergebnis wird automatisch auf das Lock angewandt.
</p>
<div class="hilfe-hint">
<strong>Hinweis:</strong> Nur verifizierte Nutzer können an Community Votes teilnehmen. Die Verifikation erfolgt über dein Profil.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-nachrichten">
<div class="hilfe-section-header" onclick="toggleSection('sec-nachrichten')">
<span class="hilfe-section-title">✉️ Nachrichten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Du kannst anderen Nutzern direkte Nachrichten senden. Navigiere dazu zu <strong>Community → Nachrichten</strong> oder klicke auf das Nachrichten-Symbol in einem Nutzerprofil.
</p>
<p>
Nachrichten sind Ende-zu-Ende zwischen dir und dem Empfänger sichtbar. Du kannst Konversationen stummschalten oder blockieren.
</p>
<div class="hilfe-warn">
<strong>Achtung:</strong> Unerwünschte Nachrichten bitte über den <em>Melden</em>-Button melden. Wiederholte Verstöße führen zu einer Account-Sperre.
</div>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -0,0 +1,168 @@
<!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>Hilfe Sicherheit & Datenschutz xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header { margin-bottom: 2rem; }
.hilfe-header h1 { font-size: 1.6rem; margin: 0 0 0.4rem 0; }
.hilfe-header p { color: var(--color-muted); font-size: 0.92rem; margin: 0; line-height: 1.6; }
.hilfe-section { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 10px; margin-bottom: 0.75rem; overflow: hidden; }
.hilfe-section-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; cursor: pointer; user-select: none; transition: background 0.15s; }
.hilfe-section-header:hover { background: rgba(255,255,255,0.03); }
.hilfe-section-title { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; font-weight: 600; }
.hilfe-section-arrow { font-size: 0.75rem; color: var(--color-muted); transition: transform 0.2s; }
.hilfe-section.open .hilfe-section-arrow { transform: rotate(90deg); }
.hilfe-section-body { display: none; padding: 0 1.25rem 1.25rem; border-top: 1px solid var(--color-secondary); }
.hilfe-section.open .hilfe-section-body { display: block; }
.hilfe-section-body p { font-size: 0.9rem; color: var(--color-muted); line-height: 1.7; margin: 0.9rem 0 0; }
.hilfe-section-body p:first-child { margin-top: 1rem; }
.hilfe-hint { background: rgba(var(--color-primary-rgb, 120,80,200), 0.08); border-left: 3px solid var(--color-primary); border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-hint strong { color: var(--color-text); }
.hilfe-warn { background: rgba(231,76,60,0.08); border-left: 3px solid #e74c3c; border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-warn strong { color: #e74c3c; }
.hilfe-info { background: var(--color-secondary); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 1rem; }
.hilfe-table th { text-align: left; color: var(--color-text); font-weight: 600; padding: 0.4rem 0.75rem 0.4rem 0; border-bottom: 1px solid var(--color-secondary); }
.hilfe-table td { color: var(--color-muted); padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; line-height: 1.5; }
.hilfe-table tr:last-child td { border-bottom: none; }
.hilfe-badge { display: inline-block; background: var(--color-secondary); border-radius: 4px; padding: 0.1rem 0.45rem; font-size: 0.78rem; font-weight: 600; color: var(--color-muted); vertical-align: middle; }
.back-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-muted); text-decoration: none; margin-bottom: 1.25rem; transition: color 0.15s; }
.back-link:hover { color: var(--color-text); }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a href="/help/overview.html" class="back-link"> Zurück zur Hilfe-Übersicht</a>
<div class="hilfe-header">
<h1>🔐 Sicherheit &amp; Datenschutz</h1>
<p>Wie deine Daten gespeichert werden und welche Sicherheitsmaßnahmen wir treffen.</p>
</div>
<div class="hilfe-section open" id="sec-grundsaetze">
<div class="hilfe-section-header" onclick="toggleSection('sec-grundsaetze')">
<span class="hilfe-section-title">📖 Unsere Grundsätze</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
xXx Sphere verarbeitet nur Daten, die für den Betrieb der Plattform notwendig sind. Es gibt keine Weitergabe an Werbepartner und keine Vermarktung von Nutzerdaten.
</p>
<p>
Alle sensiblen Daten werden verschlüsselt gespeichert oder gehasht. Der Zugriff auf Produktionsdaten ist auf das absolut notwendige Minimum beschränkt.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-gespeicherte-daten">
<div class="hilfe-section-header" onclick="toggleSection('sec-gespeicherte-daten')">
<span class="hilfe-section-title">🗄️ Was wird gespeichert?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<table class="hilfe-table">
<thead><tr><th>Datenkategorie</th><th>Speicherung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Account-Daten</span></td><td>Nutzername, E-Mail, Registrierungsdatum. E-Mail-Adresse wird nie öffentlich angezeigt.</td></tr>
<tr><td><span class="hilfe-badge">Profildaten</span></td><td>Anzeigename, Bio, Profilbild, Interessen. Sichtbarkeit ist konfigurierbar.</td></tr>
<tr><td><span class="hilfe-badge">Spielverläufe</span></td><td>Lock-Verläufe, Aufgabenprotokolle, Session-Daten. Werden nach Beendigung archiviert.</td></tr>
<tr><td><span class="hilfe-badge">Nachrichten</span></td><td>Direktnachrichten werden in der Datenbank gespeichert. Kein Ende-zu-Ende-Verschlüsselungsstandard aktuell.</td></tr>
<tr><td><span class="hilfe-badge">TTLock-Zugangsdaten</span></td><td>Benutzername im Klartext, Passwort als MD5-Hash (Anforderung der TTLock-API).</td></tr>
<tr><td><span class="hilfe-badge">Passwort</span></td><td>Wird als BCrypt-Hash gespeichert. Das Klartextpasswort ist für uns nicht einsehbar.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="hilfe-section" id="sec-passwort-hashing">
<div class="hilfe-section-header" onclick="toggleSection('sec-passwort-hashing')">
<span class="hilfe-section-title">🔒 Passwort-Hashing</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Dein Passwort wird beim Setzen sofort mit <strong>BCrypt</strong> gehasht. BCrypt ist ein adaptiver Hashing-Algorithmus mit integriertem Salt er macht Brute-Force-Angriffe rechnerisch aufwendig.
</p>
<p>
Das Klartextpasswort verlässt deinen Browser verschlüsselt per HTTPS und wird serverseitig niemals im Klartext gespeichert oder geloggt.
</p>
<div class="hilfe-hint">
<strong>Empfehlung:</strong> Verwende ein einzigartiges Passwort für xXx Sphere. Ein Passwort-Manager hilft dabei, starke Passwörter zu generieren und sicher zu speichern.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-verbindung">
<div class="hilfe-section-header" onclick="toggleSection('sec-verbindung')">
<span class="hilfe-section-title">🌐 Verbindungssicherheit</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Die gesamte Kommunikation zwischen deinem Browser und den Servern erfolgt ausschließlich über <strong>HTTPS</strong> (TLS). HTTP-Verbindungen werden automatisch auf HTTPS umgeleitet.
</p>
<div class="hilfe-info">
Session-Cookies sind als <em>HttpOnly</em> und <em>Secure</em> gesetzt, wodurch sie nicht per JavaScript ausgelesen werden können.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Kann ich meine Daten exportieren?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Ein automatischer Datenexport ist noch nicht verfügbar. Du kannst einen manuellen Export über <a href="/help/kontakt.html">Kontakt &amp; Feedback</a> anfordern wir stellen dir deine Daten dann im JSON-Format zur Verfügung.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Was passiert mit meinen Daten nach einer Konto-Löschung?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Nach einer bestätigten Account-Löschung werden alle personenbezogenen Daten innerhalb von 30 Tagen endgültig aus unseren Datenbanken gelöscht. Dazu gehören Profil, Nachrichten, Spielprotokolle und Einstellungen.
</p>
<div class="hilfe-warn">
<strong>Achtung:</strong> Öffentlich geteilte Inhalte (z. B. Beiträge in Gruppen) können bis zum Ablauf der 30 Tage noch sichtbar sein und werden danach zusammen mit deinem Account gelöscht.
</div>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -0,0 +1,194 @@
<!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>Hilfe Konto & Einstellungen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header { margin-bottom: 2rem; }
.hilfe-header h1 { font-size: 1.6rem; margin: 0 0 0.4rem 0; }
.hilfe-header p { color: var(--color-muted); font-size: 0.92rem; margin: 0; line-height: 1.6; }
.hilfe-section { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 10px; margin-bottom: 0.75rem; overflow: hidden; }
.hilfe-section-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; cursor: pointer; user-select: none; transition: background 0.15s; }
.hilfe-section-header:hover { background: rgba(255,255,255,0.03); }
.hilfe-section-title { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; font-weight: 600; }
.hilfe-section-arrow { font-size: 0.75rem; color: var(--color-muted); transition: transform 0.2s; }
.hilfe-section.open .hilfe-section-arrow { transform: rotate(90deg); }
.hilfe-section-body { display: none; padding: 0 1.25rem 1.25rem; border-top: 1px solid var(--color-secondary); }
.hilfe-section.open .hilfe-section-body { display: block; }
.hilfe-section-body p { font-size: 0.9rem; color: var(--color-muted); line-height: 1.7; margin: 0.9rem 0 0; }
.hilfe-section-body p:first-child { margin-top: 1rem; }
.hilfe-steps { list-style: none; padding: 0; margin: 1rem 0 0; display: flex; flex-direction: column; gap: 0.75rem; }
.hilfe-steps li { list-style: none; display: flex; align-items: flex-start; gap: 0.85rem; font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; }
.hilfe-steps li::before { display: none; }
.hilfe-steps li .step-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--color-primary); color: #fff; font-size: 0.75rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; }
.hilfe-hint { background: rgba(var(--color-primary-rgb, 120,80,200), 0.08); border-left: 3px solid var(--color-primary); border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-hint strong { color: var(--color-text); }
.hilfe-warn { background: rgba(231,76,60,0.08); border-left: 3px solid #e74c3c; border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-warn strong { color: #e74c3c; }
.hilfe-info { background: var(--color-secondary); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 1rem; }
.hilfe-table th { text-align: left; color: var(--color-text); font-weight: 600; padding: 0.4rem 0.75rem 0.4rem 0; border-bottom: 1px solid var(--color-secondary); }
.hilfe-table td { color: var(--color-muted); padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; line-height: 1.5; }
.hilfe-table tr:last-child td { border-bottom: none; }
.hilfe-badge { display: inline-block; background: var(--color-secondary); border-radius: 4px; padding: 0.1rem 0.45rem; font-size: 0.78rem; font-weight: 600; color: var(--color-muted); vertical-align: middle; }
.back-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-muted); text-decoration: none; margin-bottom: 1.25rem; transition: color 0.15s; }
.back-link:hover { color: var(--color-text); }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a href="/help/overview.html" class="back-link"> Zurück zur Hilfe-Übersicht</a>
<div class="hilfe-header">
<h1>⚙️ Konto &amp; Einstellungen</h1>
<p>Profil, Benachrichtigungen, Passwort und Datenschutz-Einstellungen verwalten.</p>
</div>
<div class="hilfe-section open" id="sec-profil">
<div class="hilfe-section-header" onclick="toggleSection('sec-profil')">
<span class="hilfe-section-title">👤 Profil bearbeiten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Dein Profil erreichst du unter <strong>Konto → Profil</strong>. Du kannst dort Anzeigename, Bio, Profilbild und Interessen anpassen.
</p>
<table class="hilfe-table">
<thead><tr><th>Feld</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Anzeigename</span></td><td>Sichtbarer Name in der Community. Kann jederzeit geändert werden.</td></tr>
<tr><td><span class="hilfe-badge">Bio</span></td><td>Kurze Beschreibung über dich (max. 300 Zeichen).</td></tr>
<tr><td><span class="hilfe-badge">Profilbild</span></td><td>JPG oder PNG, max. 5 MB. Wird kreisförmig zugeschnitten.</td></tr>
<tr><td><span class="hilfe-badge">Interessen</span></td><td>Tags, die anderen zeigen, was dich interessiert.</td></tr>
<tr><td><span class="hilfe-badge">Sichtbarkeit</span></td><td>Öffentlich, Freunde, oder Privat steuert, wer dein Profil sehen kann.</td></tr>
</tbody>
</table>
<div class="hilfe-hint">
<strong>Tipp:</strong> Ein vollständiges Profil erhöht deine Sichtbarkeit in der Community und erleichtert es anderen, dich zu finden.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-benachrichtigungen">
<div class="hilfe-section-header" onclick="toggleSection('sec-benachrichtigungen')">
<span class="hilfe-section-title">🔔 Benachrichtigungen konfigurieren</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Unter <strong>Einstellungen → Benachrichtigungen</strong> kannst du für jede Kategorie einzeln festlegen, ob du Benachrichtigungen erhalten möchtest.
</p>
<table class="hilfe-table">
<thead><tr><th>Kategorie</th><th>Beispiele</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Spiele</span></td><td>Lock-Ablauf, neue Aufgabe, Keyholder-Aktion</td></tr>
<tr><td><span class="hilfe-badge">Community</span></td><td>Neue Follower, Likes, Kommentare, Gruppen-Einladungen</td></tr>
<tr><td><span class="hilfe-badge">Nachrichten</span></td><td>Neue Direktnachricht</td></tr>
<tr><td><span class="hilfe-badge">System</span></td><td>Sicherheitshinweise, Account-Aktivitäten</td></tr>
</tbody>
</table>
</div>
</div>
<div class="hilfe-section" id="sec-passwort">
<div class="hilfe-section-header" onclick="toggleSection('sec-passwort')">
<span class="hilfe-section-title">🔑 Passwort ändern</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>So änderst du dein Passwort:</p>
<ol class="hilfe-steps">
<li><span class="step-num">1</span><span>Navigiere zu <strong>Einstellungen → Konto → Passwort ändern</strong>.</span></li>
<li><span class="step-num">2</span><span>Gib dein aktuelles Passwort ein.</span></li>
<li><span class="step-num">3</span><span>Gib dein neues Passwort ein und bestätige es.</span></li>
<li><span class="step-num">4</span><span>Klicke auf <em>Speichern</em>. Du wirst automatisch neu angemeldet.</span></li>
</ol>
<div class="hilfe-hint">
<strong>Passwort vergessen?</strong> Auf der Login-Seite findest du den Link <em>Passwort vergessen</em>. Du erhältst dann eine E-Mail mit einem Zurücksetzen-Link.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-datenschutz">
<div class="hilfe-section-header" onclick="toggleSection('sec-datenschutz')">
<span class="hilfe-section-title">🛡️ Datenschutz-Einstellungen</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Unter <strong>Einstellungen → Datenschutz</strong> kannst du steuern, welche Daten sichtbar sind und wie andere mit dir interagieren können.
</p>
<table class="hilfe-table">
<thead><tr><th>Einstellung</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Profil-Sichtbarkeit</span></td><td>Öffentlich, nur Freunde, oder privat.</td></tr>
<tr><td><span class="hilfe-badge">Nachrichten empfangen</span></td><td>Von allen, nur Freunden, oder niemanden.</td></tr>
<tr><td><span class="hilfe-badge">Aktivitäts-Status</span></td><td>Zeige anderen, wann du zuletzt aktiv warst.</td></tr>
<tr><td><span class="hilfe-badge">Im Dating sichtbar</span></td><td>Ob dein Profil in der Dating-Suche erscheint.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Wie kann ich meinen Account löschen?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Eine Account-Löschung kannst du über <strong>Einstellungen → Konto → Account löschen</strong> beantragen. Nach der Bestätigung werden alle deine Daten innerhalb von 30 Tagen endgültig gelöscht.
</p>
<div class="hilfe-warn">
<strong>Achtung:</strong> Aktive Locks und laufende Sessions werden beim Löschen sofort beendet. Diese Aktion ist nicht rückgängig zu machen.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Ich wurde nicht aktiviert was tun?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Nach der Registrierung erhältst du eine Aktivierungs-E-Mail. Prüfe zuerst deinen Spam-Ordner. Ist die E-Mail nicht auffindbar, kannst du auf der Login-Seite unter <em>Aktivierungsmail erneut senden</em> eine neue anfordern.
</p>
<div class="hilfe-info">
Aktivierungslinks sind 24 Stunden gültig. Danach muss ein neuer Link angefordert werden.
</div>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -107,9 +107,10 @@
<div class="hilfe-card-title">⚙️ Allgemeine Einstellungen</div>
<div class="hilfe-card-desc">Profil, Benachrichtigungen, Datenschutz und weitere Kontoeinstellungen.</div>
<div class="hilfe-card-links">
<a href="#">Profil bearbeiten</a>
<a href="#">Benachrichtigungen konfigurieren</a>
<a href="#">Passwort ändern</a>
<a href="/help/konto.html#sec-profil">Profil bearbeiten</a>
<a href="/help/konto.html#sec-benachrichtigungen">Benachrichtigungen konfigurieren</a>
<a href="/help/konto.html#sec-passwort">Passwort ändern</a>
<a href="/help/konto.html#sec-datenschutz">Datenschutz-Einstellungen</a>
</div>
</div>
<div class="hilfe-card">
@@ -127,8 +128,9 @@
<div class="hilfe-card-title">💳 Abonnements</div>
<div class="hilfe-card-desc">Informationen zu Premium-Funktionen und wie du dein Abonnement verwaltest.</div>
<div class="hilfe-card-links">
<a href="#">Premium-Funktionen im Überblick</a>
<a href="#">Abonnement kündigen</a>
<a href="/help/abonnements.html#sec-vergleich">Premium-Funktionen im Überblick</a>
<a href="/help/abonnements.html#sec-verwalten">Abonnement verwalten</a>
<a href="/help/abonnements.html#sec-faq1">Was passiert nach einer Kündigung?</a>
</div>
</div>
</div>
@@ -140,25 +142,28 @@
<div class="hilfe-card-title">🔒 Chastity Game</div>
<div class="hilfe-card-desc">Alles rund um Schlösser, Keyholder, Karten und Aufgaben im Chastity Game.</div>
<div class="hilfe-card-links">
<a href="#">Neues Lock starten</a>
<a href="#">Die Rolle als Keyholder</a>
<a href="#">Karten und Aufgaben</a>
<a href="#">TimeLock erklärt</a>
<a href="/help/chastity.html#sec-newlock">Neues Lock starten</a>
<a href="/help/chastity.html#sec-keyholder">Die Rolle als Keyholder</a>
<a href="/help/chastity.html#sec-karten">Karten und Aufgaben</a>
<a href="/help/chastity.html#sec-timelock">TimeLock erklärt</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">⛓️ BDSM Game</div>
<div class="hilfe-card-desc">Sessions erstellen, Spieler einladen und Aufgaben verwalten.</div>
<div class="hilfe-card-links">
<a href="#">Session starten</a>
<a href="#">Spieler einladen</a>
<a href="/help/bdsm.html#sec-session">Session starten</a>
<a href="/help/bdsm.html#sec-einladen">Spieler einladen</a>
<a href="/help/bdsm.html#sec-aufgaben">Aufgaben verwalten</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">⚪ Vanilla Game</div>
<div class="hilfe-card-desc">Leichtere Spiele ohne strenge Regeln für den entspannten Einstieg.</div>
<div class="hilfe-card-links">
<a href="#">Vanilla-Session starten</a>
<a href="/help/vanilla.html#sec-session">Vanilla-Session starten</a>
<a href="/help/vanilla.html#sec-karten">Karten und Aufgaben</a>
<a href="/help/vanilla.html#sec-faq2">Unterschied zu BDSM Game</a>
</div>
</div>
</div>
@@ -170,25 +175,24 @@
<div class="hilfe-card-title">👥 Gruppen</div>
<div class="hilfe-card-desc">Gruppen erstellen, beitreten und verwalten.</div>
<div class="hilfe-card-links">
<a href="#">Gruppe erstellen</a>
<a href="#">Mitglieder verwalten</a>
<a href="#">Beiträge und Abstimmungen</a>
<a href="/help/community.html#sec-gruppe-erstellen">Gruppe erstellen</a>
<a href="/help/community.html#sec-mitglieder">Mitglieder verwalten</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">📰 Feed &amp; Profil</div>
<div class="hilfe-card-desc">Beiträge teilen, Profile entdecken und die Community kennenlernen.</div>
<div class="hilfe-card-links">
<a href="#">Feed nutzen</a>
<a href="#">Profil gestalten</a>
<a href="#">Personen suchen und folgen</a>
<a href="/help/community.html#sec-feed">Feed nutzen</a>
<a href="/help/community.html#sec-profil">Profil gestalten &amp; folgen</a>
<a href="/help/community.html#sec-nachrichten">Nachrichten</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">🏆 Community Votes</div>
<div class="hilfe-card-desc">Verifikationen bewerten und an Community-Abstimmungen teilnehmen.</div>
<div class="hilfe-card-links">
<a href="#">Wie funktionieren Votes?</a>
<a href="/help/community.html#sec-votes">Wie funktionieren Votes?</a>
</div>
</div>
</div>
@@ -200,15 +204,16 @@
<div class="hilfe-card-title">🔐 Sicherheit &amp; Datenschutz</div>
<div class="hilfe-card-desc">Wie deine Daten gespeichert werden und welche Sicherheitsmaßnahmen wir treffen.</div>
<div class="hilfe-card-links">
<a href="#">Datenspeicherung</a>
<a href="#">Passwort-Hashing</a>
<a href="/help/datenschutz.html#sec-gespeicherte-daten">Was wird gespeichert?</a>
<a href="/help/datenschutz.html#sec-passwort-hashing">Passwort-Hashing</a>
<a href="/help/datenschutz.html#sec-verbindung">Verbindungssicherheit</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">🐛 Fehler melden</div>
<div class="hilfe-card-desc">Hast du einen Fehler gefunden oder einen Verbesserungsvorschlag?</div>
<div class="hilfe-card-links">
<a href="#">Feedback senden</a>
<a href="/help/kontakt.html">Feedback &amp; Kontakt</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,163 @@
<!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>Hilfe Vanilla Game xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header { margin-bottom: 2rem; }
.hilfe-header h1 { font-size: 1.6rem; margin: 0 0 0.4rem 0; }
.hilfe-header p { color: var(--color-muted); font-size: 0.92rem; margin: 0; line-height: 1.6; }
.hilfe-section { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 10px; margin-bottom: 0.75rem; overflow: hidden; }
.hilfe-section-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; cursor: pointer; user-select: none; transition: background 0.15s; }
.hilfe-section-header:hover { background: rgba(255,255,255,0.03); }
.hilfe-section-title { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; font-weight: 600; }
.hilfe-section-arrow { font-size: 0.75rem; color: var(--color-muted); transition: transform 0.2s; }
.hilfe-section.open .hilfe-section-arrow { transform: rotate(90deg); }
.hilfe-section-body { display: none; padding: 0 1.25rem 1.25rem; border-top: 1px solid var(--color-secondary); }
.hilfe-section.open .hilfe-section-body { display: block; }
.hilfe-section-body p { font-size: 0.9rem; color: var(--color-muted); line-height: 1.7; margin: 0.9rem 0 0; }
.hilfe-section-body p:first-child { margin-top: 1rem; }
.hilfe-steps { list-style: none; padding: 0; margin: 1rem 0 0; display: flex; flex-direction: column; gap: 0.75rem; }
.hilfe-steps li { list-style: none; display: flex; align-items: flex-start; gap: 0.85rem; font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; }
.hilfe-steps li::before { display: none; }
.hilfe-steps li .step-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--color-primary); color: #fff; font-size: 0.75rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; }
.hilfe-hint { background: rgba(var(--color-primary-rgb, 120,80,200), 0.08); border-left: 3px solid var(--color-primary); border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-hint strong { color: var(--color-text); }
.hilfe-warn { background: rgba(231,76,60,0.08); border-left: 3px solid #e74c3c; border-radius: 0 8px 8px 0; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-warn strong { color: #e74c3c; }
.hilfe-info { background: var(--color-secondary); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; margin-top: 1rem; }
.hilfe-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 1rem; }
.hilfe-table th { text-align: left; color: var(--color-text); font-weight: 600; padding: 0.4rem 0.75rem 0.4rem 0; border-bottom: 1px solid var(--color-secondary); }
.hilfe-table td { color: var(--color-muted); padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; line-height: 1.5; }
.hilfe-table tr:last-child td { border-bottom: none; }
.hilfe-badge { display: inline-block; background: var(--color-secondary); border-radius: 4px; padding: 0.1rem 0.45rem; font-size: 0.78rem; font-weight: 600; color: var(--color-muted); vertical-align: middle; }
.back-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-muted); text-decoration: none; margin-bottom: 1.25rem; transition: color 0.15s; }
.back-link:hover { color: var(--color-text); }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a href="/help/overview.html" class="back-link"> Zurück zur Hilfe-Übersicht</a>
<div class="hilfe-header">
<h1>⚪ Vanilla Game</h1>
<p>Leichtere, verspielte Sessions ohne strenge Regeln für den entspannten Einstieg.</p>
</div>
<div class="hilfe-section open" id="sec-intro">
<div class="hilfe-section-header" onclick="toggleSection('sec-intro')">
<span class="hilfe-section-title">📖 Was ist das Vanilla Game?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Das Vanilla Game ist der entspannte Einstieg in die Spielwelt von xXx Sphere. Es gibt keine festen Rollen und keine strikten Regeln stattdessen ziehen beide Parteien abwechselnd Karten und erfüllen lockere Aufgaben.
</p>
<p>
Das Spiel eignet sich besonders für Paare, die etwas Neues ausprobieren möchten, ohne sich auf ein intensiveres Regelwerk einzulassen.
</p>
<div class="hilfe-hint">
<strong>Tipp:</strong> Du kannst jederzeit eigene Aufgaben erstellen und den Schwierigkeitsgrad für jede Session selbst bestimmen.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-session">
<div class="hilfe-section-header" onclick="toggleSection('sec-session')">
<span class="hilfe-section-title">🚀 Session starten</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>So startest du eine Vanilla-Session:</p>
<ol class="hilfe-steps">
<li><span class="step-num">1</span><span>Navigiere zu <strong>Vanilla → Neue Session</strong>.</span></li>
<li><span class="step-num">2</span><span>Wähle einen Aufgaben-Pool (eigene Aufgaben oder Community-Vorlagen).</span></li>
<li><span class="step-num">3</span><span>Lege fest, ob ihr abwechselnd zieht oder eine Person die Aufgaben stellt.</span></li>
<li><span class="step-num">4</span><span>Lade deinen Mitspieler per Nutzername oder Einladungslink ein.</span></li>
<li><span class="step-num">5</span><span>Starte die Session der erste Spieler zieht die erste Karte.</span></li>
</ol>
</div>
</div>
<div class="hilfe-section" id="sec-karten">
<div class="hilfe-section-header" onclick="toggleSection('sec-karten')">
<span class="hilfe-section-title">🃏 Karten und Aufgaben</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Im Vanilla Game werden Karten aus einem gemeinsam gewählten Pool gezogen. Jede Karte beschreibt eine Aufgabe, die von einer oder beiden Personen erfüllt wird. Nach Erfüllung zieht die andere Person.
</p>
<table class="hilfe-table">
<thead><tr><th>Aufgabentyp</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td><span class="hilfe-badge">Solo</span></td><td>Nur die ziehende Person führt die Aufgabe aus.</td></tr>
<tr><td><span class="hilfe-badge">Gemeinsam</span></td><td>Beide Personen führen die Aufgabe zusammen aus.</td></tr>
<tr><td><span class="hilfe-badge">Wahl</span></td><td>Die ziehende Person entscheidet, wer die Aufgabe übernimmt.</td></tr>
</tbody>
</table>
<p>
Eigene Aufgaben kannst du unter <strong>Vanilla → Aufgaben</strong> verwalten.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Kann ich eine Session pausieren?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Ja. Eine laufende Session kann von beiden Spielern jederzeit pausiert werden. Sie bleibt für 24 Stunden gespeichert und kann danach fortgesetzt werden. Nach 24 Stunden Inaktivität wird die Session automatisch beendet.
</p>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Unterschied zwischen Vanilla und BDSM Game?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Das Vanilla Game hat keine festen Rollen, kein Protokoll und keine Strafmechanismen. Es eignet sich als Einstieg oder für entspannte Abende. Das BDSM Game hat explizite Rollen (Dom/Sub), ein Aufgaben- und Strafprotokoll sowie striktere Regeln.
</p>
<div class="hilfe-info">
Du kannst beide Spiele unabhängig voneinander nutzen deine Aufgaben-Sets lassen sich zwischen den Spielen teilen.
</div>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -78,6 +78,30 @@ const CARD_DEFS = [
defMin: 0,
defMax: 0,
},
{
id: 'SLOWMO_CARD',
img: '/img/card_slowmo.png',
name: 'Slow Motion',
desc: 'Alle gestarteten Aktionen (Hygiene-Öffnung, Freeze, Kartenintervall) dauern bis zum gewählten Zeitpunkt viermal so lange.',
defMin: 0,
defMax: 0,
},
{
id: 'SPEEDUP_CARD',
img: '/img/card_speedup.png',
name: 'Speed Up',
desc: 'Alle gestarteten Aktionen (Hygiene-Öffnung, Freeze, Kartenintervall) dauern bis zum gewählten Zeitpunkt viermal so kurz.',
defMin: 0,
defMax: 0,
},
{
id: 'GAME_CARD',
img: '/img/card_game.png',
name: 'Spiel-Karte',
desc: 'Ein Minispiel wird gestartet.',
defMin: 0,
defMax: 0,
},
];
/** Lookup-Objekt für Konsumenten, die nach ID auf Name/Bild/Beschreibung zugreifen. */

View File

@@ -16,9 +16,9 @@
margin-right: 0.5rem;
line-height: 1;
}
.nav-burger:hover { border-color: var(--color-primary); color: var(--color-primary); }
.nav-burger:hover { border-color: var(--color-primary); color: #fff; }
.nav-burger-icon {
font-size: 1.05rem; line-height: 1;
font-size: 1.575rem; line-height: 1;
position: relative;
display: inline-flex; align-items: center; justify-content: center;
width: 1.2em; height: 1.2em;
@@ -91,16 +91,16 @@
}
.nav-col:last-child { border-right: none; }
/* Überschrift: auf Desktop ausgeblendet, auf Mobile als Accordion-Toggle */
.nav-col-header {
display: none;
display: flex;
align-items: center; justify-content: space-between;
padding: 0.75rem 1.1rem;
font-size: 0.85rem; font-weight: 600;
padding: 0.75rem 1.1rem 0.5rem;
font-size: 1.275rem; font-weight: 700;
color: var(--color-text);
cursor: pointer;
cursor: default;
border-bottom: 1px solid var(--color-secondary);
}
.nav-col-arrow { font-size: 0.65rem; transition: transform 0.2s; }
.nav-col-arrow { display: none; font-size: 0.65rem; transition: transform 0.2s; }
.nav-col-body { padding: 0.35rem 0; }
@@ -158,7 +158,8 @@
.nav-col { border-right: none; border-bottom: 1px solid var(--color-secondary); }
.nav-col:last-child { border-bottom: none; }
.nav-col-header { display: flex; }
.nav-col-header { font-size: 0.85rem; font-weight: 600; cursor: pointer; padding: 0.75rem 1.1rem; border-bottom: none; }
.nav-col-arrow { display: block; }
.nav-col.col-open .nav-col-arrow { transform: rotate(90deg); }
.nav-col-body { display: none; padding: 0; }
@@ -256,23 +257,18 @@
${link('/dating/matches.html', '', 'Matches' )}
`;
const bdsmActive = ['/games/bdsm/neubdsm.html', '/games/bdsm/bdsmingame.html', '/games/bdsm/bdsmplayers.html'].some(p => path.startsWith(p)) ? ' active' : '';
const vanillaActive = ['/games/vanilla/neuvanilla.html', '/games/vanilla/vanillaingame.html', '/games/vanilla/vanillawarten.html'].some(p => path.startsWith(p)) ? ' active' : '';
const col4Html = `
${gameGroup('VANILLA', 'Vanilla Game', [
{ 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/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/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' },
])}
<a href="/games/vanilla/neuvanilla.html" class="nav-link${vanillaActive}" id="navVanillaGame">
<span class="nav-icon">${I('VANILLA') || ''}</span>
<span>Vanilla Game</span>
</a>
<a href="/games/bdsm/neubdsm.html" class="nav-link${bdsmActive}" id="navBdsmGame">
<span class="nav-icon">${I('BDSM') || ''}</span>
<span>BDSM Game</span>
</a>
${gameGroup('CHASTITY', 'Chastity Game', [
{ href: '/games/chastity/neulock.html', icon: 'NEW_LOCK', label: 'Neues Lock', id: 'navChastityNeu' },
{ href: '#', icon: 'ACTIVE_LOCK', label: 'Aktives Lock', id: 'navChastityAktiv' },
@@ -283,6 +279,11 @@
{ href: '/games/chastity/keyholder.html', icon: 'KEY', label: 'Keyholder' },
{ href: '/games/chastity/unlock-history.html', icon: 'HISTORY', label: 'Code-Historie' },
])}
${gameGroup('CHECK', 'Aufgabenverwaltung', [
{ href: '/games/aufgaben/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
{ href: '/games/aufgaben/toys.html', icon: 'TOYS', label: 'Toys' },
{ href: '/games/aufgaben/entdecken.html',icon: 'DISCOVER', label: 'Entdecken' },
])}
`;
// ── Dropdown-HTML ────────────────────────────────────────────────────────
@@ -306,7 +307,7 @@
])}
${column('colDating', 'Dating', col3Html, ['/dating/'])}
${column('colGames', 'Games', col4Html, [
'/games/vanilla/', '/games/bdsm/', '/games/chastity/',
'/games/vanilla/', '/games/bdsm/', '/games/chastity/', '/games/aufgaben/',
])}
</div>
<div class="nav-dropdown-footer">
@@ -417,21 +418,16 @@
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 href = (id, h) => { const el = document.getElementById(id); if (el) el.href = h; };
hide('navVanillaAktiv'); hide('navVanillaImSpiel');
hide('navBdsmAktiv'); hide('navBdsmImSpiel');
hide('navChastityAktiv');
try {
const r = await fetch('/bdsm/einladung/meine-aktive');
if (r.ok) {
const aktiv = await r.json();
hide('navBdsmNeu'); hide('navBdsmImSpiel');
show('navBdsmAktiv');
href('navBdsmAktiv', aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html');
href('navBdsmGame', aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html');
} else {
const sr = await fetch(`/bdsm?userId=${user.userId}`);
if (sr.status === 200) { hide('navBdsmNeu'); show('navBdsmImSpiel'); }
else show('navBdsmNeu');
if (sr.status === 200) href('navBdsmGame', '/games/bdsm/bdsmingame.html');
}
} catch (_) {}
@@ -439,13 +435,10 @@
const r = await fetch('/vanilla/einladung/meine-aktive');
if (r.ok) {
const aktiv = await r.json();
hide('navVanillaNeu'); hide('navVanillaImSpiel');
show('navVanillaAktiv');
href('navVanillaAktiv', aktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html');
href('navVanillaGame', aktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html');
} else {
const sr = await fetch(`/vanilla?userId=${user.userId}`);
if (sr.status === 200) { hide('navVanillaNeu'); show('navVanillaImSpiel'); }
else show('navVanillaNeu');
if (sr.status === 200) href('navVanillaGame', '/games/vanilla/vanillaingame.html');
}
} catch (_) {}

View File

@@ -42,9 +42,6 @@
{ 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: {
@@ -53,9 +50,14 @@
{ 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' },
],
},
aufgaben: {
prefixes: ['/games/aufgaben/'],
items: [
{ href: '/games/aufgaben/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' },
{ href: '/games/aufgaben/toys.html', icon: 'TOYS', label: 'Toys' },
{ href: '/games/aufgaben/entdecken.html',icon: 'DISCOVER', label: 'Entdecken' },
],
},
chastity: {

View File

@@ -3,30 +3,6 @@
const I = window.IC || function() { return ''; };
const groups = [
{
label: 'Vanilla Game',
icon: I('VANILLA'),
items: [
{ 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/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' },
]
},
{
label: 'BDSM Game',
icon: I('BDSM'),
items: [
{ 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/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' },
]
},
{
label: 'Chastity Game',
icon: I('CHASTITY'),
@@ -41,8 +17,22 @@
{ href: '/games/chastity/unlock-history.html', icon: I('HISTORY'), label: 'Code-Historie' },
]
},
{
label: 'Aufgabenverwaltung',
icon: I('CHECK'),
items: [
{ href: '/games/aufgaben/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
{ href: '/games/aufgaben/toys.html', icon: I('TOYS'), label: 'Toys' },
{ href: '/games/aufgaben/entdecken.html',icon: I('DISCOVER'), label: 'Entdecken' },
]
},
];
const vanillaCls = path.startsWith('/games/vanilla/') ? ' class="active"' : '';
const bdsmCls = path.startsWith('/games/bdsm/') ? ' class="active"' : '';
const vanillaLink = `<li><a href="/games/vanilla/neuvanilla.html" id="navVanillaGame"${vanillaCls}><span class="icon">${I('VANILLA') || '⚪'}</span> Vanilla Game</a></li>`;
const bdsmLink = `<li><a href="/games/bdsm/neubdsm.html" id="navBdsmGame"${bdsmCls}><span class="icon">${I('BDSM') || '⛓️'}</span> BDSM Game</a></li>`;
// ── Hilfsfunktion: einzelner Nav-Link ──
function navLink({ href, icon, label, badgeId }) {
@@ -137,6 +127,8 @@
${sep}
${datingItem}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${vanillaLink}
${bdsmLink}
${nav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;" id="navAdminDivider" style="display:none"></li>
${adminItem}
@@ -171,19 +163,9 @@
});
});
// "Im Spiel" und "Aktive Session" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
const navNeu = document.getElementById('navBdsmNeu');
const navAktiv = document.getElementById('navBdsmAktiv');
const navImSpiel = document.getElementById('navBdsmImSpiel');
const navCAktiv = document.getElementById('navChastityAktiv');
const navVNeu = document.getElementById('navVanillaNeu');
const navVAktiv = document.getElementById('navVanillaAktiv');
const navVImSpiel = document.getElementById('navVanillaImSpiel');
if (navAktiv) navAktiv.style.display = 'none';
if (navImSpiel) navImSpiel.style.display = 'none';
if (navCAktiv) navCAktiv.style.display = 'none';
if (navVAktiv) navVAktiv.style.display = 'none';
if (navVImSpiel) navVImSpiel.style.display = 'none';
// "Aktives Lock" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
const navCAktiv = document.getElementById('navChastityAktiv');
if (navCAktiv) navCAktiv.style.display = 'none';
// Session-Status prüfen
fetch('/login/me')
@@ -196,17 +178,14 @@
const aktivRes = await fetch('/bdsm/einladung/meine-aktive');
if (aktivRes.ok) {
const aktiv = await aktivRes.json();
if (navNeu) navNeu.style.display = 'none';
if (navImSpiel) navImSpiel.style.display = 'none';
if (navAktiv) {
navAktiv.style.display = '';
navAktiv.querySelector('a').href = aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html';
}
const el = document.getElementById('navBdsmGame');
if (el) el.href = aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html';
} else {
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
const hasSession = sessionRes.status === 200;
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
if (sessionRes.status === 200) {
const el = document.getElementById('navBdsmGame');
if (el) el.href = '/games/bdsm/bdsmingame.html';
}
}
} catch (_) {}
@@ -215,17 +194,14 @@
const vAktivRes = await fetch('/vanilla/einladung/meine-aktive');
if (vAktivRes.ok) {
const vAktiv = await vAktivRes.json();
if (navVNeu) navVNeu.style.display = 'none';
if (navVImSpiel) navVImSpiel.style.display = 'none';
if (navVAktiv) {
navVAktiv.style.display = '';
navVAktiv.querySelector('a').href = vAktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html';
}
const el = document.getElementById('navVanillaGame');
if (el) el.href = vAktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html';
} else {
const vSessionRes = await fetch(`/vanilla?userId=${user.userId}`);
const vHasSession = vSessionRes.status === 200;
if (navVNeu) navVNeu.style.display = vHasSession ? 'none' : '';
if (navVImSpiel) navVImSpiel.style.display = vHasSession ? '' : 'none';
if (vSessionRes.status === 200) {
const el = document.getElementById('navVanillaGame');
if (el) el.href = '/games/vanilla/vanillaingame.html';
}
}
} catch (_) {}