Umsetzung Aufgabenverwaltung

This commit is contained in:
2026-03-02 15:33:35 +01:00
parent c922ef6668
commit abf85f66e4
43 changed files with 6617 additions and 2103 deletions

View File

@@ -0,0 +1,603 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entdecken XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
body { display: block; min-height: 100vh; }
.layout { display: flex; min-height: 100vh; }
/* ── Sidebar ── */
.sidebar {
width: 240px; background: var(--color-card);
border-right: 1px solid var(--color-secondary);
display: flex; flex-direction: column;
position: fixed; top: 0; left: 0;
height: 100vh; overflow-y: auto;
z-index: 100; transition: transform 0.25s ease;
}
.sidebar-brand {
color: var(--color-primary); font-size: 1.1rem; font-weight: 700;
padding: 1.25rem 1.25rem 1rem;
border-bottom: 1px solid var(--color-secondary); flex-shrink: 0;
}
.sidebar ul { list-style: none; padding: 0.5rem 0; }
.sidebar ul li a {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.7rem 1.25rem; color: var(--color-text);
text-decoration: none; font-size: 0.95rem;
border-left: 3px solid transparent;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.sidebar ul li a:hover, .sidebar ul li a.active {
background: var(--color-secondary); color: var(--color-primary);
border-left-color: var(--color-primary);
}
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
/* ── Main ── */
.main { margin-left: 240px; flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
/* ── Topbar ── */
.topbar {
display: flex; align-items: center; gap: 1rem;
padding: 0.9rem 1.5rem; background: var(--color-card);
border-bottom: 1px solid var(--color-secondary);
position: sticky; top: 0; z-index: 50;
}
.topbar h1 { font-size: 1.2rem; font-weight: 600; }
.burger {
display: none; background: none; border: none; cursor: pointer;
color: var(--color-text); padding: 0.25rem 0.4rem;
border-radius: 4px; transition: background 0.15s; flex-shrink: 0;
}
.burger:hover { background: var(--color-secondary); }
.burger-icon { display: flex; flex-direction: column; gap: 5px; width: 22px; }
.burger-icon span {
display: block; height: 2px; background: var(--color-text);
border-radius: 2px; transition: transform 0.25s, opacity 0.25s;
}
.burger.open .burger-icon span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger.open .burger-icon span:nth-child(2) { opacity: 0; }
.burger.open .burger-icon span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.content { padding: 2rem 1.5rem; flex: 1; }
.overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.55); z-index: 90;
}
/* ── Search ── */
.search-bar {
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
}
.search-bar input[type="text"] {
flex: 1; padding: 0.55rem 0.85rem;
border: 1px solid var(--color-secondary); border-radius: 6px;
background: var(--color-card); color: var(--color-text);
font-size: 0.95rem; outline: none; transition: border-color 0.2s;
}
.search-bar input[type="text"]:focus { border-color: var(--color-primary); }
.search-bar input[type="text"]::placeholder { color: var(--color-muted); }
.btn-search {
background: var(--color-secondary); color: var(--color-text);
border: none; border-radius: 6px; padding: 0.55rem 1rem;
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
}
.btn-search:hover { background: var(--color-primary); color: #fff; }
/* ── Paging ── */
.paging {
display: flex; align-items: center; justify-content: center;
gap: 0.75rem; margin-top: 1rem;
}
.paging button {
background: var(--color-secondary); color: var(--color-text);
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
}
.paging button:hover:not(:disabled) { background: var(--color-primary); }
.paging button:disabled { opacity: 0.35; cursor: default; }
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
/* ── Gruppe card ── */
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
.gruppe-card {
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
}
.gruppe-card.open { border-color: rgba(233,69,96,0.35); }
.gruppe-header {
display: flex; align-items: center; gap: 0.9rem;
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
}
.gruppe-img {
width: 48px; height: 48px; border-radius: 7px;
object-fit: cover; flex-shrink: 0;
}
.gruppe-img-placeholder {
width: 48px; height: 48px; border-radius: 7px;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
}
.gruppe-meta { flex: 1; min-width: 0; }
.gruppe-name {
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
.gruppe-badge {
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
background: rgba(255,255,255,0.07); color: var(--color-muted);
}
.gruppe-badge-sub { background: rgba(46,204,113,0.15); color: var(--color-success); }
.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); }
/* ── Subscribe button ── */
.btn-sub {
background: none; border: 1px solid var(--color-secondary); border-radius: 6px;
color: var(--color-muted); font-size: 0.8rem; padding: 0.3rem 0.75rem;
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
flex-shrink: 0; white-space: nowrap;
}
.btn-sub:hover { border-color: var(--color-primary); color: var(--color-primary); }
.btn-sub.subscribed {
border-color: rgba(46,204,113,0.5); color: var(--color-success);
}
.btn-sub.subscribed:hover {
border-color: var(--color-primary); color: var(--color-primary);
background: rgba(233,69,96,0.08);
}
.btn-sub:disabled { opacity: 0.4; cursor: default; }
/* ── Gruppe body ── */
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
.sub-section + .sub-section { margin-top: 0.85rem; }
.sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
.sub-section-title {
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; color: var(--color-primary);
}
/* ── Items ── */
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
.item { border-radius: 6px; background: var(--color-secondary); overflow: hidden; }
.item-row {
display: flex; align-items: center; justify-content: space-between;
gap: 0.75rem; padding: 0.35rem 0.6rem;
cursor: pointer; user-select: none; transition: background 0.12s;
}
.item-row:hover { background: rgba(255,255,255,0.04); }
.item.open .item-row { background: rgba(233,69,96,0.08); }
.item-text {
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
.badge {
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
}
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
/* ── Item detail ── */
.item-detail {
display: none; padding: 0.5rem 0.6rem 0.6rem;
border-top: 1px solid rgba(255,255,255,0.06);
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
}
.item.open .item-detail { display: block; }
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
.item-detail-chip {
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
background: rgba(255,255,255,0.07); color: var(--color-text);
}
.item-detail-chip-toy { background: rgba(233,69,96,0.12); color: var(--color-primary); }
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
/* ── Mobile ── */
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); }
.sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,0.5); }
.main { margin-left: 0; }
.burger { display: flex; }
.overlay.visible { display: block; }
}
</style>
</head>
<body>
<div class="overlay" id="overlay"></div>
<div class="layout">
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">XXX The Game</div>
<ul>
<li><a href="/userhome.html"><span class="icon"></span> Dashboard</a></li>
<li><a href="#"><span class="icon"></span> Meine Session</a></li>
<li><a href="/aufgaben.html"><span class="icon"></span> Aufgaben</a></li>
<li><a href="/entdecken.html" class="active"><span class="icon"></span> Entdecken</a></li>
<li><a href="#"><span class="icon"></span> Strafen</a></li>
<li><a href="/toys.html"><span class="icon"></span> Toys</a></li>
<li><a href="#"><span class="icon"></span> Favoriten</a></li>
<li><a href="#"><span class="icon"></span> Rangliste</a></li>
<li><a href="#"><span class="icon"></span> Nachrichten</a></li>
<li><a href="#"><span class="icon"></span> Einstellungen</a></li>
<li><a href="#" id="logoutLink"><span class="icon"></span> Abmelden</a></li>
</ul>
</aside>
<div class="main">
<header class="topbar">
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
<span class="burger-icon"><span></span><span></span><span></span></span>
</button>
<h1>Entdecken</h1>
</header>
<div class="content">
<div class="search-bar">
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
<button class="btn-search" id="searchBtn">Suchen</button>
</div>
<div id="loading" class="loading">Wird geladen…</div>
<div id="groupList" class="gruppe-list"></div>
<div class="paging" id="paging" style="display:none;">
<button id="prevBtn"> Zurück</button>
<span class="page-info" id="pageInfo"></span>
<button id="nextBtn">Weiter </button>
</div>
</div>
</div>
</div>
<script>
const PAGE_SIZE = 10;
let currentPage = 0, totalPages = 1;
let currentName = '';
// ── XSS ──
function esc(str) {
if (str == null) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Auth ──
fetch('/login/me')
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
.then(user => { if (!user) return; loadGroups(); })
.catch(() => { window.location.href = '/login.html'; });
document.getElementById('logoutLink').addEventListener('click', e => {
e.preventDefault();
document.cookie = 'jwt=; Max-Age=0; path=/';
window.location.href = '/login.html';
});
// ── Load ──
function loadGroups() {
document.getElementById('loading').style.display = 'block';
document.getElementById('groupList').innerHTML = '';
document.getElementById('paging').style.display = 'none';
const nameParam = currentName ? `&name=${encodeURIComponent(currentName)}` : '';
fetch(`/abo/discover?page=${currentPage}&size=${PAGE_SIZE}${nameParam}`)
.then(r => r.json())
.then(data => {
totalPages = data.totalPages || 1;
renderGroups(data.content || []);
updatePaging(currentPage, totalPages);
document.getElementById('loading').style.display = 'none';
})
.catch(() => { document.getElementById('loading').textContent = 'Fehler beim Laden.'; });
}
// ── Render ──
const WERKZEUG_LABEL = {
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
};
function werkzeugChips(list) {
if (!list || list.length === 0) return '';
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
}
function toyChips(list) {
if (!list || list.length === 0) return '';
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
}
function formatSek(von, bis) {
if (von != null && bis != null) return `${von}${bis} s`;
if (von != null) return `ab ${von} s`;
if (bis != null) return `bis ${bis} s`;
return '';
}
function formatMin(von, bis) {
if (von != null && bis != null) return `${von}${bis} min`;
if (von != null) return `ab ${von} min`;
if (bis != null) return `bis ${bis} min`;
return '';
}
// Track which group card is open
let openGroupId = null;
// Track which item detail is open
let openItemId = null;
function renderGroups(groups) {
const list = document.getElementById('groupList');
if (!groups || groups.length === 0) {
list.innerHTML = '<p class="empty">Keine Gruppen gefunden.</p>';
return;
}
list.innerHTML = groups.map(g => {
const aufgabenCount = (g.aufgaben || []).length;
const strafeCount = (g.strafen || []).length;
const sperreCount = (g.sperren || []).length;
const counts = [
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
].filter(Boolean).join(' · ');
const subLabel = g.subscribed
? `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`
: '';
const subCount = g.subscriberCount > 0
? `<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`
: '';
const subBtnClass = g.subscribed ? 'btn-sub subscribed' : 'btn-sub';
const subBtnText = g.subscribed ? '♥ Abonniert' : '♥ Abonnieren';
return `
<div class="gruppe-card" id="dgroup-${esc(g.gruppenId)}">
<div class="gruppe-header">
<div style="cursor:pointer; display:flex; align-items:center; gap:0.9rem; flex:1; min-width:0;"
onclick="toggleGroup('${esc(g.gruppenId)}')">
${g.bild
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
: `<div class="gruppe-img-placeholder">⊙</div>`}
<div class="gruppe-meta">
<div class="gruppe-name">${esc(g.name)}</div>
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
${(subLabel || subCount) ? `<div class="gruppe-badges">${subCount}${subLabel}</div>` : ''}
</div>
<span class="gruppe-toggle">▶</span>
</div>
<button class="${subBtnClass}" id="subbtn-${esc(g.gruppenId)}"
onclick="toggleSubscribe('${esc(g.gruppenId)}', this)">
${subBtnText}
</button>
</div>
<div class="gruppe-body" id="dbody-${esc(g.gruppenId)}" style="display:none;">
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), renderAufgabe)}
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), renderStrafe)}
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), renderZeitstrafe)}
</div>
</div>`;
}).join('');
openItemId = null;
}
function renderSubSection(title, items, renderFn) {
return `<div class="sub-section">
<div class="sub-section-header">
<span class="sub-section-title">${esc(title)} (${items.length})</span>
</div>
${items.length === 0
? '<div class="sub-empty">Keine Einträge</div>'
: `<div class="item-list">${items.map(item => renderFn(item)).join('')}</div>`}
</div>`;
}
function renderAufgabe(a) {
const badges = [];
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
const detailRows = [];
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
return `<div class="item" id="ditem-${esc(a.aufgabeId)}">
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
<span class="item-text">${esc(a.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
</div>`;
}
function renderStrafe(s) {
const badges = [];
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
const detailRows = [];
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
return `<div class="item" id="ditem-${esc(s.strafeId)}">
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
<span class="item-text">${esc(s.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
</div>`;
}
function renderZeitstrafe(z) {
const badges = [];
const zeit = formatMin(z.minutenVon, z.minutenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
const detailRows = [];
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
return `<div class="item" id="ditem-${esc(z.sperreId)}">
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
<span class="item-text">${esc(z.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
</div>`;
}
// ── Sort ──
function sortByLevelThenName(items) {
return items.slice().sort((a, b) => {
const la = a.level ?? 999, lb = b.level ?? 999;
if (la !== lb) return la - lb;
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
});
}
function sortByName(items) {
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
}
// ── Group toggle ──
function toggleGroup(gruppenId) {
const card = document.getElementById('dgroup-' + gruppenId);
const body = document.getElementById('dbody-' + gruppenId);
if (!card) return;
if (card.classList.contains('open')) {
card.classList.remove('open');
body.style.display = 'none';
if (openGroupId === gruppenId) openGroupId = null;
} else {
if (openGroupId) {
const prev = document.getElementById('dgroup-' + openGroupId);
const prevBody = document.getElementById('dbody-' + openGroupId);
if (prev) prev.classList.remove('open');
if (prevBody) prevBody.style.display = 'none';
}
card.classList.add('open');
body.style.display = 'block';
openGroupId = gruppenId;
openItemId = null;
}
}
// ── Item toggle ──
function toggleItem(itemId) {
if (openItemId === itemId) {
const el = document.getElementById('ditem-' + itemId);
if (el) el.classList.remove('open');
openItemId = null;
return;
}
if (openItemId) {
const prev = document.getElementById('ditem-' + openItemId);
if (prev) prev.classList.remove('open');
}
const el = document.getElementById('ditem-' + itemId);
if (el) el.classList.add('open');
openItemId = itemId;
}
// ── Subscribe / Unsubscribe ──
function toggleSubscribe(gruppenId, btn) {
btn.disabled = true;
const isSubscribed = btn.classList.contains('subscribed');
const method = isSubscribed ? 'DELETE' : 'POST';
fetch(`/abo/${gruppenId}`, { method })
.then(r => {
if (r.ok || r.status === 201 || r.status === 202) {
if (isSubscribed) {
btn.classList.remove('subscribed');
btn.textContent = '♥ Abonnieren';
updateBadge(gruppenId, false);
} else {
btn.classList.add('subscribed');
btn.textContent = '♥ Abonniert';
updateBadge(gruppenId, true);
}
btn.disabled = false;
} else {
btn.disabled = false;
}
})
.catch(() => { btn.disabled = false; });
}
function updateBadge(gruppenId, subscribed) {
const card = document.getElementById('dgroup-' + gruppenId);
if (!card) return;
const badgesEl = card.querySelector('.gruppe-badges');
if (!badgesEl) return;
const subBadge = badgesEl.querySelector('.gruppe-badge-sub');
if (subscribed && !subBadge) {
badgesEl.insertAdjacentHTML('beforeend', `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`);
} else if (!subscribed && subBadge) {
subBadge.remove();
}
}
// ── Search ──
document.getElementById('searchBtn').addEventListener('click', () => {
currentName = document.getElementById('searchInput').value.trim();
currentPage = 0;
loadGroups();
});
document.getElementById('searchInput').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('searchBtn').click();
});
// ── Paging ──
function updatePaging(current, total) {
const el = document.getElementById('paging');
if (total <= 1) { el.style.display = 'none'; return; }
el.style.display = 'flex';
document.getElementById('prevBtn').disabled = current === 0;
document.getElementById('nextBtn').disabled = current >= total - 1;
document.getElementById('pageInfo').textContent = `Seite ${current + 1} von ${total}`;
}
document.getElementById('prevBtn').addEventListener('click', () => {
if (currentPage > 0) { currentPage--; loadGroups(); }
});
document.getElementById('nextBtn').addEventListener('click', () => {
if (currentPage < totalPages - 1) { currentPage++; loadGroups(); }
});
// ── Burger menu ──
const sidebar = document.getElementById('sidebar');
const burgerBtn = document.getElementById('burgerBtn');
const overlay = document.getElementById('overlay');
function openMenu() {
sidebar.classList.add('open'); overlay.classList.add('visible');
burgerBtn.classList.add('open'); burgerBtn.setAttribute('aria-label', 'Menü schließen');
}
function closeMenu() {
sidebar.classList.remove('open'); overlay.classList.remove('visible');
burgerBtn.classList.remove('open'); burgerBtn.setAttribute('aria-label', 'Menü öffnen');
}
burgerBtn.addEventListener('click', () => sidebar.classList.contains('open') ? closeMenu() : openMenu());
overlay.addEventListener('click', closeMenu);
sidebar.querySelectorAll('a').forEach(l => l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); }));
</script>
</body>
</html>