Bug Fixes und Icons Refactioring

This commit is contained in:
2026-03-24 08:02:57 +01:00
parent b63af50786
commit b85245717c
17 changed files with 2277 additions and 1936 deletions

View File

@@ -1,5 +1,5 @@
#Mon Mar 23 21:09:40 CET 2026
#Tue Mar 24 06:41:43 CET 2026
display=\:0
host=mario-mint
process-id=80279
process-id=4231
user=mario

View File

@@ -226,3 +226,88 @@ Binding(CTRL+SHIFT+T,
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-23 23:23:49.341
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
!ENTRY org.springframework.tooling.ls.eclipse.commons 1 0 2026-03-23 23:23:50.541
!MESSAGE executing callback sts4.classpath.bMrwvfMB FAILED
!ENTRY org.springframework.tooling.ls.eclipse.commons 4 0 2026-03-23 23:23:50.542
!MESSAGE java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.CompletableFuture$UniCompose@203dc124 rejected from java.util.concurrent.ThreadPoolExecutor@6d6cd631[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 194]
!STACK 0
java.util.concurrent.ExecutionException: java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.CompletableFuture$UniCompose@203dc124 rejected from java.util.concurrent.ThreadPoolExecutor@6d6cd631[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 194]
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396)
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
at org.springframework.tooling.ls.eclipse.commons.LSP4ECommandExecutor.executeClientCommand(LSP4ECommandExecutor.java:31)
at org.springframework.tooling.jdt.ls.commons.classpath.SendClasspathNotificationsJob.flush(SendClasspathNotificationsJob.java:192)
at org.springframework.tooling.jdt.ls.commons.classpath.SendClasspathNotificationsJob.run(SendClasspathNotificationsJob.java:151)
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
Caused by: java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.CompletableFuture$UniCompose@203dc124 rejected from java.util.concurrent.ThreadPoolExecutor@6d6cd631[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 194]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2081)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:841)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1376)
at java.base/java.util.concurrent.Executors$DelegatedExecutorService.execute(Executors.java:754)
at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1184)
at java.base/java.util.concurrent.CompletableFuture.thenComposeAsync(CompletableFuture.java:2352)
at org.eclipse.lsp4e.LanguageServerWrapper.executeImpl(LanguageServerWrapper.java:1054)
at org.eclipse.lsp4e.LanguageServers.lambda$23(LanguageServers.java:445)
at java.base/java.util.concurrent.CompletableFuture.uniApplyNow(CompletableFuture.java:684)
at java.base/java.util.concurrent.CompletableFuture.uniApplyStage(CompletableFuture.java:662)
at java.base/java.util.concurrent.CompletableFuture.thenApply(CompletableFuture.java:2200)
at org.eclipse.lsp4e.LanguageServers.lambda$22(LanguageServers.java:443)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)
at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
at org.eclipse.lsp4e.LanguageServers.computeFirst(LanguageServers.java:181)
at org.eclipse.lsp4e.LanguageServers.computeFirst(LanguageServers.java:145)
at org.springframework.tooling.ls.eclipse.commons.LSP4ECommandExecutor.executeClientCommand(LSP4ECommandExecutor.java:29)
... 3 more
!SESSION 2026-03-24 06:41:38.442 -----------------------------------------------
eclipse.buildId=4.39.0.20260305-0817
java.version=21.0.6
java.vendor=Eclipse Adoptium
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
Framework arguments: -product org.eclipse.epp.package.java.product
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
!ENTRY ch.qos.logback.classic 1 0 2026-03-24 06:41:39.956
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY ch.qos.logback.classic 1 0 2026-03-24 06:41:44.218
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
!ENTRY org.eclipse.ui 2 0 2026-03-24 06:41:44.360
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-24 06:41:44.360
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui 2 0 2026-03-24 06:41:44.512
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-24 06:41:44.512
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.jface 2 0 2026-03-24 06:46:26.895
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-24 06:46:26.895
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
Binding(CTRL+SHIFT+T,
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
Open a type in a Java editor,
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)
Binding(CTRL+SHIFT+T,
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
,
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<typeInfoHistroy>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1772437686926"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{FillerController.java[FillerController" modifiers="1" timestamp="1772385528555"/>
</typeInfoHistroy>

View File

@@ -16,7 +16,7 @@
<section name="DialogBoundsSettings">
<item key="DIALOG_HEIGHT" value="500"/>
<item key="DIALOG_WIDTH" value="600"/>
<item key="DIALOG_X_ORIGIN" value="980"/>
<item key="DIALOG_X_ORIGIN" value="680"/>
<item key="DIALOG_Y_ORIGIN" value="351"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu|10.0|0|GTK|1|"/>
</section>

View File

@@ -5,3 +5,4 @@
2026-03-23 17:36:54,482 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-23 17:38:51,039 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-23 21:09:44,347 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-24 06:41:47,661 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.

View File

@@ -1,3 +1,3 @@
#Mon Mar 23 21:09:40 CET 2026
#Tue Mar 24 06:41:43 CET 2026
org.eclipse.core.runtime=2
org.eclipse.platform=4.39.0.v20260226-0420

View File

@@ -4,7 +4,12 @@ import de.oaa.xxx.aufgaben.AufgabenGruppe;
import de.oaa.xxx.aufgaben.Toy;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.FinisherRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.meldung.MeldungEntity;
import de.oaa.xxx.meldung.MeldungRepository;
@@ -31,16 +36,31 @@ public class AdminController {
private final UserRepository userRepository;
private final MeldungRepository meldungRepository;
private final AufgabenGruppeRepository aufgabenGruppeRepository;
private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository;
private final SperreRepository sperreRepository;
private final FinisherRepository finisherRepository;
private final GruppenAboRepository gruppenAboRepository;
private final ToyRepository toyRepository;
public AdminController(AdminRepository adminRepository, UserRepository userRepository,
MeldungRepository meldungRepository,
AufgabenGruppeRepository aufgabenGruppeRepository,
AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository,
SperreRepository sperreRepository,
FinisherRepository finisherRepository,
GruppenAboRepository gruppenAboRepository,
ToyRepository toyRepository) {
this.adminRepository = adminRepository;
this.userRepository = userRepository;
this.meldungRepository = meldungRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository;
this.sperreRepository = sperreRepository;
this.finisherRepository = finisherRepository;
this.gruppenAboRepository = gruppenAboRepository;
this.toyRepository = toyRepository;
}
@@ -57,6 +77,8 @@ public class AdminController {
record StatusRequest(MeldungStatus status) {}
record UserSearchDto(UUID userId, String name) {}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private AdminEntity requireAdmin(Principal principal) {
@@ -115,7 +137,7 @@ public class AdminController {
public ResponseEntity<Void> updateMeldung(@PathVariable("id") UUID id,
@RequestBody StatusRequest body,
Principal principal) {
var admin = requireAdmin(principal);
requireAdmin(principal);
var user = userRepository.findByEmail(principal.getName()).orElseThrow();
MeldungEntity meldung = meldungRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
@@ -173,6 +195,11 @@ public class AdminController {
if (entity.getUserId() != null) {
return ResponseEntity.status(403).build(); // Nur System-Gruppen
}
gruppenAboRepository.deleteByAufgabenGruppe(entity);
aufgabeRepository.deleteAll(entity.getAufgaben());
strafeRepository.deleteAll(entity.getStrafen());
sperreRepository.deleteAll(entity.getSperren());
finisherRepository.deleteAll(entity.getFinisher());
aufgabenGruppeRepository.delete(entity);
return ResponseEntity.noContent().build();
}
@@ -232,6 +259,21 @@ public class AdminController {
return ResponseEntity.noContent().build();
}
// ── Benutzer-Suche (nur SUPERADMIN) ──────────────────────────────────────
@GetMapping("/users/search")
public ResponseEntity<List<UserSearchDto>> searchUsers(
@RequestParam String q, Principal principal) {
requireSuperAdmin(principal);
if (q == null || q.isBlank()) return ResponseEntity.ok(List.of());
List<UserEntity> users = userRepository.findByNameContainingIgnoreCase(q.trim());
return ResponseEntity.ok(users.stream()
.filter(u -> !adminRepository.existsByUserId(u.getUserId()))
.limit(20)
.map(u -> new UserSearchDto(u.getUserId(), u.getName()))
.toList());
}
// ── Admin-Verwaltung (nur SUPERADMIN) ────────────────────────────────────
@GetMapping("/admins")

View File

@@ -24,7 +24,7 @@ public class SseService {
}
public SseEmitter subscribe(UUID userId) {
SseEmitter emitter = new SseEmitter(300_000L); // 5 min Client reconnects automatically
SseEmitter emitter = new SseEmitter(1_800_000L); // 30 min Client reconnects automatically
emitters.computeIfAbsent(userId, k -> new CopyOnWriteArrayList<>()).add(emitter);
Runnable cleanup = () -> removeEmitter(userId, emitter);
emitter.onCompletion(cleanup);

View File

@@ -418,6 +418,18 @@
</div>
</div>
<!-- Bestätigungs-Modal -->
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:380px;">
<h2>Wirklich löschen?</h2>
<p id="confirmModalText" style="font-size:0.9rem;color:var(--color-muted);margin:0 0 0.5rem 0;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Abbrechen</button>
<button class="btn-save" id="confirmModalOk">Löschen</button>
</div>
</div>
</div>
<div class="main">
<div class="content">
@@ -502,21 +514,23 @@
<div class="form-section">
<h3>Admin hinzufügen</h3>
<div class="form-row">
<input type="text" id="adminUserId" placeholder="User-ID (UUID)">
<input type="text" id="adminSearch" placeholder="Benutzername suchen…" oninput="searchAdminUsers()">
<select id="adminRolle">
<option value="ADMIN">Admin</option>
<option value="SUPERADMIN">Superadmin</option>
</select>
<button class="btn-primary" onclick="createAdmin()">Hinzufügen</button>
</div>
<div id="adminSearchResults" style="margin-top:0.5rem;display:none;
background:var(--color-secondary);border-radius:8px;overflow:hidden;"></div>
<div id="adminAddError" style="color:var(--color-primary);font-size:0.82rem;margin-top:0.4rem;min-height:1em;"></div>
</div>
<div class="table-card">
<table class="data-table">
<thead>
<tr><th>Benutzername</th><th>User-ID</th><th>Rolle</th><th>Seit</th><th></th></tr>
<tr><th>Benutzername</th><th>Rolle</th><th>Seit</th><th></th></tr>
</thead>
<tbody id="adminsBody">
<tr><td colspan="5" class="empty-hint">Wird geladen…</td></tr>
<tr><td colspan="4" class="empty-hint">Wird geladen…</td></tr>
</tbody>
</table>
</div>
@@ -546,6 +560,12 @@ async function init() {
loadAdminGruppen();
loadAdminToys();
if (admin.rolle === 'SUPERADMIN') loadAdmins();
const _savedAdminTab = localStorage.getItem('tab_admin');
if (_savedAdminTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedAdminTab}"]`);
if (_btn && _btn.offsetParent !== null) _btn.click();
}
}
// ── Tab-Navigation ────────────────────────────────────────────────────────
@@ -556,6 +576,7 @@ document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
localStorage.setItem('tab_admin', btn.dataset.tab);
});
});
@@ -595,6 +616,26 @@ async function setMeldungStatus(id, status) {
if (r.ok) loadMeldungen();
}
// ── Bestätigungs-Modal ────────────────────────────────────────────────────
let _confirmCallback = null;
function openConfirmModal(text, callback) {
document.getElementById('confirmModalText').textContent = text;
_confirmCallback = callback;
document.getElementById('confirmModal').classList.add('open');
}
function closeConfirmModal() {
_confirmCallback = null;
document.getElementById('confirmModal').classList.remove('open');
}
document.getElementById('confirmModalCancel').addEventListener('click', closeConfirmModal);
document.getElementById('confirmModalOk').addEventListener('click', () => {
const cb = _confirmCallback; closeConfirmModal(); if (cb) cb();
});
document.getElementById('confirmModal').addEventListener('click', e => {
if (e.target === document.getElementById('confirmModal')) closeConfirmModal();
});
// ── Aufgabengruppen ───────────────────────────────────────────────────────
let pendingExpandId = null;
@@ -761,14 +802,15 @@ const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe
function deleteItem(kind, itemId, gruppenId, event) {
event.stopPropagation();
if (!confirm('Eintrag wirklich löschen?')) return;
fetch(ITEM_DELETE_URL[kind], {
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [ITEM_DELETE_FIELD[kind]]: itemId })
}).then(r => {
if (r.ok || r.status === 202) { openItemId = null; pendingExpandId = gruppenId; loadAdminGruppen(); }
else document.getElementById('gruppeActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').';
}).catch(() => { document.getElementById('gruppeActionError').textContent = 'Verbindungsfehler.'; });
openConfirmModal('Eintrag wirklich löschen?', () => {
fetch(ITEM_DELETE_URL[kind], {
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [ITEM_DELETE_FIELD[kind]]: itemId })
}).then(r => {
if (r.ok || r.status === 202) { openItemId = null; pendingExpandId = gruppenId; loadAdminGruppen(); }
else document.getElementById('gruppeActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').';
}).catch(() => { document.getElementById('gruppeActionError').textContent = 'Verbindungsfehler.'; });
});
}
async function duplicateItem(kind, itemId, gruppenId, event) {
@@ -885,14 +927,15 @@ gruppeModal.addEventListener('click', e => { if (e.target === gruppeModal) close
document.getElementById('gruppeDeleteBtn').addEventListener('click', () => {
if (!selectedGruppeId) return;
if (!confirm('System-Aufgabengruppe und alle Inhalte wirklich löschen?')) return;
const btn = document.getElementById('gruppeDeleteBtn'); btn.disabled = true;
fetch(`/admin/aufgabengruppen/${selectedGruppeId}`, { method: 'DELETE' })
.then(r => {
if (r.ok || r.status === 204) { loadAdminGruppen(); }
else { document.getElementById('gruppeActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').'; btn.disabled = false; }
})
.catch(() => { document.getElementById('gruppeActionError').textContent = 'Verbindungsfehler.'; btn.disabled = false; });
openConfirmModal('System-Aufgabengruppe und alle Inhalte wirklich löschen?', () => {
const btn = document.getElementById('gruppeDeleteBtn'); btn.disabled = true;
fetch(`/admin/aufgabengruppen/${selectedGruppeId}`, { method: 'DELETE' })
.then(r => {
if (r.ok || r.status === 204) { loadAdminGruppen(); }
else { document.getElementById('gruppeActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').'; btn.disabled = false; }
})
.catch(() => { document.getElementById('gruppeActionError').textContent = 'Verbindungsfehler.'; btn.disabled = false; });
});
});
document.getElementById('gruppeDuplicateBtn').addEventListener('click', async () => {
@@ -1300,36 +1343,73 @@ async function loadAdmins() {
const r = await fetch('/admin/admins'); if (!r.ok) return;
const list = await r.json();
const tbody = document.getElementById('adminsBody');
if (!list.length) { tbody.innerHTML = '<tr><td colspan="5" class="empty-hint">Keine Admins vorhanden.</td></tr>'; return; }
if (!list.length) { tbody.innerHTML = '<tr><td colspan="4" class="empty-hint">Keine Admins vorhanden.</td></tr>'; return; }
tbody.innerHTML = list.map(a => `
<tr>
<td>${esc(a.userName)}</td>
<td class="word-break">${a.userId}</td>
<td><span class="badge-status badge-${a.rolle.toLowerCase()}">${a.rolle}</span></td>
<td style="white-space:nowrap">${fmtDate(a.createdAt)}</td>
<td><button class="tbl-btn" onclick="deleteAdmin('${a.adminId}')">Entfernen</button></td>
<td><button class="tbl-btn" onclick="deleteAdmin('${a.adminId}', '${esc(a.userName)}')">Entfernen</button></td>
</tr>`).join('');
}
async function createAdmin() {
const userId = document.getElementById('adminUserId').value.trim();
const rolle = document.getElementById('adminRolle').value;
if (!userId) { alert('User-ID eingeben.'); return; }
const r = await fetch('/admin/admins', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, rolle })
});
if (r.status === 201) { document.getElementById('adminUserId').value = ''; loadAdmins(); }
else if (r.status === 404) alert('User nicht gefunden.');
else if (r.status === 409) alert('Benutzer ist bereits Admin.');
else alert('Fehler: ' + r.status);
let _adminSearchTimer = null;
function searchAdminUsers() {
clearTimeout(_adminSearchTimer);
const q = document.getElementById('adminSearch').value.trim();
const box = document.getElementById('adminSearchResults');
if (!q) { box.style.display = 'none'; box.innerHTML = ''; return; }
_adminSearchTimer = setTimeout(async () => {
const r = await fetch(`/admin/users/search?q=${encodeURIComponent(q)}`);
if (!r.ok) return;
const list = await r.json();
if (!list.length) {
box.innerHTML = '<div style="padding:0.6rem 0.9rem;font-size:0.88rem;color:var(--color-muted);">Keine Benutzer gefunden.</div>';
} else {
box.innerHTML = list.map(u => `
<div style="display:flex;align-items:center;justify-content:space-between;
padding:0.5rem 0.9rem;border-bottom:1px solid rgba(255,255,255,0.05);cursor:pointer;"
onmouseenter="this.style.background='rgba(255,255,255,0.04)'"
onmouseleave="this.style.background=''">
<span style="font-size:0.9rem;">${esc(u.name)}</span>
<button class="tbl-btn tbl-btn-ok" onclick="addAdminFromSearch('${u.userId}', '${esc(u.name)}')">+ Hinzufügen</button>
</div>`).join('');
}
box.style.display = 'block';
}, 300);
}
async function deleteAdmin(id) {
if (!confirm('Admin-Berechtigung wirklich entziehen?')) return;
const r = await fetch(`/admin/admins/${id}`, { method: 'DELETE' });
if (r.ok || r.status === 204) loadAdmins();
else if (r.status === 400) alert('Du kannst dich nicht selbst entfernen.');
else alert('Fehler: ' + r.status);
async function addAdminFromSearch(userId, userName) {
const rolle = document.getElementById('adminRolle').value;
const errEl = document.getElementById('adminAddError');
errEl.textContent = '';
const r = await fetch('/admin/admins', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, rolle })
});
if (r.status === 201) {
document.getElementById('adminSearch').value = '';
document.getElementById('adminSearchResults').style.display = 'none';
loadAdmins();
} else if (r.status === 409) {
errEl.textContent = `${userName} ist bereits Admin.`;
} else {
errEl.textContent = `Fehler beim Hinzufügen (HTTP ${r.status}).`;
}
}
async function deleteAdmin(id, userName) {
openConfirmModal(`Admin-Berechtigung von „${userName}" wirklich entziehen?`, async () => {
const r = await fetch(`/admin/admins/${id}`, { method: 'DELETE' });
if (r.ok || r.status === 204) {
loadAdmins();
} else if (r.status === 400) {
document.getElementById('adminAddError').textContent = 'Du kannst dich nicht selbst entfernen.';
document.querySelector('.tab-btn[data-tab="admins"]')?.click();
} else {
document.getElementById('adminAddError').textContent = `Fehler (HTTP ${r.status}).`;
}
});
}
// ── Hilfsfunktionen ───────────────────────────────────────────────────────

View File

@@ -574,7 +574,8 @@
loadProfilPosts();
profilPostsObserver.observe(document.getElementById('profilPostsSentinel'));
}
} catch {
} catch(e) {
console.error('[benutzer] loadProfile Fehler:', e);
document.getElementById('loadingHint').textContent = 'Fehler beim Laden.';
document.getElementById('loadingHint').style.display = '';
}
@@ -613,7 +614,7 @@
// Action buttons
const actions = document.getElementById('profileActions');
const isViewingOwnProfile = me && me.userId === profile.userId;
const isViewingOwnProfile = myUserId && myUserId === profile.userId;
if (isOwnProfile) {
actions.innerHTML = `<a href="/profile.html" class="btn">Profil bearbeiten</a>`;
} else if (isViewingOwnProfile) {
@@ -884,12 +885,16 @@
// ── Spielhistorie ──
const GAME_TYPE_ICON = {
CARDLOCK: '<span style="position:relative;display:inline-block;line-height:1;"><img src="/img/card.png" style="width:2.7rem;height:2.7rem;object-fit:contain;display:block;"><span style="position:absolute;bottom:-2px;right:-4px;font-size:1.3rem;line-height:1;">🔒</span></span>',
TIMELOCK: '<span style="position:relative;display:inline-block;line-height:1;"><span style="font-size:2.7rem;">⏰</span><span style="position:absolute;bottom:-2px;right:-4px;font-size:1.3rem;line-height:1;">🔒</span></span>',
BDSM: '⛓️',
VANILLA: '❤️'
CARDLOCK: IChtml('GAME_CARDLOCK'),
TIMELOCK: IChtml('GAME_TIMELOCK'),
BDSM: IC('GAME_BDSM'),
VANILLA: IC('GAME_VANILLA')
};
const ROLE_BADGE = {
KEYHOLDER: IC('ROLE_KEYHOLDER'),
LOCKEE: IC('ROLE_LOCKEE'),
PLAYER: ''
};
const ROLE_BADGE = { KEYHOLDER: '🔑', LOCKEE: '🔒', PLAYER: '' };
let gameHistoryLoaded = false;
async function loadGameHistory() {
@@ -906,10 +911,10 @@
if (entries.length === 0) { empty.style.display = ''; return; }
list.innerHTML = entries.map(e => {
const gameIconRaw = GAME_TYPE_ICON[e.gameType] || '🎮';
const gameIcon = (e.gameType === 'CARDLOCK' || e.gameType === 'TIMELOCK')
const gameIconRaw = GAME_TYPE_ICON[e.gameType];
const gameIcon = gameIconRaw
? gameIconRaw
: `<span style="font-size:2.7rem;line-height:1;">${gameIconRaw}</span>`;
: `<span style="font-size:2.7rem;line-height:1;">🎮</span>`;
const days = Math.floor(e.durationMinutes / 1440);
const hours = Math.floor((e.durationMinutes % 1440) / 60);

View File

@@ -92,8 +92,8 @@
<div class="content">
<div class="tabs">
<button class="tab-btn active" id="tabMine" onclick="switchTab('mine', this)">Mein Feed</button>
<button class="tab-btn" id="tabPublic" onclick="switchTab('public', this)">Öffentlicher Feed</button>
<button class="tab-btn active" id="tabMine" data-tab="mine" onclick="switchTab('mine', this)">Mein Feed</button>
<button class="tab-btn" id="tabPublic" data-tab="public" onclick="switchTab('public', this)">Öffentlicher Feed</button>
</div>
<!-- Mein Feed -->
@@ -194,8 +194,14 @@
btn.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
localStorage.setItem('tab_feed', name);
if (!feedState[name].loaded) loadFeed(name);
}
const _savedFeedTab = localStorage.getItem('tab_feed');
if (_savedFeedTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedFeedTab}"]`);
if (_btn) switchTab(_savedFeedTab, _btn);
}
// ── Feed loading ──
async function loadFeed(tab) {

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Freunde XXX The Game</title>
@@ -134,8 +134,8 @@
<h1 style="margin-bottom: 1.25rem;">Freunde</h1>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('friends', this)">Freunde</button>
<button class="tab-btn" onclick="switchTab('pending', this)" id="pendingTabBtn">
<button class="tab-btn active" data-tab="friends" onclick="switchTab('friends', this)">Freunde</button>
<button class="tab-btn" data-tab="pending" onclick="switchTab('pending', this)" id="pendingTabBtn">
Anfragen<span class="tab-badge" id="pendingBadge" style="display:none;"></span>
</button>
</div>
@@ -166,7 +166,7 @@
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
@@ -175,6 +175,12 @@
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
localStorage.setItem('tab_freunde', name);
}
const _savedFreundeTab = localStorage.getItem('tab_freunde');
if (_savedFreundeTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedFreundeTab}"]`);
if (_btn) switchTab(_savedFreundeTab, _btn);
}
function esc(s) {

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gruppe XXX The Game</title>
@@ -154,9 +154,9 @@
</div>
<div class="tabs" id="tabBar">
<button class="tab-btn active" onclick="switchTab('posts', this)">Beiträge</button>
<button class="tab-btn" onclick="switchTab('members', this)">Mitglieder</button>
<button class="tab-btn" id="adminTabBtn" style="display:none;" onclick="switchTab('admin', this)">Admin <span class="social-badge" id="adminBadge" style="display:none;"></span></button>
<button class="tab-btn active" data-tab="posts" onclick="switchTab('posts', this)">Beiträge</button>
<button class="tab-btn" data-tab="members" onclick="switchTab('members', this)">Mitglieder</button>
<button class="tab-btn" data-tab="admin" id="adminTabBtn" style="display:none;" onclick="switchTab('admin', this)">Admin <span class="social-badge" id="adminBadge" style="display:none;"></span></button>
</div>
<!-- Posts Tab -->
@@ -295,7 +295,7 @@
</div>
<script src="/js/shared.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
@@ -355,6 +355,7 @@
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
localStorage.setItem('tab_gruppe_' + gruppeId, name);
if (name === 'members') loadMembers();
if (name === 'admin') { loadAdminRequests(); loadReports(); }
}
@@ -374,6 +375,12 @@
await loadGruppe();
await loadPosts();
const _savedTab = localStorage.getItem('tab_gruppe_' + gruppeId);
if (_savedTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedTab}"]`);
if (_btn && _btn.style.display !== 'none') switchTab(_savedTab, _btn);
}
}
async function loadGruppe() {

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gruppen XXX The Game</title>
@@ -68,9 +68,9 @@
</div>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('mine', this)">Meine Gruppen</button>
<button class="tab-btn" onclick="switchTab('discover', this)">Entdecken</button>
<button class="tab-btn" onclick="switchTab('requests', this)">Meine Anfragen</button>
<button class="tab-btn active" data-tab="mine" onclick="switchTab('mine', this)">Meine Gruppen</button>
<button class="tab-btn" data-tab="discover" onclick="switchTab('discover', this)">Entdecken</button>
<button class="tab-btn" data-tab="requests" onclick="switchTab('requests', this)">Meine Anfragen</button>
</div>
<!-- Meine Gruppen -->
@@ -136,7 +136,7 @@
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
@@ -145,9 +145,15 @@
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
localStorage.setItem('tab_gruppen', name);
if (name === 'mine') loadMine();
if (name === 'requests') loadRequests();
}
const _savedGruppenTab = localStorage.getItem('tab_gruppen');
if (_savedGruppenTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedGruppenTab}"]`);
if (_btn) switchTab(_savedGruppenTab, _btn);
}
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }

View File

@@ -1,32 +1,49 @@
/**
* 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)
*
* Typen:
* emoji Standard-Emoji oder Unicode-Zeichen (value: string)
* symbol Unicode-Symbol (value: string)
* image Pfad zu einer Bilddatei (value: string)
* compound Doppel-Icon: base-Icon + kleines Overlay-Icon (bottom-right)
* Felder: base { type, value }, overlay { type, value }
* base kann emoji, symbol oder image sein.
*/
window.ICONS = {
// ── Navigation / Sidebar ──
HOME: { type: 'emoji', value: '' },
VANILLA: { type: 'symbol', value: '' },
BDSM: { type: 'symbol', value: '' },
CHASTITY: { type: 'symbol', value: '' },
// ── Navigation / Sidebar ──────────────────────────────────────────────
HOME: { type: 'emoji', value: '🏠' },
VANILLA: { type: 'emoji', value: '' },
BDSM: { type: 'emoji', value: '⛓️' },
CHASTITY: { type: 'emoji', value: '🔒' },
// ── Aktionen ──
PLAY_NEW: { type: 'symbol', value: '' },
PLAY_ACTIVE: { type: 'symbol', value: '▶' },
// ── Aktionen ──────────────────────────────────────────────────────────
PLAY_NEW: { type: 'emoji', value: '🆕' },
PLAY_ACTIVE: { type: 'emoji', value: '▶' },
ACTIVE_LOCK: { type: 'emoji', value: '▶️' },
WAITING: { type: 'emoji', value: '⏳' },
CHECK: { type: 'symbol', value: '' },
DISCOVER: { type: 'symbol', value: '' },
ARROW: { type: 'symbol', value: '' },
CHECK: { type: 'emoji', value: '' },
DISCOVER: { type: 'emoji', value: '🗺️' },
ARROW: { type: 'emoji', value: '▶️' },
// ── Chastity Game ──
// ── UI-Symbole ────────────────────────────────────────────────────────
CLOSE: { type: 'symbol', value: '✕' }, // Schließen / Ablehnen / Löschen
CONFIRM: { type: 'symbol', value: '✓' }, // Bestätigen / Abschließen / Annehmen
LIKE: { type: 'symbol', value: '♥' }, // Like-Button
AVATAR: { type: 'symbol', value: '◉' }, // Avatar-Platzhalter (kein Bild)
ERROR: { type: 'emoji', value: '❌' }, // Fehlerzustand
TIMER: { type: 'emoji', value: '⏱️' }, // Zeitanzeige / Stoppuhr
LIGHTNING: { type: 'emoji', value: '⚡' }, // Aktion (z. B. Zeit entfernen)
EMOJI_PICKER: { type: 'emoji', value: '😊' }, // Emoji-Picker öffnen
REMOVE: { type: 'symbol', value: '⊗' }, // Eintrag/Spiel entfernen
// ── Chastity Game ─────────────────────────────────────────────────────
NEW_LOCK: { type: 'emoji', value: '🆕' },
LOCK: { type: 'emoji', value: '🔒' },
KEY: { type: 'emoji', value: '🔑' },
HISTORY: { type: 'emoji', value: '🔙' },
VOTES: { type: 'emoji', value: '🗳️' },
// ── Social ──
// ── Social ────────────────────────────────────────────────────────────
FEED: { type: 'emoji', value: '📰' },
SEARCH: { type: 'emoji', value: '🔍' },
FRIENDS: { type: 'emoji', value: '❤️' },
@@ -35,14 +52,79 @@ window.ICONS = {
GROUPS: { type: 'emoji', value: '👥' },
INVITATIONS: { type: 'emoji', value: '✨' },
SETTINGS: { type: 'emoji', value: '⚙️' },
LOGOUT: { type: 'symbol', value: '⏏' },
PROFILE: { type: 'symbol', value: '' },
LOGOUT: { type: 'emoji', value: '⏏' },
PROFILE: { type: 'emoji', value: '👤' },
// ── Aufgaben / Items ──
TOYS: { type: 'symbol', value: '' },
// ── Aufgaben / Items ──────────────────────────────────────────────────
TOYS: { type: 'emoji', value: '' },
// ── Spielhistorie Spieltypen ────────────────────────────────────────
// Einfache Spieltypen
GAME_BDSM: { type: 'emoji', value: '⛓️' },
GAME_VANILLA: { type: 'emoji', value: '❤️' },
// Doppel-Icons: großes Basis-Icon + kleines 🔒-Overlay
GAME_CARDLOCK: {
type: 'compound',
base: { type: 'image', value: '/img/card.png' },
overlay: { type: 'emoji', value: '🔒' }
},
GAME_TIMELOCK: {
type: 'compound',
base: { type: 'emoji', value: '⏰' },
overlay: { type: 'emoji', value: '🔒' }
},
// ── Spielhistorie Rollen-Badges ─────────────────────────────────────
ROLE_KEYHOLDER: { type: 'emoji', value: '🔑' },
ROLE_LOCKEE: { type: 'emoji', value: '🔒' },
};
/** Gibt nur den Wert (String) zurück für einfache Einbindung in Templates */
// ── Hilfsfunktionen ───────────────────────────────────────────────────────────
/** Gibt den rohen Wert-String zurück (nur für einfache Icons; '' für compound). */
window.IC = function(key) {
return (window.ICONS[key] || {}).value || '';
const icon = window.ICONS[key];
return (icon && icon.type !== 'compound') ? (icon.value || '') : '';
};
/**
* Gibt ein fertiges HTML-Fragment zurück, das das Icon darstellt.
*
* @param {string} key Schlüssel aus window.ICONS
* @param {number} [size] Basisgröße in rem (Standard: 2.7)
* @returns {string} HTML-String
*/
window.IChtml = function(key, size) {
const icon = window.ICONS[key];
if (!icon) return '';
return _iconToHtml(icon, size != null ? size : 2.7);
};
function _iconToHtml(icon, size) {
switch (icon.type) {
case 'emoji':
case 'symbol':
return `<span style="font-size:${size}rem;line-height:1;">${icon.value}</span>`;
case 'image':
return `<img src="${icon.value}" style="width:${size}rem;height:${size}rem;object-fit:contain;display:block;" alt="">`;
case 'compound': {
const baseHtml = _compoundBase(icon.base, size);
const overlayHtml = _compoundOverlay(icon.overlay, size * 0.48);
return `<span style="position:relative;display:inline-block;line-height:1;">${baseHtml}${overlayHtml}</span>`;
}
default:
return '';
}
}
function _compoundBase(base, size) {
if (base.type === 'image') {
return `<img src="${base.value}" style="width:${size}rem;height:${size}rem;object-fit:contain;display:block;" alt="">`;
}
return `<span style="font-size:${size}rem;line-height:1;">${base.value}</span>`;
}
function _compoundOverlay(overlay, size) {
return `<span style="position:absolute;bottom:-2px;right:-4px;font-size:${size.toFixed(2)}rem;line-height:1;">${overlay.value}</span>`;
}