Umsetzung Aufgabenverwaltung
This commit is contained in:
1584
xxxthegame/src/main/resources/static/aufgaben.html
Normal file
1584
xxxthegame/src/main/resources/static/aufgaben.html
Normal file
File diff suppressed because it is too large
Load Diff
603
xxxthegame/src/main/resources/static/entdecken.html
Normal file
603
xxxthegame/src/main/resources/static/entdecken.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
780
xxxthegame/src/main/resources/static/toys.html
Normal file
780
xxxthegame/src/main/resources/static/toys.html
Normal file
@@ -0,0 +1,780 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Toys – 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 ── */
|
||||
.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 ── */
|
||||
.content { padding: 2rem 1.5rem; flex: 1; }
|
||||
|
||||
/* ── Overlay ── */
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
/* ── Section ── */
|
||||
.section + .section { margin-top: 2.5rem; }
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
}
|
||||
.btn-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-add:hover { background: #c73652; }
|
||||
|
||||
/* ── Toy grid ── */
|
||||
.toy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Toy card ── */
|
||||
.toy-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 0.8rem 0.9rem;
|
||||
transition: border-color 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
.toy-card { cursor: pointer; }
|
||||
.toy-card:hover { border-color: var(--color-primary); }
|
||||
.toy-card.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.06);
|
||||
}
|
||||
|
||||
.toy-img {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toy-img-placeholder {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.toy-info { flex: 1; min-width: 0; }
|
||||
.toy-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.toy-desc {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.2rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Section action buttons ── */
|
||||
.section-actions { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.btn-action {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.btn-action:disabled { opacity: 0.35; cursor: default; }
|
||||
.btn-action:not(:disabled):hover { background: var(--color-primary); color: #fff; }
|
||||
.btn-action-danger:not(:disabled):hover { background: rgba(233,69,96,0.18); color: var(--color-primary); }
|
||||
.action-error {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-primary);
|
||||
min-height: 1.1em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* ── 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 ── */
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Inline-Fehler im Grid ── */
|
||||
.grid-error {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-primary);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 200;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-backdrop.open { display: flex; }
|
||||
.modal {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.6);
|
||||
}
|
||||
.modal h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.modal label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.modal input[type="text"],
|
||||
.modal textarea {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
resize: vertical;
|
||||
}
|
||||
.modal input[type="text"]:focus,
|
||||
.modal textarea:focus { border-color: var(--color-primary); }
|
||||
.modal input[type="file"] {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.modal-actions .btn-cancel {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-cancel:hover { background: #1a4a8a; }
|
||||
.modal-actions .btn-save {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-save:hover { background: #c73652; }
|
||||
.modal-actions .btn-save:disabled { opacity: 0.5; cursor: default; }
|
||||
.modal-error {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.75rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── 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; }
|
||||
.toy-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="overlay" id="overlay"></div>
|
||||
|
||||
<!-- Erstell-/Bearbeitungs-Modal -->
|
||||
<div class="modal-backdrop" id="createModal">
|
||||
<div class="modal">
|
||||
<h2 id="modalTitle">Neues Toy</h2>
|
||||
<label for="toyName">Name *</label>
|
||||
<input type="text" id="toyName" placeholder="z.B. Vibrator" maxlength="100">
|
||||
<label for="toyDesc">Beschreibung</label>
|
||||
<textarea id="toyDesc" rows="3" placeholder="Kurze Beschreibung…" maxlength="500"></textarea>
|
||||
<label>Bild (optional)</label>
|
||||
<div id="currentImageWrap" style="display:none; align-items:center; gap:0.5rem; margin-bottom:0.4rem;">
|
||||
<img id="currentImage" style="max-width:64px; max-height:64px; border-radius:6px;" src="" alt="">
|
||||
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues Bild wählen zum Ersetzen</span>
|
||||
</div>
|
||||
<input type="file" id="toyBild" accept="image/*">
|
||||
<div class="modal-error" id="modalError"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
||||
<button class="btn-save" id="saveBtn">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</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="#"><span class="icon">⚡</span> Strafen</a></li>
|
||||
<li><a href="/toys.html" class="active"><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>Toys</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<!-- Meine Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Meine Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="editBtn" disabled>✎ Bearbeiten</button>
|
||||
<button class="btn-action btn-action-danger" id="deleteBtn" disabled>✕ Löschen</button>
|
||||
<button class="btn-add" id="openCreateBtn">+ Neu</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="actionError"></div>
|
||||
<div id="userLoading" class="loading">Wird geladen…</div>
|
||||
<div class="toy-grid" id="userGrid"></div>
|
||||
<div class="paging" id="userPaging" style="display:none;">
|
||||
<button id="userPrev">‹ Zurück</button>
|
||||
<span class="page-info" id="userPageInfo"></span>
|
||||
<button id="userNext">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System-Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">System-Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="copyBtn" disabled>⊕ In meine Toys kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="systemActionError"></div>
|
||||
<div id="systemLoading" class="loading">Wird geladen…</div>
|
||||
<div class="toy-grid" id="systemGrid"></div>
|
||||
<div class="paging" id="systemPaging" style="display:none;">
|
||||
<button id="systemPrev">‹ Zurück</button>
|
||||
<span class="page-info" id="systemPageInfo"></span>
|
||||
<button id="systemNext">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PAGE_SIZE = 12;
|
||||
let userPage = 0, userTotalPages = 1;
|
||||
let systemPage = 0, systemTotalPages = 1;
|
||||
|
||||
// ── Auth + initial load ──
|
||||
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; loadUserToys(); loadSystemToys(); })
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load user toys ──
|
||||
function loadUserToys() {
|
||||
resetSelection();
|
||||
document.getElementById('userLoading').style.display = 'block';
|
||||
fetch(`/toy/list/user?page=${userPage}&size=${PAGE_SIZE}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
userTotalPages = data.totalPages || 1;
|
||||
renderGrid('userGrid', data.content, 'selectToy');
|
||||
updatePaging('userPaging', 'userPrev', 'userNext', 'userPageInfo', userPage, userTotalPages);
|
||||
document.getElementById('userLoading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('userLoading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Load system toys ──
|
||||
function loadSystemToys() {
|
||||
resetSystemSelection();
|
||||
document.getElementById('systemLoading').style.display = 'block';
|
||||
fetch(`/toy/list/system?page=${systemPage}&size=${PAGE_SIZE}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
systemTotalPages = data.totalPages || 1;
|
||||
renderGrid('systemGrid', data.content, 'selectSystemToy');
|
||||
updatePaging('systemPaging', 'systemPrev', 'systemNext', 'systemPageInfo', systemPage, systemTotalPages);
|
||||
document.getElementById('systemLoading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('systemLoading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Selection ──
|
||||
let selectedUserToyId = null;
|
||||
|
||||
function selectToy(toyId) {
|
||||
const prev = document.querySelector('#userGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedUserToyId === toyId) {
|
||||
selectedUserToyId = null;
|
||||
} else {
|
||||
selectedUserToyId = toyId;
|
||||
document.querySelector(`#userGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
const has = selectedUserToyId != null;
|
||||
document.getElementById('editBtn').disabled = !has;
|
||||
document.getElementById('deleteBtn').disabled = !has;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
selectedUserToyId = null;
|
||||
document.getElementById('editBtn').disabled = true;
|
||||
document.getElementById('deleteBtn').disabled = true;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── System-Toy selection ──
|
||||
let selectedSystemToyId = null;
|
||||
|
||||
function selectSystemToy(toyId) {
|
||||
const prev = document.querySelector('#systemGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedSystemToyId === toyId) {
|
||||
selectedSystemToyId = null;
|
||||
} else {
|
||||
selectedSystemToyId = toyId;
|
||||
document.querySelector(`#systemGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
document.getElementById('copyBtn').disabled = selectedSystemToyId == null;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSystemSelection() {
|
||||
selectedSystemToyId = null;
|
||||
document.getElementById('copyBtn').disabled = true;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── Copy system toy ──
|
||||
document.getElementById('copyBtn').addEventListener('click', () => {
|
||||
if (!selectedSystemToyId) return;
|
||||
const btn = document.getElementById('copyBtn');
|
||||
btn.disabled = true;
|
||||
fetch(`/toy/copy/${selectedSystemToyId}`, { method: 'POST' })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
userPage = 0;
|
||||
loadUserToys();
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Du hast bereits ein Toy mit diesem Namen.';
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Fehler beim Kopieren (HTTP ' + r.status + ').';
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('systemActionError').textContent = 'Verbindungsfehler.';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Render a grid ──
|
||||
function renderGrid(gridId, toys, selectFn) {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!toys || toys.length === 0) {
|
||||
grid.innerHTML = '<p class="empty">Keine Einträge vorhanden.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = toys.map(toy => `
|
||||
<div class="toy-card" data-id="${esc(toy.toyId)}"
|
||||
${selectFn ? `onclick="${selectFn}('${esc(toy.toyId)}')"` : ''}>
|
||||
${toy.bild
|
||||
? `<img class="toy-img" src="data:image/png;base64,${toy.bild}" alt="${esc(toy.name)}">`
|
||||
: `<div class="toy-img-placeholder">◈</div>`}
|
||||
<div class="toy-info">
|
||||
<div class="toy-name">${esc(toy.name)}</div>
|
||||
${toy.beschreibung ? `<div class="toy-desc">${esc(toy.beschreibung)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ── Update paging controls ──
|
||||
function updatePaging(pagingId, prevId, nextId, infoId, current, total) {
|
||||
const el = document.getElementById(pagingId);
|
||||
if (total <= 1) { el.style.display = 'none'; return; }
|
||||
el.style.display = 'flex';
|
||||
document.getElementById(prevId).disabled = current === 0;
|
||||
document.getElementById(nextId).disabled = current >= total - 1;
|
||||
document.getElementById(infoId).textContent = `Seite ${current + 1} von ${total}`;
|
||||
}
|
||||
|
||||
// ── Paging button handlers ──
|
||||
document.getElementById('userPrev').addEventListener('click', () => { if (userPage > 0) { userPage--; loadUserToys(); } });
|
||||
document.getElementById('userNext').addEventListener('click', () => { if (userPage < userTotalPages - 1) { userPage++; loadUserToys(); } });
|
||||
document.getElementById('systemPrev').addEventListener('click', () => { if (systemPage > 0) { systemPage--; loadSystemToys(); } });
|
||||
document.getElementById('systemNext').addEventListener('click', () => { if (systemPage < systemTotalPages - 1) { systemPage++; loadSystemToys(); } });
|
||||
|
||||
// ── Header action buttons ──
|
||||
document.getElementById('editBtn').addEventListener('click', () => {
|
||||
if (selectedUserToyId) openModal(selectedUserToyId);
|
||||
});
|
||||
|
||||
document.getElementById('deleteBtn').addEventListener('click', () => {
|
||||
if (!selectedUserToyId) return;
|
||||
if (!confirm('Toy wirklich löschen?')) return;
|
||||
const btn = document.getElementById('deleteBtn');
|
||||
btn.disabled = true;
|
||||
const toyId = selectedUserToyId;
|
||||
fetch(`/toy/${toyId}`, { method: 'DELETE' })
|
||||
.then(r => {
|
||||
if (r.status === 409) {
|
||||
showActionError('Wird in Aufgaben verwendet – nicht löschbar.');
|
||||
btn.disabled = false;
|
||||
} else if (r.status === 403) {
|
||||
showActionError('Keine Berechtigung.');
|
||||
btn.disabled = false;
|
||||
} else if (r.ok || r.status === 202) {
|
||||
userPage = 0;
|
||||
loadUserToys();
|
||||
} else {
|
||||
showActionError('Fehler beim Löschen.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { showActionError('Verbindungsfehler.'); btn.disabled = false; });
|
||||
});
|
||||
|
||||
function showActionError(msg) {
|
||||
const el = document.getElementById('actionError');
|
||||
el.textContent = msg;
|
||||
setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 4000);
|
||||
}
|
||||
|
||||
// ── Create / Edit modal ──
|
||||
const modal = document.getElementById('createModal');
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
let currentEditId = null;
|
||||
|
||||
function openModal(editId) {
|
||||
currentEditId = editId || null;
|
||||
document.getElementById('modalError').style.display = 'none';
|
||||
document.getElementById('toyBild').value = '';
|
||||
if (currentEditId) {
|
||||
fetch(`/toy/${currentEditId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(toy => {
|
||||
if (!toy) return;
|
||||
document.getElementById('modalTitle').textContent = 'Toy bearbeiten';
|
||||
document.getElementById('toyName').value = toy.name || '';
|
||||
document.getElementById('toyDesc').value = toy.beschreibung || '';
|
||||
const imgWrap = document.getElementById('currentImageWrap');
|
||||
if (toy.bild) {
|
||||
document.getElementById('currentImage').src = 'data:image/png;base64,' + toy.bild;
|
||||
imgWrap.style.display = 'flex';
|
||||
} else {
|
||||
imgWrap.style.display = 'none';
|
||||
}
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
})
|
||||
.catch(() => alert('Fehler beim Laden des Toys.'));
|
||||
} else {
|
||||
document.getElementById('modalTitle').textContent = 'Neues Toy';
|
||||
document.getElementById('toyName').value = '';
|
||||
document.getElementById('toyDesc').value = '';
|
||||
document.getElementById('currentImageWrap').style.display = 'none';
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
|
||||
|
||||
function closeModal() { modal.classList.remove('open'); }
|
||||
|
||||
function editToy(toyId) { openModal(toyId); }
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const name = document.getElementById('toyName').value.trim();
|
||||
if (!name) {
|
||||
showModalError('Bitte einen Namen eingeben.');
|
||||
return;
|
||||
}
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichert…';
|
||||
|
||||
let bildBase64 = null;
|
||||
const fileInput = document.getElementById('toyBild');
|
||||
if (fileInput.files.length > 0) {
|
||||
bildBase64 = await toBase64(fileInput.files[0]);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
beschreibung: document.getElementById('toyDesc').value.trim() || null,
|
||||
bild: bildBase64
|
||||
};
|
||||
|
||||
const isEdit = currentEditId != null;
|
||||
fetch(isEdit ? `/toy/${currentEditId}` : '/toy', {
|
||||
method: isEdit ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
closeModal();
|
||||
userPage = 0;
|
||||
loadUserToys();
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
showModalError('Ein Toy mit diesem Namen existiert bereits.');
|
||||
} else {
|
||||
showModalError('Fehler beim Speichern (HTTP ' + r.status + ').');
|
||||
}
|
||||
})
|
||||
.catch(() => showModalError('Verbindungsfehler.'))
|
||||
.finally(() => { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; });
|
||||
});
|
||||
|
||||
function showModalError(msg) {
|
||||
const el = document.getElementById('modalError');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function toBase64(file) {
|
||||
const MAX = 128;
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(file);
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > MAX || h > MAX) {
|
||||
if (w >= h) { h = Math.max(1, Math.round(MAX * h / w)); w = MAX; }
|
||||
else { w = Math.max(1, Math.round(MAX * w / h)); h = MAX; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
resolve(canvas.toDataURL('image/png').split(',')[1]);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
// ── XSS-Schutz ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
@@ -6,19 +6,274 @@
|
||||
<title>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 ── */
|
||||
.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 ── */
|
||||
.content {
|
||||
padding: 2rem 1.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Overlay ── */
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
/* ── 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>
|
||||
<h1>XXX The Game</h1>
|
||||
<p id="greeting"></p>
|
||||
|
||||
<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="#" class="active"><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"><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>XXX The Game</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<p id="greeting"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const userJson = sessionStorage.getItem('user');
|
||||
if (!userJson) {
|
||||
window.location.href = '/login.html';
|
||||
} else {
|
||||
const user = JSON.parse(userJson);
|
||||
document.getElementById('greeting').textContent = 'Willkommen, ' + user.name + '!';
|
||||
// ── Auth check ──
|
||||
fetch('/login/me')
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login.html';
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(user => {
|
||||
if (user) {
|
||||
document.getElementById('greeting').textContent = 'Willkommen, ' + user.name + '!';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.href = '/login.html';
|
||||
});
|
||||
|
||||
// ── 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);
|
||||
|
||||
// Close on nav link click (mobile)
|
||||
sidebar.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 768) closeMenu();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user