Locations können nun auch Posten - bugfixes im Feed
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-13 23:04:15 +02:00
parent e2a71ab096
commit e35b095c18
53 changed files with 4186 additions and 2502 deletions

View File

@@ -7,14 +7,8 @@
<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>
.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; }
@@ -24,48 +18,9 @@
.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-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; }
.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; }
@@ -111,26 +66,6 @@
.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; }
@@ -164,25 +99,24 @@
<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 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">
<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 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>
@@ -277,18 +211,19 @@
</div>
<!-- Post lightbox dialog -->
<div class="lightbox" id="postDialog">
<div class="lightbox" id="postLightbox">
<div class="lb-layout">
<button class="lb-close" onclick="closePostDialog()"></button>
<div class="lb-post-side" id="lbPostContent"></div>
<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();submitLbComment()}"></textarea>
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="submitLbComment()">Senden</button>
<button onclick="postLbComment()">Senden</button>
</div>
</div>
</div>
@@ -374,6 +309,7 @@
if (!meRes.ok) { location.href='/login.html'; return; }
const me = await meRes.json();
myId = me.userId;
initLb(myId);
await loadGruppe();
await loadPosts();
@@ -471,15 +407,24 @@
}
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 = bilderCarousel(p.bilder);
const bildHtml = bilderGrid(p.bilder);
const editedLabel = p.editedAt ? ` <span style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>` : '';
let body = '';
// 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);
const bars = p.optionen.map(o => {
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)">
@@ -489,23 +434,25 @@
<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}`;
}).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">${av}</div>
<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>
<div class="post-date">${fmtDate(p.createdAt)}</div>
${rightBtns ? `<div style="margin-left:auto;display:flex;gap:0.25rem;">${rightBtns}</div>` : ''}
</div>
${p.beitragTyp === 'UMFRAGE' ? `<div style="font-weight:600;margin-bottom:0.5rem;">${renderTextWithHashtags(p.text)}</div>${bildHtml}` : ''}
${body}
<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>
@@ -514,11 +461,162 @@
💬 <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>`;
}
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;
@@ -620,17 +718,25 @@
// ── 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';
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 = isUmfrage ? 'Frage eingeben…' : 'Was möchtest du teilen?';
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
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;
@@ -642,11 +748,12 @@
async function submitPost() {
const text = document.getElementById('composeText').value.trim();
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
if (!text) return;
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked')?.value || 'TEXT';
const beitragTyp = hasUmfrage ? 'UMFRAGE' : 'TEXT';
let optionen = null;
let multiChoice = null;
if (beitragTyp === 'UMFRAGE') {
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;
@@ -659,9 +766,7 @@
});
if (res.ok || res.status === 201) {
document.getElementById('composeText').value = '';
document.getElementById('optionList').innerHTML = '';
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
toggleUmfrage();
resetUmfrage();
composeBilderArr = [];
renderComposeThumbs();
await loadPosts();
@@ -938,28 +1043,22 @@
// ── 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();
_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 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 = '';
let umfrageHtml = '';
if (p.beitragTyp === 'UMFRAGE') {
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
const bars = p.optionen.map(o => {
@@ -973,19 +1072,23 @@
</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}`;
umfrageHtml = bars + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
}
document.getElementById('lbPostContent').innerHTML = `
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">${av}</div>
<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>
${body}
<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>
@@ -1011,41 +1114,7 @@
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;
@@ -1071,7 +1140,7 @@
if (res.ok || res.status === 204) {
document.getElementById('post-' + postId)?.remove();
allPosts = allPosts.filter(p => p.beitragId !== postId);
closePostDialog();
closeLb();
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
}
}}
@@ -1087,13 +1156,15 @@
if (!res.ok) return;
await loadPosts();
const post = allPosts.find(p => p.beitragId === postId);
if (post) renderLbPost(post);
if (post) {
renderLbPost(post);
_lbSetupContent(postId, 'gp', post.bilder);
}
}
document.getElementById('postDialog').addEventListener('click', e => {
if (e.target === document.getElementById('postDialog')) closePostDialog();
document.getElementById('postLightbox').addEventListener('click', e => {
if (e.target === document.getElementById('postLightbox')) closeLb();
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePostDialog(); });
init();