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:
Binary file not shown.
Binary file not shown.
BIN
bin/main/de/oaa/xxx/feed/FeedController$UpdatePostRequest.class
Normal file
BIN
bin/main/de/oaa/xxx/feed/FeedController$UpdatePostRequest.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bin/main/de/oaa/xxx/feed/entity/PosterType.class
Normal file
BIN
bin/main/de/oaa/xxx/feed/entity/PosterType.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 id="ppva-${p.postId}">
|
||||
<div class="post-text">${esc(p.text)}</div>
|
||||
${bildHtml}
|
||||
${umfrageHtml}
|
||||
<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);
|
||||
|
||||
@@ -7,135 +7,49 @@
|
||||
<title>Feed – 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; }
|
||||
|
||||
.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; 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; }
|
||||
.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-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-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; }
|
||||
|
||||
/* Carousel – Stile kommen aus shared.js */
|
||||
|
||||
.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; }
|
||||
|
||||
.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; }
|
||||
|
||||
.post-card { cursor:pointer; }
|
||||
.hashtag-banner { display:flex; align-items:center; gap:0.75rem; margin-bottom:1.25rem; padding:0.65rem 1rem; background:var(--color-card); border:1px solid var(--color-secondary); border-radius:8px; }
|
||||
.hashtag-banner-tag { font-size:1.05rem; font-weight:700; color:var(--color-primary); }
|
||||
.hashtag-banner-back { margin-left:auto; font-size:0.82rem; color:var(--color-muted); text-decoration:none; }
|
||||
.hashtag-banner-back:hover { color:var(--color-primary); }
|
||||
|
||||
/* 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-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%; } }
|
||||
|
||||
/* Comment + Like-Stile kommen aus shared.js */
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<!-- Hashtag-Banner (nur sichtbar wenn ?tag=… gesetzt) -->
|
||||
<div class="hashtag-banner" id="hashtagBanner" style="display:none;">
|
||||
<span>Posts mit </span><span class="hashtag-banner-tag" id="hashtagBannerLabel"></span>
|
||||
<a class="hashtag-banner-back" href="/community/feed.html">× Zurück zum Feed</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="feedTabs">
|
||||
<button class="tab-btn active" id="tabMine" data-tab="mine" onclick="switchTab('mine', this)">Mein Feed</button>
|
||||
<button class="tab-btn" id="tabPublic" data-tab="public" onclick="switchTab('public', this)">Öffentlicher Feed</button>
|
||||
<button class="tab-btn active" data-tab="mine" onclick="switchTab('mine',this)">Mein Feed</button>
|
||||
<button class="tab-btn" data-tab="public" onclick="switchTab('public',this)">Öffentlicher Feed</button>
|
||||
</div>
|
||||
|
||||
<!-- Mein Feed -->
|
||||
<div class="tab-panel active" id="tab-mine">
|
||||
<div class="post-compose" id="compose">
|
||||
<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">
|
||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
||||
<label class="multi-toggle" id="multiChoiceRow" style="display:none;">
|
||||
<input type="checkbox" id="multiChoice"> Multi-Choice
|
||||
</label>
|
||||
<label class="privacy-toggle">
|
||||
<input type="checkbox" id="isPublic"> Öffentlich
|
||||
</label>
|
||||
</div>
|
||||
<label class="privacy-toggle"><input type="checkbox" id="isPublic"> Öffentlich</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">📷
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji">😊</button>
|
||||
<label class="compose-action-btn" title="Fotos">📷
|
||||
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
||||
</label>
|
||||
<button onclick="submitPost()" style="width:auto; margin:0;">Veröffentlichen</button>
|
||||
<button type="button" id="umfrageBtn" class="compose-action-btn" onclick="toggleUmfrage(this)" title="Umfrage">📊</button>
|
||||
<button onclick="submitPost()" style="width:auto;margin:0;">Veröffentlichen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,24 +58,21 @@
|
||||
<div class="sentinel" id="mineSentinel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Öffentlicher Feed -->
|
||||
<div class="tab-panel" id="tab-public">
|
||||
<div id="publicFeed"></div>
|
||||
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
|
||||
<div class="sentinel" id="publicSentinel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hashtag-Feed (wird angezeigt wenn ?tag=… gesetzt) -->
|
||||
<div id="tab-hashtag" style="display:none;">
|
||||
<div id="hashtagFeed"></div>
|
||||
<p class="empty-hint" id="hashtagEmpty" style="display:none;">Keine Posts mit diesem Hashtag.</p>
|
||||
<div class="sentinel" id="hashtagSentinel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Lightbox -->
|
||||
<!-- Lightbox (IDs geteilt mit shared.js) -->
|
||||
<div class="lightbox" id="postLightbox">
|
||||
<div class="lb-layout">
|
||||
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||
@@ -190,9 +101,7 @@
|
||||
<script>
|
||||
// ── State ──
|
||||
let myUserId = null;
|
||||
let activeLbPostId = null;
|
||||
let activeLbPostType = null;
|
||||
let activeHashtag = null; // set when ?tag=... is in URL
|
||||
let activeHashtag = null;
|
||||
|
||||
const feedState = {
|
||||
mine: { page:0, hasMore:true, loading:false, loaded:false },
|
||||
@@ -200,9 +109,11 @@
|
||||
hashtag: { page:0, hasMore:true, loading:false, loaded:false }
|
||||
};
|
||||
|
||||
const feedPostCache = new Map();
|
||||
const feedEditBilder = new Map();
|
||||
let composeBilderArr = [];
|
||||
|
||||
// ── Hashtag-Modus prüfen ──
|
||||
// ── Hashtag-Modus ──
|
||||
const _urlTag = new URLSearchParams(window.location.search).get('tag');
|
||||
if (_urlTag) {
|
||||
activeHashtag = _urlTag.replace(/^#/, '').toLowerCase();
|
||||
@@ -219,6 +130,7 @@
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||
if (user) {
|
||||
myUserId = user.userId;
|
||||
initLb(myUserId);
|
||||
if (activeHashtag) {
|
||||
await loadFeed('hashtag');
|
||||
} else {
|
||||
@@ -234,18 +146,11 @@
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// ── Autocomplete für Compose ──
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ta = document.getElementById('composeText');
|
||||
if (ta) attachHashtagAutocomplete(ta);
|
||||
});
|
||||
// Fallback falls DOMContentLoaded bereits gefeuert
|
||||
if (document.readyState !== 'loading') {
|
||||
const ta = document.getElementById('composeText');
|
||||
if (ta) attachHashtagAutocomplete(ta);
|
||||
}
|
||||
// Hashtag-Autocomplete
|
||||
if (document.readyState !== 'loading') attachHashtagAutocomplete(document.getElementById('composeText'));
|
||||
else document.addEventListener('DOMContentLoaded', () => attachHashtagAutocomplete(document.getElementById('composeText')));
|
||||
|
||||
// ── Tab switching ──
|
||||
// ── Tabs ──
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
@@ -254,39 +159,27 @@
|
||||
localStorage.setItem('tab_feed', name);
|
||||
if (!feedState[name].loaded) loadFeed(name);
|
||||
}
|
||||
const _savedFeedTab = localStorage.getItem('tab_feed');
|
||||
if (_savedFeedTab) {
|
||||
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedFeedTab}"]`);
|
||||
if (_btn) switchTab(_savedFeedTab, _btn);
|
||||
}
|
||||
const _savedTab = localStorage.getItem('tab_feed');
|
||||
if (_savedTab) { const _b = document.querySelector(`.tab-btn[data-tab="${_savedTab}"]`); if (_b) switchTab(_savedTab, _b); }
|
||||
|
||||
// ── Feed loading ──
|
||||
// ── Feed laden ──
|
||||
async function loadFeed(tab) {
|
||||
const state = feedState[tab];
|
||||
if (state.loading || !state.hasMore) return;
|
||||
state.loading = true;
|
||||
state.loaded = true;
|
||||
state.loading = true; state.loaded = true;
|
||||
try {
|
||||
let url;
|
||||
if (tab === 'hashtag') {
|
||||
url = `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`;
|
||||
} else {
|
||||
const base = tab === 'mine' ? '/feed/mine' : '/feed/public';
|
||||
url = `${base}?page=${state.page}&size=10`;
|
||||
}
|
||||
const url = tab === 'hashtag'
|
||||
? `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`
|
||||
: `${tab === 'mine' ? '/feed/mine' : '/feed/public'}?page=${state.page}&size=10`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const feedEl = document.getElementById(tab + 'Feed');
|
||||
if (state.page === 0 && data.posts.length === 0) {
|
||||
document.getElementById(tab + 'Empty').style.display = '';
|
||||
}
|
||||
if (state.page === 0 && data.posts.length === 0) document.getElementById(tab + 'Empty').style.display = '';
|
||||
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
|
||||
state.hasMore = data.hasMore;
|
||||
state.page++;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
} finally { state.loading = false; }
|
||||
}
|
||||
|
||||
// ── Infinite Scroll ──
|
||||
@@ -302,188 +195,148 @@
|
||||
observer.observe(document.getElementById('publicSentinel'));
|
||||
observer.observe(document.getElementById('hashtagSentinel'));
|
||||
|
||||
// bilderCarousel und carNav kommen aus shared.js
|
||||
|
||||
// ── Render post card ──
|
||||
// ── Post-Card rendern ──
|
||||
function renderPostCard(p, tab) {
|
||||
const avatarHtml = p.authorPicture
|
||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
||||
: '◉';
|
||||
feedPostCache.set(p.postId, { text: p.text, bilder: p.bilder || [], beitragTyp: p.beitragTyp, optionen: p.optionen || [], myVoteOptionIds: p.myVoteOptionIds || [], multiChoice: p.multiChoice, _tab: tab });
|
||||
const isLocPost = p.posterType === 'LOCATION';
|
||||
const authorUrl = isLocPost
|
||||
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
|
||||
: `/community/benutzer.html?userId=${p.authorId}`;
|
||||
const avatarHtml = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : (isLocPost ? '📍' : '◉');
|
||||
const privacyLabel = !p.isPublic ? ' <span style="font-size:0.7rem;color:var(--color-muted);">🔒</span>' : '';
|
||||
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
||||
? `<span class="gruppe-badge" onclick="event.stopPropagation()">👥 <a href="/community/gruppe.html?id=${p.gruppeId}" onclick="event.stopPropagation()">${esc(p.gruppeName)}</a></span>`
|
||||
: '';
|
||||
const bildHtml = bilderCarousel(p.bilder, p.postId);
|
||||
const editedLabel = p.editedAt ? ` <span style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>` : '';
|
||||
const umfrageHtml = buildUmfrageHtml(p.postId, p.optionen, p.myVoteOptionIds,
|
||||
(postId, optionId) => `event.stopPropagation(); votePost('${postId}','${optionId}','${tab}','${p.postType}')`);
|
||||
const canOwn = p.postType === 'FEED' && p.authorId === myUserId;
|
||||
const editBtn = canOwn ? `<button class="post-action-btn" onclick="event.stopPropagation();startFeedEdit('${p.postId}')" title="Bearbeiten" style="color:var(--color-muted)">✏</button>` : '';
|
||||
const deleteBtn = canOwn ? `<button class="post-action-btn post-delete" onclick="event.stopPropagation();deletePost('${p.postId}')">🗑</button>` : '';
|
||||
const meldenBtn = p.authorId !== myUserId ? `<button class="post-action-btn" onclick="event.stopPropagation();openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>` : '';
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
const hasTarget = !!p.targetUrl;
|
||||
const cardClick = hasTarget ? `window.location.href='${p.targetUrl}'` : `openLb('${p.postId}','${p.postType}')`;
|
||||
const commentBtn = hasTarget ? '' : `<button class="post-action-btn" onclick="event.stopPropagation();openLb('${p.postId}','${p.postType}')">💬 <span id="kc-${p.postId}">${p.kommentarCount}</span></button>`;
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}">
|
||||
<div class="post-header">
|
||||
<div class="post-avatar"><a href="${authorUrl}" onclick="event.stopPropagation()" style="display:contents;">${avatarHtml}</a></div>
|
||||
<div>
|
||||
<div class="post-author"><a href="${authorUrl}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${esc(p.authorName)}</a>${privacyLabel}</div>
|
||||
<div class="post-meta" id="pm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
|
||||
</div>
|
||||
${(editBtn||deleteBtn) ? `<div style="margin-left:auto;display:flex;gap:0.25rem;">${editBtn}${deleteBtn}</div>` : ''}
|
||||
</div>
|
||||
<div id="pva-${p.postId}">
|
||||
<div class="post-text">${renderTextWithHashtags(p.text)}</div>
|
||||
<div id="pbi-${p.postId}">${bilderGrid(p.bilder)}</div>
|
||||
</div>
|
||||
<div id="pea-${p.postId}" style="display:none;"></div>
|
||||
<div id="pum-${p.postId}">${umfrageHtml}</div>
|
||||
<div class="post-actions">
|
||||
<button class="post-action-btn${p.likedByMe?' active':''}" id="lk-${p.postId}" onclick="event.stopPropagation();likePost('${p.postId}','${p.postType}')">♥ <span id="lkc-${p.postId}">${p.likeCount}</span></button>
|
||||
${commentBtn}${meldenBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 => {
|
||||
// ── Umfrage-HTML (Feed-spezifisch, da Vote-Handler Seiten-State braucht) ──
|
||||
function buildUmfrageHtml(postId, optionen, myVoteOptionIds, onVoteAttrFn) {
|
||||
if (!optionen || !optionen.length) return '';
|
||||
const totalVotes = optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
||||
return '<div style="margin-top:0.5rem;">' + 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="event.stopPropagation(); votePost('${p.postId}','${o.optionId}','${tab}','${p.postType}')">
|
||||
const voted = myVoteOptionIds && myVoteOptionIds.includes(o.optionId);
|
||||
return `<div class="umfrage-option-bar${voted?' voted':''}" onclick="${onVoteAttrFn(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></div>`;
|
||||
}
|
||||
|
||||
const canDelete = p.postType === 'FEED' && p.authorId === myUserId;
|
||||
const deleteBtn = canDelete
|
||||
? `<button class="post-action-btn post-delete" onclick="event.stopPropagation(); deletePost('${p.postId}')">🗑</button>`
|
||||
: '';
|
||||
const meldenBtn = p.authorId !== myUserId
|
||||
? `<button class="post-action-btn" onclick="event.stopPropagation(); openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>`
|
||||
: '';
|
||||
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
const hasTarget = !!p.targetUrl;
|
||||
const cardClick = hasTarget
|
||||
? `window.location.href='${p.targetUrl}'`
|
||||
: `openLb('${p.postId}','${p.postType}')`;
|
||||
const commentBtn = hasTarget ? '' : `
|
||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
||||
</button>`;
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}" style="cursor:pointer;">
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avatarHtml}</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>${privacyLabel}</div>
|
||||
<div class="post-meta">${fmtDate(p.createdAt)}${groupBadge}</div>
|
||||
</div>
|
||||
${deleteBtn}
|
||||
</div>
|
||||
<div class="post-text">${renderTextWithHashtags(p.text)}</div>
|
||||
${bildHtml}
|
||||
${umfrageHtml}
|
||||
<div class="post-actions">
|
||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="lk-${p.postId}" onclick="event.stopPropagation(); likePost('${p.postId}','${p.postType}')">
|
||||
♥ <span id="lkc-${p.postId}">${p.likeCount}</span>
|
||||
</button>
|
||||
${commentBtn}
|
||||
${meldenBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
// ── Post-Bearbeitung (Feed) ──
|
||||
function startFeedEdit(postId) {
|
||||
startPostEdit({ postId, prefix: 'p', data: feedPostCache.get(postId), editBilderMap: feedEditBilder,
|
||||
saveFn: 'saveFeedEdit', cancelFn: 'cancelFeedEdit',
|
||||
addImgFn: 'feedEditAddImg', addOptionFn: 'feedEditAddOption', rmImgFn: 'feedEditRmImg' });
|
||||
}
|
||||
function cancelFeedEdit(postId) { cancelPostEdit(postId, 'p', feedEditBilder); }
|
||||
function feedEditRmImg(postId, idx) { feedEditBilder.get(postId).splice(idx, 1); _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg'); }
|
||||
function feedEditAddImg(input, postId) {
|
||||
[...input.files].forEach(f => processImageFile(f, feedEditBilder.get(postId), () => _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg')));
|
||||
input.value = '';
|
||||
}
|
||||
function feedEditAddOption(postId) { editAddOptionRow(`peo-${postId}`); }
|
||||
async function saveFeedEdit(postId) {
|
||||
const cached = feedPostCache.get(postId);
|
||||
await savePostEdit({ postId, prefix: 'p', endpoint: `/feed/posts/${postId}`,
|
||||
isUmfrage: cached?.beitragTyp === 'UMFRAGE', editBilderMap: feedEditBilder,
|
||||
onSuccess: updated => {
|
||||
feedPostCache.set(postId, { ...cached, text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice });
|
||||
applyPostEditDom(postId, 'p', updated,
|
||||
buildUmfrageHtml(postId, updated.optionen, cached?.myVoteOptionIds || [],
|
||||
(pid, oid) => `event.stopPropagation(); votePost('${pid}','${oid}','${cached?._tab}','FEED')`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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';
|
||||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
||||
addOption(); addOption();
|
||||
}
|
||||
function selectComposeBilder(input) {
|
||||
[...input.files].forEach(f => processImageFile(f, composeBilderArr, renderComposeThumbs));
|
||||
input.value = '';
|
||||
}
|
||||
function renderComposeThumbs() { renderBilderThumbs(composeBilderArr, 'composeThumbs', removeThumb); }
|
||||
function removeThumb(idx) { composeBilderArr.splice(idx, 1); renderComposeThumbs(); }
|
||||
|
||||
function toggleUmfrage(btn) {
|
||||
const opt = document.getElementById('umfrageOptions');
|
||||
const showing = opt.style.display !== 'none';
|
||||
opt.style.display = showing ? 'none' : '';
|
||||
if (btn) btn.classList.toggle('active', !showing);
|
||||
if (!showing && document.getElementById('optionList').children.length === 0) { addOption(); addOption(); }
|
||||
}
|
||||
function resetUmfrage() {
|
||||
document.getElementById('umfrageOptions').style.display = 'none';
|
||||
document.getElementById('optionList').innerHTML = '';
|
||||
document.getElementById('umfrageBtn').classList.remove('active');
|
||||
}
|
||||
function addOption() {
|
||||
const list = document.getElementById('optionList');
|
||||
const idx = list.children.length;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'umfrage-option-row';
|
||||
row.innerHTML = `<input type="text" placeholder="Option ${idx + 1}" maxlength="100">
|
||||
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);
|
||||
}
|
||||
|
||||
function selectComposeBilder(input) {
|
||||
[...input.files].forEach(f => { if (f.type.startsWith('image/')) processImageFile(f); });
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function processImageFile(file) {
|
||||
if (!file || !file.type.startsWith('image/')) return;
|
||||
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);
|
||||
const data = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];
|
||||
composeBilderArr.push(data);
|
||||
renderComposeThumbs();
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function renderComposeThumbs() {
|
||||
const container = document.getElementById('composeThumbs');
|
||||
container.innerHTML = '';
|
||||
composeBilderArr.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="removeThumb(${i})" title="Entfernen">✕</button>`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
container.style.display = composeBilderArr.length > 0 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function removeThumb(idx) {
|
||||
composeBilderArr.splice(idx, 1);
|
||||
renderComposeThumbs();
|
||||
}
|
||||
|
||||
// ── Drag & Drop ──
|
||||
const compose = document.getElementById('compose');
|
||||
compose.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
if ([...e.dataTransfer.items].some(i => i.type.startsWith('image/')))
|
||||
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));
|
||||
});
|
||||
// Drag & Drop Compose
|
||||
const _compose = document.getElementById('compose');
|
||||
_compose.addEventListener('dragover', e => { e.preventDefault(); if ([...e.dataTransfer.items].some(i=>i.type.startsWith('image/'))) _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,composeBilderArr,renderComposeThumbs)); });
|
||||
|
||||
async function submitPost() {
|
||||
const text = document.getElementById('composeText').value.trim();
|
||||
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
|
||||
if (!text && composeBilderArr.length === 0) return;
|
||||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked').value;
|
||||
const multiChoice = document.getElementById('multiChoice').checked;
|
||||
const isPublic = document.getElementById('isPublic').checked;
|
||||
|
||||
let optionen = [];
|
||||
if (beitragTyp === 'UMFRAGE') {
|
||||
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input'))
|
||||
.map(i => i.value.trim()).filter(v => v);
|
||||
if (hasUmfrage) {
|
||||
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input')).map(i=>i.value.trim()).filter(v=>v);
|
||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||
}
|
||||
|
||||
const res = await fetch('/feed/posts', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ beitragTyp, text, multiChoice, optionen, bilder: [...composeBilderArr], isPublic })
|
||||
body: JSON.stringify({ beitragTyp: hasUmfrage?'UMFRAGE':'TEXT', text, multiChoice, optionen, bilder:[...composeBilderArr], isPublic })
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const post = await res.json();
|
||||
|
||||
// Reset compose
|
||||
document.getElementById('composeText').value = '';
|
||||
composeBilderArr = [];
|
||||
renderComposeThumbs();
|
||||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
||||
toggleUmfrage();
|
||||
composeBilderArr = []; renderComposeThumbs(); resetUmfrage();
|
||||
document.getElementById('multiChoice').checked = false;
|
||||
document.getElementById('isPublic').checked = false;
|
||||
document.getElementById('optionList').innerHTML = '';
|
||||
|
||||
// Prepend to mine feed
|
||||
document.getElementById('mineEmpty').style.display = 'none';
|
||||
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
|
||||
|
||||
if (isPublic) {
|
||||
document.getElementById('publicEmpty').style.display = 'none';
|
||||
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
|
||||
@@ -492,18 +345,17 @@
|
||||
|
||||
// ── Like ──
|
||||
async function likePost(postId, postType) {
|
||||
let likeEndpoint;
|
||||
let ep;
|
||||
if (postType === 'GROUP') {
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
const gruppeId = card?.dataset?.gruppeId;
|
||||
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||
if (!gruppeId) return;
|
||||
likeEndpoint = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
||||
ep = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
||||
} else {
|
||||
likeEndpoint = `/feed/posts/${postId}/like`;
|
||||
ep = `/feed/posts/${postId}/like`;
|
||||
}
|
||||
await fetch(likeEndpoint, { method: 'POST' });
|
||||
const btn = document.getElementById('lk-' + postId);
|
||||
const lc = document.getElementById('lkc-' + postId);
|
||||
await fetch(ep, { method:'POST' });
|
||||
const btn = document.getElementById('lk-'+postId);
|
||||
const lc = document.getElementById('lkc-'+postId);
|
||||
const was = btn.classList.contains('active');
|
||||
btn.classList.toggle('active', !was);
|
||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||
@@ -512,115 +364,55 @@
|
||||
// ── Vote ──
|
||||
async function votePost(postId, optionId, tab, postType) {
|
||||
if (postType === 'GROUP') {
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
const gruppeId = card?.dataset?.gruppeId;
|
||||
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||
if (!gruppeId) return;
|
||||
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ optionId })
|
||||
});
|
||||
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||
} else {
|
||||
await fetch('/feed/posts/' + postId + '/vote', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ optionId })
|
||||
});
|
||||
await fetch(`/feed/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||
}
|
||||
reloadPost(postId, tab);
|
||||
}
|
||||
|
||||
async function reloadPost(postId, tab) {
|
||||
const state = feedState[tab];
|
||||
state.page = 0; state.hasMore = true; state.loaded = false;
|
||||
document.getElementById(tab + 'Feed').innerHTML = '';
|
||||
document.getElementById(tab + 'Empty').style.display = 'none';
|
||||
document.getElementById(tab+'Feed').innerHTML = '';
|
||||
document.getElementById(tab+'Empty').style.display = 'none';
|
||||
await loadFeed(tab);
|
||||
}
|
||||
|
||||
// ── Delete ──
|
||||
async function deletePost(postId) {
|
||||
if (!confirm('Post löschen?')) return;
|
||||
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
document.getElementById('pc-' + postId)?.remove();
|
||||
}
|
||||
const res = await fetch('/feed/posts/' + postId, { method:'DELETE' });
|
||||
if (res.ok) document.getElementById('pc-'+postId)?.remove();
|
||||
}
|
||||
|
||||
// ── Lightbox ──
|
||||
// ── Lightbox (openLb bleibt lokal, da Seiten-Cache nötig) ──
|
||||
function openLb(postId, postType) {
|
||||
activeLbPostId = postId;
|
||||
activeLbPostType = postType;
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
if (card) {
|
||||
const clone = card.cloneNode(true);
|
||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||
_lbSetupContent(postId, 'p', feedPostCache.get(postId)?.bilder);
|
||||
}
|
||||
loadLbComments(postId, postType);
|
||||
document.getElementById('postLightbox').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function openLbWithData(p) {
|
||||
activeLbPostId = p.postId;
|
||||
activeLbPostType = p.postType || 'FEED';
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = renderPostCard(p, 'mine');
|
||||
const card = tempDiv.firstElementChild;
|
||||
if (card) {
|
||||
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
||||
_lbSetupContent(p.postId, 'p', p.bilder);
|
||||
}
|
||||
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');
|
||||
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';
|
||||
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('');
|
||||
}
|
||||
|
||||
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);
|
||||
const kcEl = document.getElementById('kc-' + activeLbPostId);
|
||||
if (kcEl) kcEl.textContent = parseInt(kcEl.textContent) + 1;
|
||||
}
|
||||
|
||||
// renderKommentarHtml und toggleKommentarLike kommen aus shared.js
|
||||
|
||||
async function deleteKommentar(kommentarId, targetType, targetId) {
|
||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||||
await loadLbComments(targetId, activeLbPostType);
|
||||
}
|
||||
|
||||
// toggleEmojiPicker, insertEmoji kommen aus shared.js
|
||||
|
||||
// esc, fmtDate kommen aus shared.js
|
||||
document.getElementById('postLightbox').addEventListener('click', e => { if (e.target === document.getElementById('postLightbox')) closeLb(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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
bin/main/static/css/community.css
Normal file
104
bin/main/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>
|
||||
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>
|
||||
<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 id="hpva-${p.postId}">
|
||||
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||
${bildHtml}
|
||||
${umfrageHtml}
|
||||
<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');
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.springframework.data.domain.Slice;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import de.oaa.xxx.feed.dto.FeedItemDto;
|
||||
import de.oaa.xxx.feed.dto.FeedPostRequest;
|
||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
||||
import de.oaa.xxx.feed.entity.PosterType;
|
||||
import de.oaa.xxx.hashtag.HashtagService;
|
||||
import de.oaa.xxx.hashtag.PostHashtagEntity;
|
||||
import de.oaa.xxx.feed.entity.FeedPostOptionEntity;
|
||||
@@ -46,6 +48,11 @@ import de.oaa.xxx.gruppe.repository.GruppenbeitragRepository;
|
||||
import de.oaa.xxx.gruppe.repository.GruppenmitgliedRepository;
|
||||
import de.oaa.xxx.gruppe.repository.UmfrageOptionRepository;
|
||||
import de.oaa.xxx.gruppe.repository.UmfrageStimmeRepository;
|
||||
import de.oaa.xxx.location.entity.LocationEntity;
|
||||
import de.oaa.xxx.location.entity.LocationFollowEntity;
|
||||
import de.oaa.xxx.location.repository.LocationAdminRepository;
|
||||
import de.oaa.xxx.location.repository.LocationFollowRepository;
|
||||
import de.oaa.xxx.location.repository.LocationRepository;
|
||||
import de.oaa.xxx.social.LikeService;
|
||||
import de.oaa.xxx.social.entity.FriendshipEntity;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
@@ -76,6 +83,9 @@ public class FeedController {
|
||||
private final UserService userService;
|
||||
private final LikeService likeService;
|
||||
private final HashtagService hashtagService;
|
||||
private final LocationRepository locationRepository;
|
||||
private final LocationFollowRepository locationFollowRepository;
|
||||
private final LocationAdminRepository locationAdminRepository;
|
||||
|
||||
public FeedController(FeedPostRepository feedPostRepository,
|
||||
FeedPostLikeRepository feedPostLikeRepository,
|
||||
@@ -92,7 +102,10 @@ public class FeedController {
|
||||
UserRepository userRepository,
|
||||
UserService userService,
|
||||
LikeService likeService,
|
||||
HashtagService hashtagService) {
|
||||
HashtagService hashtagService,
|
||||
LocationRepository locationRepository,
|
||||
LocationFollowRepository locationFollowRepository,
|
||||
LocationAdminRepository locationAdminRepository) {
|
||||
this.feedPostRepository = feedPostRepository;
|
||||
this.feedPostLikeRepository = feedPostLikeRepository;
|
||||
this.feedPostOptionRepository = feedPostOptionRepository;
|
||||
@@ -109,10 +122,15 @@ public class FeedController {
|
||||
this.userService = userService;
|
||||
this.likeService = likeService;
|
||||
this.hashtagService = hashtagService;
|
||||
this.locationRepository = locationRepository;
|
||||
this.locationFollowRepository = locationFollowRepository;
|
||||
this.locationAdminRepository = locationAdminRepository;
|
||||
}
|
||||
|
||||
record FeedPage(List<FeedItemDto> posts, boolean hasMore) {}
|
||||
record VoteRequest(UUID optionId) {}
|
||||
record UpdateOptionRequest(UUID optionId, String text) {}
|
||||
record UpdatePostRequest(String text, List<String> bilder, List<UpdateOptionRequest> optionen, Boolean multiChoice) {}
|
||||
|
||||
// ── POST /feed/posts ──
|
||||
|
||||
@@ -132,6 +150,7 @@ public class FeedController {
|
||||
FeedPostEntity post = new FeedPostEntity();
|
||||
post.setPostId(UUID.randomUUID());
|
||||
post.setAuthorId(myId);
|
||||
post.setPosterType(PosterType.USER);
|
||||
post.setText(req.text().trim());
|
||||
post.setBeitragTyp(typ);
|
||||
post.setMultiChoice(typ == BeitragTyp.UMFRAGE ? req.multiChoice() : null);
|
||||
@@ -158,6 +177,61 @@ public class FeedController {
|
||||
return ResponseEntity.status(201).body(toFeedItemDtoFromPost(post, myId));
|
||||
}
|
||||
|
||||
// ── POST /feed/location/{locationId}/posts ──
|
||||
|
||||
@PostMapping("/location/{locationId}/posts")
|
||||
public ResponseEntity<FeedItemDto> createLocationPost(@PathVariable("locationId") UUID locationId,
|
||||
@RequestBody FeedPostRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
var locOpt = locationRepository.findById(locationId);
|
||||
if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
|
||||
boolean isAdmin = locOpt.get().getOwnerId().equals(myId)
|
||||
|| locationAdminRepository.existsByLocationIdAndUserId(locationId, myId);
|
||||
if (!isAdmin) return ResponseEntity.status(403).build();
|
||||
|
||||
if (req.text() == null || req.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
BeitragTyp typ;
|
||||
try {
|
||||
typ = BeitragTyp.valueOf(req.beitragTyp());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
FeedPostEntity post = new FeedPostEntity();
|
||||
post.setPostId(UUID.randomUUID());
|
||||
post.setAuthorId(locationId);
|
||||
post.setPosterType(PosterType.LOCATION);
|
||||
post.setText(req.text().trim());
|
||||
post.setBeitragTyp(typ);
|
||||
post.setMultiChoice(typ == BeitragTyp.UMFRAGE ? req.multiChoice() : null);
|
||||
post.setBilder(req.bilder() != null ? req.bilder() : List.of());
|
||||
post.setPublic(true);
|
||||
post.setCreatedAt(LocalDateTime.now());
|
||||
feedPostRepository.save(post);
|
||||
hashtagService.saveForPost(post.getText(), "FEED", post.getPostId(), post.getCreatedAt());
|
||||
LOGGER.info("Location {} hat Feed-Post {} erstellt (Typ: {})", locationId, post.getPostId(), typ);
|
||||
|
||||
if (typ == BeitragTyp.UMFRAGE && req.optionen() != null) {
|
||||
for (int i = 0; i < req.optionen().size(); i++) {
|
||||
String optText = req.optionen().get(i);
|
||||
if (optText == null || optText.isBlank()) continue;
|
||||
FeedPostOptionEntity opt = new FeedPostOptionEntity();
|
||||
opt.setOptionId(UUID.randomUUID());
|
||||
opt.setPostId(post.getPostId());
|
||||
opt.setText(optText.trim());
|
||||
opt.setReihenfolge(i);
|
||||
feedPostOptionRepository.save(opt);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.status(201).body(toFeedItemDtoFromPost(post, myId));
|
||||
}
|
||||
|
||||
// ── GET /feed/mine ──
|
||||
|
||||
@GetMapping("/mine")
|
||||
@@ -183,9 +257,15 @@ public class FeedController {
|
||||
.map(m -> m.getGruppeId())
|
||||
.toList();
|
||||
|
||||
// Collect followed location IDs
|
||||
List<UUID> followedLocationIds = locationFollowRepository.findByUserId(myId)
|
||||
.stream()
|
||||
.map(LocationFollowEntity::getLocationId)
|
||||
.toList();
|
||||
|
||||
LocalDateTime since = LocalDateTime.now().minusDays(90);
|
||||
|
||||
// Fetch feed posts from friends + self
|
||||
// Fetch feed posts from friends + self (USER type)
|
||||
List<FeedPostEntity> feedPosts = feedPostRepository
|
||||
.findByAuthorIdInAndCreatedAtAfterOrderByCreatedAtDesc(authorIds, since);
|
||||
|
||||
@@ -193,10 +273,18 @@ public class FeedController {
|
||||
List<GruppenbeitragEntity> gruppePosts = gruppeIds.isEmpty() ? List.of() :
|
||||
gruppenbeitragRepository.findByGruppeIdInAndCreatedAtAfterOrderByCreatedAtDesc(gruppeIds, since);
|
||||
|
||||
// Fetch location posts from followed locations
|
||||
List<FeedPostEntity> locationPosts = followedLocationIds.isEmpty() ? List.of() :
|
||||
feedPostRepository.findByAuthorIdInAndPosterTypeAndCreatedAtAfterOrderByCreatedAtDesc(
|
||||
followedLocationIds, PosterType.LOCATION, since);
|
||||
|
||||
// Merge, convert, sort
|
||||
List<FeedItemDto> merged = Stream.concat(
|
||||
Stream.concat(
|
||||
feedPosts.stream().map(p -> toFeedItemDtoFromPost(p, myId)),
|
||||
gruppePosts.stream().map(b -> toFeedItemDtoFromGruppe(b, myId))
|
||||
),
|
||||
locationPosts.stream().map(p -> toFeedItemDtoFromPost(p, myId))
|
||||
).sorted(Comparator.comparing(FeedItemDto::createdAt).reversed()).toList();
|
||||
|
||||
int from = page * size;
|
||||
@@ -260,6 +348,32 @@ public class FeedController {
|
||||
return ResponseEntity.ok(new FeedPage(items, !nextPage.isEmpty()));
|
||||
}
|
||||
|
||||
// ── GET /feed/location/{locationId} ──
|
||||
|
||||
@GetMapping("/location/{locationId}")
|
||||
public ResponseEntity<FeedPage> getLocationFeed(@PathVariable("locationId") UUID locationId,
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "10") int size,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (!locationRepository.existsById(locationId)) return ResponseEntity.notFound().build();
|
||||
|
||||
PageRequest pageable = PageRequest.of(page, size);
|
||||
List<FeedPostEntity> posts = feedPostRepository
|
||||
.findByAuthorIdAndPosterTypeOrderByCreatedAtDesc(locationId, PosterType.LOCATION, pageable);
|
||||
|
||||
PageRequest nextPageable = PageRequest.of(page + 1, size);
|
||||
List<FeedPostEntity> nextPage = feedPostRepository
|
||||
.findByAuthorIdAndPosterTypeOrderByCreatedAtDesc(locationId, PosterType.LOCATION, nextPageable);
|
||||
|
||||
List<FeedItemDto> items = posts.stream()
|
||||
.map(p -> toFeedItemDtoFromPost(p, myId))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(new FeedPage(items, !nextPage.isEmpty()));
|
||||
}
|
||||
|
||||
// ── GET /feed/hashtag?tag= ──
|
||||
|
||||
@GetMapping("/hashtag")
|
||||
@@ -287,9 +401,11 @@ public class FeedController {
|
||||
for (PostHashtagEntity ref : refs) {
|
||||
if ("FEED".equals(ref.getPostType())) {
|
||||
feedPostRepository.findById(ref.getPostId()).ifPresent(p -> {
|
||||
PosterType pt = p.getPosterType() != null ? p.getPosterType() : PosterType.USER;
|
||||
boolean visible = p.isPublic()
|
||||
|| p.getAuthorId().equals(myId)
|
||||
|| friendIds.contains(p.getAuthorId());
|
||||
|| friendIds.contains(p.getAuthorId())
|
||||
|| pt == PosterType.LOCATION;
|
||||
if (visible) all.add(toFeedItemDtoFromPost(p, myId));
|
||||
});
|
||||
} else if ("GROUP".equals(ref.getPostType())) {
|
||||
@@ -361,6 +477,73 @@ public class FeedController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── PUT /feed/posts/{id} ──
|
||||
|
||||
@PutMapping("/posts/{id}")
|
||||
public ResponseEntity<FeedItemDto> updatePost(@PathVariable("id") UUID id,
|
||||
@RequestBody UpdatePostRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (req.text() == null || req.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
var postOpt = feedPostRepository.findById(id);
|
||||
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
FeedPostEntity post = postOpt.get();
|
||||
|
||||
if (!canModifyPost(post, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
hashtagService.deleteForPost("FEED", id);
|
||||
post.setText(req.text().trim());
|
||||
post.setBilder(req.bilder() != null ? req.bilder() : List.of());
|
||||
post.setEditedAt(LocalDateTime.now());
|
||||
if (post.getBeitragTyp() == BeitragTyp.UMFRAGE && req.multiChoice() != null) {
|
||||
post.setMultiChoice(req.multiChoice());
|
||||
}
|
||||
feedPostRepository.save(post);
|
||||
hashtagService.saveForPost(post.getText(), "FEED", post.getPostId(), post.getCreatedAt());
|
||||
|
||||
if (req.optionen() != null && post.getBeitragTyp() == BeitragTyp.UMFRAGE) {
|
||||
List<FeedPostOptionEntity> existingOpts = feedPostOptionRepository.findByPostIdOrderByReihenfolge(id);
|
||||
Set<UUID> requestedIds = req.optionen().stream()
|
||||
.filter(o -> o.optionId() != null)
|
||||
.map(UpdateOptionRequest::optionId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (FeedPostOptionEntity existing : existingOpts) {
|
||||
if (!requestedIds.contains(existing.getOptionId())) {
|
||||
feedPostVoteRepository.deleteByOptionId(existing.getOptionId());
|
||||
feedPostOptionRepository.delete(existing);
|
||||
}
|
||||
}
|
||||
|
||||
int reihenfolge = 0;
|
||||
for (var optReq : req.optionen()) {
|
||||
if (optReq.text() == null || optReq.text().isBlank()) continue;
|
||||
final int r = reihenfolge++;
|
||||
if (optReq.optionId() != null) {
|
||||
feedPostOptionRepository.findById(optReq.optionId()).ifPresent(opt -> {
|
||||
if (opt.getPostId().equals(id)) {
|
||||
opt.setText(optReq.text().trim());
|
||||
opt.setReihenfolge(r);
|
||||
feedPostOptionRepository.save(opt);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
FeedPostOptionEntity newOpt = new FeedPostOptionEntity();
|
||||
newOpt.setOptionId(UUID.randomUUID());
|
||||
newOpt.setPostId(id);
|
||||
newOpt.setText(optReq.text().trim());
|
||||
newOpt.setReihenfolge(r);
|
||||
feedPostOptionRepository.save(newOpt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("User {} hat Feed-Post {} bearbeitet", myId, id);
|
||||
return ResponseEntity.ok(toFeedItemDtoFromPost(post, myId));
|
||||
}
|
||||
|
||||
// ── DELETE /feed/posts/{id} ──
|
||||
|
||||
@DeleteMapping("/posts/{id}")
|
||||
@@ -372,7 +555,7 @@ public class FeedController {
|
||||
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
FeedPostEntity post = postOpt.get();
|
||||
|
||||
if (!post.getAuthorId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (!canModifyPost(post, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
hashtagService.deleteForPost("FEED", id);
|
||||
feedPostVoteRepository.deleteByPostId(id);
|
||||
@@ -388,13 +571,43 @@ public class FeedController {
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private boolean canModifyPost(FeedPostEntity post, UUID myId) {
|
||||
if (post.getAuthorId().equals(myId)) return true;
|
||||
PosterType pt = post.getPosterType() != null ? post.getPosterType() : PosterType.USER;
|
||||
if (pt == PosterType.LOCATION) {
|
||||
return locationRepository.findById(post.getAuthorId())
|
||||
.map(l -> l.getOwnerId().equals(myId)
|
||||
|| locationAdminRepository.existsByLocationIdAndUserId(post.getAuthorId(), myId))
|
||||
.orElse(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private UUID resolveMyId(Principal principal) {
|
||||
if (principal == null) return null;
|
||||
return userService.requireUser(principal).getUserId();
|
||||
}
|
||||
|
||||
private FeedItemDto toFeedItemDtoFromPost(FeedPostEntity p, UUID myId) {
|
||||
PosterType pt = p.getPosterType() != null ? p.getPosterType() : PosterType.USER;
|
||||
|
||||
String authorName;
|
||||
String authorPicture;
|
||||
UUID locationId = null;
|
||||
String locationName = null;
|
||||
|
||||
if (pt == PosterType.LOCATION) {
|
||||
LocationEntity loc = locationRepository.findById(p.getAuthorId()).orElse(null);
|
||||
authorName = loc != null ? loc.getName() : "Unbekannt";
|
||||
authorPicture = loc != null ? loc.getProfilePictureLq() : null;
|
||||
locationId = p.getAuthorId();
|
||||
locationName = authorName;
|
||||
} else {
|
||||
UserEntity author = userRepository.findById(p.getAuthorId()).orElse(null);
|
||||
authorName = author != null ? author.getName() : "Unbekannt";
|
||||
authorPicture = author != null ? author.getProfilePicture() : null;
|
||||
}
|
||||
|
||||
long likeCount = feedPostLikeRepository.countByPostId(p.getPostId());
|
||||
boolean likedByMe = feedPostLikeRepository.findByPostIdAndUserId(p.getPostId(), myId).isPresent();
|
||||
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("FEED_POST", p.getPostId());
|
||||
@@ -417,14 +630,18 @@ public class FeedController {
|
||||
p.getPostId(), "FEED",
|
||||
null, null,
|
||||
p.getAuthorId(),
|
||||
author != null ? author.getName() : "Unbekannt",
|
||||
author != null ? author.getProfilePicture() : null,
|
||||
authorName,
|
||||
authorPicture,
|
||||
p.getBeitragTyp().name(), p.getText(), p.getMultiChoice(), p.getBilder(),
|
||||
p.getCreatedAt(),
|
||||
likeCount, likedByMe, kommentarCount,
|
||||
optionen, myVoteOptionIds,
|
||||
p.isPublic(),
|
||||
p.getTargetUrl()
|
||||
p.getTargetUrl(),
|
||||
p.getEditedAt(),
|
||||
pt.name(),
|
||||
locationId,
|
||||
locationName
|
||||
);
|
||||
}
|
||||
|
||||
@@ -462,6 +679,10 @@ public class FeedController {
|
||||
likeCount, likedByMe, kommentarCount,
|
||||
optionen, myVoteOptionIds,
|
||||
false,
|
||||
null,
|
||||
b.getEditedAt(),
|
||||
"USER",
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,5 +25,9 @@ public record FeedItemDto(
|
||||
List<UmfrageOptionDto> optionen,
|
||||
List<UUID> myVoteOptionIds,
|
||||
boolean isPublic,
|
||||
String targetUrl
|
||||
String targetUrl,
|
||||
LocalDateTime editedAt,
|
||||
String posterType, // "USER" | "LOCATION"
|
||||
UUID locationId,
|
||||
String locationName
|
||||
) {}
|
||||
|
||||
@@ -30,6 +30,10 @@ public class FeedPostEntity {
|
||||
@Column(name = "bild", columnDefinition = "MEDIUMTEXT")
|
||||
private List<String> bilder;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10, columnDefinition = "VARCHAR(10) DEFAULT 'USER'")
|
||||
private PosterType posterType = PosterType.USER;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10)
|
||||
private BeitragTyp beitragTyp;
|
||||
@@ -43,6 +47,9 @@ public class FeedPostEntity {
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column
|
||||
private LocalDateTime editedAt;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String targetUrl;
|
||||
}
|
||||
|
||||
5
src/main/java/de/oaa/xxx/feed/entity/PosterType.java
Normal file
5
src/main/java/de/oaa/xxx/feed/entity/PosterType.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.feed.entity;
|
||||
|
||||
public enum PosterType {
|
||||
USER, LOCATION
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.oaa.xxx.feed.repository;
|
||||
|
||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
||||
import de.oaa.xxx.feed.entity.PosterType;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
@@ -20,6 +21,10 @@ public interface FeedPostRepository extends JpaRepository<FeedPostEntity, UUID>
|
||||
|
||||
List<FeedPostEntity> findByAuthorIdOrderByCreatedAtDesc(UUID authorId, Pageable pageable);
|
||||
|
||||
List<FeedPostEntity> findByAuthorIdAndPosterTypeOrderByCreatedAtDesc(UUID authorId, PosterType posterType, Pageable pageable);
|
||||
|
||||
List<FeedPostEntity> findByAuthorIdInAndPosterTypeAndCreatedAtAfterOrderByCreatedAtDesc(List<UUID> authorIds, PosterType posterType, LocalDateTime since);
|
||||
|
||||
@Transactional
|
||||
void deleteByAuthorId(UUID authorId);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ public interface FeedPostVoteRepository extends JpaRepository<FeedPostVoteEntity
|
||||
|
||||
long countByOptionId(UUID optionId);
|
||||
|
||||
@Transactional
|
||||
void deleteByOptionId(UUID optionId);
|
||||
|
||||
@Transactional
|
||||
void deleteByPostId(UUID postId);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
public class GruppenbeitragController {
|
||||
@@ -65,6 +66,8 @@ public class GruppenbeitragController {
|
||||
}
|
||||
|
||||
record CreateBeitragRequest(String beitragTyp, String text, Boolean multiChoice, List<String> optionen, List<String> bilder) {}
|
||||
record UpdateOptionRequest(UUID optionId, String text) {}
|
||||
record UpdateBeitragRequest(String text, List<String> bilder, List<UpdateOptionRequest> optionen, Boolean multiChoice) {}
|
||||
record PostsPage(List<GruppenbeitragDto> posts, boolean hasMore) {}
|
||||
record VoteRequest(UUID optionId) {}
|
||||
record ReportRequest(String grund) {}
|
||||
@@ -153,6 +156,74 @@ public class GruppenbeitragController {
|
||||
return ResponseEntity.status(201).body(toDto(beitrag, myId));
|
||||
}
|
||||
|
||||
// ── PUT /gruppen/{id}/posts/{postId} ──
|
||||
|
||||
@PutMapping("/gruppen/{id}/posts/{postId}")
|
||||
public ResponseEntity<GruppenbeitragDto> updatePost(@PathVariable("id") UUID id,
|
||||
@PathVariable("postId") UUID postId,
|
||||
@RequestBody UpdateBeitragRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (req.text() == null || req.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
var bOpt = beitragRepository.findById(postId);
|
||||
if (bOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
GruppenbeitragEntity beitrag = bOpt.get();
|
||||
|
||||
if (!beitrag.getAuthorId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
hashtagService.deleteForPost("GROUP", postId);
|
||||
beitrag.setText(req.text().trim());
|
||||
beitrag.setBilder(req.bilder() != null ? req.bilder() : List.of());
|
||||
beitrag.setEditedAt(LocalDateTime.now());
|
||||
if (beitrag.getBeitragTyp() == BeitragTyp.UMFRAGE && req.multiChoice() != null) {
|
||||
beitrag.setMultiChoice(req.multiChoice());
|
||||
}
|
||||
beitragRepository.save(beitrag);
|
||||
hashtagService.saveForPost(beitrag.getText(), "GROUP", beitrag.getBeitragId(), beitrag.getCreatedAt());
|
||||
|
||||
if (req.optionen() != null && beitrag.getBeitragTyp() == BeitragTyp.UMFRAGE) {
|
||||
List<UmfrageOptionEntity> existingOpts = optionRepository.findByBeitragIdOrderByReihenfolge(postId);
|
||||
Set<UUID> requestedIds = req.optionen().stream()
|
||||
.filter(o -> o.optionId() != null)
|
||||
.map(UpdateOptionRequest::optionId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (UmfrageOptionEntity existing : existingOpts) {
|
||||
if (!requestedIds.contains(existing.getOptionId())) {
|
||||
stimmeRepository.deleteByOptionId(existing.getOptionId());
|
||||
optionRepository.delete(existing);
|
||||
}
|
||||
}
|
||||
|
||||
int reihenfolge = 0;
|
||||
for (var optReq : req.optionen()) {
|
||||
if (optReq.text() == null || optReq.text().isBlank()) continue;
|
||||
final int r = reihenfolge++;
|
||||
if (optReq.optionId() != null) {
|
||||
optionRepository.findById(optReq.optionId()).ifPresent(opt -> {
|
||||
if (opt.getBeitragId().equals(postId)) {
|
||||
opt.setText(optReq.text().trim());
|
||||
opt.setReihenfolge(r);
|
||||
optionRepository.save(opt);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
UmfrageOptionEntity newOpt = new UmfrageOptionEntity();
|
||||
newOpt.setOptionId(UUID.randomUUID());
|
||||
newOpt.setBeitragId(postId);
|
||||
newOpt.setText(optReq.text().trim());
|
||||
newOpt.setReihenfolge(r);
|
||||
optionRepository.save(newOpt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("User {} hat Beitrag {} in Gruppe {} bearbeitet", myId, postId, id);
|
||||
return ResponseEntity.ok(toDto(beitrag, myId));
|
||||
}
|
||||
|
||||
// ── DELETE /gruppen/{id}/posts/{postId} ──
|
||||
|
||||
@DeleteMapping("/gruppen/{id}/posts/{postId}")
|
||||
@@ -354,6 +425,7 @@ public class GruppenbeitragController {
|
||||
author != null ? author.getName() : "Unbekannt",
|
||||
author != null ? author.getProfilePicture() : null,
|
||||
b.getBeitragTyp().name(), b.getText(), b.getMultiChoice(), b.getBilder(), b.getCreatedAt(),
|
||||
likeCount, likedByMe, kommentarCount, optionen, myVoteOptionIds, reported);
|
||||
likeCount, likedByMe, kommentarCount, optionen, myVoteOptionIds, reported,
|
||||
b.getEditedAt());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@ public record GruppenbeitragDto(
|
||||
long kommentarCount,
|
||||
List<UmfrageOptionDto> optionen,
|
||||
List<UUID> myVoteOptionIds,
|
||||
boolean reported
|
||||
boolean reported,
|
||||
LocalDateTime editedAt
|
||||
) {}
|
||||
|
||||
@@ -42,4 +42,7 @@ public class GruppenbeitragEntity {
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column
|
||||
private LocalDateTime editedAt;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ public interface UmfrageStimmeRepository extends JpaRepository<UmfrageStimmeEnti
|
||||
|
||||
long countByOptionId(UUID optionId);
|
||||
|
||||
@Transactional
|
||||
void deleteByOptionId(UUID optionId);
|
||||
|
||||
@Transactional
|
||||
void deleteByBeitragId(UUID beitragId);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import de.oaa.xxx.location.repository.LocationEventRepository;
|
||||
import de.oaa.xxx.location.repository.LocationFollowRepository;
|
||||
import de.oaa.xxx.location.repository.LocationRepository;
|
||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
||||
import de.oaa.xxx.feed.entity.PosterType;
|
||||
import de.oaa.xxx.feed.repository.FeedPostRepository;
|
||||
import de.oaa.xxx.gruppe.BeitragTyp;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
@@ -168,13 +169,13 @@ public class LocationEventController {
|
||||
|
||||
// Feed-Post automatisch anlegen
|
||||
try {
|
||||
String locationName = locOpt.get().getName();
|
||||
String dateStr = event.getStartAt().format(
|
||||
java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy 'um' HH:mm 'Uhr'"));
|
||||
FeedPostEntity feedPost = new FeedPostEntity();
|
||||
feedPost.setPostId(UUID.randomUUID());
|
||||
feedPost.setAuthorId(myId);
|
||||
feedPost.setText("📍 Neue Veranstaltung bei " + locationName + ": \"" + event.getTitle() + "\" - " + dateStr);
|
||||
feedPost.setAuthorId(locationId);
|
||||
feedPost.setPosterType(PosterType.LOCATION);
|
||||
feedPost.setText("📍 Neue Veranstaltung: \"" + event.getTitle() + "\" – " + dateStr);
|
||||
feedPost.setBilder(event.getImageData() != null ? List.of(event.getImageData()) : List.of());
|
||||
feedPost.setBeitragTyp(BeitragTyp.TEXT);
|
||||
feedPost.setPublic(true);
|
||||
@@ -218,6 +219,25 @@ public class LocationEventController {
|
||||
}
|
||||
eventRepo.save(event);
|
||||
|
||||
// Feed-Post für Aktualisierung anlegen
|
||||
try {
|
||||
String dateStr = event.getStartAt().format(
|
||||
java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy 'um' HH:mm 'Uhr'"));
|
||||
FeedPostEntity feedPost = new FeedPostEntity();
|
||||
feedPost.setPostId(UUID.randomUUID());
|
||||
feedPost.setAuthorId(locationId);
|
||||
feedPost.setPosterType(PosterType.LOCATION);
|
||||
feedPost.setText("📅 Veranstaltung aktualisiert: \"" + event.getTitle() + "\" – " + dateStr);
|
||||
feedPost.setBilder(event.getImageData() != null ? List.of(event.getImageData()) : List.of());
|
||||
feedPost.setBeitragTyp(BeitragTyp.TEXT);
|
||||
feedPost.setPublic(true);
|
||||
feedPost.setCreatedAt(LocalDateTime.now());
|
||||
feedPost.setTargetUrl("/community/event-detail.html?id=" + event.getEventId());
|
||||
feedPostRepo.save(feedPost);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.warn("Feed-Post für Event-Update {} konnte nicht angelegt werden: {}", event.getEventId(), ex.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(toDetail(event, locOpt.get().getName(), myId));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 id="ppva-${p.postId}">
|
||||
<div class="post-text">${esc(p.text)}</div>
|
||||
${bildHtml}
|
||||
${umfrageHtml}
|
||||
<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);
|
||||
|
||||
@@ -7,135 +7,49 @@
|
||||
<title>Feed – 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; }
|
||||
|
||||
.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; 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; }
|
||||
.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-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-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; }
|
||||
|
||||
/* Carousel – Stile kommen aus shared.js */
|
||||
|
||||
.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; }
|
||||
|
||||
.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; }
|
||||
|
||||
.post-card { cursor:pointer; }
|
||||
.hashtag-banner { display:flex; align-items:center; gap:0.75rem; margin-bottom:1.25rem; padding:0.65rem 1rem; background:var(--color-card); border:1px solid var(--color-secondary); border-radius:8px; }
|
||||
.hashtag-banner-tag { font-size:1.05rem; font-weight:700; color:var(--color-primary); }
|
||||
.hashtag-banner-back { margin-left:auto; font-size:0.82rem; color:var(--color-muted); text-decoration:none; }
|
||||
.hashtag-banner-back:hover { color:var(--color-primary); }
|
||||
|
||||
/* 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-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%; } }
|
||||
|
||||
/* Comment + Like-Stile kommen aus shared.js */
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<!-- Hashtag-Banner (nur sichtbar wenn ?tag=… gesetzt) -->
|
||||
<div class="hashtag-banner" id="hashtagBanner" style="display:none;">
|
||||
<span>Posts mit </span><span class="hashtag-banner-tag" id="hashtagBannerLabel"></span>
|
||||
<a class="hashtag-banner-back" href="/community/feed.html">× Zurück zum Feed</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="feedTabs">
|
||||
<button class="tab-btn active" id="tabMine" data-tab="mine" onclick="switchTab('mine', this)">Mein Feed</button>
|
||||
<button class="tab-btn" id="tabPublic" data-tab="public" onclick="switchTab('public', this)">Öffentlicher Feed</button>
|
||||
<button class="tab-btn active" data-tab="mine" onclick="switchTab('mine',this)">Mein Feed</button>
|
||||
<button class="tab-btn" data-tab="public" onclick="switchTab('public',this)">Öffentlicher Feed</button>
|
||||
</div>
|
||||
|
||||
<!-- Mein Feed -->
|
||||
<div class="tab-panel active" id="tab-mine">
|
||||
<div class="post-compose" id="compose">
|
||||
<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">
|
||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
||||
<label class="multi-toggle" id="multiChoiceRow" style="display:none;">
|
||||
<input type="checkbox" id="multiChoice"> Multi-Choice
|
||||
</label>
|
||||
<label class="privacy-toggle">
|
||||
<input type="checkbox" id="isPublic"> Öffentlich
|
||||
</label>
|
||||
</div>
|
||||
<label class="privacy-toggle"><input type="checkbox" id="isPublic"> Öffentlich</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">📷
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji">😊</button>
|
||||
<label class="compose-action-btn" title="Fotos">📷
|
||||
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
||||
</label>
|
||||
<button onclick="submitPost()" style="width:auto; margin:0;">Veröffentlichen</button>
|
||||
<button type="button" id="umfrageBtn" class="compose-action-btn" onclick="toggleUmfrage(this)" title="Umfrage">📊</button>
|
||||
<button onclick="submitPost()" style="width:auto;margin:0;">Veröffentlichen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,24 +58,21 @@
|
||||
<div class="sentinel" id="mineSentinel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Öffentlicher Feed -->
|
||||
<div class="tab-panel" id="tab-public">
|
||||
<div id="publicFeed"></div>
|
||||
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
|
||||
<div class="sentinel" id="publicSentinel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hashtag-Feed (wird angezeigt wenn ?tag=… gesetzt) -->
|
||||
<div id="tab-hashtag" style="display:none;">
|
||||
<div id="hashtagFeed"></div>
|
||||
<p class="empty-hint" id="hashtagEmpty" style="display:none;">Keine Posts mit diesem Hashtag.</p>
|
||||
<div class="sentinel" id="hashtagSentinel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Lightbox -->
|
||||
<!-- Lightbox (IDs geteilt mit shared.js) -->
|
||||
<div class="lightbox" id="postLightbox">
|
||||
<div class="lb-layout">
|
||||
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||
@@ -190,9 +101,7 @@
|
||||
<script>
|
||||
// ── State ──
|
||||
let myUserId = null;
|
||||
let activeLbPostId = null;
|
||||
let activeLbPostType = null;
|
||||
let activeHashtag = null; // set when ?tag=... is in URL
|
||||
let activeHashtag = null;
|
||||
|
||||
const feedState = {
|
||||
mine: { page:0, hasMore:true, loading:false, loaded:false },
|
||||
@@ -200,9 +109,11 @@
|
||||
hashtag: { page:0, hasMore:true, loading:false, loaded:false }
|
||||
};
|
||||
|
||||
const feedPostCache = new Map();
|
||||
const feedEditBilder = new Map();
|
||||
let composeBilderArr = [];
|
||||
|
||||
// ── Hashtag-Modus prüfen ──
|
||||
// ── Hashtag-Modus ──
|
||||
const _urlTag = new URLSearchParams(window.location.search).get('tag');
|
||||
if (_urlTag) {
|
||||
activeHashtag = _urlTag.replace(/^#/, '').toLowerCase();
|
||||
@@ -219,6 +130,7 @@
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||
if (user) {
|
||||
myUserId = user.userId;
|
||||
initLb(myUserId);
|
||||
if (activeHashtag) {
|
||||
await loadFeed('hashtag');
|
||||
} else {
|
||||
@@ -234,18 +146,11 @@
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// ── Autocomplete für Compose ──
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ta = document.getElementById('composeText');
|
||||
if (ta) attachHashtagAutocomplete(ta);
|
||||
});
|
||||
// Fallback falls DOMContentLoaded bereits gefeuert
|
||||
if (document.readyState !== 'loading') {
|
||||
const ta = document.getElementById('composeText');
|
||||
if (ta) attachHashtagAutocomplete(ta);
|
||||
}
|
||||
// Hashtag-Autocomplete
|
||||
if (document.readyState !== 'loading') attachHashtagAutocomplete(document.getElementById('composeText'));
|
||||
else document.addEventListener('DOMContentLoaded', () => attachHashtagAutocomplete(document.getElementById('composeText')));
|
||||
|
||||
// ── Tab switching ──
|
||||
// ── Tabs ──
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
@@ -254,39 +159,27 @@
|
||||
localStorage.setItem('tab_feed', name);
|
||||
if (!feedState[name].loaded) loadFeed(name);
|
||||
}
|
||||
const _savedFeedTab = localStorage.getItem('tab_feed');
|
||||
if (_savedFeedTab) {
|
||||
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedFeedTab}"]`);
|
||||
if (_btn) switchTab(_savedFeedTab, _btn);
|
||||
}
|
||||
const _savedTab = localStorage.getItem('tab_feed');
|
||||
if (_savedTab) { const _b = document.querySelector(`.tab-btn[data-tab="${_savedTab}"]`); if (_b) switchTab(_savedTab, _b); }
|
||||
|
||||
// ── Feed loading ──
|
||||
// ── Feed laden ──
|
||||
async function loadFeed(tab) {
|
||||
const state = feedState[tab];
|
||||
if (state.loading || !state.hasMore) return;
|
||||
state.loading = true;
|
||||
state.loaded = true;
|
||||
state.loading = true; state.loaded = true;
|
||||
try {
|
||||
let url;
|
||||
if (tab === 'hashtag') {
|
||||
url = `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`;
|
||||
} else {
|
||||
const base = tab === 'mine' ? '/feed/mine' : '/feed/public';
|
||||
url = `${base}?page=${state.page}&size=10`;
|
||||
}
|
||||
const url = tab === 'hashtag'
|
||||
? `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`
|
||||
: `${tab === 'mine' ? '/feed/mine' : '/feed/public'}?page=${state.page}&size=10`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const feedEl = document.getElementById(tab + 'Feed');
|
||||
if (state.page === 0 && data.posts.length === 0) {
|
||||
document.getElementById(tab + 'Empty').style.display = '';
|
||||
}
|
||||
if (state.page === 0 && data.posts.length === 0) document.getElementById(tab + 'Empty').style.display = '';
|
||||
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
|
||||
state.hasMore = data.hasMore;
|
||||
state.page++;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
} finally { state.loading = false; }
|
||||
}
|
||||
|
||||
// ── Infinite Scroll ──
|
||||
@@ -302,188 +195,148 @@
|
||||
observer.observe(document.getElementById('publicSentinel'));
|
||||
observer.observe(document.getElementById('hashtagSentinel'));
|
||||
|
||||
// bilderCarousel und carNav kommen aus shared.js
|
||||
|
||||
// ── Render post card ──
|
||||
// ── Post-Card rendern ──
|
||||
function renderPostCard(p, tab) {
|
||||
const avatarHtml = p.authorPicture
|
||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
||||
: '◉';
|
||||
feedPostCache.set(p.postId, { text: p.text, bilder: p.bilder || [], beitragTyp: p.beitragTyp, optionen: p.optionen || [], myVoteOptionIds: p.myVoteOptionIds || [], multiChoice: p.multiChoice, _tab: tab });
|
||||
const isLocPost = p.posterType === 'LOCATION';
|
||||
const authorUrl = isLocPost
|
||||
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
|
||||
: `/community/benutzer.html?userId=${p.authorId}`;
|
||||
const avatarHtml = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : (isLocPost ? '📍' : '◉');
|
||||
const privacyLabel = !p.isPublic ? ' <span style="font-size:0.7rem;color:var(--color-muted);">🔒</span>' : '';
|
||||
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
||||
? `<span class="gruppe-badge" onclick="event.stopPropagation()">👥 <a href="/community/gruppe.html?id=${p.gruppeId}" onclick="event.stopPropagation()">${esc(p.gruppeName)}</a></span>`
|
||||
: '';
|
||||
const bildHtml = bilderCarousel(p.bilder, p.postId);
|
||||
const editedLabel = p.editedAt ? ` <span style="font-size:0.75rem;color:var(--color-muted);">(bearbeitet)</span>` : '';
|
||||
const umfrageHtml = buildUmfrageHtml(p.postId, p.optionen, p.myVoteOptionIds,
|
||||
(postId, optionId) => `event.stopPropagation(); votePost('${postId}','${optionId}','${tab}','${p.postType}')`);
|
||||
const canOwn = p.postType === 'FEED' && p.authorId === myUserId;
|
||||
const editBtn = canOwn ? `<button class="post-action-btn" onclick="event.stopPropagation();startFeedEdit('${p.postId}')" title="Bearbeiten" style="color:var(--color-muted)">✏</button>` : '';
|
||||
const deleteBtn = canOwn ? `<button class="post-action-btn post-delete" onclick="event.stopPropagation();deletePost('${p.postId}')">🗑</button>` : '';
|
||||
const meldenBtn = p.authorId !== myUserId ? `<button class="post-action-btn" onclick="event.stopPropagation();openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>` : '';
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
const hasTarget = !!p.targetUrl;
|
||||
const cardClick = hasTarget ? `window.location.href='${p.targetUrl}'` : `openLb('${p.postId}','${p.postType}')`;
|
||||
const commentBtn = hasTarget ? '' : `<button class="post-action-btn" onclick="event.stopPropagation();openLb('${p.postId}','${p.postType}')">💬 <span id="kc-${p.postId}">${p.kommentarCount}</span></button>`;
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}">
|
||||
<div class="post-header">
|
||||
<div class="post-avatar"><a href="${authorUrl}" onclick="event.stopPropagation()" style="display:contents;">${avatarHtml}</a></div>
|
||||
<div>
|
||||
<div class="post-author"><a href="${authorUrl}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${esc(p.authorName)}</a>${privacyLabel}</div>
|
||||
<div class="post-meta" id="pm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
|
||||
</div>
|
||||
${(editBtn||deleteBtn) ? `<div style="margin-left:auto;display:flex;gap:0.25rem;">${editBtn}${deleteBtn}</div>` : ''}
|
||||
</div>
|
||||
<div id="pva-${p.postId}">
|
||||
<div class="post-text">${renderTextWithHashtags(p.text)}</div>
|
||||
<div id="pbi-${p.postId}">${bilderGrid(p.bilder)}</div>
|
||||
</div>
|
||||
<div id="pea-${p.postId}" style="display:none;"></div>
|
||||
<div id="pum-${p.postId}">${umfrageHtml}</div>
|
||||
<div class="post-actions">
|
||||
<button class="post-action-btn${p.likedByMe?' active':''}" id="lk-${p.postId}" onclick="event.stopPropagation();likePost('${p.postId}','${p.postType}')">♥ <span id="lkc-${p.postId}">${p.likeCount}</span></button>
|
||||
${commentBtn}${meldenBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 => {
|
||||
// ── Umfrage-HTML (Feed-spezifisch, da Vote-Handler Seiten-State braucht) ──
|
||||
function buildUmfrageHtml(postId, optionen, myVoteOptionIds, onVoteAttrFn) {
|
||||
if (!optionen || !optionen.length) return '';
|
||||
const totalVotes = optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
||||
return '<div style="margin-top:0.5rem;">' + 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="event.stopPropagation(); votePost('${p.postId}','${o.optionId}','${tab}','${p.postType}')">
|
||||
const voted = myVoteOptionIds && myVoteOptionIds.includes(o.optionId);
|
||||
return `<div class="umfrage-option-bar${voted?' voted':''}" onclick="${onVoteAttrFn(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></div>`;
|
||||
}
|
||||
|
||||
const canDelete = p.postType === 'FEED' && p.authorId === myUserId;
|
||||
const deleteBtn = canDelete
|
||||
? `<button class="post-action-btn post-delete" onclick="event.stopPropagation(); deletePost('${p.postId}')">🗑</button>`
|
||||
: '';
|
||||
const meldenBtn = p.authorId !== myUserId
|
||||
? `<button class="post-action-btn" onclick="event.stopPropagation(); openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>`
|
||||
: '';
|
||||
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
const hasTarget = !!p.targetUrl;
|
||||
const cardClick = hasTarget
|
||||
? `window.location.href='${p.targetUrl}'`
|
||||
: `openLb('${p.postId}','${p.postType}')`;
|
||||
const commentBtn = hasTarget ? '' : `
|
||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
||||
</button>`;
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}" style="cursor:pointer;">
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avatarHtml}</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>${privacyLabel}</div>
|
||||
<div class="post-meta">${fmtDate(p.createdAt)}${groupBadge}</div>
|
||||
</div>
|
||||
${deleteBtn}
|
||||
</div>
|
||||
<div class="post-text">${renderTextWithHashtags(p.text)}</div>
|
||||
${bildHtml}
|
||||
${umfrageHtml}
|
||||
<div class="post-actions">
|
||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="lk-${p.postId}" onclick="event.stopPropagation(); likePost('${p.postId}','${p.postType}')">
|
||||
♥ <span id="lkc-${p.postId}">${p.likeCount}</span>
|
||||
</button>
|
||||
${commentBtn}
|
||||
${meldenBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
// ── Post-Bearbeitung (Feed) ──
|
||||
function startFeedEdit(postId) {
|
||||
startPostEdit({ postId, prefix: 'p', data: feedPostCache.get(postId), editBilderMap: feedEditBilder,
|
||||
saveFn: 'saveFeedEdit', cancelFn: 'cancelFeedEdit',
|
||||
addImgFn: 'feedEditAddImg', addOptionFn: 'feedEditAddOption', rmImgFn: 'feedEditRmImg' });
|
||||
}
|
||||
function cancelFeedEdit(postId) { cancelPostEdit(postId, 'p', feedEditBilder); }
|
||||
function feedEditRmImg(postId, idx) { feedEditBilder.get(postId).splice(idx, 1); _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg'); }
|
||||
function feedEditAddImg(input, postId) {
|
||||
[...input.files].forEach(f => processImageFile(f, feedEditBilder.get(postId), () => _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg')));
|
||||
input.value = '';
|
||||
}
|
||||
function feedEditAddOption(postId) { editAddOptionRow(`peo-${postId}`); }
|
||||
async function saveFeedEdit(postId) {
|
||||
const cached = feedPostCache.get(postId);
|
||||
await savePostEdit({ postId, prefix: 'p', endpoint: `/feed/posts/${postId}`,
|
||||
isUmfrage: cached?.beitragTyp === 'UMFRAGE', editBilderMap: feedEditBilder,
|
||||
onSuccess: updated => {
|
||||
feedPostCache.set(postId, { ...cached, text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice });
|
||||
applyPostEditDom(postId, 'p', updated,
|
||||
buildUmfrageHtml(postId, updated.optionen, cached?.myVoteOptionIds || [],
|
||||
(pid, oid) => `event.stopPropagation(); votePost('${pid}','${oid}','${cached?._tab}','FEED')`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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';
|
||||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
||||
addOption(); addOption();
|
||||
}
|
||||
function selectComposeBilder(input) {
|
||||
[...input.files].forEach(f => processImageFile(f, composeBilderArr, renderComposeThumbs));
|
||||
input.value = '';
|
||||
}
|
||||
function renderComposeThumbs() { renderBilderThumbs(composeBilderArr, 'composeThumbs', removeThumb); }
|
||||
function removeThumb(idx) { composeBilderArr.splice(idx, 1); renderComposeThumbs(); }
|
||||
|
||||
function toggleUmfrage(btn) {
|
||||
const opt = document.getElementById('umfrageOptions');
|
||||
const showing = opt.style.display !== 'none';
|
||||
opt.style.display = showing ? 'none' : '';
|
||||
if (btn) btn.classList.toggle('active', !showing);
|
||||
if (!showing && document.getElementById('optionList').children.length === 0) { addOption(); addOption(); }
|
||||
}
|
||||
function resetUmfrage() {
|
||||
document.getElementById('umfrageOptions').style.display = 'none';
|
||||
document.getElementById('optionList').innerHTML = '';
|
||||
document.getElementById('umfrageBtn').classList.remove('active');
|
||||
}
|
||||
function addOption() {
|
||||
const list = document.getElementById('optionList');
|
||||
const idx = list.children.length;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'umfrage-option-row';
|
||||
row.innerHTML = `<input type="text" placeholder="Option ${idx + 1}" maxlength="100">
|
||||
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);
|
||||
}
|
||||
|
||||
function selectComposeBilder(input) {
|
||||
[...input.files].forEach(f => { if (f.type.startsWith('image/')) processImageFile(f); });
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function processImageFile(file) {
|
||||
if (!file || !file.type.startsWith('image/')) return;
|
||||
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);
|
||||
const data = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];
|
||||
composeBilderArr.push(data);
|
||||
renderComposeThumbs();
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function renderComposeThumbs() {
|
||||
const container = document.getElementById('composeThumbs');
|
||||
container.innerHTML = '';
|
||||
composeBilderArr.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="removeThumb(${i})" title="Entfernen">✕</button>`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
container.style.display = composeBilderArr.length > 0 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function removeThumb(idx) {
|
||||
composeBilderArr.splice(idx, 1);
|
||||
renderComposeThumbs();
|
||||
}
|
||||
|
||||
// ── Drag & Drop ──
|
||||
const compose = document.getElementById('compose');
|
||||
compose.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
if ([...e.dataTransfer.items].some(i => i.type.startsWith('image/')))
|
||||
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));
|
||||
});
|
||||
// Drag & Drop Compose
|
||||
const _compose = document.getElementById('compose');
|
||||
_compose.addEventListener('dragover', e => { e.preventDefault(); if ([...e.dataTransfer.items].some(i=>i.type.startsWith('image/'))) _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,composeBilderArr,renderComposeThumbs)); });
|
||||
|
||||
async function submitPost() {
|
||||
const text = document.getElementById('composeText').value.trim();
|
||||
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
|
||||
if (!text && composeBilderArr.length === 0) return;
|
||||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked').value;
|
||||
const multiChoice = document.getElementById('multiChoice').checked;
|
||||
const isPublic = document.getElementById('isPublic').checked;
|
||||
|
||||
let optionen = [];
|
||||
if (beitragTyp === 'UMFRAGE') {
|
||||
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input'))
|
||||
.map(i => i.value.trim()).filter(v => v);
|
||||
if (hasUmfrage) {
|
||||
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input')).map(i=>i.value.trim()).filter(v=>v);
|
||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||
}
|
||||
|
||||
const res = await fetch('/feed/posts', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ beitragTyp, text, multiChoice, optionen, bilder: [...composeBilderArr], isPublic })
|
||||
body: JSON.stringify({ beitragTyp: hasUmfrage?'UMFRAGE':'TEXT', text, multiChoice, optionen, bilder:[...composeBilderArr], isPublic })
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const post = await res.json();
|
||||
|
||||
// Reset compose
|
||||
document.getElementById('composeText').value = '';
|
||||
composeBilderArr = [];
|
||||
renderComposeThumbs();
|
||||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
||||
toggleUmfrage();
|
||||
composeBilderArr = []; renderComposeThumbs(); resetUmfrage();
|
||||
document.getElementById('multiChoice').checked = false;
|
||||
document.getElementById('isPublic').checked = false;
|
||||
document.getElementById('optionList').innerHTML = '';
|
||||
|
||||
// Prepend to mine feed
|
||||
document.getElementById('mineEmpty').style.display = 'none';
|
||||
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
|
||||
|
||||
if (isPublic) {
|
||||
document.getElementById('publicEmpty').style.display = 'none';
|
||||
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
|
||||
@@ -492,18 +345,17 @@
|
||||
|
||||
// ── Like ──
|
||||
async function likePost(postId, postType) {
|
||||
let likeEndpoint;
|
||||
let ep;
|
||||
if (postType === 'GROUP') {
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
const gruppeId = card?.dataset?.gruppeId;
|
||||
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||
if (!gruppeId) return;
|
||||
likeEndpoint = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
||||
ep = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
||||
} else {
|
||||
likeEndpoint = `/feed/posts/${postId}/like`;
|
||||
ep = `/feed/posts/${postId}/like`;
|
||||
}
|
||||
await fetch(likeEndpoint, { method: 'POST' });
|
||||
const btn = document.getElementById('lk-' + postId);
|
||||
const lc = document.getElementById('lkc-' + postId);
|
||||
await fetch(ep, { method:'POST' });
|
||||
const btn = document.getElementById('lk-'+postId);
|
||||
const lc = document.getElementById('lkc-'+postId);
|
||||
const was = btn.classList.contains('active');
|
||||
btn.classList.toggle('active', !was);
|
||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||
@@ -512,115 +364,55 @@
|
||||
// ── Vote ──
|
||||
async function votePost(postId, optionId, tab, postType) {
|
||||
if (postType === 'GROUP') {
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
const gruppeId = card?.dataset?.gruppeId;
|
||||
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||
if (!gruppeId) return;
|
||||
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ optionId })
|
||||
});
|
||||
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||
} else {
|
||||
await fetch('/feed/posts/' + postId + '/vote', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ optionId })
|
||||
});
|
||||
await fetch(`/feed/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||
}
|
||||
reloadPost(postId, tab);
|
||||
}
|
||||
|
||||
async function reloadPost(postId, tab) {
|
||||
const state = feedState[tab];
|
||||
state.page = 0; state.hasMore = true; state.loaded = false;
|
||||
document.getElementById(tab + 'Feed').innerHTML = '';
|
||||
document.getElementById(tab + 'Empty').style.display = 'none';
|
||||
document.getElementById(tab+'Feed').innerHTML = '';
|
||||
document.getElementById(tab+'Empty').style.display = 'none';
|
||||
await loadFeed(tab);
|
||||
}
|
||||
|
||||
// ── Delete ──
|
||||
async function deletePost(postId) {
|
||||
if (!confirm('Post löschen?')) return;
|
||||
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
document.getElementById('pc-' + postId)?.remove();
|
||||
}
|
||||
const res = await fetch('/feed/posts/' + postId, { method:'DELETE' });
|
||||
if (res.ok) document.getElementById('pc-'+postId)?.remove();
|
||||
}
|
||||
|
||||
// ── Lightbox ──
|
||||
// ── Lightbox (openLb bleibt lokal, da Seiten-Cache nötig) ──
|
||||
function openLb(postId, postType) {
|
||||
activeLbPostId = postId;
|
||||
activeLbPostType = postType;
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
if (card) {
|
||||
const clone = card.cloneNode(true);
|
||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||
_lbSetupContent(postId, 'p', feedPostCache.get(postId)?.bilder);
|
||||
}
|
||||
loadLbComments(postId, postType);
|
||||
document.getElementById('postLightbox').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function openLbWithData(p) {
|
||||
activeLbPostId = p.postId;
|
||||
activeLbPostType = p.postType || 'FEED';
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = renderPostCard(p, 'mine');
|
||||
const card = tempDiv.firstElementChild;
|
||||
if (card) {
|
||||
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
||||
_lbSetupContent(p.postId, 'p', p.bilder);
|
||||
}
|
||||
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');
|
||||
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';
|
||||
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('');
|
||||
}
|
||||
|
||||
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);
|
||||
const kcEl = document.getElementById('kc-' + activeLbPostId);
|
||||
if (kcEl) kcEl.textContent = parseInt(kcEl.textContent) + 1;
|
||||
}
|
||||
|
||||
// renderKommentarHtml und toggleKommentarLike kommen aus shared.js
|
||||
|
||||
async function deleteKommentar(kommentarId, targetType, targetId) {
|
||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||||
await loadLbComments(targetId, activeLbPostType);
|
||||
}
|
||||
|
||||
// toggleEmojiPicker, insertEmoji kommen aus shared.js
|
||||
|
||||
// esc, fmtDate kommen aus shared.js
|
||||
document.getElementById('postLightbox').addEventListener('click', e => { if (e.target === document.getElementById('postLightbox')) closeLb(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
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>
|
||||
<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 id="hpva-${p.postId}">
|
||||
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||
${bildHtml}
|
||||
${umfrageHtml}
|
||||
<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