weitere änderungen an der Oberfläche, timelock hinzugefügt

This commit is contained in:
2026-03-23 16:11:28 +01:00
parent 409f003aec
commit 4f521b6725
132 changed files with 3743 additions and 16946 deletions

View File

@@ -0,0 +1,48 @@
/**
* Zentrale Icon-Verwaltung XXX The Game
* Alle Emojis und Symbole der App werden hier definiert.
* Typen: emoji (Standard-Emoji), symbol (Unicode-Symbol), image (Pfad zu Bilddatei)
*/
window.ICONS = {
// ── Navigation / Sidebar ──
HOME: { type: 'emoji', value: '⊞' },
VANILLA: { type: 'symbol', value: '♡' },
BDSM: { type: 'symbol', value: '◆' },
CHASTITY: { type: 'symbol', value: '⊗' },
// ── Aktionen ──
PLAY_NEW: { type: 'symbol', value: '▷' },
PLAY_ACTIVE: { type: 'symbol', value: '▶' },
ACTIVE_LOCK: { type: 'emoji', value: '▶️' },
WAITING: { type: 'emoji', value: '⏳' },
CHECK: { type: 'symbol', value: '✓' },
DISCOVER: { type: 'symbol', value: '⊙' },
ARROW: { type: 'symbol', value: '▸' },
// ── Chastity Game ──
NEW_LOCK: { type: 'emoji', value: '🆕' },
LOCK: { type: 'emoji', value: '🔒' },
KEY: { type: 'emoji', value: '🔑' },
HISTORY: { type: 'emoji', value: '🔙' },
VOTES: { type: 'emoji', value: '🗳️' },
// ── Social ──
FEED: { type: 'emoji', value: '📰' },
SEARCH: { type: 'emoji', value: '🔍' },
FRIENDS: { type: 'emoji', value: '❤️' },
MESSAGES: { type: 'emoji', value: '💬' },
NOTIFICATIONS: { type: 'emoji', value: '🔔' },
GROUPS: { type: 'emoji', value: '👥' },
INVITATIONS: { type: 'emoji', value: '✨' },
SETTINGS: { type: 'emoji', value: '⚙️' },
LOGOUT: { type: 'symbol', value: '⏏' },
PROFILE: { type: 'symbol', value: '◉' },
// ── Aufgaben / Items ──
TOYS: { type: 'symbol', value: '◈' },
};
/** Gibt nur den Wert (String) zurück für einfache Einbindung in Templates */
window.IC = function(key) {
return (window.ICONS[key] || {}).value || '';
};

View File

@@ -1,178 +1,198 @@
(function () {
const path = window.location.pathname;
const groups = [
{
label: 'Vanilla Game',
icon: '',
items: [
{ href: '/sessionvanilla.html', icon: '▷', label: 'Neue Session' },
]
},
{
label: 'BDSM Game',
icon: '',
items: [
{ href: '/neubdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
{ href: '#', icon: '⏳', label: 'Aktive Session', id: 'navBdsmAktiv' },
{ href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
{ href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' },
{ href: '/toys.html', icon: '◈', label: 'Toys' },
{ href: '/entdecken.html', icon: '⊙', label: 'Entdecken' },
]
},
{
label: 'Chastity Game',
icon: '',
items: [
{ href: '/neulock.html', icon: '🆕', label: 'Neues Lock', id: 'navChastityNeu' },
{ href: '#', icon: '▶️', label: 'Aktives Lock', id: 'navChastityAktiv' },
{ href: '/communityvotes.html', icon: '🗳️', label: 'Community Votes' },
{ href: '/meine-locks.html', icon: '🔒', label: 'Meine Locks' },
{ href: '/keyholder.html', icon: '🔑', label: 'Keyholder' },
{ href: '/unlock-history.html', icon: '🔙', label: 'Code-Historie' },
]
},
];
const homeCls = path === '/userhome.html' ? ' class="active"' : '';
const homeItem = `
<li class="sidebar-mobile-only">
<a href="/userhome.html"${homeCls}><span class="icon">⊞</span> Home</a>
</li>`;
const nav = groups.map(({ label, icon, items }) => {
const isOpen = items.some(item => item.href === path);
const openCls = isOpen ? ' open' : '';
const subItems = items.map(({ href, icon: iIcon, label: iLabel, id: iId }) => {
const cls = path === href ? ' class="active"' : '';
const idAt = iId ? ` id="${iId}"` : '';
return `<li${idAt}><a href="${href}"${cls}><span class="icon">${iIcon}</span> ${iLabel}</a></li>`;
}).join('');
return `
<li class="sidebar-group${openCls}">
<a class="sidebar-group-toggle"><span class="icon">${icon}</span> ${label}<span class="sidebar-arrow">▸</span></a>
<ul class="sidebar-sub">
${subItems}
</ul>
</li>`;
}).join('');
document.body.insertAdjacentHTML('afterbegin', `
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
<span class="burger-icon"><span></span><span></span><span></span></span>
</button>
<aside class="sidebar" id="sidebar">
<div class="sidebar-logo-area">
<a href="/userhome.html"><img src="/img/logo.png" alt="Logo"></a>
</div>
<ul>
${homeItem}
${nav}
</ul>
</aside>
`);
// Sidebar und .main in einen zentrierten App-Wrapper verschieben
const appWrapper = document.createElement('div');
appWrapper.className = 'app-wrapper';
const sidebarEl = document.getElementById('sidebar');
const mainEl = document.querySelector('.main');
document.body.insertBefore(appWrapper, sidebarEl);
appWrapper.appendChild(sidebarEl);
if (mainEl) appWrapper.appendChild(mainEl);
// Group toggle
document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => {
toggle.addEventListener('click', e => {
e.preventDefault();
toggle.closest('.sidebar-group').classList.toggle('open');
});
});
// "Im Spiel" und "Aktive Session" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
const navNeu = document.getElementById('navBdsmNeu');
const navAktiv = document.getElementById('navBdsmAktiv');
const navImSpiel = document.getElementById('navBdsmImSpiel');
const navCAktiv = document.getElementById('navChastityAktiv');
if (navAktiv) navAktiv.style.display = 'none';
if (navImSpiel) navImSpiel.style.display = 'none';
if (navCAktiv) navCAktiv.style.display = 'none';
// Session-Status prüfen
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
.then(async user => {
if (!user) return;
// BDSM Session-Status
try {
// Zuerst aktive Einladung prüfen (eigenesGeraet-Spieler)
const aktivRes = await fetch('/bdsm/einladung/meine-aktive');
if (aktivRes.ok) {
const aktiv = await aktivRes.json();
if (navNeu) navNeu.style.display = 'none';
if (navImSpiel) navImSpiel.style.display = 'none';
if (navAktiv) {
navAktiv.style.display = '';
const ziel = aktiv.sessionId
? '/bdsmingame.html'
: `/neubdsm.html`;
navAktiv.querySelector('a').href = ziel;
}
} else {
// Dann laufende Host-Session prüfen
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
const hasSession = sessionRes.status === 200;
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
}
} catch (_) { /* Menü bleibt im Standardzustand */ }
// Chastity Lock-Status
try {
const lockRes = await fetch('/keyholder/mylock');
if (lockRes.ok) {
const lockData = await lockRes.json();
const lockId = lockData.lockId;
if (navCAktiv) {
navCAktiv.style.display = '';
navCAktiv.querySelector('a').href = '/activelock.html?lockId=' + lockId;
}
}
} catch (_) { /* Menü bleibt im Standardzustand */ }
})
.catch(() => {});
const sidebar = document.getElementById('sidebar');
const burgerBtn = document.getElementById('burgerBtn');
const overlay = document.getElementById('sidebarOverlay');
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:not(.sidebar-group-toggle)').forEach(l =>
l.addEventListener('click', () => { if (window.innerWidth <= (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile').trim()) || 768)) closeMenu(); })
);
// Social sidebar auf allen App-Seiten nachladen
const s = document.createElement('script');
s.src = '/js/social-sidebar.js';
document.head.appendChild(s);
})();
(function () {
const path = window.location.pathname;
const I = window.IC || function() { return ''; };
const groups = [
{
label: 'Vanilla Game',
icon: I('VANILLA'),
items: [
{ href: '/sessionvanilla.html', icon: I('PLAY_NEW'), label: 'Neue Session' },
]
},
{
label: 'BDSM Game',
icon: I('BDSM'),
items: [
{ href: '/neubdsm.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navBdsmNeu' },
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navBdsmAktiv' },
{ href: '/bdsmingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navBdsmImSpiel' },
{ href: '/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
{ href: '/toys.html', icon: I('TOYS'), label: 'Toys' },
{ href: '/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
]
},
{
label: 'Chastity Game',
icon: I('CHASTITY'),
items: [
{ href: '/neulock.html', icon: I('NEW_LOCK'), label: 'Neues Lock', id: 'navChastityNeu' },
{ href: '#', icon: I('ACTIVE_LOCK'), label: 'Aktives Lock', id: 'navChastityAktiv' },
{ href: '/communityvotes.html', icon: I('VOTES'), label: 'Community Votes' },
{ href: '/meine-locks.html', icon: I('LOCK'), label: 'Meine Locks' },
{ href: '/keyholder.html', icon: I('KEY'), label: 'Keyholder' },
{ href: '/unlock-history.html', icon: I('HISTORY'), label: 'Code-Historie' },
]
},
];
const homeCls = path === '/userhome.html' ? ' class="active"' : '';
const homeItem = `
<li class="sidebar-mobile-only">
<a href="/userhome.html"${homeCls}><span class="icon">${I('HOME')}</span> Home</a>
</li>`;
// ── Community-Links (immer sichtbar, oberhalb der Spiele) ──
const socialLinks = [
{ href: '/feed.html', icon: I('FEED'), label: 'Feed', badgeId: null },
{ href: '/freunde.html', icon: I('FRIENDS'), label: 'Freunde', badgeId: 'socialFriendsBadge'},
{ href: '/nachrichten.html', icon: I('MESSAGES'), label: 'Nachrichten', badgeId: null },
{ href: '/benachrichtigungen.html', icon: I('NOTIFICATIONS'), label: 'Benachrichtigungen', badgeId: null },
{ href: '/gruppen.html', icon: I('GROUPS'), label: 'Gruppen', badgeId: 'socialGruppenBadge'},
{ href: '/einladungen.html', icon: I('INVITATIONS'), label: 'Einladungen', badgeId: null },
];
const socialNav = socialLinks.map(({ href, icon, label, badgeId }) => {
const cls = path === href ? ' class="active"' : '';
const badge = badgeId ? `<span class="social-badge" id="${badgeId}" style="display:none;"></span>` : '';
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
}).join('');
const nav = groups.map(({ label, icon, items }) => {
const isOpen = items.some(item => item.href === path);
const openCls = isOpen ? ' open' : '';
const subItems = items.map(({ href, icon: iIcon, label: iLabel, id: iId }) => {
const cls = path === href ? ' class="active"' : '';
const idAt = iId ? ` id="${iId}"` : '';
return `<li${idAt}><a href="${href}"${cls}><span class="icon">${iIcon}</span> ${iLabel}</a></li>`;
}).join('');
return `
<li class="sidebar-group${openCls}">
<a class="sidebar-group-toggle"><span class="icon">${icon}</span> ${label}<span class="sidebar-arrow">${I('ARROW')}</span></a>
<ul class="sidebar-sub">
${subItems}
</ul>
</li>`;
}).join('');
document.body.insertAdjacentHTML('afterbegin', `
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
<span class="burger-icon"><span></span><span></span><span></span></span>
</button>
<aside class="sidebar" id="sidebar">
<div class="sidebar-logo-area">
<a href="/userhome.html"><img src="/img/logo.png" alt="Logo"></a>
</div>
<ul>
${homeItem}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${socialNav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${nav}
</ul>
</aside>
`);
// Sidebar und .main in einen zentrierten App-Wrapper verschieben
const appWrapper = document.createElement('div');
appWrapper.className = 'app-wrapper';
const sidebarEl = document.getElementById('sidebar');
const mainEl = document.querySelector('.main');
document.body.insertBefore(appWrapper, sidebarEl);
appWrapper.appendChild(sidebarEl);
if (mainEl) appWrapper.appendChild(mainEl);
// Group toggle
document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => {
toggle.addEventListener('click', e => {
e.preventDefault();
toggle.closest('.sidebar-group').classList.toggle('open');
});
});
// "Im Spiel" und "Aktive Session" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
const navNeu = document.getElementById('navBdsmNeu');
const navAktiv = document.getElementById('navBdsmAktiv');
const navImSpiel = document.getElementById('navBdsmImSpiel');
const navCAktiv = document.getElementById('navChastityAktiv');
if (navAktiv) navAktiv.style.display = 'none';
if (navImSpiel) navImSpiel.style.display = 'none';
if (navCAktiv) navCAktiv.style.display = 'none';
// Session-Status prüfen
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
.then(async user => {
if (!user) return;
// BDSM Session-Status
try {
const aktivRes = await fetch('/bdsm/einladung/meine-aktive');
if (aktivRes.ok) {
const aktiv = await aktivRes.json();
if (navNeu) navNeu.style.display = 'none';
if (navImSpiel) navImSpiel.style.display = 'none';
if (navAktiv) {
navAktiv.style.display = '';
navAktiv.querySelector('a').href = aktiv.sessionId ? '/bdsmingame.html' : '/neubdsm.html';
}
} else {
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
const hasSession = sessionRes.status === 200;
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
}
} catch (_) {}
// Chastity Lock-Status
try {
const lockRes = await fetch('/keyholder/mylock');
if (lockRes.ok) {
const lockData = await lockRes.json();
if (navCAktiv) {
navCAktiv.style.display = '';
navCAktiv.querySelector('a').href = '/activelock.html?lockId=' + lockData.lockId;
}
}
} catch (_) {}
})
.catch(() => {});
const sidebar = document.getElementById('sidebar');
const burgerBtn = document.getElementById('burgerBtn');
const overlay = document.getElementById('sidebarOverlay');
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:not(.sidebar-group-toggle)').forEach(l =>
l.addEventListener('click', () => {
if (window.innerWidth <= (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile').trim()) || 768))
closeMenu();
})
);
// Topbar und Social-Sidebar nachladen
function loadScript(src) {
const s = document.createElement('script');
s.src = src;
document.head.appendChild(s);
}
loadScript('/js/topbar.js');
loadScript('/js/social-sidebar.js');
})();

View File

@@ -1,195 +1,94 @@
(function () {
// Verhindert doppelte Ausführung (z.B. wenn sidebar.js nachladen UND direktes <script>-Tag vorhanden)
if (document.querySelector('.social-sidebar')) return;
const path = window.location.pathname;
const links = [
{ href: '/feed.html', icon: '📰', label: 'Feed', badgeId: null, mobileBadgeId: null },
{ href: '/personen-suchen.html', icon: '🔍', label: 'Personen suchen', badgeId: null, mobileBadgeId: null },
{ href: '/freunde.html', icon: '❤️', label: 'Freunde', badgeId: 'socialFriendsBadge', mobileBadgeId: 'socialMobileFriendsBadge' },
{ href: '/nachrichten.html', icon: '📩', label: 'Nachrichten', badgeId: 'socialMsgBadge', mobileBadgeId: 'socialMobileMsgBadge' },
{ href: '/benachrichtigungen.html', icon: '🔔', label: 'Benachrichtigungen', badgeId: 'socialNotifBadge', mobileBadgeId: 'socialMobileNotifBadge' },
{ href: '/gruppen.html', icon: '👥', label: 'Gruppen', badgeId: 'socialGruppenBadge', mobileBadgeId: 'socialMobileGruppenBadge' },
{ href: '/einladungen.html', icon: '✨', label: 'Einladungen', badgeId: 'socialInvBadge', mobileBadgeId: 'socialMobileInvBadge' },
];
const profileActive = (path === '/benutzer.html' || path === '/profile.html') ? ' class="active"' : '';
// ── Rechte Desktop-Sidebar (kein Titel) ──
const desktopItems = links.map(({ href, icon, label, badgeId }) => {
const cls = path === href ? ' class="active"' : '';
const badge = badgeId ? `<span class="social-badge" id="${badgeId}" style="display:none;"></span>` : '';
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
}).join('');
const aside = document.createElement('aside');
aside.className = 'social-sidebar';
aside.innerHTML = `
<div class="social-sidebar-logo-area">
<a href="/feed.html"><img src="/img/logo_community.png" alt="Logo"></a>
</div>
<ul>
<li id="socialProfileItem">
<a href="/benutzer.html"${profileActive}>
<span class="icon" id="socialProfileIcon">◉</span>
<span id="socialProfileName">Profil</span>
</a>
</li>
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${desktopItems}
<li style="margin-top:auto;"><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
<li><a href="/einstellungen.html"${path === '/einstellungen.html' ? ' class="active"' : ''}><span class="icon">⚙️</span> Einstellungen</a></li>
<li><a href="/login/logout"><span class="icon">⏏</span> Abmelden</a></li>
</ul>`;
const appWrapper = document.querySelector('.app-wrapper');
if (appWrapper) appWrapper.appendChild(aside);
// ── Mobile: Links + Profil ins Burger-Menü einhängen ──
const sidebarUl = document.querySelector('.sidebar ul');
if (sidebarUl) {
const mobileLinks = links.map(({ href, icon, label, mobileBadgeId }) => {
const cls = path === href ? ' class="active"' : '';
const badge = mobileBadgeId
? `<span class="social-badge" id="${mobileBadgeId}" style="display:none;"></span>`
: '';
return `<li class="sidebar-mobile-only"><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
}).join('');
const mobileProfileActive = profileActive;
const mobileProfile = `
<li class="sidebar-mobile-only" id="socialMobileProfileItem">
<a href="/benutzer.html"${mobileProfileActive}>
<span class="icon" id="socialMobileProfileIcon">◉</span>
<span id="socialMobileProfileName">Profil</span>
</a>
</li>`;
const sep = '<li class="sidebar-mobile-only"><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>';
const mobileSettings = `<li class="sidebar-mobile-only"><a href="/einstellungen.html"${path === '/einstellungen.html' ? ' class="active"' : ''}><span class="icon">⚙️</span> Einstellungen</a></li>`;
const logoutLi = sidebarUl.querySelector('a[href="/login/logout"]')?.closest('li');
if (logoutLi) {
logoutLi.insertAdjacentHTML('beforebegin', sep + mobileLinks + mobileProfile + mobileSettings);
} else {
sidebarUl.insertAdjacentHTML('beforeend', sep + mobileLinks + mobileProfile + mobileSettings);
}
}
// ── Profil-Daten nachladen (Name + Avatar) ──
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
.then(user => {
if (!user) return;
function updateProfileEntry(nameId, iconId, itemId) {
const nameEl = document.getElementById(nameId);
if (nameEl) nameEl.textContent = user.name;
const iconEl = document.getElementById(iconId);
if (iconEl && user.profilePicture) {
iconEl.innerHTML = `<img src="data:image/png;base64,${user.profilePicture}" class="sidebar-profile-img" alt="">`;
}
const anchor = document.querySelector('#' + itemId + ' a');
if (anchor) anchor.href = '/benutzer.html?userId=' + user.userId;
}
updateProfileEntry('socialProfileName', 'socialProfileIcon', 'socialProfileItem');
updateProfileEntry('socialMobileProfileName', 'socialMobileProfileIcon', 'socialMobileProfileItem');
})
.catch(() => {});
// ── Badge-Zähler ──
function setBadge(ids, count) {
ids.forEach(id => {
if (!id) return;
const el = document.getElementById(id);
if (!el) return;
el.textContent = count;
el.style.display = count > 0 ? '' : 'none';
});
}
// ── Ton abspielen ──
// Browser erlauben audio.play() sobald der Nutzer mindestens einmal interagiert hat.
let userHasInteracted = false;
document.addEventListener('click', () => { userHasInteracted = true; }, { passive: true });
document.addEventListener('keydown', () => { userHasInteracted = true; }, { passive: true });
document.addEventListener('touchstart', () => { userHasInteracted = true; }, { passive: true });
function playSound(src) {
if (!userHasInteracted) return;
try {
const audio = new Audio(src);
audio.volume = 0.6;
audio.play().catch(() => {});
} catch(e) {}
}
// ── Initiale Badge-Counts laden ──
fetch('/social/friends/pending/count')
.then(r => r.ok ? r.json() : 0)
.then(n => setBadge(['socialFriendsBadge', 'socialMobileFriendsBadge'], n))
.catch(() => {});
fetch('/social/messages/unread/count')
.then(r => r.ok ? r.json() : 0)
.then(n => setBadge(['socialMsgBadge', 'socialMobileMsgBadge'], n))
.catch(() => {});
fetch('/notifications/unread/count')
.then(r => r.ok ? r.json() : 0)
.then(n => setBadge(['socialNotifBadge', 'socialMobileNotifBadge'], 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(([joins, reports]) => setBadge(['socialGruppenBadge', 'socialMobileGruppenBadge'], joins + reports))
.catch(() => {});
Promise.all([
fetch('/keyholder/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('/bdsm/einladung/pending').then(r => r.ok ? r.json() : []).catch(() => [])
]).then(([khInvs, lockeeInvs, bdsmInvs]) =>
setBadge(['socialInvBadge', 'socialMobileInvBadge'], khInvs.length + lockeeInvs.length + bdsmInvs.length)
).catch(() => {});
// ── SSE: Echtzeit-Push vom Server ──
function connectSse() {
const es = new EventSource('/events/stream');
es.addEventListener('DM', e => {
try {
const data = JSON.parse(e.data);
setBadge(['socialMsgBadge', 'socialMobileMsgBadge'], data.unreadCount || 0);
// Nur Ton abspielen wenn nicht gerade auf der Nachrichten-Seite
if (window.location.pathname !== '/nachrichten.html') {
playSound('/audio/message.mp3');
}
// Nachrichten-Seite: sofortiges Laden neuer Nachrichten auslösen
if (typeof window.__sseOnDm === 'function') window.__sseOnDm(data);
} catch(ex) {}
});
es.addEventListener('NOTIFICATION', e => {
try {
const data = JSON.parse(e.data);
setBadge(['socialNotifBadge', 'socialMobileNotifBadge'], data.unreadCount || 0);
if (window.location.pathname !== '/benachrichtigungen.html') {
playSound('/audio/notification.mp3');
}
if (typeof window.__sseOnNotification === 'function') window.__sseOnNotification(data);
} catch(ex) {}
});
es.onerror = () => {
es.close();
// Nach 5 Sekunden neu verbinden
setTimeout(connectSse, 5000);
};
}
connectSse();
})();
(function () {
// Badge + SSE service (kein Sidebar-Rendering mehr)
// ── Badge-Zähler ──
function setBadge(ids, count, topbarType) {
ids.forEach(id => {
if (!id) return;
const el = document.getElementById(id);
if (!el) return;
el.textContent = count;
el.style.display = count > 0 ? '' : 'none';
});
if (topbarType && window.__topbarSetBadge) window.__topbarSetBadge(topbarType, count);
}
// ── Ton abspielen ──
let userHasInteracted = false;
document.addEventListener('click', () => { userHasInteracted = true; }, { passive: true });
document.addEventListener('keydown', () => { userHasInteracted = true; }, { passive: true });
document.addEventListener('touchstart', () => { userHasInteracted = true; }, { passive: true });
function playSound(src) {
if (!userHasInteracted) return;
try {
const audio = new Audio(src);
audio.volume = 0.6;
audio.play().catch(() => {});
} catch(e) {}
}
// ── Initiale Badge-Counts laden ──
fetch('/social/friends/pending/count')
.then(r => r.ok ? r.json() : 0)
.then(n => setBadge(['socialFriendsBadge'], n, null))
.catch(() => {});
fetch('/social/messages/unread/count')
.then(r => r.ok ? r.json() : 0)
.then(n => setBadge(['socialMsgBadge'], n, 'msg'))
.catch(() => {});
fetch('/notifications/unread/count')
.then(r => r.ok ? r.json() : 0)
.then(n => setBadge(['socialNotifBadge'], n, 'notif'))
.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(([joins, reports]) => setBadge(['socialGruppenBadge'], joins + reports, null))
.catch(() => {});
Promise.all([
fetch('/keyholder/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('/bdsm/einladung/pending').then(r => r.ok ? r.json() : []).catch(() => [])
]).then(([khInvs, lockeeInvs, bdsmInvs]) =>
setBadge(['socialInvBadge'], khInvs.length + lockeeInvs.length + bdsmInvs.length, 'inv')
).catch(() => {});
// ── SSE: Echtzeit-Push vom Server ──
function connectSse() {
const es = new EventSource('/events/stream');
es.addEventListener('DM', e => {
try {
const data = JSON.parse(e.data);
setBadge(['socialMsgBadge'], data.unreadCount || 0, 'msg');
if (window.location.pathname !== '/nachrichten.html') {
playSound('/audio/message.mp3');
}
if (typeof window.__sseOnDm === 'function') window.__sseOnDm(data);
} catch(ex) {}
});
es.addEventListener('NOTIFICATION', e => {
try {
const data = JSON.parse(e.data);
setBadge(['socialNotifBadge'], data.unreadCount || 0, 'notif');
if (window.location.pathname !== '/benachrichtigungen.html') {
playSound('/audio/notification.mp3');
}
if (typeof window.__sseOnNotification === 'function') window.__sseOnNotification(data);
} catch(ex) {}
});
es.onerror = () => {
es.close();
setTimeout(connectSse, 5000);
};
}
connectSse();
})();

View File

@@ -0,0 +1,420 @@
(function () {
if (document.querySelector('.topbar')) return;
function esc(s) {
return String(s ?? '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── Warten bis app-wrapper existiert (sidebar.js läuft synchron davor) ──
function init() {
const appWrapper = document.querySelector('.app-wrapper');
if (!appWrapper) { setTimeout(init, 30); return; }
injectHTML(appWrapper);
loadProfile();
setupSearch();
setupOverlayButtons();
loadInitialBadges();
}
setTimeout(init, 0);
// ── HTML Struktur ──
function injectHTML(appWrapper) {
const topbar = document.createElement('div');
topbar.className = 'topbar';
topbar.id = 'topbar';
topbar.innerHTML = `
<div class="topbar-left"></div>
<div class="topbar-search-wrap">
<span class="topbar-search-icon">${IC('SEARCH')}</span>
<input type="text" id="topbarSearchInput" placeholder="Suchen…" autocomplete="off" spellcheck="false">
<div class="topbar-search-overlay" id="topbarSearchOverlay"></div>
</div>
<div class="topbar-right">
<button class="topbar-btn" id="topbarMsgBtn" title="Nachrichten">
${IC('MESSAGES')}
<span class="topbar-badge" id="topbarMsgBadge"></span>
</button>
<button class="topbar-btn" id="topbarNotifBtn" title="Benachrichtigungen">
${IC('NOTIFICATIONS')}
<span class="topbar-badge" id="topbarNotifBadge"></span>
</button>
<button class="topbar-btn" id="topbarInvBtn" title="Einladungen">
${IC('INVITATIONS')}
<span class="topbar-badge" id="topbarInvBadge"></span>
</button>
<button class="topbar-btn topbar-profile-btn" id="topbarProfileBtn">
<span class="topbar-avatar-placeholder" id="topbarAvatarWrap">${IC('PROFILE')}</span>
<span class="topbar-username" id="topbarUsername">…</span>
</button>
</div>`;
appWrapper.insertAdjacentElement('beforebegin', topbar);
// Panel-Overlays am Ende von body einfügen
document.body.insertAdjacentHTML('beforeend', `
<div class="topbar-panel" id="topbarMsgPanel">
<div class="topbar-panel-header">
<span>${IC('MESSAGES')} Nachrichten</span>
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
</div>
<div class="topbar-panel-body" id="topbarMsgBody"></div>
<div class="topbar-panel-footer"><a href="/nachrichten.html">Alle Nachrichten →</a></div>
</div>
<div class="topbar-panel" id="topbarNotifPanel">
<div class="topbar-panel-header">
<span>${IC('NOTIFICATIONS')} Benachrichtigungen</span>
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
</div>
<div class="topbar-panel-body" id="topbarNotifBody"></div>
<div class="topbar-panel-footer"><a href="/benachrichtigungen.html">Alle anzeigen →</a></div>
</div>
<div class="topbar-panel" id="topbarInvPanel">
<div class="topbar-panel-header">
<span>${IC('INVITATIONS')} Einladungen</span>
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
</div>
<div class="topbar-panel-body" id="topbarInvBody"></div>
<div class="topbar-panel-footer"><a href="/einladungen.html">Alle anzeigen →</a></div>
</div>
<div class="topbar-panel" id="topbarProfilePanel">
<div class="topbar-panel-header">
<span>Konto</span>
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
</div>
<div class="topbar-panel-body topbar-profile-body">
<div class="topbar-profile-card">
<span id="topbarPanelAvatarWrap" style="font-size:2.5rem;line-height:1;">${IC('PROFILE')}</span>
<div>
<div id="topbarPanelName" style="font-weight:700;font-size:1rem;"></div>
</div>
</div>
<hr style="border:none;border-top:1px solid var(--color-secondary);margin:0;">
<nav class="topbar-profile-nav">
<a id="topbarProfileLink" href="/benutzer.html" class="topbar-profile-link">
<span>${IC('PROFILE')}</span> Mein Profil
</a>
<a href="/einstellungen.html" class="topbar-profile-link">
<span>${IC('SETTINGS')}</span> Einstellungen
</a>
<a href="/login/logout" class="topbar-profile-link topbar-profile-link--danger">
<span>${IC('LOGOUT')}</span> Abmelden
</a>
</nav>
</div>
</div>
`);
}
function IC(key) { return window.IC ? window.IC(key) : (window.ICONS?.[key]?.value || ''); }
// ── Profil laden ──
function loadProfile() {
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
.then(user => {
if (!user) return;
const nameEl = document.getElementById('topbarUsername');
if (nameEl) nameEl.textContent = user.name;
const avatarWrap = document.getElementById('topbarAvatarWrap');
if (avatarWrap && user.profilePicture) {
avatarWrap.innerHTML = `<img src="data:image/png;base64,${user.profilePicture}" class="topbar-avatar" alt="">`;
}
const panelName = document.getElementById('topbarPanelName');
if (panelName) panelName.textContent = user.name;
const panelAvatar = document.getElementById('topbarPanelAvatarWrap');
if (panelAvatar && user.profilePicture) {
panelAvatar.innerHTML = `<img src="data:image/png;base64,${user.profilePicture}" style="width:3rem;height:3rem;border-radius:50%;object-fit:cover;" alt="">`;
}
const profileLink = document.getElementById('topbarProfileLink');
if (profileLink && user.userId) profileLink.href = '/benutzer.html?userId=' + user.userId;
})
.catch(() => {});
}
// ── Suche ──
function setupSearch() {
const input = document.getElementById('topbarSearchInput');
const overlay = document.getElementById('topbarSearchOverlay');
if (!input || !overlay) return;
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
const q = input.value.trim();
if (q.length < 2) { overlay.innerHTML = ''; overlay.classList.remove('open'); return; }
overlay.innerHTML = '<div class="topbar-search-hint">Suche…</div>';
overlay.classList.add('open');
timer = setTimeout(() => doSearch(q, overlay), 300);
});
document.addEventListener('click', e => {
if (!e.target.closest('.topbar-search-wrap')) {
overlay.classList.remove('open');
}
});
}
async function doSearch(q, overlay) {
try {
const res = await fetch('/social/users/search?q=' + encodeURIComponent(q));
if (!res.ok) { overlay.innerHTML = '<div class="topbar-search-hint">Fehler bei der Suche.</div>'; return; }
const users = await res.json();
if (!users || users.length === 0) {
overlay.innerHTML = '<div class="topbar-search-hint">Keine Ergebnisse.</div>';
return;
}
overlay.innerHTML = users.map(u => {
const av = u.profilePicture
? `<img src="data:image/png;base64,${esc(u.profilePicture)}" class="topbar-search-avatar" alt="">`
: `<span class="topbar-search-avatar topbar-search-avatar--placeholder">${IC('PROFILE')}</span>`;
return `<a href="/benutzer.html?userId=${esc(u.userId)}" class="topbar-search-result">
${av}
<span style="font-size:0.92rem;font-weight:600;">${esc(u.name)}</span>
</a>`;
}).join('');
} catch (e) {
overlay.innerHTML = '<div class="topbar-search-hint">Fehler bei der Suche.</div>';
}
}
// ── Panel-Overlays ──
let _activePanel = null;
function positionPanel(panel, btn) {
const topbar = document.getElementById('topbar');
const tRect = topbar ? topbar.getBoundingClientRect() : btn.getBoundingClientRect();
panel.style.top = tRect.bottom + 'px';
panel.style.right = Math.max(4, window.innerWidth - tRect.right) + 'px';
panel.style.left = 'auto';
}
function openPanel(panelId, btnId, loadFn) {
const panel = document.getElementById(panelId);
const btn = document.getElementById(btnId);
if (!panel || !btn) return;
if (_activePanel === panel && panel.classList.contains('open')) {
closeAllPanels(); return;
}
closeAllPanels();
positionPanel(panel, btn);
panel.classList.add('open');
_activePanel = panel;
if (loadFn) loadFn();
}
function closeAllPanels() {
document.querySelectorAll('.topbar-panel.open').forEach(p => p.classList.remove('open'));
_activePanel = null;
}
window.__topbarCloseAll = closeAllPanels;
document.addEventListener('click', e => {
if (!e.target.closest('.topbar-panel') && !e.target.closest('.topbar-btn'))
closeAllPanels();
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAllPanels(); });
function setupOverlayButtons() {
const msgBtn = document.getElementById('topbarMsgBtn');
const notifBtn = document.getElementById('topbarNotifBtn');
const invBtn = document.getElementById('topbarInvBtn');
const profileBtn = document.getElementById('topbarProfileBtn');
if (msgBtn) msgBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarMsgPanel', 'topbarMsgBtn', loadMessages); });
if (notifBtn) notifBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarNotifPanel', 'topbarNotifBtn', loadNotifications); });
if (invBtn) invBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarInvPanel', 'topbarInvBtn', loadInvitations); });
if (profileBtn) profileBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarProfilePanel', 'topbarProfileBtn', null); });
}
// ── Nachrichten ──
async function loadMessages() {
const body = document.getElementById('topbarMsgBody');
if (!body) return;
body.innerHTML = '<div class="topbar-panel-hint">Wird geladen…</div>';
try {
const res = await fetch('/social/messages');
if (!res.ok) { body.innerHTML = '<div class="topbar-panel-hint">Keine Nachrichten.</div>'; return; }
const convos = await res.json();
if (!convos.length) { body.innerHTML = '<div class="topbar-panel-hint">Noch keine Nachrichten.</div>'; return; }
body.innerHTML = convos.slice(0, 7).map(c => {
const av = c.partner?.profilePicture
? `<img src="data:image/png;base64,${esc(c.partner.profilePicture)}" class="topbar-item-avatar" alt="">`
: `<span class="topbar-item-avatar topbar-item-avatar--placeholder">${IC('PROFILE')}</span>`;
const bold = c.unreadCount > 0 ? 'font-weight:700;' : '';
const badge = c.unreadCount > 0
? `<span class="topbar-item-badge">${c.unreadCount > 99 ? '99+' : c.unreadCount}</span>` : '';
return `<a href="/nachrichten.html?userId=${esc(c.partner?.userId)}" class="topbar-panel-item">
${av}
<div class="topbar-panel-item-body">
<div style="${bold}font-size:0.88rem;">${esc(c.partner?.name || '')}</div>
<div class="topbar-panel-item-sub">${esc(c.lastMessage?.text || '')}</div>
</div>
${badge}
</a>`;
}).join('');
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
}
// ── Benachrichtigungen ──
async function loadNotifications() {
const body = document.getElementById('topbarNotifBody');
if (!body) return;
body.innerHTML = '<div class="topbar-panel-hint">Wird geladen…</div>';
try {
const res = await fetch('/notifications');
if (!res.ok) { body.innerHTML = '<div class="topbar-panel-hint">Keine Benachrichtigungen.</div>'; return; }
const notifs = await res.json();
if (!notifs.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine neuen Benachrichtigungen.</div>'; return; }
body.innerHTML = `<div style="padding:0.3rem 1rem;text-align:right;">
<button onclick="window.__topbarMarkAllRead()" class="topbar-mark-all-btn">Alle gelesen</button>
</div>`;
notifs.forEach(n => {
const el = document.createElement('div');
const tag = n.targetUrl ? 'a' : 'div';
const href = n.targetUrl ? `href="${esc(n.targetUrl)}"` : '';
const unread = !n.read;
el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item${unread ? ' topbar-notif-item--unread' : ''}"
onclick="window.__topbarMarkNotifRead('${esc(n.id)}')">
${unread ? '<span class="topbar-notif-dot"></span>' : '<span style="width:7px;flex-shrink:0;"></span>'}
<div class="topbar-panel-item-body">
<div style="font-size:0.85rem;line-height:1.4;${unread ? 'font-weight:600;' : ''}">${esc(n.text)}</div>
<div class="topbar-panel-item-sub">${n.sentAt ? new Date(n.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'}) : ''}</div>
</div>
</${tag}>`;
body.appendChild(el.firstElementChild);
});
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
}
window.__topbarMarkNotifRead = async function (id) {
try {
await fetch('/notifications/' + id + '/read', { method: 'POST' });
const el = document.querySelector(`.topbar-notif-item--unread[onclick*="${id}"]`);
if (el) el.classList.remove('topbar-notif-item--unread');
const r = await fetch('/notifications/unread/count');
if (r.ok) setTopbarBadge('notif', await r.json());
} catch (e) {}
};
window.__topbarMarkAllRead = async function () {
try {
await fetch('/notifications/read-all', { method: 'POST' });
setTopbarBadge('notif', 0);
loadNotifications();
} catch (e) {}
};
// ── Einladungen ──
async function loadInvitations() {
const body = document.getElementById('topbarInvBody');
if (!body) return;
body.innerHTML = '<div class="topbar-panel-hint">Wird geladen…</div>';
try {
const [lr, kr, br] = await Promise.all([
fetch('/lockee/invitations/mine'),
fetch('/keyholder/invitations/mine'),
fetch('/bdsm/einladung/pending')
]);
const lockee = lr.ok ? await lr.json() : [];
const kh = kr.ok ? await kr.json() : [];
const bdsm = br.ok ? await br.json() : [];
const all = [
...lockee.map(i => ({ ...i, _type: 'lockee' })),
...kh.map(i => ({ ...i, _type: 'keyholder' })),
...bdsm.map(i => ({ ...i, _type: 'bdsm' }))
];
if (!all.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine offenen Einladungen.</div>'; return; }
body.innerHTML = '';
all.forEach(inv => body.appendChild(buildInvCard(inv)));
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
}
function buildInvCard(inv) {
let typeIcon, typeName, line, declineUrl, declineMethod = 'DELETE', declineBody = null, acceptHtml;
if (inv._type === 'lockee') {
typeIcon = IC('LOCK'); typeName = 'Lockee-Einladung'; line = inv.lockName || 'Lock';
declineUrl = '/lockee/invitation/' + encodeURIComponent(inv.token);
acceptHtml = `<a href="/einladungen.html" class="topbar-inv-btn topbar-inv-btn--accept">Details</a>`;
} else if (inv._type === 'keyholder') {
typeIcon = IC('KEY'); typeName = 'Keyholder-Einladung'; line = inv.lockName || 'Lock';
declineUrl = '/keyholder/invitations/mine/' + encodeURIComponent(inv.token);
acceptHtml = `<a href="/keyholder/invitation/${esc(inv.token)}" class="topbar-inv-btn topbar-inv-btn--accept">Annehmen</a>`;
} else {
typeIcon = IC('BDSM'); typeName = 'BDSM Game'; line = inv.senderName || 'Einladung';
const id = inv.id || inv.einladungId || '';
declineUrl = '/bdsm/einladung/' + encodeURIComponent(id) + '/antwort';
declineMethod = 'PUT';
declineBody = JSON.stringify({ annahme: false });
acceptHtml = `<a href="/bdsm-einladung.html?id=${esc(id)}" class="topbar-inv-btn topbar-inv-btn--accept">Details</a>`;
}
const senderPic = inv.senderAvatar || inv.lockOwnerAvatar;
const av = senderPic
? `<img src="data:image/png;base64,${esc(senderPic)}" class="topbar-item-avatar" alt="">`
: `<span class="topbar-item-avatar topbar-item-avatar--placeholder">${IC('PROFILE')}</span>`;
const div = document.createElement('div');
div.className = 'topbar-panel-item topbar-inv-card';
div.innerHTML = `${av}
<div class="topbar-panel-item-body">
<div class="topbar-panel-item-sub">${typeIcon} ${typeName}</div>
<div style="font-weight:600;font-size:0.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(line)}</div>
</div>
<div style="display:flex;gap:0.3rem;flex-shrink:0;">
<button class="topbar-inv-btn topbar-inv-btn--decline"
data-url="${esc(declineUrl)}"
data-method="${declineMethod}"
data-body="${esc(declineBody || '')}"
onclick="window.__topbarDecline(this)">✕</button>
${acceptHtml}
</div>`;
return div;
}
window.__topbarDecline = async function (btn) {
btn.disabled = true;
const url = btn.dataset.url;
const method = btn.dataset.method;
const body = btn.dataset.body || null;
try {
const opts = { method };
if (body) { opts.headers = { 'Content-Type': 'application/json' }; opts.body = body; }
const res = await fetch(url, opts);
if (res.ok || res.status === 204) {
const card = btn.closest('.topbar-inv-card');
if (card) card.remove();
const remaining = document.getElementById('topbarInvBody')?.querySelectorAll('.topbar-inv-card').length || 0;
setTopbarBadge('inv', remaining);
} else { btn.disabled = false; }
} catch (e) { btn.disabled = false; }
};
// ── Badge-Verwaltung ──
function setTopbarBadge(type, count) {
const map = { msg: 'topbarMsgBadge', notif: 'topbarNotifBadge', inv: 'topbarInvBadge' };
const el = document.getElementById(map[type]);
if (!el) return;
el.textContent = count > 99 ? '99+' : count;
el.style.display = count > 0 ? 'inline-block' : 'none';
}
// Für social-sidebar.js zugänglich
window.__topbarSetBadge = setTopbarBadge;
function loadInitialBadges() {
fetch('/social/messages/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('msg', n)).catch(() => {});
fetch('/notifications/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('notif', n)).catch(() => {});
Promise.all([
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('/keyholder/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('/bdsm/einladung/pending').then(r => r.ok ? r.json() : []).catch(() => [])
]).then(([l, k, b]) => setTopbarBadge('inv', l.length + k.length + b.length)).catch(() => {});
}
})();