Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1416 lines
66 KiB
HTML
1416 lines
66 KiB
HTML
<!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>←</button>
|
||
<span class="gallery-pos" id="galleryPos"></span>
|
||
<button id="galleryNext" onclick="galleryPage(1)">→</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>←</button>
|
||
<span class="gallery-pos" id="friendsPos"></span>
|
||
<button id="friendsNext" onclick="friendsPage(1)">→</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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
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} · ${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>
|