Aufgabenverwaltung angepasst, Eventseite weiter bearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-14 22:04:47 +02:00
parent e35b095c18
commit fdc0cfce95
43 changed files with 861 additions and 370 deletions

View File

@@ -120,7 +120,8 @@
.gruppe-info { font-size:0.75rem; color:var(--color-muted); margin-top:0.2rem; }
.gruppe-badges { display:flex; gap:0.3rem; margin-top:0.25rem; flex-wrap:wrap; }
.gruppe-badge { font-size:0.65rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(255,255,255,0.07); color:var(--color-muted); }
.gruppe-badge-public { background:rgba(46,204,113,0.15); color:var(--color-success); }
.gruppe-badge-public { background:rgba(46,204,113,0.15); color:var(--color-success); }
.gruppe-badge-vanilla { background:#e8f5e9; color:#2e7d32; border:1px solid #a5d6a7; }
.gruppe-toggle { font-size:0.75rem; color:var(--color-muted); flex-shrink:0; transition:transform 0.2s; }
.gruppe-card.open .gruppe-toggle { transform:rotate(90deg); }
.gruppe-body { border-top:1px solid var(--color-secondary); padding:1rem 1rem 0.75rem; }
@@ -277,6 +278,12 @@
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild neues wählen zum Ersetzen</span>
</div>
<input type="file" id="gBild" accept="image/*">
<label style="margin-top:0.5rem;">
<span class="modal-check">
<input type="checkbox" id="gVanilla">
Auch für Vanilla-Game verfügbar
</span>
</label>
<div class="modal-error" id="gruppeModalError"></div>
<div class="modal-actions">
<button class="btn-cancel" id="gruppeModalCancel">Abbrechen</button>
@@ -986,7 +993,7 @@ function renderAdminGruppen(gruppen) {
<div class="gruppe-meta">
<div class="gruppe-name">${esc(g.name)}</div>
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
<div class="gruppe-badges"><span class="gruppe-badge gruppe-badge-public">Öffentlich</span></div>
<div class="gruppe-badges"><span class="gruppe-badge gruppe-badge-public">Öffentlich</span>${g.vanillaAvailable ? '<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>' : ''}</div>
</div>
<span class="gruppe-toggle">▶</span>
</div>
@@ -1262,6 +1269,7 @@ function openGruppeModal(editId) {
document.getElementById('gName').value = g.name || '';
document.getElementById('gVon').value = g.von || '';
document.getElementById('gDesc').value = g.beschreibung || '';
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
const imgWrap = document.getElementById('gCurrentImgWrap');
if (g.bild) { document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild; imgWrap.style.display = 'flex'; }
else imgWrap.style.display = 'none';
@@ -1270,6 +1278,7 @@ function openGruppeModal(editId) {
document.getElementById('gName').value = '';
document.getElementById('gVon').value = '';
document.getElementById('gDesc').value = '';
document.getElementById('gVanilla').checked = false;
document.getElementById('gCurrentImgWrap').style.display = 'none';
}
gruppeModal.classList.add('open');
@@ -1348,7 +1357,7 @@ gruppeModalSave.addEventListener('click', async () => {
let bildBase64 = null;
const fi = document.getElementById('gBild');
if (fi.files.length > 0) bildBase64 = await toBase64(fi.files[0]);
const payload = { name, von: document.getElementById('gVon').value.trim() || null, beschreibung: document.getElementById('gDesc').value.trim() || null, bild: bildBase64 };
const payload = { name, von: document.getElementById('gVon').value.trim() || null, beschreibung: document.getElementById('gDesc').value.trim() || null, vanillaAvailable: document.getElementById('gVanilla').checked, bild: bildBase64 };
const isEdit = currentEditGruppeId != null;
fetch(isEdit ? `/admin/aufgabengruppen/${currentEditGruppeId}` : '/admin/aufgabengruppen', {
method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)

View File

@@ -195,6 +195,7 @@ function renderPage(evt) {
const totalAttendees = (evt.attendees || []).length;
const attending = evt.attendingMe;
const isPast = new Date(evt.startAt) < new Date();
document.getElementById('content').innerHTML = `
<div class="evt-header">
@@ -211,11 +212,13 @@ function renderPage(evt) {
<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>
${isPast
? `<span style="color:var(--color-muted);font-size:0.85rem;">Veranstaltung bereits beendet</span>`
: `<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>

View File

@@ -445,6 +445,9 @@ function renderPage() {
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
</div>
<div class="event-list" id="eventList"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></div>
<div id="allEventsLinkWrap" style="display:none;margin-top:0.75rem;">
<a id="allEventsLink" href="#" class="btn" style="display:inline-block;font-size:0.85rem;background:var(--color-secondary);color:var(--color-text);text-decoration:none;padding:0.45rem 1rem;border-radius:6px;">Alle Events anzeigen →</a>
</div>
<div id="pastEventsSection" style="display:none;">
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
<div class="event-list" id="pastEventList"></div>
@@ -517,8 +520,8 @@ function renderPage() {
${locHeaderHtml}
${hoursHtml}
${gallerySection}
${feedSection}
${eventsSection}`;
${eventsSection}
${feedSection}`;
}
}
@@ -587,15 +590,27 @@ async function loadEvents() {
if (!list) return;
const now = new Date();
const future = events.filter(e => new Date(e.startAt) >= now);
const future = events.filter(e => new Date(e.startAt) >= now)
.sort((a, b) => new Date(a.startAt) - new Date(b.startAt));
const past = events.filter(e => new Date(e.startAt) < now)
.slice(-5) // letzte 5
.slice(-3) // letzte 3
.reverse(); // neueste zuerst
list.innerHTML = future.length
? future.map(e => buildEventCard(e, false)).join('')
const preview = future.slice(0, 3);
list.innerHTML = preview.length
? preview.map(e => buildEventCard(e, false)).join('')
: '<p style="color:var(--color-muted);font-size:0.9rem;">Keine bevorstehenden Veranstaltungen.</p>';
const linkWrap = document.getElementById('allEventsLinkWrap');
if (linkWrap) {
if (future.length > 3) {
document.getElementById('allEventsLink').href = `/community/location-events.html?id=${locationId}`;
linkWrap.style.display = '';
} else {
linkWrap.style.display = 'none';
}
}
const pastSection = document.getElementById('pastEventsSection');
if (past.length && pastSection) {
document.getElementById('pastEventList').innerHTML = past.map(e => buildEventCard(e, true)).join('');
@@ -1358,11 +1373,15 @@ function renderLocPost(p) {
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteLocPost('${p.postId}')" title="Löschen">🗑</button>
</div>` : '';
const authorUrl = p.posterType === 'LOCATION'
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
: `/community/benutzer.html?userId=${p.authorId}`;
return `<div class="post-card${clickableClass}" id="lp-${p.postId}"${onClickAttr}>
<div class="post-header">
<div class="post-avatar">${avHtml}</div>
<div class="post-avatar"><a href="${authorUrl}" onclick="event.stopPropagation()" style="display:contents;">${avHtml}</a></div>
<div>
<div class="post-author">${escHtml(p.authorName || p.locationName || '')}</div>
<div class="post-author"><a href="${authorUrl}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${escHtml(p.authorName || p.locationName || '')}</a></div>
<div class="post-meta">${dateStr}${editedHtml}</div>
</div>
${adminBtns}
@@ -1401,11 +1420,15 @@ function openLpLb(postId) {
+ ' ' + 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 lbAuthorUrl = p.posterType === 'LOCATION'
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
: `/community/benutzer.html?userId=${p.authorId}`;
document.getElementById('lbPostBody').innerHTML = `
<div class="post-header">
<div class="post-avatar">${avHtml}</div>
<div class="post-avatar"><a href="${lbAuthorUrl}" style="display:contents;">${avHtml}</a></div>
<div>
<div class="post-author">${escHtml(p.authorName || p.locationName || '')}</div>
<div class="post-author"><a href="${lbAuthorUrl}" style="color:inherit;text-decoration:none;">${escHtml(p.authorName || p.locationName || '')}</a></div>
<div class="post-meta">${dateStr}${editedHtml}</div>
</div>
</div>

View File

@@ -0,0 +1,164 @@
<!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>Alle Events xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/community.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); }
.page-title { font-size:1.15rem; font-weight:700; margin:0 0 1.25rem; }
.event-list { display:flex; flex-direction:column; gap:0.75rem; }
.event-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; display:flex; gap:0.75rem; padding:0.75rem; text-decoration:none; color:inherit; transition:border-color 0.15s; cursor:pointer; }
.event-card:hover { border-color:var(--color-primary); }
.event-card-img { width:64px; height:64px; border-radius:8px; object-fit:cover; background:var(--color-secondary); flex-shrink:0; overflow:hidden; display:flex; align-items:center; justify-content:center; font-size:1.4rem; }
.event-card-img img { width:100%; height:100%; object-fit:cover; }
.event-card-body { flex:1; min-width:0; }
.event-card-title { font-weight:600; font-size:0.92rem; margin:0 0 0.2rem; }
.event-card-date { font-size:0.78rem; color:var(--color-muted); }
.event-card-attendees { font-size:0.78rem; color:var(--color-muted); }
.paging-bar { display:flex; align-items:center; justify-content:center; gap:0.75rem; margin-top:1.25rem; flex-wrap:wrap; }
.paging-bar button { background:var(--color-secondary); border:none; color:var(--color-text); padding:0.45rem 1rem; border-radius:6px; font-size:0.88rem; cursor:pointer; transition:background 0.15s; }
.paging-bar button:hover:not(:disabled) { background:var(--color-primary); color:#fff; }
.paging-bar button:disabled { opacity:0.4; cursor:default; }
.paging-info { font-size:0.85rem; color:var(--color-muted); }
.empty-hint { color:var(--color-muted); font-size:0.9rem; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<a class="back-link" id="backLink" href="#"> Zurück zur Location</a>
<div id="locName" class="page-title">Alle Events</div>
<div id="eventList" class="event-list">
<p class="empty-hint">Wird geladen…</p>
</div>
<div class="paging-bar" id="pagingBar" style="display:none;">
<button id="prevBtn" onclick="changePage(-1)" disabled> Zurück</button>
<span class="paging-info" id="pagingInfo"></span>
<button id="nextBtn" onclick="changePage(1)">Weiter </button>
</div>
</div>
</div>
<script src="/js/nav.js"></script>
<script>
const PAGE_SIZE = 10;
const params = new URLSearchParams(location.search);
const locationId = params.get('id');
let allEvents = [];
let currentPage = 0;
document.getElementById('backLink').href = `/community/location-detail.html?id=${locationId}`;
function escHtml(s) {
if (!s) return '';
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function formatDate(iso) {
if (!iso) return '';
const d = new Date(iso);
return d.toLocaleDateString('de-DE', { weekday:'short', day:'2-digit', month:'2-digit', year:'numeric' })
+ ' ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
}
function buildEventCard(e) {
const imgHtml = e.imageData
? `<img src="data:image/jpeg;base64,${e.imageData}" alt="${escHtml(e.title)}">`
: '🗓';
return `
<a class="event-card" href="/community/event-detail.html?id=${e.eventId}">
<div class="event-card-img">${imgHtml}</div>
<div class="event-card-body">
<div class="event-card-title">${escHtml(e.title)}</div>
<div class="event-card-date">${formatDate(e.startAt)}</div>
<div class="event-card-attendees">${e.attendeeCount} Teilnehmer*in(nen)${e.attendingMe ? ' · Du nimmst teil' : ''}</div>
</div>
</a>`;
}
function renderPage() {
const list = document.getElementById('eventList');
const totalPages = Math.ceil(allEvents.length / PAGE_SIZE);
const start = currentPage * PAGE_SIZE;
const slice = allEvents.slice(start, start + PAGE_SIZE);
list.innerHTML = slice.length
? slice.map(buildEventCard).join('')
: '<p class="empty-hint">Keine weiteren Veranstaltungen.</p>';
const pagingBar = document.getElementById('pagingBar');
if (allEvents.length > PAGE_SIZE) {
pagingBar.style.display = '';
document.getElementById('prevBtn').disabled = currentPage === 0;
document.getElementById('nextBtn').disabled = currentPage >= totalPages - 1;
document.getElementById('pagingInfo').textContent =
`Seite ${currentPage + 1} von ${totalPages} (${allEvents.length} Events)`;
} else {
pagingBar.style.display = 'none';
}
}
function changePage(dir) {
const totalPages = Math.ceil(allEvents.length / PAGE_SIZE);
const next = currentPage + dir;
if (next < 0 || next >= totalPages) return;
currentPage = next;
renderPage();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
async function init() {
if (!locationId) {
document.getElementById('eventList').innerHTML = '<p class="empty-hint">Keine Location-ID angegeben.</p>';
return;
}
// Locationname laden
const locRes = await fetch(`/locations/${locationId}`);
if (locRes.ok) {
const loc = await locRes.json();
document.getElementById('locName').textContent = `Alle Events ${loc.name}`;
document.title = `Events ${loc.name} xXx Sphere`;
}
// Events laden
const res = await fetch(`/locations/${locationId}/events`);
if (!res.ok) {
document.getElementById('eventList').innerHTML = '<p class="empty-hint">Events konnten nicht geladen werden.</p>';
return;
}
const events = await res.json();
const now = new Date();
allEvents = events
.filter(e => new Date(e.startAt) >= now)
.sort((a, b) => new Date(a.startAt) - new Date(b.startAt));
if (allEvents.length === 0) {
document.getElementById('eventList').innerHTML = '<p class="empty-hint">Keine bevorstehenden Veranstaltungen.</p>';
return;
}
renderPage();
}
init();
</script>
</body>
</html>

View File

@@ -95,6 +95,7 @@
}
.gruppe-badge-private { background: rgba(233,69,96,0.15); color: var(--color-primary); }
.gruppe-badge-public { background: rgba(46,204,113,0.15); color: var(--color-success); }
.gruppe-badge-vanilla { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
@@ -367,6 +368,12 @@
Gruppe veröffentlichen (für alle sichtbar)
</span>
</label>
<label>
<span class="modal-check">
<input type="checkbox" id="gVanilla">
Auch für Vanilla-Game verfügbar
</span>
</label>
<div class="modal-error" id="modalError"></div>
<div class="modal-actions">
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
@@ -715,6 +722,7 @@
if (g.privateGruppe) badges.push(`<span class="gruppe-badge gruppe-badge-private">Privat</span>`);
else badges.push(`<span class="gruppe-badge gruppe-badge-public">Öffentlich</span>`);
if (type === 'user' && g.subscriberCount > 0) badges.push(`<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`);
if (g.vanillaAvailable) badges.push(`<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>`);
return `
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
@@ -1069,6 +1077,7 @@
pubCb.checked = !g.privateGruppe;
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
document.getElementById('gPublicLabel').style.display = 'block';
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
const imgWrap = document.getElementById('gCurrentImgWrap');
if (g.bild) {
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
@@ -1086,6 +1095,7 @@
document.getElementById('gDesc').value = '';
document.getElementById('gPublic').checked = false;
document.getElementById('gPublicLabel').style.display = 'none';
document.getElementById('gVanilla').checked = false;
document.getElementById('gCurrentImgWrap').style.display = 'none';
gruppeModal.classList.add('open');
document.getElementById('gName').focus();
@@ -1119,6 +1129,7 @@
name,
beschreibung: document.getElementById('gDesc').value.trim() || null,
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
vanillaAvailable: document.getElementById('gVanilla').checked,
bild: bildBase64
};

View File

@@ -97,6 +97,7 @@
.item-img { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.gruppe-item-name, .toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
.gruppe-item-desc, .toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
.vanilla-badge { display: inline-block; font-size: 0.65rem; font-weight: 700; padding: 0.1rem 0.35rem; border-radius: 3px; background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; margin-left: 0.35rem; vertical-align: middle; letter-spacing: 0.03em; }
.empty-hint { color: var(--color-muted); font-size: 0.875rem; font-style: italic; padding: 0.5rem 0; }
.aufgaben-section-label { font-size: 0.78rem; font-weight: 600; color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 1rem 0 0.5rem 0; }
.aufgaben-section-label:first-child { margin-top: 0; }
@@ -653,9 +654,10 @@
}
ul.innerHTML = gruppen.map(g => {
const checked = savedGruppen.has(g.gruppenId);
const vanillaBadge = g.vanillaAvailable ? '<span class="vanilla-badge">Vanilla</span>' : '';
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
<span><span class="gruppe-item-name">${g.name}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
<span><span class="gruppe-item-name">${g.name}${vanillaBadge}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
</label></li>`;
}).join('');

View File

@@ -887,11 +887,14 @@
</div>`
: '';
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
const authorUrl = p.posterType === 'LOCATION'
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
: `/community/benutzer.html?userId=${p.authorId}`;
return `<div class="post-card" id="hpc-${p.postId}"${gruppeIdAttr} onclick="homeOpenPost('${p.postId}')">
<div class="post-header">
<div class="post-avatar">${avatarHtml}</div>
<div class="post-avatar"><a href="${authorUrl}" onclick="event.stopPropagation()" style="display:contents;">${avatarHtml}</a></div>
<div>
<div class="post-author">${esc(p.authorName)}${privacyLabel}</div>
<div class="post-author"><a href="${authorUrl}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${esc(p.authorName)}</a>${privacyLabel}</div>
<div class="post-meta" id="hpm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
</div>
${ownBtns}