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>
|
<title>Profil – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<style>
|
||||||
/* ── Profile Header ── */
|
/* ── Profile Header ── */
|
||||||
.profil-header {
|
.profil-header {
|
||||||
@@ -487,50 +488,13 @@
|
|||||||
.vorliebe-chip.bw-EHER_NICHT { border-color:#fb8c00; color:#fb8c00; }
|
.vorliebe-chip.bw-EHER_NICHT { border-color:#fb8c00; color:#fb8c00; }
|
||||||
.vorliebe-chip.bw-GEHT_GAR_NICHT { border-color:#e53935; color:#e53935; }
|
.vorliebe-chip.bw-GEHT_GAR_NICHT { border-color:#e53935; color:#e53935; }
|
||||||
|
|
||||||
/* ── Post cards (profile posts tab) ── */
|
/* ── Post-Bild-Wrap (Profil-spezifisch) ── */
|
||||||
.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 { position:relative; cursor:pointer; display:block; }
|
.post-bild-wrap { position:relative; cursor:pointer; display:block; }
|
||||||
.post-bild-wrap:hover .post-bild { opacity:0.82; }
|
.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-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-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; }
|
/* ── Bild-Navigation in Lightbox ── */
|
||||||
.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; }
|
|
||||||
.lb-img-nav { display:flex; gap:0.75rem; align-items:center; justify-content:center; margin-top:0.75rem; flex-wrap:wrap; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="app">
|
<body class="app">
|
||||||
@@ -652,8 +616,9 @@
|
|||||||
<script src="/js/shared.js"></script>
|
<script src="/js/shared.js"></script>
|
||||||
<script src="/js/image-viewer.js"></script>
|
<script src="/js/image-viewer.js"></script>
|
||||||
<script src="/js/icons.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/social-sidebar.js"></script>
|
||||||
|
<script src="/js/hashtag.js"></script>
|
||||||
<script src="/js/meldung.js"></script>
|
<script src="/js/meldung.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── State ──
|
// ── State ──
|
||||||
@@ -680,6 +645,9 @@
|
|||||||
let activeLbPostId = null;
|
let activeLbPostId = null;
|
||||||
let activeLbMode = null; // 'post' | 'image'
|
let activeLbMode = null; // 'post' | 'image'
|
||||||
let activeLbImageIdx = null;
|
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 ──
|
// ── Label maps ──
|
||||||
const GESCHLECHT_LABEL = { WEIBLICH: 'weiblich', DIVERS: 'divers', MAENNLICH: 'männlich' };
|
const GESCHLECHT_LABEL = { WEIBLICH: 'weiblich', DIVERS: 'divers', MAENNLICH: 'männlich' };
|
||||||
@@ -719,6 +687,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
myUserId = me ? me.userId : null;
|
myUserId = me ? me.userId : null;
|
||||||
|
initLb(myUserId);
|
||||||
isOwnProfile = !previewMode && me && me.userId === profile.userId;
|
isOwnProfile = !previewMode && me && me.userId === profile.userId;
|
||||||
profileData = profile;
|
profileData = profile;
|
||||||
allImages = images;
|
allImages = images;
|
||||||
@@ -1248,6 +1217,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderLbImageBody() {
|
function renderLbImageBody() {
|
||||||
|
document.querySelector('#postLightbox .lb-layout')?.classList.remove('lb-text-only');
|
||||||
const img = allImages[activeLbImageIdx];
|
const img = allImages[activeLbImageIdx];
|
||||||
const total = allImages.length;
|
const total = allImages.length;
|
||||||
const prevDisabled = activeLbImageIdx === 0 ? 'disabled' : '';
|
const prevDisabled = activeLbImageIdx === 0 ? 'disabled' : '';
|
||||||
@@ -1589,42 +1559,48 @@
|
|||||||
const avatarHtml = p.authorPicture
|
const avatarHtml = p.authorPicture
|
||||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
||||||
: '◉';
|
: '◉';
|
||||||
const bildRaw = bilderCarousel(p.bilder);
|
profilPostCache[p.postId] = p;
|
||||||
const bildHtml = bildRaw
|
profilPostBilder.set(p.postId, p.bilder || []);
|
||||||
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}</div>`
|
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 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 = '';
|
let umfrageHtml = '';
|
||||||
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
||||||
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 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 pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
|
||||||
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
|
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
|
||||||
return `<div class="umfrage-option-bar${voted ? ' voted' : ''}" onclick="voteProfilPost('${p.postId}','${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-fill" style="width:${pct}%"></div>
|
||||||
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
||||||
</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 isOwn = p.authorId === myUserId;
|
||||||
const deleteBtn = canDelete
|
const rightBtns = isOwn
|
||||||
? `<button class="post-action-btn post-delete" onclick="deleteProfilPost('${p.postId}')">🗑</button>`
|
? `<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}">
|
return `<div class="post-card" id="pp-${p.postId}">
|
||||||
<div class="post-header">
|
<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>
|
||||||
<div class="post-author">${esc(p.authorName)}${privacyLabel}</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">${fmtDate(p.createdAt)}</div>
|
<div class="post-date" id="ppm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
${deleteBtn}
|
${rightBtns}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="ppva-${p.postId}">
|
||||||
<div class="post-text">${esc(p.text)}</div>
|
<div class="post-text">${esc(p.text)}</div>
|
||||||
${bildHtml}
|
<div id="ppbi-${p.postId}" class="post-bild-wrap" data-post-id="${p.postId}">${bildHtml}</div>
|
||||||
${umfrageHtml}
|
</div>
|
||||||
|
<div id="ppea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="ppum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="pp-like-${p.postId}" onclick="likeProfilPost('${p.postId}')">
|
<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>
|
♥ <span id="pp-lc-${p.postId}">${p.likeCount}</span>
|
||||||
@@ -1665,6 +1641,73 @@
|
|||||||
if (res.ok) document.getElementById('pp-' + postId)?.remove();
|
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) ──
|
// ── Lightbox (Post + Bild) ──
|
||||||
function openPostLb(postId) {
|
function openPostLb(postId) {
|
||||||
activeLbMode = 'post';
|
activeLbMode = 'post';
|
||||||
@@ -1674,7 +1717,9 @@
|
|||||||
if (card) {
|
if (card) {
|
||||||
const clone = card.cloneNode(true);
|
const clone = card.cloneNode(true);
|
||||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
|
clone.querySelectorAll('[id^="ppea-"]').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||||
|
_lbSetupContent(postId, 'pp', profilPostBilder.get(postId) || []);
|
||||||
}
|
}
|
||||||
loadLbComments();
|
loadLbComments();
|
||||||
document.getElementById('postLightbox').classList.add('open');
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
@@ -1728,7 +1773,6 @@
|
|||||||
if (e.target === document.getElementById('postLightbox')) closeLb();
|
if (e.target === document.getElementById('postLightbox')) closeLb();
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Escape' && document.getElementById('postLightbox').classList.contains('open')) closeLb();
|
|
||||||
if (activeLbMode === 'image') {
|
if (activeLbMode === 'image') {
|
||||||
if (e.key === 'ArrowLeft') lbGalleryNav(-1);
|
if (e.key === 'ArrowLeft') lbGalleryNav(-1);
|
||||||
if (e.key === 'ArrowRight') lbGalleryNav(1);
|
if (e.key === 'ArrowRight') lbGalleryNav(1);
|
||||||
|
|||||||
@@ -7,135 +7,49 @@
|
|||||||
<title>Feed – xXx Sphere</title>
|
<title>Feed – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<style>
|
||||||
.tabs { display:flex; gap:0; margin-bottom:1.5rem; border-bottom:1px solid var(--color-secondary); }
|
.post-card { cursor:pointer; }
|
||||||
.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; }
|
|
||||||
|
|
||||||
.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 { 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-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 { margin-left:auto; font-size:0.82rem; color:var(--color-muted); text-decoration:none; }
|
||||||
.hashtag-banner-back:hover { color:var(--color-primary); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="app">
|
<body class="app">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<!-- Hashtag-Banner (nur sichtbar wenn ?tag=… gesetzt) -->
|
|
||||||
<div class="hashtag-banner" id="hashtagBanner" style="display:none;">
|
<div class="hashtag-banner" id="hashtagBanner" style="display:none;">
|
||||||
<span>Posts mit </span><span class="hashtag-banner-tag" id="hashtagBannerLabel"></span>
|
<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>
|
<a class="hashtag-banner-back" href="/community/feed.html">× Zurück zum Feed</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs" id="feedTabs">
|
<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 active" 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" data-tab="public" onclick="switchTab('public',this)">Öffentlicher Feed</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mein Feed -->
|
|
||||||
<div class="tab-panel active" id="tab-mine">
|
<div class="tab-panel active" id="tab-mine">
|
||||||
<div class="post-compose" id="compose">
|
<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>
|
<textarea id="composeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
|
||||||
<div class="compose-thumbs" id="composeThumbs"></div>
|
<div class="compose-thumbs" id="composeThumbs"></div>
|
||||||
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
||||||
<div id="optionList"></div>
|
<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>
|
||||||
<div class="compose-footer">
|
<div class="compose-footer">
|
||||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
<label class="privacy-toggle"><input type="checkbox" id="isPublic"> Öffentlich</label>
|
||||||
<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>
|
|
||||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
<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>
|
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji">😊</button>
|
||||||
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
<label class="compose-action-btn" title="Fotos">📷
|
||||||
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
||||||
</label>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,24 +58,21 @@
|
|||||||
<div class="sentinel" id="mineSentinel"></div>
|
<div class="sentinel" id="mineSentinel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Öffentlicher Feed -->
|
|
||||||
<div class="tab-panel" id="tab-public">
|
<div class="tab-panel" id="tab-public">
|
||||||
<div id="publicFeed"></div>
|
<div id="publicFeed"></div>
|
||||||
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
|
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
|
||||||
<div class="sentinel" id="publicSentinel"></div>
|
<div class="sentinel" id="publicSentinel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hashtag-Feed (wird angezeigt wenn ?tag=… gesetzt) -->
|
|
||||||
<div id="tab-hashtag" style="display:none;">
|
<div id="tab-hashtag" style="display:none;">
|
||||||
<div id="hashtagFeed"></div>
|
<div id="hashtagFeed"></div>
|
||||||
<p class="empty-hint" id="hashtagEmpty" style="display:none;">Keine Posts mit diesem Hashtag.</p>
|
<p class="empty-hint" id="hashtagEmpty" style="display:none;">Keine Posts mit diesem Hashtag.</p>
|
||||||
<div class="sentinel" id="hashtagSentinel"></div>
|
<div class="sentinel" id="hashtagSentinel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Post Lightbox -->
|
<!-- Lightbox (IDs geteilt mit shared.js) -->
|
||||||
<div class="lightbox" id="postLightbox">
|
<div class="lightbox" id="postLightbox">
|
||||||
<div class="lb-layout">
|
<div class="lb-layout">
|
||||||
<button class="lb-close" onclick="closeLb()">✕</button>
|
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||||
@@ -190,9 +101,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// ── State ──
|
// ── State ──
|
||||||
let myUserId = null;
|
let myUserId = null;
|
||||||
let activeLbPostId = null;
|
let activeHashtag = null;
|
||||||
let activeLbPostType = null;
|
|
||||||
let activeHashtag = null; // set when ?tag=... is in URL
|
|
||||||
|
|
||||||
const feedState = {
|
const feedState = {
|
||||||
mine: { page:0, hasMore:true, loading:false, loaded:false },
|
mine: { page:0, hasMore:true, loading:false, loaded:false },
|
||||||
@@ -200,9 +109,11 @@
|
|||||||
hashtag: { page:0, hasMore:true, loading:false, loaded:false }
|
hashtag: { page:0, hasMore:true, loading:false, loaded:false }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const feedPostCache = new Map();
|
||||||
|
const feedEditBilder = new Map();
|
||||||
let composeBilderArr = [];
|
let composeBilderArr = [];
|
||||||
|
|
||||||
// ── Hashtag-Modus prüfen ──
|
// ── Hashtag-Modus ──
|
||||||
const _urlTag = new URLSearchParams(window.location.search).get('tag');
|
const _urlTag = new URLSearchParams(window.location.search).get('tag');
|
||||||
if (_urlTag) {
|
if (_urlTag) {
|
||||||
activeHashtag = _urlTag.replace(/^#/, '').toLowerCase();
|
activeHashtag = _urlTag.replace(/^#/, '').toLowerCase();
|
||||||
@@ -219,6 +130,7 @@
|
|||||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
myUserId = user.userId;
|
myUserId = user.userId;
|
||||||
|
initLb(myUserId);
|
||||||
if (activeHashtag) {
|
if (activeHashtag) {
|
||||||
await loadFeed('hashtag');
|
await loadFeed('hashtag');
|
||||||
} else {
|
} else {
|
||||||
@@ -234,18 +146,11 @@
|
|||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
// ── Autocomplete für Compose ──
|
// Hashtag-Autocomplete
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
if (document.readyState !== 'loading') attachHashtagAutocomplete(document.getElementById('composeText'));
|
||||||
const ta = document.getElementById('composeText');
|
else document.addEventListener('DOMContentLoaded', () => attachHashtagAutocomplete(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tab switching ──
|
// ── Tabs ──
|
||||||
function switchTab(name, btn) {
|
function switchTab(name, btn) {
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
@@ -254,39 +159,27 @@
|
|||||||
localStorage.setItem('tab_feed', name);
|
localStorage.setItem('tab_feed', name);
|
||||||
if (!feedState[name].loaded) loadFeed(name);
|
if (!feedState[name].loaded) loadFeed(name);
|
||||||
}
|
}
|
||||||
const _savedFeedTab = localStorage.getItem('tab_feed');
|
const _savedTab = localStorage.getItem('tab_feed');
|
||||||
if (_savedFeedTab) {
|
if (_savedTab) { const _b = document.querySelector(`.tab-btn[data-tab="${_savedTab}"]`); if (_b) switchTab(_savedTab, _b); }
|
||||||
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedFeedTab}"]`);
|
|
||||||
if (_btn) switchTab(_savedFeedTab, _btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Feed loading ──
|
// ── Feed laden ──
|
||||||
async function loadFeed(tab) {
|
async function loadFeed(tab) {
|
||||||
const state = feedState[tab];
|
const state = feedState[tab];
|
||||||
if (state.loading || !state.hasMore) return;
|
if (state.loading || !state.hasMore) return;
|
||||||
state.loading = true;
|
state.loading = true; state.loaded = true;
|
||||||
state.loaded = true;
|
|
||||||
try {
|
try {
|
||||||
let url;
|
const url = tab === 'hashtag'
|
||||||
if (tab === 'hashtag') {
|
? `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`
|
||||||
url = `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`;
|
: `${tab === 'mine' ? '/feed/mine' : '/feed/public'}?page=${state.page}&size=10`;
|
||||||
} else {
|
|
||||||
const base = tab === 'mine' ? '/feed/mine' : '/feed/public';
|
|
||||||
url = `${base}?page=${state.page}&size=10`;
|
|
||||||
}
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const feedEl = document.getElementById(tab + 'Feed');
|
const feedEl = document.getElementById(tab + 'Feed');
|
||||||
if (state.page === 0 && data.posts.length === 0) {
|
if (state.page === 0 && data.posts.length === 0) document.getElementById(tab + 'Empty').style.display = '';
|
||||||
document.getElementById(tab + 'Empty').style.display = '';
|
|
||||||
}
|
|
||||||
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
|
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
|
||||||
state.hasMore = data.hasMore;
|
state.hasMore = data.hasMore;
|
||||||
state.page++;
|
state.page++;
|
||||||
} finally {
|
} finally { state.loading = false; }
|
||||||
state.loading = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Infinite Scroll ──
|
// ── Infinite Scroll ──
|
||||||
@@ -302,188 +195,148 @@
|
|||||||
observer.observe(document.getElementById('publicSentinel'));
|
observer.observe(document.getElementById('publicSentinel'));
|
||||||
observer.observe(document.getElementById('hashtagSentinel'));
|
observer.observe(document.getElementById('hashtagSentinel'));
|
||||||
|
|
||||||
// bilderCarousel und carNav kommen aus shared.js
|
// ── Post-Card rendern ──
|
||||||
|
|
||||||
// ── Render post card ──
|
|
||||||
function renderPostCard(p, tab) {
|
function renderPostCard(p, tab) {
|
||||||
const avatarHtml = p.authorPicture
|
feedPostCache.set(p.postId, { text: p.text, bilder: p.bilder || [], beitragTyp: p.beitragTyp, optionen: p.optionen || [], myVoteOptionIds: p.myVoteOptionIds || [], multiChoice: p.multiChoice, _tab: tab });
|
||||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
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 privacyLabel = !p.isPublic ? ' <span style="font-size:0.7rem;color:var(--color-muted);">🔒</span>' : '';
|
||||||
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
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>`
|
? `<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 = '';
|
// ── Umfrage-HTML (Feed-spezifisch, da Vote-Handler Seiten-State braucht) ──
|
||||||
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
function buildUmfrageHtml(postId, optionen, myVoteOptionIds, onVoteAttrFn) {
|
||||||
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
if (!optionen || !optionen.length) return '';
|
||||||
umfrageHtml = '<div style="margin-top:0.5rem;">' + p.optionen.map(o => {
|
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 pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
|
||||||
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
|
const voted = myVoteOptionIds && myVoteOptionIds.includes(o.optionId);
|
||||||
return `<div class="umfrage-option-bar${voted ? ' voted' : ''}" onclick="event.stopPropagation(); votePost('${p.postId}','${o.optionId}','${tab}','${p.postType}')">
|
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-fill" style="width:${pct}%"></div>
|
||||||
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
||||||
</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;
|
// ── Post-Bearbeitung (Feed) ──
|
||||||
const deleteBtn = canDelete
|
function startFeedEdit(postId) {
|
||||||
? `<button class="post-action-btn post-delete" onclick="event.stopPropagation(); deletePost('${p.postId}')">🗑</button>`
|
startPostEdit({ postId, prefix: 'p', data: feedPostCache.get(postId), editBilderMap: feedEditBilder,
|
||||||
: '';
|
saveFn: 'saveFeedEdit', cancelFn: 'cancelFeedEdit',
|
||||||
const meldenBtn = p.authorId !== myUserId
|
addImgFn: 'feedEditAddImg', addOptionFn: 'feedEditAddOption', rmImgFn: 'feedEditRmImg' });
|
||||||
? `<button class="post-action-btn" onclick="event.stopPropagation(); openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>`
|
}
|
||||||
: '';
|
function cancelFeedEdit(postId) { cancelPostEdit(postId, 'p', feedEditBilder); }
|
||||||
|
function feedEditRmImg(postId, idx) { feedEditBilder.get(postId).splice(idx, 1); _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg'); }
|
||||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
function feedEditAddImg(input, postId) {
|
||||||
const hasTarget = !!p.targetUrl;
|
[...input.files].forEach(f => processImageFile(f, feedEditBilder.get(postId), () => _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg')));
|
||||||
const cardClick = hasTarget
|
input.value = '';
|
||||||
? `window.location.href='${p.targetUrl}'`
|
}
|
||||||
: `openLb('${p.postId}','${p.postType}')`;
|
function feedEditAddOption(postId) { editAddOptionRow(`peo-${postId}`); }
|
||||||
const commentBtn = hasTarget ? '' : `
|
async function saveFeedEdit(postId) {
|
||||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
const cached = feedPostCache.get(postId);
|
||||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
await savePostEdit({ postId, prefix: 'p', endpoint: `/feed/posts/${postId}`,
|
||||||
</button>`;
|
isUmfrage: cached?.beitragTyp === 'UMFRAGE', editBilderMap: feedEditBilder,
|
||||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}" style="cursor:pointer;">
|
onSuccess: updated => {
|
||||||
<div class="post-header">
|
feedPostCache.set(postId, { ...cached, text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice });
|
||||||
<div class="post-avatar">${avatarHtml}</div>
|
applyPostEditDom(postId, 'p', updated,
|
||||||
<div>
|
buildUmfrageHtml(postId, updated.optionen, cached?.myVoteOptionIds || [],
|
||||||
<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>
|
(pid, oid) => `event.stopPropagation(); votePost('${pid}','${oid}','${cached?._tab}','FEED')`));
|
||||||
<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>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Compose ──
|
// ── Compose ──
|
||||||
function toggleUmfrage() {
|
function selectComposeBilder(input) {
|
||||||
const isUmfrage = document.querySelector('input[name="beitragTyp"]:checked').value === 'UMFRAGE';
|
[...input.files].forEach(f => processImageFile(f, composeBilderArr, renderComposeThumbs));
|
||||||
document.getElementById('umfrageOptions').style.display = isUmfrage ? '' : 'none';
|
input.value = '';
|
||||||
document.getElementById('multiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
|
||||||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
|
||||||
addOption(); addOption();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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() {
|
function addOption() {
|
||||||
const list = document.getElementById('optionList');
|
const list = document.getElementById('optionList');
|
||||||
const idx = list.children.length;
|
const row = document.createElement('div'); row.className = 'umfrage-option-row';
|
||||||
const row = document.createElement('div');
|
row.innerHTML = `<input type="text" placeholder="Option ${list.children.length + 1}" maxlength="100">
|
||||||
row.className = 'umfrage-option-row';
|
|
||||||
row.innerHTML = `<input type="text" placeholder="Option ${idx + 1}" maxlength="100">
|
|
||||||
<button onclick="this.parentElement.remove()">✕</button>`;
|
<button onclick="this.parentElement.remove()">✕</button>`;
|
||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectComposeBilder(input) {
|
// Drag & Drop Compose
|
||||||
[...input.files].forEach(f => { if (f.type.startsWith('image/')) processImageFile(f); });
|
const _compose = document.getElementById('compose');
|
||||||
input.value = '';
|
_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)); });
|
||||||
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));
|
|
||||||
});
|
|
||||||
|
|
||||||
async function submitPost() {
|
async function submitPost() {
|
||||||
const text = document.getElementById('composeText').value.trim();
|
const text = document.getElementById('composeText').value.trim();
|
||||||
|
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
|
||||||
if (!text && composeBilderArr.length === 0) return;
|
if (!text && composeBilderArr.length === 0) return;
|
||||||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked').value;
|
|
||||||
const multiChoice = document.getElementById('multiChoice').checked;
|
const multiChoice = document.getElementById('multiChoice').checked;
|
||||||
const isPublic = document.getElementById('isPublic').checked;
|
const isPublic = document.getElementById('isPublic').checked;
|
||||||
|
|
||||||
let optionen = [];
|
let optionen = [];
|
||||||
if (beitragTyp === 'UMFRAGE') {
|
if (hasUmfrage) {
|
||||||
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input'))
|
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input')).map(i=>i.value.trim()).filter(v=>v);
|
||||||
.map(i => i.value.trim()).filter(v => v);
|
|
||||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch('/feed/posts', {
|
const res = await fetch('/feed/posts', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
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;
|
if (!res.ok) return;
|
||||||
const post = await res.json();
|
const post = await res.json();
|
||||||
|
|
||||||
// Reset compose
|
|
||||||
document.getElementById('composeText').value = '';
|
document.getElementById('composeText').value = '';
|
||||||
composeBilderArr = [];
|
composeBilderArr = []; renderComposeThumbs(); resetUmfrage();
|
||||||
renderComposeThumbs();
|
|
||||||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
|
||||||
toggleUmfrage();
|
|
||||||
document.getElementById('multiChoice').checked = false;
|
document.getElementById('multiChoice').checked = false;
|
||||||
document.getElementById('isPublic').checked = false;
|
document.getElementById('isPublic').checked = false;
|
||||||
document.getElementById('optionList').innerHTML = '';
|
|
||||||
|
|
||||||
// Prepend to mine feed
|
|
||||||
document.getElementById('mineEmpty').style.display = 'none';
|
document.getElementById('mineEmpty').style.display = 'none';
|
||||||
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
|
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
|
||||||
|
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
document.getElementById('publicEmpty').style.display = 'none';
|
document.getElementById('publicEmpty').style.display = 'none';
|
||||||
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
|
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
|
||||||
@@ -492,18 +345,17 @@
|
|||||||
|
|
||||||
// ── Like ──
|
// ── Like ──
|
||||||
async function likePost(postId, postType) {
|
async function likePost(postId, postType) {
|
||||||
let likeEndpoint;
|
let ep;
|
||||||
if (postType === 'GROUP') {
|
if (postType === 'GROUP') {
|
||||||
const card = document.getElementById('pc-' + postId);
|
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||||
const gruppeId = card?.dataset?.gruppeId;
|
|
||||||
if (!gruppeId) return;
|
if (!gruppeId) return;
|
||||||
likeEndpoint = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
ep = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
||||||
} else {
|
} else {
|
||||||
likeEndpoint = `/feed/posts/${postId}/like`;
|
ep = `/feed/posts/${postId}/like`;
|
||||||
}
|
}
|
||||||
await fetch(likeEndpoint, { method: 'POST' });
|
await fetch(ep, { method:'POST' });
|
||||||
const btn = document.getElementById('lk-' + postId);
|
const btn = document.getElementById('lk-'+postId);
|
||||||
const lc = document.getElementById('lkc-' + postId);
|
const lc = document.getElementById('lkc-'+postId);
|
||||||
const was = btn.classList.contains('active');
|
const was = btn.classList.contains('active');
|
||||||
btn.classList.toggle('active', !was);
|
btn.classList.toggle('active', !was);
|
||||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||||
@@ -512,115 +364,55 @@
|
|||||||
// ── Vote ──
|
// ── Vote ──
|
||||||
async function votePost(postId, optionId, tab, postType) {
|
async function votePost(postId, optionId, tab, postType) {
|
||||||
if (postType === 'GROUP') {
|
if (postType === 'GROUP') {
|
||||||
const card = document.getElementById('pc-' + postId);
|
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||||
const gruppeId = card?.dataset?.gruppeId;
|
|
||||||
if (!gruppeId) return;
|
if (!gruppeId) return;
|
||||||
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, {
|
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ optionId })
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await fetch('/feed/posts/' + postId + '/vote', {
|
await fetch(`/feed/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ optionId })
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
reloadPost(postId, tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reloadPost(postId, tab) {
|
|
||||||
const state = feedState[tab];
|
const state = feedState[tab];
|
||||||
state.page = 0; state.hasMore = true; state.loaded = false;
|
state.page = 0; state.hasMore = true; state.loaded = false;
|
||||||
document.getElementById(tab + 'Feed').innerHTML = '';
|
document.getElementById(tab+'Feed').innerHTML = '';
|
||||||
document.getElementById(tab + 'Empty').style.display = 'none';
|
document.getElementById(tab+'Empty').style.display = 'none';
|
||||||
await loadFeed(tab);
|
await loadFeed(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Delete ──
|
// ── Delete ──
|
||||||
async function deletePost(postId) {
|
async function deletePost(postId) {
|
||||||
if (!confirm('Post löschen?')) return;
|
if (!confirm('Post löschen?')) return;
|
||||||
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
|
const res = await fetch('/feed/posts/' + postId, { method:'DELETE' });
|
||||||
if (res.ok) {
|
if (res.ok) document.getElementById('pc-'+postId)?.remove();
|
||||||
document.getElementById('pc-' + postId)?.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Lightbox ──
|
// ── Lightbox (openLb bleibt lokal, da Seiten-Cache nötig) ──
|
||||||
function openLb(postId, postType) {
|
function openLb(postId, postType) {
|
||||||
activeLbPostId = postId;
|
|
||||||
activeLbPostType = postType;
|
|
||||||
const card = document.getElementById('pc-' + postId);
|
const card = document.getElementById('pc-' + postId);
|
||||||
if (card) {
|
if (card) {
|
||||||
const clone = card.cloneNode(true);
|
const clone = card.cloneNode(true);
|
||||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||||
|
_lbSetupContent(postId, 'p', feedPostCache.get(postId)?.bilder);
|
||||||
}
|
}
|
||||||
loadLbComments(postId, postType);
|
loadLbComments(postId, postType);
|
||||||
document.getElementById('postLightbox').classList.add('open');
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLbWithData(p) {
|
function openLbWithData(p) {
|
||||||
activeLbPostId = p.postId;
|
|
||||||
activeLbPostType = p.postType || 'FEED';
|
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = renderPostCard(p, 'mine');
|
tempDiv.innerHTML = renderPostCard(p, 'mine');
|
||||||
const card = tempDiv.firstElementChild;
|
const card = tempDiv.firstElementChild;
|
||||||
if (card) {
|
if (card) {
|
||||||
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
||||||
|
_lbSetupContent(p.postId, 'p', p.bilder);
|
||||||
}
|
}
|
||||||
loadLbComments(p.postId, p.postType || 'FEED');
|
loadLbComments(p.postId, p.postType || 'FEED');
|
||||||
document.getElementById('postLightbox').classList.add('open');
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLb() {
|
document.getElementById('postLightbox').addEventListener('click', e => { if (e.target === document.getElementById('postLightbox')) 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
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,14 +7,8 @@
|
|||||||
<title>Gruppe – xXx Sphere</title>
|
<title>Gruppe – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<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 */
|
/* Header */
|
||||||
.gruppe-header { display:flex; align-items:center; gap:1rem; margin-bottom:1.5rem; flex-wrap:wrap; }
|
.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; }
|
.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 { 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; }
|
.gruppe-header-actions button, .gruppe-header-actions a.btn { margin:0; width:auto; padding:0.4rem 0.9rem; font-size:0.85rem; }
|
||||||
|
|
||||||
/* Posts */
|
/* Compose-Typ (Gruppe-spezifisch) */
|
||||||
.post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; }
|
|
||||||
.compose-type { display:flex; gap:1.5rem; margin-bottom:0.75rem; }
|
.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; }
|
.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 */
|
/* Kommentare */
|
||||||
.comments-section { margin-top:0.75rem; border-top:1px solid var(--color-secondary); padding-top:0.75rem; }
|
.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; }
|
.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 */
|
||||||
.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 { 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; }
|
.dialog-backdrop.visible { display:flex; }
|
||||||
@@ -164,25 +99,24 @@
|
|||||||
<div class="tab-panel active" id="tab-posts">
|
<div class="tab-panel active" id="tab-posts">
|
||||||
<!-- Compose -->
|
<!-- Compose -->
|
||||||
<div class="post-compose" id="compose" style="display:none;">
|
<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>
|
<textarea id="composeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
|
||||||
<div class="compose-thumbs" id="composeThumbs"></div>
|
<div class="compose-thumbs" id="composeThumbs"></div>
|
||||||
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
||||||
<div id="optionList"></div>
|
<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>
|
||||||
<div class="compose-footer">
|
<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;">
|
<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>
|
<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">📷
|
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
||||||
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
||||||
</label>
|
</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>
|
<button onclick="submitPost()" style="width:auto; margin:0;">Veröffentlichen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,18 +211,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Post lightbox dialog -->
|
<!-- Post lightbox dialog -->
|
||||||
<div class="lightbox" id="postDialog">
|
<div class="lightbox" id="postLightbox">
|
||||||
<div class="lb-layout">
|
<div class="lb-layout">
|
||||||
<button class="lb-close" onclick="closePostDialog()">✕</button>
|
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||||
<div class="lb-post-side" id="lbPostContent"></div>
|
<div class="lb-post-side" id="lbPostBody"></div>
|
||||||
<div class="lb-comments-panel">
|
<div class="lb-comments-panel">
|
||||||
|
<div class="lb-comments-header">Kommentare</div>
|
||||||
<div class="lb-comments-list" id="lbCommentsList"></div>
|
<div class="lb-comments-list" id="lbCommentsList"></div>
|
||||||
<div class="lb-comment-compose">
|
<div class="lb-comment-compose">
|
||||||
<textarea id="lbCommentInput" placeholder="Kommentieren…" maxlength="500" rows="3"
|
<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">
|
<div class="lb-comment-compose-actions">
|
||||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -374,6 +309,7 @@
|
|||||||
if (!meRes.ok) { location.href='/login.html'; return; }
|
if (!meRes.ok) { location.href='/login.html'; return; }
|
||||||
const me = await meRes.json();
|
const me = await meRes.json();
|
||||||
myId = me.userId;
|
myId = me.userId;
|
||||||
|
initLb(myId);
|
||||||
|
|
||||||
await loadGruppe();
|
await loadGruppe();
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
@@ -471,15 +407,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const gruppeEditBilder = new Map();
|
||||||
|
|
||||||
function renderPostCard(p) {
|
function renderPostCard(p) {
|
||||||
|
const canEdit = p.authorId === myId;
|
||||||
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
||||||
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
|
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') {
|
if (p.beitragTyp === 'UMFRAGE') {
|
||||||
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
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 pct = total > 0 ? Math.round(o.stimmenCount / total * 100) : 0;
|
||||||
const voted = p.myVoteOptionIds.includes(o.optionId);
|
const voted = p.myVoteOptionIds.includes(o.optionId);
|
||||||
return `<div class="umfrage-option-bar ${voted?'voted':''}" onclick="event.stopPropagation(); vote('${p.beitragId}','${o.optionId}',this)">
|
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>
|
<span>${pct}% (${o.stimmenCount})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('') + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
|
||||||
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}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 `
|
return `
|
||||||
<div class="post-card" id="post-${p.beitragId}" onclick="openPostDialog('${p.beitragId}')" style="cursor:pointer;">
|
<div class="post-card" id="post-${p.beitragId}" onclick="openPostDialog('${p.beitragId}')" style="cursor:pointer;">
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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-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>
|
||||||
<div class="post-date">${fmtDate(p.createdAt)}</div>
|
${rightBtns ? `<div style="margin-left:auto;display:flex;gap:0.25rem;">${rightBtns}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
${p.beitragTyp === 'UMFRAGE' ? `<div style="font-weight:600;margin-bottom:0.5rem;">${renderTextWithHashtags(p.text)}</div>${bildHtml}` : ''}
|
<div id="gpva-${p.beitragId}">${editableHtml}</div>
|
||||||
${body}
|
<div id="gpea-${p.beitragId}" style="display:none;"></div>
|
||||||
|
<div id="gpum-${p.beitragId}">${barsHtml}</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="event.stopPropagation(); toggleLike('${p.beitragId}',this)" id="like-btn-${p.beitragId}">
|
<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>
|
♥ <span id="like-count-${p.beitragId}">${p.likeCount}</span>
|
||||||
@@ -514,11 +461,162 @@
|
|||||||
💬 <span id="kmt-count-${p.beitragId}">${p.kommentarCount}</span>
|
💬 <span id="kmt-count-${p.beitragId}">${p.kommentarCount}</span>
|
||||||
</button>
|
</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>'}
|
${!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>
|
||||||
</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) {
|
async function toggleLike(postId, btn) {
|
||||||
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/like', { method:'POST' });
|
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/like', { method:'POST' });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
@@ -620,17 +718,25 @@
|
|||||||
|
|
||||||
// ── Compose ──
|
// ── Compose ──
|
||||||
|
|
||||||
function toggleUmfrage() {
|
function toggleUmfrage(btn) {
|
||||||
const isUmfrage = document.querySelector('input[name="beitragTyp"]:checked')?.value === 'UMFRAGE';
|
const options = document.getElementById('umfrageOptions');
|
||||||
document.getElementById('umfrageOptions').style.display = isUmfrage ? '' : 'none';
|
const isShowing = options.style.display !== 'none';
|
||||||
document.getElementById('multiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
options.style.display = isShowing ? 'none' : '';
|
||||||
const placeholder = document.getElementById('composeText');
|
const placeholder = document.getElementById('composeText');
|
||||||
placeholder.placeholder = isUmfrage ? 'Frage eingeben…' : 'Was möchtest du teilen?';
|
placeholder.placeholder = isShowing ? 'Was möchtest du teilen?' : 'Frage eingeben…';
|
||||||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
if (btn) btn.classList.toggle('active', !isShowing);
|
||||||
|
if (!isShowing && document.getElementById('optionList').children.length === 0) {
|
||||||
addOption(); addOption();
|
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() {
|
function addOption() {
|
||||||
const list = document.getElementById('optionList');
|
const list = document.getElementById('optionList');
|
||||||
const idx = list.children.length;
|
const idx = list.children.length;
|
||||||
@@ -642,11 +748,12 @@
|
|||||||
|
|
||||||
async function submitPost() {
|
async function submitPost() {
|
||||||
const text = document.getElementById('composeText').value.trim();
|
const text = document.getElementById('composeText').value.trim();
|
||||||
|
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked')?.value || 'TEXT';
|
const beitragTyp = hasUmfrage ? 'UMFRAGE' : 'TEXT';
|
||||||
let optionen = null;
|
let optionen = null;
|
||||||
let multiChoice = null;
|
let multiChoice = null;
|
||||||
if (beitragTyp === 'UMFRAGE') {
|
if (hasUmfrage) {
|
||||||
optionen = Array.from(document.querySelectorAll('#optionList input')).map(i => i.value.trim()).filter(v => v);
|
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; }
|
if (optionen.length < 2) { showModal('Hinweis', 'Bitte mindestens 2 Optionen eingeben.', [{ label: 'OK' }]); return; }
|
||||||
multiChoice = document.getElementById('multiChoice').checked;
|
multiChoice = document.getElementById('multiChoice').checked;
|
||||||
@@ -659,9 +766,7 @@
|
|||||||
});
|
});
|
||||||
if (res.ok || res.status === 201) {
|
if (res.ok || res.status === 201) {
|
||||||
document.getElementById('composeText').value = '';
|
document.getElementById('composeText').value = '';
|
||||||
document.getElementById('optionList').innerHTML = '';
|
resetUmfrage();
|
||||||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
|
||||||
toggleUmfrage();
|
|
||||||
composeBilderArr = [];
|
composeBilderArr = [];
|
||||||
renderComposeThumbs();
|
renderComposeThumbs();
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
@@ -938,28 +1043,22 @@
|
|||||||
|
|
||||||
// ── Post dialog ──
|
// ── Post dialog ──
|
||||||
|
|
||||||
let lbPostId = null;
|
|
||||||
|
|
||||||
async function openPostDialog(postId) {
|
async function openPostDialog(postId) {
|
||||||
lbPostId = postId;
|
|
||||||
const post = allPosts.find(p => p.beitragId === postId);
|
const post = allPosts.find(p => p.beitragId === postId);
|
||||||
if (!post) return;
|
if (!post) return;
|
||||||
renderLbPost(post);
|
renderLbPost(post);
|
||||||
document.getElementById('postDialog').classList.add('open');
|
_lbSetupContent(postId, 'gp', post.bilder);
|
||||||
await loadLbComments();
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
await loadLbComments(postId, 'GROUP');
|
||||||
document.getElementById('lbCommentInput').focus();
|
document.getElementById('lbCommentInput').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePostDialog() {
|
|
||||||
document.getElementById('postDialog').classList.remove('open');
|
|
||||||
lbPostId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLbPost(p) {
|
function renderLbPost(p) {
|
||||||
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
||||||
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
|
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') {
|
if (p.beitragTyp === 'UMFRAGE') {
|
||||||
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
||||||
const bars = p.optionen.map(o => {
|
const bars = p.optionen.map(o => {
|
||||||
@@ -973,19 +1072,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).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>`;
|
umfrageHtml = 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}`;
|
|
||||||
}
|
}
|
||||||
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-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>
|
||||||
<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-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 class="post-date">${fmtDate(p.createdAt)}</div>
|
||||||
</div>
|
</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;">
|
<div class="post-actions" style="margin-top:0.75rem;">
|
||||||
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="toggleLikeLb('${p.beitragId}',this)">
|
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="toggleLikeLb('${p.beitragId}',this)">
|
||||||
♥ <span id="lb-like-count-${p.beitragId}">${p.likeCount}</span>
|
♥ <span id="lb-like-count-${p.beitragId}">${p.likeCount}</span>
|
||||||
@@ -1011,41 +1114,7 @@
|
|||||||
if (post) { post.likeCount = newCount; post.likedByMe = !isActive; }
|
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) {
|
async function reportPostLb(postId, btn) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -1071,7 +1140,7 @@
|
|||||||
if (res.ok || res.status === 204) {
|
if (res.ok || res.status === 204) {
|
||||||
document.getElementById('post-' + postId)?.remove();
|
document.getElementById('post-' + postId)?.remove();
|
||||||
allPosts = allPosts.filter(p => p.beitragId !== postId);
|
allPosts = allPosts.filter(p => p.beitragId !== postId);
|
||||||
closePostDialog();
|
closeLb();
|
||||||
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
|
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -1087,13 +1156,15 @@
|
|||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
const post = allPosts.find(p => p.beitragId === postId);
|
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 => {
|
document.getElementById('postLightbox').addEventListener('click', e => {
|
||||||
if (e.target === document.getElementById('postDialog')) closePostDialog();
|
if (e.target === document.getElementById('postLightbox')) closeLb();
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePostDialog(); });
|
|
||||||
|
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Location – xXx Sphere</title>
|
<title>Location – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<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 { 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); }
|
.back-link:hover { color:var(--color-primary); }
|
||||||
@@ -216,6 +217,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 ───────────────────────────────────────────────────── -->
|
<!-- ── Galerie Lightbox ───────────────────────────────────────────────────── -->
|
||||||
<div class="lb" id="lightbox" onclick="closeLightbox()">
|
<div class="lb" id="lightbox" onclick="closeLightbox()">
|
||||||
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
||||||
@@ -226,6 +249,8 @@
|
|||||||
|
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
|
<script src="/js/hashtag.js"></script>
|
||||||
|
<script src="/js/shared.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const locationId = params.get('id');
|
const locationId = params.get('id');
|
||||||
@@ -303,6 +328,15 @@ async function loadPage() {
|
|||||||
isFollowing = !!locDetail.following;
|
isFollowing = !!locDetail.following;
|
||||||
|
|
||||||
renderPage();
|
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) {
|
if (isAdmin) {
|
||||||
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
||||||
@@ -315,6 +349,8 @@ async function loadPage() {
|
|||||||
} else {
|
} else {
|
||||||
loadEvents();
|
loadEvents();
|
||||||
}
|
}
|
||||||
|
await loadLocFeed();
|
||||||
|
initLocFeedObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPage() {
|
function renderPage() {
|
||||||
@@ -372,6 +408,37 @@ function renderPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="gallery-grid" id="galleryGrid">${galleryHtml}</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 = `
|
const eventsSection = `
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
Veranstaltungen
|
Veranstaltungen
|
||||||
@@ -396,6 +463,7 @@ function renderPage() {
|
|||||||
${locHeaderHtml}
|
${locHeaderHtml}
|
||||||
${hoursHtml}
|
${hoursHtml}
|
||||||
${gallerySection}
|
${gallerySection}
|
||||||
|
${feedSection}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel" id="tab-admins">
|
<div class="tab-panel" id="tab-admins">
|
||||||
@@ -449,6 +517,7 @@ function renderPage() {
|
|||||||
${locHeaderHtml}
|
${locHeaderHtml}
|
||||||
${hoursHtml}
|
${hoursHtml}
|
||||||
${gallerySection}
|
${gallerySection}
|
||||||
|
${feedSection}
|
||||||
${eventsSection}`;
|
${eventsSection}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1135,6 +1204,304 @@ async function sendInboxReply() {
|
|||||||
} catch { showAlert('Fehler beim Senden.'); input.value = text; }
|
} 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 ──────────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
loadPage();
|
loadPage();
|
||||||
</script>
|
</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-next{right:0.3rem}
|
||||||
.car-indicator{text-align:center;font-size:0.75rem;color:var(--color-muted);margin-top:0.25rem}
|
.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 ── */
|
/* ── 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{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)}
|
.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) {
|
function bilderCarousel(bilder) {
|
||||||
if (!bilder || bilder.length === 0) return '';
|
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) =>
|
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>`
|
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
|
||||||
).join('');
|
).join('');
|
||||||
return `<div class="post-carousel">
|
const nav = bilder.length > 1
|
||||||
${slides}
|
? `<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">‹</button>
|
||||||
<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>
|
<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>`;
|
</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) {
|
function carNav(btn, dir) {
|
||||||
@@ -209,6 +313,7 @@ async function loadReplies(kommentarId) {
|
|||||||
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
|
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
|
||||||
const replies = await res.json();
|
const replies = await res.json();
|
||||||
const section = document.getElementById('replies-' + kommentarId);
|
const section = document.getElementById('replies-' + kommentarId);
|
||||||
|
replies.reverse();
|
||||||
section.innerHTML = (replies.length === 0
|
section.innerHTML = (replies.length === 0
|
||||||
? '<p style="color:var(--color-muted);font-size:0.78rem;margin-bottom:0.35rem;">Noch keine Antworten.</p>'
|
? '<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(''))
|
: replies.map(r => renderReplyHtml(r, kommentarId)).join(''))
|
||||||
@@ -235,3 +340,237 @@ async function deleteReply(replyId, parentId) {
|
|||||||
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
|
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
|
||||||
await loadReplies(parentId);
|
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>
|
<title>Home – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<style>
|
||||||
.game-grid {
|
.game-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -184,64 +185,9 @@
|
|||||||
}
|
}
|
||||||
.friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
|
.friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
|
||||||
|
|
||||||
/* ── Compose ── */
|
/* ── Post-Cards (Home: klickbar + Hover) ── */
|
||||||
.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-card { cursor:pointer; 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-card:hover { border-color:var(--color-primary); }
|
.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 ── */
|
/* ── Spiel starten ── */
|
||||||
.start-game-grid {
|
.start-game-grid {
|
||||||
@@ -413,21 +359,19 @@
|
|||||||
<!-- Feed Compose + Vorschau -->
|
<!-- Feed Compose + Vorschau -->
|
||||||
<div class="section-label">Feed 📰</div>
|
<div class="section-label">Feed 📰</div>
|
||||||
<div class="post-compose" id="homeCompose">
|
<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>
|
<textarea id="homeComposeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
|
||||||
<div class="compose-thumbs" id="homeComposeThumbs"></div>
|
<div class="compose-thumbs" id="homeComposeThumbs"></div>
|
||||||
<div class="umfrage-options" id="homeUmfrageOptions" style="display:none;">
|
<div class="umfrage-options" id="homeUmfrageOptions" style="display:none;">
|
||||||
<div id="homeOptionList"></div>
|
<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>
|
||||||
<div class="compose-footer">
|
<div class="compose-footer">
|
||||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
<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">
|
<label class="privacy-toggle">
|
||||||
<input type="checkbox" id="homeIsPublic"> Öffentlich
|
<input type="checkbox" id="homeIsPublic"> Öffentlich
|
||||||
</label>
|
</label>
|
||||||
@@ -437,6 +381,7 @@
|
|||||||
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
||||||
<input type="file" id="homeComposeBildFile" accept="image/*" multiple style="display:none;" onchange="homeSelectBilder(this)">
|
<input type="file" id="homeComposeBildFile" accept="image/*" multiple style="display:none;" onchange="homeSelectBilder(this)">
|
||||||
</label>
|
</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>
|
<button onclick="homeSubmitPost()" style="width:auto;margin:0;">Veröffentlichen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,6 +427,7 @@
|
|||||||
.then(user => {
|
.then(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
myUserId = user.userId;
|
myUserId = user.userId;
|
||||||
|
initLb(user.userId);
|
||||||
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
|
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
|
||||||
Promise.all([loadActiveGames(user.userId), loadActiveLock()]).then(() => {
|
Promise.all([loadActiveGames(user.userId), loadActiveLock()]).then(() => {
|
||||||
const hasGames = document.getElementById('activeGamesSection').style.display !== 'none';
|
const hasGames = document.getElementById('activeGamesSection').style.display !== 'none';
|
||||||
@@ -770,15 +716,22 @@
|
|||||||
|
|
||||||
let homeComposeBilder = [];
|
let homeComposeBilder = [];
|
||||||
|
|
||||||
function homeToggleUmfrage() {
|
function homeToggleUmfrage(btn) {
|
||||||
const isUmfrage = document.querySelector('input[name="homeBeitragTyp"]:checked').value === 'UMFRAGE';
|
const options = document.getElementById('homeUmfrageOptions');
|
||||||
document.getElementById('homeUmfrageOptions').style.display = isUmfrage ? '' : 'none';
|
const isShowing = options.style.display !== 'none';
|
||||||
document.getElementById('homeMultiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
options.style.display = isShowing ? 'none' : '';
|
||||||
if (isUmfrage && document.getElementById('homeOptionList').children.length === 0) {
|
if (btn) btn.classList.toggle('active', !isShowing);
|
||||||
|
if (!isShowing && document.getElementById('homeOptionList').children.length === 0) {
|
||||||
homeAddOption(); homeAddOption();
|
homeAddOption(); homeAddOption();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function homeResetUmfrage() {
|
||||||
|
document.getElementById('homeUmfrageOptions').style.display = 'none';
|
||||||
|
document.getElementById('homeOptionList').innerHTML = '';
|
||||||
|
document.getElementById('homeUmfrageBtn').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
function homeAddOption() {
|
function homeAddOption() {
|
||||||
const list = document.getElementById('homeOptionList');
|
const list = document.getElementById('homeOptionList');
|
||||||
const idx = list.children.length;
|
const idx = list.children.length;
|
||||||
@@ -789,57 +742,28 @@
|
|||||||
list.appendChild(row);
|
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() {
|
function homeRenderThumbs() {
|
||||||
const container = document.getElementById('homeComposeThumbs');
|
renderBilderThumbs(homeComposeBilder, 'homeComposeThumbs', i => {
|
||||||
container.innerHTML = '';
|
homeComposeBilder.splice(i, 1);
|
||||||
homeComposeBilder.forEach((b, i) => {
|
homeRenderThumbs();
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
container.style.display = homeComposeBilder.length > 0 ? 'flex' : 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function homeRemoveThumb(idx) {
|
function homeSelectBilder(input) {
|
||||||
homeComposeBilder.splice(idx, 1);
|
[...input.files].forEach(f => processImageFile(f, homeComposeBilder, homeRenderThumbs));
|
||||||
homeRenderThumbs();
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function homeSubmitPost() {
|
async function homeSubmitPost() {
|
||||||
const text = document.getElementById('homeComposeText').value.trim();
|
const text = document.getElementById('homeComposeText').value.trim();
|
||||||
|
const hasUmfrage = document.getElementById('homeUmfrageOptions').style.display !== 'none';
|
||||||
if (!text && homeComposeBilder.length === 0) return;
|
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 multiChoice = document.getElementById('homeMultiChoice').checked;
|
||||||
const isPublic = document.getElementById('homeIsPublic').checked;
|
const isPublic = document.getElementById('homeIsPublic').checked;
|
||||||
|
|
||||||
let optionen = [];
|
let optionen = [];
|
||||||
if (beitragTyp === 'UMFRAGE') {
|
if (hasUmfrage) {
|
||||||
optionen = Array.from(document.getElementById('homeOptionList').querySelectorAll('input'))
|
optionen = Array.from(document.getElementById('homeOptionList').querySelectorAll('input'))
|
||||||
.map(i => i.value.trim()).filter(v => v);
|
.map(i => i.value.trim()).filter(v => v);
|
||||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||||
@@ -856,11 +780,9 @@
|
|||||||
document.getElementById('homeComposeText').value = '';
|
document.getElementById('homeComposeText').value = '';
|
||||||
homeComposeBilder = [];
|
homeComposeBilder = [];
|
||||||
homeRenderThumbs();
|
homeRenderThumbs();
|
||||||
document.querySelector('input[name="homeBeitragTyp"][value="TEXT"]').checked = true;
|
homeResetUmfrage();
|
||||||
homeToggleUmfrage();
|
|
||||||
document.getElementById('homeMultiChoice').checked = false;
|
document.getElementById('homeMultiChoice').checked = false;
|
||||||
document.getElementById('homeIsPublic').checked = false;
|
document.getElementById('homeIsPublic').checked = false;
|
||||||
document.getElementById('homeOptionList').innerHTML = '';
|
|
||||||
|
|
||||||
// Prepend in Vorschau
|
// Prepend in Vorschau
|
||||||
const feedList = document.getElementById('feedList');
|
const feedList = document.getElementById('feedList');
|
||||||
@@ -882,7 +804,7 @@
|
|||||||
homeCompose.addEventListener('drop', e => {
|
homeCompose.addEventListener('drop', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
homeCompose.classList.remove('drag-over');
|
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 = {};
|
const homePostCache = {};
|
||||||
|
|
||||||
let activeLbPostId = null;
|
|
||||||
let activeLbPostType = null;
|
|
||||||
|
|
||||||
function homeOpenPost(postId) {
|
function homeOpenPost(postId) {
|
||||||
const p = homePostCache[postId];
|
const p = homePostCache[postId];
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
activeLbPostId = p.postId;
|
|
||||||
activeLbPostType = p.postType || 'FEED';
|
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = renderHomePostCard(p);
|
tempDiv.innerHTML = renderHomePostCard(p);
|
||||||
const card = tempDiv.firstElementChild;
|
const card = tempDiv.firstElementChild;
|
||||||
if (card) {
|
if (card) {
|
||||||
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
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.getElementById('postLightbox').classList.add('open');
|
||||||
document.body.style.overflow = 'hidden';
|
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 => {
|
document.getElementById('postLightbox').addEventListener('click', e => {
|
||||||
if (e.target === document.getElementById('postLightbox')) closeLb();
|
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) {
|
// ── Like / Delete ──────────────────────────────────────────────────────────
|
||||||
const targetType = postType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
|
|
||||||
try {
|
async function likeHomePost(postId, postType) {
|
||||||
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${postId}`);
|
const ep = postType === 'GROUP'
|
||||||
const comments = await res.json();
|
? `/gruppen/${document.getElementById('hpc-'+postId)?.dataset?.gruppeId}/posts/${postId}/like`
|
||||||
document.getElementById('lbCommentsList').innerHTML = comments.length === 0
|
: `/feed/posts/${postId}/like`;
|
||||||
? '<p style="color:var(--color-muted);font-size:0.82rem;margin:0.4rem;">Noch keine Kommentare.</p>'
|
await fetch(ep, { method: 'POST' });
|
||||||
: comments.map(k => renderKommentarHtml(k, targetType, postId, { myUserId })).join('');
|
const btn = document.getElementById('hlk-' + postId);
|
||||||
} catch (_) {}
|
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() {
|
async function deleteHomePost(postId) {
|
||||||
if (!activeLbPostId) return;
|
if (!confirm('Post löschen?')) return;
|
||||||
const input = document.getElementById('lbCommentInput');
|
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
|
||||||
const text = input.value.trim();
|
if (res.ok) document.getElementById('hpc-' + postId)?.remove();
|
||||||
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 deleteKommentar(kommentarId, targetType, targetId) {
|
// ── Post-Karte ────────────────────────────────────────────────────────────
|
||||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
|
||||||
await loadLbComments(targetId, activeLbPostType);
|
const homeEditBilder = new Map();
|
||||||
}
|
|
||||||
|
|
||||||
function renderHomePostCard(p) {
|
function renderHomePostCard(p) {
|
||||||
homePostCache[p.postId] = p;
|
homePostCache[p.postId] = p;
|
||||||
@@ -963,7 +865,8 @@
|
|||||||
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
||||||
? `<span class="gruppe-badge">👥 ${esc(p.gruppeName)}</span>`
|
? `<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 = '';
|
let umfrageHtml = '';
|
||||||
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
||||||
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
||||||
@@ -976,24 +879,74 @@
|
|||||||
</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>`;
|
||||||
}
|
}
|
||||||
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-header">
|
||||||
<div class="post-avatar">${avatarHtml}</div>
|
<div class="post-avatar">${avatarHtml}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="post-author">${esc(p.authorName)}${privacyLabel}</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>
|
</div>
|
||||||
|
${ownBtns}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="hpva-${p.postId}">
|
||||||
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||||
${bildHtml}
|
<div id="hpbi-${p.postId}">${bildHtml}</div>
|
||||||
${umfrageHtml}
|
</div>
|
||||||
|
<div id="hpea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="hpum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}">♥ <span>${p.likeCount}</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">💬 <span>${p.kommentarCount}</span></button>
|
<button class="post-action-btn" onclick="event.stopPropagation();homeOpenPost('${p.postId}')">💬 <span id="hkc-${p.postId}">${p.kommentarCount}</span></button>
|
||||||
</div>
|
</div>
|
||||||
</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() {
|
async function loadFeed() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/feed/mine?size=3&page=0');
|
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.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
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.FeedItemDto;
|
||||||
import de.oaa.xxx.feed.dto.FeedPostRequest;
|
import de.oaa.xxx.feed.dto.FeedPostRequest;
|
||||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
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.HashtagService;
|
||||||
import de.oaa.xxx.hashtag.PostHashtagEntity;
|
import de.oaa.xxx.hashtag.PostHashtagEntity;
|
||||||
import de.oaa.xxx.feed.entity.FeedPostOptionEntity;
|
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.GruppenmitgliedRepository;
|
||||||
import de.oaa.xxx.gruppe.repository.UmfrageOptionRepository;
|
import de.oaa.xxx.gruppe.repository.UmfrageOptionRepository;
|
||||||
import de.oaa.xxx.gruppe.repository.UmfrageStimmeRepository;
|
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.LikeService;
|
||||||
import de.oaa.xxx.social.entity.FriendshipEntity;
|
import de.oaa.xxx.social.entity.FriendshipEntity;
|
||||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||||
@@ -76,6 +83,9 @@ public class FeedController {
|
|||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final LikeService likeService;
|
private final LikeService likeService;
|
||||||
private final HashtagService hashtagService;
|
private final HashtagService hashtagService;
|
||||||
|
private final LocationRepository locationRepository;
|
||||||
|
private final LocationFollowRepository locationFollowRepository;
|
||||||
|
private final LocationAdminRepository locationAdminRepository;
|
||||||
|
|
||||||
public FeedController(FeedPostRepository feedPostRepository,
|
public FeedController(FeedPostRepository feedPostRepository,
|
||||||
FeedPostLikeRepository feedPostLikeRepository,
|
FeedPostLikeRepository feedPostLikeRepository,
|
||||||
@@ -92,7 +102,10 @@ public class FeedController {
|
|||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
LikeService likeService,
|
LikeService likeService,
|
||||||
HashtagService hashtagService) {
|
HashtagService hashtagService,
|
||||||
|
LocationRepository locationRepository,
|
||||||
|
LocationFollowRepository locationFollowRepository,
|
||||||
|
LocationAdminRepository locationAdminRepository) {
|
||||||
this.feedPostRepository = feedPostRepository;
|
this.feedPostRepository = feedPostRepository;
|
||||||
this.feedPostLikeRepository = feedPostLikeRepository;
|
this.feedPostLikeRepository = feedPostLikeRepository;
|
||||||
this.feedPostOptionRepository = feedPostOptionRepository;
|
this.feedPostOptionRepository = feedPostOptionRepository;
|
||||||
@@ -109,10 +122,15 @@ public class FeedController {
|
|||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.likeService = likeService;
|
this.likeService = likeService;
|
||||||
this.hashtagService = hashtagService;
|
this.hashtagService = hashtagService;
|
||||||
|
this.locationRepository = locationRepository;
|
||||||
|
this.locationFollowRepository = locationFollowRepository;
|
||||||
|
this.locationAdminRepository = locationAdminRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
record FeedPage(List<FeedItemDto> posts, boolean hasMore) {}
|
record FeedPage(List<FeedItemDto> posts, boolean hasMore) {}
|
||||||
record VoteRequest(UUID optionId) {}
|
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 ──
|
// ── POST /feed/posts ──
|
||||||
|
|
||||||
@@ -132,6 +150,7 @@ public class FeedController {
|
|||||||
FeedPostEntity post = new FeedPostEntity();
|
FeedPostEntity post = new FeedPostEntity();
|
||||||
post.setPostId(UUID.randomUUID());
|
post.setPostId(UUID.randomUUID());
|
||||||
post.setAuthorId(myId);
|
post.setAuthorId(myId);
|
||||||
|
post.setPosterType(PosterType.USER);
|
||||||
post.setText(req.text().trim());
|
post.setText(req.text().trim());
|
||||||
post.setBeitragTyp(typ);
|
post.setBeitragTyp(typ);
|
||||||
post.setMultiChoice(typ == BeitragTyp.UMFRAGE ? req.multiChoice() : null);
|
post.setMultiChoice(typ == BeitragTyp.UMFRAGE ? req.multiChoice() : null);
|
||||||
@@ -158,6 +177,61 @@ public class FeedController {
|
|||||||
return ResponseEntity.status(201).body(toFeedItemDtoFromPost(post, myId));
|
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 ──
|
// ── GET /feed/mine ──
|
||||||
|
|
||||||
@GetMapping("/mine")
|
@GetMapping("/mine")
|
||||||
@@ -183,9 +257,15 @@ public class FeedController {
|
|||||||
.map(m -> m.getGruppeId())
|
.map(m -> m.getGruppeId())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
// Collect followed location IDs
|
||||||
|
List<UUID> followedLocationIds = locationFollowRepository.findByUserId(myId)
|
||||||
|
.stream()
|
||||||
|
.map(LocationFollowEntity::getLocationId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
LocalDateTime since = LocalDateTime.now().minusDays(90);
|
LocalDateTime since = LocalDateTime.now().minusDays(90);
|
||||||
|
|
||||||
// Fetch feed posts from friends + self
|
// Fetch feed posts from friends + self (USER type)
|
||||||
List<FeedPostEntity> feedPosts = feedPostRepository
|
List<FeedPostEntity> feedPosts = feedPostRepository
|
||||||
.findByAuthorIdInAndCreatedAtAfterOrderByCreatedAtDesc(authorIds, since);
|
.findByAuthorIdInAndCreatedAtAfterOrderByCreatedAtDesc(authorIds, since);
|
||||||
|
|
||||||
@@ -193,10 +273,18 @@ public class FeedController {
|
|||||||
List<GruppenbeitragEntity> gruppePosts = gruppeIds.isEmpty() ? List.of() :
|
List<GruppenbeitragEntity> gruppePosts = gruppeIds.isEmpty() ? List.of() :
|
||||||
gruppenbeitragRepository.findByGruppeIdInAndCreatedAtAfterOrderByCreatedAtDesc(gruppeIds, since);
|
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
|
// Merge, convert, sort
|
||||||
List<FeedItemDto> merged = Stream.concat(
|
List<FeedItemDto> merged = Stream.concat(
|
||||||
|
Stream.concat(
|
||||||
feedPosts.stream().map(p -> toFeedItemDtoFromPost(p, myId)),
|
feedPosts.stream().map(p -> toFeedItemDtoFromPost(p, myId)),
|
||||||
gruppePosts.stream().map(b -> toFeedItemDtoFromGruppe(b, myId))
|
gruppePosts.stream().map(b -> toFeedItemDtoFromGruppe(b, myId))
|
||||||
|
),
|
||||||
|
locationPosts.stream().map(p -> toFeedItemDtoFromPost(p, myId))
|
||||||
).sorted(Comparator.comparing(FeedItemDto::createdAt).reversed()).toList();
|
).sorted(Comparator.comparing(FeedItemDto::createdAt).reversed()).toList();
|
||||||
|
|
||||||
int from = page * size;
|
int from = page * size;
|
||||||
@@ -260,6 +348,32 @@ public class FeedController {
|
|||||||
return ResponseEntity.ok(new FeedPage(items, !nextPage.isEmpty()));
|
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= ──
|
// ── GET /feed/hashtag?tag= ──
|
||||||
|
|
||||||
@GetMapping("/hashtag")
|
@GetMapping("/hashtag")
|
||||||
@@ -287,9 +401,11 @@ public class FeedController {
|
|||||||
for (PostHashtagEntity ref : refs) {
|
for (PostHashtagEntity ref : refs) {
|
||||||
if ("FEED".equals(ref.getPostType())) {
|
if ("FEED".equals(ref.getPostType())) {
|
||||||
feedPostRepository.findById(ref.getPostId()).ifPresent(p -> {
|
feedPostRepository.findById(ref.getPostId()).ifPresent(p -> {
|
||||||
|
PosterType pt = p.getPosterType() != null ? p.getPosterType() : PosterType.USER;
|
||||||
boolean visible = p.isPublic()
|
boolean visible = p.isPublic()
|
||||||
|| p.getAuthorId().equals(myId)
|
|| p.getAuthorId().equals(myId)
|
||||||
|| friendIds.contains(p.getAuthorId());
|
|| friendIds.contains(p.getAuthorId())
|
||||||
|
|| pt == PosterType.LOCATION;
|
||||||
if (visible) all.add(toFeedItemDtoFromPost(p, myId));
|
if (visible) all.add(toFeedItemDtoFromPost(p, myId));
|
||||||
});
|
});
|
||||||
} else if ("GROUP".equals(ref.getPostType())) {
|
} else if ("GROUP".equals(ref.getPostType())) {
|
||||||
@@ -361,6 +477,73 @@ public class FeedController {
|
|||||||
return ResponseEntity.ok().build();
|
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} ──
|
// ── DELETE /feed/posts/{id} ──
|
||||||
|
|
||||||
@DeleteMapping("/posts/{id}")
|
@DeleteMapping("/posts/{id}")
|
||||||
@@ -372,7 +555,7 @@ public class FeedController {
|
|||||||
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
|
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
FeedPostEntity post = postOpt.get();
|
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);
|
hashtagService.deleteForPost("FEED", id);
|
||||||
feedPostVoteRepository.deleteByPostId(id);
|
feedPostVoteRepository.deleteByPostId(id);
|
||||||
@@ -388,13 +571,43 @@ public class FeedController {
|
|||||||
|
|
||||||
// ── Helpers ──
|
// ── 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) {
|
private UUID resolveMyId(Principal principal) {
|
||||||
if (principal == null) return null;
|
if (principal == null) return null;
|
||||||
return userService.requireUser(principal).getUserId();
|
return userService.requireUser(principal).getUserId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private FeedItemDto toFeedItemDtoFromPost(FeedPostEntity p, UUID myId) {
|
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);
|
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());
|
long likeCount = feedPostLikeRepository.countByPostId(p.getPostId());
|
||||||
boolean likedByMe = feedPostLikeRepository.findByPostIdAndUserId(p.getPostId(), myId).isPresent();
|
boolean likedByMe = feedPostLikeRepository.findByPostIdAndUserId(p.getPostId(), myId).isPresent();
|
||||||
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("FEED_POST", p.getPostId());
|
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("FEED_POST", p.getPostId());
|
||||||
@@ -417,14 +630,18 @@ public class FeedController {
|
|||||||
p.getPostId(), "FEED",
|
p.getPostId(), "FEED",
|
||||||
null, null,
|
null, null,
|
||||||
p.getAuthorId(),
|
p.getAuthorId(),
|
||||||
author != null ? author.getName() : "Unbekannt",
|
authorName,
|
||||||
author != null ? author.getProfilePicture() : null,
|
authorPicture,
|
||||||
p.getBeitragTyp().name(), p.getText(), p.getMultiChoice(), p.getBilder(),
|
p.getBeitragTyp().name(), p.getText(), p.getMultiChoice(), p.getBilder(),
|
||||||
p.getCreatedAt(),
|
p.getCreatedAt(),
|
||||||
likeCount, likedByMe, kommentarCount,
|
likeCount, likedByMe, kommentarCount,
|
||||||
optionen, myVoteOptionIds,
|
optionen, myVoteOptionIds,
|
||||||
p.isPublic(),
|
p.isPublic(),
|
||||||
p.getTargetUrl()
|
p.getTargetUrl(),
|
||||||
|
p.getEditedAt(),
|
||||||
|
pt.name(),
|
||||||
|
locationId,
|
||||||
|
locationName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +679,10 @@ public class FeedController {
|
|||||||
likeCount, likedByMe, kommentarCount,
|
likeCount, likedByMe, kommentarCount,
|
||||||
optionen, myVoteOptionIds,
|
optionen, myVoteOptionIds,
|
||||||
false,
|
false,
|
||||||
|
null,
|
||||||
|
b.getEditedAt(),
|
||||||
|
"USER",
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,5 +25,9 @@ public record FeedItemDto(
|
|||||||
List<UmfrageOptionDto> optionen,
|
List<UmfrageOptionDto> optionen,
|
||||||
List<UUID> myVoteOptionIds,
|
List<UUID> myVoteOptionIds,
|
||||||
boolean isPublic,
|
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")
|
@Column(name = "bild", columnDefinition = "MEDIUMTEXT")
|
||||||
private List<String> bilder;
|
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)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false, length = 10)
|
@Column(nullable = false, length = 10)
|
||||||
private BeitragTyp beitragTyp;
|
private BeitragTyp beitragTyp;
|
||||||
@@ -43,6 +47,9 @@ public class FeedPostEntity {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private LocalDateTime editedAt;
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String targetUrl;
|
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;
|
package de.oaa.xxx.feed.repository;
|
||||||
|
|
||||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
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.Pageable;
|
||||||
import org.springframework.data.domain.Slice;
|
import org.springframework.data.domain.Slice;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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> 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
|
@Transactional
|
||||||
void deleteByAuthorId(UUID authorId);
|
void deleteByAuthorId(UUID authorId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public interface FeedPostVoteRepository extends JpaRepository<FeedPostVoteEntity
|
|||||||
|
|
||||||
long countByOptionId(UUID optionId);
|
long countByOptionId(UUID optionId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void deleteByOptionId(UUID optionId);
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
void deleteByPostId(UUID postId);
|
void deleteByPostId(UUID postId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class GruppenbeitragController {
|
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 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 PostsPage(List<GruppenbeitragDto> posts, boolean hasMore) {}
|
||||||
record VoteRequest(UUID optionId) {}
|
record VoteRequest(UUID optionId) {}
|
||||||
record ReportRequest(String grund) {}
|
record ReportRequest(String grund) {}
|
||||||
@@ -153,6 +156,74 @@ public class GruppenbeitragController {
|
|||||||
return ResponseEntity.status(201).body(toDto(beitrag, myId));
|
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} ──
|
// ── DELETE /gruppen/{id}/posts/{postId} ──
|
||||||
|
|
||||||
@DeleteMapping("/gruppen/{id}/posts/{postId}")
|
@DeleteMapping("/gruppen/{id}/posts/{postId}")
|
||||||
@@ -354,6 +425,7 @@ public class GruppenbeitragController {
|
|||||||
author != null ? author.getName() : "Unbekannt",
|
author != null ? author.getName() : "Unbekannt",
|
||||||
author != null ? author.getProfilePicture() : null,
|
author != null ? author.getProfilePicture() : null,
|
||||||
b.getBeitragTyp().name(), b.getText(), b.getMultiChoice(), b.getBilder(), b.getCreatedAt(),
|
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,
|
long kommentarCount,
|
||||||
List<UmfrageOptionDto> optionen,
|
List<UmfrageOptionDto> optionen,
|
||||||
List<UUID> myVoteOptionIds,
|
List<UUID> myVoteOptionIds,
|
||||||
boolean reported
|
boolean reported,
|
||||||
|
LocalDateTime editedAt
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -42,4 +42,7 @@ public class GruppenbeitragEntity {
|
|||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private LocalDateTime editedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public interface UmfrageStimmeRepository extends JpaRepository<UmfrageStimmeEnti
|
|||||||
|
|
||||||
long countByOptionId(UUID optionId);
|
long countByOptionId(UUID optionId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void deleteByOptionId(UUID optionId);
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
void deleteByBeitragId(UUID beitragId);
|
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.LocationFollowRepository;
|
||||||
import de.oaa.xxx.location.repository.LocationRepository;
|
import de.oaa.xxx.location.repository.LocationRepository;
|
||||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
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.feed.repository.FeedPostRepository;
|
||||||
import de.oaa.xxx.gruppe.BeitragTyp;
|
import de.oaa.xxx.gruppe.BeitragTyp;
|
||||||
import de.oaa.xxx.social.SystemMessageService;
|
import de.oaa.xxx.social.SystemMessageService;
|
||||||
@@ -168,13 +169,13 @@ public class LocationEventController {
|
|||||||
|
|
||||||
// Feed-Post automatisch anlegen
|
// Feed-Post automatisch anlegen
|
||||||
try {
|
try {
|
||||||
String locationName = locOpt.get().getName();
|
|
||||||
String dateStr = event.getStartAt().format(
|
String dateStr = event.getStartAt().format(
|
||||||
java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy 'um' HH:mm 'Uhr'"));
|
java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy 'um' HH:mm 'Uhr'"));
|
||||||
FeedPostEntity feedPost = new FeedPostEntity();
|
FeedPostEntity feedPost = new FeedPostEntity();
|
||||||
feedPost.setPostId(UUID.randomUUID());
|
feedPost.setPostId(UUID.randomUUID());
|
||||||
feedPost.setAuthorId(myId);
|
feedPost.setAuthorId(locationId);
|
||||||
feedPost.setText("📍 Neue Veranstaltung bei " + locationName + ": \"" + event.getTitle() + "\" - " + dateStr);
|
feedPost.setPosterType(PosterType.LOCATION);
|
||||||
|
feedPost.setText("📍 Neue Veranstaltung: \"" + event.getTitle() + "\" – " + dateStr);
|
||||||
feedPost.setBilder(event.getImageData() != null ? List.of(event.getImageData()) : List.of());
|
feedPost.setBilder(event.getImageData() != null ? List.of(event.getImageData()) : List.of());
|
||||||
feedPost.setBeitragTyp(BeitragTyp.TEXT);
|
feedPost.setBeitragTyp(BeitragTyp.TEXT);
|
||||||
feedPost.setPublic(true);
|
feedPost.setPublic(true);
|
||||||
@@ -218,6 +219,25 @@ public class LocationEventController {
|
|||||||
}
|
}
|
||||||
eventRepo.save(event);
|
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));
|
return ResponseEntity.ok(toDetail(event, locOpt.get().getName(), myId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Profil – xXx Sphere</title>
|
<title>Profil – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<style>
|
||||||
/* ── Profile Header ── */
|
/* ── Profile Header ── */
|
||||||
.profil-header {
|
.profil-header {
|
||||||
@@ -487,50 +488,13 @@
|
|||||||
.vorliebe-chip.bw-EHER_NICHT { border-color:#fb8c00; color:#fb8c00; }
|
.vorliebe-chip.bw-EHER_NICHT { border-color:#fb8c00; color:#fb8c00; }
|
||||||
.vorliebe-chip.bw-GEHT_GAR_NICHT { border-color:#e53935; color:#e53935; }
|
.vorliebe-chip.bw-GEHT_GAR_NICHT { border-color:#e53935; color:#e53935; }
|
||||||
|
|
||||||
/* ── Post cards (profile posts tab) ── */
|
/* ── Post-Bild-Wrap (Profil-spezifisch) ── */
|
||||||
.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 { position:relative; cursor:pointer; display:block; }
|
.post-bild-wrap { position:relative; cursor:pointer; display:block; }
|
||||||
.post-bild-wrap:hover .post-bild { opacity:0.82; }
|
.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-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-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; }
|
/* ── Bild-Navigation in Lightbox ── */
|
||||||
.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; }
|
|
||||||
.lb-img-nav { display:flex; gap:0.75rem; align-items:center; justify-content:center; margin-top:0.75rem; flex-wrap:wrap; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="app">
|
<body class="app">
|
||||||
@@ -652,8 +616,9 @@
|
|||||||
<script src="/js/shared.js"></script>
|
<script src="/js/shared.js"></script>
|
||||||
<script src="/js/image-viewer.js"></script>
|
<script src="/js/image-viewer.js"></script>
|
||||||
<script src="/js/icons.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/social-sidebar.js"></script>
|
||||||
|
<script src="/js/hashtag.js"></script>
|
||||||
<script src="/js/meldung.js"></script>
|
<script src="/js/meldung.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── State ──
|
// ── State ──
|
||||||
@@ -680,6 +645,9 @@
|
|||||||
let activeLbPostId = null;
|
let activeLbPostId = null;
|
||||||
let activeLbMode = null; // 'post' | 'image'
|
let activeLbMode = null; // 'post' | 'image'
|
||||||
let activeLbImageIdx = null;
|
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 ──
|
// ── Label maps ──
|
||||||
const GESCHLECHT_LABEL = { WEIBLICH: 'weiblich', DIVERS: 'divers', MAENNLICH: 'männlich' };
|
const GESCHLECHT_LABEL = { WEIBLICH: 'weiblich', DIVERS: 'divers', MAENNLICH: 'männlich' };
|
||||||
@@ -719,6 +687,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
myUserId = me ? me.userId : null;
|
myUserId = me ? me.userId : null;
|
||||||
|
initLb(myUserId);
|
||||||
isOwnProfile = !previewMode && me && me.userId === profile.userId;
|
isOwnProfile = !previewMode && me && me.userId === profile.userId;
|
||||||
profileData = profile;
|
profileData = profile;
|
||||||
allImages = images;
|
allImages = images;
|
||||||
@@ -1248,6 +1217,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderLbImageBody() {
|
function renderLbImageBody() {
|
||||||
|
document.querySelector('#postLightbox .lb-layout')?.classList.remove('lb-text-only');
|
||||||
const img = allImages[activeLbImageIdx];
|
const img = allImages[activeLbImageIdx];
|
||||||
const total = allImages.length;
|
const total = allImages.length;
|
||||||
const prevDisabled = activeLbImageIdx === 0 ? 'disabled' : '';
|
const prevDisabled = activeLbImageIdx === 0 ? 'disabled' : '';
|
||||||
@@ -1589,42 +1559,48 @@
|
|||||||
const avatarHtml = p.authorPicture
|
const avatarHtml = p.authorPicture
|
||||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
||||||
: '◉';
|
: '◉';
|
||||||
const bildRaw = bilderCarousel(p.bilder);
|
profilPostCache[p.postId] = p;
|
||||||
const bildHtml = bildRaw
|
profilPostBilder.set(p.postId, p.bilder || []);
|
||||||
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}</div>`
|
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 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 = '';
|
let umfrageHtml = '';
|
||||||
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
||||||
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 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 pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
|
||||||
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
|
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
|
||||||
return `<div class="umfrage-option-bar${voted ? ' voted' : ''}" onclick="voteProfilPost('${p.postId}','${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-fill" style="width:${pct}%"></div>
|
||||||
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
||||||
</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 isOwn = p.authorId === myUserId;
|
||||||
const deleteBtn = canDelete
|
const rightBtns = isOwn
|
||||||
? `<button class="post-action-btn post-delete" onclick="deleteProfilPost('${p.postId}')">🗑</button>`
|
? `<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}">
|
return `<div class="post-card" id="pp-${p.postId}">
|
||||||
<div class="post-header">
|
<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>
|
||||||
<div class="post-author">${esc(p.authorName)}${privacyLabel}</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">${fmtDate(p.createdAt)}</div>
|
<div class="post-date" id="ppm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
${deleteBtn}
|
${rightBtns}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="ppva-${p.postId}">
|
||||||
<div class="post-text">${esc(p.text)}</div>
|
<div class="post-text">${esc(p.text)}</div>
|
||||||
${bildHtml}
|
<div id="ppbi-${p.postId}" class="post-bild-wrap" data-post-id="${p.postId}">${bildHtml}</div>
|
||||||
${umfrageHtml}
|
</div>
|
||||||
|
<div id="ppea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="ppum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="pp-like-${p.postId}" onclick="likeProfilPost('${p.postId}')">
|
<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>
|
♥ <span id="pp-lc-${p.postId}">${p.likeCount}</span>
|
||||||
@@ -1665,6 +1641,73 @@
|
|||||||
if (res.ok) document.getElementById('pp-' + postId)?.remove();
|
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) ──
|
// ── Lightbox (Post + Bild) ──
|
||||||
function openPostLb(postId) {
|
function openPostLb(postId) {
|
||||||
activeLbMode = 'post';
|
activeLbMode = 'post';
|
||||||
@@ -1674,7 +1717,9 @@
|
|||||||
if (card) {
|
if (card) {
|
||||||
const clone = card.cloneNode(true);
|
const clone = card.cloneNode(true);
|
||||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
|
clone.querySelectorAll('[id^="ppea-"]').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||||
|
_lbSetupContent(postId, 'pp', profilPostBilder.get(postId) || []);
|
||||||
}
|
}
|
||||||
loadLbComments();
|
loadLbComments();
|
||||||
document.getElementById('postLightbox').classList.add('open');
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
@@ -1728,7 +1773,6 @@
|
|||||||
if (e.target === document.getElementById('postLightbox')) closeLb();
|
if (e.target === document.getElementById('postLightbox')) closeLb();
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Escape' && document.getElementById('postLightbox').classList.contains('open')) closeLb();
|
|
||||||
if (activeLbMode === 'image') {
|
if (activeLbMode === 'image') {
|
||||||
if (e.key === 'ArrowLeft') lbGalleryNav(-1);
|
if (e.key === 'ArrowLeft') lbGalleryNav(-1);
|
||||||
if (e.key === 'ArrowRight') lbGalleryNav(1);
|
if (e.key === 'ArrowRight') lbGalleryNav(1);
|
||||||
|
|||||||
@@ -7,135 +7,49 @@
|
|||||||
<title>Feed – xXx Sphere</title>
|
<title>Feed – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<style>
|
||||||
.tabs { display:flex; gap:0; margin-bottom:1.5rem; border-bottom:1px solid var(--color-secondary); }
|
.post-card { cursor:pointer; }
|
||||||
.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; }
|
|
||||||
|
|
||||||
.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 { 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-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 { margin-left:auto; font-size:0.82rem; color:var(--color-muted); text-decoration:none; }
|
||||||
.hashtag-banner-back:hover { color:var(--color-primary); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="app">
|
<body class="app">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<!-- Hashtag-Banner (nur sichtbar wenn ?tag=… gesetzt) -->
|
|
||||||
<div class="hashtag-banner" id="hashtagBanner" style="display:none;">
|
<div class="hashtag-banner" id="hashtagBanner" style="display:none;">
|
||||||
<span>Posts mit </span><span class="hashtag-banner-tag" id="hashtagBannerLabel"></span>
|
<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>
|
<a class="hashtag-banner-back" href="/community/feed.html">× Zurück zum Feed</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs" id="feedTabs">
|
<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 active" 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" data-tab="public" onclick="switchTab('public',this)">Öffentlicher Feed</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mein Feed -->
|
|
||||||
<div class="tab-panel active" id="tab-mine">
|
<div class="tab-panel active" id="tab-mine">
|
||||||
<div class="post-compose" id="compose">
|
<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>
|
<textarea id="composeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
|
||||||
<div class="compose-thumbs" id="composeThumbs"></div>
|
<div class="compose-thumbs" id="composeThumbs"></div>
|
||||||
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
||||||
<div id="optionList"></div>
|
<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>
|
||||||
<div class="compose-footer">
|
<div class="compose-footer">
|
||||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
<label class="privacy-toggle"><input type="checkbox" id="isPublic"> Öffentlich</label>
|
||||||
<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>
|
|
||||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
<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>
|
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji">😊</button>
|
||||||
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
<label class="compose-action-btn" title="Fotos">📷
|
||||||
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
||||||
</label>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,24 +58,21 @@
|
|||||||
<div class="sentinel" id="mineSentinel"></div>
|
<div class="sentinel" id="mineSentinel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Öffentlicher Feed -->
|
|
||||||
<div class="tab-panel" id="tab-public">
|
<div class="tab-panel" id="tab-public">
|
||||||
<div id="publicFeed"></div>
|
<div id="publicFeed"></div>
|
||||||
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
|
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
|
||||||
<div class="sentinel" id="publicSentinel"></div>
|
<div class="sentinel" id="publicSentinel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hashtag-Feed (wird angezeigt wenn ?tag=… gesetzt) -->
|
|
||||||
<div id="tab-hashtag" style="display:none;">
|
<div id="tab-hashtag" style="display:none;">
|
||||||
<div id="hashtagFeed"></div>
|
<div id="hashtagFeed"></div>
|
||||||
<p class="empty-hint" id="hashtagEmpty" style="display:none;">Keine Posts mit diesem Hashtag.</p>
|
<p class="empty-hint" id="hashtagEmpty" style="display:none;">Keine Posts mit diesem Hashtag.</p>
|
||||||
<div class="sentinel" id="hashtagSentinel"></div>
|
<div class="sentinel" id="hashtagSentinel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Post Lightbox -->
|
<!-- Lightbox (IDs geteilt mit shared.js) -->
|
||||||
<div class="lightbox" id="postLightbox">
|
<div class="lightbox" id="postLightbox">
|
||||||
<div class="lb-layout">
|
<div class="lb-layout">
|
||||||
<button class="lb-close" onclick="closeLb()">✕</button>
|
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||||
@@ -190,9 +101,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// ── State ──
|
// ── State ──
|
||||||
let myUserId = null;
|
let myUserId = null;
|
||||||
let activeLbPostId = null;
|
let activeHashtag = null;
|
||||||
let activeLbPostType = null;
|
|
||||||
let activeHashtag = null; // set when ?tag=... is in URL
|
|
||||||
|
|
||||||
const feedState = {
|
const feedState = {
|
||||||
mine: { page:0, hasMore:true, loading:false, loaded:false },
|
mine: { page:0, hasMore:true, loading:false, loaded:false },
|
||||||
@@ -200,9 +109,11 @@
|
|||||||
hashtag: { page:0, hasMore:true, loading:false, loaded:false }
|
hashtag: { page:0, hasMore:true, loading:false, loaded:false }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const feedPostCache = new Map();
|
||||||
|
const feedEditBilder = new Map();
|
||||||
let composeBilderArr = [];
|
let composeBilderArr = [];
|
||||||
|
|
||||||
// ── Hashtag-Modus prüfen ──
|
// ── Hashtag-Modus ──
|
||||||
const _urlTag = new URLSearchParams(window.location.search).get('tag');
|
const _urlTag = new URLSearchParams(window.location.search).get('tag');
|
||||||
if (_urlTag) {
|
if (_urlTag) {
|
||||||
activeHashtag = _urlTag.replace(/^#/, '').toLowerCase();
|
activeHashtag = _urlTag.replace(/^#/, '').toLowerCase();
|
||||||
@@ -219,6 +130,7 @@
|
|||||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
myUserId = user.userId;
|
myUserId = user.userId;
|
||||||
|
initLb(myUserId);
|
||||||
if (activeHashtag) {
|
if (activeHashtag) {
|
||||||
await loadFeed('hashtag');
|
await loadFeed('hashtag');
|
||||||
} else {
|
} else {
|
||||||
@@ -234,18 +146,11 @@
|
|||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
// ── Autocomplete für Compose ──
|
// Hashtag-Autocomplete
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
if (document.readyState !== 'loading') attachHashtagAutocomplete(document.getElementById('composeText'));
|
||||||
const ta = document.getElementById('composeText');
|
else document.addEventListener('DOMContentLoaded', () => attachHashtagAutocomplete(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tab switching ──
|
// ── Tabs ──
|
||||||
function switchTab(name, btn) {
|
function switchTab(name, btn) {
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
@@ -254,39 +159,27 @@
|
|||||||
localStorage.setItem('tab_feed', name);
|
localStorage.setItem('tab_feed', name);
|
||||||
if (!feedState[name].loaded) loadFeed(name);
|
if (!feedState[name].loaded) loadFeed(name);
|
||||||
}
|
}
|
||||||
const _savedFeedTab = localStorage.getItem('tab_feed');
|
const _savedTab = localStorage.getItem('tab_feed');
|
||||||
if (_savedFeedTab) {
|
if (_savedTab) { const _b = document.querySelector(`.tab-btn[data-tab="${_savedTab}"]`); if (_b) switchTab(_savedTab, _b); }
|
||||||
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedFeedTab}"]`);
|
|
||||||
if (_btn) switchTab(_savedFeedTab, _btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Feed loading ──
|
// ── Feed laden ──
|
||||||
async function loadFeed(tab) {
|
async function loadFeed(tab) {
|
||||||
const state = feedState[tab];
|
const state = feedState[tab];
|
||||||
if (state.loading || !state.hasMore) return;
|
if (state.loading || !state.hasMore) return;
|
||||||
state.loading = true;
|
state.loading = true; state.loaded = true;
|
||||||
state.loaded = true;
|
|
||||||
try {
|
try {
|
||||||
let url;
|
const url = tab === 'hashtag'
|
||||||
if (tab === 'hashtag') {
|
? `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`
|
||||||
url = `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`;
|
: `${tab === 'mine' ? '/feed/mine' : '/feed/public'}?page=${state.page}&size=10`;
|
||||||
} else {
|
|
||||||
const base = tab === 'mine' ? '/feed/mine' : '/feed/public';
|
|
||||||
url = `${base}?page=${state.page}&size=10`;
|
|
||||||
}
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const feedEl = document.getElementById(tab + 'Feed');
|
const feedEl = document.getElementById(tab + 'Feed');
|
||||||
if (state.page === 0 && data.posts.length === 0) {
|
if (state.page === 0 && data.posts.length === 0) document.getElementById(tab + 'Empty').style.display = '';
|
||||||
document.getElementById(tab + 'Empty').style.display = '';
|
|
||||||
}
|
|
||||||
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
|
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
|
||||||
state.hasMore = data.hasMore;
|
state.hasMore = data.hasMore;
|
||||||
state.page++;
|
state.page++;
|
||||||
} finally {
|
} finally { state.loading = false; }
|
||||||
state.loading = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Infinite Scroll ──
|
// ── Infinite Scroll ──
|
||||||
@@ -302,188 +195,148 @@
|
|||||||
observer.observe(document.getElementById('publicSentinel'));
|
observer.observe(document.getElementById('publicSentinel'));
|
||||||
observer.observe(document.getElementById('hashtagSentinel'));
|
observer.observe(document.getElementById('hashtagSentinel'));
|
||||||
|
|
||||||
// bilderCarousel und carNav kommen aus shared.js
|
// ── Post-Card rendern ──
|
||||||
|
|
||||||
// ── Render post card ──
|
|
||||||
function renderPostCard(p, tab) {
|
function renderPostCard(p, tab) {
|
||||||
const avatarHtml = p.authorPicture
|
feedPostCache.set(p.postId, { text: p.text, bilder: p.bilder || [], beitragTyp: p.beitragTyp, optionen: p.optionen || [], myVoteOptionIds: p.myVoteOptionIds || [], multiChoice: p.multiChoice, _tab: tab });
|
||||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
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 privacyLabel = !p.isPublic ? ' <span style="font-size:0.7rem;color:var(--color-muted);">🔒</span>' : '';
|
||||||
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
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>`
|
? `<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 = '';
|
// ── Umfrage-HTML (Feed-spezifisch, da Vote-Handler Seiten-State braucht) ──
|
||||||
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
function buildUmfrageHtml(postId, optionen, myVoteOptionIds, onVoteAttrFn) {
|
||||||
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
if (!optionen || !optionen.length) return '';
|
||||||
umfrageHtml = '<div style="margin-top:0.5rem;">' + p.optionen.map(o => {
|
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 pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
|
||||||
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
|
const voted = myVoteOptionIds && myVoteOptionIds.includes(o.optionId);
|
||||||
return `<div class="umfrage-option-bar${voted ? ' voted' : ''}" onclick="event.stopPropagation(); votePost('${p.postId}','${o.optionId}','${tab}','${p.postType}')">
|
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-fill" style="width:${pct}%"></div>
|
||||||
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
||||||
</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;
|
// ── Post-Bearbeitung (Feed) ──
|
||||||
const deleteBtn = canDelete
|
function startFeedEdit(postId) {
|
||||||
? `<button class="post-action-btn post-delete" onclick="event.stopPropagation(); deletePost('${p.postId}')">🗑</button>`
|
startPostEdit({ postId, prefix: 'p', data: feedPostCache.get(postId), editBilderMap: feedEditBilder,
|
||||||
: '';
|
saveFn: 'saveFeedEdit', cancelFn: 'cancelFeedEdit',
|
||||||
const meldenBtn = p.authorId !== myUserId
|
addImgFn: 'feedEditAddImg', addOptionFn: 'feedEditAddOption', rmImgFn: 'feedEditRmImg' });
|
||||||
? `<button class="post-action-btn" onclick="event.stopPropagation(); openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>`
|
}
|
||||||
: '';
|
function cancelFeedEdit(postId) { cancelPostEdit(postId, 'p', feedEditBilder); }
|
||||||
|
function feedEditRmImg(postId, idx) { feedEditBilder.get(postId).splice(idx, 1); _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg'); }
|
||||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
function feedEditAddImg(input, postId) {
|
||||||
const hasTarget = !!p.targetUrl;
|
[...input.files].forEach(f => processImageFile(f, feedEditBilder.get(postId), () => _renderEditThumbs(feedEditBilder, postId, 'p', 'feedEditRmImg')));
|
||||||
const cardClick = hasTarget
|
input.value = '';
|
||||||
? `window.location.href='${p.targetUrl}'`
|
}
|
||||||
: `openLb('${p.postId}','${p.postType}')`;
|
function feedEditAddOption(postId) { editAddOptionRow(`peo-${postId}`); }
|
||||||
const commentBtn = hasTarget ? '' : `
|
async function saveFeedEdit(postId) {
|
||||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
const cached = feedPostCache.get(postId);
|
||||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
await savePostEdit({ postId, prefix: 'p', endpoint: `/feed/posts/${postId}`,
|
||||||
</button>`;
|
isUmfrage: cached?.beitragTyp === 'UMFRAGE', editBilderMap: feedEditBilder,
|
||||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}" style="cursor:pointer;">
|
onSuccess: updated => {
|
||||||
<div class="post-header">
|
feedPostCache.set(postId, { ...cached, text: updated.text, bilder: updated.bilder || [], optionen: updated.optionen || [], multiChoice: updated.multiChoice });
|
||||||
<div class="post-avatar">${avatarHtml}</div>
|
applyPostEditDom(postId, 'p', updated,
|
||||||
<div>
|
buildUmfrageHtml(postId, updated.optionen, cached?.myVoteOptionIds || [],
|
||||||
<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>
|
(pid, oid) => `event.stopPropagation(); votePost('${pid}','${oid}','${cached?._tab}','FEED')`));
|
||||||
<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>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Compose ──
|
// ── Compose ──
|
||||||
function toggleUmfrage() {
|
function selectComposeBilder(input) {
|
||||||
const isUmfrage = document.querySelector('input[name="beitragTyp"]:checked').value === 'UMFRAGE';
|
[...input.files].forEach(f => processImageFile(f, composeBilderArr, renderComposeThumbs));
|
||||||
document.getElementById('umfrageOptions').style.display = isUmfrage ? '' : 'none';
|
input.value = '';
|
||||||
document.getElementById('multiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
|
||||||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
|
||||||
addOption(); addOption();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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() {
|
function addOption() {
|
||||||
const list = document.getElementById('optionList');
|
const list = document.getElementById('optionList');
|
||||||
const idx = list.children.length;
|
const row = document.createElement('div'); row.className = 'umfrage-option-row';
|
||||||
const row = document.createElement('div');
|
row.innerHTML = `<input type="text" placeholder="Option ${list.children.length + 1}" maxlength="100">
|
||||||
row.className = 'umfrage-option-row';
|
|
||||||
row.innerHTML = `<input type="text" placeholder="Option ${idx + 1}" maxlength="100">
|
|
||||||
<button onclick="this.parentElement.remove()">✕</button>`;
|
<button onclick="this.parentElement.remove()">✕</button>`;
|
||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectComposeBilder(input) {
|
// Drag & Drop Compose
|
||||||
[...input.files].forEach(f => { if (f.type.startsWith('image/')) processImageFile(f); });
|
const _compose = document.getElementById('compose');
|
||||||
input.value = '';
|
_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)); });
|
||||||
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));
|
|
||||||
});
|
|
||||||
|
|
||||||
async function submitPost() {
|
async function submitPost() {
|
||||||
const text = document.getElementById('composeText').value.trim();
|
const text = document.getElementById('composeText').value.trim();
|
||||||
|
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
|
||||||
if (!text && composeBilderArr.length === 0) return;
|
if (!text && composeBilderArr.length === 0) return;
|
||||||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked').value;
|
|
||||||
const multiChoice = document.getElementById('multiChoice').checked;
|
const multiChoice = document.getElementById('multiChoice').checked;
|
||||||
const isPublic = document.getElementById('isPublic').checked;
|
const isPublic = document.getElementById('isPublic').checked;
|
||||||
|
|
||||||
let optionen = [];
|
let optionen = [];
|
||||||
if (beitragTyp === 'UMFRAGE') {
|
if (hasUmfrage) {
|
||||||
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input'))
|
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input')).map(i=>i.value.trim()).filter(v=>v);
|
||||||
.map(i => i.value.trim()).filter(v => v);
|
|
||||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch('/feed/posts', {
|
const res = await fetch('/feed/posts', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
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;
|
if (!res.ok) return;
|
||||||
const post = await res.json();
|
const post = await res.json();
|
||||||
|
|
||||||
// Reset compose
|
|
||||||
document.getElementById('composeText').value = '';
|
document.getElementById('composeText').value = '';
|
||||||
composeBilderArr = [];
|
composeBilderArr = []; renderComposeThumbs(); resetUmfrage();
|
||||||
renderComposeThumbs();
|
|
||||||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
|
||||||
toggleUmfrage();
|
|
||||||
document.getElementById('multiChoice').checked = false;
|
document.getElementById('multiChoice').checked = false;
|
||||||
document.getElementById('isPublic').checked = false;
|
document.getElementById('isPublic').checked = false;
|
||||||
document.getElementById('optionList').innerHTML = '';
|
|
||||||
|
|
||||||
// Prepend to mine feed
|
|
||||||
document.getElementById('mineEmpty').style.display = 'none';
|
document.getElementById('mineEmpty').style.display = 'none';
|
||||||
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
|
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
|
||||||
|
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
document.getElementById('publicEmpty').style.display = 'none';
|
document.getElementById('publicEmpty').style.display = 'none';
|
||||||
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
|
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
|
||||||
@@ -492,18 +345,17 @@
|
|||||||
|
|
||||||
// ── Like ──
|
// ── Like ──
|
||||||
async function likePost(postId, postType) {
|
async function likePost(postId, postType) {
|
||||||
let likeEndpoint;
|
let ep;
|
||||||
if (postType === 'GROUP') {
|
if (postType === 'GROUP') {
|
||||||
const card = document.getElementById('pc-' + postId);
|
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||||
const gruppeId = card?.dataset?.gruppeId;
|
|
||||||
if (!gruppeId) return;
|
if (!gruppeId) return;
|
||||||
likeEndpoint = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
ep = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
||||||
} else {
|
} else {
|
||||||
likeEndpoint = `/feed/posts/${postId}/like`;
|
ep = `/feed/posts/${postId}/like`;
|
||||||
}
|
}
|
||||||
await fetch(likeEndpoint, { method: 'POST' });
|
await fetch(ep, { method:'POST' });
|
||||||
const btn = document.getElementById('lk-' + postId);
|
const btn = document.getElementById('lk-'+postId);
|
||||||
const lc = document.getElementById('lkc-' + postId);
|
const lc = document.getElementById('lkc-'+postId);
|
||||||
const was = btn.classList.contains('active');
|
const was = btn.classList.contains('active');
|
||||||
btn.classList.toggle('active', !was);
|
btn.classList.toggle('active', !was);
|
||||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||||
@@ -512,115 +364,55 @@
|
|||||||
// ── Vote ──
|
// ── Vote ──
|
||||||
async function votePost(postId, optionId, tab, postType) {
|
async function votePost(postId, optionId, tab, postType) {
|
||||||
if (postType === 'GROUP') {
|
if (postType === 'GROUP') {
|
||||||
const card = document.getElementById('pc-' + postId);
|
const gruppeId = document.getElementById('pc-'+postId)?.dataset?.gruppeId;
|
||||||
const gruppeId = card?.dataset?.gruppeId;
|
|
||||||
if (!gruppeId) return;
|
if (!gruppeId) return;
|
||||||
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, {
|
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ optionId })
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await fetch('/feed/posts/' + postId + '/vote', {
|
await fetch(`/feed/posts/${postId}/vote`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({optionId}) });
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ optionId })
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
reloadPost(postId, tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reloadPost(postId, tab) {
|
|
||||||
const state = feedState[tab];
|
const state = feedState[tab];
|
||||||
state.page = 0; state.hasMore = true; state.loaded = false;
|
state.page = 0; state.hasMore = true; state.loaded = false;
|
||||||
document.getElementById(tab + 'Feed').innerHTML = '';
|
document.getElementById(tab+'Feed').innerHTML = '';
|
||||||
document.getElementById(tab + 'Empty').style.display = 'none';
|
document.getElementById(tab+'Empty').style.display = 'none';
|
||||||
await loadFeed(tab);
|
await loadFeed(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Delete ──
|
// ── Delete ──
|
||||||
async function deletePost(postId) {
|
async function deletePost(postId) {
|
||||||
if (!confirm('Post löschen?')) return;
|
if (!confirm('Post löschen?')) return;
|
||||||
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
|
const res = await fetch('/feed/posts/' + postId, { method:'DELETE' });
|
||||||
if (res.ok) {
|
if (res.ok) document.getElementById('pc-'+postId)?.remove();
|
||||||
document.getElementById('pc-' + postId)?.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Lightbox ──
|
// ── Lightbox (openLb bleibt lokal, da Seiten-Cache nötig) ──
|
||||||
function openLb(postId, postType) {
|
function openLb(postId, postType) {
|
||||||
activeLbPostId = postId;
|
|
||||||
activeLbPostType = postType;
|
|
||||||
const card = document.getElementById('pc-' + postId);
|
const card = document.getElementById('pc-' + postId);
|
||||||
if (card) {
|
if (card) {
|
||||||
const clone = card.cloneNode(true);
|
const clone = card.cloneNode(true);
|
||||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||||
|
_lbSetupContent(postId, 'p', feedPostCache.get(postId)?.bilder);
|
||||||
}
|
}
|
||||||
loadLbComments(postId, postType);
|
loadLbComments(postId, postType);
|
||||||
document.getElementById('postLightbox').classList.add('open');
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLbWithData(p) {
|
function openLbWithData(p) {
|
||||||
activeLbPostId = p.postId;
|
|
||||||
activeLbPostType = p.postType || 'FEED';
|
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = renderPostCard(p, 'mine');
|
tempDiv.innerHTML = renderPostCard(p, 'mine');
|
||||||
const card = tempDiv.firstElementChild;
|
const card = tempDiv.firstElementChild;
|
||||||
if (card) {
|
if (card) {
|
||||||
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
||||||
|
_lbSetupContent(p.postId, 'p', p.bilder);
|
||||||
}
|
}
|
||||||
loadLbComments(p.postId, p.postType || 'FEED');
|
loadLbComments(p.postId, p.postType || 'FEED');
|
||||||
document.getElementById('postLightbox').classList.add('open');
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLb() {
|
document.getElementById('postLightbox').addEventListener('click', e => { if (e.target === document.getElementById('postLightbox')) 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
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,14 +7,8 @@
|
|||||||
<title>Gruppe – xXx Sphere</title>
|
<title>Gruppe – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<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 */
|
/* Header */
|
||||||
.gruppe-header { display:flex; align-items:center; gap:1rem; margin-bottom:1.5rem; flex-wrap:wrap; }
|
.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; }
|
.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 { 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; }
|
.gruppe-header-actions button, .gruppe-header-actions a.btn { margin:0; width:auto; padding:0.4rem 0.9rem; font-size:0.85rem; }
|
||||||
|
|
||||||
/* Posts */
|
/* Compose-Typ (Gruppe-spezifisch) */
|
||||||
.post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; }
|
|
||||||
.compose-type { display:flex; gap:1.5rem; margin-bottom:0.75rem; }
|
.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; }
|
.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 */
|
/* Kommentare */
|
||||||
.comments-section { margin-top:0.75rem; border-top:1px solid var(--color-secondary); padding-top:0.75rem; }
|
.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; }
|
.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 */
|
||||||
.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 { 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; }
|
.dialog-backdrop.visible { display:flex; }
|
||||||
@@ -164,25 +99,24 @@
|
|||||||
<div class="tab-panel active" id="tab-posts">
|
<div class="tab-panel active" id="tab-posts">
|
||||||
<!-- Compose -->
|
<!-- Compose -->
|
||||||
<div class="post-compose" id="compose" style="display:none;">
|
<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>
|
<textarea id="composeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
|
||||||
<div class="compose-thumbs" id="composeThumbs"></div>
|
<div class="compose-thumbs" id="composeThumbs"></div>
|
||||||
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
||||||
<div id="optionList"></div>
|
<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>
|
||||||
<div class="compose-footer">
|
<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;">
|
<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>
|
<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">📷
|
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
||||||
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
||||||
</label>
|
</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>
|
<button onclick="submitPost()" style="width:auto; margin:0;">Veröffentlichen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,18 +211,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Post lightbox dialog -->
|
<!-- Post lightbox dialog -->
|
||||||
<div class="lightbox" id="postDialog">
|
<div class="lightbox" id="postLightbox">
|
||||||
<div class="lb-layout">
|
<div class="lb-layout">
|
||||||
<button class="lb-close" onclick="closePostDialog()">✕</button>
|
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||||
<div class="lb-post-side" id="lbPostContent"></div>
|
<div class="lb-post-side" id="lbPostBody"></div>
|
||||||
<div class="lb-comments-panel">
|
<div class="lb-comments-panel">
|
||||||
|
<div class="lb-comments-header">Kommentare</div>
|
||||||
<div class="lb-comments-list" id="lbCommentsList"></div>
|
<div class="lb-comments-list" id="lbCommentsList"></div>
|
||||||
<div class="lb-comment-compose">
|
<div class="lb-comment-compose">
|
||||||
<textarea id="lbCommentInput" placeholder="Kommentieren…" maxlength="500" rows="3"
|
<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">
|
<div class="lb-comment-compose-actions">
|
||||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -374,6 +309,7 @@
|
|||||||
if (!meRes.ok) { location.href='/login.html'; return; }
|
if (!meRes.ok) { location.href='/login.html'; return; }
|
||||||
const me = await meRes.json();
|
const me = await meRes.json();
|
||||||
myId = me.userId;
|
myId = me.userId;
|
||||||
|
initLb(myId);
|
||||||
|
|
||||||
await loadGruppe();
|
await loadGruppe();
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
@@ -471,15 +407,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const gruppeEditBilder = new Map();
|
||||||
|
|
||||||
function renderPostCard(p) {
|
function renderPostCard(p) {
|
||||||
|
const canEdit = p.authorId === myId;
|
||||||
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
||||||
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
|
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') {
|
if (p.beitragTyp === 'UMFRAGE') {
|
||||||
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
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 pct = total > 0 ? Math.round(o.stimmenCount / total * 100) : 0;
|
||||||
const voted = p.myVoteOptionIds.includes(o.optionId);
|
const voted = p.myVoteOptionIds.includes(o.optionId);
|
||||||
return `<div class="umfrage-option-bar ${voted?'voted':''}" onclick="event.stopPropagation(); vote('${p.beitragId}','${o.optionId}',this)">
|
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>
|
<span>${pct}% (${o.stimmenCount})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('') + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
|
||||||
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}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 `
|
return `
|
||||||
<div class="post-card" id="post-${p.beitragId}" onclick="openPostDialog('${p.beitragId}')" style="cursor:pointer;">
|
<div class="post-card" id="post-${p.beitragId}" onclick="openPostDialog('${p.beitragId}')" style="cursor:pointer;">
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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-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>
|
||||||
<div class="post-date">${fmtDate(p.createdAt)}</div>
|
${rightBtns ? `<div style="margin-left:auto;display:flex;gap:0.25rem;">${rightBtns}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
${p.beitragTyp === 'UMFRAGE' ? `<div style="font-weight:600;margin-bottom:0.5rem;">${renderTextWithHashtags(p.text)}</div>${bildHtml}` : ''}
|
<div id="gpva-${p.beitragId}">${editableHtml}</div>
|
||||||
${body}
|
<div id="gpea-${p.beitragId}" style="display:none;"></div>
|
||||||
|
<div id="gpum-${p.beitragId}">${barsHtml}</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="event.stopPropagation(); toggleLike('${p.beitragId}',this)" id="like-btn-${p.beitragId}">
|
<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>
|
♥ <span id="like-count-${p.beitragId}">${p.likeCount}</span>
|
||||||
@@ -514,11 +461,162 @@
|
|||||||
💬 <span id="kmt-count-${p.beitragId}">${p.kommentarCount}</span>
|
💬 <span id="kmt-count-${p.beitragId}">${p.kommentarCount}</span>
|
||||||
</button>
|
</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>'}
|
${!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>
|
||||||
</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) {
|
async function toggleLike(postId, btn) {
|
||||||
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/like', { method:'POST' });
|
const res = await fetch('/gruppen/' + gruppeId + '/posts/' + postId + '/like', { method:'POST' });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
@@ -620,17 +718,25 @@
|
|||||||
|
|
||||||
// ── Compose ──
|
// ── Compose ──
|
||||||
|
|
||||||
function toggleUmfrage() {
|
function toggleUmfrage(btn) {
|
||||||
const isUmfrage = document.querySelector('input[name="beitragTyp"]:checked')?.value === 'UMFRAGE';
|
const options = document.getElementById('umfrageOptions');
|
||||||
document.getElementById('umfrageOptions').style.display = isUmfrage ? '' : 'none';
|
const isShowing = options.style.display !== 'none';
|
||||||
document.getElementById('multiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
options.style.display = isShowing ? 'none' : '';
|
||||||
const placeholder = document.getElementById('composeText');
|
const placeholder = document.getElementById('composeText');
|
||||||
placeholder.placeholder = isUmfrage ? 'Frage eingeben…' : 'Was möchtest du teilen?';
|
placeholder.placeholder = isShowing ? 'Was möchtest du teilen?' : 'Frage eingeben…';
|
||||||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
if (btn) btn.classList.toggle('active', !isShowing);
|
||||||
|
if (!isShowing && document.getElementById('optionList').children.length === 0) {
|
||||||
addOption(); addOption();
|
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() {
|
function addOption() {
|
||||||
const list = document.getElementById('optionList');
|
const list = document.getElementById('optionList');
|
||||||
const idx = list.children.length;
|
const idx = list.children.length;
|
||||||
@@ -642,11 +748,12 @@
|
|||||||
|
|
||||||
async function submitPost() {
|
async function submitPost() {
|
||||||
const text = document.getElementById('composeText').value.trim();
|
const text = document.getElementById('composeText').value.trim();
|
||||||
|
const hasUmfrage = document.getElementById('umfrageOptions').style.display !== 'none';
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked')?.value || 'TEXT';
|
const beitragTyp = hasUmfrage ? 'UMFRAGE' : 'TEXT';
|
||||||
let optionen = null;
|
let optionen = null;
|
||||||
let multiChoice = null;
|
let multiChoice = null;
|
||||||
if (beitragTyp === 'UMFRAGE') {
|
if (hasUmfrage) {
|
||||||
optionen = Array.from(document.querySelectorAll('#optionList input')).map(i => i.value.trim()).filter(v => v);
|
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; }
|
if (optionen.length < 2) { showModal('Hinweis', 'Bitte mindestens 2 Optionen eingeben.', [{ label: 'OK' }]); return; }
|
||||||
multiChoice = document.getElementById('multiChoice').checked;
|
multiChoice = document.getElementById('multiChoice').checked;
|
||||||
@@ -659,9 +766,7 @@
|
|||||||
});
|
});
|
||||||
if (res.ok || res.status === 201) {
|
if (res.ok || res.status === 201) {
|
||||||
document.getElementById('composeText').value = '';
|
document.getElementById('composeText').value = '';
|
||||||
document.getElementById('optionList').innerHTML = '';
|
resetUmfrage();
|
||||||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
|
||||||
toggleUmfrage();
|
|
||||||
composeBilderArr = [];
|
composeBilderArr = [];
|
||||||
renderComposeThumbs();
|
renderComposeThumbs();
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
@@ -938,28 +1043,22 @@
|
|||||||
|
|
||||||
// ── Post dialog ──
|
// ── Post dialog ──
|
||||||
|
|
||||||
let lbPostId = null;
|
|
||||||
|
|
||||||
async function openPostDialog(postId) {
|
async function openPostDialog(postId) {
|
||||||
lbPostId = postId;
|
|
||||||
const post = allPosts.find(p => p.beitragId === postId);
|
const post = allPosts.find(p => p.beitragId === postId);
|
||||||
if (!post) return;
|
if (!post) return;
|
||||||
renderLbPost(post);
|
renderLbPost(post);
|
||||||
document.getElementById('postDialog').classList.add('open');
|
_lbSetupContent(postId, 'gp', post.bilder);
|
||||||
await loadLbComments();
|
document.getElementById('postLightbox').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
await loadLbComments(postId, 'GROUP');
|
||||||
document.getElementById('lbCommentInput').focus();
|
document.getElementById('lbCommentInput').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePostDialog() {
|
|
||||||
document.getElementById('postDialog').classList.remove('open');
|
|
||||||
lbPostId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLbPost(p) {
|
function renderLbPost(p) {
|
||||||
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
||||||
const av = p.authorPicture ? `<img src="data:image/png;base64,${p.authorPicture}" alt="">` : '◉';
|
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') {
|
if (p.beitragTyp === 'UMFRAGE') {
|
||||||
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
const total = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
||||||
const bars = p.optionen.map(o => {
|
const bars = p.optionen.map(o => {
|
||||||
@@ -973,19 +1072,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).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>`;
|
umfrageHtml = 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}`;
|
|
||||||
}
|
}
|
||||||
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-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>
|
||||||
<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-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 class="post-date">${fmtDate(p.createdAt)}</div>
|
||||||
</div>
|
</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;">
|
<div class="post-actions" style="margin-top:0.75rem;">
|
||||||
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="toggleLikeLb('${p.beitragId}',this)">
|
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="toggleLikeLb('${p.beitragId}',this)">
|
||||||
♥ <span id="lb-like-count-${p.beitragId}">${p.likeCount}</span>
|
♥ <span id="lb-like-count-${p.beitragId}">${p.likeCount}</span>
|
||||||
@@ -1011,41 +1114,7 @@
|
|||||||
if (post) { post.likeCount = newCount; post.likedByMe = !isActive; }
|
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) {
|
async function reportPostLb(postId, btn) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -1071,7 +1140,7 @@
|
|||||||
if (res.ok || res.status === 204) {
|
if (res.ok || res.status === 204) {
|
||||||
document.getElementById('post-' + postId)?.remove();
|
document.getElementById('post-' + postId)?.remove();
|
||||||
allPosts = allPosts.filter(p => p.beitragId !== postId);
|
allPosts = allPosts.filter(p => p.beitragId !== postId);
|
||||||
closePostDialog();
|
closeLb();
|
||||||
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
|
if (allPosts.length === 0) { document.getElementById('postsEmpty').style.display = ''; document.getElementById('loadMoreBtn').style.display = 'none'; }
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -1087,13 +1156,15 @@
|
|||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
const post = allPosts.find(p => p.beitragId === postId);
|
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 => {
|
document.getElementById('postLightbox').addEventListener('click', e => {
|
||||||
if (e.target === document.getElementById('postDialog')) closePostDialog();
|
if (e.target === document.getElementById('postLightbox')) closeLb();
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePostDialog(); });
|
|
||||||
|
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Location – xXx Sphere</title>
|
<title>Location – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<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 { 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); }
|
.back-link:hover { color:var(--color-primary); }
|
||||||
@@ -216,6 +217,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 ───────────────────────────────────────────────────── -->
|
<!-- ── Galerie Lightbox ───────────────────────────────────────────────────── -->
|
||||||
<div class="lb" id="lightbox" onclick="closeLightbox()">
|
<div class="lb" id="lightbox" onclick="closeLightbox()">
|
||||||
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
||||||
@@ -226,6 +249,8 @@
|
|||||||
|
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
|
<script src="/js/hashtag.js"></script>
|
||||||
|
<script src="/js/shared.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const locationId = params.get('id');
|
const locationId = params.get('id');
|
||||||
@@ -303,6 +328,15 @@ async function loadPage() {
|
|||||||
isFollowing = !!locDetail.following;
|
isFollowing = !!locDetail.following;
|
||||||
|
|
||||||
renderPage();
|
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) {
|
if (isAdmin) {
|
||||||
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
||||||
@@ -315,6 +349,8 @@ async function loadPage() {
|
|||||||
} else {
|
} else {
|
||||||
loadEvents();
|
loadEvents();
|
||||||
}
|
}
|
||||||
|
await loadLocFeed();
|
||||||
|
initLocFeedObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPage() {
|
function renderPage() {
|
||||||
@@ -372,6 +408,37 @@ function renderPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="gallery-grid" id="galleryGrid">${galleryHtml}</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 = `
|
const eventsSection = `
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
Veranstaltungen
|
Veranstaltungen
|
||||||
@@ -396,6 +463,7 @@ function renderPage() {
|
|||||||
${locHeaderHtml}
|
${locHeaderHtml}
|
||||||
${hoursHtml}
|
${hoursHtml}
|
||||||
${gallerySection}
|
${gallerySection}
|
||||||
|
${feedSection}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel" id="tab-admins">
|
<div class="tab-panel" id="tab-admins">
|
||||||
@@ -449,6 +517,7 @@ function renderPage() {
|
|||||||
${locHeaderHtml}
|
${locHeaderHtml}
|
||||||
${hoursHtml}
|
${hoursHtml}
|
||||||
${gallerySection}
|
${gallerySection}
|
||||||
|
${feedSection}
|
||||||
${eventsSection}`;
|
${eventsSection}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1135,6 +1204,304 @@ async function sendInboxReply() {
|
|||||||
} catch { showAlert('Fehler beim Senden.'); input.value = text; }
|
} 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 ──────────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
loadPage();
|
loadPage();
|
||||||
</script>
|
</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-next{right:0.3rem}
|
||||||
.car-indicator{text-align:center;font-size:0.75rem;color:var(--color-muted);margin-top:0.25rem}
|
.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 ── */
|
/* ── 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{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)}
|
.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) {
|
function bilderCarousel(bilder) {
|
||||||
if (!bilder || bilder.length === 0) return '';
|
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) =>
|
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>`
|
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
|
||||||
).join('');
|
).join('');
|
||||||
return `<div class="post-carousel">
|
const nav = bilder.length > 1
|
||||||
${slides}
|
? `<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">‹</button>
|
||||||
<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>
|
<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>`;
|
</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) {
|
function carNav(btn, dir) {
|
||||||
@@ -209,6 +313,7 @@ async function loadReplies(kommentarId) {
|
|||||||
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
|
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
|
||||||
const replies = await res.json();
|
const replies = await res.json();
|
||||||
const section = document.getElementById('replies-' + kommentarId);
|
const section = document.getElementById('replies-' + kommentarId);
|
||||||
|
replies.reverse();
|
||||||
section.innerHTML = (replies.length === 0
|
section.innerHTML = (replies.length === 0
|
||||||
? '<p style="color:var(--color-muted);font-size:0.78rem;margin-bottom:0.35rem;">Noch keine Antworten.</p>'
|
? '<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(''))
|
: replies.map(r => renderReplyHtml(r, kommentarId)).join(''))
|
||||||
@@ -235,3 +340,237 @@ async function deleteReply(replyId, parentId) {
|
|||||||
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
|
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
|
||||||
await loadReplies(parentId);
|
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>
|
<title>Home – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/community.css">
|
||||||
<style>
|
<style>
|
||||||
.game-grid {
|
.game-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -184,64 +185,9 @@
|
|||||||
}
|
}
|
||||||
.friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
|
.friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
|
||||||
|
|
||||||
/* ── Compose ── */
|
/* ── Post-Cards (Home: klickbar + Hover) ── */
|
||||||
.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-card { cursor:pointer; 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-card:hover { border-color:var(--color-primary); }
|
.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 ── */
|
/* ── Spiel starten ── */
|
||||||
.start-game-grid {
|
.start-game-grid {
|
||||||
@@ -413,21 +359,19 @@
|
|||||||
<!-- Feed Compose + Vorschau -->
|
<!-- Feed Compose + Vorschau -->
|
||||||
<div class="section-label">Feed 📰</div>
|
<div class="section-label">Feed 📰</div>
|
||||||
<div class="post-compose" id="homeCompose">
|
<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>
|
<textarea id="homeComposeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
|
||||||
<div class="compose-thumbs" id="homeComposeThumbs"></div>
|
<div class="compose-thumbs" id="homeComposeThumbs"></div>
|
||||||
<div class="umfrage-options" id="homeUmfrageOptions" style="display:none;">
|
<div class="umfrage-options" id="homeUmfrageOptions" style="display:none;">
|
||||||
<div id="homeOptionList"></div>
|
<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>
|
||||||
<div class="compose-footer">
|
<div class="compose-footer">
|
||||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
<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">
|
<label class="privacy-toggle">
|
||||||
<input type="checkbox" id="homeIsPublic"> Öffentlich
|
<input type="checkbox" id="homeIsPublic"> Öffentlich
|
||||||
</label>
|
</label>
|
||||||
@@ -437,6 +381,7 @@
|
|||||||
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
||||||
<input type="file" id="homeComposeBildFile" accept="image/*" multiple style="display:none;" onchange="homeSelectBilder(this)">
|
<input type="file" id="homeComposeBildFile" accept="image/*" multiple style="display:none;" onchange="homeSelectBilder(this)">
|
||||||
</label>
|
</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>
|
<button onclick="homeSubmitPost()" style="width:auto;margin:0;">Veröffentlichen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,6 +427,7 @@
|
|||||||
.then(user => {
|
.then(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
myUserId = user.userId;
|
myUserId = user.userId;
|
||||||
|
initLb(user.userId);
|
||||||
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
|
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
|
||||||
Promise.all([loadActiveGames(user.userId), loadActiveLock()]).then(() => {
|
Promise.all([loadActiveGames(user.userId), loadActiveLock()]).then(() => {
|
||||||
const hasGames = document.getElementById('activeGamesSection').style.display !== 'none';
|
const hasGames = document.getElementById('activeGamesSection').style.display !== 'none';
|
||||||
@@ -770,15 +716,22 @@
|
|||||||
|
|
||||||
let homeComposeBilder = [];
|
let homeComposeBilder = [];
|
||||||
|
|
||||||
function homeToggleUmfrage() {
|
function homeToggleUmfrage(btn) {
|
||||||
const isUmfrage = document.querySelector('input[name="homeBeitragTyp"]:checked').value === 'UMFRAGE';
|
const options = document.getElementById('homeUmfrageOptions');
|
||||||
document.getElementById('homeUmfrageOptions').style.display = isUmfrage ? '' : 'none';
|
const isShowing = options.style.display !== 'none';
|
||||||
document.getElementById('homeMultiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
options.style.display = isShowing ? 'none' : '';
|
||||||
if (isUmfrage && document.getElementById('homeOptionList').children.length === 0) {
|
if (btn) btn.classList.toggle('active', !isShowing);
|
||||||
|
if (!isShowing && document.getElementById('homeOptionList').children.length === 0) {
|
||||||
homeAddOption(); homeAddOption();
|
homeAddOption(); homeAddOption();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function homeResetUmfrage() {
|
||||||
|
document.getElementById('homeUmfrageOptions').style.display = 'none';
|
||||||
|
document.getElementById('homeOptionList').innerHTML = '';
|
||||||
|
document.getElementById('homeUmfrageBtn').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
function homeAddOption() {
|
function homeAddOption() {
|
||||||
const list = document.getElementById('homeOptionList');
|
const list = document.getElementById('homeOptionList');
|
||||||
const idx = list.children.length;
|
const idx = list.children.length;
|
||||||
@@ -789,57 +742,28 @@
|
|||||||
list.appendChild(row);
|
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() {
|
function homeRenderThumbs() {
|
||||||
const container = document.getElementById('homeComposeThumbs');
|
renderBilderThumbs(homeComposeBilder, 'homeComposeThumbs', i => {
|
||||||
container.innerHTML = '';
|
homeComposeBilder.splice(i, 1);
|
||||||
homeComposeBilder.forEach((b, i) => {
|
homeRenderThumbs();
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
container.style.display = homeComposeBilder.length > 0 ? 'flex' : 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function homeRemoveThumb(idx) {
|
function homeSelectBilder(input) {
|
||||||
homeComposeBilder.splice(idx, 1);
|
[...input.files].forEach(f => processImageFile(f, homeComposeBilder, homeRenderThumbs));
|
||||||
homeRenderThumbs();
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function homeSubmitPost() {
|
async function homeSubmitPost() {
|
||||||
const text = document.getElementById('homeComposeText').value.trim();
|
const text = document.getElementById('homeComposeText').value.trim();
|
||||||
|
const hasUmfrage = document.getElementById('homeUmfrageOptions').style.display !== 'none';
|
||||||
if (!text && homeComposeBilder.length === 0) return;
|
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 multiChoice = document.getElementById('homeMultiChoice').checked;
|
||||||
const isPublic = document.getElementById('homeIsPublic').checked;
|
const isPublic = document.getElementById('homeIsPublic').checked;
|
||||||
|
|
||||||
let optionen = [];
|
let optionen = [];
|
||||||
if (beitragTyp === 'UMFRAGE') {
|
if (hasUmfrage) {
|
||||||
optionen = Array.from(document.getElementById('homeOptionList').querySelectorAll('input'))
|
optionen = Array.from(document.getElementById('homeOptionList').querySelectorAll('input'))
|
||||||
.map(i => i.value.trim()).filter(v => v);
|
.map(i => i.value.trim()).filter(v => v);
|
||||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||||
@@ -856,11 +780,9 @@
|
|||||||
document.getElementById('homeComposeText').value = '';
|
document.getElementById('homeComposeText').value = '';
|
||||||
homeComposeBilder = [];
|
homeComposeBilder = [];
|
||||||
homeRenderThumbs();
|
homeRenderThumbs();
|
||||||
document.querySelector('input[name="homeBeitragTyp"][value="TEXT"]').checked = true;
|
homeResetUmfrage();
|
||||||
homeToggleUmfrage();
|
|
||||||
document.getElementById('homeMultiChoice').checked = false;
|
document.getElementById('homeMultiChoice').checked = false;
|
||||||
document.getElementById('homeIsPublic').checked = false;
|
document.getElementById('homeIsPublic').checked = false;
|
||||||
document.getElementById('homeOptionList').innerHTML = '';
|
|
||||||
|
|
||||||
// Prepend in Vorschau
|
// Prepend in Vorschau
|
||||||
const feedList = document.getElementById('feedList');
|
const feedList = document.getElementById('feedList');
|
||||||
@@ -882,7 +804,7 @@
|
|||||||
homeCompose.addEventListener('drop', e => {
|
homeCompose.addEventListener('drop', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
homeCompose.classList.remove('drag-over');
|
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 = {};
|
const homePostCache = {};
|
||||||
|
|
||||||
let activeLbPostId = null;
|
|
||||||
let activeLbPostType = null;
|
|
||||||
|
|
||||||
function homeOpenPost(postId) {
|
function homeOpenPost(postId) {
|
||||||
const p = homePostCache[postId];
|
const p = homePostCache[postId];
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
activeLbPostId = p.postId;
|
|
||||||
activeLbPostType = p.postType || 'FEED';
|
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = renderHomePostCard(p);
|
tempDiv.innerHTML = renderHomePostCard(p);
|
||||||
const card = tempDiv.firstElementChild;
|
const card = tempDiv.firstElementChild;
|
||||||
if (card) {
|
if (card) {
|
||||||
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
card.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||||
document.getElementById('lbPostBody').innerHTML = card.innerHTML;
|
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.getElementById('postLightbox').classList.add('open');
|
||||||
document.body.style.overflow = 'hidden';
|
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 => {
|
document.getElementById('postLightbox').addEventListener('click', e => {
|
||||||
if (e.target === document.getElementById('postLightbox')) closeLb();
|
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) {
|
// ── Like / Delete ──────────────────────────────────────────────────────────
|
||||||
const targetType = postType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
|
|
||||||
try {
|
async function likeHomePost(postId, postType) {
|
||||||
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${postId}`);
|
const ep = postType === 'GROUP'
|
||||||
const comments = await res.json();
|
? `/gruppen/${document.getElementById('hpc-'+postId)?.dataset?.gruppeId}/posts/${postId}/like`
|
||||||
document.getElementById('lbCommentsList').innerHTML = comments.length === 0
|
: `/feed/posts/${postId}/like`;
|
||||||
? '<p style="color:var(--color-muted);font-size:0.82rem;margin:0.4rem;">Noch keine Kommentare.</p>'
|
await fetch(ep, { method: 'POST' });
|
||||||
: comments.map(k => renderKommentarHtml(k, targetType, postId, { myUserId })).join('');
|
const btn = document.getElementById('hlk-' + postId);
|
||||||
} catch (_) {}
|
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() {
|
async function deleteHomePost(postId) {
|
||||||
if (!activeLbPostId) return;
|
if (!confirm('Post löschen?')) return;
|
||||||
const input = document.getElementById('lbCommentInput');
|
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
|
||||||
const text = input.value.trim();
|
if (res.ok) document.getElementById('hpc-' + postId)?.remove();
|
||||||
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 deleteKommentar(kommentarId, targetType, targetId) {
|
// ── Post-Karte ────────────────────────────────────────────────────────────
|
||||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
|
||||||
await loadLbComments(targetId, activeLbPostType);
|
const homeEditBilder = new Map();
|
||||||
}
|
|
||||||
|
|
||||||
function renderHomePostCard(p) {
|
function renderHomePostCard(p) {
|
||||||
homePostCache[p.postId] = p;
|
homePostCache[p.postId] = p;
|
||||||
@@ -963,7 +865,8 @@
|
|||||||
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
||||||
? `<span class="gruppe-badge">👥 ${esc(p.gruppeName)}</span>`
|
? `<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 = '';
|
let umfrageHtml = '';
|
||||||
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
||||||
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
||||||
@@ -976,24 +879,74 @@
|
|||||||
</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>`;
|
||||||
}
|
}
|
||||||
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-header">
|
||||||
<div class="post-avatar">${avatarHtml}</div>
|
<div class="post-avatar">${avatarHtml}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="post-author">${esc(p.authorName)}${privacyLabel}</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>
|
</div>
|
||||||
|
${ownBtns}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="hpva-${p.postId}">
|
||||||
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||||
${bildHtml}
|
<div id="hpbi-${p.postId}">${bildHtml}</div>
|
||||||
${umfrageHtml}
|
</div>
|
||||||
|
<div id="hpea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="hpum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}">♥ <span>${p.likeCount}</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">💬 <span>${p.kommentarCount}</span></button>
|
<button class="post-action-btn" onclick="event.stopPropagation();homeOpenPost('${p.postId}')">💬 <span id="hkc-${p.postId}">${p.kommentarCount}</span></button>
|
||||||
</div>
|
</div>
|
||||||
</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() {
|
async function loadFeed() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/feed/mine?size=3&page=0');
|
const res = await fetch('/feed/mine?size=3&page=0');
|
||||||
|
|||||||
Reference in New Issue
Block a user