Weiter an den Locations gearbeitet
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:
@@ -20,7 +20,7 @@
|
||||
.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; }
|
||||
.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; }
|
||||
@@ -31,23 +31,92 @@
|
||||
.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>
|
||||
<div class="main">
|
||||
<a id="backLink" href="/community/events.html" class="back-link">← Veranstaltungen</a>
|
||||
<body class="app">
|
||||
|
||||
<div id="content">
|
||||
<p style="color:var(--color-muted);">Wird geladen…</p>
|
||||
<!-- ── 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 & 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/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
@@ -78,17 +147,16 @@ async function loadPage() {
|
||||
if (!evtRes.ok) { document.getElementById('content').innerHTML = '<p>Veranstaltung nicht gefunden.</p>'; return; }
|
||||
if (meRes.ok) { const me = await meRes.json(); myUserId = me.userId; }
|
||||
|
||||
const evt = await evtRes.json();
|
||||
_evtData = await evtRes.json();
|
||||
|
||||
// Rücklink zur Location
|
||||
const backLink = document.getElementById('backLink');
|
||||
if (evt.locationId) {
|
||||
backLink.href = `/community/location-detail.html?id=${evt.locationId}`;
|
||||
backLink.textContent = `← ${escHtml(evt.locationName) || 'Location'}`;
|
||||
if (_evtData.locationId) {
|
||||
backLink.href = `/community/location-detail.html?id=${_evtData.locationId}`;
|
||||
backLink.textContent = `← ${_evtData.locationName || 'Location'}`;
|
||||
}
|
||||
document.title = `${evt.title} – xXx Sphere`;
|
||||
document.title = `${_evtData.title} – xXx Sphere`;
|
||||
|
||||
renderPage(evt);
|
||||
renderPage(_evtData);
|
||||
}
|
||||
|
||||
function renderPage(evt) {
|
||||
@@ -96,7 +164,9 @@ function renderPage(evt) {
|
||||
? `<img src="data:image/jpeg;base64,${evt.imageData}" alt="${escHtml(evt.title)}">`
|
||||
: '🗓';
|
||||
|
||||
// Teilnehmende nach Geschlecht gruppieren
|
||||
const isFuture = new Date(evt.startAt) > new Date();
|
||||
const canEdit = !!evt.isAdmin && isFuture;
|
||||
|
||||
const byGender = {};
|
||||
(evt.attendees || []).forEach(a => {
|
||||
const g = a.geschlecht || 'UNBEKANNT';
|
||||
@@ -134,13 +204,20 @@ function renderPage(evt) {
|
||||
${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 class="attend-btn">
|
||||
<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 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>
|
||||
@@ -154,9 +231,8 @@ function renderPage(evt) {
|
||||
|
||||
async function toggleAttend() {
|
||||
const res = await fetch(`/location-events/${eventId}/attend`, { method: 'POST' });
|
||||
if (!res.ok) { alert('Fehler beim Aktualisieren.'); return; }
|
||||
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) {
|
||||
@@ -165,10 +241,107 @@ async function toggleAttend() {
|
||||
btn.style.color = data.attending ? 'var(--color-text)' : '';
|
||||
}
|
||||
if (countEl) countEl.textContent = `${data.attendeeCount} Teilnehmer*in(nen)`;
|
||||
|
||||
// Teilnehmendenliste neu laden
|
||||
const evtRes = await fetch(`/location-events/${eventId}`);
|
||||
if (evtRes.ok) { renderPage(await evtRes.json()); }
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user