Files
xxx-sphere-web/bin/main/static/community/feed.html
Mario a13b8e894f
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Alles mögliche
2026-04-08 00:53:49 +02:00

570 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed 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; }
.post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; transition:border-color 0.15s; }
.post-compose.drag-over { border-color:var(--color-primary); background:rgba(var(--color-primary-rgb,180,0,60),0.06); }
.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; }
.umfrage-options { margin-top:0.5rem; }
.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; }
.privacy-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
.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; }
.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-meta { font-size:0.75rem; color:var(--color-muted); }
.post-date { font-size:0.75rem; color:var(--color-muted); margin-left:auto; }
.post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
.post-bild { width:100%; max-height:400px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
.post-actions { display:flex; gap:1rem; margin-top:0.75rem; align-items:center; flex-wrap:wrap; }
.post-action-btn { background:none; border:none; color:var(--color-muted); cursor:pointer; font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; margin:0; width:auto; }
.post-action-btn:hover { color:var(--color-primary); background:none; }
.post-action-btn.active { color:var(--color-primary); }
.post-delete { margin-left:auto; }
.post-delete:hover { color:#c0392b !important; }
/* Carousel Stile kommen aus shared.js */
.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; }
.gruppe-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.75rem; color:var(--color-muted); background:var(--color-secondary); border-radius:4px; padding:0.15rem 0.45rem; margin-top:0.1rem; }
.gruppe-badge a { color:inherit; text-decoration:none; }
.gruppe-badge a:hover { color:var(--color-primary); }
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
.sentinel { height:1px; }
/* 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-header { font-size:0.78rem; font-weight:600; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.06em; padding:0.7rem 1rem; border-bottom:1px solid var(--color-secondary); flex-shrink:0; }
.lb-comments-list { flex:1; overflow-y:auto; padding:0.75rem; }
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; flex-direction:column; gap:0.5rem; flex-shrink:0; }
.lb-comment-compose textarea { width:100%; font-size:0.85rem; padding:0.35rem 0.6rem; resize:none; background:var(--color-secondary); border:1px solid var(--color-secondary); border-radius:6px; color:var(--color-text); font-family:inherit; outline:none; transition:border-color 0.2s; box-sizing:border-box; }
.lb-comment-compose textarea:focus { border-color:var(--color-primary); }
.lb-comment-compose-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
@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%; } }
/* Comment + Like-Stile kommen aus shared.js */
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div class="tabs">
<button class="tab-btn active" id="tabMine" data-tab="mine" onclick="switchTab('mine', this)">Mein Feed</button>
<button class="tab-btn" id="tabPublic" data-tab="public" onclick="switchTab('public', this)">Öffentlicher Feed</button>
</div>
<!-- Mein Feed -->
<div class="tab-panel active" id="tab-mine">
<div class="post-compose" id="compose">
<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">
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
<label class="multi-toggle" id="multiChoiceRow" style="display:none;">
<input type="checkbox" id="multiChoice"> Multi-Choice
</label>
<label class="privacy-toggle">
<input type="checkbox" id="isPublic"> Öffentlich
</label>
</div>
<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="mineFeed"></div>
<p class="empty-hint" id="mineEmpty" style="display:none;">Noch keine Beiträge. Schreib den ersten!</p>
<div class="sentinel" id="mineSentinel"></div>
</div>
<!-- Öffentlicher Feed -->
<div class="tab-panel" id="tab-public">
<div id="publicFeed"></div>
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
<div class="sentinel" id="publicSentinel"></div>
</div>
</div>
</div>
<!-- Post Lightbox -->
<div class="lightbox" id="postLightbox">
<div class="lb-layout">
<button class="lb-close" onclick="closeLb()"></button>
<div class="lb-post-side" id="lbPostBody"></div>
<div class="lb-comments-panel">
<div class="lb-comments-header">Kommentare</div>
<div class="lb-comments-list" id="lbCommentsList"></div>
<div class="lb-comment-compose">
<textarea id="lbCommentInput" placeholder="Kommentar schreiben…" maxlength="500" rows="3"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();postLbComment()}"></textarea>
<div class="lb-comment-compose-actions">
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
<button onclick="postLbComment()">Senden</button>
</div>
</div>
</div>
</div>
</div>
<script src="/js/shared.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/meldung.js"></script>
<script>
// ── State ──
let myUserId = null;
let activeLbPostId = null;
let activeLbPostType = null;
const feedState = {
mine: { page:0, hasMore:true, loading:false, loaded:false },
public: { page:0, hasMore:true, loading:false, loaded:false }
};
let composeBilderArr = [];
// ── Boot ──
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
if (user) {
myUserId = user.userId;
const raw = sessionStorage.getItem('feedOpenPost');
if (raw) {
sessionStorage.removeItem('feedOpenPost');
loadFeed('mine');
openLbWithData(JSON.parse(raw));
} else {
await loadFeed('mine');
}
}
}).catch(() => {});
// ── Tab switching ──
function switchTab(name, btn) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
localStorage.setItem('tab_feed', name);
if (!feedState[name].loaded) loadFeed(name);
}
const _savedFeedTab = localStorage.getItem('tab_feed');
if (_savedFeedTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedFeedTab}"]`);
if (_btn) switchTab(_savedFeedTab, _btn);
}
// ── Feed loading ──
async function loadFeed(tab) {
const state = feedState[tab];
if (state.loading || !state.hasMore) return;
state.loading = true;
state.loaded = true;
try {
const endpoint = tab === 'mine' ? '/feed/mine' : '/feed/public';
const res = await fetch(`${endpoint}?page=${state.page}&size=10`);
if (!res.ok) return;
const data = await res.json();
const feedEl = document.getElementById(tab + 'Feed');
if (state.page === 0 && data.posts.length === 0) {
document.getElementById(tab + 'Empty').style.display = '';
}
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
state.hasMore = data.hasMore;
state.page++;
} finally {
state.loading = false;
}
}
// ── Infinite Scroll ──
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) return;
if (e.target.id === 'mineSentinel') loadFeed('mine');
if (e.target.id === 'publicSentinel') loadFeed('public');
});
}, { threshold: 0.5 });
observer.observe(document.getElementById('mineSentinel'));
observer.observe(document.getElementById('publicSentinel'));
// bilderCarousel und carNav kommen aus shared.js
// ── Render post card ──
function renderPostCard(p, tab) {
const avatarHtml = p.authorPicture
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
: '◉';
const privacyLabel = !p.isPublic ? ' <span style="font-size:0.7rem;color:var(--color-muted);">🔒</span>' : '';
const groupBadge = p.postType === 'GROUP' && p.gruppeId
? `<span class="gruppe-badge" onclick="event.stopPropagation()">👥 <a href="/community/gruppe.html?id=${p.gruppeId}" onclick="event.stopPropagation()">${esc(p.gruppeName)}</a></span>`
: '';
const bildHtml = bilderCarousel(p.bilder, p.postId);
let umfrageHtml = '';
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
umfrageHtml = '<div style="margin-top:0.5rem;">' + p.optionen.map(o => {
const pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
return `<div class="umfrage-option-bar${voted ? ' voted' : ''}" onclick="event.stopPropagation(); votePost('${p.postId}','${o.optionId}','${tab}','${p.postType}')">
<div class="umfrage-bar-fill" style="width:${pct}%"></div>
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
</div>`;
}).join('') + `<div class="umfrage-total">${totalVotes} Stimme${totalVotes !== 1 ? 'n' : ''}</div></div>`;
}
const canDelete = p.postType === 'FEED' && p.authorId === myUserId;
const deleteBtn = canDelete
? `<button class="post-action-btn post-delete" onclick="event.stopPropagation(); deletePost('${p.postId}')">🗑</button>`
: '';
const meldenBtn = p.authorId !== myUserId
? `<button class="post-action-btn" onclick="event.stopPropagation(); openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>`
: '';
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
const hasTarget = !!p.targetUrl;
const cardClick = hasTarget
? `window.location.href='${p.targetUrl}'`
: `openLb('${p.postId}','${p.postType}')`;
const commentBtn = hasTarget ? '' : `
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
</button>`;
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}" style="cursor:pointer;">
<div class="post-header">
<div class="post-avatar">${avatarHtml}</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>${privacyLabel}</div>
<div class="post-meta">${fmtDate(p.createdAt)}${groupBadge}</div>
</div>
${deleteBtn}
</div>
<div class="post-text">${esc(p.text)}</div>
${bildHtml}
${umfrageHtml}
<div class="post-actions">
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="lk-${p.postId}" onclick="event.stopPropagation(); likePost('${p.postId}','${p.postType}')">
♥ <span id="lkc-${p.postId}">${p.likeCount}</span>
</button>
${commentBtn}
${meldenBtn}
</div>
</div>`;
}
// ── 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';
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
addOption(); addOption();
}
}
function addOption() {
const list = document.getElementById('optionList');
const idx = list.children.length;
const row = document.createElement('div');
row.className = 'umfrage-option-row';
row.innerHTML = `<input type="text" placeholder="Option ${idx + 1}" maxlength="100">
<button onclick="this.parentElement.remove()">✕</button>`;
list.appendChild(row);
}
function selectComposeBilder(input) {
[...input.files].forEach(f => { if (f.type.startsWith('image/')) processImageFile(f); });
input.value = '';
}
function processImageFile(file) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
const maxSize = 1024;
const canvas = document.createElement('canvas');
const scale = Math.min(maxSize / img.width, maxSize / 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);
const data = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];
composeBilderArr.push(data);
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();
}
// ── Drag & Drop ──
const compose = document.getElementById('compose');
compose.addEventListener('dragover', e => {
e.preventDefault();
if ([...e.dataTransfer.items].some(i => i.type.startsWith('image/')))
compose.classList.add('drag-over');
});
compose.addEventListener('dragleave', e => {
if (!compose.contains(e.relatedTarget)) compose.classList.remove('drag-over');
});
compose.addEventListener('drop', e => {
e.preventDefault();
compose.classList.remove('drag-over');
[...e.dataTransfer.files]
.filter(f => f.type.startsWith('image/'))
.forEach(f => processImageFile(f));
});
async function submitPost() {
const text = document.getElementById('composeText').value.trim();
if (!text && composeBilderArr.length === 0) return;
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked').value;
const multiChoice = document.getElementById('multiChoice').checked;
const isPublic = document.getElementById('isPublic').checked;
let optionen = [];
if (beitragTyp === 'UMFRAGE') {
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input'))
.map(i => i.value.trim()).filter(v => v);
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
}
const res = await fetch('/feed/posts', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ beitragTyp, text, multiChoice, optionen, bilder: [...composeBilderArr], isPublic })
});
if (!res.ok) return;
const post = await res.json();
// Reset compose
document.getElementById('composeText').value = '';
composeBilderArr = [];
renderComposeThumbs();
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
toggleUmfrage();
document.getElementById('multiChoice').checked = false;
document.getElementById('isPublic').checked = false;
document.getElementById('optionList').innerHTML = '';
// Prepend to mine feed
document.getElementById('mineEmpty').style.display = 'none';
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
if (isPublic) {
document.getElementById('publicEmpty').style.display = 'none';
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
}
}
// ── Like ──
async function likePost(postId, postType) {
let likeEndpoint;
if (postType === 'GROUP') {
const card = document.getElementById('pc-' + postId);
const gruppeId = card?.dataset?.gruppeId;
if (!gruppeId) return;
likeEndpoint = `/gruppen/${gruppeId}/posts/${postId}/like`;
} else {
likeEndpoint = `/feed/posts/${postId}/like`;
}
await fetch(likeEndpoint, { method: 'POST' });
const btn = document.getElementById('lk-' + postId);
const lc = document.getElementById('lkc-' + postId);
const was = btn.classList.contains('active');
btn.classList.toggle('active', !was);
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
}
// ── Vote ──
async function votePost(postId, optionId, tab, postType) {
if (postType === 'GROUP') {
const card = document.getElementById('pc-' + postId);
const gruppeId = card?.dataset?.gruppeId;
if (!gruppeId) return;
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ optionId })
});
} else {
await fetch('/feed/posts/' + postId + '/vote', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ optionId })
});
}
reloadPost(postId, tab);
}
async function reloadPost(postId, tab) {
const state = feedState[tab];
state.page = 0; state.hasMore = true; state.loaded = false;
document.getElementById(tab + 'Feed').innerHTML = '';
document.getElementById(tab + 'Empty').style.display = 'none';
await loadFeed(tab);
}
// ── Delete ──
async function deletePost(postId) {
if (!confirm('Post löschen?')) return;
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
if (res.ok) {
document.getElementById('pc-' + postId)?.remove();
}
}
// ── Lightbox ──
function openLb(postId, postType) {
activeLbPostId = postId;
activeLbPostType = postType;
const card = document.getElementById('pc-' + postId);
if (card) {
const clone = card.cloneNode(true);
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
}
loadLbComments(postId, postType);
document.getElementById('postLightbox').classList.add('open');
}
function openLbWithData(p) {
activeLbPostId = p.postId;
activeLbPostType = p.postType || 'FEED';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = renderPostCard(p, 'mine');
const card = tempDiv.firstElementChild;
if (card) {
card.querySelectorAll('.post-actions').forEach(el => el.remove());
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
}
loadLbComments(p.postId, p.postType || 'FEED');
document.getElementById('postLightbox').classList.add('open');
}
function closeLb() {
document.getElementById('postLightbox').classList.remove('open');
activeLbPostId = null;
activeLbPostType = null;
}
document.getElementById('postLightbox').addEventListener('click', e => {
if (e.target === document.getElementById('postLightbox')) closeLb();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && document.getElementById('postLightbox').classList.contains('open')) closeLb();
});
async function loadLbComments(postId, postType) {
const targetType = postType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${postId}`);
const comments = await res.json();
document.getElementById('lbCommentsList').innerHTML = comments.length === 0
? '<p style="color:var(--color-muted);font-size:0.82rem;margin:0.4rem;">Noch keine Kommentare.</p>'
: comments.map(k => renderKommentarHtml(k, targetType, postId, { myUserId })).join('');
}
async function postLbComment() {
if (!activeLbPostId) return;
const input = document.getElementById('lbCommentInput');
const text = input.value.trim();
if (!text) return;
const targetType = activeLbPostType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
await fetch('/social/kommentare', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetType, targetId: activeLbPostId, text })
});
input.value = '';
await loadLbComments(activeLbPostId, activeLbPostType);
const kcEl = document.getElementById('kc-' + activeLbPostId);
if (kcEl) kcEl.textContent = parseInt(kcEl.textContent) + 1;
}
// renderKommentarHtml und toggleKommentarLike kommen aus shared.js
async function deleteKommentar(kommentarId, targetType, targetId) {
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
await loadLbComments(targetId, activeLbPostType);
}
// toggleEmojiPicker, insertEmoji kommen aus shared.js
// esc, fmtDate kommen aus shared.js
</script>
</body>
</html>