Files
xxx-sphere-web/bin/main/static/js/mobile-nav.js
Mario e2a71ab096
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Hashtags eingeführt
2026-04-11 01:14:33 +02:00

460 lines
22 KiB
JavaScript

(function () {
if (window.__mobileNavLoaded) return;
window.__mobileNavLoaded = true;
const path = window.location.pathname;
const I = window.IC || function () { return ''; };
const TOPBAR_H = '4.875rem';
// ── CSS ──────────────────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `
.mobile-topbar {
display: none;
position: fixed;
top: 0; left: 0; right: 0;
height: ${TOPBAR_H};
background: var(--color-card);
border-bottom: 1px solid var(--color-secondary);
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
z-index: 500;
align-items: center;
padding: 0 4px 0 0.25rem;
}
.mobile-topbar-logo {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
pointer-events: auto;
}
.mobile-topbar-logo img {
height: 3rem;
width: auto;
}
.mobile-topbar-actions {
display: flex;
align-items: center;
margin-left: auto;
}
.mobile-tb-btn {
position: relative;
background: none;
border: none;
color: var(--color-text);
font-size: 1.3rem;
line-height: 1;
padding: 0.55rem 0.6rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
text-decoration: none;
transition: background 0.12s;
flex-shrink: 0;
}
.mobile-tb-btn:hover { background: var(--color-secondary); }
.mobile-tb-badge {
position: absolute;
top: 3px; right: 3px;
background: var(--color-primary);
color: #fff;
border-radius: 10px;
font-size: 0.87rem;
font-weight: 700;
min-width: 1.65em;
padding: 0.15em 0.375em;
display: none;
line-height: 1.4;
text-align: center;
}
.mobile-tb-avatar {
width: 2.7rem;
height: 2.7rem;
border-radius: 50%;
object-fit: cover;
}
/* ── Mobile Menu Backdrop ── */
.mob-menu-backdrop {
display: none;
position: fixed;
top: ${TOPBAR_H};
left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.55);
z-index: 998;
}
.mob-menu-backdrop.open { display: block; }
/* ── Mobile Menu Panel ── */
.mob-menu-panel {
position: fixed;
top: ${TOPBAR_H};
right: 0;
bottom: 0;
width: min(80%, 360px);
background: var(--color-card);
border-left: 1px solid var(--color-secondary);
z-index: 999;
overflow-y: auto;
transform: translateX(100%);
transition: transform 0.25s ease;
}
.mob-menu-panel.open { transform: translateX(0); }
/* ── Accordion ── */
.mnav-section { border-bottom: 1px solid var(--color-secondary); }
.mnav-section:last-child { border-bottom: none; }
.mnav-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.85rem 1.1rem;
cursor: pointer;
user-select: none;
font-size: 0.88rem;
font-weight: 600;
color: var(--color-text);
transition: background 0.12s;
}
.mnav-section-header:hover { background: var(--color-secondary); }
.mnav-section-arrow {
font-size: 0.65rem;
transition: transform 0.2s;
color: var(--color-muted);
}
.mnav-section.open .mnav-section-arrow { transform: rotate(90deg); }
.mnav-section-body { display: none; }
.mnav-section.open .mnav-section-body { display: block; }
/* ── Menu Links ── */
.mnav-link {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.6rem 1.1rem 0.6rem 1.5rem;
text-decoration: none;
color: var(--color-text);
font-size: 0.9rem;
transition: background 0.12s, color 0.12s;
}
.mnav-link:hover { background: var(--color-secondary); color: var(--color-primary); }
.mnav-link.active { color: var(--color-primary); background: rgba(var(--color-primary-rgb,233,69,96),0.08); }
.mnav-icon { width: 1.3rem; text-align: center; font-size: 1rem; flex-shrink: 0; }
.mnav-badge {
margin-left: auto;
background: var(--color-primary); color: #fff;
border-radius: 10px; font-size: 0.68rem; font-weight: 700;
min-width: 1.2em; padding: 0.1em 0.3em; display: none;
}
.mnav-link--danger { color: var(--color-primary); }
.mnav-link--danger:hover { background: rgba(var(--color-primary-rgb,233,69,96),0.1); color: var(--color-primary); }
/* ── Show only on mobile ── */
@media (max-width: 768px) {
.mobile-topbar { display: flex; }
body.app { padding-top: ${TOPBAR_H}; }
}
`;
document.head.appendChild(style);
// ── Helper ───────────────────────────────────────────────────────────────
function lnk(href, iconKey, label, id, badgeId) {
const cls = path === href ? ' active' : '';
const idAttr = id ? ` id="${id}"` : '';
const badge = badgeId ? `<span class="mnav-badge" id="${badgeId}"></span>` : '';
const icon = iconKey ? `<span class="mnav-icon">${I(iconKey) || ''}</span>` : `<span class="mnav-icon"></span>`;
return `<a href="${href}" class="mnav-link${cls}"${idAttr}>${icon}<span>${label}</span>${badge}</a>`;
}
// ── Menu-Sektionen ────────────────────────────────────────────────────────
const SECTIONS = [
{
label: 'Allgemein',
prefixes: ['/userhome.html', '/search.html', '/community/nachrichten.html',
'/community/benachrichtigungen.html', '/games/common/einladungen.html'],
html: `
${lnk('/userhome.html', 'HOME', 'Home' )}
${lnk('/search.html', 'SEARCH', 'Suche' )}
<a href="/admin/admin.html" class="mnav-link${path === '/admin/admin.html' ? ' active' : ''}"
id="mnavAdminLink" style="display:none">
<span class="mnav-icon">${I('ADMIN') || '⚙'}</span><span>Administration</span></a>
${lnk('/community/nachrichten.html', 'MESSAGES', 'Nachrichten', null, 'mnavBadgeMsg' )}
${lnk('/community/benachrichtigungen.html', 'NOTIFICATIONS', 'Benachrichtigungen', null, 'mnavBadgeNotif' )}
${lnk('/games/common/einladungen.html', 'INVITATIONS', 'Einladungen', null, 'mnavBadgeInv' )}
`,
},
{
label: 'Community',
prefixes: ['/community/'],
html: `
${lnk('/community/feed.html', 'FEED', 'Feed' )}
${lnk('/community/freunde.html', 'FRIENDS', 'Freunde', null, 'mnavBadgeFriends' )}
${lnk('/community/gruppen.html', 'GROUPS', 'Gruppen', null, 'mnavBadgeGruppen' )}
${lnk('/community/locations.html', 'LOCATION', 'Locations' )}
${lnk('/community/events.html', 'EVENT', 'Veranstaltungen' )}
`,
},
{
label: 'Dating',
prefixes: ['/dating/'],
html: `
<a href="/dating/dating.html" class="mnav-link${path === '/dating/dating.html' ? ' active' : ''}" id="mnavDatingLink">
<span class="mnav-icon">${I('DATING') || '♥'}</span><span>Dating</span></a>
${lnk('/dating/besucher.html', '', 'Besucher')}
${lnk('/dating/likes.html', '', 'Likes' )}
${lnk('/dating/matches.html', '', 'Matches' )}
`,
},
{
label: 'Vanilla Game',
prefixes: ['/games/vanilla/'],
html: `
${lnk('/games/vanilla/neuvanilla.html', 'PLAY_NEW', 'Neue Session', 'mnavVanillaNeu' )}
<a href="#" class="mnav-link" id="mnavVanillaAktiv" style="display:none">
<span class="mnav-icon">${I('WAITING') || ''}</span><span>Aktive Session</span></a>
<a href="/games/vanilla/vanillaingame.html" class="mnav-link${path === '/games/vanilla/vanillaingame.html' ? ' active' : ''}"
id="mnavVanillaImSpiel" style="display:none">
<span class="mnav-icon">${I('PLAY_ACTIVE') || ''}</span><span>Im Spiel</span></a>
${lnk('/games/vanilla/aufgaben.html', 'CHECK', 'Aufgaben' )}
${lnk('/games/vanilla/toys.html', 'TOYS', 'Toys' )}
${lnk('/games/vanilla/entdecken.html', 'DISCOVER', 'Entdecken' )}
`,
},
{
label: 'BDSM Game',
prefixes: ['/games/bdsm/'],
html: `
${lnk('/games/bdsm/neubdsm.html', 'PLAY_NEW', 'Neue Session', 'mnavBdsmNeu' )}
<a href="#" class="mnav-link" id="mnavBdsmAktiv" style="display:none">
<span class="mnav-icon">${I('WAITING') || ''}</span><span>Aktive Session</span></a>
<a href="/games/bdsm/bdsmingame.html" class="mnav-link${path === '/games/bdsm/bdsmingame.html' ? ' active' : ''}"
id="mnavBdsmImSpiel" style="display:none">
<span class="mnav-icon">${I('PLAY_ACTIVE') || ''}</span><span>Im Spiel</span></a>
${lnk('/games/bdsm/aufgaben.html', 'CHECK', 'Aufgaben' )}
${lnk('/games/bdsm/toys.html', 'TOYS', 'Toys' )}
${lnk('/games/bdsm/entdecken.html', 'DISCOVER', 'Entdecken' )}
`,
},
{
label: 'Chastity Game',
prefixes: ['/games/chastity/'],
html: `
${lnk('/games/chastity/neulock.html', 'NEW_LOCK', 'Neues Lock', 'mnavChastityNeu' )}
<a href="#" class="mnav-link" id="mnavChastityAktiv" style="display:none">
<span class="mnav-icon">${I('ACTIVE_LOCK') || ''}</span><span>Aktives Lock</span></a>
${lnk('/games/chastity/communityvotes.html', 'VOTES', 'Community Votes' )}
${lnk('/games/chastity/meine-locks.html', 'LOCK', 'Meine Vorlagen' )}
${lnk('/games/chastity/entdecken-vorlagen.html', 'DISCOVER', 'Entdecken' )}
${lnk('/games/chastity/keyholder-finden.html', 'FRIENDS', 'Keyholder finden' )}
${lnk('/games/chastity/keyholder.html', 'KEY', 'Keyholder' )}
${lnk('/games/chastity/unlock-history.html', 'HISTORY', 'Code-Historie' )}
`,
},
{
label: 'Konto',
prefixes: ['/konto/', '/help/'],
html: `
${lnk('/konto/einstellungen.html', 'SETTINGS', 'Einstellungen')}
${lnk('/help/overview.html', 'HELP', 'Hilfe' )}
<a href="/login/logout" class="mnav-link mnav-link--danger">
<span class="mnav-icon">${I('LOGOUT') || ''}</span><span>Abmelden</span></a>
`,
},
];
// ── Mobile Topbar ─────────────────────────────────────────────────────────
const topbarEl = document.createElement('div');
topbarEl.className = 'mobile-topbar';
topbarEl.id = 'mobileTopbar';
topbarEl.innerHTML = `
<a href="/userhome.html" class="mobile-topbar-logo">
<img src="/img/icon.png" alt="xXx Sphere">
</a>
<div class="mobile-topbar-actions">
<a href="/community/nachrichten.html" class="mobile-tb-btn" title="Nachrichten">
${I('MESSAGES')}
<span class="mobile-tb-badge" id="mobTbMsgBadge"></span>
</a>
<a href="/community/benachrichtigungen.html" class="mobile-tb-btn" title="Benachrichtigungen">
${I('NOTIFICATIONS')}
<span class="mobile-tb-badge" id="mobTbNotifBadge"></span>
</a>
<a href="/games/common/einladungen.html" class="mobile-tb-btn" title="Einladungen">
${I('INVITATIONS')}
<span class="mobile-tb-badge" id="mobTbInvBadge"></span>
</a>
<a href="/community/benutzer.html" class="mobile-tb-btn" id="mobTbProfileBtn" title="Profil">
${I('PROFILE')}
</a>
<button class="mobile-tb-btn" id="mobMenuToggle" aria-label="Menü">
${I('MENU') || '☰'}
</button>
</div>
`;
document.body.insertAdjacentElement('afterbegin', topbarEl);
// ── Overlay ───────────────────────────────────────────────────────────────
const backdropEl = document.createElement('div');
backdropEl.className = 'mob-menu-backdrop';
backdropEl.id = 'mobMenuBackdrop';
const panelEl = document.createElement('div');
panelEl.className = 'mob-menu-panel';
panelEl.id = 'mobMenuPanel';
panelEl.innerHTML = SECTIONS.map(s => {
const isOpen = s.prefixes.some(p => path.startsWith(p) || path === p);
return `
<div class="mnav-section${isOpen ? ' open' : ''}">
<div class="mnav-section-header">
<span>${s.label}</span>
<span class="mnav-section-arrow">▶</span>
</div>
<div class="mnav-section-body">${s.html}</div>
</div>`;
}).join('');
document.body.appendChild(backdropEl);
document.body.appendChild(panelEl);
// ── Accordion (nur eine Sektion gleichzeitig offen) ──────────────────────
panelEl.querySelectorAll('.mnav-section-header').forEach(h => {
h.addEventListener('click', () => {
const section = h.closest('.mnav-section');
const isOpen = section.classList.contains('open');
panelEl.querySelectorAll('.mnav-section').forEach(s => s.classList.remove('open'));
if (!isOpen) section.classList.add('open');
});
});
// ── Open / Close ──────────────────────────────────────────────────────────
function openMenu() {
panelEl.classList.add('open');
backdropEl.classList.add('open');
}
function closeMenu() {
panelEl.classList.remove('open');
backdropEl.classList.remove('open');
}
document.getElementById('mobMenuToggle').addEventListener('click', e => {
e.stopPropagation();
panelEl.classList.contains('open') ? closeMenu() : openMenu();
});
backdropEl.addEventListener('click', closeMenu);
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMenu(); });
panelEl.querySelectorAll('.mnav-link').forEach(l => {
l.addEventListener('click', () => { if (l.getAttribute('href') !== '#') closeMenu(); });
});
// ── Badges ────────────────────────────────────────────────────────────────
function setBadge(id, n) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = n > 99 ? '99+' : n;
el.style.display = n > 0 ? 'inline-block' : 'none';
}
fetch('/social/messages/unread/count').then(r => r.ok ? r.json() : 0).then(n => {
setBadge('mobTbMsgBadge', n); setBadge('mnavBadgeMsg', n);
}).catch(() => {});
fetch('/notifications/unread/count').then(r => r.ok ? r.json() : 0).then(n => {
setBadge('mobTbNotifBadge', n); setBadge('mnavBadgeNotif', n);
}).catch(() => {});
Promise.all([
fetch('/lockee/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
fetch('/keyholder/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
fetch('/bdsm/einladung/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
fetch('/vanilla/einladung/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
]).then(([l, k, b, v]) => {
const n = l + k + b + v;
setBadge('mobTbInvBadge', n); setBadge('mnavBadgeInv', n);
}).catch(() => {});
fetch('/social/friends/pending/count').then(r => r.ok ? r.json() : 0)
.then(n => setBadge('mnavBadgeFriends', n)).catch(() => {});
Promise.all([
fetch('/gruppen/requests/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
fetch('/gruppen/reports/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
]).then(([j, rep]) => setBadge('mnavBadgeGruppen', j + rep)).catch(() => {});
// ── User / Dynamische Links ───────────────────────────────────────────────
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
if (!user) return;
// Profilbild
const profileBtn = document.getElementById('mobTbProfileBtn');
if (profileBtn) {
profileBtn.href = '/community/benutzer.html?userId=' + user.userId;
if (user.profilePicture) {
profileBtn.innerHTML =
`<img src="data:image/png;base64,${user.profilePicture}" class="mobile-tb-avatar" alt="">`;
}
}
// Admin
if (user.admin) {
const el = document.getElementById('mnavAdminLink');
if (el) el.style.display = '';
}
// Dating
const datingLink = document.getElementById('mnavDatingLink');
if (datingLink) {
datingLink.href = user.datingAktiv
? '/dating/dating.html'
: '/konto/einstellungen.html#sec-dating';
}
const hide = id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; };
const show = id => { const el = document.getElementById(id); if (el) el.style.display = ''; };
const setHref = (id, h) => { const el = document.getElementById(id); if (el) el.href = h; };
// BDSM
try {
const r = await fetch('/bdsm/einladung/meine-aktive');
if (r.ok) {
const aktiv = await r.json();
hide('mnavBdsmNeu'); hide('mnavBdsmImSpiel'); show('mnavBdsmAktiv');
setHref('mnavBdsmAktiv', aktiv.sessionId ? '/games/bdsm/bdsmingame.html' : '/games/bdsm/neubdsm.html');
} else {
const sr = await fetch(`/bdsm?userId=${user.userId}`);
if (sr.status === 200) { hide('mnavBdsmNeu'); show('mnavBdsmImSpiel'); }
else show('mnavBdsmNeu');
}
} catch (_) {}
// Vanilla
try {
const r = await fetch('/vanilla/einladung/meine-aktive');
if (r.ok) {
const aktiv = await r.json();
hide('mnavVanillaNeu'); hide('mnavVanillaImSpiel'); show('mnavVanillaAktiv');
setHref('mnavVanillaAktiv', aktiv.sessionId ? '/games/vanilla/vanillaingame.html' : '/games/vanilla/neuvanilla.html');
} else {
const sr = await fetch(`/vanilla?userId=${user.userId}`);
if (sr.status === 200) { hide('mnavVanillaNeu'); show('mnavVanillaImSpiel'); }
else show('mnavVanillaNeu');
}
} catch (_) {}
// Chastity
try {
const r = await fetch('/keyholder/mylock');
if (r.ok) {
const lock = await r.json();
show('mnavChastityAktiv');
setHref('mnavChastityAktiv', '/games/chastity/activelock.html?lockId=' + lock.lockId);
}
} catch (_) {}
}).catch(() => {});
})();