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
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
<title>Location – 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>
|
||||
.back-link { display:inline-flex; align-items:center; gap:0.35rem; color:var(--color-muted); font-size:0.88rem; text-decoration:none; margin-bottom:1rem; }
|
||||
.back-link:hover { color:var(--color-primary); }
|
||||
@@ -216,6 +217,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Post-Lightbox ──────────────────────────────────────────────────────── -->
|
||||
<div class="lightbox" id="postLightbox">
|
||||
<div class="lb-layout">
|
||||
<div class="lb-post-side">
|
||||
<div id="lbPostBody"></div>
|
||||
</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>
|
||||
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Galerie Lightbox ───────────────────────────────────────────────────── -->
|
||||
<div class="lb" id="lightbox" onclick="closeLightbox()">
|
||||
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
||||
@@ -226,6 +249,8 @@
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/hashtag.js"></script>
|
||||
<script src="/js/shared.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const locationId = params.get('id');
|
||||
@@ -303,6 +328,15 @@ async function loadPage() {
|
||||
isFollowing = !!locDetail.following;
|
||||
|
||||
renderPage();
|
||||
initLb(myUserId);
|
||||
const _feedTa = document.getElementById('locFeedText');
|
||||
if (_feedTa) attachHashtagAutocomplete(_feedTa);
|
||||
const _compose = document.getElementById('locFeedCompose');
|
||||
if (_compose) {
|
||||
_compose.addEventListener('dragover', e => { e.preventDefault(); _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, locFeedImages, renderLocFeedThumbs)); });
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
||||
@@ -315,6 +349,8 @@ async function loadPage() {
|
||||
} else {
|
||||
loadEvents();
|
||||
}
|
||||
await loadLocFeed();
|
||||
initLocFeedObserver();
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
@@ -372,6 +408,37 @@ function renderPage() {
|
||||
</div>
|
||||
<div class="gallery-grid" id="galleryGrid">${galleryHtml}</div>`;
|
||||
|
||||
const feedComposeHtml = isAdmin ? `
|
||||
<div class="post-compose" id="locFeedCompose">
|
||||
<textarea id="locFeedText" placeholder="Was möchtest du teilen?" rows="3"
|
||||
oninput="locFeedTextInput(this)" onpaste="locFeedOnPaste(event)"></textarea>
|
||||
<div class="compose-thumbs" id="locFeedThumbs"></div>
|
||||
<div class="umfrage-options" id="locUmfrageOptions" style="display:none;">
|
||||
<div id="locOptionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="addLocOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle"><input type="checkbox" id="locMultiChoice"> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-left:auto;">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'locFeedText')" title="Emoji">😊</button>
|
||||
<label class="compose-action-btn" title="Fotos">📷
|
||||
<input type="file" id="locFeedBildFile" accept="image/*" multiple style="display:none;"
|
||||
onchange="locFeedAddImages(this)">
|
||||
</label>
|
||||
<button type="button" id="locUmfrageBtn" class="compose-action-btn" onclick="toggleLocUmfrage(this)" title="Umfrage">📊</button>
|
||||
<button onclick="submitLocFeedPost()" style="width:auto;margin:0;">Veröffentlichen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
const feedSection = `
|
||||
<div class="section-title" style="margin-top:1.5rem;">Beiträge</div>
|
||||
${feedComposeHtml}
|
||||
<div id="locFeedList"></div>
|
||||
<div class="sentinel" id="locFeedSentinel"></div>`;
|
||||
|
||||
const eventsSection = `
|
||||
<div class="section-title">
|
||||
Veranstaltungen
|
||||
@@ -396,6 +463,7 @@ function renderPage() {
|
||||
${locHeaderHtml}
|
||||
${hoursHtml}
|
||||
${gallerySection}
|
||||
${feedSection}
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-admins">
|
||||
@@ -449,6 +517,7 @@ function renderPage() {
|
||||
${locHeaderHtml}
|
||||
${hoursHtml}
|
||||
${gallerySection}
|
||||
${feedSection}
|
||||
${eventsSection}`;
|
||||
}
|
||||
}
|
||||
@@ -1135,6 +1204,304 @@ async function sendInboxReply() {
|
||||
} catch { showAlert('Fehler beim Senden.'); input.value = text; }
|
||||
}
|
||||
|
||||
// ── Location Feed ─────────────────────────────────────────────────────────────
|
||||
const locPostCache = {};
|
||||
const locPostBilder = new Map();
|
||||
const locEditBilder = new Map();
|
||||
let locFeedPage = 0;
|
||||
let locFeedHasMore = true;
|
||||
let locFeedLoading = false;
|
||||
let locFeedImages = [];
|
||||
|
||||
function locFeedTextInput(ta) {
|
||||
ta.style.height = 'auto';
|
||||
ta.style.height = Math.min(ta.scrollHeight, 220) + 'px';
|
||||
}
|
||||
|
||||
function locFeedOnPaste(e) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
for (const item of items) {
|
||||
if (item.type.startsWith('image/')) {
|
||||
e.preventDefault();
|
||||
processImageFile(item.getAsFile(), locFeedImages, renderLocFeedThumbs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function locFeedAddImages(input) {
|
||||
const files = Array.from(input.files || []);
|
||||
files.forEach(f => processImageFile(f, locFeedImages, renderLocFeedThumbs));
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function renderLocFeedThumbs() {
|
||||
const wrap = document.getElementById('locFeedThumbs');
|
||||
if (!wrap) return;
|
||||
wrap.style.display = locFeedImages.length ? 'flex' : 'none';
|
||||
wrap.innerHTML = locFeedImages.map((b64, i) => `
|
||||
<div class="compose-thumb">
|
||||
<img src="data:image/jpeg;base64,${b64}" alt="">
|
||||
<button class="compose-thumb-remove" onclick="locFeedRemoveImg(${i})">✕</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function locFeedRemoveImg(idx) {
|
||||
locFeedImages.splice(idx, 1);
|
||||
renderLocFeedThumbs();
|
||||
}
|
||||
|
||||
function toggleLocUmfrage(btn) {
|
||||
const opt = document.getElementById('locUmfrageOptions');
|
||||
if (!opt) return;
|
||||
const showing = opt.style.display !== 'none';
|
||||
opt.style.display = showing ? 'none' : '';
|
||||
if (btn) btn.classList.toggle('active', !showing);
|
||||
if (!showing && document.getElementById('locOptionList').children.length === 0) { addLocOption(); addLocOption(); }
|
||||
}
|
||||
|
||||
function resetLocUmfrage() {
|
||||
const opt = document.getElementById('locUmfrageOptions');
|
||||
if (opt) opt.style.display = 'none';
|
||||
const list = document.getElementById('locOptionList');
|
||||
if (list) list.innerHTML = '';
|
||||
const btn = document.getElementById('locUmfrageBtn');
|
||||
if (btn) btn.classList.remove('active');
|
||||
}
|
||||
|
||||
function addLocOption() {
|
||||
const list = document.getElementById('locOptionList');
|
||||
if (!list) return;
|
||||
const row = document.createElement('div'); row.className = 'umfrage-option-row';
|
||||
row.innerHTML = `<input type="text" placeholder="Option ${list.children.length + 1}" maxlength="100">
|
||||
<button onclick="this.parentElement.remove()">✕</button>`;
|
||||
list.appendChild(row);
|
||||
}
|
||||
|
||||
async function submitLocFeedPost() {
|
||||
const text = (document.getElementById('locFeedText')?.value || '').trim();
|
||||
const hasUmfrage = document.getElementById('locUmfrageOptions')?.style.display !== 'none';
|
||||
if (!text && locFeedImages.length === 0) return;
|
||||
const multiChoice = document.getElementById('locMultiChoice')?.checked || false;
|
||||
let optionen = [];
|
||||
if (hasUmfrage) {
|
||||
optionen = Array.from(document.getElementById('locOptionList')?.querySelectorAll('input') || [])
|
||||
.map(i => i.value.trim()).filter(v => v);
|
||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||
}
|
||||
const body = { beitragTyp: hasUmfrage ? 'UMFRAGE' : 'TEXT', text, multiChoice, optionen, bilder: locFeedImages, isPublic: true };
|
||||
try {
|
||||
const res = await fetch(`/feed/location/${locationId}/posts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const post = await res.json();
|
||||
document.getElementById('locFeedText').value = '';
|
||||
locFeedImages = [];
|
||||
renderLocFeedThumbs();
|
||||
resetLocUmfrage();
|
||||
if (document.getElementById('locMultiChoice')) document.getElementById('locMultiChoice').checked = false;
|
||||
const list = document.getElementById('locFeedList');
|
||||
if (list) {
|
||||
if (list.querySelector('.empty-hint')) list.innerHTML = '';
|
||||
list.insertAdjacentHTML('afterbegin', renderLocPost(post));
|
||||
}
|
||||
} catch { showAlert('Fehler beim Posten.'); }
|
||||
}
|
||||
|
||||
async function loadLocFeed() {
|
||||
if (!locationId || locFeedLoading || !locFeedHasMore) return;
|
||||
locFeedLoading = true;
|
||||
try {
|
||||
const res = await fetch(`/feed/location/${locationId}?page=${locFeedPage}&size=10`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
locFeedHasMore = data.hasMore;
|
||||
locFeedPage++;
|
||||
const list = document.getElementById('locFeedList');
|
||||
if (!list) return;
|
||||
if (locFeedPage === 1 && (!data.posts || data.posts.length === 0)) {
|
||||
list.innerHTML = '<p class="empty-hint">Noch keine Beiträge.</p>';
|
||||
return;
|
||||
}
|
||||
if (list.querySelector('.empty-hint')) list.innerHTML = '';
|
||||
data.posts.forEach(p => {
|
||||
list.insertAdjacentHTML('beforeend', renderLocPost(p));
|
||||
});
|
||||
} finally {
|
||||
locFeedLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLocPost(p) {
|
||||
locPostCache[p.postId] = p;
|
||||
locPostBilder.set(p.postId, p.bilder || []);
|
||||
|
||||
const avHtml = p.authorPicture
|
||||
? `<img src="data:image/jpeg;base64,${p.authorPicture}" alt="">`
|
||||
: '📍';
|
||||
const dateStr = new Date(p.createdAt).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' })
|
||||
+ ' ' + new Date(p.createdAt).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' });
|
||||
const editedHtml = p.editedAt ? ' <span style="font-size:0.7rem;color:var(--color-muted);">(bearbeitet)</span>' : '';
|
||||
|
||||
const bildHtml = bilderGrid(p.bilder);
|
||||
const onClickAttr = p.targetUrl
|
||||
? ` onclick="window.location.href='${p.targetUrl}'"`
|
||||
: ` onclick="openLpLb('${p.postId}')"`;
|
||||
const clickableClass = ' clickable';
|
||||
|
||||
const adminBtns = isAdmin ? `
|
||||
<div style="margin-left:auto;display:flex;gap:0.3rem;">
|
||||
<button class="post-action-btn" onclick="event.stopPropagation();startLocEdit('${p.postId}')" title="Bearbeiten">✏</button>
|
||||
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteLocPost('${p.postId}')" title="Löschen">🗑</button>
|
||||
</div>` : '';
|
||||
|
||||
return `<div class="post-card${clickableClass}" id="lp-${p.postId}"${onClickAttr}>
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avHtml}</div>
|
||||
<div>
|
||||
<div class="post-author">${escHtml(p.authorName || p.locationName || '')}</div>
|
||||
<div class="post-meta">${dateStr}${editedHtml}</div>
|
||||
</div>
|
||||
${adminBtns}
|
||||
</div>
|
||||
<div id="lpva-${p.postId}">
|
||||
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||
<div id="lpbi-${p.postId}">${bildHtml}</div>
|
||||
</div>
|
||||
<div id="lpea-${p.postId}" style="display:none;"></div>
|
||||
<div class="post-actions">
|
||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" onclick="event.stopPropagation();toggleLpLike('${p.postId}',this)">
|
||||
♥ <span id="lplic-${p.postId}">${p.likeCount}</span>
|
||||
</button>
|
||||
<button class="post-action-btn" onclick="event.stopPropagation();openLpLb('${p.postId}')">
|
||||
💬 ${p.kommentarCount}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function toggleLpLike(postId, btn) {
|
||||
const res = await fetch(`/feed/posts/${postId}/like`, { method: 'POST' });
|
||||
if (!res.ok) return;
|
||||
btn.classList.toggle('active');
|
||||
const span = document.getElementById('lplic-' + postId);
|
||||
if (span) span.textContent = parseInt(span.textContent) + (btn.classList.contains('active') ? 1 : -1);
|
||||
}
|
||||
|
||||
function openLpLb(postId) {
|
||||
const p = locPostCache[postId];
|
||||
if (!p) return;
|
||||
const avHtml = p.authorPicture
|
||||
? `<img src="data:image/jpeg;base64,${p.authorPicture}" alt="">`
|
||||
: '📍';
|
||||
const dateStr = new Date(p.createdAt).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' })
|
||||
+ ' ' + new Date(p.createdAt).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' });
|
||||
const editedHtml = p.editedAt ? ' <span style="font-size:0.7rem;color:var(--color-muted);">(bearbeitet)</span>' : '';
|
||||
|
||||
document.getElementById('lbPostBody').innerHTML = `
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avHtml}</div>
|
||||
<div>
|
||||
<div class="post-author">${escHtml(p.authorName || p.locationName || '')}</div>
|
||||
<div class="post-meta">${dateStr}${editedHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lpva-${p.postId}">
|
||||
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||
<div id="lpbi-${p.postId}"></div>
|
||||
</div>
|
||||
<div class="post-actions" style="margin-top:0.75rem;">
|
||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" onclick="toggleLpLike('${p.postId}',this)">
|
||||
♥ <span id="lplic-${p.postId}">${p.likeCount}</span>
|
||||
</button>
|
||||
</div>`;
|
||||
_lbSetupContent(p.postId, 'lp', locPostBilder.get(p.postId) || []);
|
||||
document.getElementById('postLightbox').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
loadLbComments(p.postId, 'FEED');
|
||||
}
|
||||
|
||||
async function deleteLocPost(postId) {
|
||||
if (!confirm('Beitrag löschen?')) return;
|
||||
const res = await fetch(`/feed/posts/${postId}`, { method: 'DELETE' });
|
||||
if (!res.ok) { showAlert('Fehler beim Löschen.'); return; }
|
||||
document.getElementById('lp-' + postId)?.remove();
|
||||
delete locPostCache[postId];
|
||||
}
|
||||
|
||||
function startLocEdit(postId) {
|
||||
const p = locPostCache[postId];
|
||||
if (!p) return;
|
||||
const bilder = (p.bilder || []).slice();
|
||||
locEditBilder.set(postId, bilder);
|
||||
startPostEdit({
|
||||
postId,
|
||||
prefix: 'lp',
|
||||
text: p.text || '',
|
||||
bilder,
|
||||
editBilderMap: locEditBilder,
|
||||
beitragTyp: p.beitragTyp,
|
||||
optionen: p.optionen || [],
|
||||
multiChoice: p.multiChoice,
|
||||
rmImgFn: `locEditRmImg('${postId}',IDX)`,
|
||||
addImgFn: `locEditAddImg(this,'${postId}')`,
|
||||
addOptionFn: `locEditAddOption('${postId}')`,
|
||||
cancelFn: `cancelLocEdit('${postId}')`,
|
||||
saveFn: `saveLocEdit('${postId}')`
|
||||
});
|
||||
}
|
||||
|
||||
function cancelLocEdit(postId) {
|
||||
cancelPostEdit(postId, 'lp');
|
||||
}
|
||||
|
||||
function locEditRmImg(postId, idx) {
|
||||
const bilder = locEditBilder.get(postId);
|
||||
if (bilder) bilder.splice(idx, 1);
|
||||
_renderEditThumbs(locEditBilder, postId, 'lp', (pid, i) => locEditRmImg(pid, i));
|
||||
}
|
||||
|
||||
function locEditAddImg(input, postId) {
|
||||
const bilder = locEditBilder.get(postId) || [];
|
||||
locEditBilder.set(postId, bilder);
|
||||
Array.from(input.files || []).forEach(f =>
|
||||
processImageFile(f, bilder, () => _renderEditThumbs(locEditBilder, postId, 'lp', (pid, i) => locEditRmImg(pid, i)))
|
||||
);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function locEditAddOption(postId) {
|
||||
editAddOptionRow('lpeo-' + postId);
|
||||
}
|
||||
|
||||
async function saveLocEdit(postId) {
|
||||
await savePostEdit({
|
||||
postId,
|
||||
prefix: 'lp',
|
||||
editBilderMap: locEditBilder,
|
||||
endpoint: `/feed/posts/${postId}`,
|
||||
onSuccess: (updated) => {
|
||||
locPostCache[postId] = updated;
|
||||
locPostBilder.set(postId, updated.bilder || []);
|
||||
applyPostEditDom(updated, postId, 'lp', locPostBilder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let _locFeedObserver = null;
|
||||
function initLocFeedObserver() {
|
||||
if (_locFeedObserver) return;
|
||||
const s = document.getElementById('locFeedSentinel');
|
||||
if (!s) return;
|
||||
_locFeedObserver = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) loadLocFeed();
|
||||
}, { threshold: 0.1 });
|
||||
_locFeedObserver.observe(s);
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
loadPage();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user