Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1103 lines
58 KiB
HTML
1103 lines
58 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">
|
||
<style>
|
||
.tabs { display:flex; gap:0; margin-bottom:1.5rem; border-bottom:1px solid var(--color-secondary); }
|
||
.tab-btn { background:none; border:none; border-bottom:3px solid transparent; border-radius:0; padding:0.6rem 1.25rem; font-size:0.95rem; font-weight:600; color:var(--color-muted); cursor:pointer; margin-bottom:-1px; transition:color 0.15s,border-color 0.15s; }
|
||
.tab-btn:hover { color:var(--color-text); background:none; }
|
||
.tab-btn.active { color:var(--color-primary); border-bottom-color:var(--color-primary); }
|
||
.tab-panel { display:none; }
|
||
.tab-panel.active { display:block; }
|
||
|
||
/* 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; }
|
||
|
||
/* Posts */
|
||
.post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; }
|
||
.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; }
|
||
.post-compose textarea { width:100%; 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:70px; box-sizing:border-box; }
|
||
.post-compose textarea:focus { border-color:var(--color-primary); }
|
||
.compose-thumbs { display:none; flex-wrap:wrap; gap:0.5rem; margin-top:0.5rem; }
|
||
.compose-thumb { position:relative; width:64px; height:64px; flex-shrink:0; }
|
||
.compose-thumb img { width:64px; height:64px; object-fit:cover; border-radius:6px; display:block; }
|
||
.compose-thumb-remove { position:absolute; top:-5px; right:-5px; background:rgba(0,0,0,0.7); border:none; color:#fff; width:18px; height:18px; border-radius:50%; font-size:0.65rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; margin:0; width:auto; line-height:1; }
|
||
.compose-action-btn { background:none; border:1px solid var(--color-secondary); color:var(--color-muted); border-radius:6px; padding:0.35rem 0.6rem; font-size:0.95rem; cursor:pointer; margin:0; width:auto; transition:border-color 0.15s,color 0.15s; }
|
||
.compose-action-btn:hover { border-color:var(--color-primary); color:var(--color-primary); background:none; }
|
||
label.compose-action-btn { display:inline-flex; align-items:center; }
|
||
.umfrage-options { margin-top:0.5rem; }
|
||
.post-bild { width:100%; max-height:400px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
|
||
.umfrage-option-row { display:flex; gap:0.5rem; margin-bottom:0.4rem; }
|
||
.umfrage-option-row input { flex:1; }
|
||
.umfrage-option-row button { width:auto; margin:0; padding:0.3rem 0.6rem; font-size:0.8rem; }
|
||
.compose-footer { display:flex; justify-content:space-between; align-items:center; margin-top:0.75rem; flex-wrap:wrap; gap:0.5rem; }
|
||
.multi-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
|
||
|
||
.post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; }
|
||
.post-header { display:flex; align-items:center; gap:0.7rem; margin-bottom:0.6rem; }
|
||
.post-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.95rem; flex-shrink:0; overflow:hidden; }
|
||
.post-avatar img { width:100%; height:100%; object-fit:cover; }
|
||
.post-author { font-weight:600; font-size:0.9rem; }
|
||
.post-date { font-size:0.75rem; color:var(--color-muted); margin-left:auto; }
|
||
.post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
|
||
.post-actions { display:flex; gap:1rem; margin-top:0.75rem; align-items:center; flex-wrap:wrap; }
|
||
.post-action-btn { background:none; border:none; color:var(--color-muted); cursor:pointer; font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; }
|
||
.post-action-btn:hover { color:var(--color-primary); background:none; }
|
||
.post-action-btn.active { color:var(--color-primary); }
|
||
.post-action-btn.danger:hover { color:#c0392b; }
|
||
.post-delete { margin-left:auto; }
|
||
|
||
/* Umfrage */
|
||
.umfrage-option-bar { margin:0.3rem 0; cursor:pointer; border-radius:6px; overflow:hidden; border:1px solid var(--color-secondary); position:relative; transition:border-color 0.15s; }
|
||
.umfrage-option-bar:hover { border-color:var(--color-primary); }
|
||
.umfrage-option-bar.voted { border-color:var(--color-primary); }
|
||
.umfrage-bar-fill { position:absolute; inset:0; background:rgba(var(--color-primary-rgb,180,0,60),0.15); transition:width 0.4s; }
|
||
.umfrage-bar-content { position:relative; display:flex; justify-content:space-between; padding:0.45rem 0.75rem; font-size:0.88rem; }
|
||
.umfrage-total { font-size:0.78rem; color:var(--color-muted); margin-top:0.3rem; }
|
||
|
||
/* 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; }
|
||
|
||
/* Post lightbox */
|
||
.lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:300; align-items:center; justify-content:center; }
|
||
.lightbox.open { display:flex; }
|
||
.lb-layout { display:flex; max-width:min(1340px, calc(100vw - 2rem)); width:95vw; height:min(90vh, 1100px); background:var(--color-card); border-radius:12px; overflow:hidden; position:relative; }
|
||
.lb-post-side .post-bild { max-height:1024px; }
|
||
.lb-close { position:absolute; top:0.6rem; right:0.6rem; background:rgba(0,0,0,0.55); border:none; color:#fff; font-size:1.1rem; width:2rem; height:2rem; border-radius:50%; cursor:pointer; z-index:10; display:flex; align-items:center; justify-content:center; padding:0; margin:0; }
|
||
.lb-post-side { flex:1; overflow-y:auto; padding:1.25rem; border-right:1px solid var(--color-secondary); min-width:0; }
|
||
.lb-comments-panel { width:300px; flex-shrink:0; display:flex; flex-direction:column; }
|
||
.lb-comments-list { flex:1; overflow-y:auto; padding:0.75rem; }
|
||
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; flex-direction:column; gap:0.5rem; flex-shrink:0; }
|
||
.lb-comment-compose textarea { width:100%; font-size:0.85rem; padding:0.35rem 0.6rem; resize:none; background:var(--color-secondary); border:1px solid var(--color-secondary); border-radius:6px; color:var(--color-text); font-family:inherit; outline:none; transition:border-color 0.2s; box-sizing:border-box; }
|
||
.lb-comment-compose textarea:focus { border-color:var(--color-primary); }
|
||
.lb-comment-compose-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
|
||
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
|
||
@media (max-width:650px) {
|
||
.lb-layout { flex-direction:column; height:95vh; }
|
||
.lb-post-side { border-right:none; border-bottom:1px solid var(--color-secondary); max-height:55vh; }
|
||
.lb-comments-panel { width:100%; }
|
||
}
|
||
|
||
/* 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;">
|
||
<div class="compose-type">
|
||
<label><input type="radio" name="beitragTyp" value="TEXT" checked onchange="toggleUmfrage()"> Text</label>
|
||
<label><input type="radio" name="beitragTyp" value="UMFRAGE" onchange="toggleUmfrage()"> Umfrage</label>
|
||
</div>
|
||
<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>
|
||
<button onclick="addOption()" style="width:auto; margin:0; padding:0.3rem 0.75rem; font-size:0.8rem; margin-top:0.4rem;">+ Option</button>
|
||
</div>
|
||
<div class="compose-footer">
|
||
<label class="multi-toggle" id="multiChoiceRow" style="display:none;">
|
||
<input type="checkbox" id="multiChoice"> Multi-Choice
|
||
</label>
|
||
<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 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="postDialog">
|
||
<div class="lb-layout">
|
||
<button class="lb-close" onclick="closePostDialog()">✕</button>
|
||
<div class="lb-post-side" id="lbPostContent"></div>
|
||
<div class="lb-comments-panel">
|
||
<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();submitLbComment()}"></textarea>
|
||
<div class="lb-comment-compose-actions">
|
||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
||
<button onclick="submitLbComment()">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;
|
||
|
||
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';
|
||
}
|
||
|
||
|
||
function renderPostCard(p) {
|
||
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
||
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
|
||
const bildHtml = bilderCarousel(p.bilder);
|
||
|
||
let body = '';
|
||
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="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('');
|
||
body = bars + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
|
||
} else {
|
||
body = `<div class="post-text">${renderTextWithHashtags(p.text)}</div>${bildHtml}`;
|
||
}
|
||
|
||
return `
|
||
<div class="post-card" id="post-${p.beitragId}" onclick="openPostDialog('${p.beitragId}')" style="cursor:pointer;">
|
||
<div class="post-header">
|
||
<div class="post-avatar">${av}</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>
|
||
<div class="post-date">${fmtDate(p.createdAt)}</div>
|
||
</div>
|
||
${p.beitragTyp === 'UMFRAGE' ? `<div style="font-weight:600;margin-bottom:0.5rem;">${renderTextWithHashtags(p.text)}</div>${bildHtml}` : ''}
|
||
${body}
|
||
<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>'}
|
||
${canDelete ? `<button class="post-action-btn danger post-delete" onclick="event.stopPropagation(); deletePost('${p.beitragId}',this)">✕</button>` : ''}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
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() {
|
||
const isUmfrage = document.querySelector('input[name="beitragTyp"]:checked')?.value === 'UMFRAGE';
|
||
document.getElementById('umfrageOptions').style.display = isUmfrage ? '' : 'none';
|
||
document.getElementById('multiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
||
const placeholder = document.getElementById('composeText');
|
||
placeholder.placeholder = isUmfrage ? 'Frage eingeben…' : 'Was möchtest du teilen?';
|
||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
||
addOption(); addOption();
|
||
}
|
||
}
|
||
|
||
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();
|
||
if (!text) return;
|
||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked')?.value || 'TEXT';
|
||
let optionen = null;
|
||
let multiChoice = null;
|
||
if (beitragTyp === 'UMFRAGE') {
|
||
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 = '';
|
||
document.getElementById('optionList').innerHTML = '';
|
||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
||
toggleUmfrage();
|
||
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 ──
|
||
|
||
let lbPostId = null;
|
||
|
||
async function openPostDialog(postId) {
|
||
lbPostId = postId;
|
||
const post = allPosts.find(p => p.beitragId === postId);
|
||
if (!post) return;
|
||
renderLbPost(post);
|
||
document.getElementById('postDialog').classList.add('open');
|
||
await loadLbComments();
|
||
document.getElementById('lbCommentInput').focus();
|
||
}
|
||
|
||
function closePostDialog() {
|
||
document.getElementById('postDialog').classList.remove('open');
|
||
lbPostId = null;
|
||
}
|
||
|
||
function renderLbPost(p) {
|
||
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
||
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
|
||
const bildHtml = bilderCarousel(p.bilder);
|
||
let body = '';
|
||
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('');
|
||
body = `<div style="font-weight:600;margin-bottom:0.5rem;">${esc(p.text)}</div>${bildHtml}${bars}<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
|
||
} else {
|
||
body = `<div class="post-text">${esc(p.text)}</div>${bildHtml}`;
|
||
}
|
||
document.getElementById('lbPostContent').innerHTML = `
|
||
<div class="post-header">
|
||
<div class="post-avatar">${av}</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>
|
||
${body}
|
||
<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 deleteKommentar(kommentarId) {
|
||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||
await loadLbComments();
|
||
}
|
||
|
||
async function loadLbComments() {
|
||
if (!lbPostId) return;
|
||
const list = document.getElementById('lbCommentsList');
|
||
list.innerHTML = '';
|
||
const res = await fetch('/social/kommentare?targetType=GROUP_POST&targetId=' + lbPostId);
|
||
if (!res.ok) return;
|
||
const kmts = await res.json();
|
||
kmts.forEach(k => list.insertAdjacentHTML('beforeend', renderKommentarHtml(k, 'GROUP_POST', lbPostId, { myUserId: myId })));
|
||
list.scrollTop = list.scrollHeight;
|
||
}
|
||
|
||
async function submitLbComment() {
|
||
if (!lbPostId) return;
|
||
const input = document.getElementById('lbCommentInput');
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
const res = await fetch('/social/kommentare', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ targetType: 'GROUP_POST', targetId: lbPostId, text })
|
||
});
|
||
if (res.ok || res.status === 201) {
|
||
input.value = '';
|
||
await loadLbComments();
|
||
const countEl = document.getElementById('kmt-count-' + lbPostId);
|
||
if (countEl) countEl.textContent = parseInt(countEl.textContent) + 1;
|
||
const post = allPosts.find(p => p.beitragId === lbPostId);
|
||
if (post) post.kommentarCount++;
|
||
}
|
||
}
|
||
|
||
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);
|
||
closePostDialog();
|
||
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);
|
||
}
|
||
|
||
document.getElementById('postDialog').addEventListener('click', e => {
|
||
if (e.target === document.getElementById('postDialog')) closePostDialog();
|
||
});
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePostDialog(); });
|
||
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|