Locations können nun auch Posten - bugfixes im Feed
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
<title>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
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
104
src/main/resources/static/css/community.css
Normal file
104
src/main/resources/static/css/community.css
Normal 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}
|
||||
@@ -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)">‹</button>
|
||||
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">›</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)">‹</button>
|
||||
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">›</button>
|
||||
<div class="car-indicator"><span class="car-cur">1</span> / ${bilder.length}</div>`
|
||||
: '';
|
||||
return `<div class="post-carousel">${slides}${nav}</div>`;
|
||||
}
|
||||
|
||||
/** Richtet die Lightbox-Inhalte ein:
|
||||
* – View-Area bekommt Flex-Layout damit das Bild den Platz füllt
|
||||
* – Bild-Container bekommt lb-ic-Klasse + Carousel (für alle Bildanzahlen)
|
||||
* – Post-Text bekommt scrollbare Höhenbegrenzung
|
||||
*/
|
||||
function _lbSetupContent(postId, prefix, bilder) {
|
||||
const 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>');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user