Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1174 lines
61 KiB
HTML
1174 lines
61 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>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>
|