Bugfixes, Dating angefangen
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-01 22:06:46 +02:00
parent 912718fc40
commit 87c85b1b17
123 changed files with 28977 additions and 462 deletions

View File

@@ -2416,7 +2416,7 @@ function renderVlListe() {
<span style="font-size:0.72rem;color:var(--color-muted);flex-shrink:0;margin-right:0.5rem;">#${i.sortOrder}</span>
<div class="item-badges" style="flex-shrink:0;">
<button class="btn-item-edit" onclick="editItem('${i.itemId}')">✎</button>
<button class="btn-item-delete" onclick="deleteItem('${i.itemId}','${escAdminHtml(i.name).replace(/'/g,"\\'")}')">✕</button>
<button class="btn-item-delete" onclick="deleteVorliebeItem('${i.itemId}','${escAdminHtml(i.name).replace(/'/g,"\\'")}')">✕</button>
</div>
</div>
</div>`).join('')}</div>`
@@ -2498,13 +2498,14 @@ async function saveKategorie() {
} else { errEl.textContent = 'Fehler beim Speichern.'; }
}
async function deleteKategorie(id, name) {
if (!confirm(`Kategorie "${name}" löschen?`)) return;
const errEl = document.getElementById('vlError');
const r = await fetch(`/admin/vorlieben/kategorien/${id}`, { method: 'DELETE' });
if (r.ok) { await loadVorliebenAdmin(); }
else if (r.status === 409) { errEl.textContent = 'Kategorie enthält noch Vorlieben bitte zuerst alle Vorlieben dieser Kategorie löschen.'; }
else { errEl.textContent = 'Fehler beim Löschen.'; }
function deleteKategorie(id, name) {
openConfirmModal(`Kategorie ${name}" löschen?`, async () => {
const errEl = document.getElementById('vlError');
const r = await fetch(`/admin/vorlieben/kategorien/${id}`, { method: 'DELETE' });
if (r.ok) { await loadVorliebenAdmin(); }
else if (r.status === 409) { errEl.textContent = 'Kategorie enthält noch Vorlieben bitte zuerst alle Vorlieben dieser Kategorie löschen.'; }
else { errEl.textContent = 'Fehler beim Löschen.'; }
});
}
// ── Items CRUD ──
@@ -2554,12 +2555,13 @@ async function saveItem() {
} else { errEl.textContent = 'Fehler beim Speichern.'; }
}
async function deleteItem(id, name) {
if (!confirm(`Vorliebe "${name}" löschen? Alle Nutzerbewertungen werden ebenfalls gelöscht.`)) return;
const errEl = document.getElementById('vlError');
const r = await fetch(`/admin/vorlieben/items/${id}`, { method: 'DELETE' });
if (r.ok) { await loadVorliebenAdmin(); }
else { errEl.textContent = 'Fehler beim Löschen.'; }
function deleteVorliebeItem(id, name) {
openConfirmModal(`Vorliebe ${name}" löschen? Alle Nutzerbewertungen werden ebenfalls gelöscht.`, async () => {
const errEl = document.getElementById('vlError');
const r = await fetch(`/admin/vorlieben/items/${id}`, { method: 'DELETE' });
if (r.ok) { await loadVorliebenAdmin(); }
else { errEl.textContent = 'Fehler beim Löschen.'; }
});
}
// ── Export / Import ──

View File

@@ -444,6 +444,12 @@
</div>
</div>
<!-- Vorlieben (inline, zwischen allgemeinen Angaben und Freunden) -->
<div id="vorliebenSection" style="display:none; margin-top:1rem;">
<div class="section-label">Vorlieben</div>
<div id="vorliebenDisplay"></div>
</div>
<!-- Freunde -->
<div id="friendsSection" style="display:none; margin-top:1rem;">
<div class="section-label">Freunde <span id="friendsCount" style="font-weight:400;color:var(--color-muted);font-size:0.85rem;"></span></div>
@@ -455,11 +461,10 @@
</div>
</div>
<!-- Tabs: Feed | Pinnwand | Vorlieben | Spielhistorie | Keyholder-Angebote -->
<!-- Tabs: Feed | Pinnwand | Spielhistorie | Keyholder-Angebote -->
<div class="profil-tabs" style="margin-top:1.25rem;">
<button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button>
<button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button>
<button class="profil-tab-btn" id="tabBtnVorlieben" onclick="switchProfilTab('vorlieben', this)" style="display:none;">Vorlieben</button>
<button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this)">Spielhistorie</button>
<button class="profil-tab-btn" id="tabBtnKhOffers" onclick="switchProfilTab('khoffers', this)">Keyholder-Angebote</button>
</div>
@@ -480,11 +485,6 @@
<div id="pinnwandList"></div>
</div>
<!-- Vorlieben Tab -->
<div class="profil-tab-panel" id="tab-vorlieben">
<div id="vorliebenDisplay" style="margin-top:0.75rem;"></div>
</div>
<!-- Spielhistorie Tab -->
<div class="profil-tab-panel" id="tab-gamehistory">
<div id="gameHistoryList" style="margin-top:0.75rem;"></div>
@@ -593,6 +593,11 @@
profileData = profile;
allImages = images;
// Profilbesuch tracken (nur fremde Profile, kein Preview-Modus)
if (!isOwnProfile && !previewMode && myUserId) {
fetch('/social/profile-visits/' + targetUserId, { method: 'POST' }).catch(() => {});
}
// ── Preview-Modus: friendStatus simulieren ──
if (previewMode) {
profile.friendStatus = (previewMode === 'FREUND') ? 'FRIEND' : 'NONE';
@@ -716,12 +721,11 @@
const btnFeed = document.getElementById('tabBtnPosts');
const btnPinnwand = document.getElementById('tabBtnPinnwand');
const btnHistory = document.getElementById('tabBtnGameHistory');
const btnVorlieben = document.getElementById('tabBtnVorlieben');
if (!showFeed) { btnFeed.style.display = 'none'; document.getElementById('tab-posts').classList.remove('active'); }
if (!showPinnwand) { btnPinnwand.style.display = 'none'; }
if (!showHistory) { btnHistory.style.display = 'none'; }
if (showVorlieben) { btnVorlieben.style.display = ''; loadVorlieben(); }
if (showVorlieben) { document.getElementById('vorliebenSection').style.display = ''; loadVorlieben(); }
// Ersten sichtbaren Tab aktivieren
if (!showFeed) {
@@ -735,6 +739,7 @@
document.getElementById('tab-gamehistory').classList.add('active');
}
}
}
// ── Vorlieben anzeigen ──

View File

@@ -0,0 +1,32 @@
<!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>Dating xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="app">
<div class="main">
<div class="content">
<h1>Dating</h1>
<p style="color:var(--color-muted);">Kommt bald.</p>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
fetch('/login/me').then(r => {
if (r.status === 401) window.location.href = '/login.html';
return r.ok ? r.json() : null;
}).then(user => {
if (!user) return;
if (!user.datingAktiv) window.location.href = '/konto/einstellungen.html#sec-dating';
}).catch(() => { window.location.href = '/login.html'; });
</script>
</body>
</html>

View File

@@ -43,14 +43,10 @@
},
];
const homeCls = path === '/userhome.html' ? ' class="active"' : '';
const homeItem = `
<li class="sidebar-mobile-only">
<a href="/userhome.html"${homeCls}><span class="icon">${I('HOME')}</span> Home</a>
</li>`;
// ── Community-Links (immer sichtbar, oberhalb der Spiele) ──
const socialLinks = [
{ href: '/userhome.html', icon: I('HOME') || '⌂', label: 'Home', badgeId: null },
{ href: '/community/feed.html', icon: I('FEED'), label: 'Feed', badgeId: null },
{ href: '/community/freunde.html', icon: I('FRIENDS'), label: 'Freunde', badgeId: 'socialFriendsBadge'},
{ href: '/community/nachrichten.html', icon: I('MESSAGES'), label: 'Nachrichten', badgeId: null },
@@ -64,6 +60,10 @@
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
}).join('');
const datingActive = path === '/dating.html';
const datingCls = datingActive ? ' class="active"' : '';
const datingItem = `<li id="navDating"><a href="/konto/einstellungen.html#sec-dating"${datingCls}><span class="icon">${I('DATING') || '♥'}</span> Dating</a></li>`;
const fullHref = path + window.location.search;
const nav = groups.map(({ label, icon, items }) => {
const isOpen = items.some(item => item.href === path || item.href === fullHref);
@@ -105,6 +105,8 @@
<ul>
${socialNav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${datingItem}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${nav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;" id="navAdminDivider" style="display:none"></li>
${adminItem}
@@ -204,6 +206,14 @@
}
} catch (_) {}
// Dating-Link
const navDating = document.getElementById('navDating');
if (navDating) {
navDating.querySelector('a').href = user.datingAktiv
? '/dating.html'
: '/konto/einstellungen.html#sec-dating';
}
// Admin-Link
if (user.admin) {
const navAdminLink = document.getElementById('navAdminLink');

View File

@@ -472,6 +472,55 @@
</div>
</div>
<!-- Dating -->
<div class="settings-section" id="sec-dating">
<div class="settings-section-header" onclick="toggleSection('sec-dating')">
<span class="settings-section-title">♥ Dating</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Dating aktivieren</div>
<div class="settings-row-desc">Zeige dein Profil im Dating-Bereich. Ein Standort ist erforderlich.</div>
</div>
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;">
<input type="checkbox" id="datingAktiv" style="width:1.1rem;height:1.1rem;accent-color:var(--color-primary);cursor:pointer;" onchange="onDatingToggle()">
</label>
</div>
<div id="datingStadtRow" style="display:none;">
<div class="settings-row" style="flex-wrap:wrap;gap:0.5rem;">
<div class="settings-row-info">
<div class="settings-row-label">Standort (Stadt)</div>
<div class="settings-row-desc">Wird im Dating-Bereich angezeigt.</div>
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-bottom:0.35rem;position:relative;">
<div style="position:relative;flex:1;display:flex;">
<input type="text" id="datingStadt" placeholder="Stadt suchen und auswählen…" autocomplete="off"
style="flex:1;padding:0.55rem 2rem 0.55rem 0.8rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;"
oninput="onStadtInput()">
<button id="datingStadtClear" onclick="clearStadt()" title="Auswahl aufheben"
style="display:none;position:absolute;right:0.4rem;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--color-muted);font-size:1.1rem;cursor:pointer;padding:0.1rem 0.3rem;margin:0;line-height:1;">×</button>
</div>
<button onclick="detectLocation()" title="Standort ermitteln"
style="white-space:nowrap;padding:0.55rem 0.9rem;margin:0;font-size:0.85rem;">
⌖ Ermitteln
</button>
<ul id="stadtSuggestions" style="display:none;position:absolute;top:100%;left:0;right:3.5rem;
background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;
z-index:100;list-style:none;margin:0.2rem 0 0;padding:0;max-height:200px;overflow-y:auto;"></ul>
</div>
<div id="datingLocMsg" style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.5rem;"></div>
</div>
<div class="settings-row" style="border-top:1px solid var(--color-secondary);padding-top:0.75rem;margin-top:0.25rem;">
<button onclick="saveDating()" id="saveDatingBtn" style="margin:0;padding:0.55rem 1.25rem;font-size:0.9rem;">Speichern</button>
<span id="datingMsg" style="font-size:0.85rem;"></span>
</div>
</div>
</div>
<!-- Datenschutz -->
<div class="settings-section" id="sec-datenschutz">
<div class="settings-section-header" onclick="toggleSection('sec-datenschutz')">
@@ -1134,8 +1183,152 @@
loadBdsmDefaults();
loadSubscription();
loadTtlockUserConfig();
loadDating();
openSectionFromHash();
// ── Dating ──────────────────────────────────────────────────────────────
async function loadDating() {
const res = await fetch('/login/me');
if (!res.ok) return;
const user = await res.json();
document.getElementById('datingAktiv').checked = !!user.datingAktiv;
if (user.datingStadt) {
const input = document.getElementById('datingStadt');
input.value = user.datingStadt;
input.readOnly = true;
document.getElementById('datingStadtClear').style.display = '';
}
if (user.datingLat != null) _datingLat = user.datingLat;
if (user.datingLon != null) _datingLon = user.datingLon;
document.getElementById('datingStadtRow').style.display = user.datingAktiv ? '' : 'none';
}
function onDatingToggle() {
const aktiv = document.getElementById('datingAktiv').checked;
document.getElementById('datingStadtRow').style.display = aktiv ? '' : 'none';
}
let _stadtSuggestTimer = null;
let _datingLat = null;
let _datingLon = null;
function onStadtInput() {
const q = document.getElementById('datingStadt').value.trim();
_datingLat = null;
_datingLon = null;
document.getElementById('datingStadtClear').style.display = 'none';
clearTimeout(_stadtSuggestTimer);
if (q.length < 2) { hideSuggestions(); return; }
_stadtSuggestTimer = setTimeout(() => fetchStadtSuggestions(q), 300);
}
async function fetchStadtSuggestions(q) {
try {
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&addressdetails=1&limit=5&featuretype=city`;
const res = await fetch(url, { headers: { 'Accept-Language': 'de' } });
if (!res.ok) return;
const results = await res.json();
const ul = document.getElementById('stadtSuggestions');
if (!results.length) { hideSuggestions(); return; }
ul.innerHTML = results.map(r => {
const city = r.address.city || r.address.town || r.address.village || r.address.county || r.name;
const country = r.address.country || '';
const label = city + (country ? ', ' + country : '');
return `<li style="padding:0.5rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
onmousedown="selectStadt(event, '${label.replace(/'/g, "\\'")}', ${parseFloat(r.lat)}, ${parseFloat(r.lon)})">${label}</li>`;
}).join('');
ul.style.display = '';
} catch (_) { hideSuggestions(); }
}
function selectStadt(e, label, lat, lon) {
e.preventDefault();
const input = document.getElementById('datingStadt');
input.value = label;
input.readOnly = true;
_datingLat = lat;
_datingLon = lon;
document.getElementById('datingStadtClear').style.display = '';
hideSuggestions();
}
function clearStadt() {
const input = document.getElementById('datingStadt');
input.value = '';
input.readOnly = false;
_datingLat = null;
_datingLon = null;
document.getElementById('datingStadtClear').style.display = 'none';
input.focus();
}
function hideSuggestions() {
document.getElementById('stadtSuggestions').style.display = 'none';
}
document.addEventListener('click', e => {
if (!e.target.closest('#datingStadtRow')) hideSuggestions();
});
async function detectLocation() {
const msgEl = document.getElementById('datingLocMsg');
if (!navigator.geolocation) { msgEl.textContent = 'Geolocation nicht unterstützt.'; return; }
msgEl.textContent = 'Standort wird ermittelt…';
navigator.geolocation.getCurrentPosition(async pos => {
try {
const { latitude, longitude } = pos.coords;
const res = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&addressdetails=1`,
{ headers: { 'Accept-Language': 'de' } }
);
if (!res.ok) throw new Error();
const data = await res.json();
const city = data.address.city || data.address.town || data.address.village || data.address.county || '';
const country = data.address.country || '';
const input = document.getElementById('datingStadt');
input.value = city + (country ? ', ' + country : '');
input.readOnly = true;
_datingLat = latitude;
_datingLon = longitude;
document.getElementById('datingStadtClear').style.display = '';
msgEl.textContent = '';
} catch (_) { msgEl.textContent = 'Standort konnte nicht ermittelt werden.'; }
}, () => { msgEl.textContent = 'Zugriff auf Standort verweigert.'; });
}
async function saveDating() {
const aktiv = document.getElementById('datingAktiv').checked;
const stadt = document.getElementById('datingStadt').value.trim();
const msgEl = document.getElementById('datingMsg');
if (aktiv && (!stadt || _datingLat == null || _datingLon == null)) {
msgEl.textContent = 'Bitte eine Stadt aus der Liste auswählen oder per GPS ermitteln.';
msgEl.style.color = 'var(--color-primary)';
return;
}
const btn = document.getElementById('saveDatingBtn');
btn.disabled = true;
try {
const res = await fetch('/user/me/dating', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ datingAktiv: aktiv, datingStadt: stadt || null, datingLat: _datingLat, datingLon: _datingLon })
});
if (res.ok) {
showToast();
msgEl.textContent = '';
} else {
msgEl.textContent = 'Fehler beim Speichern.';
msgEl.style.color = 'var(--color-primary)';
}
} catch (_) {
msgEl.textContent = 'Server nicht erreichbar.';
msgEl.style.color = 'var(--color-primary)';
} finally {
btn.disabled = false;
}
}
// ── TTLock ──────────────────────────────────────────────────────────────
let _ttlPasswordSet = false;

View File

@@ -1,87 +1,159 @@
<!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>Home xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.game-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.25rem;
margin-top: 1.5rem;
}
.game-card {
background: var(--color-secondary);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 1.5rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.game-card-icon { font-size: 2rem; line-height: 1; }
.game-card-title { font-size: 1.1rem; font-weight: 700; margin: 0; }
.game-card-desc { font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; flex: 1; }
.game-card-btn { margin-top: 0.25rem; width: auto; align-self: flex-start; padding: 0.5rem 1.25rem; }
.welcome { font-size: 0.95rem; color: var(--color-muted); margin: 0.25rem 0 0; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1 style="margin:0 0 0.15rem;">Home</h1>
<p class="welcome" id="greeting"></p>
<div class="game-grid">
<div class="game-card">
<div class="game-card-icon"></div>
<h2 class="game-card-title">Vanilla Game</h2>
<p class="game-card-desc">
Entdecke spielerische Rollenspiele und Aufgaben in einem entspannten Rahmen.
Ideal für den Einstieg ohne Regeln, nur Spaß zu zweit oder in der Gruppe.
</p>
<a href="/games/vanilla/sessionvanilla.html"><button class="game-card-btn">Neue Session starten</button></a>
</div>
<div class="game-card">
<div class="game-card-icon"></div>
<h2 class="game-card-title">BDSM Game</h2>
<p class="game-card-desc">
Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen.
Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinem Partner.
</p>
<a href="/games/bdsm/neubdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
</div>
<div class="game-card">
<div class="game-card-icon"></div>
<h2 class="game-card-title">Chastity Game</h2>
<p class="game-card-desc">
Erlebe Keuschheit auf eine neue Art: Kartenbasierte Locks, Keyholder-System,
Community-Abstimmungen und tägliche Verifizierungen machen jedes Lock einzigartig.
</p>
<a href="/games/chastity/neulock.html"><button class="game-card-btn">Neues Lock erstellen</button></a>
</div>
</div>
</div>
</div>
<!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>Home xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.game-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.25rem;
margin-top: 1.5rem;
}
.game-card {
background: var(--color-secondary);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 1.5rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.game-card-icon { font-size: 2rem; line-height: 1; }
.game-card-title { font-size: 1.1rem; font-weight: 700; margin: 0; }
.game-card-desc { font-size: 0.88rem; color: var(--color-muted); line-height: 1.6; flex: 1; }
.game-card-btn { margin-top: 0.25rem; width: auto; align-self: flex-start; padding: 0.5rem 1.25rem; }
.welcome { font-size: 0.95rem; color: var(--color-muted); margin: 0.25rem 0 0; }
.section-label {
font-size: 0.8rem; font-weight: 600; color: var(--color-muted);
text-transform: uppercase; letter-spacing: 0.05em;
margin: 2rem 0 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
}
.visitors-strip {
display: flex; flex-wrap: wrap; gap: 0.75rem;
}
.visitor-card {
display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
text-decoration: none; color: var(--color-text);
width: 72px;
}
.visitor-card:hover .visitor-avatar { border-color: var(--color-primary); }
.visitor-avatar {
width: 56px; height: 56px; border-radius: 50%;
background: var(--color-secondary);
border: 2px solid var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
transition: border-color 0.15s;
}
.visitor-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.visitor-name {
font-size: 0.75rem; text-align: center;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
width: 100%;
}
.visitor-time { font-size: 0.68rem; color: var(--color-muted); text-align: center; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1 style="margin:0 0 0.15rem;">Home</h1>
<p class="welcome" id="greeting"></p>
<div class="game-grid">
<div class="game-card">
<div class="game-card-icon"></div>
<h2 class="game-card-title">Vanilla Game</h2>
<p class="game-card-desc">
Entdecke spielerische Rollenspiele und Aufgaben in einem entspannten Rahmen.
Ideal für den Einstieg ohne Regeln, nur Spaß zu zweit oder in der Gruppe.
</p>
<a href="/games/vanilla/sessionvanilla.html"><button class="game-card-btn">Neue Session starten</button></a>
</div>
<div class="game-card">
<div class="game-card-icon"></div>
<h2 class="game-card-title">BDSM Game</h2>
<p class="game-card-desc">
Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen.
Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinem Partner.
</p>
<a href="/games/bdsm/neubdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
</div>
<div class="game-card">
<div class="game-card-icon"></div>
<h2 class="game-card-title">Chastity Game</h2>
<p class="game-card-desc">
Erlebe Keuschheit auf eine neue Art: Kartenbasierte Locks, Keyholder-System,
Community-Abstimmungen und tägliche Verifizierungen machen jedes Lock einzigartig.
</p>
<a href="/games/chastity/neulock.html"><button class="game-card-btn">Neues Lock erstellen</button></a>
</div>
</div>
<!-- Profilbesucher -->
<div id="visitorsSection" style="display:none;">
<div class="section-label">Profilbesucher</div>
<div class="visitors-strip" id="visitorsStrip"></div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
fetch('/login/me')
.then(r => {
if (r.status === 401) { window.location.href = '/login.html'; return null; }
return r.json();
})
.then(user => {
if (user) document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
})
.catch(() => { window.location.href = '/login.html'; });
</script>
</body>
</html>
<script src="/js/sidebar.js"></script>
<script>
fetch('/login/me')
.then(r => {
if (r.status === 401) { window.location.href = '/login.html'; return null; }
return r.json();
})
.then(user => {
if (user) {
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
loadVisitors();
}
})
.catch(() => { window.location.href = '/login.html'; });
function relativeTime(isoString) {
const diff = Math.floor((Date.now() - new Date(isoString)) / 1000);
if (diff < 60) return 'gerade eben';
if (diff < 3600) return 'vor ' + Math.floor(diff / 60) + ' Min.';
if (diff < 86400) return 'vor ' + Math.floor(diff / 3600) + ' Std.';
return 'vor ' + Math.floor(diff / 86400) + ' Tag' + (Math.floor(diff / 86400) === 1 ? '' : 'en');
}
async function loadVisitors() {
try {
const res = await fetch('/social/profile-visits/my-visitors');
if (!res.ok) return;
const visitors = await res.json();
if (!visitors.length) return;
const strip = document.getElementById('visitorsStrip');
strip.innerHTML = visitors.map(v => `
<a class="visitor-card" href="/community/benutzer.html?userId=${v.userId}">
<div class="visitor-avatar">
${v.profilePicture
? `<img src="data:image/png;base64,${v.profilePicture}" alt="${v.name}">`
: '◉'}
</div>
<span class="visitor-name">${v.name}</span>
<span class="visitor-time">${relativeTime(v.visitedAt)}</span>
</a>
`).join('');
document.getElementById('visitorsSection').style.display = '';
} catch (_) {}
}
</script>
</body>
</html>