Files
xxx-sphere-web/bin/main/static/community/event-detail.html
Mario 2b0ce62d33
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Menp überarbeitet
2026-04-08 16:52:43 +02:00

351 lines
18 KiB
HTML
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Veranstaltung xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.back-link { display:inline-flex; align-items:center; gap:0.35rem; color:var(--color-muted); font-size:0.88rem; text-decoration:none; margin-bottom:1rem; }
.back-link:hover { color:var(--color-primary); }
.evt-header { display:flex; gap:1rem; align-items:flex-start; margin-bottom:1.25rem; flex-wrap:wrap; }
.evt-img { width:120px; height:120px; border-radius:12px; background:var(--color-secondary); object-fit:cover; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:3rem; overflow:hidden; border:2px solid var(--color-secondary); }
.evt-img img { width:100%; height:100%; object-fit:cover; }
.evt-meta { flex:1; min-width:0; }
.evt-title { font-size:1.4rem; font-weight:700; margin:0 0 0.3rem; }
.evt-location { color:var(--color-muted); font-size:0.88rem; margin-bottom:0.2rem; }
.evt-date { font-size:0.88rem; color:var(--color-muted); margin-bottom:0.5rem; }
.evt-desc { font-size:0.93rem; line-height:1.55; white-space:pre-wrap; word-break:break-word; margin-top:0.5rem; }
.attend-btn { display:inline-flex; align-items:center; gap:0.4rem; margin-top:0.75rem; flex-wrap:wrap; }
.section-title { font-size:1rem; font-weight:700; margin:1.5rem 0 0.75rem; }
.gender-group { margin-bottom:1.25rem; }
.gender-label { font-size:0.78rem; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--color-muted); margin-bottom:0.5rem; }
.attendee-list { display:flex; flex-wrap:wrap; gap:0.6rem; }
.attendee-chip { display:flex; align-items:center; gap:0.5rem; background:var(--color-card); border:1px solid var(--color-secondary); border-radius:20px; padding:0.3rem 0.6rem 0.3rem 0.3rem; text-decoration:none; color:inherit; transition:border-color 0.15s; font-size:0.85rem; }
.attendee-chip:hover { border-color:var(--color-primary); }
.attendee-avatar { width:28px; height:28px; border-radius:50%; background:var(--color-secondary); object-fit:cover; flex-shrink:0; overflow:hidden; display:flex; align-items:center; justify-content:center; font-size:0.8rem; }
.attendee-avatar img { width:100%; height:100%; object-fit:cover; }
.count-badge { background:var(--color-secondary); border-radius:12px; padding:0.15rem 0.6rem; font-size:0.78rem; color:var(--color-muted); margin-left:0.25rem; display:inline-block; }
/* Modal */
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:200; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.modal { background:var(--color-card); border-radius:12px; width:min(520px,95vw); max-height:90vh; overflow-y:auto; padding:1.5rem; }
.modal h3 { margin:0 0 1rem; }
.modal-footer { display:flex; gap:0.75rem; justify-content:flex-end; margin-top:1.25rem; flex-wrap:wrap; }
.img-preview { width:80px; height:80px; border-radius:8px; object-fit:cover; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:1.5rem; flex-shrink:0; overflow:hidden; border:1px solid var(--color-secondary); }
.img-preview img { width:100%; height:100%; object-fit:cover; }
.img-row { display:flex; gap:0.75rem; align-items:center; margin-bottom:0.5rem; }
</style>
</head>
<body class="app">
<!-- ── Hinweis-Modal ──────────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="alertModal">
<div class="modal" style="width:min(380px,95vw);">
<p id="alertMessage" style="margin:0 0 1.25rem;font-size:0.95rem;"></p>
<div class="modal-footer">
<button class="btn" onclick="document.getElementById('alertModal').classList.remove('open')">OK</button>
</div>
</div>
</div>
<!-- ── Bestätigungs-Modal ─────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="width:min(380px,95vw);">
<h3 id="confirmTitle" style="margin:0 0 0.75rem;"></h3>
<p id="confirmMessage" style="margin:0 0 1.25rem;font-size:0.95rem;color:var(--color-muted);"></p>
<div class="modal-footer">
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeConfirm()">Abbrechen</button>
<button class="btn" id="confirmOkBtn" style="background:#c0392b;">Löschen</button>
</div>
</div>
</div>
<!-- ── Edit-Modal ────────────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="editModal">
<div class="modal">
<h3>Veranstaltung bearbeiten</h3>
<div class="img-row">
<div class="img-preview" id="editPicPreview">🗓</div>
<div>
<label class="btn" style="display:inline-block;cursor:pointer;font-size:0.85rem;">
Bild ändern
<input type="file" id="editPicFile" accept="image/*" style="display:none;" onchange="onEditPicChange(this)">
</label>
</div>
</div>
<label>Titel *</label>
<input type="text" id="editTitle" maxlength="200">
<label>Beschreibung <span style="color:var(--color-muted);font-size:0.8rem;">(max. 1000 Zeichen)</span></label>
<textarea id="editDesc" maxlength="1000" rows="4"
style="resize:vertical;width:100%;box-sizing:border-box;padding:0.65rem 0.9rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:1rem;outline:none;font-family:inherit;transition:border-color 0.2s;"
onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'"></textarea>
<label>Datum &amp; Uhrzeit *</label>
<input type="datetime-local" id="editStartAt">
<div class="modal-footer">
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeEditModal()">Abbrechen</button>
<button class="btn" id="editSubmitBtn" onclick="submitEdit()">Speichern</button>
</div>
</div>
</div>
<div class="main">
<div class="content">
<a id="backLink" href="/community/events.html" class="back-link">← Veranstaltungen</a>
<div id="content">
<p style="color:var(--color-muted);">Wird geladen…</p>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
const params = new URLSearchParams(location.search);
const eventId = params.get('id');
let myUserId = null;
let _evtData = null;
let _editImg = null;
function showAlert(msg) {
document.getElementById('alertMessage').textContent = msg;
document.getElementById('alertModal').classList.add('open');
}
function escHtml(s) {
return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function formatDate(dt) {
if (!dt) return '';
const d = new Date(dt);
return d.toLocaleDateString('de-DE', { weekday:'long', day:'2-digit', month:'long', year:'numeric' })
+ ', ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
}
const GENDER_LABELS = {
WEIBLICH: 'Frauen',
MAENNLICH: 'Männer',
DIVERS: 'Divers',
UNBEKANNT: 'Sonstiges'
};
async function loadPage() {
if (!eventId) { document.getElementById('content').innerHTML = '<p>Keine Event-ID angegeben.</p>'; return; }
const [meRes, evtRes] = await Promise.all([
fetch('/login/me'),
fetch(`/location-events/${eventId}`)
]);
if (!evtRes.ok) { document.getElementById('content').innerHTML = '<p>Veranstaltung nicht gefunden.</p>'; return; }
if (meRes.ok) { const me = await meRes.json(); myUserId = me.userId; }
_evtData = await evtRes.json();
const backLink = document.getElementById('backLink');
if (_evtData.locationId) {
backLink.href = `/community/location-detail.html?id=${_evtData.locationId}`;
backLink.textContent = `${_evtData.locationName || 'Location'}`;
}
document.title = `${_evtData.title} xXx Sphere`;
renderPage(_evtData);
}
function renderPage(evt) {
const imgHtml = evt.imageData
? `<img src="data:image/jpeg;base64,${evt.imageData}" alt="${escHtml(evt.title)}">`
: '🗓';
const isFuture = new Date(evt.startAt) > new Date();
const canEdit = !!evt.isAdmin && isFuture;
const byGender = {};
(evt.attendees || []).forEach(a => {
const g = a.geschlecht || 'UNBEKANNT';
if (!byGender[g]) byGender[g] = [];
byGender[g].push(a);
});
const genderOrder = ['WEIBLICH', 'MAENNLICH', 'DIVERS', 'UNBEKANNT'];
const attendeesHtml = genderOrder
.filter(g => byGender[g] && byGender[g].length > 0)
.map(g => {
const chips = byGender[g].map(a => {
const avatarHtml = a.profilePictureLq
? `<img src="data:image/jpeg;base64,${a.profilePictureLq}" alt="${escHtml(a.name)}">`
: a.name.charAt(0).toUpperCase();
return `<a class="attendee-chip" href="/community/benutzer.html?userId=${a.userId}">
<div class="attendee-avatar">${avatarHtml}</div>
${escHtml(a.name)}
</a>`;
}).join('');
return `<div class="gender-group">
<div class="gender-label">${GENDER_LABELS[g] || g} <span class="count-badge">${byGender[g].length}</span></div>
<div class="attendee-list">${chips}</div>
</div>`;
}).join('');
const totalAttendees = (evt.attendees || []).length;
const attending = evt.attendingMe;
document.getElementById('content').innerHTML = `
<div class="evt-header">
<div class="evt-img">${imgHtml}</div>
<div class="evt-meta">
<div class="evt-title">${escHtml(evt.title)}</div>
${evt.locationName ? `<div class="evt-location">📍 <a href="/community/location-detail.html?id=${evt.locationId}" style="color:inherit;text-decoration:none;">${escHtml(evt.locationName)}</a></div>` : ''}
<div class="evt-date">🗓 ${formatDate(evt.startAt)}</div>
${evt.description ? `<div class="evt-desc">${escHtml(evt.description)}</div>` : ''}
<div style="display:flex;flex-direction:column;gap:0.5rem;margin-top:0.75rem;">
${canEdit ? `
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);font-size:0.85rem;" onclick="openEditModal()">✎ Bearbeiten</button>
<button class="btn" style="background:#c0392b;font-size:0.85rem;" onclick="openDeleteConfirm()">Löschen</button>
</div>` : ''}
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
<button class="btn" id="attendBtn"
style="${attending ? 'background:var(--color-secondary);color:var(--color-text);' : ''}"
onclick="toggleAttend()">
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
</button>
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
</div>
</div>
</div>
</div>
${totalAttendees > 0 ? `
<div class="section-title">Teilnehmende</div>
${attendeesHtml}
` : '<p style="color:var(--color-muted);font-size:0.9rem;margin-top:1rem;">Noch keine Teilnehmenden.</p>'}
`;
}
async function toggleAttend() {
const res = await fetch(`/location-events/${eventId}/attend`, { method: 'POST' });
if (!res.ok) { showAlert('Fehler beim Aktualisieren.'); return; }
const data = await res.json();
const btn = document.getElementById('attendBtn');
const countEl = document.getElementById('attendCount');
if (btn) {
btn.textContent = data.attending ? '✓ Ich bin dabei' : '+ Ich bin dabei';
btn.style.background = data.attending ? 'var(--color-secondary)' : '';
btn.style.color = data.attending ? 'var(--color-text)' : '';
}
if (countEl) countEl.textContent = `${data.attendeeCount} Teilnehmer*in(nen)`;
const evtRes = await fetch(`/location-events/${eventId}`);
if (evtRes.ok) { _evtData = await evtRes.json(); renderPage(_evtData); }
}
// ── Edit Modal ────────────────────────────────────────────────────────────────
function resizeImage(file, maxPx, quality) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
let w = img.naturalWidth, h = img.naturalHeight;
if (w > maxPx || h > maxPx) {
if (w >= h) { h = Math.max(1, Math.round(maxPx * h / w)); w = maxPx; }
else { w = Math.max(1, Math.round(maxPx * w / h)); h = maxPx; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
resolve(canvas.toDataURL('image/jpeg', quality || 0.85).split(',')[1]);
};
img.onerror = reject;
img.src = url;
});
}
function openEditModal() {
if (!_evtData) return;
_editImg = null;
document.getElementById('editTitle').value = _evtData.title || '';
document.getElementById('editDesc').value = _evtData.description || '';
const nowLocal = new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16);
const startVal = _evtData.startAt ? _evtData.startAt.slice(0, 16) : '';
document.getElementById('editStartAt').min = nowLocal;
document.getElementById('editStartAt').value = startVal;
const preview = document.getElementById('editPicPreview');
preview.innerHTML = _evtData.imageData
? `<img src="data:image/jpeg;base64,${_evtData.imageData}" alt="">`
: '🗓';
document.getElementById('editModal').classList.add('open');
}
function closeEditModal() { document.getElementById('editModal').classList.remove('open'); }
async function onEditPicChange(input) {
const file = input.files[0]; if (!file) return;
_editImg = await resizeImage(file, 1024, 0.88);
document.getElementById('editPicPreview').innerHTML = `<img src="data:image/jpeg;base64,${_editImg}" alt="">`;
}
async function submitEdit() {
const title = document.getElementById('editTitle').value.trim();
const desc = document.getElementById('editDesc').value;
const startAt = document.getElementById('editStartAt').value;
if (!title) { showAlert('Bitte gib einen Titel ein.'); return; }
if (!startAt) { showAlert('Bitte wähle Datum und Uhrzeit.'); return; }
if (new Date(startAt) <= new Date()) { showAlert('Der Termin muss in der Zukunft liegen.'); return; }
const btn = document.getElementById('editSubmitBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
try {
const body = { title, description: desc };
if (startAt) body.startAt = startAt + ':00';
if (_editImg) body.imageData = _editImg;
const res = await fetch(`/locations/${_evtData.locationId}/events/${eventId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.status === 422) { showAlert('Der Termin muss in der Zukunft liegen.'); return; }
if (!res.ok) { showAlert('Fehler beim Speichern.'); return; }
_evtData = await res.json();
closeEditModal();
renderPage(_evtData);
document.title = `${_evtData.title} xXx Sphere`;
} catch { showAlert('Fehler beim Speichern.'); }
finally { btn.disabled = false; btn.textContent = 'Speichern'; }
}
// ── Confirm / Delete ──────────────────────────────────────────────────────────
let _confirmCallback = null;
function openConfirm(title, message, onOk) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
_confirmCallback = onOk;
document.getElementById('confirmModal').classList.add('open');
}
function closeConfirm() { document.getElementById('confirmModal').classList.remove('open'); _confirmCallback = null; }
document.getElementById('confirmOkBtn').addEventListener('click', () => { if (_confirmCallback) _confirmCallback(); closeConfirm(); });
function openDeleteConfirm() {
openConfirm(
'Veranstaltung löschen',
'Soll diese Veranstaltung wirklich gelöscht werden? Alle angemeldeten Teilnehmenden werden benachrichtigt.',
async () => {
const res = await fetch(`/locations/${_evtData.locationId}/events/${eventId}`, { method: 'DELETE' });
if (!res.ok) { showAlert('Fehler beim Löschen.'); return; }
location.href = `/community/location-detail.html?id=${_evtData.locationId}`;
}
);
}
loadPage();
</script>
</body>
</html>