Aufgabenverwaltung angepasst, Eventseite weiter bearbeitet
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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -121,6 +121,7 @@
|
|||||||
.gruppe-badges { display:flex; gap:0.3rem; margin-top:0.25rem; flex-wrap:wrap; }
|
.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 { 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-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-card.open .gruppe-toggle { transform:rotate(90deg); }
|
||||||
.gruppe-body { border-top:1px solid var(--color-secondary); padding:1rem 1rem 0.75rem; }
|
.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>
|
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues wählen zum Ersetzen</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="gBild" accept="image/*">
|
<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-error" id="gruppeModalError"></div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn-cancel" id="gruppeModalCancel">Abbrechen</button>
|
<button class="btn-cancel" id="gruppeModalCancel">Abbrechen</button>
|
||||||
@@ -986,7 +993,7 @@ function renderAdminGruppen(gruppen) {
|
|||||||
<div class="gruppe-meta">
|
<div class="gruppe-meta">
|
||||||
<div class="gruppe-name">${esc(g.name)}</div>
|
<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-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>
|
</div>
|
||||||
<span class="gruppe-toggle">▶</span>
|
<span class="gruppe-toggle">▶</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1262,6 +1269,7 @@ function openGruppeModal(editId) {
|
|||||||
document.getElementById('gName').value = g.name || '';
|
document.getElementById('gName').value = g.name || '';
|
||||||
document.getElementById('gVon').value = g.von || '';
|
document.getElementById('gVon').value = g.von || '';
|
||||||
document.getElementById('gDesc').value = g.beschreibung || '';
|
document.getElementById('gDesc').value = g.beschreibung || '';
|
||||||
|
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
|
||||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||||
if (g.bild) { document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild; imgWrap.style.display = 'flex'; }
|
if (g.bild) { document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild; imgWrap.style.display = 'flex'; }
|
||||||
else imgWrap.style.display = 'none';
|
else imgWrap.style.display = 'none';
|
||||||
@@ -1270,6 +1278,7 @@ function openGruppeModal(editId) {
|
|||||||
document.getElementById('gName').value = '';
|
document.getElementById('gName').value = '';
|
||||||
document.getElementById('gVon').value = '';
|
document.getElementById('gVon').value = '';
|
||||||
document.getElementById('gDesc').value = '';
|
document.getElementById('gDesc').value = '';
|
||||||
|
document.getElementById('gVanilla').checked = false;
|
||||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||||
}
|
}
|
||||||
gruppeModal.classList.add('open');
|
gruppeModal.classList.add('open');
|
||||||
@@ -1348,7 +1357,7 @@ gruppeModalSave.addEventListener('click', async () => {
|
|||||||
let bildBase64 = null;
|
let bildBase64 = null;
|
||||||
const fi = document.getElementById('gBild');
|
const fi = document.getElementById('gBild');
|
||||||
if (fi.files.length > 0) bildBase64 = await toBase64(fi.files[0]);
|
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;
|
const isEdit = currentEditGruppeId != null;
|
||||||
fetch(isEdit ? `/admin/aufgabengruppen/${currentEditGruppeId}` : '/admin/aufgabengruppen', {
|
fetch(isEdit ? `/admin/aufgabengruppen/${currentEditGruppeId}` : '/admin/aufgabengruppen', {
|
||||||
method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ function renderPage(evt) {
|
|||||||
|
|
||||||
const totalAttendees = (evt.attendees || []).length;
|
const totalAttendees = (evt.attendees || []).length;
|
||||||
const attending = evt.attendingMe;
|
const attending = evt.attendingMe;
|
||||||
|
const isPast = new Date(evt.startAt) < new Date();
|
||||||
|
|
||||||
document.getElementById('content').innerHTML = `
|
document.getElementById('content').innerHTML = `
|
||||||
<div class="evt-header">
|
<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>
|
<button class="btn" style="background:#c0392b;font-size:0.85rem;" onclick="openDeleteConfirm()">Löschen</button>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
|
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
|
||||||
<button class="btn" id="attendBtn"
|
${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);' : ''}"
|
style="${attending ? 'background:var(--color-secondary);color:var(--color-text);' : ''}"
|
||||||
onclick="toggleAttend()">
|
onclick="toggleAttend()">
|
||||||
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
||||||
</button>
|
</button>`}
|
||||||
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
|
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -445,6 +445,9 @@ function renderPage() {
|
|||||||
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
|
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="event-list" id="eventList"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></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 id="pastEventsSection" style="display:none;">
|
||||||
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
|
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
|
||||||
<div class="event-list" id="pastEventList"></div>
|
<div class="event-list" id="pastEventList"></div>
|
||||||
@@ -517,8 +520,8 @@ function renderPage() {
|
|||||||
${locHeaderHtml}
|
${locHeaderHtml}
|
||||||
${hoursHtml}
|
${hoursHtml}
|
||||||
${gallerySection}
|
${gallerySection}
|
||||||
${feedSection}
|
${eventsSection}
|
||||||
${eventsSection}`;
|
${feedSection}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,15 +590,27 @@ async function loadEvents() {
|
|||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
const now = new Date();
|
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)
|
const past = events.filter(e => new Date(e.startAt) < now)
|
||||||
.slice(-5) // letzte 5
|
.slice(-3) // letzte 3
|
||||||
.reverse(); // neueste zuerst
|
.reverse(); // neueste zuerst
|
||||||
|
|
||||||
list.innerHTML = future.length
|
const preview = future.slice(0, 3);
|
||||||
? future.map(e => buildEventCard(e, false)).join('')
|
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>';
|
: '<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');
|
const pastSection = document.getElementById('pastEventsSection');
|
||||||
if (past.length && pastSection) {
|
if (past.length && pastSection) {
|
||||||
document.getElementById('pastEventList').innerHTML = past.map(e => buildEventCard(e, true)).join('');
|
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>
|
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteLocPost('${p.postId}')" title="Löschen">🗑</button>
|
||||||
</div>` : '';
|
</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}>
|
return `<div class="post-card${clickableClass}" id="lp-${p.postId}"${onClickAttr}>
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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 class="post-meta">${dateStr}${editedHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
${adminBtns}
|
${adminBtns}
|
||||||
@@ -1401,11 +1420,15 @@ function openLpLb(postId) {
|
|||||||
+ ' ' + new Date(p.createdAt).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' });
|
+ ' ' + 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 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 = `
|
document.getElementById('lbPostBody').innerHTML = `
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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 class="post-meta">${dateStr}${editedHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
164
bin/main/static/community/location-events.html
Normal file
164
bin/main/static/community/location-events.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
}
|
}
|
||||||
.gruppe-badge-private { background: rgba(233,69,96,0.15); color: var(--color-primary); }
|
.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-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-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-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||||
|
|
||||||
@@ -367,6 +368,12 @@
|
|||||||
Gruppe veröffentlichen (für alle sichtbar)
|
Gruppe veröffentlichen (für alle sichtbar)
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</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-error" id="modalError"></div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
<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>`);
|
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>`);
|
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 (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 `
|
return `
|
||||||
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
|
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
|
||||||
@@ -1069,6 +1077,7 @@
|
|||||||
pubCb.checked = !g.privateGruppe;
|
pubCb.checked = !g.privateGruppe;
|
||||||
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
||||||
document.getElementById('gPublicLabel').style.display = 'block';
|
document.getElementById('gPublicLabel').style.display = 'block';
|
||||||
|
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
|
||||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||||
if (g.bild) {
|
if (g.bild) {
|
||||||
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
||||||
@@ -1086,6 +1095,7 @@
|
|||||||
document.getElementById('gDesc').value = '';
|
document.getElementById('gDesc').value = '';
|
||||||
document.getElementById('gPublic').checked = false;
|
document.getElementById('gPublic').checked = false;
|
||||||
document.getElementById('gPublicLabel').style.display = 'none';
|
document.getElementById('gPublicLabel').style.display = 'none';
|
||||||
|
document.getElementById('gVanilla').checked = false;
|
||||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||||
gruppeModal.classList.add('open');
|
gruppeModal.classList.add('open');
|
||||||
document.getElementById('gName').focus();
|
document.getElementById('gName').focus();
|
||||||
@@ -1119,6 +1129,7 @@
|
|||||||
name,
|
name,
|
||||||
beschreibung: document.getElementById('gDesc').value.trim() || null,
|
beschreibung: document.getElementById('gDesc').value.trim() || null,
|
||||||
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
|
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
|
||||||
|
vanillaAvailable: document.getElementById('gVanilla').checked,
|
||||||
bild: bildBase64
|
bild: bildBase64
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
.item-img { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
.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-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; }
|
.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; }
|
.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 { 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; }
|
.aufgaben-section-label:first-child { margin-top: 0; }
|
||||||
@@ -653,9 +654,10 @@
|
|||||||
}
|
}
|
||||||
ul.innerHTML = gruppen.map(g => {
|
ul.innerHTML = gruppen.map(g => {
|
||||||
const checked = savedGruppen.has(g.gruppenId);
|
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' : ''}">
|
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
|
||||||
<input type="checkbox" value="${g.gruppenId}"${checked ? ' 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="">` : ''}
|
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
|
||||||
</label></li>`;
|
</label></li>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
@@ -887,11 +887,14 @@
|
|||||||
</div>`
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
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}')">
|
return `<div class="post-card" id="hpc-${p.postId}"${gruppeIdAttr} onclick="homeOpenPost('${p.postId}')">
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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 class="post-meta" id="hpm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
|
||||||
</div>
|
</div>
|
||||||
${ownBtns}
|
${ownBtns}
|
||||||
|
|||||||
@@ -203,8 +203,10 @@ public class AdminController {
|
|||||||
@GetMapping("/aufgabengruppen")
|
@GetMapping("/aufgabengruppen")
|
||||||
public ResponseEntity<List<AufgabenGruppe>> getAufgabengruppen(Principal principal) {
|
public ResponseEntity<List<AufgabenGruppe>> getAufgabengruppen(Principal principal) {
|
||||||
requireAdmin(principal);
|
requireAdmin(principal);
|
||||||
List<AufgabenGruppeEntity> list = aufgabenGruppeRepository
|
List<AufgabenGruppeEntity> list = aufgabenGruppeRepository.findAll().stream()
|
||||||
.findByUserIdIsNull(PageRequest.of(0, 1000)).getContent();
|
.filter(g -> g.getUserId() == null)
|
||||||
|
.sorted(java.util.Comparator.comparing(AufgabenGruppeEntity::getName, String.CASE_INSENSITIVE_ORDER))
|
||||||
|
.toList();
|
||||||
return ResponseEntity.ok(list.stream().map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
return ResponseEntity.ok(list.stream().map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,9 +232,11 @@ public class AdminController {
|
|||||||
entity.setName(gruppe.getName());
|
entity.setName(gruppe.getName());
|
||||||
entity.setBeschreibung(gruppe.getBeschreibung());
|
entity.setBeschreibung(gruppe.getBeschreibung());
|
||||||
entity.setVon(gruppe.getVon());
|
entity.setVon(gruppe.getVon());
|
||||||
|
entity.setVanillaAvailable(gruppe.isVanillaAvailable());
|
||||||
if (gruppe.getBild() != null) {
|
if (gruppe.getBild() != null) {
|
||||||
entity.setBild(java.util.Base64.getDecoder().decode(gruppe.getBild()));
|
entity.setBild(java.util.Base64.getDecoder().decode(gruppe.getBild()));
|
||||||
}
|
}
|
||||||
|
aufgabenGruppeRepository.save(entity);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ public class SecurityConfig {
|
|||||||
http
|
http
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.headers(headers -> headers
|
||||||
|
.frameOptions(frame -> frame.deny())
|
||||||
|
.contentTypeOptions(ct -> {})
|
||||||
|
.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000))
|
||||||
|
.addHeaderWriter((request, response) -> {
|
||||||
|
response.setHeader("X-XSS-Protection", "1; mode=block");
|
||||||
|
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
|
response.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
|
||||||
|
})
|
||||||
|
)
|
||||||
.exceptionHandling(ex -> ex
|
.exceptionHandling(ex -> ex
|
||||||
.authenticationEntryPoint((request, response, authException) ->
|
.authenticationEntryPoint((request, response, authException) ->
|
||||||
response.sendRedirect("/login.html")))
|
response.sendRedirect("/login.html")))
|
||||||
@@ -77,6 +87,7 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/dating/matches.html").authenticated()
|
.requestMatchers("/dating/matches.html").authenticated()
|
||||||
.requestMatchers("/community/locations.html").authenticated()
|
.requestMatchers("/community/locations.html").authenticated()
|
||||||
.requestMatchers("/community/location-detail.html").authenticated()
|
.requestMatchers("/community/location-detail.html").authenticated()
|
||||||
|
.requestMatchers("/community/location-events.html").authenticated()
|
||||||
.requestMatchers("/community/events.html").authenticated()
|
.requestMatchers("/community/events.html").authenticated()
|
||||||
.requestMatchers("/community/event-detail.html").authenticated()
|
.requestMatchers("/community/event-detail.html").authenticated()
|
||||||
.requestMatchers("/gruppen/**").authenticated()
|
.requestMatchers("/gruppen/**").authenticated()
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import de.oaa.xxx.mail.Email;
|
|||||||
import de.oaa.xxx.mail.MailService;
|
import de.oaa.xxx.mail.MailService;
|
||||||
import de.oaa.xxx.support.SupportUserService;
|
import de.oaa.xxx.support.SupportUserService;
|
||||||
import de.oaa.xxx.user.UserRepository;
|
import de.oaa.xxx.user.UserRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/feedback")
|
@RequestMapping("/api/feedback")
|
||||||
@@ -20,6 +25,10 @@ public class FeedbackController {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final SupportUserService supportUserService;
|
private final SupportUserService supportUserService;
|
||||||
|
|
||||||
|
/** Rate-Limiting: Key → letzter Aufruf (Epoch-Sekunden) */
|
||||||
|
private final Map<String, Long> lastCallAt = new ConcurrentHashMap<>();
|
||||||
|
private static final long RATE_LIMIT_SECONDS = 60;
|
||||||
|
|
||||||
public FeedbackController(MailService mailService,
|
public FeedbackController(MailService mailService,
|
||||||
FeedbackRepository feedbackRepository,
|
FeedbackRepository feedbackRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
@@ -33,11 +42,29 @@ public class FeedbackController {
|
|||||||
record FeedbackRequest(String name, String seite, String grund, String text) {}
|
record FeedbackRequest(String name, String seite, String grund, String text) {}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Void> send(@RequestBody FeedbackRequest req, Principal principal) {
|
public ResponseEntity<Void> send(@RequestBody FeedbackRequest req,
|
||||||
|
Principal principal,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
if (req.text() == null || req.text().isBlank() || req.text().length() < 10 || req.text().length() > 1000) {
|
if (req.text() == null || req.text().isBlank() || req.text().length() < 10 || req.text().length() > 1000) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E-Mail-Header-Injection verhindern: Felder dürfen keine Zeilenumbrüche enthalten
|
||||||
|
if (containsLineBreak(req.name()) || containsLineBreak(req.seite()) || containsLineBreak(req.grund())) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate-Limiting: 1 Aufruf pro Minute pro eingeloggtem User oder IP
|
||||||
|
String rateLimitKey = principal != null ? "user:" + principal.getName()
|
||||||
|
: "ip:" + httpRequest.getRemoteAddr();
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
Long last = lastCallAt.get(rateLimitKey);
|
||||||
|
if (last != null && (now - last) < RATE_LIMIT_SECONDS) {
|
||||||
|
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
|
||||||
|
}
|
||||||
|
lastCallAt.put(rateLimitKey, now);
|
||||||
|
|
||||||
// Eingeloggten User ermitteln (optional)
|
// Eingeloggten User ermitteln (optional)
|
||||||
UUID userId = null;
|
UUID userId = null;
|
||||||
if (principal != null) {
|
if (principal != null) {
|
||||||
@@ -86,4 +113,8 @@ public class FeedbackController {
|
|||||||
if (s == null) return "";
|
if (s == null) return "";
|
||||||
return s.replace("&", "&").replace("<", "<").replace(">", ">");
|
return s.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean containsLineBreak(String s) {
|
||||||
|
return s != null && (s.contains("\n") || s.contains("\r"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ public class AboController {
|
|||||||
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
|
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
|
||||||
|
|
||||||
List<AufgabenGruppe> dtos = gruppeRepository
|
List<AufgabenGruppe> dtos = gruppeRepository
|
||||||
.findPublicFromOthers(user.getUserId(), namePattern).stream()
|
.listAllWithUserAndSearch(user.getUserId(), namePattern, org.springframework.data.domain.PageRequest.of(0, 500)).stream()
|
||||||
|
.filter(g -> !g.isPrivateGruppe() && g.getUserId() != null && !g.getUserId().equals(user.getUserId()))
|
||||||
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
||||||
.sorted(Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
.sorted(Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
||||||
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
package de.oaa.xxx.games.bdsm.controller;
|
package de.oaa.xxx.games.bdsm.controller;
|
||||||
|
|
||||||
import java.security.Principal;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
|
||||||
import java.util.Base64;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeList;
|
||||||
import java.util.UUID;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppePage;
|
||||||
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeService;
|
||||||
|
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||||
|
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.SperreRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.StrafeRepository;
|
||||||
|
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||||
|
import de.oaa.xxx.user.UserEntity;
|
||||||
|
import de.oaa.xxx.user.UserService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -22,20 +32,9 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||||
|
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
|
import java.security.Principal;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeList;
|
import java.util.Base64;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppePage;
|
import java.util.UUID;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeService;
|
|
||||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
|
||||||
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.SperreRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.StrafeRepository;
|
|
||||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
|
||||||
import de.oaa.xxx.user.UserEntity;
|
|
||||||
import de.oaa.xxx.user.UserService;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/gruppe")
|
@RequestMapping("/gruppe")
|
||||||
@@ -75,7 +74,7 @@ public class AufgabenGruppeController {
|
|||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Paginierte Listen ──
|
// ── Paginierte Listen (alle Gruppen – BDSM-Verwaltung und Spielstart) ──
|
||||||
|
|
||||||
@GetMapping("/list/user")
|
@GetMapping("/list/user")
|
||||||
public ResponseEntity<AufgabenGruppePage> listUser(
|
public ResponseEntity<AufgabenGruppePage> listUser(
|
||||||
@@ -105,7 +104,7 @@ public class AufgabenGruppeController {
|
|||||||
UUID userId = userService.requireUser(principal).getUserId();
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
String searchPattern = search != null ? "%" + search + "%" : null;
|
String searchPattern = search != null ? "%" + search + "%" : null;
|
||||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||||
list.setGruppen(gruppeRepository.listWithUserAndSearch(userId, searchPattern, PageRequest.of(0, 500))
|
list.setGruppen(gruppeRepository.listAllWithUserAndSearch(userId, searchPattern, PageRequest.of(0, 500))
|
||||||
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||||
return ResponseEntity.ok(list);
|
return ResponseEntity.ok(list);
|
||||||
}
|
}
|
||||||
@@ -118,6 +117,7 @@ public class AufgabenGruppeController {
|
|||||||
return ResponseEntity.ok(list);
|
return ResponseEntity.ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gibt eine einzelne Gruppe zurück – typunabhängig, da BDSM-Spielstart auch vanillaAvailable-Gruppen laden muss. */
|
||||||
@GetMapping("/{gruppeId}")
|
@GetMapping("/{gruppeId}")
|
||||||
public ResponseEntity<AufgabenGruppe> get(@PathVariable("gruppeId") UUID gruppeId) {
|
public ResponseEntity<AufgabenGruppe> get(@PathVariable("gruppeId") UUID gruppeId) {
|
||||||
return gruppeRepository.findById(gruppeId)
|
return gruppeRepository.findById(gruppeId)
|
||||||
@@ -142,8 +142,9 @@ public class AufgabenGruppeController {
|
|||||||
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
|
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
|
||||||
entity.setUserId(user.getUserId());
|
entity.setUserId(user.getUserId());
|
||||||
entity.setPrivateGruppe(true);
|
entity.setPrivateGruppe(true);
|
||||||
|
// vanillaAvailable kommt aus dem Request-Body (Checkbox im Frontend)
|
||||||
gruppeRepository.save(entity);
|
gruppeRepository.save(entity);
|
||||||
LOGGER.debug("User {} hat AufgabenGruppe '{}' ({}) erstellt", user.getUserId(), entity.getName(), entity.getGruppenId());
|
LOGGER.debug("User {} hat BDSM-AufgabenGruppe '{}' ({}) erstellt", user.getUserId(), entity.getName(), entity.getGruppenId());
|
||||||
return ResponseEntity.created(
|
return ResponseEntity.created(
|
||||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getGruppenId()).toUri()
|
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getGruppenId()).toUri()
|
||||||
).build();
|
).build();
|
||||||
@@ -169,11 +170,12 @@ public class AufgabenGruppeController {
|
|||||||
entity.setBeschreibung(gruppe.getBeschreibung());
|
entity.setBeschreibung(gruppe.getBeschreibung());
|
||||||
entity.setVon(gruppe.getVon());
|
entity.setVon(gruppe.getVon());
|
||||||
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
|
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
|
||||||
|
entity.setVanillaAvailable(gruppe.isVanillaAvailable());
|
||||||
if (gruppe.getBild() != null) {
|
if (gruppe.getBild() != null) {
|
||||||
entity.setBild(Base64.getDecoder().decode(gruppe.getBild()));
|
entity.setBild(Base64.getDecoder().decode(gruppe.getBild()));
|
||||||
}
|
}
|
||||||
gruppeRepository.save(entity);
|
gruppeRepository.save(entity);
|
||||||
LOGGER.debug("User {} hat AufgabenGruppe {} aktualisiert", user.getUserId(), gruppeId);
|
LOGGER.debug("User {} hat BDSM-AufgabenGruppe {} aktualisiert", user.getUserId(), gruppeId);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ public class AufgabenGruppe {
|
|||||||
private String bild;
|
private String bild;
|
||||||
private long subscriberCount;
|
private long subscriberCount;
|
||||||
private boolean subscribed;
|
private boolean subscribed;
|
||||||
|
private boolean vanillaAvailable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ public class AufgabenGruppeDisplay {
|
|||||||
private boolean privateGruppe;
|
private boolean privateGruppe;
|
||||||
private String bild;
|
private String bild;
|
||||||
private String von;
|
private String von;
|
||||||
|
private boolean vanillaAvailable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ public class AufgabenGruppeEntity {
|
|||||||
private byte[] bild;
|
private byte[] bild;
|
||||||
@Column
|
@Column
|
||||||
private String von;
|
private String von;
|
||||||
|
@Column(columnDefinition = "BOOLEAN DEFAULT FALSE NOT NULL")
|
||||||
|
private boolean vanillaAvailable = false;
|
||||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||||
private List<AufgabeEntity> aufgaben;
|
private List<AufgabeEntity> aufgaben;
|
||||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||||
@@ -62,6 +64,7 @@ public class AufgabenGruppeEntity {
|
|||||||
gruppe.setPrivateGruppe(privateGruppe);
|
gruppe.setPrivateGruppe(privateGruppe);
|
||||||
gruppe.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
gruppe.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
||||||
gruppe.setVon(von);
|
gruppe.setVon(von);
|
||||||
|
gruppe.setVanillaAvailable(vanillaAvailable);
|
||||||
gruppe.setAufgaben(aufgaben.stream().map(AufgabeEntity::toAufgabe).toList());
|
gruppe.setAufgaben(aufgaben.stream().map(AufgabeEntity::toAufgabe).toList());
|
||||||
gruppe.setStrafen(strafen.stream().map(StrafeEntity::toStrafe).toList());
|
gruppe.setStrafen(strafen.stream().map(StrafeEntity::toStrafe).toList());
|
||||||
gruppe.setSperren(sperren.stream().map(SperreEntity::toSperre).toList());
|
gruppe.setSperren(sperren.stream().map(SperreEntity::toSperre).toList());
|
||||||
@@ -78,6 +81,7 @@ public class AufgabenGruppeEntity {
|
|||||||
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
|
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
|
||||||
entity.setBild(gruppe.getBild() != null ? Base64.getDecoder().decode(gruppe.getBild()) : null);
|
entity.setBild(gruppe.getBild() != null ? Base64.getDecoder().decode(gruppe.getBild()) : null);
|
||||||
entity.setVon(gruppe.getVon());
|
entity.setVon(gruppe.getVon());
|
||||||
|
entity.setVanillaAvailable(gruppe.isVanillaAvailable());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +94,7 @@ public class AufgabenGruppeEntity {
|
|||||||
display.setPrivateGruppe(privateGruppe);
|
display.setPrivateGruppe(privateGruppe);
|
||||||
display.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
display.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
||||||
display.setVon(von);
|
display.setVon(von);
|
||||||
|
display.setVanillaAvailable(vanillaAvailable);
|
||||||
return display;
|
return display;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,46 @@
|
|||||||
package de.oaa.xxx.games.common.repository;
|
package de.oaa.xxx.games.common.repository;
|
||||||
|
|
||||||
|
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface AufgabenGruppeRepository extends JpaRepository<AufgabenGruppeEntity, UUID> {
|
public interface AufgabenGruppeRepository extends JpaRepository<AufgabenGruppeEntity, UUID> {
|
||||||
|
|
||||||
@Query("select age from AufgabenGruppeEntity age where age.userId = :userId")
|
// ── Account-Löschung (UserService) ────────────────────────────────────────
|
||||||
|
|
||||||
|
@Query("select g from AufgabenGruppeEntity g where g.userId = :userId")
|
||||||
List<AufgabenGruppeEntity> findByUserId(@Param("userId") UUID userId);
|
List<AufgabenGruppeEntity> findByUserId(@Param("userId") UUID userId);
|
||||||
|
|
||||||
long countByUserId(UUID userId);
|
long countByUserId(UUID userId);
|
||||||
|
|
||||||
Page<AufgabenGruppeEntity> findByUserIdIsNull(Pageable pageable);
|
// ── BDSM-Verwaltung: alle Gruppen paginiert ───────────────────────────────
|
||||||
|
|
||||||
Page<AufgabenGruppeEntity> findByUserId(UUID userId, Pageable pageable);
|
Page<AufgabenGruppeEntity> findByUserId(UUID userId, Pageable pageable);
|
||||||
|
|
||||||
@Query("select age from AufgabenGruppeEntity age where (age.privateGruppe = false or age.userId = :userId) and (:search is null or age.name like :search)")
|
Page<AufgabenGruppeEntity> findByUserIdIsNull(Pageable pageable);
|
||||||
List<AufgabenGruppeEntity> listWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
|
||||||
|
|
||||||
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and (:search is null or age.name like :search)")
|
// ── Vanilla-Verwaltung: nur vanillaAvailable=true, mit Inhalt ─────────────
|
||||||
List<AufgabenGruppeEntity> listPublicWithSearch(@Param("search") String search, PageRequest pageable);
|
|
||||||
|
|
||||||
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and age.userId is not null and age.userId <> :userId and (:name is null or lower(age.name) like lower(:name))")
|
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId = :userId AND g.vanillaAvailable = true AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||||
List<AufgabenGruppeEntity> findPublicFromOthers(@Param("userId") UUID userId, @Param("name") String name);
|
Page<AufgabenGruppeEntity> findByUserIdAndVanillaAvailableTrueWithContent(@Param("userId") UUID userId, Pageable pageable);
|
||||||
|
|
||||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE (g.privateGruppe = false OR g.userId = :userId) AND g.strafen IS EMPTY AND g.sperren IS EMPTY AND (:search IS NULL OR g.name LIKE :search)")
|
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId IS NULL AND g.vanillaAvailable = true AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||||
List<AufgabenGruppeEntity> listVanillaSafeWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
Page<AufgabenGruppeEntity> findSystemGroupsByVanillaAvailableTrueWithContent(Pageable pageable);
|
||||||
|
|
||||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.privateGruppe = false AND g.userId IS NOT NULL AND g.userId <> :userId AND g.strafen IS EMPTY AND g.sperren IS EMPTY AND (:name IS NULL OR LOWER(g.name) LIKE LOWER(:name))")
|
// ── Spielstart-Auswahl ────────────────────────────────────────────────────
|
||||||
List<AufgabenGruppeEntity> findVanillaSafePublicFromOthers(@Param("userId") UUID userId, @Param("name") String name);
|
|
||||||
|
|
||||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId = :userId AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
/** Nur vanillaAvailable-Gruppen – für Vanilla-Spielstart und Vanilla-Suche. */
|
||||||
Page<AufgabenGruppeEntity> findByUserIdWithContent(@Param("userId") UUID userId, Pageable pageable);
|
@Query("select g from AufgabenGruppeEntity g where g.vanillaAvailable = true and (g.privateGruppe = false or g.userId = :userId) and (:search is null or g.name like :search)")
|
||||||
|
List<AufgabenGruppeEntity> listVanillaAvailableWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||||
|
|
||||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId IS NULL AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
/** Alle Gruppen – für BDSM-Spielstart (vanillaAvailable-Gruppen werden im Frontend hervorgehoben). */
|
||||||
Page<AufgabenGruppeEntity> findSystemGroupsWithContent(Pageable pageable);
|
@Query("select g from AufgabenGruppeEntity g where (g.privateGruppe = false or g.userId = :userId) and (:search is null or g.name like :search)")
|
||||||
|
List<AufgabenGruppeEntity> listAllWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -58,16 +56,21 @@ public class VanillaAboController {
|
|||||||
Principal principal) {
|
Principal principal) {
|
||||||
UserEntity user = userService.requireUser(principal);
|
UserEntity user = userService.requireUser(principal);
|
||||||
|
|
||||||
Page<GruppenAboEntity> dbPage = aboRepository.findByUserIdWithContent(
|
List<AufgabenGruppe> all = aboRepository.findByUserId(user.getUserId()).stream()
|
||||||
user.getUserId(), PageRequest.of(page, size, Sort.by("aufgabenGruppe.name")));
|
.map(GruppenAboEntity::getAufgabenGruppe)
|
||||||
List<AufgabenGruppe> dtos = dbPage.getContent().stream()
|
.filter(g -> g.isVanillaAvailable()
|
||||||
.map(a -> enrich(a.getAufgabenGruppe(), user.getUserId(), true))
|
&& !g.isPrivateGruppe()
|
||||||
|
&& (!g.getAufgaben().isEmpty() || !g.getFinisher().isEmpty()))
|
||||||
|
.map(g -> enrich(g, user.getUserId(), true))
|
||||||
|
.sorted(java.util.Comparator.comparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||||
.toList();
|
.toList();
|
||||||
|
int total = all.size();
|
||||||
|
int start = page * size;
|
||||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||||
result.setContent(dtos);
|
result.setContent(start >= total ? List.of() : all.subList(start, Math.min(start + size, total)));
|
||||||
result.setCurrentPage(dbPage.getNumber());
|
result.setCurrentPage(page);
|
||||||
result.setTotalPages(dbPage.getTotalPages());
|
result.setTotalPages(total == 0 ? 1 : (int) Math.ceil((double) total / size));
|
||||||
result.setTotalElements(dbPage.getTotalElements());
|
result.setTotalElements(total);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +87,8 @@ public class VanillaAboController {
|
|||||||
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
|
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
|
||||||
|
|
||||||
List<AufgabenGruppe> dtos = gruppeRepository
|
List<AufgabenGruppe> dtos = gruppeRepository
|
||||||
.findVanillaSafePublicFromOthers(user.getUserId(), namePattern).stream()
|
.listVanillaAvailableWithUserAndSearch(user.getUserId(), namePattern, PageRequest.of(0, 500)).stream()
|
||||||
|
.filter(g -> !g.isPrivateGruppe() && g.getUserId() != null && !g.getUserId().equals(user.getUserId()))
|
||||||
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
||||||
.sorted(java.util.Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
.sorted(java.util.Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
||||||
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||||
@@ -111,8 +115,7 @@ public class VanillaAboController {
|
|||||||
if (gruppe == null || gruppe.isPrivateGruppe() || user.getUserId().equals(gruppe.getUserId())) {
|
if (gruppe == null || gruppe.isPrivateGruppe() || user.getUserId().equals(gruppe.getUserId())) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
// Vanilla-safe validation
|
if (!gruppe.isVanillaAvailable()) {
|
||||||
if (!gruppe.getStrafen().isEmpty() || !gruppe.getSperren().isEmpty()) {
|
|
||||||
return ResponseEntity.status(403).build();
|
return ResponseEntity.status(403).build();
|
||||||
}
|
}
|
||||||
if (aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), gruppe)) {
|
if (aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), gruppe)) {
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
package de.oaa.xxx.games.vanilla.controller;
|
package de.oaa.xxx.games.vanilla.controller;
|
||||||
|
|
||||||
import java.security.Principal;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
|
||||||
import java.util.Base64;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeList;
|
||||||
import java.util.UUID;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppePage;
|
||||||
|
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeService;
|
||||||
|
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||||
|
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.SperreRepository;
|
||||||
|
import de.oaa.xxx.games.common.repository.StrafeRepository;
|
||||||
|
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||||
|
import de.oaa.xxx.user.UserEntity;
|
||||||
|
import de.oaa.xxx.user.UserService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -22,20 +32,9 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||||
|
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
|
import java.security.Principal;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeList;
|
import java.util.Base64;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppePage;
|
import java.util.UUID;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeService;
|
|
||||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
|
||||||
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.SperreRepository;
|
|
||||||
import de.oaa.xxx.games.common.repository.StrafeRepository;
|
|
||||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
|
||||||
import de.oaa.xxx.user.UserEntity;
|
|
||||||
import de.oaa.xxx.user.UserService;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/vanilla/gruppe")
|
@RequestMapping("/vanilla/gruppe")
|
||||||
@@ -84,7 +83,7 @@ public class VanillaAufgabenGruppeController {
|
|||||||
Principal principal) {
|
Principal principal) {
|
||||||
UserEntity user = resolveUser(principal);
|
UserEntity user = resolveUser(principal);
|
||||||
if (user == null) return ResponseEntity.status(401).build();
|
if (user == null) return ResponseEntity.status(401).build();
|
||||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findByUserIdWithContent(
|
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findByUserIdAndVanillaAvailableTrueWithContent(
|
||||||
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
|
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
|
||||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||||
result.setContent(dbPage.getContent().stream().map(entity -> {
|
result.setContent(dbPage.getContent().stream().map(entity -> {
|
||||||
@@ -102,11 +101,10 @@ public class VanillaAufgabenGruppeController {
|
|||||||
public ResponseEntity<AufgabenGruppePage> listSystem(
|
public ResponseEntity<AufgabenGruppePage> listSystem(
|
||||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||||
@RequestParam(name = "size", defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
|
@RequestParam(name = "size", defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
|
||||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findSystemGroupsWithContent(
|
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findSystemGroupsByVanillaAvailableTrueWithContent(
|
||||||
PageRequest.of(page, size, Sort.by("name")));
|
PageRequest.of(page, size, Sort.by("name")));
|
||||||
AufgabenGruppePage r = new AufgabenGruppePage();
|
AufgabenGruppePage r = new AufgabenGruppePage();
|
||||||
r.setContent(dbPage.getContent().stream()
|
r.setContent(dbPage.getContent().stream().map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
||||||
.map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
|
||||||
r.setCurrentPage(dbPage.getNumber());
|
r.setCurrentPage(dbPage.getNumber());
|
||||||
r.setTotalPages(dbPage.getTotalPages());
|
r.setTotalPages(dbPage.getTotalPages());
|
||||||
r.setTotalElements(dbPage.getTotalElements());
|
r.setTotalElements(dbPage.getTotalElements());
|
||||||
@@ -120,7 +118,8 @@ public class VanillaAufgabenGruppeController {
|
|||||||
UUID userId = userService.requireUser(principal).getUserId();
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
String searchPattern = search != null ? "%" + search + "%" : null;
|
String searchPattern = search != null ? "%" + search + "%" : null;
|
||||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||||
list.setGruppen(gruppeRepository.listVanillaSafeWithUserAndSearch(userId, searchPattern, PageRequest.of(0, 500))
|
list.setGruppen(gruppeRepository.listVanillaAvailableWithUserAndSearch(
|
||||||
|
userId, searchPattern, PageRequest.of(0, 500))
|
||||||
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||||
return ResponseEntity.ok(list);
|
return ResponseEntity.ok(list);
|
||||||
}
|
}
|
||||||
@@ -129,7 +128,7 @@ public class VanillaAufgabenGruppeController {
|
|||||||
public ResponseEntity<AufgabenGruppeList> getOwn(@RequestParam("userId") UUID userId) {
|
public ResponseEntity<AufgabenGruppeList> getOwn(@RequestParam("userId") UUID userId) {
|
||||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||||
list.setGruppen(gruppeRepository.findByUserId(userId).stream()
|
list.setGruppen(gruppeRepository.findByUserId(userId).stream()
|
||||||
.filter(g -> g.getStrafen().isEmpty() && g.getSperren().isEmpty())
|
.filter(AufgabenGruppeEntity::isVanillaAvailable)
|
||||||
.map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
.map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||||
return ResponseEntity.ok(list);
|
return ResponseEntity.ok(list);
|
||||||
}
|
}
|
||||||
@@ -137,7 +136,7 @@ public class VanillaAufgabenGruppeController {
|
|||||||
@GetMapping("/{gruppeId}")
|
@GetMapping("/{gruppeId}")
|
||||||
public ResponseEntity<AufgabenGruppe> get(@PathVariable("gruppeId") UUID gruppeId) {
|
public ResponseEntity<AufgabenGruppe> get(@PathVariable("gruppeId") UUID gruppeId) {
|
||||||
return gruppeRepository.findById(gruppeId)
|
return gruppeRepository.findById(gruppeId)
|
||||||
.filter(g -> g.getStrafen().isEmpty() && g.getSperren().isEmpty())
|
.filter(AufgabenGruppeEntity::isVanillaAvailable)
|
||||||
.map(entity -> ResponseEntity.ok(entity.toAufgabenGruppe()))
|
.map(entity -> ResponseEntity.ok(entity.toAufgabenGruppe()))
|
||||||
.orElse(ResponseEntity.status(403).build());
|
.orElse(ResponseEntity.status(403).build());
|
||||||
}
|
}
|
||||||
@@ -159,6 +158,7 @@ public class VanillaAufgabenGruppeController {
|
|||||||
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
|
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
|
||||||
entity.setUserId(user.getUserId());
|
entity.setUserId(user.getUserId());
|
||||||
entity.setPrivateGruppe(true);
|
entity.setPrivateGruppe(true);
|
||||||
|
entity.setVanillaAvailable(true);
|
||||||
gruppeRepository.save(entity);
|
gruppeRepository.save(entity);
|
||||||
LOGGER.debug("User {} hat Vanilla-AufgabenGruppe '{}' ({}) erstellt", user.getUserId(), entity.getName(), entity.getGruppenId());
|
LOGGER.debug("User {} hat Vanilla-AufgabenGruppe '{}' ({}) erstellt", user.getUserId(), entity.getName(), entity.getGruppenId());
|
||||||
return ResponseEntity.created(
|
return ResponseEntity.created(
|
||||||
@@ -181,8 +181,7 @@ public class VanillaAufgabenGruppeController {
|
|||||||
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
|
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
|
||||||
if (entity == null) return ResponseEntity.notFound().build();
|
if (entity == null) return ResponseEntity.notFound().build();
|
||||||
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
|
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
|
||||||
// Vanilla-safe check: cannot edit a non-vanilla-safe group
|
if (!entity.isVanillaAvailable()) return ResponseEntity.status(403).build();
|
||||||
if (!entity.getStrafen().isEmpty() || !entity.getSperren().isEmpty()) return ResponseEntity.status(403).build();
|
|
||||||
|
|
||||||
entity.setName(gruppe.getName().trim());
|
entity.setName(gruppe.getName().trim());
|
||||||
entity.setBeschreibung(gruppe.getBeschreibung());
|
entity.setBeschreibung(gruppe.getBeschreibung());
|
||||||
@@ -202,10 +201,9 @@ public class VanillaAufgabenGruppeController {
|
|||||||
public ResponseEntity<Void> copy(@PathVariable("gruppeId") UUID gruppeId, Principal principal) {
|
public ResponseEntity<Void> copy(@PathVariable("gruppeId") UUID gruppeId, Principal principal) {
|
||||||
UserEntity user = resolveUser(principal);
|
UserEntity user = resolveUser(principal);
|
||||||
if (user == null) return ResponseEntity.status(401).build();
|
if (user == null) return ResponseEntity.status(401).build();
|
||||||
// Only allow copying vanilla-safe groups
|
|
||||||
AufgabenGruppeEntity source = gruppeRepository.findById(gruppeId).orElse(null);
|
AufgabenGruppeEntity source = gruppeRepository.findById(gruppeId).orElse(null);
|
||||||
if (source == null) return ResponseEntity.notFound().build();
|
if (source == null) return ResponseEntity.notFound().build();
|
||||||
if (!source.getStrafen().isEmpty() || !source.getSperren().isEmpty()) return ResponseEntity.status(403).build();
|
if (!source.isVanillaAvailable()) return ResponseEntity.status(403).build();
|
||||||
try {
|
try {
|
||||||
aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId());
|
aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId());
|
||||||
return ResponseEntity.status(201).build();
|
return ResponseEntity.status(201).build();
|
||||||
@@ -228,8 +226,7 @@ public class VanillaAufgabenGruppeController {
|
|||||||
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
|
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
|
||||||
if (entity == null) return ResponseEntity.noContent().build();
|
if (entity == null) return ResponseEntity.noContent().build();
|
||||||
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
|
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
|
||||||
// Only allow deletion of vanilla-safe groups
|
if (!entity.isVanillaAvailable()) return ResponseEntity.status(403).build();
|
||||||
if (!entity.getStrafen().isEmpty() || !entity.getSperren().isEmpty()) return ResponseEntity.status(403).build();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
aboRepository.deleteByAufgabenGruppe(entity);
|
aboRepository.deleteByAufgabenGruppe(entity);
|
||||||
@@ -249,7 +246,7 @@ public class VanillaAufgabenGruppeController {
|
|||||||
public ResponseEntity<Void> delete(@RequestBody AufgabenGruppe gruppe) {
|
public ResponseEntity<Void> delete(@RequestBody AufgabenGruppe gruppe) {
|
||||||
try {
|
try {
|
||||||
gruppeRepository.findById(gruppe.getGruppenId()).ifPresent(entity -> {
|
gruppeRepository.findById(gruppe.getGruppenId()).ifPresent(entity -> {
|
||||||
if (entity.getStrafen().isEmpty() && entity.getSperren().isEmpty()) {
|
if (entity.isVanillaAvailable()) {
|
||||||
gruppeRepository.delete(entity);
|
gruppeRepository.delete(entity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -266,5 +263,4 @@ public class VanillaAufgabenGruppeController {
|
|||||||
if (principal == null) return null;
|
if (principal == null) return null;
|
||||||
return userService.requireUser(principal);
|
return userService.requireUser(principal);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class NotificationController {
|
|||||||
n.put("text", m.getText());
|
n.put("text", m.getText());
|
||||||
n.put("sentAt", m.getSentAt().toString());
|
n.put("sentAt", m.getSentAt().toString());
|
||||||
n.put("read", m.getReadAt() != null);
|
n.put("read", m.getReadAt() != null);
|
||||||
n.put("targetUrl", m.getTargetUrl() != null ? m.getTargetUrl() : "");
|
n.put("targetUrl", sanitizeTargetUrl(m.getTargetUrl()));
|
||||||
userRepository.findById(m.getSenderId()).ifPresent(sender -> {
|
userRepository.findById(m.getSenderId()).ifPresent(sender -> {
|
||||||
n.put("senderName", sender.getName());
|
n.put("senderName", sender.getName());
|
||||||
n.put("senderAvatar", sender.getProfilePicture() != null ? sender.getProfilePicture() : "");
|
n.put("senderAvatar", sender.getProfilePicture() != null ? sender.getProfilePicture() : "");
|
||||||
@@ -67,6 +67,12 @@ public class NotificationController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Erlaubt nur relative Pfade (beginnen mit '/') – verhindert javascript:-URLs und externe Redirects. */
|
||||||
|
private String sanitizeTargetUrl(String url) {
|
||||||
|
if (url == null || url.isBlank()) return "";
|
||||||
|
return url.startsWith("/") ? url : "";
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@PostMapping("/read-all")
|
@PostMapping("/read-all")
|
||||||
public ResponseEntity<Void> markAllRead(Principal principal) {
|
public ResponseEntity<Void> markAllRead(Principal principal) {
|
||||||
|
|||||||
@@ -283,7 +283,8 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{userId}/bdsm-defaults")
|
@GetMapping("/{userId}/bdsm-defaults")
|
||||||
public ResponseEntity<Map<String, Object>> getBdsmDefaultsForUser(@PathVariable("userId") UUID userId) {
|
public ResponseEntity<Map<String, Object>> getBdsmDefaultsForUser(@PathVariable("userId") UUID userId, Principal principal) {
|
||||||
|
userService.requireUser(principal);
|
||||||
var userOpt = userRepository.findById(userId);
|
var userOpt = userRepository.findById(userId);
|
||||||
if (userOpt.isEmpty()) return ResponseEntity.notFound().build();
|
if (userOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
UserEntity user = userOpt.get();
|
UserEntity user = userOpt.get();
|
||||||
|
|||||||
@@ -121,6 +121,7 @@
|
|||||||
.gruppe-badges { display:flex; gap:0.3rem; margin-top:0.25rem; flex-wrap:wrap; }
|
.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 { 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-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-card.open .gruppe-toggle { transform:rotate(90deg); }
|
||||||
.gruppe-body { border-top:1px solid var(--color-secondary); padding:1rem 1rem 0.75rem; }
|
.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>
|
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues wählen zum Ersetzen</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="gBild" accept="image/*">
|
<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-error" id="gruppeModalError"></div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn-cancel" id="gruppeModalCancel">Abbrechen</button>
|
<button class="btn-cancel" id="gruppeModalCancel">Abbrechen</button>
|
||||||
@@ -986,7 +993,7 @@ function renderAdminGruppen(gruppen) {
|
|||||||
<div class="gruppe-meta">
|
<div class="gruppe-meta">
|
||||||
<div class="gruppe-name">${esc(g.name)}</div>
|
<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-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>
|
</div>
|
||||||
<span class="gruppe-toggle">▶</span>
|
<span class="gruppe-toggle">▶</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1262,6 +1269,7 @@ function openGruppeModal(editId) {
|
|||||||
document.getElementById('gName').value = g.name || '';
|
document.getElementById('gName').value = g.name || '';
|
||||||
document.getElementById('gVon').value = g.von || '';
|
document.getElementById('gVon').value = g.von || '';
|
||||||
document.getElementById('gDesc').value = g.beschreibung || '';
|
document.getElementById('gDesc').value = g.beschreibung || '';
|
||||||
|
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
|
||||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||||
if (g.bild) { document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild; imgWrap.style.display = 'flex'; }
|
if (g.bild) { document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild; imgWrap.style.display = 'flex'; }
|
||||||
else imgWrap.style.display = 'none';
|
else imgWrap.style.display = 'none';
|
||||||
@@ -1270,6 +1278,7 @@ function openGruppeModal(editId) {
|
|||||||
document.getElementById('gName').value = '';
|
document.getElementById('gName').value = '';
|
||||||
document.getElementById('gVon').value = '';
|
document.getElementById('gVon').value = '';
|
||||||
document.getElementById('gDesc').value = '';
|
document.getElementById('gDesc').value = '';
|
||||||
|
document.getElementById('gVanilla').checked = false;
|
||||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||||
}
|
}
|
||||||
gruppeModal.classList.add('open');
|
gruppeModal.classList.add('open');
|
||||||
@@ -1348,7 +1357,7 @@ gruppeModalSave.addEventListener('click', async () => {
|
|||||||
let bildBase64 = null;
|
let bildBase64 = null;
|
||||||
const fi = document.getElementById('gBild');
|
const fi = document.getElementById('gBild');
|
||||||
if (fi.files.length > 0) bildBase64 = await toBase64(fi.files[0]);
|
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;
|
const isEdit = currentEditGruppeId != null;
|
||||||
fetch(isEdit ? `/admin/aufgabengruppen/${currentEditGruppeId}` : '/admin/aufgabengruppen', {
|
fetch(isEdit ? `/admin/aufgabengruppen/${currentEditGruppeId}` : '/admin/aufgabengruppen', {
|
||||||
method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ function renderPage(evt) {
|
|||||||
|
|
||||||
const totalAttendees = (evt.attendees || []).length;
|
const totalAttendees = (evt.attendees || []).length;
|
||||||
const attending = evt.attendingMe;
|
const attending = evt.attendingMe;
|
||||||
|
const isPast = new Date(evt.startAt) < new Date();
|
||||||
|
|
||||||
document.getElementById('content').innerHTML = `
|
document.getElementById('content').innerHTML = `
|
||||||
<div class="evt-header">
|
<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>
|
<button class="btn" style="background:#c0392b;font-size:0.85rem;" onclick="openDeleteConfirm()">Löschen</button>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
|
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
|
||||||
<button class="btn" id="attendBtn"
|
${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);' : ''}"
|
style="${attending ? 'background:var(--color-secondary);color:var(--color-text);' : ''}"
|
||||||
onclick="toggleAttend()">
|
onclick="toggleAttend()">
|
||||||
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
||||||
</button>
|
</button>`}
|
||||||
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
|
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -445,6 +445,9 @@ function renderPage() {
|
|||||||
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
|
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="event-list" id="eventList"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></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 id="pastEventsSection" style="display:none;">
|
||||||
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
|
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
|
||||||
<div class="event-list" id="pastEventList"></div>
|
<div class="event-list" id="pastEventList"></div>
|
||||||
@@ -517,8 +520,8 @@ function renderPage() {
|
|||||||
${locHeaderHtml}
|
${locHeaderHtml}
|
||||||
${hoursHtml}
|
${hoursHtml}
|
||||||
${gallerySection}
|
${gallerySection}
|
||||||
${feedSection}
|
${eventsSection}
|
||||||
${eventsSection}`;
|
${feedSection}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,15 +590,27 @@ async function loadEvents() {
|
|||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
const now = new Date();
|
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)
|
const past = events.filter(e => new Date(e.startAt) < now)
|
||||||
.slice(-5) // letzte 5
|
.slice(-3) // letzte 3
|
||||||
.reverse(); // neueste zuerst
|
.reverse(); // neueste zuerst
|
||||||
|
|
||||||
list.innerHTML = future.length
|
const preview = future.slice(0, 3);
|
||||||
? future.map(e => buildEventCard(e, false)).join('')
|
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>';
|
: '<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');
|
const pastSection = document.getElementById('pastEventsSection');
|
||||||
if (past.length && pastSection) {
|
if (past.length && pastSection) {
|
||||||
document.getElementById('pastEventList').innerHTML = past.map(e => buildEventCard(e, true)).join('');
|
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>
|
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteLocPost('${p.postId}')" title="Löschen">🗑</button>
|
||||||
</div>` : '';
|
</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}>
|
return `<div class="post-card${clickableClass}" id="lp-${p.postId}"${onClickAttr}>
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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 class="post-meta">${dateStr}${editedHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
${adminBtns}
|
${adminBtns}
|
||||||
@@ -1401,11 +1420,15 @@ function openLpLb(postId) {
|
|||||||
+ ' ' + new Date(p.createdAt).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' });
|
+ ' ' + 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 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 = `
|
document.getElementById('lbPostBody').innerHTML = `
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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 class="post-meta">${dateStr}${editedHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
164
src/main/resources/static/community/location-events.html
Normal file
164
src/main/resources/static/community/location-events.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
}
|
}
|
||||||
.gruppe-badge-private { background: rgba(233,69,96,0.15); color: var(--color-primary); }
|
.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-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-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-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||||
|
|
||||||
@@ -367,6 +368,12 @@
|
|||||||
Gruppe veröffentlichen (für alle sichtbar)
|
Gruppe veröffentlichen (für alle sichtbar)
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</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-error" id="modalError"></div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
<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>`);
|
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>`);
|
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 (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 `
|
return `
|
||||||
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
|
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
|
||||||
@@ -1069,6 +1077,7 @@
|
|||||||
pubCb.checked = !g.privateGruppe;
|
pubCb.checked = !g.privateGruppe;
|
||||||
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
||||||
document.getElementById('gPublicLabel').style.display = 'block';
|
document.getElementById('gPublicLabel').style.display = 'block';
|
||||||
|
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
|
||||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||||
if (g.bild) {
|
if (g.bild) {
|
||||||
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
||||||
@@ -1086,6 +1095,7 @@
|
|||||||
document.getElementById('gDesc').value = '';
|
document.getElementById('gDesc').value = '';
|
||||||
document.getElementById('gPublic').checked = false;
|
document.getElementById('gPublic').checked = false;
|
||||||
document.getElementById('gPublicLabel').style.display = 'none';
|
document.getElementById('gPublicLabel').style.display = 'none';
|
||||||
|
document.getElementById('gVanilla').checked = false;
|
||||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||||
gruppeModal.classList.add('open');
|
gruppeModal.classList.add('open');
|
||||||
document.getElementById('gName').focus();
|
document.getElementById('gName').focus();
|
||||||
@@ -1119,6 +1129,7 @@
|
|||||||
name,
|
name,
|
||||||
beschreibung: document.getElementById('gDesc').value.trim() || null,
|
beschreibung: document.getElementById('gDesc').value.trim() || null,
|
||||||
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
|
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
|
||||||
|
vanillaAvailable: document.getElementById('gVanilla').checked,
|
||||||
bild: bildBase64
|
bild: bildBase64
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
.item-img { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
.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-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; }
|
.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; }
|
.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 { 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; }
|
.aufgaben-section-label:first-child { margin-top: 0; }
|
||||||
@@ -653,9 +654,10 @@
|
|||||||
}
|
}
|
||||||
ul.innerHTML = gruppen.map(g => {
|
ul.innerHTML = gruppen.map(g => {
|
||||||
const checked = savedGruppen.has(g.gruppenId);
|
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' : ''}">
|
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
|
||||||
<input type="checkbox" value="${g.gruppenId}"${checked ? ' 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="">` : ''}
|
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
|
||||||
</label></li>`;
|
</label></li>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
@@ -887,11 +887,14 @@
|
|||||||
</div>`
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
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}')">
|
return `<div class="post-card" id="hpc-${p.postId}"${gruppeIdAttr} onclick="homeOpenPost('${p.postId}')">
|
||||||
<div class="post-header">
|
<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>
|
||||||
<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 class="post-meta" id="hpm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
|
||||||
</div>
|
</div>
|
||||||
${ownBtns}
|
${ownBtns}
|
||||||
|
|||||||
Reference in New Issue
Block a user