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 class="post-text">${esc(p.text)}</div>
|
<div id="ppva-${p.postId}">
|
||||||
${bildHtml}
|
<div class="post-text">${esc(p.text)}</div>
|
||||||
${umfrageHtml}
|
<div id="ppbi-${p.postId}" class="post-bild-wrap" data-post-id="${p.postId}">${bildHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div id="ppea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="ppum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<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);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
: '';
|
||||||
</div>`;
|
return `<div class="post-carousel">${slides}${nav}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Richtet die Lightbox-Inhalte ein:
|
||||||
|
* – View-Area bekommt Flex-Layout damit das Bild den Platz füllt
|
||||||
|
* – Bild-Container bekommt lb-ic-Klasse + Carousel (für alle Bildanzahlen)
|
||||||
|
* – Post-Text bekommt scrollbare Höhenbegrenzung
|
||||||
|
*/
|
||||||
|
function _lbSetupContent(postId, prefix, bilder) {
|
||||||
|
const body = document.getElementById('lbPostBody');
|
||||||
|
const va = body.querySelector(`#${prefix}va-${postId}`);
|
||||||
|
if (va) va.classList.add('lb-va');
|
||||||
|
const hasImages = bilder && bilder.length > 0;
|
||||||
|
const pbi = body.querySelector(`#${prefix}bi-${postId}`);
|
||||||
|
if (pbi) {
|
||||||
|
if (hasImages) {
|
||||||
|
pbi.classList.add('lb-ic');
|
||||||
|
pbi.innerHTML = bilderCarousel(bilder);
|
||||||
|
} else {
|
||||||
|
pbi.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (va) va.querySelector('.post-text')?.classList.add('lb-text');
|
||||||
|
|
||||||
|
// Text-only layout: kein Bild → Kommentare unterhalb, volle Breite
|
||||||
|
const layout = document.querySelector('#postLightbox .lb-layout');
|
||||||
|
if (layout) layout.classList.toggle('lb-text-only', !hasImages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bilder-Grid (Feed-Karten, orientierungsabhängig) ──────────────────────────
|
||||||
|
const POST_IMG_SIZE = 500; // px — Breite und Höhe des Bild-Containers
|
||||||
|
let _pigSeq = 0;
|
||||||
|
const _pigStore = new Map(); // id → bilder[]
|
||||||
|
|
||||||
|
function bilderGrid(bilder) {
|
||||||
|
if (!bilder || bilder.length === 0) return '';
|
||||||
|
const S = POST_IMG_SIZE;
|
||||||
|
const id = 'pig-' + (++_pigSeq);
|
||||||
|
|
||||||
|
if (bilder.length === 1) {
|
||||||
|
// Längere Seite = S, kürzere letterboxed
|
||||||
|
return `<div class="post-img-grid pig-contain" id="${id}" style="width:${S}px;height:${S}px;grid-template-columns:1fr;grid-template-rows:1fr;">
|
||||||
|
<div class="pig-item pig-contain"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2+ Bilder: Orientierung des ersten Bilds bestimmt das Layout → deferred init
|
||||||
|
_pigStore.set(id, bilder);
|
||||||
|
return `<div class="post-img-grid" id="${id}" style="width:${S}px;height:${S}px;">` +
|
||||||
|
`<img src="data:image/jpeg;base64,${bilder[0]}" style="display:none;position:absolute;" alt="" onload="_pigInit('${id}',this)">` +
|
||||||
|
`</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pigInit(id, probe) {
|
||||||
|
const bilder = _pigStore.get(id);
|
||||||
|
if (!bilder) return;
|
||||||
|
_pigStore.delete(id);
|
||||||
|
const grid = document.getElementById(id);
|
||||||
|
if (!grid) return;
|
||||||
|
const land = probe.naturalWidth >= probe.naturalHeight; // quer = landscape
|
||||||
|
const n = bilder.length;
|
||||||
|
const extra = n - 3;
|
||||||
|
const moreHtml = extra > 0 ? `<div class="pig-more">+${extra}</div>` : '';
|
||||||
|
|
||||||
|
grid.innerHTML = ''; // Probe-Bild entfernen
|
||||||
|
|
||||||
|
// Fraktionale Werte → Browser berücksichtigt gap automatisch, Trennstrich immer genau in der Mitte
|
||||||
|
if (n === 2) {
|
||||||
|
if (land) {
|
||||||
|
// Quer-erstes Bild → beide übereinander (Trennstrich horizontal in der Mitte)
|
||||||
|
grid.style.gridTemplateColumns = '1fr';
|
||||||
|
grid.style.gridTemplateRows = '1fr 1fr';
|
||||||
|
} else {
|
||||||
|
// Hochkant-erstes Bild → beide nebeneinander (Trennstrich vertikal in der Mitte)
|
||||||
|
grid.style.gridTemplateColumns = '1fr 1fr';
|
||||||
|
grid.style.gridTemplateRows = '1fr';
|
||||||
|
}
|
||||||
|
grid.insertAdjacentHTML('beforeend',
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>`);
|
||||||
|
} else {
|
||||||
|
// 3+ Bilder (ab 4 gleiche Darstellung wie bei 3, +N-Overlay auf Zelle 3)
|
||||||
|
// Dunkler Hintergrund nur hier, damit der Spalt zwischen den Zellen nicht auffällt
|
||||||
|
grid.classList.add('pig-dark');
|
||||||
|
grid.style.gridTemplateColumns = '1fr 1fr';
|
||||||
|
grid.style.gridTemplateRows = '1fr 1fr';
|
||||||
|
if (land) {
|
||||||
|
// Quer → Bild 1 oben (volle Breite), Bilder 2+3 nebeneinander unten
|
||||||
|
grid.insertAdjacentHTML('beforeend',
|
||||||
|
`<div class="pig-item" style="grid-column:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
|
||||||
|
} else {
|
||||||
|
// Hochkant → Bild 1 links (volle Höhe), Bilder 2+3 übereinander rechts
|
||||||
|
grid.insertAdjacentHTML('beforeend',
|
||||||
|
`<div class="pig-item" style="grid-row:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function carNav(btn, dir) {
|
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 class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
<div id="hpva-${p.postId}">
|
||||||
${bildHtml}
|
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||||
${umfrageHtml}
|
<div id="hpbi-${p.postId}">${bildHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div id="hpea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="hpum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<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');
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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 class="post-text">${esc(p.text)}</div>
|
<div id="ppva-${p.postId}">
|
||||||
${bildHtml}
|
<div class="post-text">${esc(p.text)}</div>
|
||||||
${umfrageHtml}
|
<div id="ppbi-${p.postId}" class="post-bild-wrap" data-post-id="${p.postId}">${bildHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div id="ppea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="ppum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<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);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
: '';
|
||||||
</div>`;
|
return `<div class="post-carousel">${slides}${nav}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Richtet die Lightbox-Inhalte ein:
|
||||||
|
* – View-Area bekommt Flex-Layout damit das Bild den Platz füllt
|
||||||
|
* – Bild-Container bekommt lb-ic-Klasse + Carousel (für alle Bildanzahlen)
|
||||||
|
* – Post-Text bekommt scrollbare Höhenbegrenzung
|
||||||
|
*/
|
||||||
|
function _lbSetupContent(postId, prefix, bilder) {
|
||||||
|
const body = document.getElementById('lbPostBody');
|
||||||
|
const va = body.querySelector(`#${prefix}va-${postId}`);
|
||||||
|
if (va) va.classList.add('lb-va');
|
||||||
|
const hasImages = bilder && bilder.length > 0;
|
||||||
|
const pbi = body.querySelector(`#${prefix}bi-${postId}`);
|
||||||
|
if (pbi) {
|
||||||
|
if (hasImages) {
|
||||||
|
pbi.classList.add('lb-ic');
|
||||||
|
pbi.innerHTML = bilderCarousel(bilder);
|
||||||
|
} else {
|
||||||
|
pbi.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (va) va.querySelector('.post-text')?.classList.add('lb-text');
|
||||||
|
|
||||||
|
// Text-only layout: kein Bild → Kommentare unterhalb, volle Breite
|
||||||
|
const layout = document.querySelector('#postLightbox .lb-layout');
|
||||||
|
if (layout) layout.classList.toggle('lb-text-only', !hasImages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bilder-Grid (Feed-Karten, orientierungsabhängig) ──────────────────────────
|
||||||
|
const POST_IMG_SIZE = 500; // px — Breite und Höhe des Bild-Containers
|
||||||
|
let _pigSeq = 0;
|
||||||
|
const _pigStore = new Map(); // id → bilder[]
|
||||||
|
|
||||||
|
function bilderGrid(bilder) {
|
||||||
|
if (!bilder || bilder.length === 0) return '';
|
||||||
|
const S = POST_IMG_SIZE;
|
||||||
|
const id = 'pig-' + (++_pigSeq);
|
||||||
|
|
||||||
|
if (bilder.length === 1) {
|
||||||
|
// Längere Seite = S, kürzere letterboxed
|
||||||
|
return `<div class="post-img-grid pig-contain" id="${id}" style="width:${S}px;height:${S}px;grid-template-columns:1fr;grid-template-rows:1fr;">
|
||||||
|
<div class="pig-item pig-contain"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2+ Bilder: Orientierung des ersten Bilds bestimmt das Layout → deferred init
|
||||||
|
_pigStore.set(id, bilder);
|
||||||
|
return `<div class="post-img-grid" id="${id}" style="width:${S}px;height:${S}px;">` +
|
||||||
|
`<img src="data:image/jpeg;base64,${bilder[0]}" style="display:none;position:absolute;" alt="" onload="_pigInit('${id}',this)">` +
|
||||||
|
`</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pigInit(id, probe) {
|
||||||
|
const bilder = _pigStore.get(id);
|
||||||
|
if (!bilder) return;
|
||||||
|
_pigStore.delete(id);
|
||||||
|
const grid = document.getElementById(id);
|
||||||
|
if (!grid) return;
|
||||||
|
const land = probe.naturalWidth >= probe.naturalHeight; // quer = landscape
|
||||||
|
const n = bilder.length;
|
||||||
|
const extra = n - 3;
|
||||||
|
const moreHtml = extra > 0 ? `<div class="pig-more">+${extra}</div>` : '';
|
||||||
|
|
||||||
|
grid.innerHTML = ''; // Probe-Bild entfernen
|
||||||
|
|
||||||
|
// Fraktionale Werte → Browser berücksichtigt gap automatisch, Trennstrich immer genau in der Mitte
|
||||||
|
if (n === 2) {
|
||||||
|
if (land) {
|
||||||
|
// Quer-erstes Bild → beide übereinander (Trennstrich horizontal in der Mitte)
|
||||||
|
grid.style.gridTemplateColumns = '1fr';
|
||||||
|
grid.style.gridTemplateRows = '1fr 1fr';
|
||||||
|
} else {
|
||||||
|
// Hochkant-erstes Bild → beide nebeneinander (Trennstrich vertikal in der Mitte)
|
||||||
|
grid.style.gridTemplateColumns = '1fr 1fr';
|
||||||
|
grid.style.gridTemplateRows = '1fr';
|
||||||
|
}
|
||||||
|
grid.insertAdjacentHTML('beforeend',
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>`);
|
||||||
|
} else {
|
||||||
|
// 3+ Bilder (ab 4 gleiche Darstellung wie bei 3, +N-Overlay auf Zelle 3)
|
||||||
|
// Dunkler Hintergrund nur hier, damit der Spalt zwischen den Zellen nicht auffällt
|
||||||
|
grid.classList.add('pig-dark');
|
||||||
|
grid.style.gridTemplateColumns = '1fr 1fr';
|
||||||
|
grid.style.gridTemplateRows = '1fr 1fr';
|
||||||
|
if (land) {
|
||||||
|
// Quer → Bild 1 oben (volle Breite), Bilder 2+3 nebeneinander unten
|
||||||
|
grid.insertAdjacentHTML('beforeend',
|
||||||
|
`<div class="pig-item" style="grid-column:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
|
||||||
|
} else {
|
||||||
|
// Hochkant → Bild 1 links (volle Höhe), Bilder 2+3 übereinander rechts
|
||||||
|
grid.insertAdjacentHTML('beforeend',
|
||||||
|
`<div class="pig-item" style="grid-row:1/3"><img src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[1]}" alt=""></div>` +
|
||||||
|
`<div class="pig-item"><img src="data:image/jpeg;base64,${bilder[2]}" alt="">${moreHtml}</div>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function carNav(btn, dir) {
|
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 class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
<div id="hpva-${p.postId}">
|
||||||
${bildHtml}
|
<div class="post-text">${renderTextWithHashtags(p.text || '')}</div>
|
||||||
${umfrageHtml}
|
<div id="hpbi-${p.postId}">${bildHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div id="hpea-${p.postId}" style="display:none;"></div>
|
||||||
|
<div id="hpum-${p.postId}">${umfrageHtml}</div>
|
||||||
<div class="post-actions">
|
<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