Files
xxx-sphere-web/bin/main/static/community/benutzer.html
Mario 87c85b1b17
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Bugfixes, Dating angefangen
2026-04-01 22:06:46 +02:00

1416 lines
66 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>Profil xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Profile Header ── */
.profil-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
text-align: center;
}
.profil-avatar {
width: 110px;
height: 110px;
border-radius: 50%;
border: 3px solid var(--color-secondary);
background: var(--color-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--color-muted);
overflow: hidden;
flex-shrink: 0;
cursor: zoom-in;
}
.profil-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.profil-name {
font-size: 1.4rem;
font-weight: 700;
color: var(--color-text);
}
/* ── Tags ── */
.profil-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
justify-content: center;
margin-bottom: 0.25rem;
}
.profil-tag {
background: var(--color-secondary);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 20px;
padding: 0.3rem 0.75rem;
font-size: 0.8rem;
color: var(--color-muted);
display: flex;
align-items: center;
gap: 0.35rem;
}
.profil-tag .label { color: rgba(255,255,255,0.4); font-size: 0.7rem; }
.profil-tag .value { color: var(--color-text); }
/* ── Action buttons ── */
.profil-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
width: 100%;
padding: 0.85rem 0;
border-top: 1px solid var(--color-secondary);
border-bottom: 1px solid var(--color-secondary);
margin: 0.5rem 0 0.25rem;
}
.profil-actions:empty { display: none; }
.profil-actions button,
.profil-actions a.btn {
margin-top: 0;
width: auto;
padding: 0.55rem 1.2rem;
}
/* ── Section labels ── */
.section-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 1.75rem 0 0.75rem;
border-top: 1px solid var(--color-secondary);
padding-top: 1.5rem;
}
/* ── Gallery Carousel ── */
.gallery-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.4rem;
}
.gallery-thumb {
aspect-ratio: 1;
overflow: hidden;
border-radius: 6px;
background: var(--color-secondary);
cursor: pointer;
position: relative;
}
.gallery-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: opacity 0.2s;
}
.gallery-thumb:hover img { opacity: 0.85; }
.gallery-thumb-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.65));
padding: 0.25rem 0.4rem 0.3rem;
display: flex;
gap: 0.35rem;
opacity: 0;
transition: opacity 0.2s;
}
.gallery-thumb:hover .gallery-thumb-overlay { opacity: 1; }
.gallery-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
}
.gallery-nav button {
background: var(--color-secondary);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
color: var(--color-text);
padding: 0.35rem 0.9rem;
cursor: pointer;
font-size: 1rem;
margin: 0;
width: auto;
transition: background 0.15s;
}
.gallery-nav button:hover { background: var(--color-primary); }
.gallery-nav button:disabled { opacity: 0.3; cursor: default; background: var(--color-secondary); }
.gallery-nav .gallery-pos { font-size: 0.8rem; color: var(--color-muted); }
/* ── Friends strip ── */
.friends-strip {
display: flex;
gap: 0.75rem;
flex-wrap: nowrap;
overflow: hidden;
}
.friend-thumb {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
cursor: pointer;
flex: 0 0 calc(20% - 0.6rem);
min-width: 0;
}
.friend-thumb img, .friend-thumb .friend-avatar-placeholder {
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
background: var(--color-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: var(--color-muted);
flex-shrink: 0;
}
.friend-thumb span {
font-size: 0.75rem;
color: var(--color-text);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 64px;
}
.friend-thumb:hover span { color: var(--color-primary); }
/* ── Description ── */
.profil-beschreibung {
background: var(--color-secondary);
border-radius: 8px;
padding: 1rem;
font-size: 0.95rem;
color: var(--color-text);
line-height: 1.55;
white-space: pre-wrap;
}
/* ── Pinnwand ── */
.pinnwand-write {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
align-items: flex-start;
}
.pinnwand-write textarea {
flex: 1;
padding: 0.6rem 0.9rem;
border: 1px solid var(--color-secondary);
border-radius: 6px;
background: var(--color-secondary);
color: var(--color-text);
font-size: 0.9rem;
outline: none;
resize: none;
min-height: 56px;
font-family: inherit;
transition: border-color 0.2s;
}
.pinnwand-write textarea:focus { border-color: var(--color-primary); }
.pinnwand-write button {
width: auto;
padding: 0.55rem 1rem;
white-space: nowrap;
flex-shrink: 0;
align-self: flex-end;
}
.pinnwand-entry {
background: var(--color-secondary);
border-radius: 8px;
padding: 0.85rem 1rem;
margin-bottom: 0.65rem;
}
.entry-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.5rem;
}
.entry-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--color-card);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: var(--color-muted);
flex-shrink: 0;
}
.entry-avatar img { width: 100%; height: 100%; object-fit: cover; }
.entry-meta { flex: 1; }
.entry-meta .author { font-weight: 600; font-size: 0.88rem; color: var(--color-text); }
.entry-meta .time { font-size: 0.74rem; color: var(--color-muted); }
.entry-text {
font-size: 0.92rem;
color: var(--color-text);
line-height: 1.5;
margin-bottom: 0.6rem;
white-space: pre-wrap;
}
.entry-actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
/* ── Keyholder-Angebote Tab ── */
.kh-offer-card {
background:var(--color-card); border:1px solid var(--color-secondary);
border-radius:10px; padding:0.75rem 1rem; margin-bottom:0.6rem;
display:flex; align-items:center; gap:0.85rem;
}
.kh-offer-type-icon {
position:relative; width:2.2rem; height:2.2rem; flex-shrink:0;
display:flex; align-items:center; justify-content:center;
}
.kh-offer-type-icon .icon-base { font-size:1.8rem; line-height:1; }
.kh-offer-type-icon img.icon-base { width:1.8rem; height:1.8rem; object-fit:contain; }
.kh-offer-type-icon .icon-lock {
position:absolute; bottom:-2px; right:-4px;
font-size:1.5rem; line-height:1;
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
}
.kh-offer-body { flex:1; min-width:0; }
.kh-offer-name { font-weight:700; font-size:0.95rem; margin-bottom:0.2rem;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.kh-offer-meta { font-size:0.78rem; color:var(--color-muted); display:flex; flex-wrap:wrap; gap:0.4rem; }
.kh-offer-badge {
display:inline-block; font-size:0.72rem; padding:0.1rem 0.45rem;
border-radius:4px; background:rgba(255,255,255,0.07); border:1px solid var(--color-secondary);
}
.kh-offer-badge.direct { background:rgba(46,204,113,0.12); border-color:rgba(46,204,113,0.3); color:#2ecc71; }
.kh-offer-badge.confirm { background:rgba(230,126,34,0.12); border-color:rgba(230,126,34,0.3); color:#e67e22; }
/* ── Comments (section container) ── */
.comments-section {
margin-top: 0.65rem;
padding-top: 0.65rem;
border-top: 1px solid rgba(255,255,255,0.06);
}
/* ── Profil Tabs ── */
.profil-tabs { display:flex; gap:0; margin-bottom:1.25rem; border-bottom:1px solid var(--color-secondary); }
.profil-tab-btn { background:none; border:none; border-bottom:3px solid transparent; border-radius:0; padding:0.6rem 1.25rem; font-size:0.9rem; font-weight:600; color:var(--color-muted); cursor:pointer; margin-bottom:-1px; transition:color 0.15s,border-color 0.15s; }
.profil-tab-btn:hover { color:var(--color-text); background:none; }
.profil-tab-btn.active { color:var(--color-primary); border-bottom-color:var(--color-primary); }
.profil-tab-panel { display:none; }
.profil-tab-panel.active { display:block; }
/* ── Vorlieben Tab ── */
.vorlieben-group { margin-bottom:1.25rem; }
.vorlieben-group-title {
font-size:0.78rem; font-weight:700; color:var(--color-muted);
text-transform:uppercase; letter-spacing:0.05em;
margin-bottom:0.5rem; padding-bottom:0.3rem;
border-bottom:1px solid var(--color-secondary);
}
.vorlieben-chips { display:flex; flex-wrap:wrap; gap:0.4rem; }
.vorliebe-chip {
display:inline-block; padding:0.25rem 0.65rem; border-radius:999px;
font-size:0.82rem; border:1px solid var(--color-secondary);
background:var(--color-card); color:var(--color-text);
}
.vorliebe-chip.bw-UNBEDINGT { border-color:#2e7d32; color:#2e7d32; }
.vorliebe-chip.bw-MAG_ICH { border-color:#81c784; color:#81c784; }
.vorliebe-chip.bw-WILL_AUSPROBIEREN{ border-color:#1e88e5; color:#1e88e5; }
.vorliebe-chip.bw-NEUTRAL { border-color:#fdd835; color:#fdd835; }
.vorliebe-chip.bw-EHER_NICHT { border-color:#fb8c00; color:#fb8c00; }
.vorliebe-chip.bw-GEHT_GAR_NICHT { border-color:#e53935; color:#e53935; }
/* ── Post cards (profile posts tab) ── */
.post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; }
.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-date { font-size:0.75rem; color:var(--color-muted); margin-left:auto; }
.post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
.post-bild { width:100%; max-height:360px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; transition:opacity 0.2s; }
.post-bild-wrap { position:relative; cursor:pointer; display:block; }
.post-bild-wrap:hover .post-bild { opacity:0.82; }
.post-bild-hover-icon { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; opacity:0; transition:opacity 0.2s; pointer-events:none; font-size:1.6rem; }
.post-bild-wrap:hover .post-bild-hover-icon { opacity:1; }
.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); cursor:pointer; font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; margin:0; width:auto; }
.post-action-btn:hover { color:var(--color-primary); background:none; }
.post-action-btn.active { color:var(--color-primary); }
.post-delete { margin-left:auto; }
.post-delete:hover { color:#c0392b !important; }
.umfrage-option-bar { margin:0.3rem 0; cursor:pointer; border-radius:6px; overflow:hidden; border:1px solid var(--color-secondary); position:relative; transition:border-color 0.15s; }
.umfrage-option-bar:hover { border-color:var(--color-primary); }
.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); transition:width 0.4s; }
.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; }
/* ── Post / Bild Lightbox ── */
.lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:400; align-items:center; justify-content:center; }
.lightbox.open { display:flex; }
.lb-layout { display:flex; max-width:920px; width:95vw; max-height:90vh; background:var(--color-card); border-radius:12px; overflow:hidden; position:relative; }
.lb-close { position:absolute; top:0.6rem; right:0.6rem; background:rgba(0,0,0,0.55); border:none; color:#fff; font-size:1.1rem; width:2rem; height:2rem; border-radius:50%; cursor:pointer; z-index:10; display:flex; align-items:center; justify-content:center; padding:0; margin:0; }
.lb-post-side { flex:1; overflow-y:auto; padding:1.25rem; border-right:1px solid var(--color-secondary); min-width:0; }
.lb-comments-panel { width:300px; flex-shrink:0; display:flex; flex-direction:column; }
.lb-comments-header { font-size:0.78rem; font-weight:600; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.06em; padding:0.7rem 1rem; border-bottom:1px solid var(--color-secondary); flex-shrink:0; }
.lb-comments-list { flex:1; overflow-y:auto; padding:0.75rem; }
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; flex-direction:column; gap:0.5rem; flex-shrink:0; }
.lb-comment-compose textarea { width:100%; font-size:0.85rem; padding:0.35rem 0.6rem; resize:none; background:var(--color-secondary); border:1px solid var(--color-secondary); border-radius:6px; color:var(--color-text); font-family:inherit; outline:none; transition:border-color 0.2s; box-sizing:border-box; }
.lb-comment-compose textarea:focus { border-color:var(--color-primary); }
.lb-comment-compose-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
.lb-img-nav { display:flex; gap:0.75rem; align-items:center; justify-content:center; margin-top:0.75rem; flex-wrap:wrap; }
.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; }
@media (max-width:650px) { .lb-layout { flex-direction:column; max-height:95vh; } .lb-post-side { border-right:none; border-bottom:1px solid var(--color-secondary); max-height:55vh; } .lb-comments-panel { width:100%; } }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<p id="loadingHint" style="color:var(--color-muted);">Wird geladen…</p>
<div id="profileView" style="display:none;">
<!-- Header -->
<div class="profil-header">
<div class="profil-avatar" id="profilePic" onclick="openAvatarLightbox()"></div>
<div class="profil-name" id="profileName"></div>
<div class="profil-actions" id="profileActions"></div>
<div class="profil-tags" id="profileTags"></div>
</div>
<!-- Description (always above tabs) -->
<div class="section-label" id="beschreibungLabel" style="display:none;">Über mich</div>
<div class="profil-beschreibung" id="profilBeschreibung" style="display:none;"></div>
<!-- Fotos (immer sichtbar) -->
<div id="gallerieCarousel" style="display:none; margin-top:1rem;">
<div class="gallery-strip" id="galleryStrip"></div>
<div class="gallery-nav">
<button id="galleryPrev" onclick="galleryPage(-1)" disabled>&#8592;</button>
<span class="gallery-pos" id="galleryPos"></span>
<button id="galleryNext" onclick="galleryPage(1)">&#8594;</button>
</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>
<div class="friends-strip" id="friendsStrip"></div>
<div class="gallery-nav" id="friendsNav" style="display:none;">
<button id="friendsPrev" onclick="friendsPage(-1)" disabled>&#8592;</button>
<span class="gallery-pos" id="friendsPos"></span>
<button id="friendsNext" onclick="friendsPage(1)">&#8594;</button>
</div>
</div>
<!-- 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="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this)">Spielhistorie</button>
<button class="profil-tab-btn" id="tabBtnKhOffers" onclick="switchProfilTab('khoffers', this)">Keyholder-Angebote</button>
</div>
<!-- Feed Tab (vorausgewählt) -->
<div class="profil-tab-panel active" id="tab-posts">
<div id="profilPostsFeed"></div>
<p id="profilPostsEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Noch keine Posts.</p>
<div id="profilPostsSentinel" style="height:1px;"></div>
</div>
<!-- Pinnwand Tab -->
<div class="profil-tab-panel" id="tab-pinnwand">
<div class="pinnwand-write">
<textarea id="pinnwandText" placeholder="Schreib etwas auf die Pinnwand…" rows="2"></textarea>
<button onclick="postPinnwand()">Posten</button>
</div>
<div id="pinnwandList"></div>
</div>
<!-- Spielhistorie Tab -->
<div class="profil-tab-panel" id="tab-gamehistory">
<div id="gameHistoryList" style="margin-top:0.75rem;"></div>
<p id="gameHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p>
</div>
<!-- Keyholder-Angebote Tab -->
<div class="profil-tab-panel" id="tab-khoffers">
<div id="khOffersList" style="margin-top:0.75rem;"></div>
<p id="khOffersEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine Keyholder-Angebote vorhanden.</p>
</div>
</div>
</div>
</div>
<!-- Post / Bild 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 type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
<button onclick="postLbComment()">Senden</button>
</div>
</div>
</div>
</div>
</div>
<script src="/js/shared.js"></script>
<script src="/js/image-viewer.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/meldung.js"></script>
<script>
// ── State ──
const params = new URLSearchParams(window.location.search);
let targetUserId = params.get('userId');
const previewMode = params.get('preview'); // 'FREUND' | 'UNBEKANNT' | null
let myUserId = null;
let isOwnProfile = false;
let profileData = null;
let avatarSrc = null;
let allImages = [];
let galleryOffset = 0;
const PAGE_SIZE = 4;
let allFriends = [];
let friendsOffset = 0;
const FRIENDS_PAGE = 5;
// Posts tab state
let postsPage = 0;
let postsHasMore = true;
let postsLoading = false;
let activeLbPostId = null;
let activeLbMode = null; // 'post' | 'image'
let activeLbImageIdx = null;
// ── Label maps ──
const GESCHLECHT_LABEL = { WEIBLICH: 'weiblich', DIVERS: 'divers', MAENNLICH: 'männlich' };
const NEIGUNG_LABEL = { DEVOT: 'devot', EHER_DEVOT: 'eher devot', SWITCHER: 'Switcher', EHER_DOMINANT: 'eher dominant', DOMINANT: 'dominant', KEINES: 'keines' };
const BEZIEHUNG_LABEL = { SINGLE: 'single', IN_EINER_BEZIEHUNG: 'in einer Beziehung', VERHEIRATET: 'verheiratet', IN_EINER_OFFENEN_BEZIEHUNG: 'in einer offenen Beziehung', IN_EINER_OFFENEN_EHE: 'in einer offenen Ehe' };
// ── Boot ──
if (!targetUserId || targetUserId === 'me') {
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
.then(user => {
if (user) {
targetUserId = user.userId;
loadProfile();
} else {
window.location.href = '/login.html';
}
});
} else {
loadProfile();
}
async function loadProfile() {
try {
const [me, profile, images] = await Promise.all([
fetch('/login/me').then(r => r.ok ? r.json() : null),
fetch('/social/users/' + targetUserId).then(r => r.ok ? r.json() : null),
fetch('/social/profile-images?userId=' + targetUserId).then(r => r.ok ? r.json() : [])
]);
document.getElementById('loadingHint').style.display = 'none';
if (!profile) {
document.getElementById('loadingHint').textContent = 'Profil nicht gefunden.';
document.getElementById('loadingHint').style.display = '';
return;
}
myUserId = me ? me.userId : null;
isOwnProfile = !previewMode && me && me.userId === profile.userId;
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';
showPreviewBanner(previewMode);
}
const isFriend = profile.friendStatus === 'FRIEND';
document.title = profile.name + ' xXx Sphere';
renderHeader(profile);
// ── Galerie ──
if (canSee(profile.sichtbarkeitGalerie, isFriend, isOwnProfile)) {
renderGallery();
}
// ── Freunde ──
if (canSee(profile.sichtbarkeitFreunde, isFriend, isOwnProfile)) {
loadFriends();
}
if (profile.beschreibung && canSee(profile.sichtbarkeitGrunddaten, isFriend, isOwnProfile)) {
document.getElementById('beschreibungLabel').style.display = '';
const el = document.getElementById('profilBeschreibung');
el.style.display = '';
el.textContent = profile.beschreibung;
}
// ── Tabs: Feed, Pinnwand, Spielhistorie ──
applyTabPrivacy(profile, isFriend);
activateTabFromUrl();
if (canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile)) {
await loadPinnwand();
}
document.getElementById('profileView').style.display = '';
// Feed-Tab ist vorausgewählt → sofort laden (nur wenn sichtbar)
if (canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile)) {
loadProfilPosts();
profilPostsObserver.observe(document.getElementById('profilPostsSentinel'));
}
} catch(e) {
console.error('[benutzer] loadProfile Fehler:', e);
document.getElementById('loadingHint').textContent = 'Fehler beim Laden.';
document.getElementById('loadingHint').style.display = '';
}
}
function renderHeader(profile) {
// Avatar
const picData = profile.profilePictureHq || profile.profilePicture;
if (picData) {
avatarSrc = 'data:image/png;base64,' + picData;
document.getElementById('profilePic').innerHTML = `<img src="${avatarSrc}" alt="">`;
}
document.getElementById('profileName').textContent = profile.name;
// Tags
const tags = document.getElementById('profileTags');
const addTag = (label, value) => {
if (!value) return;
const d = document.createElement('div');
d.className = 'profil-tag';
d.innerHTML = `<span class="label">${label}</span><span class="value">${esc(value)}</span>`;
tags.appendChild(d);
};
const grunddatenVisible = canSee(profile.sichtbarkeitGrunddaten, profile.friendStatus === 'FRIEND', isOwnProfile);
if (grunddatenVisible && profile.alter) addTag('Alter', profile.alter + ' J.');
if (grunddatenVisible && profile.groesse) addTag('Größe', profile.groesse + ' cm');
if (grunddatenVisible && profile.gewicht) addTag('Gewicht', profile.gewicht + ' kg');
if (grunddatenVisible && profile.geschlecht) addTag('Geschlecht', GESCHLECHT_LABEL[profile.geschlecht] || profile.geschlecht);
if (grunddatenVisible && profile.neigung) addTag('Neigung', NEIGUNG_LABEL[profile.neigung] || profile.neigung);
if (grunddatenVisible && profile.beziehungsstatus) addTag('Beziehung', BEZIEHUNG_LABEL[profile.beziehungsstatus] || profile.beziehungsstatus);
const xpVisible = canSee(profile.sichtbarkeitXp, profile.friendStatus === 'FRIEND', isOwnProfile);
if (xpVisible && profile.lockeeXp > 0) addTag('🔒 Lockee XP', profile.lockeeXp + ' XP');
if (xpVisible && profile.keyholderXp > 0) addTag('🔑 Keyholder XP', profile.keyholderXp + ' XP');
if (xpVisible && profile.bdsmXp > 0) addTag('⛓ BDSM XP', profile.bdsmXp + ' XP');
// Action buttons
const actions = document.getElementById('profileActions');
const isViewingOwnProfile = myUserId && myUserId === profile.userId;
if (isOwnProfile) {
actions.innerHTML = `<a href="/konto/profile.html" class="btn">Profil bearbeiten</a>`;
} else if (isViewingOwnProfile) {
// eigenes Profil im Preview-Modus keine Aktionsbuttons anzeigen
actions.innerHTML = '';
} else {
let html = '';
if (profile.friendStatus === 'FRIEND') {
html += `<a href="/community/nachrichten.html?userId=${profile.userId}" class="btn">✉ Nachricht</a>`;
} else if (profile.friendStatus === 'PENDING_SENT') {
html += `<button disabled>Anfrage gesendet</button>`;
} else if (profile.friendStatus === 'PENDING_RECEIVED') {
html += `<button id="friendActionBtn" onclick="acceptFriend()">✓ Anfrage annehmen</button>`;
} else {
html += `<button id="friendActionBtn" onclick="addFriend()">+ Freund hinzufügen</button>`;
}
html += ` <button onclick="openMeldungDialog('PROFIL','${profile.userId}')" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.4rem 0.8rem;cursor:pointer;font-size:0.85rem;">⚑ Melden</button>`;
actions.innerHTML = html;
}
}
// ── Privacy helpers ──
function canSee(sichtbarkeit, isFriend, isOwn) {
if (isOwn || !sichtbarkeit) return true;
if (sichtbarkeit === 'ALLE') return true;
if (sichtbarkeit === 'NUR_FREUNDE') return isFriend;
// NUR_ICH
return false;
}
function applyTabPrivacy(profile, isFriend) {
const showFeed = canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile);
const showPinnwand = canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile);
const showHistory = canSee(profile.sichtbarkeitLockhistorie, isFriend, isOwnProfile);
const showVorlieben = canSee(profile.sichtbarkeitVorlieben, isFriend, isOwnProfile);
const btnFeed = document.getElementById('tabBtnPosts');
const btnPinnwand = document.getElementById('tabBtnPinnwand');
const btnHistory = document.getElementById('tabBtnGameHistory');
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) { document.getElementById('vorliebenSection').style.display = ''; loadVorlieben(); }
// Ersten sichtbaren Tab aktivieren
if (!showFeed) {
btnFeed.classList.remove('active');
document.getElementById('tab-posts').classList.remove('active');
if (showPinnwand) {
btnPinnwand.classList.add('active');
document.getElementById('tab-pinnwand').classList.add('active');
} else if (showHistory) {
btnHistory.classList.add('active');
document.getElementById('tab-gamehistory').classList.add('active');
}
}
}
// ── Vorlieben anzeigen ──
const BEWERTUNG_ORDER = ['UNBEDINGT','MAG_ICH','WILL_AUSPROBIEREN','NEUTRAL','EHER_NICHT','GEHT_GAR_NICHT'];
const BEWERTUNG_LABEL = {
UNBEDINGT: 'Unbedingt', MAG_ICH: 'Mag ich',
WILL_AUSPROBIEREN: 'Will ich ausprobieren', NEUTRAL: 'Neutral',
EHER_NICHT: 'Eher nicht', GEHT_GAR_NICHT: 'Geht gar nicht',
};
let _vorliebenLoaded = false;
async function loadVorlieben() {
if (_vorliebenLoaded) return;
_vorliebenLoaded = true;
const container = document.getElementById('vorliebenDisplay');
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Wird geladen…</p>';
try {
const [dataRes, itemsRes] = await Promise.all([
fetch('/vorlieben/user/' + targetUserId),
fetch('/vorlieben/items'),
]);
if (!dataRes.ok || !itemsRes.ok) { container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</p>'; return; }
const data = await dataRes.json();
const kategorien = await itemsRes.json();
if (!data.canSee) {
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Nicht sichtbar.</p>'; return;
}
const ratings = data.ratings || {};
// Build itemId → name map
const itemNames = {};
kategorien.forEach(k => k.items.forEach(i => { itemNames[i.itemId] = i.name; }));
// Group by bewertung
const grouped = {};
Object.entries(ratings).forEach(([itemId, bw]) => {
if (!grouped[bw]) grouped[bw] = [];
grouped[bw].push(itemNames[itemId] || itemId);
});
const visibleGroups = BEWERTUNG_ORDER.filter(bw => grouped[bw]?.length);
if (!visibleGroups.length) {
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Keine Vorlieben angegeben.</p>'; return;
}
container.innerHTML = visibleGroups.map(bw => `
<div class="vorlieben-group">
<div class="vorlieben-group-title">${BEWERTUNG_LABEL[bw]}</div>
<div class="vorlieben-chips">
${grouped[bw].map(name =>
`<span class="vorliebe-chip bw-${bw}">${escV(name)}</span>`).join('')}
</div>
</div>`).join('');
} catch(e) {
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</p>';
}
}
function escV(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function showPreviewBanner(mode) {
const banner = document.createElement('div');
banner.style.cssText = 'background:var(--color-secondary);border:1px solid var(--color-primary);border-radius:8px;padding:0.65rem 1rem;margin-bottom:1rem;font-size:0.88rem;display:flex;align-items:center;justify-content:space-between;gap:0.75rem;';
const label = mode === 'FREUND' ? '👥 Vorschau aus Freundessicht' : '👤 Vorschau aus Sicht einer fremden Person';
banner.innerHTML = `<span>${label}</span><a href="/konto/einstellungen.html" style="font-size:0.82rem;color:var(--color-primary);">← Einstellungen</a>`;
document.getElementById('profileView').prepend(banner);
}
// ── Tab switching ──
let _gameHistoryLoaded = false;
let _khOffersLoaded = false;
function switchProfilTab(name, btn) {
document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.profil-tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
if (name === 'gamehistory' && !_gameHistoryLoaded) {
_gameHistoryLoaded = true;
loadGameHistory();
}
if (name === 'khoffers' && !_khOffersLoaded) {
_khOffersLoaded = true;
loadKhOffers();
}
// URL-QueryParam aktualisieren, damit der Tab nach F5 erhalten bleibt
const url = new URL(window.location.href);
if (name === 'posts') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', name);
}
history.replaceState(null, '', url.toString());
}
function activateTabFromUrl() {
const tab = new URLSearchParams(window.location.search).get('tab');
if (!tab || tab === 'posts') return;
const btn = document.querySelector(`[id^="tabBtn"][onclick*="'${tab}'"]`);
if (btn && btn.style.display !== 'none') {
switchProfilTab(tab, btn);
}
}
// ── Gallery Carousel ──
function renderGallery() {
if (allImages.length === 0) return;
document.getElementById('gallerieCarousel').style.display = '';
const page = allImages.slice(galleryOffset, galleryOffset + PAGE_SIZE);
document.getElementById('galleryStrip').innerHTML = page.map((img, i) => {
const idx = galleryOffset + i;
return `<div class="gallery-thumb" onclick="openGallery(${idx})">
<img src="data:image/jpeg;base64,${img.imageData}" alt="Foto ${idx + 1}">
<div class="gallery-thumb-overlay">
<button class="btn-like ${img.likedByMe ? 'liked' : ''}"
onclick="event.stopPropagation(); quickLike(${idx}, this)">
${img.likeCount}
</button>
</div>
</div>`;
}).join('');
const total = allImages.length;
document.getElementById('galleryPrev').disabled = galleryOffset === 0;
document.getElementById('galleryNext').disabled = galleryOffset + PAGE_SIZE >= total;
document.getElementById('galleryPos').textContent = total > 0
? (galleryOffset + 1) + '' + Math.min(galleryOffset + PAGE_SIZE, total) + ' / ' + total
: '';
}
function galleryPage(dir) {
galleryOffset = Math.max(0, Math.min(galleryOffset + dir * PAGE_SIZE, allImages.length - 1));
renderGallery();
}
// ── Friends Strip ──
async function loadFriends() {
try {
const res = await fetch('/social/friends/user/' + targetUserId);
if (!res.ok) return;
allFriends = await res.json();
} catch (e) { return; }
friendsOffset = 0;
renderFriendsStrip();
}
function renderFriendsStrip() {
const section = document.getElementById('friendsSection');
if (allFriends.length === 0) { section.style.display = 'none'; return; }
section.style.display = '';
document.getElementById('friendsCount').textContent = '(' + allFriends.length + ')';
const page = allFriends.slice(friendsOffset, friendsOffset + FRIENDS_PAGE);
const strip = document.getElementById('friendsStrip');
strip.innerHTML = page.map(f => {
const pic = f.profilePicture
? `<img src="data:image/png;base64,${f.profilePicture}" alt="${esc(f.name)}">`
: `<div class="friend-avatar-placeholder">👤</div>`;
return `<div class="friend-thumb" onclick="window.location='/community/benutzer.html?userId=${f.userId}'">
${pic}
<span title="${esc(f.name)}">${esc(f.name)}</span>
</div>`;
}).join('');
const nav = document.getElementById('friendsNav');
if (allFriends.length > FRIENDS_PAGE) {
nav.style.display = '';
document.getElementById('friendsPrev').disabled = friendsOffset === 0;
document.getElementById('friendsNext').disabled = friendsOffset + FRIENDS_PAGE >= allFriends.length;
const cur = Math.floor(friendsOffset / FRIENDS_PAGE) + 1;
const total = Math.ceil(allFriends.length / FRIENDS_PAGE);
document.getElementById('friendsPos').textContent = cur + ' / ' + total;
} else {
nav.style.display = 'none';
}
}
function friendsPage(dir) {
friendsOffset = Math.max(0, Math.min(friendsOffset + dir * FRIENDS_PAGE, allFriends.length - 1));
renderFriendsStrip();
}
async function quickLike(idx, btn) {
const img = allImages[idx];
await fetch('/social/profile-images/' + img.imageId + '/like', { method: 'POST' });
img.likedByMe = !img.likedByMe;
img.likeCount += img.likedByMe ? 1 : -1;
btn.className = 'btn-like' + (img.likedByMe ? ' liked' : '');
btn.textContent = '♥ ' + img.likeCount;
if (activeLbMode === 'image' && activeLbImageIdx === idx) renderLbImageBody();
}
// ── Bild-Lightbox (Avatar = imageViewer, Galerie = shared lightbox) ──
function openAvatarLightbox() {
if (!avatarSrc) return;
imageViewer.open({ images: [{ src: avatarSrc }] });
}
function renderLbImageBody() {
const img = allImages[activeLbImageIdx];
const total = allImages.length;
const prevDisabled = activeLbImageIdx === 0 ? 'disabled' : '';
const nextDisabled = activeLbImageIdx === total - 1 ? 'disabled' : '';
const likeClass = img.likedByMe ? ' active' : '';
document.getElementById('lbPostBody').innerHTML = `
<img src="data:image/jpeg;base64,${img.imageData}" alt=""
style="width:100%;max-height:360px;object-fit:contain;border-radius:8px;display:block;">
<div class="lb-img-nav">
<button class="post-action-btn" ${prevDisabled} onclick="lbGalleryNav(-1)">← Zurück</button>
${!isOwnProfile ? `<button class="post-action-btn${likeClass}" id="lbImgLikeBtn" onclick="lbToggleImageLike()">
♥ <span id="lbImgLikeCount">${img.likeCount}</span></button>` : ''}
<span style="font-size:0.8rem;color:var(--color-muted);">${activeLbImageIdx + 1} / ${total}</span>
<button class="post-action-btn" ${nextDisabled} onclick="lbGalleryNav(1)">Weiter →</button>
</div>`;
}
function openGallery(index) {
activeLbMode = 'image';
activeLbImageIdx = index;
activeLbPostId = null;
renderLbImageBody();
loadLbComments();
document.getElementById('postLightbox').classList.add('open');
}
function lbGalleryNav(dir) {
activeLbImageIdx = Math.max(0, Math.min(activeLbImageIdx + dir, allImages.length - 1));
renderLbImageBody();
loadLbComments();
}
async function lbToggleImageLike() {
const img = allImages[activeLbImageIdx];
await fetch('/social/profile-images/' + img.imageId + '/like', { method: 'POST' });
img.likedByMe = !img.likedByMe;
img.likeCount += img.likedByMe ? 1 : -1;
renderLbImageBody();
renderGallery();
}
// ── Pinnwand ──
async function loadPinnwand() {
const res = await fetch('/social/pinnwand?userId=' + targetUserId);
if (!res.ok) return;
const eintraege = await res.json();
const list = document.getElementById('pinnwandList');
list.innerHTML = eintraege.length === 0
? '<p style="color:var(--color-muted);font-size:0.85rem;">Noch keine Einträge.</p>'
: eintraege.map(e => renderEintragHtml(e)).join('');
}
function renderEintragHtml(e) {
const canDelete = (e.authorId === myUserId || e.profilUserId === myUserId);
const avatarHtml = e.authorPicture ? `<img src="data:image/png;base64,${e.authorPicture}" alt="">` : '◉';
return `<div class="pinnwand-entry" id="entry-${e.eintragId}">
<div class="entry-header">
<div class="entry-avatar">${avatarHtml}</div>
<div class="entry-meta">
<div class="author">${esc(e.authorName)}</div>
<div class="time">${fmtDate(e.createdAt)}</div>
</div>
${canDelete ? `<button class="btn-delete-small" onclick="deleteEintrag('${e.eintragId}')">✕</button>` : ''}
</div>
<div class="entry-text">${esc(e.text)}</div>
<div class="entry-actions">
<button class="btn-like ${e.likedByMe ? 'liked' : ''}" id="like-e-${e.eintragId}"
onclick="toggleEintragLike('${e.eintragId}')">♥ <span id="lc-e-${e.eintragId}">${e.likeCount}</span></button>
<button class="btn-text" onclick="toggleComments('${e.eintragId}','PINNWAND')">
Kommentare (${e.kommentarCount})
</button>
</div>
<div class="comments-section" id="comments-${e.eintragId}" style="display:none;"></div>
</div>`;
}
// ── Spielhistorie ──
const GAME_TYPE_ICON = {
CARDLOCK: IChtml('GAME_CARDLOCK'),
TIMELOCK: IChtml('GAME_TIMELOCK'),
BDSM: IC('GAME_BDSM'),
VANILLA: IC('GAME_VANILLA')
};
const ROLE_BADGE = {
KEYHOLDER: IC('ROLE_KEYHOLDER'),
LOCKEE: IC('ROLE_LOCKEE'),
PLAYER: ''
};
let gameHistoryLoaded = false;
async function loadGameHistory() {
if (gameHistoryLoaded) return;
gameHistoryLoaded = true;
const list = document.getElementById('gameHistoryList');
const empty = document.getElementById('gameHistoryEmpty');
list.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>';
try {
const res = await fetch('/gamehistory?userId=' + targetUserId);
if (!res.ok) { list.innerHTML = ''; return; }
const entries = await res.json();
list.innerHTML = '';
if (entries.length === 0) { empty.style.display = ''; return; }
list.innerHTML = entries.map(e => {
const gameIconRaw = GAME_TYPE_ICON[e.gameType];
const gameIcon = gameIconRaw
? gameIconRaw
: `<span style="font-size:2.7rem;line-height:1;">🎮</span>`;
const days = Math.floor(e.durationMinutes / 1440);
const hours = Math.floor((e.durationMinutes % 1440) / 60);
const mins = e.durationMinutes % 60;
const dur = days > 0
? `${days}d ${hours}h ${mins}min`
: hours > 0 ? `${hours}h ${mins}min` : `${mins}min`;
const participants = (e.participants || []).map(p => {
const badge = ROLE_BADGE[p.role] || '';
const img = p.picture
? `<img src="data:image/png;base64,${p.picture}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">`
: `<div style="width:40px;height:40px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;">👤</div>`;
return `<a href="/community/benutzer.html?userId=${esc(p.userId)}" style="position:relative;flex-shrink:0;text-decoration:none;" title="${esc(p.name || '')} (${p.role})">
${img}
${badge ? `<span style="position:absolute;top:-4px;right:-4px;font-size:0.9rem;line-height:1;">${badge}</span>` : ''}
</a>`;
}).join('');
return `<div style="display:flex;align-items:flex-start;gap:0.85rem;padding:0.75rem 0;border-bottom:1px solid var(--color-secondary);">
<div style="flex-shrink:0;width:3rem;text-align:center;">${gameIcon}</div>
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(e.gameName) || 'Unbenannt'}</div>
<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.15rem;">⏱ ${dur} &nbsp;·&nbsp; ${new Date(e.unlockTime).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'})}</div>
</div>
<div style="display:flex;gap:0.35rem;align-items:center;flex-shrink:0;">${participants}</div>
</div>`;
}).join('');
} catch(e) { list.innerHTML = ''; }
}
// ── Keyholder-Angebote ──
const KH_GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' };
async function loadKhOffers() {
const list = document.getElementById('khOffersList');
const empty = document.getElementById('khOffersEmpty');
list.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>';
try {
const res = await fetch('/keyholder-offers/user/' + targetUserId);
if (!res.ok) { list.innerHTML = ''; return; }
const offers = await res.json();
list.innerHTML = '';
if (offers.length === 0) { empty.style.display = ''; return; }
offers.forEach(o => list.appendChild(buildKhOfferCard(o)));
} catch(e) { list.innerHTML = ''; }
}
function buildKhOfferCard(o) {
const esc = s => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; };
const genderTags = (o.targetGenders && o.targetGenders.length > 0)
? o.targetGenders.map(g => `<span class="kh-offer-badge">${esc(KH_GENDER_LABELS[g] || g)}</span>`).join('')
: '<span class="kh-offer-badge">Alle</span>';
const modeBadge = o.directStart
? '<span class="kh-offer-badge direct">Direktstart</span>'
: '<span class="kh-offer-badge confirm">Mit Bestätigung</span>';
const typeIcon = o.templateType === 'TIMELOCK'
? `<div class="kh-offer-type-icon"><span class="icon-base">🕐</span><span class="icon-lock">🔒</span></div>`
: `<div class="kh-offer-type-icon"><img src="img/card.png" class="icon-base" alt="Karten-Lock"><span class="icon-lock">🔒</span></div>`;
const div = document.createElement('div');
div.className = 'kh-offer-card';
div.innerHTML = `
${typeIcon}
<div class="kh-offer-body">
<div class="kh-offer-name">${esc(o.templateName || 'Unbenannt')}</div>
<div class="kh-offer-meta">
${modeBadge} ${genderTags}
<span class="kh-offer-badge">✓ ${o.acceptanceCount}× angenommen</span>
</div>
</div>`;
return div;
}
async function postPinnwand() {
const ta = document.getElementById('pinnwandText');
const text = ta.value.trim();
if (!text) return;
const res = await fetch('/social/pinnwand', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profilUserId: targetUserId, text })
});
if (res.ok) { ta.value = ''; await loadPinnwand(); }
}
async function deleteEintrag(eintragId) {
await fetch('/social/pinnwand/' + eintragId, { method: 'DELETE' });
await loadPinnwand();
}
async function toggleEintragLike(eintragId) {
await fetch('/social/pinnwand/' + eintragId + '/like', { method: 'POST' });
const btn = document.getElementById('like-e-' + eintragId);
const lc = document.getElementById('lc-e-' + eintragId);
const was = btn.classList.contains('liked');
btn.classList.toggle('liked', !was);
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
}
// ── Comments ──
async function toggleComments(targetId, targetType) {
const section = document.getElementById('comments-' + targetId);
if (section.style.display === 'none') {
section.style.display = '';
await loadComments(targetId, targetType);
} else {
section.style.display = 'none';
}
}
// loadComments und renderKommentarHtml kommen aus shared.js
async function loadComments(targetId, targetType) {
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${targetId}`);
const comments = await res.json();
const section = document.getElementById('comments-' + targetId);
section.innerHTML = (comments.length === 0
? '<p style="color:var(--color-muted);font-size:0.82rem;margin-bottom:0.4rem;">Noch keine Kommentare.</p>'
: comments.map(k => renderKommentarHtml(k, targetType, targetId, { myUserId, showReplies: true })).join(''))
+ `<div class="comment-write">
<input type="text" id="ci-${targetId}" placeholder="Kommentar schreiben…" maxlength="500"
onkeydown="if(event.key==='Enter') postComment('${targetId}','${targetType}')">
<button onclick="postComment('${targetId}','${targetType}')">Senden</button>
</div>`;
}
async function postComment(targetId, targetType) {
const input = document.getElementById('ci-' + targetId);
const text = input.value.trim();
if (!text) return;
await fetch('/social/kommentare', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetType, targetId, text })
});
input.value = '';
await loadComments(targetId, targetType);
}
async function deleteKommentar(kommentarId, targetType, targetId) {
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
if (document.getElementById('postLightbox')?.classList.contains('open')) {
await loadLbComments();
} else {
await loadComments(targetId, targetType);
}
}
// ── Friend actions ──
async function addFriend() {
const btn = document.getElementById('friendActionBtn');
btn.disabled = true; btn.textContent = '…';
try {
const res = await fetch('/social/friends/request', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receiverId: targetUserId })
});
btn.textContent = (res.ok || res.status === 201) ? 'Anfrage gesendet' : 'Fehler';
} catch { btn.disabled = false; btn.textContent = '+ Freund hinzufügen'; }
}
async function acceptFriend() {
const btn = document.getElementById('friendActionBtn');
btn.disabled = true; btn.textContent = '…';
try {
const pending = await fetch('/social/friends/pending').then(r => r.json());
const f = pending.find(p => p.user.userId === targetUserId);
if (!f) { btn.textContent = 'Fehler'; return; }
const res = await fetch('/social/friends/accept', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ friendshipId: f.friendshipId })
});
if (res.ok) location.reload();
else { btn.disabled = false; btn.textContent = '✓ Anfrage annehmen'; }
} catch { btn.disabled = false; btn.textContent = '✓ Anfrage annehmen'; }
}
// ── Profil Posts ──
async function loadProfilPosts() {
if (postsLoading || !postsHasMore) return;
postsLoading = true;
try {
const res = await fetch(`/feed/user/${targetUserId}?page=${postsPage}&size=10`);
if (!res.ok) return;
const data = await res.json();
const feed = document.getElementById('profilPostsFeed');
if (postsPage === 0 && data.posts.length === 0) {
document.getElementById('profilPostsEmpty').style.display = '';
}
data.posts.forEach(p => {
feed.insertAdjacentHTML('beforeend', renderProfilPostCard(p));
});
postsHasMore = data.hasMore;
postsPage++;
} finally {
postsLoading = false;
}
}
// bilderCarousel und carNav kommen aus shared.js
function renderProfilPostCard(p) {
const avatarHtml = p.authorPicture
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
: '◉';
const bildRaw = bilderCarousel(p.bilder);
const bildHtml = bildRaw
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}</div>`
: '';
const privacyLabel = p.isPublic ? '' : '<span style="font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem;">🔒 privat</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' : ''}" onclick="voteProfilPost('${p.postId}','${o.optionId}')">
<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 canDelete = p.authorId === myUserId;
const deleteBtn = canDelete
? `<button class="post-action-btn post-delete" onclick="deleteProfilPost('${p.postId}')">🗑</button>`
: '';
return `<div class="post-card" id="pp-${p.postId}">
<div class="post-header">
<div class="post-avatar">${avatarHtml}</div>
<div>
<div class="post-author">${esc(p.authorName)}${privacyLabel}</div>
<div class="post-date">${fmtDate(p.createdAt)}</div>
</div>
${deleteBtn}
</div>
<div class="post-text">${esc(p.text)}</div>
${bildHtml}
${umfrageHtml}
<div class="post-actions">
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="pp-like-${p.postId}" onclick="likeProfilPost('${p.postId}')">
♥ <span id="pp-lc-${p.postId}">${p.likeCount}</span>
</button>
<button class="post-action-btn" onclick="openPostLb('${p.postId}')">
💬 ${p.kommentarCount}
</button>
</div>
</div>`;
}
async function likeProfilPost(postId) {
await fetch('/feed/posts/' + postId + '/like', { method: 'POST' });
const btn = document.getElementById('pp-like-' + postId);
const lc = document.getElementById('pp-lc-' + postId);
const was = btn.classList.contains('active');
btn.classList.toggle('active', !was);
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
}
async function voteProfilPost(postId, optionId) {
await fetch('/feed/posts/' + postId + '/vote', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ optionId })
});
// Reload affected post card
const res = await fetch(`/feed/user/${targetUserId}?page=0&size=1`);
// Simple: reload all posts
postsPage = 0; postsHasMore = true;
document.getElementById('profilPostsFeed').innerHTML = '';
document.getElementById('profilPostsEmpty').style.display = 'none';
loadProfilPosts();
}
async function deleteProfilPost(postId) {
if (!confirm('Post löschen?')) return;
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
if (res.ok) document.getElementById('pp-' + postId)?.remove();
}
// ── Lightbox (Post + Bild) ──
function openPostLb(postId) {
activeLbMode = 'post';
activeLbPostId = postId;
activeLbImageIdx = null;
const card = document.getElementById('pp-' + postId);
if (card) {
const clone = card.cloneNode(true);
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
}
loadLbComments();
document.getElementById('postLightbox').classList.add('open');
}
function closeLb() {
document.getElementById('postLightbox').classList.remove('open');
activeLbMode = null;
activeLbPostId = null;
activeLbImageIdx = null;
}
async function loadLbComments() {
let targetType, targetId;
if (activeLbMode === 'image') {
targetType = 'IMAGE';
targetId = allImages[activeLbImageIdx].imageId;
} else {
targetType = 'FEED_POST';
targetId = activeLbPostId;
}
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${targetId}`);
const comments = await res.json();
document.getElementById('lbCommentsList').innerHTML = comments.length === 0
? '<p style="color:var(--color-muted);font-size:0.82rem;margin:0.4rem;">Noch keine Kommentare.</p>'
: comments.map(k => renderKommentarHtml(k, targetType, targetId, { myUserId, showReplies: false })).join('');
}
async function postLbComment() {
const input = document.getElementById('lbCommentInput');
const text = input.value.trim();
if (!text) return;
let targetType, targetId;
if (activeLbMode === 'image') {
targetType = 'IMAGE';
targetId = allImages[activeLbImageIdx].imageId;
} else {
if (!activeLbPostId) return;
targetType = 'FEED_POST';
targetId = activeLbPostId;
}
await fetch('/social/kommentare', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetType, targetId, text })
});
input.value = '';
await loadLbComments();
}
document.getElementById('postLightbox').addEventListener('click', e => {
if (e.target === document.getElementById('postLightbox')) closeLb();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && document.getElementById('postLightbox').classList.contains('open')) closeLb();
if (activeLbMode === 'image') {
if (e.key === 'ArrowLeft') lbGalleryNav(-1);
if (e.key === 'ArrowRight') lbGalleryNav(1);
}
});
// Infinite scroll for profil posts
const profilPostsObserver = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadProfilPosts();
}, { threshold: 0.5 });
// Klick auf Post-Bilder → Post-Lightbox öffnen
document.getElementById('profilPostsFeed').addEventListener('click', e => {
const wrap = e.target.closest('.post-bild-wrap');
if (!wrap) return;
openPostLb(wrap.dataset.postId);
});
// esc, fmtDate, toggleEmojiPicker, insertEmoji kommen aus shared.js
</script>
</body>
</html>