Files
xxx-sphere-web/bin/main/static/userhome.html
Mario fdc0cfce95
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Aufgabenverwaltung angepasst, Eventseite weiter bearbeitet
2026-04-14 22:04:47 +02:00

1031 lines
49 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">
<link rel="stylesheet" href="/css/community.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: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 0.5rem;
}
@media (max-width: 680px) {
.activity-grid { flex-direction: column; }
}
.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: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem;
}
/* Avatar-Karte */
.soc-card {
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; }
/* ── Post-Cards (Home: klickbar + Hover) ── */
.post-card { cursor:pointer; transition:border-color 0.15s; }
.post-card:hover { border-color:var(--color-primary); }
/* ── Spiel starten ── */
.start-game-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.75rem;
}
.start-game-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 1.25rem 1rem;
text-decoration: none;
color: var(--color-text);
transition: border-color 0.15s, background 0.15s;
text-align: center;
}
.start-game-card:hover {
border-color: var(--color-primary);
background: rgba(var(--color-primary-rgb,233,69,96),0.06);
}
.start-game-icon { font-size: 2rem; line-height: 1; }
.start-game-title { font-size: 0.9rem; font-weight: 600; }
/* ── 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>
<!-- Kein Spiel aktiv Starten -->
<div id="startGameSection" style="display:none;">
<div class="section-label">Spiel starten 🎮</div>
<div class="start-game-grid">
<a href="/games/vanilla/neuvanilla.html" class="start-game-card">
<div class="start-game-icon">🎭</div>
<div class="start-game-title">Vanilla Game starten</div>
</a>
<a href="/games/bdsm/neubdsm.html" class="start-game-card">
<div class="start-game-icon"></div>
<div class="start-game-title">BDSM-Game starten</div>
</a>
<a href="/games/chastity/neulock.html" class="start-game-card">
<div class="start-game-icon">🔒</div>
<div class="start-game-title">Chastity-Lock starten</div>
</a>
</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">
<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>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
<button onclick="homeAddOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
<label class="multi-toggle">
<input type="checkbox" id="homeMultiChoice"> Mehrfachauswahl möglich
</label>
</div>
</div>
<div class="compose-footer">
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
<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 type="button" id="homeUmfrageBtn" class="compose-action-btn" onclick="homeToggleUmfrage(this)" title="Umfrage hinzufügen">📊</button>
<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>
<!-- Post Lightbox -->
<div class="lightbox" id="postLightbox">
<div class="lb-layout">
<button class="lb-close" onclick="closeLb()"></button>
<div class="lb-post-side" id="lbPostBody"></div>
<div class="lb-comments-panel">
<div class="lb-comments-header">Kommentare</div>
<div class="lb-comments-list" id="lbCommentsList"></div>
<div class="lb-comment-compose">
<textarea id="lbCommentInput" placeholder="Kommentar schreiben…" maxlength="500" rows="3"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();postLbComment()}"></textarea>
<div class="lb-comment-compose-actions">
<button onclick="postLbComment()">Senden</button>
</div>
</div>
</div>
</div>
</div>
<script src="/js/shared.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/hashtag.js"></script>
<script src="/js/nav.js"></script>
<script>
let myUserId = null;
fetch('/login/me')
.then(r => {
if (r.status === 401) { window.location.href = '/login.html'; return null; }
return r.json();
})
.then(user => {
if (user) {
myUserId = user.userId;
initLb(user.userId);
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
Promise.all([loadActiveGames(user.userId), loadActiveLock()]).then(() => {
const hasGames = document.getElementById('activeGamesSection').style.display !== 'none';
const hasLock = document.getElementById('activeLockSection').style.display !== 'none';
if (!hasGames && !hasLock) {
document.getElementById('startGameSection').style.display = '';
}
});
loadInvites();
loadFriendRequests();
loadVisitors();
loadMyEvents();
loadLocEvents();
loadFeed();
attachHashtagAutocomplete(document.getElementById('homeComposeText'));
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(btn) {
const options = document.getElementById('homeUmfrageOptions');
const isShowing = options.style.display !== 'none';
options.style.display = isShowing ? 'none' : '';
if (btn) btn.classList.toggle('active', !isShowing);
if (!isShowing && document.getElementById('homeOptionList').children.length === 0) {
homeAddOption(); homeAddOption();
}
}
function homeResetUmfrage() {
document.getElementById('homeUmfrageOptions').style.display = 'none';
document.getElementById('homeOptionList').innerHTML = '';
document.getElementById('homeUmfrageBtn').classList.remove('active');
}
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 homeRenderThumbs() {
renderBilderThumbs(homeComposeBilder, 'homeComposeThumbs', i => {
homeComposeBilder.splice(i, 1);
homeRenderThumbs();
});
}
function homeSelectBilder(input) {
[...input.files].forEach(f => processImageFile(f, homeComposeBilder, homeRenderThumbs));
input.value = '';
}
async function homeSubmitPost() {
const text = document.getElementById('homeComposeText').value.trim();
const hasUmfrage = document.getElementById('homeUmfrageOptions').style.display !== 'none';
if (!text && homeComposeBilder.length === 0) return;
const beitragTyp = hasUmfrage ? 'UMFRAGE' : 'TEXT';
const multiChoice = document.getElementById('homeMultiChoice').checked;
const isPublic = document.getElementById('homeIsPublic').checked;
let optionen = [];
if (hasUmfrage) {
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();
homeResetUmfrage();
document.getElementById('homeMultiChoice').checked = false;
document.getElementById('homeIsPublic').checked = false;
// 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(f => processImageFile(f, homeComposeBilder, homeRenderThumbs));
});
}
// ── Feed-Vorschau ──────────────────────────────────────────────────────────
const homePostCache = {};
function homeOpenPost(postId) {
const p = homePostCache[postId];
if (!p) return;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = renderHomePostCard(p);
const card = tempDiv.firstElementChild;
if (card) {
card.querySelectorAll('.post-actions').forEach(el => el.remove());
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
_lbSetupContent(postId, 'hp', p.bilder);
}
loadLbComments(p.postId, p.postType || 'FEED');
document.getElementById('postLightbox').classList.add('open');
document.body.style.overflow = 'hidden';
}
document.getElementById('postLightbox').addEventListener('click', e => {
if (e.target === document.getElementById('postLightbox')) closeLb();
});
// ── Like / Delete ──────────────────────────────────────────────────────────
async function likeHomePost(postId, postType) {
const ep = postType === 'GROUP'
? `/gruppen/${document.getElementById('hpc-'+postId)?.dataset?.gruppeId}/posts/${postId}/like`
: `/feed/posts/${postId}/like`;
await fetch(ep, { method: 'POST' });
const btn = document.getElementById('hlk-' + postId);
const lc = document.getElementById('hlkc-' + postId);
const was = btn.classList.contains('active');
btn.classList.toggle('active', !was);
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
}
async function deleteHomePost(postId) {
if (!confirm('Post löschen?')) return;
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
if (res.ok) document.getElementById('hpc-' + postId)?.remove();
}
// ── Post-Karte ────────────────────────────────────────────────────────────
const homeEditBilder = new Map();
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 = bilderGrid(p.bilder);
const editedLabel = p.editedAt ? ` <span style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>` : '';
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>`;
}
const canOwn = p.postType === 'FEED' && p.authorId === myUserId;
const ownBtns = canOwn
? `<div style="margin-left:auto;display:flex;gap:0.4rem;">
<button class="post-action-btn" onclick="event.stopPropagation();startHomeEdit('${p.postId}')" title="Bearbeiten" style="color:var(--color-muted)">✏</button>
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteHomePost('${p.postId}')" title="Löschen">🗑</button>
</div>`
: '';
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
const authorUrl = p.posterType === 'LOCATION'
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
: `/community/benutzer.html?userId=${p.authorId}`;
return `<div class="post-card" id="hpc-${p.postId}"${gruppeIdAttr} onclick="homeOpenPost('${p.postId}')">
<div class="post-header">
<div class="post-avatar"><a href="${authorUrl}" onclick="event.stopPropagation()" style="display:contents;">${avatarHtml}</a></div>
<div>
<div class="post-author"><a href="${authorUrl}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${esc(p.authorName)}</a>${privacyLabel}</div>
<div class="post-meta" id="hpm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
</div>
${ownBtns}
</div>
<div id="hpva-${p.postId}">
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
<div id="hpbi-${p.postId}">${bildHtml}</div>
</div>
<div id="hpea-${p.postId}" style="display:none;"></div>
<div id="hpum-${p.postId}">${umfrageHtml}</div>
<div class="post-actions">
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="hlk-${p.postId}" onclick="event.stopPropagation();likeHomePost('${p.postId}','${p.postType}')">♥ <span id="hlkc-${p.postId}">${p.likeCount}</span></button>
<button class="post-action-btn" onclick="event.stopPropagation();homeOpenPost('${p.postId}')">💬 <span id="hkc-${p.postId}">${p.kommentarCount}</span></button>
</div>
</div>`;
}
// ── Post-Bearbeitung (Home) ───────────────────────────────────────────────
function startHomeEdit(postId) {
const data = homePostCache[postId];
if (!data) return;
startPostEdit({ postId, prefix: 'hp', data, editBilderMap: homeEditBilder,
saveFn: 'saveHomeEdit', cancelFn: 'cancelHomeEdit',
addImgFn: 'homeEditAddImg', addOptionFn: 'homeEditAddOption', rmImgFn: 'homeEditRmImg' });
}
function cancelHomeEdit(postId) { cancelPostEdit(postId, 'hp', homeEditBilder); }
function homeEditRmImg(postId, idx) {
homeEditBilder.get(postId).splice(idx, 1);
_renderEditThumbs(homeEditBilder, postId, 'hp', 'homeEditRmImg');
}
function homeEditAddImg(input, postId) {
[...input.files].forEach(f => processImageFile(f, homeEditBilder.get(postId), () => _renderEditThumbs(homeEditBilder, postId, 'hp', 'homeEditRmImg')));
input.value = '';
}
function homeEditAddOption(postId) { editAddOptionRow(`hpeo-${postId}`); }
async function saveHomeEdit(postId) {
const cached = homePostCache[postId];
await savePostEdit({ postId, prefix: 'hp', endpoint: `/feed/posts/${postId}`,
isUmfrage: cached?.beitragTyp === 'UMFRAGE', editBilderMap: homeEditBilder,
onSuccess: updated => {
homePostCache[postId] = { ...cached, text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice };
const totalVotes = (updated.optionen || []).reduce((s, o) => s + o.stimmenCount, 0);
const umfrageHtml = updated.optionen?.length > 0
? '<div style="margin-top:0.5rem;">' + updated.optionen.map(o => {
const pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
return `<div class="umfrage-option-bar"><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>`
: '';
applyPostEditDom(postId, 'hp', updated, umfrageHtml);
}
});
}
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>