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>Profil 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>
/* ── Profile Header ── */
.profil-header {
@@ -487,50 +488,13 @@
.vorliebe-chip.bw-EHER_NICHT { border-color:#fb8c00; color:#fb8c00; }
.vorliebe-chip.bw-GEHT_GAR_NICHT { border-color:#e53935; color:#e53935; }
/* ── Post cards (profile posts tab) ── */
.post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; }
.post-header { display:flex; align-items:center; gap:0.7rem; margin-bottom:0.6rem; }
.post-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.95rem; flex-shrink:0; overflow:hidden; }
.post-avatar img { width:100%; height:100%; object-fit:cover; }
.post-author { font-weight:600; font-size:0.9rem; }
.post-date { font-size:0.75rem; color:var(--color-muted); margin-left:auto; }
.post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
.post-bild { width:100%; max-height:360px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; transition:opacity 0.2s; }
/* ── Post-Bild-Wrap (Profil-spezifisch) ── */
.post-bild-wrap { position:relative; cursor:pointer; display:block; }
.post-bild-wrap:hover .post-bild { opacity:0.82; }
.post-bild-hover-icon { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; opacity:0; transition:opacity 0.2s; pointer-events:none; font-size:1.6rem; }
.post-bild-wrap:hover .post-bild-hover-icon { opacity:1; }
.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; }
.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; }
/* ── Post / Bild Lightbox ── */
.lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:400; 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; }
/* ── Bild-Navigation in Lightbox ── */
.lb-img-nav { display:flex; gap:0.75rem; align-items:center; justify-content:center; margin-top:0.75rem; flex-wrap:wrap; }
.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; }
@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%; } }
</style>
</head>
<body class="app">
@@ -652,8 +616,9 @@
<script src="/js/shared.js"></script>
<script src="/js/image-viewer.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/hashtag.js"></script>
<script src="/js/meldung.js"></script>
<script>
// ── State ──
@@ -680,6 +645,9 @@
let activeLbPostId = null;
let activeLbMode = null; // 'post' | 'image'
let activeLbImageIdx = null;
const profilPostBilder = new Map(); // postId → bilder[] für Lightbox-Carousel
const profilPostCache = {}; // postId → post-Objekt für Edit
const profilEditBilder = new Map(); // postId → bilder[] während Bearbeitung
// ── Label maps ──
const GESCHLECHT_LABEL = { WEIBLICH: 'weiblich', DIVERS: 'divers', MAENNLICH: 'männlich' };
@@ -719,6 +687,7 @@
}
myUserId = me ? me.userId : null;
initLb(myUserId);
isOwnProfile = !previewMode && me && me.userId === profile.userId;
profileData = profile;
allImages = images;
@@ -1248,6 +1217,7 @@
}
function renderLbImageBody() {
document.querySelector('#postLightbox .lb-layout')?.classList.remove('lb-text-only');
const img = allImages[activeLbImageIdx];
const total = allImages.length;
const prevDisabled = activeLbImageIdx === 0 ? 'disabled' : '';
@@ -1589,42 +1559,48 @@
const avatarHtml = p.authorPicture
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
: '◉';
const bildRaw = bilderCarousel(p.bilder);
const bildHtml = bildRaw
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}</div>`
: '';
profilPostCache[p.postId] = p;
profilPostBilder.set(p.postId, p.bilder || []);
const bildHtml = bilderGrid(p.bilder);
const privacyLabel = p.isPublic ? '' : '<span style="font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem;">🔒 privat</span>';
const editedLabel = p.editedAt ? ' <span class="edited-label" 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);
umfrageHtml = '<div style="margin-top:0.5rem;">' + p.optionen.map(o => {
umfrageHtml = 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="voteProfilPost('${p.postId}','${o.optionId}')">
<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>`;
}).join('') + `<div class="umfrage-total">${totalVotes} Stimme${totalVotes !== 1 ? 'n' : ''}</div>`;
}
const canDelete = p.authorId === myUserId;
const deleteBtn = canDelete
? `<button class="post-action-btn post-delete" onclick="deleteProfilPost('${p.postId}')">🗑</button>`
const isOwn = p.authorId === myUserId;
const rightBtns = isOwn
? `<div style="margin-left:auto;display:flex;gap:0.25rem;">
<button class="post-action-btn" onclick="event.stopPropagation();startProfilEdit('${p.postId}')" title="Bearbeiten" style="color:var(--color-muted)">✏</button>
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteProfilPost('${p.postId}')">🗑</button>
</div>`
: '';
return `<div class="post-card" id="pp-${p.postId}">
<div class="post-header">
<div class="post-avatar">${avatarHtml}</div>
<div class="post-avatar"><a href="/community/benutzer.html?userId=${p.authorId}" onclick="event.stopPropagation()" style="display:contents;">${avatarHtml}</a></div>
<div>
<div class="post-author">${esc(p.authorName)}${privacyLabel}</div>
<div class="post-date">${fmtDate(p.createdAt)}</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-date" id="ppm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}</div>
</div>
${deleteBtn}
${rightBtns}
</div>
<div class="post-text">${esc(p.text)}</div>
${bildHtml}
${umfrageHtml}
<div id="ppva-${p.postId}">
<div class="post-text">${esc(p.text)}</div>
<div id="ppbi-${p.postId}" class="post-bild-wrap" data-post-id="${p.postId}">${bildHtml}</div>
</div>
<div id="ppea-${p.postId}" style="display:none;"></div>
<div id="ppum-${p.postId}">${umfrageHtml}</div>
<div class="post-actions">
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="pp-like-${p.postId}" onclick="likeProfilPost('${p.postId}')">
♥ <span id="pp-lc-${p.postId}">${p.likeCount}</span>
@@ -1665,6 +1641,73 @@
if (res.ok) document.getElementById('pp-' + postId)?.remove();
}
// ── Profil-Post-Bearbeitung ──
function startProfilEdit(postId) {
const post = profilPostCache[postId];
if (!post) return;
startPostEdit({
postId,
prefix: 'pp',
data: post,
editBilderMap: profilEditBilder,
saveFn: 'saveProfilEdit',
cancelFn: 'cancelProfilEdit',
addImgFn: 'profilEditAddImg',
addOptionFn: 'profilEditAddOption',
rmImgFn: 'profilEditRmImg'
});
}
function cancelProfilEdit(postId) {
cancelPostEdit(postId, 'pp', profilEditBilder);
}
function profilEditRmImg(postId, idx) {
profilEditBilder.get(postId)?.splice(idx, 1);
_renderEditThumbs(profilEditBilder, postId, 'pp', 'profilEditRmImg');
}
function profilEditAddImg(input, postId) {
const arr = profilEditBilder.get(postId);
if (!arr) return;
[...input.files].forEach(f => processImageFile(f, arr, () => {
_renderEditThumbs(profilEditBilder, postId, 'pp', 'profilEditRmImg');
}));
input.value = '';
}
function profilEditAddOption(postId) {
editAddOptionRow('ppeo-' + postId);
}
async function saveProfilEdit(postId) {
const post = profilPostCache[postId];
await savePostEdit({
postId,
prefix: 'pp',
endpoint: '/feed/posts/' + postId,
isUmfrage: post?.beitragTyp === 'UMFRAGE',
editBilderMap: profilEditBilder,
onSuccess: updated => {
profilPostCache[postId] = { ...profilPostCache[postId], text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [] };
profilPostBilder.set(postId, updated.bilder || []);
let umfrageHtml = '';
if (post?.beitragTyp === 'UMFRAGE' && updated.optionen) {
const totalVotes = updated.optionen.reduce((s, o) => s + o.stimmenCount, 0);
umfrageHtml = updated.optionen.map(o => {
const pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
return `<div class="umfrage-option-bar" onclick="voteProfilPost('${postId}','${o.optionId}')">
<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>`;
}
applyPostEditDom(postId, 'pp', updated, umfrageHtml);
}
});
}
// ── Lightbox (Post + Bild) ──
function openPostLb(postId) {
activeLbMode = 'post';
@@ -1674,7 +1717,9 @@
if (card) {
const clone = card.cloneNode(true);
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
clone.querySelectorAll('[id^="ppea-"]').forEach(el => el.remove());
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
_lbSetupContent(postId, 'pp', profilPostBilder.get(postId) || []);
}
loadLbComments();
document.getElementById('postLightbox').classList.add('open');
@@ -1728,7 +1773,6 @@
if (e.target === document.getElementById('postLightbox')) closeLb();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && document.getElementById('postLightbox').classList.contains('open')) closeLb();
if (activeLbMode === 'image') {
if (e.key === 'ArrowLeft') lbGalleryNav(-1);
if (e.key === 'ArrowRight') lbGalleryNav(1);