Files
xxx-sphere-web/src/main/resources/static/userhome.html
Mario a13b8e894f
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Alles mögliche
2026-04-08 00:53:49 +02:00

925 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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);
}
/* ── Aktivitäts-Grid (Besucher / Likes / Matches) ── */
.activity-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 0.5rem;
}
@media (max-width: 680px) {
.activity-grid { grid-template-columns: 1fr; }
}
.activity-col {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 0.75rem 0.85rem 0.85rem;
}
.activity-col-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 0.7rem;
}
.activity-col-title {
font-size: 0.78rem; font-weight: 700; color: var(--color-muted);
text-transform: uppercase; letter-spacing: 0.05em;
}
.activity-col-link {
font-size: 0.75rem; color: var(--color-primary);
text-decoration: none; font-weight: 600;
}
.activity-col-link:hover { text-decoration: underline; }
.activity-row {
display: flex; gap: 0.5rem;
}
/* Avatar-Karte */
.soc-card {
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
text-decoration: none; color: var(--color-text); cursor: pointer; min-width: 0;
}
.soc-card:hover .soc-avatar { border-color: var(--color-primary); }
.soc-avatar {
width: 48px; height: 48px; border-radius: 50%;
background: var(--color-secondary); border: 2px solid var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; overflow: hidden; flex-shrink: 0;
transition: border-color 0.15s; position: relative;
}
.soc-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.soc-lock {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center; font-size: 0.95rem;
}
.soc-name {
font-size: 0.68rem; text-align: center;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
}
.soc-sub { font-size: 0.62rem; color: var(--color-muted); text-align: center; }
.soc-sub-accent { font-size: 0.62rem; color: var(--color-primary); font-weight: 600; text-align: center; }
.activity-empty { font-size: 0.8rem; color: var(--color-muted); text-align: center; padding: 0.5rem 0; }
/* ── Location-Events ── */
.loc-event-list { display: flex; flex-direction: column; gap: 0.6rem; }
.loc-event-card {
display: flex; gap: 0.75rem; align-items: center;
background: var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 10px; padding: 0.65rem 0.85rem;
text-decoration: none; color: inherit;
transition: border-color 0.15s;
}
.loc-event-card:hover { border-color: var(--color-primary); }
.loc-event-thumb {
width: 48px; height: 48px; border-radius: 8px; flex-shrink: 0;
background: var(--color-card); overflow: hidden;
display: flex; align-items: center; justify-content: center; font-size: 1.4rem;
}
.loc-event-thumb img { width: 100%; height: 100%; object-fit: cover; }
.loc-event-body { flex: 1; min-width: 0; }
.loc-event-location { font-size: 0.75rem; color: var(--color-muted); margin-bottom: 0.1rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.loc-event-title { font-size: 0.92rem; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.loc-event-date { font-size: 0.75rem; color: var(--color-primary); margin-top: 0.15rem; }
/* ── Aktive Spiele ── */
.active-game-list { display: flex; flex-direction: column; gap: 0.6rem; }
.active-game-card {
display: flex; gap: 0.75rem; align-items: center;
background: var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 10px; padding: 0.65rem 0.85rem;
text-decoration: none; color: inherit;
transition: border-color 0.15s;
}
.active-game-card:hover { border-color: var(--color-primary); }
.active-game-icon {
width: 48px; height: 48px; border-radius: 8px; flex-shrink: 0;
background: var(--color-card);
display: flex; align-items: center; justify-content: center; font-size: 1.6rem;
}
.active-game-body { flex: 1; min-width: 0; }
.active-game-title { font-size: 0.92rem; font-weight: 600; }
.active-game-sub { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.1rem; }
.active-game-action {
font-size: 0.8rem; color: var(--color-primary); font-weight: 600; flex-shrink: 0;
}
/* ── Einladungen ── */
.invite-list { display: flex; flex-direction: column; gap: 0.6rem; }
.invite-card {
display: flex; gap: 0.75rem; align-items: center;
background: var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 10px; padding: 0.65rem 0.85rem;
text-decoration: none; color: inherit;
transition: border-color 0.15s;
}
.invite-card:hover { border-color: var(--color-primary); }
.invite-avatar {
width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;
background: var(--color-card);
display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
overflow: hidden;
}
.invite-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.invite-body { flex: 1; min-width: 0; }
.invite-from { font-size: 0.88rem; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.invite-type { font-size: 0.73rem; color: var(--color-muted); margin-top: 0.1rem; }
.invite-action { font-size: 0.8rem; color: var(--color-primary); font-weight: 600; flex-shrink: 0; }
/* ── Freundschaftsanfragen ── */
.friend-req-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.friend-req-card {
display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
text-decoration: none; color: var(--color-text); width: 72px;
}
.friend-req-card:hover .friend-req-avatar { border-color: var(--color-primary); }
.friend-req-avatar {
width: 56px; height: 56px; border-radius: 50%;
background: var(--color-secondary);
border: 2px solid var(--color-primary);
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
transition: border-color 0.15s;
}
.friend-req-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.friend-req-name {
font-size: 0.75rem; text-align: center;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
}
.friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
/* ── Compose ── */
.post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; transition:border-color 0.15s; }
.post-compose.drag-over { border-color:var(--color-primary); background:rgba(var(--color-primary-rgb,180,0,60),0.06); }
.compose-type { display:flex; gap:1.5rem; margin-bottom:0.75rem; }
.compose-type label { display:flex; align-items:center; gap:0.4rem; font-size:0.9rem; cursor:pointer; }
.post-compose 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; min-height:70px; box-sizing:border-box; }
.post-compose textarea:focus { border-color:var(--color-primary); }
.compose-thumbs { display:none; flex-wrap:wrap; gap:0.5rem; margin-top:0.5rem; }
.compose-thumb { position:relative; width:64px; height:64px; flex-shrink:0; }
.compose-thumb img { width:64px; height:64px; object-fit:cover; border-radius:6px; display:block; }
.compose-thumb-remove { position:absolute; top:-5px; right:-5px; background:rgba(0,0,0,0.7); border:none; color:#fff; border-radius:50%; font-size:0.65rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; margin:0; width:auto; line-height:1; }
.umfrage-options { margin-top:0.5rem; }
.umfrage-option-row { display:flex; gap:0.5rem; margin-bottom:0.4rem; }
.umfrage-option-row input { flex:1; }
.umfrage-option-row button { width:auto; margin:0; padding:0.3rem 0.6rem; font-size:0.8rem; }
.compose-footer { display:flex; justify-content:space-between; align-items:center; margin-top:0.75rem; flex-wrap:wrap; gap:0.5rem; }
.multi-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
.privacy-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
.compose-action-btn { background:none; border:1px solid var(--color-secondary); color:var(--color-muted); border-radius:6px; padding:0.35rem 0.6rem; font-size:0.95rem; cursor:pointer; margin:0; width:auto; transition:border-color 0.15s,color 0.15s; }
.compose-action-btn:hover { border-color:var(--color-primary); color:var(--color-primary); background:none; }
label.compose-action-btn { display:inline-flex; align-items:center; }
/* ── Post Cards (1:1 wie Feed) ── */
.post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; cursor:pointer; transition:border-color 0.15s; }
.post-card:hover { border-color:var(--color-primary); }
.post-header { display:flex; align-items:center; gap:0.7rem; margin-bottom:0.6rem; }
.post-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.95rem; flex-shrink:0; overflow:hidden; }
.post-avatar img { width:100%; height:100%; object-fit:cover; }
.post-author { font-weight:600; font-size:0.9rem; }
.post-meta { font-size:0.75rem; color:var(--color-muted); }
.post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
.post-bild { width:100%; max-height:400px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
.post-actions { display:flex; gap:1rem; margin-top:0.75rem; align-items:center; flex-wrap:wrap; }
.post-action-btn { background:none; border:none; color:var(--color-muted); font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; margin:0; width:auto; pointer-events:none; }
.post-action-btn.active { color:var(--color-primary); }
.gruppe-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.75rem; color:var(--color-muted); background:var(--color-secondary); border-radius:4px; padding:0.15rem 0.45rem; margin-left:0.3rem; }
.umfrage-option-bar { margin:0.3rem 0; border-radius:6px; overflow:hidden; border:1px solid var(--color-secondary); position:relative; }
.umfrage-option-bar.voted { border-color:var(--color-primary); }
.umfrage-bar-fill { position:absolute; inset:0; background:rgba(var(--color-primary-rgb,180,0,60),0.15); }
.umfrage-bar-content { position:relative; display:flex; justify-content:space-between; padding:0.45rem 0.75rem; font-size:0.88rem; }
.umfrage-total { font-size:0.78rem; color:var(--color-muted); margin-top:0.3rem; }
/* ── Neue Mitglieder ── */
.new-members-strip {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding-bottom: 0.4rem;
scrollbar-width: thin;
scrollbar-color: var(--color-secondary) transparent;
}
.new-members-strip::-webkit-scrollbar { height: 4px; }
.new-members-strip::-webkit-scrollbar-thumb { background: var(--color-secondary); border-radius: 2px; }
.nm-card {
flex: 0 0 160px;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
overflow: hidden;
text-decoration: none;
color: var(--color-text);
display: flex;
flex-direction: column;
transition: border-color 0.15s, box-shadow 0.15s;
}
.nm-card:hover { border-color: var(--color-primary); box-shadow: 0 4px 18px rgba(0,0,0,0.35); }
.nm-card-img {
width: 100%; aspect-ratio: 1; flex-shrink: 0;
overflow: hidden; background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 2.5rem; position: relative;
}
.nm-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.nm-card-body { padding: 0.6rem 0.65rem; display: flex; flex-direction: column; gap: 0.25rem; }
.nm-card-name { font-weight: 700; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nm-card-meta { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.nm-card-chip {
padding: 0.1rem 0.4rem; border-radius: 20px;
background: var(--color-secondary); font-size: 0.7rem; color: var(--color-muted);
}
.nm-card-desc {
font-size: 0.75rem; color: var(--color-muted); line-height: 1.35;
overflow: hidden; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
</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>
<!-- Aktive Spiele -->
<div id="activeGamesSection" style="display:none;">
<div class="section-label">Aktive Spiele 🎮</div>
<div class="active-game-list" id="activeGamesList"></div>
</div>
<!-- Aktiver Lock -->
<div id="activeLockSection" style="display:none;">
<div class="section-label">Aktiver Lock 🔒</div>
<div class="active-game-list" id="activeLockList"></div>
</div>
<!-- Einladungen -->
<div id="invitesSection" style="display:none;">
<div class="section-label">Einladungen 📨</div>
<div class="invite-list" id="invitesList"></div>
</div>
<!-- Freundschaftsanfragen -->
<div id="friendReqSection" style="display:none;">
<div class="section-label">Freundschaftsanfragen 🤝</div>
<div class="friend-req-strip" id="friendReqStrip"></div>
</div>
<!-- Aktivitäts-Grid -->
<div id="socialGridBlock" style="display:none;">
<div class="section-label">Aktivität</div>
<div class="activity-grid">
<div class="activity-col" id="visitorsCol" style="display:none;">
<div class="activity-col-header">
<span class="activity-col-title">👀 Besucher</span>
<a class="activity-col-link" href="/dating/besucher.html">Alle →</a>
</div>
<div class="activity-row" id="visitorsRow"></div>
</div>
<div class="activity-col" id="likesCol" style="display:none;">
<div class="activity-col-header">
<span class="activity-col-title">❤️ Likes</span>
<a class="activity-col-link" href="/dating/likes.html">Alle →</a>
</div>
<div class="activity-row" id="likesRow"></div>
</div>
<div class="activity-col" id="matchesCol" style="display:none;">
<div class="activity-col-header">
<span class="activity-col-title">💕 Matches</span>
<a class="activity-col-link" href="/dating/matches.html">Alle →</a>
</div>
<div class="activity-row" id="matchesRow"></div>
</div>
</div>
</div>
<!-- Neue Mitglieder -->
<div id="newMembersSection" style="display:none;">
<div class="section-label">Neue Mitglieder ✨</div>
<div class="new-members-strip" id="newMembersStrip"></div>
</div>
<!-- Meine angemeldeten Events -->
<div id="myEventsSection" style="display:none;">
<div class="section-label">Meine Veranstaltungen 🎟</div>
<div class="loc-event-list" id="myEventsList"></div>
</div>
<!-- Nächste Events abonnierter Locations -->
<div id="locEventsSection" style="display:none;">
<div class="section-label">Nächste Veranstaltungen 📍</div>
<div class="loc-event-list" id="locEventsList"></div>
</div>
<!-- Feed Compose + Vorschau -->
<div class="section-label">Feed 📰</div>
<div class="post-compose" id="homeCompose">
<div class="compose-type">
<label><input type="radio" name="homeBeitragTyp" value="TEXT" checked onchange="homeToggleUmfrage()"> Text</label>
<label><input type="radio" name="homeBeitragTyp" value="UMFRAGE" onchange="homeToggleUmfrage()"> Umfrage</label>
</div>
<textarea id="homeComposeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
<div class="compose-thumbs" id="homeComposeThumbs"></div>
<div class="umfrage-options" id="homeUmfrageOptions" style="display:none;">
<div id="homeOptionList"></div>
<button onclick="homeAddOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;margin-top:0.4rem;">+ Option</button>
</div>
<div class="compose-footer">
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
<label class="multi-toggle" id="homeMultiChoiceRow" style="display:none;">
<input type="checkbox" id="homeMultiChoice"> Multi-Choice
</label>
<label class="privacy-toggle">
<input type="checkbox" id="homeIsPublic"> Öffentlich
</label>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;">
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'homeComposeText')" title="Emoji einfügen">😊</button>
<label class="compose-action-btn" title="Fotos hinzufügen">📷
<input type="file" id="homeComposeBildFile" accept="image/*" multiple style="display:none;" onchange="homeSelectBilder(this)">
</label>
<button onclick="homeSubmitPost()" style="width:auto;margin:0;">Veröffentlichen</button>
</div>
</div>
</div>
<div id="feedSection" style="display:none;">
<div id="feedList"></div>
<a href="/community/feed.html"><button style="width:100%;margin-top:0.1rem;">Weiter zum Feed →</button></a>
</div>
</div>
</div>
<script src="/js/shared.js"></script>
<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 + '!';
loadActiveGames(user.userId);
loadActiveLock();
loadInvites();
loadFriendRequests();
loadVisitors();
loadMyEvents();
loadLocEvents();
loadFeed();
if (user.datingAktiv) {
loadWhoLikesMe();
loadMatches();
loadNewDatingMembers();
}
}
})
.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');
}
// ── Aktive Spiele ──────────────────────────────────────────────────────────
async function loadActiveGames(userId) {
try {
const items = [];
const [vRes, bRes] = await Promise.all([
fetch('/vanilla?userId=' + userId),
fetch('/bdsm?userId=' + userId)
]);
if (vRes.ok) {
const v = await vRes.json();
items.push({
icon: '🎭',
title: 'Vanilla-Spiel',
sub: 'Level ' + v.level + ' · gestartet ' + relativeTime(v.startZeit),
href: '/games/vanilla/vanillaingame.html?sessionId=' + v.sessionId
});
}
if (bRes.ok) {
const b = await bRes.json();
items.push({
icon: '⛓',
title: 'BDSM-Spiel',
sub: 'Level ' + b.level + ' · gestartet ' + relativeTime(b.startZeit),
href: '/games/bdsm/bdsmingame.html?sessionId=' + b.sessionId
});
}
if (!items.length) return;
const list = document.getElementById('activeGamesList');
list.innerHTML = items.map(i => `
<a class="active-game-card" href="${esc(i.href)}">
<div class="active-game-icon">${i.icon}</div>
<div class="active-game-body">
<div class="active-game-title">${esc(i.title)}</div>
<div class="active-game-sub">${esc(i.sub)}</div>
</div>
<span class="active-game-action">Weiterspielen →</span>
</a>`).join('');
document.getElementById('activeGamesSection').style.display = '';
} catch (_) {}
}
// ── Aktiver Lock ───────────────────────────────────────────────────────────
async function loadActiveLock() {
try {
const [cardRes, timeRes] = await Promise.all([
fetch('/keyholder/mylock'),
fetch('/keyholder/timelock/mylock')
]);
const items = [];
if (cardRes.status === 200) {
const d = await cardRes.json();
items.push({ lockId: d.lockId, page: 'activelock' });
}
if (timeRes.status === 200) {
const d = await timeRes.json();
items.push({ lockId: d.lockId, page: 'activetimelock' });
}
if (!items.length) return;
const list = document.getElementById('activeLockList');
list.innerHTML = items.map(i => `
<a class="active-game-card" href="/games/chastity/${i.page}.html?lockId=${esc(i.lockId)}">
<div class="active-game-icon">🔒</div>
<div class="active-game-body">
<div class="active-game-title">Keuschheitslock aktiv</div>
<div class="active-game-sub">${i.page === 'activetimelock' ? 'TimeLock' : 'CardLock'} · Tippen für Details</div>
</div>
<span class="active-game-action">Zum Lock →</span>
</a>`).join('');
document.getElementById('activeLockSection').style.display = '';
} catch (_) {}
}
// ── Einladungen ────────────────────────────────────────────────────────────
async function loadInvites() {
try {
const [vRes, bRes, cRes] = await Promise.all([
fetch('/vanilla/einladung/pending'),
fetch('/bdsm/einladung/pending'),
fetch('/lockee/invitations/mine')
]);
const items = [];
if (vRes.ok) {
const list = await vRes.json();
list.forEach(e => items.push({
avatar: e.inviterAvatar,
from: e.inviterName || 'Jemand',
type: 'Vanilla-Spieleinladung',
href: '/games/common/einladungen.html'
}));
}
if (bRes.ok) {
const list = await bRes.json();
list.forEach(e => items.push({
avatar: e.inviterAvatar,
from: e.inviterName || 'Jemand',
type: 'BDSM-Spieleinladung',
href: '/games/common/einladungen.html'
}));
}
if (cRes.ok) {
const list = await cRes.json();
list.forEach(e => items.push({
avatar: e.keyholderProfilePic,
from: e.keyholderName || 'Jemand',
type: 'Keuschheitslock-Einladung: ' + esc(e.lockName),
href: '/games/chastity/joinlock.html?token=' + esc(e.token)
}));
}
if (!items.length) return;
const container = document.getElementById('invitesList');
container.innerHTML = items.map(i => `
<a class="invite-card" href="${i.href}">
<div class="invite-avatar">
${i.avatar
? `<img src="data:image/png;base64,${i.avatar}" alt="">`
: '◉'}
</div>
<div class="invite-body">
<div class="invite-from">${esc(i.from)}</div>
<div class="invite-type">${i.type}</div>
</div>
<span class="invite-action">Ansehen →</span>
</a>`).join('');
document.getElementById('invitesSection').style.display = '';
} catch (_) {}
}
// ── Freundschaftsanfragen ──────────────────────────────────────────────────
async function loadFriendRequests() {
try {
const res = await fetch('/social/friends/pending');
if (!res.ok) return;
const requests = await res.json();
if (!requests.length) return;
const strip = document.getElementById('friendReqStrip');
strip.innerHTML = requests.map(r => {
const u = r.userProfile;
return `
<a class="friend-req-card" href="/community/freunde.html">
<div class="friend-req-avatar">
${u.profilePicture
? `<img src="data:image/png;base64,${u.profilePicture}" alt="${esc(u.name)}">`
: '◉'}
</div>
<span class="visitor-name">${esc(u.name)}</span>
<span class="friend-req-badge">+ Anfrage</span>
</a>`;
}).join('');
document.getElementById('friendReqSection').style.display = '';
} catch (_) {}
}
// ── Aktivitäts-Grid ───────────────────────────────────────────────────────
function socAvatarHtml(pic, blurred = false) {
if (!pic) return '◉';
return `<img src="data:image/png;base64,${pic}" alt=""${blurred ? ' style="filter:blur(5px);transform:scale(1.1)"' : ''}>`;
}
function showSocialGrid() {
document.getElementById('socialGridBlock').style.display = '';
}
async function loadWhoLikesMe() {
try {
const res = await fetch('/dating/who-likes-me');
if (!res.ok) return;
const data = await res.json();
if (data.total === 0) return;
document.getElementById('likesRow').innerHTML = data.likers.slice(0, 4).map(l => {
if (data.premium && l.userId) {
return `<a class="soc-card" href="/community/benutzer.html?userId=${l.userId}">
<div class="soc-avatar">${socAvatarHtml(l.profilePicture)}</div>
<span class="soc-name">${esc(l.name)}</span>
</a>`;
}
return `<div class="soc-card">
<div class="soc-avatar">
${socAvatarHtml(l.profilePicture, true)}
<span class="soc-lock">🔒</span>
</div>
<span class="soc-sub-accent">Premium</span>
</div>`;
}).join('');
document.getElementById('likesCol').style.display = '';
showSocialGrid();
} catch (_) {}
}
async function loadMatches() {
try {
const res = await fetch('/dating/matches');
if (!res.ok) return;
const matches = await res.json();
if (!matches.length) return;
document.getElementById('matchesRow').innerHTML = matches.slice(0, 4).map(m => `
<a class="soc-card" href="/community/benutzer.html?userId=${m.userId}">
<div class="soc-avatar">${socAvatarHtml(m.profilePicture)}</div>
<span class="soc-name">${esc(m.name)}</span>
<span class="soc-sub-accent">♥</span>
</a>`).join('');
document.getElementById('matchesCol').style.display = '';
showSocialGrid();
} catch (_) {}
}
// ── Neue Mitglieder ───────────────────────────────────────────────────────
async function loadNewDatingMembers() {
try {
const res = await fetch('/user/new-members');
if (!res.ok) return;
const members = await res.json();
if (!members.length) return;
const strip = document.getElementById('newMembersStrip');
strip.innerHTML = members.map(p => {
const img = p.profilePicture
? `<img src="data:image/png;base64,${p.profilePicture}" alt="${esc(p.name)}" loading="lazy">`
: `<span>👤</span>`;
const chips = [
p.alter ? `<span class="nm-card-chip">${p.alter} J.</span>` : '',
p.geschlecht ? `<span class="nm-card-chip">${esc(p.geschlecht)}</span>` : '',
p.neigung ? `<span class="nm-card-chip">${esc(p.neigung)}</span>` : '',
p.datingStadt ? `<span class="nm-card-chip">${esc(p.datingStadt)}</span>` : '',
].filter(Boolean).join('');
const desc = p.beschreibung
? `<div class="nm-card-desc">${esc(p.beschreibung)}</div>` : '';
return `<a class="nm-card" href="/community/benutzer.html?userId=${p.userId}">
<div class="nm-card-img">${img}</div>
<div class="nm-card-body">
<div class="nm-card-name">${esc(p.name)}</div>
<div class="nm-card-meta">${chips}</div>
${desc}
</div>
</a>`;
}).join('');
document.getElementById('newMembersSection').style.display = '';
} catch (_) {}
}
// ── Feed Compose ──────────────────────────────────────────────────────────
let homeComposeBilder = [];
function homeToggleUmfrage() {
const isUmfrage = document.querySelector('input[name="homeBeitragTyp"]:checked').value === 'UMFRAGE';
document.getElementById('homeUmfrageOptions').style.display = isUmfrage ? '' : 'none';
document.getElementById('homeMultiChoiceRow').style.display = isUmfrage ? '' : 'none';
if (isUmfrage && document.getElementById('homeOptionList').children.length === 0) {
homeAddOption(); homeAddOption();
}
}
function homeAddOption() {
const list = document.getElementById('homeOptionList');
const idx = list.children.length;
const row = document.createElement('div');
row.className = 'umfrage-option-row';
row.innerHTML = `<input type="text" placeholder="Option ${idx + 1}" maxlength="100">
<button onclick="this.parentElement.remove()">✕</button>`;
list.appendChild(row);
}
function homeSelectBilder(input) {
[...input.files].forEach(f => { if (f.type.startsWith('image/')) homeProcessImage(f); });
input.value = '';
}
function homeProcessImage(file) {
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
const maxSize = 1024;
const canvas = document.createElement('canvas');
const scale = Math.min(maxSize / img.width, maxSize / img.height, 1);
canvas.width = Math.round(img.width * scale);
canvas.height = Math.round(img.height * scale);
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
homeComposeBilder.push(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
homeRenderThumbs();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function homeRenderThumbs() {
const container = document.getElementById('homeComposeThumbs');
container.innerHTML = '';
homeComposeBilder.forEach((b, i) => {
const div = document.createElement('div');
div.className = 'compose-thumb';
div.innerHTML = `<img src="data:image/jpeg;base64,${b}" alt="">
<button class="compose-thumb-remove" onclick="homeRemoveThumb(${i})">✕</button>`;
container.appendChild(div);
});
container.style.display = homeComposeBilder.length > 0 ? 'flex' : 'none';
}
function homeRemoveThumb(idx) {
homeComposeBilder.splice(idx, 1);
homeRenderThumbs();
}
async function homeSubmitPost() {
const text = document.getElementById('homeComposeText').value.trim();
if (!text && homeComposeBilder.length === 0) return;
const beitragTyp = document.querySelector('input[name="homeBeitragTyp"]:checked').value;
const multiChoice = document.getElementById('homeMultiChoice').checked;
const isPublic = document.getElementById('homeIsPublic').checked;
let optionen = [];
if (beitragTyp === 'UMFRAGE') {
optionen = Array.from(document.getElementById('homeOptionList').querySelectorAll('input'))
.map(i => i.value.trim()).filter(v => v);
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
}
const res = await fetch('/feed/posts', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ beitragTyp, text, multiChoice, optionen, bilder: [...homeComposeBilder], isPublic })
});
if (!res.ok) return;
const post = await res.json();
// Reset
document.getElementById('homeComposeText').value = '';
homeComposeBilder = [];
homeRenderThumbs();
document.querySelector('input[name="homeBeitragTyp"][value="TEXT"]').checked = true;
homeToggleUmfrage();
document.getElementById('homeMultiChoice').checked = false;
document.getElementById('homeIsPublic').checked = false;
document.getElementById('homeOptionList').innerHTML = '';
// Prepend in Vorschau
const feedList = document.getElementById('feedList');
feedList.insertAdjacentHTML('afterbegin', renderHomePostCard(post));
document.getElementById('feedSection').style.display = '';
}
// Drag & Drop
const homeCompose = document.getElementById('homeCompose');
if (homeCompose) {
homeCompose.addEventListener('dragover', e => {
e.preventDefault();
if ([...e.dataTransfer.items].some(i => i.type.startsWith('image/')))
homeCompose.classList.add('drag-over');
});
homeCompose.addEventListener('dragleave', e => {
if (!homeCompose.contains(e.relatedTarget)) homeCompose.classList.remove('drag-over');
});
homeCompose.addEventListener('drop', e => {
e.preventDefault();
homeCompose.classList.remove('drag-over');
[...e.dataTransfer.files].filter(f => f.type.startsWith('image/')).forEach(homeProcessImage);
});
}
// ── Feed-Vorschau ──────────────────────────────────────────────────────────
const homePostCache = {};
function homeOpenPost(postId) {
const p = homePostCache[postId];
if (p) sessionStorage.setItem('feedOpenPost', JSON.stringify(p));
window.location.href = '/community/feed.html';
}
function renderHomePostCard(p) {
homePostCache[p.postId] = p;
const avatarHtml = p.authorPicture
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
: '◉';
const privacyLabel = !p.isPublic ? ' <span style="font-size:0.7rem;color:var(--color-muted);">🔒</span>' : '';
const groupBadge = p.postType === 'GROUP' && p.gruppeId
? `<span class="gruppe-badge">👥 ${esc(p.gruppeName)}</span>`
: '';
const bildHtml = bilderCarousel(p.bilder);
let umfrageHtml = '';
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
umfrageHtml = '<div style="margin-top:0.5rem;">' + p.optionen.map(o => {
const pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
return `<div class="umfrage-option-bar${voted ? ' voted' : ''}">
<div class="umfrage-bar-fill" style="width:${pct}%"></div>
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
</div>`;
}).join('') + `<div class="umfrage-total">${totalVotes} Stimme${totalVotes !== 1 ? 'n' : ''}</div></div>`;
}
return `<div class="post-card" onclick="homeOpenPost('${p.postId}')" style="cursor:pointer">
<div class="post-header">
<div class="post-avatar">${avatarHtml}</div>
<div>
<div class="post-author">${esc(p.authorName)}${privacyLabel}</div>
<div class="post-meta">${fmtDate(p.createdAt)}${groupBadge}</div>
</div>
</div>
<div class="post-text">${esc(p.text || '')}</div>
${bildHtml}
${umfrageHtml}
<div class="post-actions">
<button class="post-action-btn${p.likedByMe ? ' active' : ''}">♥ <span>${p.likeCount}</span></button>
<button class="post-action-btn">💬 <span>${p.kommentarCount}</span></button>
</div>
</div>`;
}
async function loadFeed() {
try {
const res = await fetch('/feed/mine?size=3&page=0');
if (!res.ok) return;
const data = await res.json();
const posts = data.posts;
if (!posts || !posts.length) return;
document.getElementById('feedList').innerHTML = posts.map(renderHomePostCard).join('');
document.getElementById('feedSection').style.display = '';
} catch (_) {}
}
// ── Events ────────────────────────────────────────────────────────────────
function renderEventCards(events, listId, sectionId) {
if (!events.length) return;
const list = document.getElementById(listId);
list.innerHTML = events.map(e => {
const thumb = e.imageData
? `<img src="data:image/jpeg;base64,${e.imageData}" alt="">`
: '🗓';
const date = new Date(e.startAt);
const dateStr = date.toLocaleDateString('de-DE', { weekday:'short', day:'numeric', month:'short' })
+ ', ' + date.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
return `
<a class="loc-event-card" href="/community/event-detail.html?id=${e.eventId}">
<div class="loc-event-thumb">${thumb}</div>
<div class="loc-event-body">
<div class="loc-event-location">${esc(e.locationName)}</div>
<div class="loc-event-title">${esc(e.title)}</div>
<div class="loc-event-date">${dateStr}</div>
</div>
</a>`;
}).join('');
document.getElementById(sectionId).style.display = '';
}
async function loadMyEvents() {
try {
const res = await fetch('/location-events/attending-next');
if (!res.ok) return;
const events = await res.json();
renderEventCards(events, 'myEventsList', 'myEventsSection');
} catch (_) {}
}
async function loadLocEvents() {
try {
const res = await fetch('/location-events/followed-next');
if (!res.ok) return;
const events = await res.json();
renderEventCards(events, 'locEventsList', 'locEventsSection');
} catch (_) {}
}
// ── Profilbesucher ────────────────────────────────────────────────────────
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;
document.getElementById('visitorsRow').innerHTML = visitors.slice(0, 4).map(v => `
<a class="soc-card" href="/community/benutzer.html?userId=${v.userId}">
<div class="soc-avatar">${socAvatarHtml(v.profilePicture)}</div>
<span class="soc-name">${esc(v.name)}</span>
<span class="soc-sub">${relativeTime(v.visitedAt)}</span>
</a>`).join('');
document.getElementById('visitorsCol').style.display = '';
showSocialGrid();
} catch (_) {}
}
</script>
</body>
</html>