Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
590 lines
33 KiB
JavaScript
590 lines
33 KiB
JavaScript
// ─────────────────────────────────────────────────────────────────────────────
|
||
// shared.js – Gemeinsame Helfer & Komponenten
|
||
// Einbinden: <script src="/js/shared.js"></script>
|
||
// (vor allen Seiten-Skripten, nach CSS-Links)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
// ── CSS-Injection (Comment + Carousel) ────────────────────────────────────────
|
||
(function injectSharedStyles() {
|
||
if (document.getElementById('shared-styles')) return;
|
||
const s = document.createElement('style');
|
||
s.id = 'shared-styles';
|
||
s.textContent = `
|
||
/* ── Karussell ── */
|
||
.post-carousel{position:relative;margin-top:0.5rem}
|
||
.car-slide{display:none}
|
||
.car-slide.active{display:block}
|
||
.car-btn{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);border:none;color:#fff;font-size:2.2rem;width:auto;min-width:2.4rem;height:3.2rem;border-radius:8px;cursor:pointer;z-index:5;display:flex;align-items:center;justify-content:center;padding:0 0.5rem;margin:0;line-height:1}
|
||
.car-prev{left:0.3rem}
|
||
.car-next{right:0.3rem}
|
||
.car-indicator{text-align:center;font-size:0.75rem;color:var(--color-muted);margin-top:0.25rem}
|
||
|
||
/* ── Bilder-Grid (nur im Feed) ── */
|
||
.post-img-grid{display:grid;gap:2px;margin:0.5rem auto 0;border-radius:6px;overflow:hidden;flex-shrink:0}
|
||
.pig-item{position:relative;overflow:hidden;min-width:0;min-height:0}
|
||
.pig-item img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
||
.pig-contain img{object-fit:contain}
|
||
.pig-dark{background:#111}.pig-dark .pig-item{background:#111}
|
||
.pig-more{position:absolute;inset:0;background:rgba(0,0,0,0.55);display:flex;align-items:center;justify-content:center;font-size:1.8rem;font-weight:700;color:#fff;pointer-events:none}
|
||
|
||
/* ── Like / Löschen-Buttons ── */
|
||
.btn-like{background:none;border:1px solid rgba(255,255,255,0.15);border-radius:20px;padding:0.2rem 0.65rem;color:var(--color-muted);font-size:0.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:0.3rem;margin:0;width:auto;transition:border-color 0.15s,color 0.15s}
|
||
.btn-like:hover,.btn-like.liked{border-color:var(--color-primary);color:var(--color-primary)}
|
||
.btn-delete-small{background:none;border:none;color:rgba(200,50,50,0.6);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0}
|
||
.btn-delete-small:hover{color:var(--color-primary)}
|
||
.btn-text{background:none;border:none;color:var(--color-muted);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0;text-decoration:underline;text-decoration-color:rgba(255,255,255,0.2)}
|
||
.btn-text:hover{color:var(--color-text)}
|
||
|
||
/* ── Kommentare ── */
|
||
.comment-item{display:flex;gap:0.5rem;margin-bottom:0.5rem}
|
||
.comment-avatar{width:28px;height:28px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:0.75rem;flex-shrink:0;overflow:hidden}
|
||
.comment-avatar img{width:100%;height:100%;object-fit:cover}
|
||
.comment-body{flex:1;background:rgba(255,255,255,0.04);border-radius:6px;padding:0.5rem 0.65rem}
|
||
.comment-author{font-size:0.8rem;font-weight:600;color:var(--color-text)}
|
||
.comment-date{font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem}
|
||
.comment-text{font-size:0.85rem;color:rgba(255,255,255,0.75);margin-top:0.2rem;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
||
.comment-actions{display:flex;gap:0.4rem;margin-top:0.3rem;align-items:center}
|
||
.replies-section{margin-top:0.5rem;padding-left:0.5rem;border-left:2px solid rgba(255,255,255,0.06)}
|
||
.comment-write{display:flex;gap:0.4rem;margin-top:0.5rem}
|
||
.comment-write input{flex:1;padding:0.4rem 0.75rem;font-size:0.85rem}
|
||
.comment-write button{width:auto;padding:0.4rem 0.75rem;font-size:0.82rem;white-space:nowrap}
|
||
`;
|
||
document.head.appendChild(s);
|
||
})();
|
||
|
||
// ── HTML-Escape ────────────────────────────────────────────────────────────────
|
||
function esc(str) {
|
||
if (!str) return '';
|
||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/\n/g, '<br>');
|
||
}
|
||
|
||
// ── Datum-Format ──────────────────────────────────────────────────────────────
|
||
function fmtDate(iso) {
|
||
if (!iso) return '';
|
||
const d = new Date(iso);
|
||
return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
// ── Emoji-Picker ──────────────────────────────────────────────────────────────
|
||
const EMOJIS = ['😊','😂','❤️','😍','🔥','👍','🥰','😎','🤔','😘','💕','🎉','✨','💋','😈','🫦','🍑','🍆','🔞','🥵','😭','😢','😤','🙄','🤦','🤷','🙏','💪','😏','🤩'];
|
||
let _emojiTarget = null;
|
||
|
||
function toggleEmojiPicker(btn, targetId) {
|
||
_emojiTarget = document.getElementById(targetId);
|
||
let picker = document.getElementById('sharedEmojiPicker');
|
||
if (!picker) {
|
||
picker = document.createElement('div');
|
||
picker.id = 'sharedEmojiPicker';
|
||
picker.style.cssText = 'position:fixed;z-index:9000;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.5rem;display:flex;flex-wrap:wrap;gap:0.2rem;max-width:260px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
|
||
EMOJIS.forEach(em => {
|
||
const b = document.createElement('button');
|
||
b.textContent = em;
|
||
b.style.cssText = 'background:none;border:none;font-size:1.3rem;cursor:pointer;padding:0.2rem;margin:0;width:auto;line-height:1;';
|
||
b.onclick = e => { e.stopPropagation(); insertEmoji(em); };
|
||
picker.appendChild(b);
|
||
});
|
||
document.body.appendChild(picker);
|
||
}
|
||
if (picker.style.display === 'flex') { picker.style.display = 'none'; return; }
|
||
picker.style.display = 'flex';
|
||
requestAnimationFrame(() => {
|
||
const rect = btn.getBoundingClientRect();
|
||
const ph = picker.offsetHeight, pw = picker.offsetWidth;
|
||
let top = rect.top - ph - 8;
|
||
let left = rect.left;
|
||
if (top < 8) top = rect.bottom + 8;
|
||
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
|
||
picker.style.top = top + 'px';
|
||
picker.style.left = left + 'px';
|
||
});
|
||
}
|
||
|
||
function insertEmoji(emoji) {
|
||
if (!_emojiTarget) return;
|
||
const start = _emojiTarget.selectionStart ?? _emojiTarget.value.length;
|
||
const end = _emojiTarget.selectionEnd ?? start;
|
||
_emojiTarget.value = _emojiTarget.value.slice(0, start) + emoji + _emojiTarget.value.slice(end);
|
||
_emojiTarget.selectionStart = _emojiTarget.selectionEnd = start + emoji.length;
|
||
_emojiTarget.focus();
|
||
}
|
||
|
||
document.addEventListener('click', e => {
|
||
const picker = document.getElementById('sharedEmojiPicker');
|
||
if (picker && picker.style.display === 'flex'
|
||
&& !picker.contains(e.target)
|
||
&& !e.target.closest('[onclick*="toggleEmojiPicker"]')) {
|
||
picker.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// ── Bild-Karussell (Lightbox/Detail-Ansicht) ─────────────────────────────────
|
||
function bilderCarousel(bilder, initialIdx = 0) {
|
||
if (!bilder || bilder.length === 0) return '';
|
||
const idx = Math.max(0, Math.min(initialIdx, bilder.length - 1));
|
||
const slides = bilder.map((b, i) =>
|
||
`<div class="car-slide${i === idx ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
|
||
).join('');
|
||
const nav = bilder.length > 1
|
||
? `<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">‹</button>
|
||
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">›</button>
|
||
<div class="car-indicator"><span class="car-cur">${idx + 1}</span> / ${bilder.length}</div>`
|
||
: '';
|
||
return `<div class="post-carousel">${slides}${nav}</div>`;
|
||
}
|
||
|
||
/** Richtet die Lightbox-Inhalte ein:
|
||
* – View-Area bekommt Flex-Layout damit das Bild den Platz füllt
|
||
* – Bild-Container bekommt lb-ic-Klasse + Carousel (für alle Bildanzahlen)
|
||
* – Post-Text bekommt scrollbare Höhenbegrenzung
|
||
*/
|
||
function _lbSetupContent(postId, prefix, bilder) {
|
||
const initialIdx = window.__pigNextIdx != null ? window.__pigNextIdx : 0;
|
||
window.__pigNextIdx = null;
|
||
const body = document.getElementById('lbPostBody');
|
||
const va = body.querySelector(`#${prefix}va-${postId}`);
|
||
if (va) va.classList.add('lb-va');
|
||
const hasImages = bilder && bilder.length > 0;
|
||
const pbi = body.querySelector(`#${prefix}bi-${postId}`);
|
||
if (pbi) {
|
||
if (hasImages) {
|
||
pbi.classList.add('lb-ic');
|
||
pbi.innerHTML = bilderCarousel(bilder, initialIdx);
|
||
} else {
|
||
pbi.style.display = 'none';
|
||
}
|
||
}
|
||
if (va) va.querySelector('.post-text')?.classList.add('lb-text');
|
||
|
||
// Text-only layout: kein Bild → Kommentare unterhalb, volle Breite
|
||
const layout = document.querySelector('#postLightbox .lb-layout');
|
||
if (layout) layout.classList.toggle('lb-text-only', !hasImages);
|
||
}
|
||
|
||
// ── Bilder-Grid (Feed-Karten, orientierungsabhängig) ──────────────────────────
|
||
const POST_IMG_SIZE = 500; // px — Breite und Höhe des Bild-Containers
|
||
let _pigSeq = 0;
|
||
const _pigStore = new Map(); // id → bilder[]
|
||
|
||
function bilderGrid(bilder) {
|
||
if (!bilder || bilder.length === 0) return '';
|
||
const S = POST_IMG_SIZE;
|
||
const id = 'pig-' + (++_pigSeq);
|
||
|
||
if (bilder.length === 1) {
|
||
// Längere Seite = S, kürzere letterboxed
|
||
return `<div class="post-img-grid pig-contain" id="${id}" style="width:${S}px;height:${S}px;grid-template-columns:1fr;grid-template-rows:1fr;">
|
||
<div class="pig-item pig-contain"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>
|
||
</div>`;
|
||
}
|
||
|
||
// 2+ Bilder: Orientierung des ersten Bilds bestimmt das Layout → deferred init
|
||
_pigStore.set(id, bilder);
|
||
return `<div class="post-img-grid" id="${id}" style="width:${S}px;height:${S}px;">` +
|
||
`<img src="data:image/jpeg;base64,${bilder[0]}" style="display:none;position:absolute;" alt="" onload="_pigInit('${id}',this)">` +
|
||
`</div>`;
|
||
}
|
||
|
||
function _pigInit(id, probe) {
|
||
const bilder = _pigStore.get(id);
|
||
if (!bilder) return;
|
||
_pigStore.delete(id);
|
||
const grid = document.getElementById(id);
|
||
if (!grid) return;
|
||
const land = probe.naturalWidth >= probe.naturalHeight; // quer = landscape
|
||
const n = bilder.length;
|
||
const extra = n - 3;
|
||
const moreHtml = extra > 0 ? `<div class="pig-more">+${extra}</div>` : '';
|
||
|
||
grid.innerHTML = ''; // Probe-Bild entfernen
|
||
|
||
// Fraktionale Werte → Browser berücksichtigt gap automatisch, Trennstrich immer genau in der Mitte
|
||
if (n === 2) {
|
||
if (land) {
|
||
// Quer-erstes Bild → beide übereinander (Trennstrich horizontal in der Mitte)
|
||
grid.style.gridTemplateColumns = '1fr';
|
||
grid.style.gridTemplateRows = '1fr 1fr';
|
||
} else {
|
||
// Hochkant-erstes Bild → beide nebeneinander (Trennstrich vertikal in der Mitte)
|
||
grid.style.gridTemplateColumns = '1fr 1fr';
|
||
grid.style.gridTemplateRows = '1fr';
|
||
}
|
||
grid.insertAdjacentHTML('beforeend',
|
||
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>`);
|
||
} else {
|
||
// 3+ Bilder (ab 4 gleiche Darstellung wie bei 3, +N-Overlay auf Zelle 3)
|
||
// Dunkler Hintergrund nur hier, damit der Spalt zwischen den Zellen nicht auffällt
|
||
grid.classList.add('pig-dark');
|
||
grid.style.gridTemplateColumns = '1fr 1fr';
|
||
grid.style.gridTemplateRows = '1fr 1fr';
|
||
if (land) {
|
||
// Quer → Bild 1 oben (volle Breite), Bilder 2+3 nebeneinander unten
|
||
grid.insertAdjacentHTML('beforeend',
|
||
`<div class="pig-item" style="grid-column:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
|
||
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
|
||
} else {
|
||
// Hochkant → Bild 1 links (volle Höhe), Bilder 2+3 übereinander rechts
|
||
grid.insertAdjacentHTML('beforeend',
|
||
`<div class="pig-item" style="grid-row:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
|
||
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
|
||
}
|
||
}
|
||
|
||
// Click-Listener: angeklicktes Bild als Startindex für die Lightbox merken
|
||
grid.querySelectorAll('.pig-item').forEach((item, idx) => {
|
||
item.style.cursor = 'pointer';
|
||
item.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
window.__pigNextIdx = idx;
|
||
item.closest('.post-card')?.click();
|
||
});
|
||
});
|
||
}
|
||
|
||
function carNav(btn, dir) {
|
||
const car = btn.closest('.post-carousel');
|
||
const slides = Array.from(car.querySelectorAll('.car-slide'));
|
||
const cur = slides.findIndex(s => s.classList.contains('active'));
|
||
slides[cur].classList.remove('active');
|
||
const next = (cur + dir + slides.length) % slides.length;
|
||
slides[next].classList.add('active');
|
||
const ind = car.querySelector('.car-cur');
|
||
if (ind) ind.textContent = next + 1;
|
||
}
|
||
|
||
// ── Kommentar-Rendering ───────────────────────────────────────────────────────
|
||
// opts: { myUserId, showReplies }
|
||
// Seite muss definieren: deleteKommentar(kommentarId, targetType, targetId)
|
||
function renderKommentarHtml(k, targetType, targetId, opts) {
|
||
const { myUserId = null, showReplies = false } = opts || {};
|
||
const avatarHtml = k.authorPicture
|
||
? `<img src="data:image/png;base64,${k.authorPicture}" alt="">`
|
||
: '◉';
|
||
const canDelete = k.authorId === myUserId;
|
||
const replyLabel = k.replyCount > 0 ? `Antworten (${k.replyCount})` : 'Antworten';
|
||
return `<div class="comment-item" id="kom-${k.kommentarId}">
|
||
<div class="comment-avatar">${avatarHtml}</div>
|
||
<div class="comment-body">
|
||
<span class="comment-author">${esc(k.authorName)}</span>
|
||
<span class="comment-date">${fmtDate(k.createdAt)}</span>
|
||
<div class="comment-text">${esc(k.text)}</div>
|
||
<div class="comment-actions">
|
||
<button class="btn-like${k.likedByMe ? ' liked' : ''}" id="lk-kom-${k.kommentarId}"
|
||
onclick="toggleKommentarLike('${k.kommentarId}')">♥ <span id="lkc-kom-${k.kommentarId}">${k.likeCount}</span></button>
|
||
${showReplies ? `<button class="btn-text" onclick="toggleReplies('${k.kommentarId}')">${replyLabel}</button>` : ''}
|
||
${canDelete ? `<button class="btn-delete-small" onclick="deleteKommentar('${k.kommentarId}','${targetType}','${targetId}')">✕</button>` : ''}
|
||
</div>
|
||
${showReplies ? `<div class="replies-section" id="replies-${k.kommentarId}" style="display:none;"></div>` : ''}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderReplyHtml(r, parentId) {
|
||
const avatarHtml = r.authorPicture
|
||
? `<img src="data:image/png;base64,${r.authorPicture}" alt="">`
|
||
: '◉';
|
||
const canDelete = typeof window.myUserId !== 'undefined' && r.authorId === window.myUserId;
|
||
return `<div class="comment-item" id="kom-${r.kommentarId}" style="margin-bottom:0.35rem;">
|
||
<div class="comment-avatar" style="width:22px;height:22px;font-size:0.75rem;">${avatarHtml}</div>
|
||
<div class="comment-body" style="padding:0.35rem 0.55rem;">
|
||
<span class="comment-author">${esc(r.authorName)}</span>
|
||
<span class="comment-date">${fmtDate(r.createdAt)}</span>
|
||
<div class="comment-text">${esc(r.text)}</div>
|
||
<div class="comment-actions">
|
||
<button class="btn-like${r.likedByMe ? ' liked' : ''}" id="lk-kom-${r.kommentarId}"
|
||
onclick="toggleKommentarLike('${r.kommentarId}')">♥ <span id="lkc-kom-${r.kommentarId}">${r.likeCount}</span></button>
|
||
${canDelete ? `<button class="btn-delete-small" onclick="deleteReply('${r.kommentarId}','${parentId}')">✕</button>` : ''}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
async function toggleKommentarLike(kommentarId) {
|
||
await fetch('/social/kommentare/' + kommentarId + '/like', { method: 'POST' });
|
||
const btn = document.getElementById('lk-kom-' + kommentarId);
|
||
const lc = document.getElementById('lkc-kom-' + kommentarId);
|
||
if (!btn || !lc) return;
|
||
const was = btn.classList.contains('liked');
|
||
btn.classList.toggle('liked', !was);
|
||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||
}
|
||
|
||
async function toggleReplies(kommentarId) {
|
||
const section = document.getElementById('replies-' + kommentarId);
|
||
if (section.style.display === 'none') {
|
||
section.style.display = '';
|
||
await loadReplies(kommentarId);
|
||
} else {
|
||
section.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function loadReplies(kommentarId) {
|
||
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
|
||
const replies = await res.json();
|
||
const section = document.getElementById('replies-' + kommentarId);
|
||
replies.reverse();
|
||
section.innerHTML = (replies.length === 0
|
||
? '<p style="color:var(--color-muted);font-size:0.78rem;margin-bottom:0.35rem;">Noch keine Antworten.</p>'
|
||
: replies.map(r => renderReplyHtml(r, kommentarId)).join(''))
|
||
+ `<div class="comment-write">
|
||
<input type="text" id="ri-${kommentarId}" placeholder="Antwort schreiben…" maxlength="500"
|
||
onkeydown="if(event.key==='Enter') postReply('${kommentarId}')">
|
||
<button onclick="postReply('${kommentarId}')">Senden</button>
|
||
</div>`;
|
||
}
|
||
|
||
async function postReply(kommentarId) {
|
||
const input = document.getElementById('ri-' + kommentarId);
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
await fetch('/social/kommentare', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ targetType: 'KOMMENTAR', targetId: kommentarId, text })
|
||
});
|
||
input.value = '';
|
||
await loadReplies(kommentarId);
|
||
}
|
||
|
||
async function deleteReply(replyId, parentId) {
|
||
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
|
||
await loadReplies(parentId);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Gemeinsame Lightbox (standardisierte IDs: #postLightbox, #lbPostBody,
|
||
// #lbCommentsList, #lbCommentInput – auf allen Feed-Seiten gleich)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
let _lbMyUserId = null;
|
||
let _lbPostId = null;
|
||
let _lbPostType = null;
|
||
|
||
/** Muss nach dem Login mit der eigenen userId aufgerufen werden. */
|
||
function initLb(userId) { _lbMyUserId = userId; }
|
||
|
||
function closeLb() {
|
||
document.getElementById('postLightbox')?.classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
_lbPostId = null; _lbPostType = null;
|
||
}
|
||
|
||
// Escape schließt, Pfeiltasten navigieren das Karussell
|
||
document.addEventListener('keydown', e => {
|
||
const lb = document.getElementById('postLightbox');
|
||
if (!lb?.classList.contains('open')) return;
|
||
if (e.key === 'Escape') { closeLb(); return; }
|
||
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
||
const car = lb.querySelector('.post-carousel');
|
||
if (!car) return;
|
||
const slides = Array.from(car.querySelectorAll('.car-slide'));
|
||
if (slides.length <= 1) return;
|
||
const cur = slides.findIndex(s => s.classList.contains('active'));
|
||
const next = (cur + (e.key === 'ArrowLeft' ? -1 : 1) + slides.length) % slides.length;
|
||
slides[cur].classList.remove('active');
|
||
slides[next].classList.add('active');
|
||
const ind = car.querySelector('.car-cur');
|
||
if (ind) ind.textContent = next + 1;
|
||
});
|
||
|
||
async function loadLbComments(postId, postType) {
|
||
_lbPostId = postId; _lbPostType = postType;
|
||
const targetType = postType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
|
||
try {
|
||
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${postId}`);
|
||
const comments = await res.json();
|
||
comments.reverse();
|
||
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: _lbMyUserId })).join('');
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function postLbComment() {
|
||
if (!_lbPostId) return;
|
||
const input = document.getElementById('lbCommentInput');
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
const targetType = _lbPostType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
|
||
await fetch('/social/kommentare', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ targetType, targetId: _lbPostId, text })
|
||
});
|
||
input.value = '';
|
||
await loadLbComments(_lbPostId, _lbPostType);
|
||
const kcEl = document.getElementById('kc-' + _lbPostId) || document.getElementById('hkc-' + _lbPostId);
|
||
if (kcEl) kcEl.textContent = parseInt(kcEl.textContent || '0') + 1;
|
||
}
|
||
|
||
async function deleteKommentar(kommentarId, targetType, targetId) {
|
||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||
await loadLbComments(targetId, _lbPostType);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Bild-Verarbeitung (Compose & Edit)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/** Liest eine Bilddatei, skaliert auf max. 1024px, komprimiert auf JPEG 85 %
|
||
* und hängt den Base64-String an bilderArr an, dann ruft renderFn() auf. */
|
||
function processImageFile(file, bilderArr, renderFn) {
|
||
if (!file || !file.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);
|
||
bilderArr.push(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
|
||
renderFn();
|
||
};
|
||
img.src = e.target.result;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
/** Rendert Vorschau-Thumbnails in containerId; rmCallback(idx) wird beim ✕ aufgerufen. */
|
||
function renderBilderThumbs(bilderArr, containerId, rmCallback) {
|
||
const c = document.getElementById(containerId);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
bilderArr.forEach((b, i) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'compose-thumb';
|
||
const img = document.createElement('img');
|
||
img.src = `data:image/jpeg;base64,${b}`;
|
||
const btn = document.createElement('button');
|
||
btn.className = 'compose-thumb-remove';
|
||
btn.textContent = '✕'; btn.title = 'Entfernen';
|
||
btn.onclick = ev => { ev.stopPropagation(); rmCallback(i); };
|
||
div.appendChild(img); div.appendChild(btn);
|
||
c.appendChild(div);
|
||
});
|
||
c.style.display = bilderArr.length > 0 ? 'flex' : 'none';
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Post-Bearbeitung (gemeinsam, über Präfix parametrisiert)
|
||
// Präfixe: 'p' (feed), 'hp' (userhome), 'gp' (gruppe)
|
||
// IDs-Muster: ${prefix}va-, ${prefix}bi-, ${prefix}ea-, ${prefix}um-,
|
||
// ${prefix}m-, ${prefix}et-, ${prefix}et-tb-, ${prefix}eo-, ${prefix}mc-
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* cfg: { postId, prefix, data, editBilderMap,
|
||
* saveFn, cancelFn, addImgFn, addOptionFn, rmImgFn }
|
||
* Alle Fn-Namen sind Strings (globale Funktionsnamen auf der jeweiligen Seite).
|
||
*/
|
||
function startPostEdit(cfg) {
|
||
const { postId, prefix, data, editBilderMap, saveFn, cancelFn, addImgFn, addOptionFn, rmImgFn } = cfg;
|
||
editBilderMap.set(postId, [...(data.bilder || [])]);
|
||
document.getElementById(`${prefix}va-${postId}`).style.display = 'none';
|
||
document.getElementById(`${prefix}um-${postId}`).style.display = 'none';
|
||
|
||
const isUmfrage = data.beitragTyp === 'UMFRAGE';
|
||
const optionenHtml = isUmfrage
|
||
? `<div id="${prefix}eo-${postId}" style="margin-top:0.5rem;">${(data.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();${addOptionFn}('${postId}')" 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="${prefix}mc-${postId}" ${data.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();${addImgFn}(this,'${postId}')">
|
||
</label>
|
||
<button onclick="event.stopPropagation();${saveFn}('${postId}')" style="width:auto;margin:0;">Speichern</button>
|
||
<button onclick="event.stopPropagation();${cancelFn}('${postId}')" style="width:auto;margin:0;background:var(--color-secondary);color:var(--color-text);">Abbrechen</button>
|
||
</div>`;
|
||
|
||
const ea = document.getElementById(`${prefix}ea-${postId}`);
|
||
ea.style.display = ''; ea.onclick = e => e.stopPropagation();
|
||
ea.innerHTML = `<textarea id="${prefix}et-${postId}" 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(data.text || '')}</textarea>
|
||
<div class="compose-thumbs" id="${prefix}et-tb-${postId}" style="margin-top:0.4rem;"></div>
|
||
${optionenHtml}${actionRow}`;
|
||
_renderEditThumbs(editBilderMap, postId, prefix, rmImgFn);
|
||
}
|
||
|
||
function cancelPostEdit(postId, prefix, editBilderMap) {
|
||
document.getElementById(`${prefix}va-${postId}`).style.display = '';
|
||
document.getElementById(`${prefix}um-${postId}`).style.display = '';
|
||
document.getElementById(`${prefix}ea-${postId}`).style.display = 'none';
|
||
editBilderMap.delete(postId);
|
||
}
|
||
|
||
function _renderEditThumbs(editBilderMap, postId, prefix, rmFn) {
|
||
const bilder = editBilderMap.get(postId) || [];
|
||
const c = document.getElementById(`${prefix}et-tb-${postId}`);
|
||
if (!c) return;
|
||
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();${rmFn}('${postId}',${i})">✕</button></div>`
|
||
).join('');
|
||
c.style.display = bilder.length > 0 ? 'flex' : 'none';
|
||
}
|
||
|
||
function editAddOptionRow(containerId) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
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'));
|
||
}
|
||
|
||
/**
|
||
* cfg: { postId, prefix, endpoint, isUmfrage, editBilderMap, onSuccess }
|
||
* onSuccess(updated) – Seite aktualisiert Cache und DOM.
|
||
*/
|
||
async function savePostEdit(cfg) {
|
||
const { postId, prefix, endpoint, isUmfrage, editBilderMap, onSuccess } = cfg;
|
||
const text = document.getElementById(`${prefix}et-${postId}`).value.trim();
|
||
if (!text) return;
|
||
const bilder = editBilderMap.get(postId) || [];
|
||
const optionen = isUmfrage
|
||
? Array.from(document.querySelectorAll(`#${prefix}eo-${postId} input[type=text]`))
|
||
.map(inp => ({ optionId: inp.dataset.optionId || null, text: inp.value.trim() }))
|
||
.filter(o => o.text)
|
||
: null;
|
||
const multiChoice = isUmfrage ? (document.getElementById(`${prefix}mc-${postId}`)?.checked ?? false) : null;
|
||
const res = await fetch(endpoint, {
|
||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text, bilder, optionen, multiChoice })
|
||
});
|
||
if (!res.ok) return;
|
||
editBilderMap.delete(postId);
|
||
onSuccess(await res.json());
|
||
}
|
||
|
||
/** Aktualisiert Text, Bilder, Edit-Area, Umfrage und (bearbeitet)-Label im DOM. */
|
||
function applyPostEditDom(postId, prefix, updated, umfrageHtml) {
|
||
document.getElementById(`${prefix}va-${postId}`).querySelector('.post-text').innerHTML = renderTextWithHashtags(updated.text);
|
||
document.getElementById(`${prefix}bi-${postId}`).innerHTML = bilderGrid(updated.bilder);
|
||
document.getElementById(`${prefix}va-${postId}`).style.display = '';
|
||
document.getElementById(`${prefix}ea-${postId}`).style.display = 'none';
|
||
const pum = document.getElementById(`${prefix}um-${postId}`);
|
||
if (pum) { pum.innerHTML = umfrageHtml || ''; pum.style.display = ''; }
|
||
const meta = document.getElementById(`${prefix}m-${postId}`);
|
||
if (meta && !meta.querySelector('.edited-label')) {
|
||
meta.insertAdjacentHTML('beforeend', ' <span class="edited-label" style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>');
|
||
}
|
||
}
|