Files
xxx-sphere-web/bin/main/static/js/shared.js

238 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─────────────────────────────────────────────────────────────────────────────
// shared.js Gemeinsame Helfer & Komponenten
// Einbinden: <script src="/js/shared.js"></script>
// (vor allen Seiten-Skripten, nach CSS-Links)
// ─────────────────────────────────────────────────────────────────────────────
// ── CSS-Injection (Comment + Carousel) ────────────────────────────────────────
(function injectSharedStyles() {
if (document.getElementById('shared-styles')) return;
const s = document.createElement('style');
s.id = 'shared-styles';
s.textContent = `
/* ── Karussell ── */
.post-carousel{position:relative;margin-top:0.5rem}
.car-slide{display:none}
.car-slide.active{display:block}
.car-btn{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);border:none;color:#fff;font-size:2.2rem;width:auto;min-width:2.4rem;height:3.2rem;border-radius:8px;cursor:pointer;z-index:5;display:flex;align-items:center;justify-content:center;padding:0 0.5rem;margin:0;line-height:1}
.car-prev{left:0.3rem}
.car-next{right:0.3rem}
.car-indicator{text-align:center;font-size:0.75rem;color:var(--color-muted);margin-top:0.25rem}
/* ── Like / Löschen-Buttons ── */
.btn-like{background:none;border:1px solid rgba(255,255,255,0.15);border-radius:20px;padding:0.2rem 0.65rem;color:var(--color-muted);font-size:0.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:0.3rem;margin:0;width:auto;transition:border-color 0.15s,color 0.15s}
.btn-like:hover,.btn-like.liked{border-color:var(--color-primary);color:var(--color-primary)}
.btn-delete-small{background:none;border:none;color:rgba(200,50,50,0.6);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0}
.btn-delete-small:hover{color:var(--color-primary)}
.btn-text{background:none;border:none;color:var(--color-muted);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0;text-decoration:underline;text-decoration-color:rgba(255,255,255,0.2)}
.btn-text:hover{color:var(--color-text)}
/* ── Kommentare ── */
.comment-item{display:flex;gap:0.5rem;margin-bottom:0.5rem}
.comment-avatar{width:28px;height:28px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:0.75rem;flex-shrink:0;overflow:hidden}
.comment-avatar img{width:100%;height:100%;object-fit:cover}
.comment-body{flex:1;background:rgba(255,255,255,0.04);border-radius:6px;padding:0.5rem 0.65rem}
.comment-author{font-size:0.8rem;font-weight:600;color:var(--color-text)}
.comment-date{font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem}
.comment-text{font-size:0.85rem;color:rgba(255,255,255,0.75);margin-top:0.2rem;line-height:1.45;white-space:pre-wrap;word-break:break-word}
.comment-actions{display:flex;gap:0.4rem;margin-top:0.3rem;align-items:center}
.replies-section{margin-top:0.5rem;padding-left:0.5rem;border-left:2px solid rgba(255,255,255,0.06)}
.comment-write{display:flex;gap:0.4rem;margin-top:0.5rem}
.comment-write input{flex:1;padding:0.4rem 0.75rem;font-size:0.85rem}
.comment-write button{width:auto;padding:0.4rem 0.75rem;font-size:0.82rem;white-space:nowrap}
`;
document.head.appendChild(s);
})();
// ── HTML-Escape ────────────────────────────────────────────────────────────────
function esc(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/\n/g, '<br>');
}
// ── Datum-Format ──────────────────────────────────────────────────────────────
function fmtDate(iso) {
if (!iso) return '';
const d = new Date(iso);
return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
// ── Emoji-Picker ──────────────────────────────────────────────────────────────
const EMOJIS = ['😊','😂','❤️','😍','🔥','👍','🥰','😎','🤔','😘','💕','🎉','✨','💋','😈','🫦','🍑','🍆','🔞','🥵','😭','😢','😤','🙄','🤦','🤷','🙏','💪','😏','🤩'];
let _emojiTarget = null;
function toggleEmojiPicker(btn, targetId) {
_emojiTarget = document.getElementById(targetId);
let picker = document.getElementById('sharedEmojiPicker');
if (!picker) {
picker = document.createElement('div');
picker.id = 'sharedEmojiPicker';
picker.style.cssText = 'position:fixed;z-index:9000;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.5rem;display:flex;flex-wrap:wrap;gap:0.2rem;max-width:260px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
EMOJIS.forEach(em => {
const b = document.createElement('button');
b.textContent = em;
b.style.cssText = 'background:none;border:none;font-size:1.3rem;cursor:pointer;padding:0.2rem;margin:0;width:auto;line-height:1;';
b.onclick = e => { e.stopPropagation(); insertEmoji(em); };
picker.appendChild(b);
});
document.body.appendChild(picker);
}
if (picker.style.display === 'flex') { picker.style.display = 'none'; return; }
picker.style.display = 'flex';
requestAnimationFrame(() => {
const rect = btn.getBoundingClientRect();
const ph = picker.offsetHeight, pw = picker.offsetWidth;
let top = rect.top - ph - 8;
let left = rect.left;
if (top < 8) top = rect.bottom + 8;
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
picker.style.top = top + 'px';
picker.style.left = left + 'px';
});
}
function insertEmoji(emoji) {
if (!_emojiTarget) return;
const start = _emojiTarget.selectionStart ?? _emojiTarget.value.length;
const end = _emojiTarget.selectionEnd ?? start;
_emojiTarget.value = _emojiTarget.value.slice(0, start) + emoji + _emojiTarget.value.slice(end);
_emojiTarget.selectionStart = _emojiTarget.selectionEnd = start + emoji.length;
_emojiTarget.focus();
}
document.addEventListener('click', e => {
const picker = document.getElementById('sharedEmojiPicker');
if (picker && picker.style.display === 'flex'
&& !picker.contains(e.target)
&& !e.target.closest('[onclick*="toggleEmojiPicker"]')) {
picker.style.display = 'none';
}
});
// ── Bild-Karussell ────────────────────────────────────────────────────────────
function bilderCarousel(bilder) {
if (!bilder || bilder.length === 0) return '';
if (bilder.length === 1) {
return `<div style="margin-top:0.5rem;"><img class="post-bild" src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>`;
}
const slides = bilder.map((b, i) =>
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
).join('');
return `<div class="post-carousel">
${slides}
<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">&#8249;</button>
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">&#8250;</button>
<div class="car-indicator"><span class="car-cur">1</span>/${bilder.length}</div>
</div>`;
}
function carNav(btn, dir) {
const car = btn.closest('.post-carousel');
const slides = Array.from(car.querySelectorAll('.car-slide'));
const cur = slides.findIndex(s => s.classList.contains('active'));
slides[cur].classList.remove('active');
const next = (cur + dir + slides.length) % slides.length;
slides[next].classList.add('active');
const ind = car.querySelector('.car-cur');
if (ind) ind.textContent = next + 1;
}
// ── Kommentar-Rendering ───────────────────────────────────────────────────────
// opts: { myUserId, showReplies }
// Seite muss definieren: deleteKommentar(kommentarId, targetType, targetId)
function renderKommentarHtml(k, targetType, targetId, opts) {
const { myUserId = null, showReplies = false } = opts || {};
const avatarHtml = k.authorPicture
? `<img src="data:image/png;base64,${k.authorPicture}" alt="">`
: '◉';
const canDelete = k.authorId === myUserId;
const replyLabel = k.replyCount > 0 ? `Antworten (${k.replyCount})` : 'Antworten';
return `<div class="comment-item" id="kom-${k.kommentarId}">
<div class="comment-avatar">${avatarHtml}</div>
<div class="comment-body">
<span class="comment-author">${esc(k.authorName)}</span>
<span class="comment-date">${fmtDate(k.createdAt)}</span>
<div class="comment-text">${esc(k.text)}</div>
<div class="comment-actions">
<button class="btn-like${k.likedByMe ? ' liked' : ''}" id="lk-kom-${k.kommentarId}"
onclick="toggleKommentarLike('${k.kommentarId}')">♥ <span id="lkc-kom-${k.kommentarId}">${k.likeCount}</span></button>
${showReplies ? `<button class="btn-text" onclick="toggleReplies('${k.kommentarId}')">${replyLabel}</button>` : ''}
${canDelete ? `<button class="btn-delete-small" onclick="deleteKommentar('${k.kommentarId}','${targetType}','${targetId}')">✕</button>` : ''}
</div>
${showReplies ? `<div class="replies-section" id="replies-${k.kommentarId}" style="display:none;"></div>` : ''}
</div>
</div>`;
}
function renderReplyHtml(r, parentId) {
const avatarHtml = r.authorPicture
? `<img src="data:image/png;base64,${r.authorPicture}" alt="">`
: '◉';
const canDelete = typeof window.myUserId !== 'undefined' && r.authorId === window.myUserId;
return `<div class="comment-item" id="kom-${r.kommentarId}" style="margin-bottom:0.35rem;">
<div class="comment-avatar" style="width:22px;height:22px;font-size:0.75rem;">${avatarHtml}</div>
<div class="comment-body" style="padding:0.35rem 0.55rem;">
<span class="comment-author">${esc(r.authorName)}</span>
<span class="comment-date">${fmtDate(r.createdAt)}</span>
<div class="comment-text">${esc(r.text)}</div>
<div class="comment-actions">
<button class="btn-like${r.likedByMe ? ' liked' : ''}" id="lk-kom-${r.kommentarId}"
onclick="toggleKommentarLike('${r.kommentarId}')">♥ <span id="lkc-kom-${r.kommentarId}">${r.likeCount}</span></button>
${canDelete ? `<button class="btn-delete-small" onclick="deleteReply('${r.kommentarId}','${parentId}')">✕</button>` : ''}
</div>
</div>
</div>`;
}
async function toggleKommentarLike(kommentarId) {
await fetch('/social/kommentare/' + kommentarId + '/like', { method: 'POST' });
const btn = document.getElementById('lk-kom-' + kommentarId);
const lc = document.getElementById('lkc-kom-' + kommentarId);
if (!btn || !lc) return;
const was = btn.classList.contains('liked');
btn.classList.toggle('liked', !was);
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
}
async function toggleReplies(kommentarId) {
const section = document.getElementById('replies-' + kommentarId);
if (section.style.display === 'none') {
section.style.display = '';
await loadReplies(kommentarId);
} else {
section.style.display = 'none';
}
}
async function loadReplies(kommentarId) {
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
const replies = await res.json();
const section = document.getElementById('replies-' + kommentarId);
section.innerHTML = (replies.length === 0
? '<p style="color:var(--color-muted);font-size:0.78rem;margin-bottom:0.35rem;">Noch keine Antworten.</p>'
: replies.map(r => renderReplyHtml(r, kommentarId)).join(''))
+ `<div class="comment-write">
<input type="text" id="ri-${kommentarId}" placeholder="Antwort schreiben…" maxlength="500"
onkeydown="if(event.key==='Enter') postReply('${kommentarId}')">
<button onclick="postReply('${kommentarId}')">Senden</button>
</div>`;
}
async function postReply(kommentarId) {
const input = document.getElementById('ri-' + kommentarId);
const text = input.value.trim();
if (!text) return;
await fetch('/social/kommentare', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetType: 'KOMMENTAR', targetId: kommentarId, text })
});
input.value = '';
await loadReplies(kommentarId);
}
async function deleteReply(replyId, parentId) {
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
await loadReplies(parentId);
}