Files
xxx-sphere-web/bin/main/static/community/gruppe.html
Mario e35b095c18
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Locations können nun auch Posten - bugfixes im Feed
2026-04-13 23:04:15 +02:00

1174 lines
61 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>Gruppe xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/community.css">
<style>
/* Header */
.gruppe-header { display:flex; align-items:center; gap:1rem; margin-bottom:1.5rem; flex-wrap:wrap; }
.gruppe-avatar { width:72px; height:72px; border-radius:12px; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:2rem; flex-shrink:0; overflow:hidden; }
.gruppe-avatar img { width:100%; height:100%; object-fit:cover; }
.gruppe-header-info h2 { margin:0 0 0.2rem; font-size:1.3rem; }
.gruppe-header-info p { margin:0; font-size:0.85rem; color:var(--color-muted); }
.gruppe-header-actions { margin-left:auto; display:flex; gap:0.5rem; }
.gruppe-header-actions button, .gruppe-header-actions a.btn { margin:0; width:auto; padding:0.4rem 0.9rem; font-size:0.85rem; }
/* Compose-Typ (Gruppe-spezifisch) */
.compose-type { display:flex; gap:1.5rem; margin-bottom:0.75rem; }
.compose-type label { display:flex; align-items:center; gap:0.4rem; font-size:0.9rem; cursor:pointer; }
/* Kommentare */
.comments-section { margin-top:0.75rem; border-top:1px solid var(--color-secondary); padding-top:0.75rem; }
.comment-compose { display:flex; gap:0.5rem; margin-top:0.5rem; }
.comment-compose input { flex:1; font-size:0.85rem; padding:0.35rem 0.6rem; height:auto; }
.comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
/* Members */
.member-list { list-style:none; margin:0; padding:0; }
.member-item { display:flex; align-items:center; gap:0.75rem; padding:0.6rem 0; border-bottom:1px solid var(--color-secondary); }
.member-item:last-child { border-bottom:none; }
.member-avatar { width:38px; height:38px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:1rem; overflow:hidden; flex-shrink:0; }
.member-avatar img { width:100%; height:100%; object-fit:cover; }
.member-name { flex:1; font-weight:600; }
.role-badge { font-size:0.7rem; font-weight:700; padding:0.1rem 0.4rem; border-radius:4px; background:var(--color-primary); color:#fff; display:inline-block; }
.role-badge.mitglied { background:var(--color-secondary); color:var(--color-text); }
.member-actions { display:flex; gap:0.4rem; }
.member-actions button { margin:0; width:auto; padding:0.25rem 0.6rem; font-size:0.75rem; }
/* Admin */
.admin-section { margin-bottom:1.5rem; }
.admin-section h3 { font-size:1rem; font-weight:600; color:var(--color-primary); border-bottom:1px solid var(--color-secondary); padding-bottom:0.5rem; margin-bottom:0.75rem; }
.request-item { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:8px; padding:0.8rem; margin-bottom:0.6rem; }
.request-item .req-name { font-weight:600; margin-bottom:0.2rem; }
.request-item .req-msg { font-size:0.83rem; color:var(--color-muted); margin-bottom:0.6rem; font-style:italic; }
.request-item .req-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
.request-item .req-actions button { margin:0; width:auto; padding:0.4rem 0.85rem; font-size:0.82rem; font-weight:600; }
.meldung-item { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:8px; padding:0.8rem; margin-bottom:0.6rem; }
.meld-post-preview { background:var(--color-secondary); border-radius:6px; padding:0.6rem 0.75rem; margin-bottom:0.6rem; }
.meld-post-meta { font-size:0.75rem; color:var(--color-muted); margin-bottom:0.3rem; }
.meld-post-text { font-size:0.88rem; white-space:pre-wrap; word-break:break-word; }
.meldung-item .meld-grund { font-size:0.83rem; color:var(--color-muted); margin-bottom:0.6rem; }
.meldung-item .meld-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
.meldung-item .meld-actions button { margin:0; width:auto; padding:0.4rem 0.85rem; font-size:0.82rem; font-weight:600; }
.edit-form label { display:block; font-size:0.85rem; color:var(--color-muted); margin-bottom:0.25rem; margin-top:0.6rem; }
.edit-form input, .edit-form textarea { width:100%; box-sizing:border-box; }
.edit-form textarea { padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:80px; }
.edit-form textarea:focus { border-color:var(--color-primary); }
.edit-form .toggle-row { display:flex; align-items:center; gap:0.75rem; margin-top:0.6rem; }
.edit-form .toggle-row label { margin:0; font-size:0.9rem; color:var(--color-text); }
.img-preview { width:100%; max-height:140px; object-fit:cover; border-radius:6px; margin-top:0.5rem; display:none; }
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
/* Dialog */
.dialog-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:200; align-items:center; justify-content:center; }
.dialog-backdrop.visible { display:flex; }
.dialog { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:12px; padding:2rem; width:100%; max-width:420px; box-shadow:0 12px 40px rgba(0,0,0,0.6); }
.dialog h3 { color:var(--color-primary); font-size:1.1rem; margin-bottom:1.25rem; }
.dialog p { color:var(--color-muted); font-size:0.88rem; margin-bottom:0; }
.dialog-actions { display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.5rem; }
.dialog-actions button { flex:none; margin:0; padding:0.55rem 1.1rem; font-size:0.9rem; width:auto; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<!-- Group header -->
<div class="gruppe-header">
<div class="gruppe-avatar" id="gruppeAvatar">👥</div>
<div class="gruppe-header-info">
<h2 id="gruppeName">Wird geladen…</h2>
<p id="gruppeMeta"></p>
</div>
<div class="gruppe-header-actions" id="headerActions"></div>
</div>
<div class="tabs" id="tabBar">
<button class="tab-btn active" data-tab="posts" onclick="switchTab('posts', this)">Beiträge</button>
<button class="tab-btn" data-tab="members" onclick="switchTab('members', this)">Mitglieder</button>
<button class="tab-btn" data-tab="admin" id="adminTabBtn" style="display:none;" onclick="switchTab('admin', this)">Admin <span class="social-badge" id="adminBadge" style="display:none;"></span></button>
</div>
<!-- Posts Tab -->
<div class="tab-panel active" id="tab-posts">
<!-- Compose -->
<div class="post-compose" id="compose" style="display:none;">
<textarea id="composeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
<div class="compose-thumbs" id="composeThumbs"></div>
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
<div id="optionList"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
<button onclick="addOption()" style="width:auto; margin:0; padding:0.3rem 0.75rem; font-size:0.8rem;">+ Option</button>
<label class="multi-toggle">
<input type="checkbox" id="multiChoice"> Mehrfachauswahl möglich
</label>
</div>
</div>
<div class="compose-footer">
<div style="display:flex;gap:0.5rem;align-items:center;">
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji einfügen">😊</button>
<label class="compose-action-btn" title="Fotos hinzufügen">📷
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
</label>
<button type="button" id="umfrageBtn" class="compose-action-btn" onclick="toggleUmfrage(this)" title="Umfrage hinzufügen">📊</button>
<button onclick="submitPost()" style="width:auto; margin:0;">Veröffentlichen</button>
</div>
</div>
</div>
<div id="postsFeed"></div>
<p class="empty-hint" id="postsEmpty" style="display:none;">Noch keine Beiträge. Schreib den ersten!</p>
<div style="text-align:center; margin-top:0.75rem;">
<button id="loadMoreBtn" onclick="loadMorePosts()" style="display:none; width:auto; padding:0.4rem 1.25rem; background:var(--color-secondary); color:var(--color-text);">Mehr laden</button>
</div>
</div>
<!-- Members Tab -->
<div class="tab-panel" id="tab-members">
<ul class="member-list" id="memberList"></ul>
</div>
<!-- Admin Tab -->
<div class="tab-panel" id="tab-admin">
<div class="admin-section">
<h3>Gruppe bearbeiten</h3>
<div class="edit-form">
<label>Name</label>
<input type="text" id="editName" maxlength="100">
<label>Beschreibung</label>
<textarea id="editDesc" maxlength="1000"></textarea>
<label>Bild</label>
<input type="file" id="editBildFile" accept="image/*" onchange="previewBild(this,'editBildPreview','editBildData')">
<img id="editBildPreview" class="img-preview" alt="">
<input type="hidden" id="editBildData">
<div class="toggle-row">
<input type="checkbox" id="editPrivate">
<label for="editPrivate">Private Gruppe</label>
</div>
<button onclick="saveGruppe()" style="margin-top:0.75rem; width:auto; padding:0.4rem 1rem;">Speichern</button>
<p class="message" id="saveMsg" style="display:none; margin-top:0.5rem;"></p>
</div>
</div>
<div class="admin-section">
<h3>Beitrittsanfragen</h3>
<div id="requestsSection">
<p class="empty-hint" id="reqEmpty">Keine ausstehenden Anfragen.</p>
</div>
</div>
<div class="admin-section">
<h3>Gemeldete Beiträge</h3>
<div id="reportsSection">
<p class="empty-hint" id="repEmpty">Keine Meldungen.</p>
</div>
</div>
<div class="admin-section">
<h3>Gruppe löschen</h3>
<button onclick="openDeleteDialog()" style="background:#c0392b; width:auto; padding:0.4rem 1rem;">Gruppe löschen</button>
</div>
</div>
</div>
</div>
<!-- Leave confirmation -->
<div class="dialog-backdrop" id="leaveDialog">
<div class="dialog">
<h3>Gruppe verlassen</h3>
<p>Möchtest du diese Gruppe wirklich verlassen?</p>
<div class="dialog-actions">
<button class="secondary" onclick="document.getElementById('leaveDialog').classList.remove('visible')">Abbrechen</button>
<button onclick="confirmLeave()" style="background:#c0392b;">Verlassen</button>
</div>
</div>
</div>
<!-- Delete confirmation -->
<div class="dialog-backdrop" id="deleteDialog">
<div class="dialog">
<h3>Gruppe löschen</h3>
<p>Diese Aktion kann nicht rückgängig gemacht werden. Alle Beiträge und Mitgliedschaften werden gelöscht.</p>
<div class="dialog-actions">
<button class="secondary" onclick="document.getElementById('deleteDialog').classList.remove('visible')">Abbrechen</button>
<button onclick="deleteGruppe()" style="background:#c0392b;">Löschen</button>
</div>
</div>
</div>
<!-- Generic confirm/info modal -->
<div class="dialog-backdrop" id="genericModal">
<div class="dialog" style="text-align:center;">
<h3 id="gModalTitle"></h3>
<p id="gModalText" style="margin-bottom:0;"></p>
<div class="dialog-actions" id="gModalActions" style="justify-content:center; margin-top:1.5rem;"></div>
</div>
</div>
<!-- Post lightbox dialog -->
<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="Kommentieren…" 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/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/hashtag.js"></script>
<script>
// ── Generic modal helpers ──
function showModal(title, text, actions) {
document.getElementById('gModalTitle').textContent = title;
const textEl = document.getElementById('gModalText');
textEl.textContent = text;
textEl.style.display = text ? '' : 'none';
const actEl = document.getElementById('gModalActions');
actEl.innerHTML = '';
actions.forEach(a => {
const btn = document.createElement('button');
btn.textContent = a.label;
if (a.secondary) btn.className = 'secondary';
if (a.danger) btn.style.background = '#c0392b';
btn.style.cssText += ';margin:0;width:auto;';
btn.onclick = () => { closeModal(); if (a.onClick) a.onClick(); };
actEl.appendChild(btn);
});
document.getElementById('genericModal').classList.add('visible');
}
function closeModal() {
document.getElementById('genericModal').classList.remove('visible');
}
function showSaveMsg(text, type) {
const el = document.getElementById('saveMsg');
el.textContent = text;
el.className = 'message ' + type;
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 3000);
}
document.getElementById('genericModal').addEventListener('click', e => {
if (e.target === document.getElementById('genericModal')) closeModal();
});
// ──────────────────────────────────────────────────────────────────────
const params = new URLSearchParams(location.search);
const gruppeId = params.get('gruppeId');
if (!gruppeId) location.href = '/community/gruppen.html';
let myId = null;
let myRole = null;
let gruppeData = null;
let allPosts = [];
let currentPage = 0;
const PAGE_SIZE = 10;
// esc, fmtDate kommen aus shared.js
function switchTab(name, btn) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
localStorage.setItem('tab_gruppe_' + gruppeId, name);
if (name === 'members') loadMembers();
if (name === 'admin') { loadAdminRequests(); loadReports(); }
}
function avatarHtml(pic, size) {
return pic
? `<img src="data:image/png;base64,${pic}" alt="">`
: '◉';
}
async function init() {
// Get my ID
const meRes = await fetch('/login/me');
if (!meRes.ok) { location.href='/login.html'; return; }
const me = await meRes.json();
myId = me.userId;
initLb(myId);
await loadGruppe();
await loadPosts();
const _composeText = document.getElementById('composeText');
if (_composeText) attachHashtagAutocomplete(_composeText);
const _savedTab = localStorage.getItem('tab_gruppe_' + gruppeId);
if (_savedTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedTab}"]`);
if (_btn && _btn.style.display !== 'none') switchTab(_savedTab, _btn);
}
}
async function loadGruppe() {
const res = await fetch('/gruppen/' + gruppeId);
if (!res.ok) { document.getElementById('gruppeName').textContent = 'Nicht gefunden'; return; }
gruppeData = await res.json();
myRole = gruppeData.myRole;
document.title = gruppeData.name + ' xXx Sphere';
document.getElementById('gruppeName').textContent = gruppeData.name;
document.getElementById('gruppeMeta').textContent =
gruppeData.memberCount + ' Mitglied' + (gruppeData.memberCount !== 1 ? 'er' : '') +
(gruppeData.isPrivate ? ' · 🔒 Privat' : '');
const av = document.getElementById('gruppeAvatar');
if (gruppeData.bild) av.innerHTML = `<img src="data:image/jpeg;base64,${gruppeData.bild}" alt="">`;
// Header actions
const ha = document.getElementById('headerActions');
if (myRole) {
ha.innerHTML = `<button class="secondary" onclick="leaveGruppe()">Verlassen</button>`;
} else if (gruppeData.myRequestStatus === 'AUSSTEHEND') {
ha.innerHTML = `<button disabled style="opacity:0.6;">Anfrage ausstehend</button>`;
} else {
ha.innerHTML = `<a href="/community/gruppen.html" class="btn secondary">← Zurück</a>`;
}
// Admin tab
if (myRole === 'ADMIN') {
document.getElementById('adminTabBtn').style.display = '';
// Pre-fill edit form
document.getElementById('editName').value = gruppeData.name;
document.getElementById('editDesc').value = gruppeData.beschreibung || '';
document.getElementById('editPrivate').checked = gruppeData.isPrivate;
if (gruppeData.bild) {
const prev = document.getElementById('editBildPreview');
prev.src = 'data:image/jpeg;base64,' + gruppeData.bild;
prev.style.display = 'block';
document.getElementById('editBildData').value = gruppeData.bild;
}
}
// Show compose if member
if (myRole) document.getElementById('compose').style.display = '';
// Badges für ausstehende Anfragen + Meldungen (nur Admin)
if (myRole === 'ADMIN') {
Promise.all([
fetch('/gruppen/' + gruppeId + '/requests').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('/gruppen/' + gruppeId + '/reports').then(r => r.ok ? r.json() : []).catch(() => [])
]).then(([reqs, reps]) => { adminJoinsCount = reqs.length; adminReportsCount = reps.length; setAdminBadge(); });
}
}
async function loadPosts() {
if (!myRole) { document.getElementById('postsEmpty').textContent = 'Tritt der Gruppe bei, um Beiträge zu sehen.'; document.getElementById('postsEmpty').style.display = ''; return; }
currentPage = 0;
allPosts = [];
document.getElementById('postsFeed').innerHTML = '';
await fetchPage(0);
}
async function loadMorePosts() {
await fetchPage(currentPage + 1);
}
async function fetchPage(page) {
const res = await fetch('/gruppen/' + gruppeId + '/posts?page=' + page + '&size=' + PAGE_SIZE);
if (!res.ok) return;
const data = await res.json();
if (page === 0 && data.posts.length === 0) {
document.getElementById('postsEmpty').style.display = '';
} else {
document.getElementById('postsEmpty').style.display = 'none';
}
data.posts.forEach(p => {
allPosts.push(p);
document.getElementById('postsFeed').insertAdjacentHTML('beforeend', renderPostCard(p));
});
currentPage = page;
const btn = document.getElementById('loadMoreBtn');
btn.style.display = data.hasMore ? '' : 'none';
}
const gruppeEditBilder = new Map();
function renderPostCard(p) {
const canEdit = p.authorId === myId;
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
const bildHtml = bilderGrid(p.bilder);
const editedLabel = p.editedAt ? ` <span style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>` : '';
// editable view area: question/text + images
const textStyle = p.beitragTyp === 'UMFRAGE' ? ' style="font-weight:600;margin-bottom:0.5rem;"' : '';
const editableHtml = `<div class="post-text"${textStyle}>${renderTextWithHashtags(p.text)}</div><div id="gpbi-${p.beitragId}">${bildHtml}</div>`;
// poll bars (only for UMFRAGE, not editable)
let barsHtml = '';
if (p.beitragTyp === 'UMFRAGE') {
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
barsHtml = p.optionen.map(o => {
const pct = total > 0 ? Math.round(o.stimmenCount / total * 100) : 0;
const voted = p.myVoteOptionIds.includes(o.optionId);
return `<div class="umfrage-option-bar ${voted?'voted':''}" onclick="event.stopPropagation(); vote('${p.beitragId}','${o.optionId}',this)">
<div class="umfrage-bar-fill" style="width:${pct}%"></div>
<div class="umfrage-bar-content">
<span>${esc(o.text)}</span>
<span>${pct}% (${o.stimmenCount})</span>
</div>
</div>`;
}).join('') + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
}
const rightBtns = (canEdit ? `<button class="post-action-btn" onclick="event.stopPropagation();startGruppeEdit('${p.beitragId}')" title="Bearbeiten" style="color:var(--color-muted)">✏</button>` : '')
+ (canDelete ? `<button class="post-action-btn danger post-delete" onclick="event.stopPropagation(); deletePost('${p.beitragId}',this)">✕</button>` : '');
return `
<div class="post-card" id="post-${p.beitragId}" onclick="openPostDialog('${p.beitragId}')" style="cursor:pointer;">
<div class="post-header">
<div class="post-avatar"><a href="/community/benutzer.html?userId=${p.authorId}" onclick="event.stopPropagation()" style="display:contents;">${av}</a></div>
<div>
<div class="post-author"><a href="/community/benutzer.html?userId=${p.authorId}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${esc(p.authorName)}</a></div>
<div class="post-meta" id="gpm-${p.beitragId}">${fmtDate(p.createdAt)}${editedLabel}</div>
</div>
${rightBtns ? `<div style="margin-left:auto;display:flex;gap:0.25rem;">${rightBtns}</div>` : ''}
</div>
<div id="gpva-${p.beitragId}">${editableHtml}</div>
<div id="gpea-${p.beitragId}" style="display:none;"></div>
<div id="gpum-${p.beitragId}">${barsHtml}</div>
<div class="post-actions">
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="event.stopPropagation(); toggleLike('${p.beitragId}',this)" id="like-btn-${p.beitragId}">
♥ <span id="like-count-${p.beitragId}">${p.likeCount}</span>
</button>
<button class="post-action-btn" onclick="event.stopPropagation(); openPostDialog('${p.beitragId}')">
💬 <span id="kmt-count-${p.beitragId}">${p.kommentarCount}</span>
</button>
${!p.reported ? `<button class="post-action-btn" onclick="event.stopPropagation(); reportPost('${p.beitragId}',this)">⚑ Melden</button>` : '<span style="font-size:0.78rem;color:var(--color-muted);">Gemeldet</span>'}
</div>
</div>`;
}
function buildGruppeUmfrageHtml(beitragId, optionen, myVoteOptionIds, multiChoice) {
if (!optionen || optionen.length === 0) return '';
const total = optionen.reduce((s, o) => s + o.stimmenCount, 0);
return optionen.map(o => {
const pct = total > 0 ? Math.round(o.stimmenCount / total * 100) : 0;
const voted = myVoteOptionIds.includes(o.optionId);
return `<div class="umfrage-option-bar ${voted?'voted':''}" onclick="event.stopPropagation(); vote('${beitragId}','${o.optionId}',this)">
<div class="umfrage-bar-fill" style="width:${pct}%"></div>
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}% (${o.stimmenCount})</span></div>
</div>`;
}).join('') + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${multiChoice?' · Multi-Choice':''}</div>`;
}
function startGruppeEdit(beitragId) {
const post = allPosts.find(p => p.beitragId === beitragId);
if (!post) return;
gruppeEditBilder.set(beitragId, [...(post.bilder || [])]);
document.getElementById('gpva-' + beitragId).style.display = 'none';
document.getElementById('gpum-' + beitragId).style.display = 'none';
const isUmfrage = post.beitragTyp === 'UMFRAGE';
const optionenHtml = isUmfrage
? `<div id="gpeo-${beitragId}" style="margin-top:0.5rem;">${(post.optionen || []).map((o) =>
`<div class="umfrage-option-row">
<input type="text" value="${esc(o.text)}" maxlength="200" data-option-id="${o.optionId}"
style="flex:1;padding:0.4rem 0.6rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.9rem;"
onmousedown="event.stopPropagation()" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();this.closest('.umfrage-option-row').remove()" style="width:auto;margin:0;padding:0.3rem 0.6rem;font-size:0.8rem;">✕</button>
</div>`).join('')}
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.3rem;" onclick="event.stopPropagation()">
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();gruppeEditAddOption('${beitragId}')" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
<label class="multi-toggle" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
<input type="checkbox" id="gpmc-${beitragId}" ${post.multiChoice ? 'checked' : ''}> Mehrfachauswahl möglich
</label>
</div>
</div>`
: '';
const actionRow = `<div style="display:flex;gap:0.5rem;align-items:center;margin-top:0.5rem;" onclick="event.stopPropagation()">
<label class="compose-action-btn" title="Fotos hinzufügen">📷
<input type="file" accept="image/*" multiple style="display:none;" onchange="event.stopPropagation();gruppeEditAddImg(this,'${beitragId}')">
</label>
<button onclick="event.stopPropagation();saveGruppeEdit('${beitragId}')" style="width:auto;margin:0;">Speichern</button>
<button onclick="event.stopPropagation();cancelGruppeEdit('${beitragId}')" style="width:auto;margin:0;background:var(--color-secondary);color:var(--color-text);">Abbrechen</button>
</div>`;
const ea = document.getElementById('gpea-' + beitragId);
ea.style.display = '';
ea.onclick = e => e.stopPropagation();
ea.innerHTML = `
<textarea id="gpet-${beitragId}" style="width:100%;box-sizing:border-box;padding:0.6rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;resize:vertical;min-height:70px;" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${esc(post.text)}</textarea>
<div class="compose-thumbs" id="gpet-tb-${beitragId}" style="margin-top:0.4rem;"></div>
${optionenHtml}
${actionRow}`;
renderGruppeEditThumbs(beitragId);
}
function cancelGruppeEdit(beitragId) {
document.getElementById('gpva-' + beitragId).style.display = '';
document.getElementById('gpum-' + beitragId).style.display = '';
document.getElementById('gpea-' + beitragId).style.display = 'none';
gruppeEditBilder.delete(beitragId);
}
function renderGruppeEditThumbs(beitragId) {
const bilder = gruppeEditBilder.get(beitragId) || [];
const c = document.getElementById('gpet-tb-' + beitragId);
c.innerHTML = bilder.map((b, i) =>
`<div class="compose-thumb"><img src="data:image/jpeg;base64,${b}" alt="">
<button class="compose-thumb-remove" onclick="event.stopPropagation();gruppeEditRmImg('${beitragId}',${i})">✕</button></div>`
).join('');
c.style.display = bilder.length > 0 ? 'flex' : 'none';
}
function gruppeEditRmImg(beitragId, idx) {
gruppeEditBilder.get(beitragId).splice(idx, 1);
renderGruppeEditThumbs(beitragId);
}
function gruppeEditAddImg(input, beitragId) {
[...input.files].forEach(f => {
if (!f.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
const MAX = 1024, canvas = document.createElement('canvas');
const s = Math.min(MAX / img.width, MAX / img.height, 1);
canvas.width = Math.round(img.width * s); canvas.height = Math.round(img.height * s);
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
gruppeEditBilder.get(beitragId).push(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
renderGruppeEditThumbs(beitragId);
};
img.src = e.target.result;
};
reader.readAsDataURL(f);
});
input.value = '';
}
function gruppeEditAddOption(beitragId) {
const container = document.getElementById('gpeo-' + beitragId);
const count = container.querySelectorAll('input[type=text]').length;
const row = document.createElement('div');
row.className = 'umfrage-option-row';
row.innerHTML = `<input type="text" placeholder="Option ${count + 1}" maxlength="200"
style="flex:1;padding:0.4rem 0.6rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.9rem;"
onmousedown="event.stopPropagation()" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();this.closest('.umfrage-option-row').remove()" style="width:auto;margin:0;padding:0.3rem 0.6rem;font-size:0.8rem;">✕</button>`;
container.insertBefore(row, container.querySelector('div:last-child'));
}
async function saveGruppeEdit(beitragId) {
const text = document.getElementById('gpet-' + beitragId).value.trim();
if (!text) return;
const post = allPosts.find(p => p.beitragId === beitragId);
const bilder = gruppeEditBilder.get(beitragId) || [];
const isUmfrageEdit = post?.beitragTyp === 'UMFRAGE';
const optionen = isUmfrageEdit
? Array.from(document.querySelectorAll(`#gpeo-${beitragId} input[type=text]`))
.map(inp => ({ optionId: inp.dataset.optionId || null, text: inp.value.trim() }))
.filter(o => o.text)
: null;
const multiChoice = isUmfrageEdit ? (document.getElementById('gpmc-' + beitragId)?.checked ?? false) : null;
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + beitragId, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, bilder, optionen, multiChoice })
});
if (!res.ok) return;
const updated = await res.json();
const idx = allPosts.findIndex(p => p.beitragId === beitragId);
if (idx >= 0) allPosts[idx] = { ...allPosts[idx], text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice };
gruppeEditBilder.delete(beitragId);
const gpva = document.getElementById('gpva-' + beitragId);
gpva.querySelector('.post-text').innerHTML = renderTextWithHashtags(updated.text);
const pbi = document.getElementById('gpbi-' + beitragId);
if (pbi) pbi.innerHTML = bilderGrid(updated.bilder);
gpva.style.display = '';
document.getElementById('gpea-' + beitragId).style.display = 'none';
const gpum = document.getElementById('gpum-' + beitragId);
gpum.innerHTML = buildGruppeUmfrageHtml(beitragId, updated.optionen, post?.myVoteOptionIds || [], post?.multiChoice);
gpum.style.display = '';
const meta = document.getElementById('gpm-' + beitragId);
if (meta && !meta.querySelector('.edited-label')) {
meta.insertAdjacentHTML('beforeend', ' <span class="edited-label" style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>');
}
}
async function toggleLike(postId, btn) {
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/like', { method:'POST' });
if (!res.ok) return;
const countEl = document.getElementById('like-count-' + postId);
const isActive = btn.classList.contains('active');
btn.classList.toggle('active');
countEl.textContent = parseInt(countEl.textContent) + (isActive ? -1 : 1);
}
async function vote(postId, optionId, barEl) {
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/vote', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ optionId })
});
if (!res.ok) return;
// Reload posts to update vote counts
await loadPosts();
}
async function reportPost(postId, btn) {
btn.disabled = true;
const grund = prompt('Grund der Meldung (optional):');
if (grund === null) { btn.disabled = false; return; }
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/report', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ grund })
});
if (res.ok || res.status === 201) {
btn.textContent = 'Gemeldet';
btn.disabled = true;
} else {
btn.disabled = false;
}
}
async function deletePost(postId, btn) {
showModal('Beitrag löschen', 'Beitrag wirklich löschen?', [
{ label: 'Abbrechen', secondary: true },
{ label: 'Löschen', danger: true, onClick: async () => {
btn.disabled = true;
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId, { method:'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('post-' + postId)?.remove();
allPosts = allPosts.filter(p => p.beitragId !== postId);
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
} else { btn.disabled = false; }
}}
]);
}
// ── Compose image ──
let composeBilderArr = [];
function selectComposeBilder(input) {
[...input.files].forEach(f => { if (f.type.startsWith('image/')) processComposeImage(f); });
input.value = '';
}
function processComposeImage(file) {
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
const MAX = 1024;
const canvas = document.createElement('canvas');
const scale = Math.min(MAX / img.width, MAX / img.height, 1);
canvas.width = Math.round(img.width * scale);
canvas.height = Math.round(img.height * scale);
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
composeBilderArr.push(canvas.toDataURL('image/jpeg', 0.88).split(',')[1]);
renderComposeThumbs();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function renderComposeThumbs() {
const container = document.getElementById('composeThumbs');
container.innerHTML = '';
composeBilderArr.forEach((b, i) => {
const div = document.createElement('div');
div.className = 'compose-thumb';
div.innerHTML = `<img src="data:image/jpeg;base64,${b}" alt="">
<button class="compose-thumb-remove" onclick="removeThumb(${i})" title="Entfernen">✕</button>`;
container.appendChild(div);
});
container.style.display = composeBilderArr.length > 0 ? 'flex' : 'none';
}
function removeThumb(idx) {
composeBilderArr.splice(idx, 1);
renderComposeThumbs();
}
// ── Compose ──
function toggleUmfrage(btn) {
const options = document.getElementById('umfrageOptions');
const isShowing = options.style.display !== 'none';
options.style.display = isShowing ? 'none' : '';
const placeholder = document.getElementById('composeText');
placeholder.placeholder = isShowing ? 'Was möchtest du teilen?' : 'Frage eingeben…';
if (btn) btn.classList.toggle('active', !isShowing);
if (!isShowing && document.getElementById('optionList').children.length === 0) {
addOption(); addOption();
}
}
function resetUmfrage() {
document.getElementById('umfrageOptions').style.display = 'none';
document.getElementById('optionList').innerHTML = '';
document.getElementById('composeText').placeholder = 'Was möchtest du teilen?';
document.getElementById('umfrageBtn').classList.remove('active');
}
function addOption() {
const list = document.getElementById('optionList');
const idx = list.children.length;
const div = document.createElement('div');
div.className = 'umfrage-option-row';
div.innerHTML = `<input type="text" placeholder="Option ${idx+1}" maxlength="200"><button onclick="this.parentElement.remove()">✕</button>`;
list.appendChild(div);
}
async function submitPost() {
const text = document.getElementById('composeText').value.trim();
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
if (!text) return;
const beitragTyp = hasUmfrage ? 'UMFRAGE' : 'TEXT';
let optionen = null;
let multiChoice = null;
if (hasUmfrage) {
optionen = Array.from(document.querySelectorAll('#optionList input')).map(i => i.value.trim()).filter(v => v);
if (optionen.length < 2) { showModal('Hinweis', 'Bitte mindestens 2 Optionen eingeben.', [{ label: 'OK' }]); return; }
multiChoice = document.getElementById('multiChoice').checked;
}
const bilder = [...composeBilderArr];
const res = await fetch('/gruppen/' + gruppeId + '/posts', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ beitragTyp, text, multiChoice, optionen, bilder })
});
if (res.ok || res.status === 201) {
document.getElementById('composeText').value = '';
resetUmfrage();
composeBilderArr = [];
renderComposeThumbs();
await loadPosts();
}
}
// ── Members ──
async function loadMembers() {
const res = await fetch('/gruppen/' + gruppeId + '/members');
if (!res.ok) return;
const members = await res.json();
const list = document.getElementById('memberList');
list.innerHTML = '';
members.forEach(m => {
const av = m.userPicture ? `<img src="data:image/png;base64,${m.userPicture}" alt="">` : '◉';
const roleBadge = m.rolle === 'ADMIN'
? '<span class="role-badge">Admin</span>'
: '<span class="role-badge mitglied">Mitglied</span>';
let actions = '';
if (myRole === 'ADMIN' && m.userId !== myId) {
actions = `<div class="member-actions">
<button onclick="promoteMember('${m.userId}',this)">Zum Admin</button>
<button onclick="removeMember('${m.userId}',this)" style="background:#c0392b;">Entfernen</button>
</div>`;
}
list.insertAdjacentHTML('beforeend', `
<li class="member-item" id="member-${m.userId}">
<div class="member-avatar">${av}</div>
<a href="/community/benutzer.html?userId=${m.userId}" class="member-name" style="text-decoration:none;color:inherit;">${esc(m.userName)}</a>
${roleBadge}
${actions}
</li>`);
});
}
async function removeMember(userId, btn) {
showModal('Mitglied entfernen', 'Dieses Mitglied wirklich aus der Gruppe entfernen?', [
{ label: 'Abbrechen', secondary: true },
{ label: 'Entfernen', danger: true, onClick: async () => {
btn.disabled = true;
const res = await fetch('/gruppen/' + gruppeId + '/members/' + userId, { method:'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('member-' + userId)?.remove();
} else { btn.disabled = false; }
}}
]);
}
async function promoteMember(userId, btn) {
showModal('Mitglied befördern', 'Dieses Mitglied zum Admin befördern?', [
{ label: 'Abbrechen', secondary: true },
{ label: 'Befördern', onClick: async () => {
btn.disabled = true;
const res = await fetch('/gruppen/' + gruppeId + '/members/' + userId + '/promote', { method:'POST' });
if (res.ok) {
await loadMembers();
} else { btn.disabled = false; }
}}
]);
}
// ── Leave / Join ──
function leaveGruppe() {
document.getElementById('leaveDialog').classList.add('visible');
}
async function confirmLeave() {
const res = await fetch('/gruppen/' + gruppeId + '/leave', { method:'DELETE' });
if (res.ok || res.status === 204) {
location.href = '/community/gruppen.html';
}
}
// ── Admin ──
let adminJoinsCount = 0;
let adminReportsCount = 0;
function setAdminBadge() {
const total = adminJoinsCount + adminReportsCount;
const badge = document.getElementById('adminBadge');
if (!badge) return;
badge.textContent = total;
badge.style.display = total > 0 ? '' : 'none';
}
async function loadAdminRequests() {
const sec = document.getElementById('requestsSection');
const res = await fetch('/gruppen/' + gruppeId + '/requests');
if (!res.ok) return;
const data = await res.json();
adminJoinsCount = data.length;
setAdminBadge();
sec.innerHTML = '';
if (data.length === 0) { sec.innerHTML = '<p class="empty-hint">Keine ausstehenden Anfragen.</p>'; return; }
data.forEach(r => {
const av = r.userPicture ? `<img src="data:image/png;base64,${r.userPicture}" alt="">` : '◉';
sec.insertAdjacentHTML('beforeend', `
<div class="request-item" id="req-${r.anfrageId}">
<div style="display:flex;align-items:center;gap:0.6rem;margin-bottom:0.4rem;">
<div class="comment-avatar">${av}</div>
<span class="req-name">${esc(r.userName)}</span>
</div>
${r.nachricht ? `<div class="req-msg">"${esc(r.nachricht)}"</div>` : ''}
<div class="req-actions">
<button onclick="rejectRequest('${r.anfrageId}',this)" class="secondary">✕ Ablehnen</button>
<button onclick="approveRequest('${r.anfrageId}',this)">✓ Genehmigen</button>
</div>
</div>`);
});
}
async function approveRequest(reqId, btn) {
btn.disabled = true;
const res = await fetch('/gruppen/' + gruppeId + '/requests/' + reqId + '/approve', { method:'POST' });
if (res.ok) {
document.getElementById('req-' + reqId)?.remove();
adminJoinsCount = Math.max(0, adminJoinsCount - 1);
setAdminBadge();
} else { btn.disabled = false; }
}
async function rejectRequest(reqId, btn) {
btn.disabled = true;
const res = await fetch('/gruppen/' + gruppeId + '/requests/' + reqId, { method:'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('req-' + reqId)?.remove();
adminJoinsCount = Math.max(0, adminJoinsCount - 1);
setAdminBadge();
} else { btn.disabled = false; }
}
async function loadReports() {
const sec = document.getElementById('reportsSection');
const res = await fetch('/gruppen/' + gruppeId + '/reports');
if (!res.ok) return;
const data = await res.json();
adminReportsCount = data.length;
setAdminBadge();
sec.innerHTML = '';
if (data.length === 0) { sec.innerHTML = '<p class="empty-hint">Keine Meldungen.</p>'; return; }
// Fetch post data for each report (use cache when available)
const postPromises = data.map(m => {
const cached = allPosts.find(p => p.beitragId === m.beitragId);
if (cached) return Promise.resolve(cached);
return fetch('/gruppen/' + gruppeId + '/posts/' + m.beitragId)
.then(r => r.ok ? r.json() : null).catch(() => null);
});
const posts = await Promise.all(postPromises);
data.forEach((m, i) => {
const post = posts[i];
let postHtml;
if (post) {
const typeLabel = post.beitragTyp === 'UMFRAGE' ? '📊 Umfrage' : '✏ Text';
const firstBild = post.bilder && post.bilder.length > 0 ? post.bilder[0] : null;
const bildHtml = firstBild
? `<img src="data:image/jpeg;base64,${firstBild}" style="max-height:100px;max-width:100%;border-radius:4px;margin-top:0.4rem;object-fit:contain;display:block;">`
: '';
postHtml = `<div class="meld-post-preview">
<div class="meld-post-meta">${typeLabel} · ${esc(post.authorName)} · ${fmtDate(post.createdAt)}</div>
<div class="meld-post-text">${esc(post.text.substring(0, 250))}${post.text.length > 250 ? '…' : ''}</div>
${bildHtml}
</div>`;
} else {
postHtml = `<div class="meld-post-preview"><div class="meld-post-meta" style="font-style:italic;">Beitrag nicht mehr verfügbar</div></div>`;
}
sec.insertAdjacentHTML('beforeend', `
<div class="meldung-item" id="meld-${m.meldungId}">
${postHtml}
<div class="meld-grund">Gemeldet von <b>${esc(m.melderName)}</b>${m.grund ? ': ' + esc(m.grund) : ''} · ${fmtDate(m.gemeldetAt)}</div>
<div class="meld-actions">
<button onclick="dismissReport('${m.meldungId}',this)" class="secondary">Meldung verwerfen</button>
${post ? `<button onclick="deletePostAdmin('${m.meldungId}','${post.beitragId}')" style="background:var(--color-primary);">Post löschen</button>` : ''}
</div>
</div>`);
});
}
async function dismissReport(meldungId, btn) {
btn.disabled = true;
const res = await fetch('/gruppen/' + gruppeId + '/reports/' + meldungId, { method:'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('meld-' + meldungId)?.remove();
adminReportsCount = Math.max(0, adminReportsCount - 1);
setAdminBadge();
} else { btn.disabled = false; }
}
async function deletePostAdmin(meldungId, postId) {
showModal('Beitrag löschen', 'Diesen Beitrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.', [
{ label: 'Abbrechen', secondary: true },
{ label: 'Löschen', danger: true, onClick: () => doDeletePostAdmin(meldungId, postId) }
]);
}
async function doDeletePostAdmin(meldungId, postId) {
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId, { method:'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('post-' + postId)?.remove();
allPosts = allPosts.filter(p => p.beitragId !== postId);
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
await loadReports(); // refresh list (cascade deletes all reports for this post)
}
}
async function saveGruppe() {
const body = {
name: document.getElementById('editName').value.trim() || null,
beschreibung: document.getElementById('editDesc').value.trim() || null,
bild: document.getElementById('editBildData').value || null,
isPrivate: document.getElementById('editPrivate').checked
};
const res = await fetch('/gruppen/' + gruppeId, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
if (res.ok) {
showSaveMsg('Gespeichert.', 'success');
await loadGruppe();
} else {
showSaveMsg('Fehler beim Speichern.', 'error');
}
}
function openDeleteDialog() {
document.getElementById('deleteDialog').classList.add('visible');
}
async function deleteGruppe() {
const res = await fetch('/gruppen/' + gruppeId, { method:'DELETE' });
if (res.ok || res.status === 204) {
location.href = '/community/gruppen.html';
} else {
showModal('Fehler', 'Fehler beim Löschen der Gruppe.', [{ label: 'OK' }]);
}
}
function previewBild(input, previewId, dataId) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
const original = new Image();
original.onload = () => {
const MAX = 256;
let w = original.width, h = original.height;
if (w > MAX || h > MAX) {
if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
else { w = Math.round(w * MAX / h); h = MAX; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(original, 0, 0, w, h);
const scaled = canvas.toDataURL('image/jpeg', 0.85);
const img = document.getElementById(previewId);
img.src = scaled;
img.style.display = 'block';
document.getElementById(dataId).value = scaled.split(',')[1];
};
original.src = e.target.result;
};
reader.readAsDataURL(file);
}
document.getElementById('deleteDialog').addEventListener('click', e => {
if (e.target === document.getElementById('deleteDialog'))
document.getElementById('deleteDialog').classList.remove('visible');
});
// ── Post dialog ──
async function openPostDialog(postId) {
const post = allPosts.find(p => p.beitragId === postId);
if (!post) return;
renderLbPost(post);
_lbSetupContent(postId, 'gp', post.bilder);
document.getElementById('postLightbox').classList.add('open');
document.body.style.overflow = 'hidden';
await loadLbComments(postId, 'GROUP');
document.getElementById('lbCommentInput').focus();
}
function renderLbPost(p) {
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
let umfrageHtml = '';
if (p.beitragTyp === 'UMFRAGE') {
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
const bars = p.optionen.map(o => {
const pct = total > 0 ? Math.round(o.stimmenCount / total * 100) : 0;
const voted = p.myVoteOptionIds.includes(o.optionId);
return `<div class="umfrage-option-bar ${voted?'voted':''}" onclick="voteLb('${p.beitragId}','${o.optionId}')">
<div class="umfrage-bar-fill" style="width:${pct}%"></div>
<div class="umfrage-bar-content">
<span>${esc(o.text)}</span>
<span>${pct}% (${o.stimmenCount})</span>
</div>
</div>`;
}).join('');
umfrageHtml = bars + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
}
const textStyle = p.beitragTyp === 'UMFRAGE' ? ' style="font-weight:600;margin-bottom:0.5rem;"' : '';
document.getElementById('lbPostBody').innerHTML = `
<div class="post-header">
<div class="post-avatar"><a href="/community/benutzer.html?userId=${p.authorId}" style="display:contents;">${av}</a></div>
<div>
<div class="post-author"><a href="/community/benutzer.html?userId=${p.authorId}" style="color:inherit;text-decoration:none;">${esc(p.authorName)}</a></div>
<div class="post-date">${fmtDate(p.createdAt)}</div>
</div>
</div>
<div id="gpva-${p.beitragId}">
<div class="post-text"${textStyle}>${renderTextWithHashtags(p.text)}</div>
<div id="gpbi-${p.beitragId}"></div>
</div>
<div id="gpum-${p.beitragId}">${umfrageHtml}</div>
<div class="post-actions" style="margin-top:0.75rem;">
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="toggleLikeLb('${p.beitragId}',this)">
♥ <span id="lb-like-count-${p.beitragId}">${p.likeCount}</span>
</button>
${!p.reported ? `<button class="post-action-btn" onclick="reportPostLb('${p.beitragId}',this)">⚑ Melden</button>` : '<span style="font-size:0.78rem;color:var(--color-muted);">Gemeldet</span>'}
${canDelete ? `<button class="post-action-btn danger" onclick="deletePostLb('${p.beitragId}')">✕ Löschen</button>` : ''}
</div>`;
}
async function toggleLikeLb(postId, btn) {
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/like', { method:'POST' });
if (!res.ok) return;
const isActive = btn.classList.contains('active');
btn.classList.toggle('active');
const countEl = document.getElementById('lb-like-count-' + postId);
const newCount = parseInt(countEl.textContent) + (isActive ? -1 : 1);
countEl.textContent = newCount;
const feedBtn = document.getElementById('like-btn-' + postId);
const feedCount = document.getElementById('like-count-' + postId);
if (feedBtn) feedBtn.classList.toggle('active', !isActive);
if (feedCount) feedCount.textContent = newCount;
const post = allPosts.find(p => p.beitragId === postId);
if (post) { post.likeCount = newCount; post.likedByMe = !isActive; }
}
async function reportPostLb(postId, btn) {
btn.disabled = true;
const grund = prompt('Grund der Meldung (optional):');
if (grund === null) { btn.disabled = false; return; }
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/report', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ grund })
});
if (res.ok || res.status === 201) {
btn.textContent = 'Gemeldet'; btn.disabled = true;
const post = allPosts.find(p => p.beitragId === postId);
if (post) post.reported = true;
} else { btn.disabled = false; }
}
async function deletePostLb(postId) {
showModal('Beitrag löschen', 'Beitrag wirklich löschen?', [
{ label: 'Abbrechen', secondary: true },
{ label: 'Löschen', danger: true, onClick: async () => {
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId, { method:'DELETE' });
if (res.ok || res.status === 204) {
document.getElementById('post-' + postId)?.remove();
allPosts = allPosts.filter(p => p.beitragId !== postId);
closeLb();
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
}
}}
]);
}
async function voteLb(postId, optionId) {
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/vote', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ optionId })
});
if (!res.ok) return;
await loadPosts();
const post = allPosts.find(p => p.beitragId === postId);
if (post) {
renderLbPost(post);
_lbSetupContent(postId, 'gp', post.bilder);
}
}
document.getElementById('postLightbox').addEventListener('click', e => {
if (e.target === document.getElementById('postLightbox')) closeLb();
});
init();
</script>
</body>
</html>