Files
xxx-sphere-web/bin/main/static/js/image-viewer.js
Mario b81ad25c9f
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Bugfixes im Dating und im Profil
2026-04-04 15:45:55 +02:00

238 lines
12 KiB
JavaScript
Raw Permalink 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.
// ─────────────────────────────────────────────────────────────────────────────
// image-viewer.js Universelle Bild-Lightbox
//
// Einbinden: <script src="/js/shared.js"></script> (vorher)
// <script src="/js/image-viewer.js"></script>
//
// Zwei Modi:
// Modus A Nur Bild (kein Like, keine Kommentare):
// imageViewer.open({ images: [{ src }] })
//
// Modus B Galerie mit Like + Kommentare:
// imageViewer.open({
// images: [{ src, id, likedByMe, likeCount }],
// index: 0,
// showLike: true,
// showComments: true,
// myUserId: '...',
// onLike: async (img) => {} // optional; sonst POST /social/profile-images/{id}/like
// })
//
// Globale Instanz: window.imageViewer
// ─────────────────────────────────────────────────────────────────────────────
class ImageViewer {
constructor() {
this._cfg = null;
this._idx = 0;
this.isOpen = false;
this._injectStyles();
this._injectHTML();
this._bindEvents();
}
// ── Öffentliche API ───────────────────────────────────────────────────────
open(cfg) {
this._cfg = cfg;
this._idx = cfg.index || 0;
this.isOpen = true;
const multi = cfg.images.length > 1;
const showLike = !!cfg.showLike;
const showCom = !!cfg.showComments;
this._q('ivPrev').style.display = multi ? '' : 'none';
this._q('ivNext').style.display = multi ? '' : 'none';
this._q('ivCounter').style.display = multi ? '' : 'none';
this._q('ivLikeBtn').style.display = showLike ? '' : 'none';
this._q('ivComments').style.display = showCom ? '' : 'none';
this._render();
this._q('imageViewer').classList.add('open');
this._updateLayout();
}
close() {
this._q('imageViewer').classList.remove('open');
this.isOpen = false;
this._cfg = null;
}
/** Kommentare im offenen Viewer neu laden (z.B. nach externem Löschen) */
reloadComments() {
if (this.isOpen && this._cfg?.showComments) this._loadComments();
}
// ── Internes Rendering ────────────────────────────────────────────────────
_q(id) { return document.getElementById(id); }
_render() {
const img = this._cfg.images[this._idx];
this._q('ivImg').src = img.src;
const total = this._cfg.images.length;
this._q('ivCounter').textContent = `${this._idx + 1} / ${total}`;
this._q('ivPrev').disabled = this._idx === 0;
this._q('ivNext').disabled = this._idx === total - 1;
if (this._cfg.showLike) this._syncLike();
if (this._cfg.showComments) this._loadComments();
}
_syncLike() {
const img = this._cfg.images[this._idx];
const btn = this._q('ivLikeBtn');
btn.className = 'btn-like' + (img.likedByMe ? ' liked' : '');
this._q('ivLikeCount').textContent = img.likeCount;
}
async _loadComments() {
const img = this._cfg.images[this._idx];
const res = await fetch(`/social/kommentare?targetType=IMAGE&targetId=${img.id}`);
const comments = await res.json();
const myUserId = this._cfg.myUserId || null;
this._q('ivCommentsList').innerHTML = comments.length === 0
? '<p style="color:var(--color-muted);font-size:0.82rem;margin-bottom:0.4rem;">Noch keine Kommentare.</p>'
: comments.map(k => renderKommentarHtml(k, 'IMAGE', img.id, { myUserId, showReplies: true })).join('');
}
async _postComment() {
const input = this._q('ivCommentInput');
const text = input.value.trim();
if (!text) return;
const img = this._cfg.images[this._idx];
await fetch('/social/kommentare', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetType: 'IMAGE', targetId: img.id, text })
});
input.value = '';
await this._loadComments();
}
async _toggleLike() {
const img = this._cfg.images[this._idx];
const onLike = this._cfg.onLike;
img.likedByMe = !img.likedByMe;
img.likeCount += img.likedByMe ? 1 : -1;
this._syncLike();
try {
if (onLike) await onLike(img);
else await fetch('/social/profile-images/' + img.id + '/like', { method: 'POST' });
} catch {
img.likedByMe = !img.likedByMe;
img.likeCount += img.likedByMe ? 1 : -1;
this._syncLike();
}
}
_prev() { if (this._idx > 0) { this._idx--; this._render(); } }
_next() { if (this._idx < this._cfg.images.length - 1) { this._idx++; this._render(); } }
_updateLayout() {
const el = this._q('ivLayout');
if (!el) return;
const bp = parseInt(getComputedStyle(document.documentElement)
.getPropertyValue('--breakpoint-mobile').trim()) || 768;
el.classList.toggle('iv-narrow', window.innerWidth <= bp);
}
// ── CSS + HTML Injection ──────────────────────────────────────────────────
_injectStyles() {
if (document.getElementById('iv-styles')) return;
const s = document.createElement('style');
s.id = 'iv-styles';
s.textContent = `
#imageViewer{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.88);z-index:500;align-items:center;justify-content:center;padding:2rem}
#imageViewer.open{display:flex}
#ivLayout{display:flex;flex-direction:row;gap:1rem;height:min(90vh,1024px);max-width:min(1340px,calc(100vw - 4rem));align-items:stretch}
#ivImageSide{width:1024px;flex-shrink:1;min-width:0;display:flex;flex-direction:column}
.iv-image-box{flex:1;position:relative;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center}
#ivImg{width:100%;height:100%;object-fit:contain;display:block}
.iv-overlay{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(transparent,rgba(0,0,0,0.6));border-radius:0 0 12px 12px;padding:2rem 0.75rem 0.6rem;display:flex;align-items:center;justify-content:space-between;gap:0.5rem}
.iv-nav-btn{background:rgba(0,0,0,0.35);border:1px solid rgba(255,255,255,0.2);border-radius:6px;color:#fff;padding:0.3rem 0.75rem;cursor:pointer;margin:0;width:auto;font-size:1rem;flex-shrink:0;transition:background 0.15s}
.iv-nav-btn:hover{background:rgba(0,0,0,0.65)}
.iv-nav-btn:disabled{opacity:.25;cursor:default}
.iv-overlay-center{display:flex;align-items:center;gap:0.6rem;flex:1;justify-content:center}
#ivCounter{font-size:0.8rem;color:rgba(255,255,255,0.75)}
.iv-close{position:fixed;top:1rem;right:1rem;background:rgba(0,0,0,0.55);border:1px solid rgba(255,255,255,0.2);color:#fff;font-size:1.1rem;width:2.2rem;height:2.2rem;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;margin:0;z-index:502;transition:background 0.15s}
.iv-close:hover{background:rgba(180,30,30,0.8)}
#ivComments{background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;width:280px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden}
.iv-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}
#ivCommentsList{flex:1;overflow-y:auto;padding:0.65rem 0.75rem;scrollbar-width:thin;scrollbar-color:var(--color-secondary) transparent}
.iv-comment-compose{display:flex;gap:0.4rem;padding:0.65rem 0.75rem;border-top:1px solid var(--color-secondary);flex-shrink:0;align-items:center}
.iv-comment-compose input{flex:1;padding:0.4rem 0.65rem;font-size:0.85rem}
.iv-comment-compose button{width:auto;padding:0.4rem 0.7rem;font-size:0.82rem;white-space:nowrap}
#ivLayout.iv-narrow{flex-direction:column;height:auto;max-height:90vh;overflow-y:auto;width:calc(100vw - 1rem);max-width:calc(100vw - 1rem)}
#ivLayout.iv-narrow #ivImageSide{width:100%;flex-shrink:0}
#ivLayout.iv-narrow .iv-image-box{height:min(45vh,360px);flex:none}
#ivLayout.iv-narrow #ivComments{width:100%;max-height:40vh;flex-shrink:0}
`;
document.head.appendChild(s);
}
_injectHTML() {
if (document.getElementById('imageViewer')) return;
const div = document.createElement('div');
div.id = 'imageViewer';
div.innerHTML = `
<button class="iv-close" id="ivClose">✕</button>
<div id="ivLayout">
<div id="ivImageSide">
<div class="iv-image-box">
<img id="ivImg" src="" alt="">
<div class="iv-overlay">
<button class="iv-nav-btn" id="ivPrev">&#8592;</button>
<div class="iv-overlay-center">
<span id="ivCounter"></span>
<button class="btn-like" id="ivLikeBtn">♥ <span id="ivLikeCount">0</span></button>
</div>
<button class="iv-nav-btn" id="ivNext">&#8594;</button>
</div>
</div>
</div>
<div id="ivComments">
<div class="iv-comments-header">Kommentare</div>
<div id="ivCommentsList"></div>
<div class="iv-comment-compose">
<input type="text" id="ivCommentInput" placeholder="Kommentar schreiben…" maxlength="500">
<button type="button" onclick="toggleEmojiPicker(this,'ivCommentInput')" title="Emoji"
style="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;">😊</button>
<button id="ivCommentSend">Senden</button>
</div>
</div>
</div>`;
document.body.appendChild(div);
}
_bindEvents() {
const init = () => {
this._q('ivClose').addEventListener('click', () => this.close());
this._q('imageViewer').addEventListener('click', e => {
if (e.target === this._q('imageViewer')) this.close();
});
this._q('ivPrev').addEventListener('click', () => this._prev());
this._q('ivNext').addEventListener('click', () => this._next());
this._q('ivLikeBtn').addEventListener('click', () => this._toggleLike());
this._q('ivCommentSend').addEventListener('click', () => this._postComment());
this._q('ivCommentInput').addEventListener('keydown', e => {
if (e.key === 'Enter') this._postComment();
});
window.addEventListener('resize', () => this._updateLayout());
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
document.addEventListener('keydown', e => {
if (!this.isOpen) return;
if (e.key === 'Escape') this.close();
if (e.key === 'ArrowLeft') this._prev();
if (e.key === 'ArrowRight') this._next();
});
}
}
window.imageViewer = new ImageViewer();