Bugfixes, Dating angefangen
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -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 ──
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
32
bin/main/static/dating.html
Normal file
32
bin/main/static/dating.html
Normal 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>
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user