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,6 +7,7 @@
<title>Home 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>
.game-grid {
display: grid;
@@ -184,64 +185,9 @@
}
.friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
/* ── Compose ── */
.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; 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 Cards (1:1 wie Feed) ── */
.post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; cursor:pointer; transition:border-color 0.15s; }
/* ── Post-Cards (Home: klickbar + Hover) ── */
.post-card { cursor:pointer; transition:border-color 0.15s; }
.post-card:hover { border-color:var(--color-primary); }
.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-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); font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; margin:0; width:auto; pointer-events:none; }
.post-action-btn.active { color:var(--color-primary); }
.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-left:0.3rem; }
.umfrage-option-bar { margin:0.3rem 0; border-radius:6px; overflow:hidden; border:1px solid var(--color-secondary); position:relative; }
.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); }
.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; }
/* ── 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 { flex:1; overflow-y:auto; padding:1.25rem; border-right:1px solid var(--color-secondary); min-width:0; }
.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-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%; } }
/* ── Spiel starten ── */
.start-game-grid {
@@ -413,21 +359,19 @@
<!-- Feed Compose + Vorschau -->
<div class="section-label">Feed 📰</div>
<div class="post-compose" id="homeCompose">
<div class="compose-type">
<label><input type="radio" name="homeBeitragTyp" value="TEXT" checked onchange="homeToggleUmfrage()"> Text</label>
<label><input type="radio" name="homeBeitragTyp" value="UMFRAGE" onchange="homeToggleUmfrage()"> Umfrage</label>
</div>
<textarea id="homeComposeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
<div class="compose-thumbs" id="homeComposeThumbs"></div>
<div class="umfrage-options" id="homeUmfrageOptions" style="display:none;">
<div id="homeOptionList"></div>
<button onclick="homeAddOption()" 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="homeAddOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
<label class="multi-toggle">
<input type="checkbox" id="homeMultiChoice"> Mehrfachauswahl möglich
</label>
</div>
</div>
<div class="compose-footer">
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
<label class="multi-toggle" id="homeMultiChoiceRow" style="display:none;">
<input type="checkbox" id="homeMultiChoice"> Multi-Choice
</label>
<label class="privacy-toggle">
<input type="checkbox" id="homeIsPublic"> Öffentlich
</label>
@@ -437,6 +381,7 @@
<label class="compose-action-btn" title="Fotos hinzufügen">📷
<input type="file" id="homeComposeBildFile" accept="image/*" multiple style="display:none;" onchange="homeSelectBilder(this)">
</label>
<button type="button" id="homeUmfrageBtn" class="compose-action-btn" onclick="homeToggleUmfrage(this)" title="Umfrage hinzufügen">📊</button>
<button onclick="homeSubmitPost()" style="width:auto;margin:0;">Veröffentlichen</button>
</div>
</div>
@@ -482,6 +427,7 @@
.then(user => {
if (user) {
myUserId = user.userId;
initLb(user.userId);
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
Promise.all([loadActiveGames(user.userId), loadActiveLock()]).then(() => {
const hasGames = document.getElementById('activeGamesSection').style.display !== 'none';
@@ -770,15 +716,22 @@
let homeComposeBilder = [];
function homeToggleUmfrage() {
const isUmfrage = document.querySelector('input[name="homeBeitragTyp"]:checked').value === 'UMFRAGE';
document.getElementById('homeUmfrageOptions').style.display = isUmfrage ? '' : 'none';
document.getElementById('homeMultiChoiceRow').style.display = isUmfrage ? '' : 'none';
if (isUmfrage && document.getElementById('homeOptionList').children.length === 0) {
function homeToggleUmfrage(btn) {
const options = document.getElementById('homeUmfrageOptions');
const isShowing = options.style.display !== 'none';
options.style.display = isShowing ? 'none' : '';
if (btn) btn.classList.toggle('active', !isShowing);
if (!isShowing && document.getElementById('homeOptionList').children.length === 0) {
homeAddOption(); homeAddOption();
}
}
function homeResetUmfrage() {
document.getElementById('homeUmfrageOptions').style.display = 'none';
document.getElementById('homeOptionList').innerHTML = '';
document.getElementById('homeUmfrageBtn').classList.remove('active');
}
function homeAddOption() {
const list = document.getElementById('homeOptionList');
const idx = list.children.length;
@@ -789,57 +742,28 @@
list.appendChild(row);
}
function homeSelectBilder(input) {
[...input.files].forEach(f => { if (f.type.startsWith('image/')) homeProcessImage(f); });
input.value = '';
}
function homeProcessImage(file) {
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);
homeComposeBilder.push(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
homeRenderThumbs();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function homeRenderThumbs() {
const container = document.getElementById('homeComposeThumbs');
container.innerHTML = '';
homeComposeBilder.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="homeRemoveThumb(${i})">✕</button>`;
container.appendChild(div);
renderBilderThumbs(homeComposeBilder, 'homeComposeThumbs', i => {
homeComposeBilder.splice(i, 1);
homeRenderThumbs();
});
container.style.display = homeComposeBilder.length > 0 ? 'flex' : 'none';
}
function homeRemoveThumb(idx) {
homeComposeBilder.splice(idx, 1);
homeRenderThumbs();
function homeSelectBilder(input) {
[...input.files].forEach(f => processImageFile(f, homeComposeBilder, homeRenderThumbs));
input.value = '';
}
async function homeSubmitPost() {
const text = document.getElementById('homeComposeText').value.trim();
const hasUmfrage = document.getElementById('homeUmfrageOptions').style.display !== 'none';
if (!text && homeComposeBilder.length === 0) return;
const beitragTyp = document.querySelector('input[name="homeBeitragTyp"]:checked').value;
const beitragTyp = hasUmfrage ? 'UMFRAGE' : 'TEXT';
const multiChoice = document.getElementById('homeMultiChoice').checked;
const isPublic = document.getElementById('homeIsPublic').checked;
let optionen = [];
if (beitragTyp === 'UMFRAGE') {
if (hasUmfrage) {
optionen = Array.from(document.getElementById('homeOptionList').querySelectorAll('input'))
.map(i => i.value.trim()).filter(v => v);
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
@@ -856,11 +780,9 @@
document.getElementById('homeComposeText').value = '';
homeComposeBilder = [];
homeRenderThumbs();
document.querySelector('input[name="homeBeitragTyp"][value="TEXT"]').checked = true;
homeToggleUmfrage();
homeResetUmfrage();
document.getElementById('homeMultiChoice').checked = false;
document.getElementById('homeIsPublic').checked = false;
document.getElementById('homeOptionList').innerHTML = '';
// Prepend in Vorschau
const feedList = document.getElementById('feedList');
@@ -882,7 +804,7 @@
homeCompose.addEventListener('drop', e => {
e.preventDefault();
homeCompose.classList.remove('drag-over');
[...e.dataTransfer.files].filter(f => f.type.startsWith('image/')).forEach(homeProcessImage);
[...e.dataTransfer.files].filter(f => f.type.startsWith('image/')).forEach(f => processImageFile(f, homeComposeBilder, homeRenderThumbs));
});
}
@@ -890,69 +812,49 @@
const homePostCache = {};
let activeLbPostId = null;
let activeLbPostType = null;
function homeOpenPost(postId) {
const p = homePostCache[postId];
if (!p) return;
activeLbPostId = p.postId;
activeLbPostType = p.postType || 'FEED';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = renderHomePostCard(p);
const card = tempDiv.firstElementChild;
if (card) {
card.querySelectorAll('.post-actions').forEach(el => el.remove());
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
_lbSetupContent(postId, 'hp', p.bilder);
}
loadLbComments(p.postId, activeLbPostType);
loadLbComments(p.postId, p.postType || 'FEED');
document.getElementById('postLightbox').classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeLb() {
document.getElementById('postLightbox').classList.remove('open');
document.body.style.overflow = '';
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';
try {
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('');
} catch (_) {}
// ── Like / Delete ──────────────────────────────────────────────────────────
async function likeHomePost(postId, postType) {
const ep = postType === 'GROUP'
? `/gruppen/${document.getElementById('hpc-'+postId)?.dataset?.gruppeId}/posts/${postId}/like`
: `/feed/posts/${postId}/like`;
await fetch(ep, { method: 'POST' });
const btn = document.getElementById('hlk-' + postId);
const lc = document.getElementById('hlkc-' + postId);
const was = btn.classList.contains('active');
btn.classList.toggle('active', !was);
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
}
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);
async function deleteHomePost(postId) {
if (!confirm('Post löschen?')) return;
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
if (res.ok) document.getElementById('hpc-' + postId)?.remove();
}
async function deleteKommentar(kommentarId, targetType, targetId) {
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
await loadLbComments(targetId, activeLbPostType);
}
// ── Post-Karte ────────────────────────────────────────────────────────────
const homeEditBilder = new Map();
function renderHomePostCard(p) {
homePostCache[p.postId] = p;
@@ -963,7 +865,8 @@
const groupBadge = p.postType === 'GROUP' && p.gruppeId
? `<span class="gruppe-badge">👥 ${esc(p.gruppeName)}</span>`
: '';
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 umfrageHtml = '';
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
@@ -976,24 +879,74 @@
</div>`;
}).join('') + `<div class="umfrage-total">${totalVotes} Stimme${totalVotes !== 1 ? 'n' : ''}</div></div>`;
}
return `<div class="post-card" onclick="homeOpenPost('${p.postId}')" style="cursor:pointer">
const canOwn = p.postType === 'FEED' && p.authorId === myUserId;
const ownBtns = canOwn
? `<div style="margin-left:auto;display:flex;gap:0.4rem;">
<button class="post-action-btn" onclick="event.stopPropagation();startHomeEdit('${p.postId}')" title="Bearbeiten" style="color:var(--color-muted)">✏</button>
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteHomePost('${p.postId}')" title="Löschen">🗑</button>
</div>`
: '';
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
return `<div class="post-card" id="hpc-${p.postId}"${gruppeIdAttr} onclick="homeOpenPost('${p.postId}')">
<div class="post-header">
<div class="post-avatar">${avatarHtml}</div>
<div>
<div class="post-author">${esc(p.authorName)}${privacyLabel}</div>
<div class="post-meta">${fmtDate(p.createdAt)}${groupBadge}</div>
<div class="post-meta" id="hpm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
</div>
${ownBtns}
</div>
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
${bildHtml}
${umfrageHtml}
<div id="hpva-${p.postId}">
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
<div id="hpbi-${p.postId}">${bildHtml}</div>
</div>
<div id="hpea-${p.postId}" style="display:none;"></div>
<div id="hpum-${p.postId}">${umfrageHtml}</div>
<div class="post-actions">
<button class="post-action-btn${p.likedByMe ? ' active' : ''}">♥ <span>${p.likeCount}</span></button>
<button class="post-action-btn">💬 <span>${p.kommentarCount}</span></button>
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="hlk-${p.postId}" onclick="event.stopPropagation();likeHomePost('${p.postId}','${p.postType}')">♥ <span id="hlkc-${p.postId}">${p.likeCount}</span></button>
<button class="post-action-btn" onclick="event.stopPropagation();homeOpenPost('${p.postId}')">💬 <span id="hkc-${p.postId}">${p.kommentarCount}</span></button>
</div>
</div>`;
}
// ── Post-Bearbeitung (Home) ───────────────────────────────────────────────
function startHomeEdit(postId) {
const data = homePostCache[postId];
if (!data) return;
startPostEdit({ postId, prefix: 'hp', data, editBilderMap: homeEditBilder,
saveFn: 'saveHomeEdit', cancelFn: 'cancelHomeEdit',
addImgFn: 'homeEditAddImg', addOptionFn: 'homeEditAddOption', rmImgFn: 'homeEditRmImg' });
}
function cancelHomeEdit(postId) { cancelPostEdit(postId, 'hp', homeEditBilder); }
function homeEditRmImg(postId, idx) {
homeEditBilder.get(postId).splice(idx, 1);
_renderEditThumbs(homeEditBilder, postId, 'hp', 'homeEditRmImg');
}
function homeEditAddImg(input, postId) {
[...input.files].forEach(f => processImageFile(f, homeEditBilder.get(postId), () => _renderEditThumbs(homeEditBilder, postId, 'hp', 'homeEditRmImg')));
input.value = '';
}
function homeEditAddOption(postId) { editAddOptionRow(`hpeo-${postId}`); }
async function saveHomeEdit(postId) {
const cached = homePostCache[postId];
await savePostEdit({ postId, prefix: 'hp', endpoint: `/feed/posts/${postId}`,
isUmfrage: cached?.beitragTyp === 'UMFRAGE', editBilderMap: homeEditBilder,
onSuccess: updated => {
homePostCache[postId] = { ...cached, text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice };
const totalVotes = (updated.optionen || []).reduce((s, o) => s + o.stimmenCount, 0);
const umfrageHtml = updated.optionen?.length > 0
? '<div style="margin-top:0.5rem;">' + updated.optionen.map(o => {
const pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
return `<div class="umfrage-option-bar"><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>`
: '';
applyPostEditDom(postId, 'hp', updated, umfrageHtml);
}
});
}
async function loadFeed() {
try {
const res = await fetch('/feed/mine?size=3&page=0');