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

Binary file not shown.

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);

File diff suppressed because it is too large Load Diff

View File

@@ -7,14 +7,8 @@
<title>Gruppe 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>
.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; }
/* Header */
.gruppe-header { display:flex; align-items:center; gap:1rem; margin-bottom:1.5rem; flex-wrap:wrap; }
.gruppe-avatar { width:72px; height:72px; border-radius:12px; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:2rem; flex-shrink:0; overflow:hidden; }
@@ -24,48 +18,9 @@
.gruppe-header-actions { margin-left:auto; display:flex; gap:0.5rem; }
.gruppe-header-actions button, .gruppe-header-actions a.btn { margin:0; width:auto; padding:0.4rem 0.9rem; font-size:0.85rem; }
/* Posts */
.post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; }
/* Compose-Typ (Gruppe-spezifisch) */
.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; }
.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; }
.umfrage-options { margin-top:0.5rem; }
.post-bild { width:100%; max-height:400px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
.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; }
.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-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; }
.post-action-btn:hover { color:var(--color-primary); background:none; }
.post-action-btn.active { color:var(--color-primary); }
.post-action-btn.danger:hover { color:#c0392b; }
.post-delete { margin-left:auto; }
/* Umfrage */
.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; }
/* Kommentare */
.comments-section { margin-top:0.75rem; border-top:1px solid var(--color-secondary); padding-top:0.75rem; }
@@ -111,26 +66,6 @@
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
/* 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 .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-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%; }
}
/* Dialog */
.dialog-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:200; align-items:center; justify-content:center; }
.dialog-backdrop.visible { display:flex; }
@@ -164,25 +99,24 @@
<div class="tab-panel active" id="tab-posts">
<!-- Compose -->
<div class="post-compose" id="compose" style="display:none;">
<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 style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
<button onclick="addOption()" style="width:auto; margin:0; padding:0.3rem 0.75rem; font-size:0.8rem;">+ Option</button>
<label class="multi-toggle">
<input type="checkbox" id="multiChoice"> Mehrfachauswahl möglich
</label>
</div>
</div>
<div class="compose-footer">
<label class="multi-toggle" id="multiChoiceRow" style="display:none;">
<input type="checkbox" id="multiChoice"> Multi-Choice
</label>
<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 type="button" id="umfrageBtn" class="compose-action-btn" onclick="toggleUmfrage(this)" title="Umfrage hinzufügen">📊</button>
<button onclick="submitPost()" style="width:auto; margin:0;">Veröffentlichen</button>
</div>
</div>
@@ -277,18 +211,19 @@
</div>
<!-- Post lightbox dialog -->
<div class="lightbox" id="postDialog">
<div class="lightbox" id="postLightbox">
<div class="lb-layout">
<button class="lb-close" onclick="closePostDialog()"></button>
<div class="lb-post-side" id="lbPostContent"></div>
<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="Kommentieren…" maxlength="500" rows="3"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();submitLbComment()}"></textarea>
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="submitLbComment()">Senden</button>
<button onclick="postLbComment()">Senden</button>
</div>
</div>
</div>
@@ -374,6 +309,7 @@
if (!meRes.ok) { location.href='/login.html'; return; }
const me = await meRes.json();
myId = me.userId;
initLb(myId);
await loadGruppe();
await loadPosts();
@@ -471,15 +407,24 @@
}
const gruppeEditBilder = new Map();
function renderPostCard(p) {
const canEdit = p.authorId === myId;
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
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 body = '';
// editable view area: question/text + images
const textStyle = p.beitragTyp === 'UMFRAGE' ? ' style="font-weight:600;margin-bottom:0.5rem;"' : '';
const editableHtml = `<div class="post-text"${textStyle}>${renderTextWithHashtags(p.text)}</div><div id="gpbi-${p.beitragId}">${bildHtml}</div>`;
// poll bars (only for UMFRAGE, not editable)
let barsHtml = '';
if (p.beitragTyp === 'UMFRAGE') {
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
const bars = p.optionen.map(o => {
barsHtml = p.optionen.map(o => {
const pct = total > 0 ? Math.round(o.stimmenCount / total * 100) : 0;
const voted = p.myVoteOptionIds.includes(o.optionId);
return `<div class="umfrage-option-bar ${voted?'voted':''}" onclick="event.stopPropagation(); vote('${p.beitragId}','${o.optionId}',this)">
@@ -489,23 +434,25 @@
<span>${pct}% (${o.stimmenCount})</span>
</div>
</div>`;
}).join('');
body = bars + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
} else {
body = `<div class="post-text">${renderTextWithHashtags(p.text)}</div>${bildHtml}`;
}).join('') + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
}
const rightBtns = (canEdit ? `<button class="post-action-btn" onclick="event.stopPropagation();startGruppeEdit('${p.beitragId}')" title="Bearbeiten" style="color:var(--color-muted)">✏</button>` : '')
+ (canDelete ? `<button class="post-action-btn danger post-delete" onclick="event.stopPropagation(); deletePost('${p.beitragId}',this)">✕</button>` : '');
return `
<div class="post-card" id="post-${p.beitragId}" onclick="openPostDialog('${p.beitragId}')" style="cursor:pointer;">
<div class="post-header">
<div class="post-avatar">${av}</div>
<div class="post-avatar"><a href="/community/benutzer.html?userId=${p.authorId}" onclick="event.stopPropagation()" style="display:contents;">${av}</a></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></div>
<div class="post-meta" id="gpm-${p.beitragId}">${fmtDate(p.createdAt)}${editedLabel}</div>
</div>
<div class="post-date">${fmtDate(p.createdAt)}</div>
${rightBtns ? `<div style="margin-left:auto;display:flex;gap:0.25rem;">${rightBtns}</div>` : ''}
</div>
${p.beitragTyp === 'UMFRAGE' ? `<div style="font-weight:600;margin-bottom:0.5rem;">${renderTextWithHashtags(p.text)}</div>${bildHtml}` : ''}
${body}
<div id="gpva-${p.beitragId}">${editableHtml}</div>
<div id="gpea-${p.beitragId}" style="display:none;"></div>
<div id="gpum-${p.beitragId}">${barsHtml}</div>
<div class="post-actions">
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="event.stopPropagation(); toggleLike('${p.beitragId}',this)" id="like-btn-${p.beitragId}">
♥ <span id="like-count-${p.beitragId}">${p.likeCount}</span>
@@ -514,11 +461,162 @@
💬 <span id="kmt-count-${p.beitragId}">${p.kommentarCount}</span>
</button>
${!p.reported ? `<button class="post-action-btn" onclick="event.stopPropagation(); reportPost('${p.beitragId}',this)">⚑ Melden</button>` : '<span style="font-size:0.78rem;color:var(--color-muted);">Gemeldet</span>'}
${canDelete ? `<button class="post-action-btn danger post-delete" onclick="event.stopPropagation(); deletePost('${p.beitragId}',this)">✕</button>` : ''}
</div>
</div>`;
}
function buildGruppeUmfrageHtml(beitragId, optionen, myVoteOptionIds, multiChoice) {
if (!optionen || optionen.length === 0) return '';
const total = optionen.reduce((s, o) => s + o.stimmenCount, 0);
return optionen.map(o => {
const pct = total > 0 ? Math.round(o.stimmenCount / total * 100) : 0;
const voted = myVoteOptionIds.includes(o.optionId);
return `<div class="umfrage-option-bar ${voted?'voted':''}" onclick="event.stopPropagation(); vote('${beitragId}','${o.optionId}',this)">
<div class="umfrage-bar-fill" style="width:${pct}%"></div>
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}% (${o.stimmenCount})</span></div>
</div>`;
}).join('') + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${multiChoice?' · Multi-Choice':''}</div>`;
}
function startGruppeEdit(beitragId) {
const post = allPosts.find(p => p.beitragId === beitragId);
if (!post) return;
gruppeEditBilder.set(beitragId, [...(post.bilder || [])]);
document.getElementById('gpva-' + beitragId).style.display = 'none';
document.getElementById('gpum-' + beitragId).style.display = 'none';
const isUmfrage = post.beitragTyp === 'UMFRAGE';
const optionenHtml = isUmfrage
? `<div id="gpeo-${beitragId}" style="margin-top:0.5rem;">${(post.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();gruppeEditAddOption('${beitragId}')" 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="gpmc-${beitragId}" ${post.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();gruppeEditAddImg(this,'${beitragId}')">
</label>
<button onclick="event.stopPropagation();saveGruppeEdit('${beitragId}')" style="width:auto;margin:0;">Speichern</button>
<button onclick="event.stopPropagation();cancelGruppeEdit('${beitragId}')" style="width:auto;margin:0;background:var(--color-secondary);color:var(--color-text);">Abbrechen</button>
</div>`;
const ea = document.getElementById('gpea-' + beitragId);
ea.style.display = '';
ea.onclick = e => e.stopPropagation();
ea.innerHTML = `
<textarea id="gpet-${beitragId}" 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(post.text)}</textarea>
<div class="compose-thumbs" id="gpet-tb-${beitragId}" style="margin-top:0.4rem;"></div>
${optionenHtml}
${actionRow}`;
renderGruppeEditThumbs(beitragId);
}
function cancelGruppeEdit(beitragId) {
document.getElementById('gpva-' + beitragId).style.display = '';
document.getElementById('gpum-' + beitragId).style.display = '';
document.getElementById('gpea-' + beitragId).style.display = 'none';
gruppeEditBilder.delete(beitragId);
}
function renderGruppeEditThumbs(beitragId) {
const bilder = gruppeEditBilder.get(beitragId) || [];
const c = document.getElementById('gpet-tb-' + beitragId);
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();gruppeEditRmImg('${beitragId}',${i})">✕</button></div>`
).join('');
c.style.display = bilder.length > 0 ? 'flex' : 'none';
}
function gruppeEditRmImg(beitragId, idx) {
gruppeEditBilder.get(beitragId).splice(idx, 1);
renderGruppeEditThumbs(beitragId);
}
function gruppeEditAddImg(input, beitragId) {
[...input.files].forEach(f => {
if (!f.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);
gruppeEditBilder.get(beitragId).push(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
renderGruppeEditThumbs(beitragId);
};
img.src = e.target.result;
};
reader.readAsDataURL(f);
});
input.value = '';
}
function gruppeEditAddOption(beitragId) {
const container = document.getElementById('gpeo-' + beitragId);
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'));
}
async function saveGruppeEdit(beitragId) {
const text = document.getElementById('gpet-' + beitragId).value.trim();
if (!text) return;
const post = allPosts.find(p => p.beitragId === beitragId);
const bilder = gruppeEditBilder.get(beitragId) || [];
const isUmfrageEdit = post?.beitragTyp === 'UMFRAGE';
const optionen = isUmfrageEdit
? Array.from(document.querySelectorAll(`#gpeo-${beitragId} input[type=text]`))
.map(inp => ({ optionId: inp.dataset.optionId || null, text: inp.value.trim() }))
.filter(o => o.text)
: null;
const multiChoice = isUmfrageEdit ? (document.getElementById('gpmc-' + beitragId)?.checked ?? false) : null;
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + beitragId, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, bilder, optionen, multiChoice })
});
if (!res.ok) return;
const updated = await res.json();
const idx = allPosts.findIndex(p => p.beitragId === beitragId);
if (idx >= 0) allPosts[idx] = { ...allPosts[idx], text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice };
gruppeEditBilder.delete(beitragId);
const gpva = document.getElementById('gpva-' + beitragId);
gpva.querySelector('.post-text').innerHTML = renderTextWithHashtags(updated.text);
const pbi = document.getElementById('gpbi-' + beitragId);
if (pbi) pbi.innerHTML = bilderGrid(updated.bilder);
gpva.style.display = '';
document.getElementById('gpea-' + beitragId).style.display = 'none';
const gpum = document.getElementById('gpum-' + beitragId);
gpum.innerHTML = buildGruppeUmfrageHtml(beitragId, updated.optionen, post?.myVoteOptionIds || [], post?.multiChoice);
gpum.style.display = '';
const meta = document.getElementById('gpm-' + beitragId);
if (meta && !meta.querySelector('.edited-label')) {
meta.insertAdjacentHTML('beforeend', ' <span class="edited-label" style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>');
}
}
async function toggleLike(postId, btn) {
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/like', { method:'POST' });
if (!res.ok) return;
@@ -620,17 +718,25 @@
// ── 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';
function toggleUmfrage(btn) {
const options = document.getElementById('umfrageOptions');
const isShowing = options.style.display !== 'none';
options.style.display = isShowing ? 'none' : '';
const placeholder = document.getElementById('composeText');
placeholder.placeholder = isUmfrage ? 'Frage eingeben…' : 'Was möchtest du teilen?';
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
placeholder.placeholder = isShowing ? 'Was möchtest du teilen?' : 'Frage eingeben…';
if (btn) btn.classList.toggle('active', !isShowing);
if (!isShowing && document.getElementById('optionList').children.length === 0) {
addOption(); addOption();
}
}
function resetUmfrage() {
document.getElementById('umfrageOptions').style.display = 'none';
document.getElementById('optionList').innerHTML = '';
document.getElementById('composeText').placeholder = 'Was möchtest du teilen?';
document.getElementById('umfrageBtn').classList.remove('active');
}
function addOption() {
const list = document.getElementById('optionList');
const idx = list.children.length;
@@ -642,11 +748,12 @@
async function submitPost() {
const text = document.getElementById('composeText').value.trim();
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
if (!text) return;
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked')?.value || 'TEXT';
const beitragTyp = hasUmfrage ? 'UMFRAGE' : 'TEXT';
let optionen = null;
let multiChoice = null;
if (beitragTyp === 'UMFRAGE') {
if (hasUmfrage) {
optionen = Array.from(document.querySelectorAll('#optionList input')).map(i => i.value.trim()).filter(v => v);
if (optionen.length < 2) { showModal('Hinweis', 'Bitte mindestens 2 Optionen eingeben.', [{ label: 'OK' }]); return; }
multiChoice = document.getElementById('multiChoice').checked;
@@ -659,9 +766,7 @@
});
if (res.ok || res.status === 201) {
document.getElementById('composeText').value = '';
document.getElementById('optionList').innerHTML = '';
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
toggleUmfrage();
resetUmfrage();
composeBilderArr = [];
renderComposeThumbs();
await loadPosts();
@@ -938,28 +1043,22 @@
// ── Post dialog ──
let lbPostId = null;
async function openPostDialog(postId) {
lbPostId = postId;
const post = allPosts.find(p => p.beitragId === postId);
if (!post) return;
renderLbPost(post);
document.getElementById('postDialog').classList.add('open');
await loadLbComments();
_lbSetupContent(postId, 'gp', post.bilder);
document.getElementById('postLightbox').classList.add('open');
document.body.style.overflow = 'hidden';
await loadLbComments(postId, 'GROUP');
document.getElementById('lbCommentInput').focus();
}
function closePostDialog() {
document.getElementById('postDialog').classList.remove('open');
lbPostId = null;
}
function renderLbPost(p) {
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
const bildHtml = bilderCarousel(p.bilder);
let body = '';
let umfrageHtml = '';
if (p.beitragTyp === 'UMFRAGE') {
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
const bars = p.optionen.map(o => {
@@ -973,19 +1072,23 @@
</div>
</div>`;
}).join('');
body = `<div style="font-weight:600;margin-bottom:0.5rem;">${esc(p.text)}</div>${bildHtml}${bars}<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
} else {
body = `<div class="post-text">${esc(p.text)}</div>${bildHtml}`;
umfrageHtml = bars + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
}
document.getElementById('lbPostContent').innerHTML = `
const textStyle = p.beitragTyp === 'UMFRAGE' ? ' style="font-weight:600;margin-bottom:0.5rem;"' : '';
document.getElementById('lbPostBody').innerHTML = `
<div class="post-header">
<div class="post-avatar">${av}</div>
<div class="post-avatar"><a href="/community/benutzer.html?userId=${p.authorId}" style="display:contents;">${av}</a></div>
<div>
<div class="post-author"><a href="/community/benutzer.html?userId=${p.authorId}" style="color:inherit;text-decoration:none;">${esc(p.authorName)}</a></div>
<div class="post-date">${fmtDate(p.createdAt)}</div>
</div>
</div>
${body}
<div id="gpva-${p.beitragId}">
<div class="post-text"${textStyle}>${renderTextWithHashtags(p.text)}</div>
<div id="gpbi-${p.beitragId}"></div>
</div>
<div id="gpum-${p.beitragId}">${umfrageHtml}</div>
<div class="post-actions" style="margin-top:0.75rem;">
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="toggleLikeLb('${p.beitragId}',this)">
♥ <span id="lb-like-count-${p.beitragId}">${p.likeCount}</span>
@@ -1011,41 +1114,7 @@
if (post) { post.likeCount = newCount; post.likedByMe = !isActive; }
}
async function deleteKommentar(kommentarId) {
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
await loadLbComments();
}
async function loadLbComments() {
if (!lbPostId) return;
const list = document.getElementById('lbCommentsList');
list.innerHTML = '';
const res = await fetch('/social/kommentare?targetType=GROUP_POST&targetId=' + lbPostId);
if (!res.ok) return;
const kmts = await res.json();
kmts.forEach(k => list.insertAdjacentHTML('beforeend', renderKommentarHtml(k, 'GROUP_POST', lbPostId, { myUserId: myId })));
list.scrollTop = list.scrollHeight;
}
async function submitLbComment() {
if (!lbPostId) return;
const input = document.getElementById('lbCommentInput');
const text = input.value.trim();
if (!text) return;
const res = await fetch('/social/kommentare', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ targetType: 'GROUP_POST', targetId: lbPostId, text })
});
if (res.ok || res.status === 201) {
input.value = '';
await loadLbComments();
const countEl = document.getElementById('kmt-count-' + lbPostId);
if (countEl) countEl.textContent = parseInt(countEl.textContent) + 1;
const post = allPosts.find(p => p.beitragId === lbPostId);
if (post) post.kommentarCount++;
}
}
async function reportPostLb(postId, btn) {
btn.disabled = true;
@@ -1071,7 +1140,7 @@
if (res.ok || res.status === 204) {
document.getElementById('post-' + postId)?.remove();
allPosts = allPosts.filter(p => p.beitragId !== postId);
closePostDialog();
closeLb();
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
}
}}
@@ -1087,13 +1156,15 @@
if (!res.ok) return;
await loadPosts();
const post = allPosts.find(p => p.beitragId === postId);
if (post) renderLbPost(post);
if (post) {
renderLbPost(post);
_lbSetupContent(postId, 'gp', post.bilder);
}
}
document.getElementById('postDialog').addEventListener('click', e => {
if (e.target === document.getElementById('postDialog')) closePostDialog();
document.getElementById('postLightbox').addEventListener('click', e => {
if (e.target === document.getElementById('postLightbox')) closeLb();
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePostDialog(); });
init();

View File

@@ -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>

View File

@@ -0,0 +1,104 @@
/* ── Tabs ── */
.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 ── */
.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)}
.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}
.compose-footer{display:flex;justify-content:space-between;align-items:center;margin-top:0.75rem;flex-wrap:wrap;gap:0.5rem}
.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}
.compose-action-btn.active{border-color:var(--color-primary);color:var(--color-primary)}
label.compose-action-btn{display:inline-flex;align-items:center}
.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}
/* ── Umfrage-Compose ── */
.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}
/* ── Post-Card ── */
.post-card{background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:1rem;margin-bottom:0.9rem}
.post-card.clickable{cursor:pointer;transition:border-color 0.15s}
.post-card.clickable: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-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}
/* ── Umfrage-Bars ── */
.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}
/* ── Gruppen-Badge / Diverse ── */
.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:700;align-items:center;justify-content:center}
.lightbox.open{display:flex}
/* Max-Breite: 1024px Bild + 300px Kommentare + Padding; Max-Höhe: 1024px Bild + Header/Text */
.lb-layout{display:flex;max-width:min(1400px,calc(100vw - 2rem));width:95vw;height:min(90vh,1200px);background:var(--color-card);border-radius:12px;overflow:hidden;position:relative}
/* Post-Seite als Flex-Spalte damit das Bild den Platz füllt */
.lb-post-side{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:1.25rem;border-right:1px solid var(--color-secondary);min-width:0}
.lb-post-side>*{flex-shrink:0}
.lb-post-side>.post-header{margin-bottom:0.75rem}
/* View-Area (va): wächst, enthält Bild + Text */
.lb-va{flex:1!important;min-height:0;display:flex;flex-direction:column;gap:0.4rem}
/* Bild-Container: füllt verbleibende Höhe */
.lb-ic{flex:1;min-height:0;position:relative;overflow:hidden;border-radius:4px}
.lb-ic .post-carousel{position:absolute;inset:0;display:flex;flex-direction:column}
.lb-ic .car-slide{display:none}
.lb-ic .car-slide.active{flex:1;min-height:0;display:flex;align-items:center;justify-content:center}
.lb-ic .car-slide img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;display:block}
.lb-ic .car-indicator{flex-shrink:0;text-align:center;font-size:0.75rem;color:var(--color-muted);padding:0.2rem 0}
/* Post-Text: scrollbar bei langem Inhalt */
.lb-text{flex-shrink:0!important;max-height:100px;overflow-y:auto;font-size:0.95rem;line-height:1.5;white-space:pre-wrap;word-break:break-word}
.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);flex:0 0 58vh}.lb-comments-panel{width:100%;flex:1}}
/* ── Text-only Lightbox (kein Bild → Kommentare unterhalb, volle Breite) ── */
.lb-text-only{flex-direction:column;width:min(680px,calc(100vw - 2rem));height:min(680px,calc(100vw - 2rem),90vh)}
.lb-text-only .lb-post-side{flex:0 0 auto;border-right:none;border-bottom:1px solid var(--color-secondary);overflow-y:auto;max-height:55%}
.lb-text-only .lb-va{flex:0 0 auto!important;min-height:unset}
.lb-text-only .lb-text{max-height:none!important}
.lb-text-only .lb-comments-panel{width:100%;flex:1;min-height:0;display:flex;flex-direction:column}
.lb-text-only .lb-comments-list{flex:1;overflow-y:auto}
.lb-text-only .lb-comment-compose{flex-shrink:0}

View File

@@ -19,6 +19,14 @@
.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)}
@@ -110,21 +118,117 @@ document.addEventListener('click', e => {
}
});
// ── Bild-Karussell ────────────────────────────────────────────────────────────
// ── Bild-Karussell (Lightbox/Detail-Ansicht) ─────────────────────────────────
function bilderCarousel(bilder) {
if (!bilder || bilder.length === 0) return '';
if (bilder.length === 1) {
return `<div style="margin-top:0.5rem;"><img class="post-bild" src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>`;
}
const slides = bilder.map((b, i) =>
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
).join('');
return `<div class="post-carousel">
${slides}
<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">&#8249;</button>
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">&#8250;</button>
<div class="car-indicator"><span class="car-cur">1</span>/${bilder.length}</div>
</div>`;
const nav = bilder.length > 1
? `<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">&#8249;</button>
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">&#8250;</button>
<div class="car-indicator"><span class="car-cur">1</span>&#8202;/&#8202;${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 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);
} 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>`);
}
}
}
function carNav(btn, dir) {
@@ -209,6 +313,7 @@ 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(''))
@@ -235,3 +340,237 @@ 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>');
}
}

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');