diff --git a/.gitignore b/.gitignore
index da56994..2d6aca8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,6 @@
# Ignore Gradle build output directory
build
.aider*
+
+# Secrets – niemals einchecken
+.env
diff --git a/bin/main/de/oaa/xxx/config/SecurityConfig.class b/bin/main/de/oaa/xxx/config/SecurityConfig.class
index 740f6aa..dee7a62 100644
Binary files a/bin/main/de/oaa/xxx/config/SecurityConfig.class and b/bin/main/de/oaa/xxx/config/SecurityConfig.class differ
diff --git a/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class b/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class
index 61b7dd5..f1930d3 100644
Binary files a/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class and b/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class differ
diff --git a/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class b/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class
index 8bf8cb7..616c577 100644
Binary files a/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class and b/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class differ
diff --git a/bin/main/de/oaa/xxx/feed/FeedController.class b/bin/main/de/oaa/xxx/feed/FeedController.class
index a360759..daaeaf9 100644
Binary files a/bin/main/de/oaa/xxx/feed/FeedController.class and b/bin/main/de/oaa/xxx/feed/FeedController.class differ
diff --git a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$CreateBeitragRequest.class b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$CreateBeitragRequest.class
index 18d9ea8..b1e2d5c 100644
Binary files a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$CreateBeitragRequest.class and b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$CreateBeitragRequest.class differ
diff --git a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$PostsPage.class b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$PostsPage.class
index 97f995f..a51d49b 100644
Binary files a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$PostsPage.class and b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$PostsPage.class differ
diff --git a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$ReportRequest.class b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$ReportRequest.class
index 03932cb..342011a 100644
Binary files a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$ReportRequest.class and b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$ReportRequest.class differ
diff --git a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$VoteRequest.class b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$VoteRequest.class
index f241976..5f4eb52 100644
Binary files a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$VoteRequest.class and b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController$VoteRequest.class differ
diff --git a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class
index 6106767..d243124 100644
Binary files a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class and b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class differ
diff --git a/bin/main/de/oaa/xxx/hashtag/HashtagController.class b/bin/main/de/oaa/xxx/hashtag/HashtagController.class
new file mode 100644
index 0000000..fbe4fb3
Binary files /dev/null and b/bin/main/de/oaa/xxx/hashtag/HashtagController.class differ
diff --git a/bin/main/de/oaa/xxx/hashtag/HashtagEntity.class b/bin/main/de/oaa/xxx/hashtag/HashtagEntity.class
new file mode 100644
index 0000000..8d32874
Binary files /dev/null and b/bin/main/de/oaa/xxx/hashtag/HashtagEntity.class differ
diff --git a/bin/main/de/oaa/xxx/hashtag/HashtagRepository.class b/bin/main/de/oaa/xxx/hashtag/HashtagRepository.class
new file mode 100644
index 0000000..578286f
Binary files /dev/null and b/bin/main/de/oaa/xxx/hashtag/HashtagRepository.class differ
diff --git a/bin/main/de/oaa/xxx/hashtag/HashtagService.class b/bin/main/de/oaa/xxx/hashtag/HashtagService.class
new file mode 100644
index 0000000..0dd6f18
Binary files /dev/null and b/bin/main/de/oaa/xxx/hashtag/HashtagService.class differ
diff --git a/bin/main/de/oaa/xxx/hashtag/PostHashtagEntity.class b/bin/main/de/oaa/xxx/hashtag/PostHashtagEntity.class
new file mode 100644
index 0000000..80968da
Binary files /dev/null and b/bin/main/de/oaa/xxx/hashtag/PostHashtagEntity.class differ
diff --git a/bin/main/de/oaa/xxx/hashtag/PostHashtagRepository$HashtagFrequency.class b/bin/main/de/oaa/xxx/hashtag/PostHashtagRepository$HashtagFrequency.class
new file mode 100644
index 0000000..bbdce78
Binary files /dev/null and b/bin/main/de/oaa/xxx/hashtag/PostHashtagRepository$HashtagFrequency.class differ
diff --git a/bin/main/de/oaa/xxx/hashtag/PostHashtagRepository.class b/bin/main/de/oaa/xxx/hashtag/PostHashtagRepository.class
new file mode 100644
index 0000000..da1a0e5
Binary files /dev/null and b/bin/main/de/oaa/xxx/hashtag/PostHashtagRepository.class differ
diff --git a/bin/main/static/community/feed.html b/bin/main/static/community/feed.html
index a7d0de5..efe43d2 100644
--- a/bin/main/static/community/feed.html
+++ b/bin/main/static/community/feed.html
@@ -68,6 +68,11 @@
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
.sentinel { height:1px; }
+ .hashtag-banner { display:flex; align-items:center; gap:0.75rem; margin-bottom:1.25rem; padding:0.65rem 1rem; background:var(--color-card); border:1px solid var(--color-secondary); border-radius:8px; }
+ .hashtag-banner-tag { font-size:1.05rem; font-weight:700; color:var(--color-primary); }
+ .hashtag-banner-back { margin-left:auto; font-size:0.82rem; color:var(--color-muted); text-decoration:none; }
+ .hashtag-banner-back:hover { color:var(--color-primary); }
+
/* Lightbox */
.lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:300; align-items:center; justify-content:center; }
.lightbox.open { display:flex; }
@@ -92,7 +97,13 @@
-
+
+
+
+
@@ -140,6 +151,13 @@
+
+
+
+
Keine Posts mit diesem Hashtag.
+
+
+
@@ -165,37 +183,68 @@
-
+
+
-
+
+
@@ -157,7 +128,7 @@
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
- function gruppeCard(g, showJoin = false) {
+ function gruppeCard(g) {
const img = g.bild
? ``
: `👥
`;
@@ -165,16 +136,6 @@
? `${g.myRole === 'ADMIN' ? 'Admin' : 'Mitglied'}`
: '';
const privBadge = g.isPrivate ? ' 🔒' : '';
- let actions = '';
- if (showJoin && !g.myRole) {
- if (g.myRequestStatus === 'AUSSTEHEND') {
- actions = ``;
- } else if (g.isPrivate) {
- actions = ``;
- } else {
- actions = ``;
- }
- }
return `
${img}
@@ -183,7 +144,6 @@
${g.memberCount} Mitglied${g.memberCount !== 1 ? 'er' : ''} · ${g.postCount} Beiträge
${g.beschreibung ? `
${esc(g.beschreibung.substring(0,80))}${g.beschreibung.length>80?'…':''}
` : ''}
- ${actions ? `
${actions}
` : ''}
`;
}
@@ -223,37 +183,6 @@
}));
}
- async function doSearch() {
- const q = document.getElementById('searchInput').value.trim();
- if (!q) return;
- try {
- const res = await fetch('/gruppen/search?q=' + encodeURIComponent(q));
- if (!res.ok) return;
- const data = await res.json();
- const grid = document.getElementById('discoverGrid');
- const hint = document.getElementById('discoverHint');
- grid.innerHTML = '';
- if (data.length === 0) { hint.textContent = 'Keine Gruppen gefunden.'; hint.style.display = ''; return; }
- hint.style.display = 'none';
- data.forEach(g => grid.insertAdjacentHTML('beforeend', gruppeCard(g, true)));
- } catch(e) { console.error(e); }
- }
-
- async function joinGruppe(gruppeId, btn) {
- btn.disabled = true;
- btn.textContent = '…';
- try {
- const res = await fetch('/gruppen/' + gruppeId + '/join', { method: 'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
- if (res.ok || res.status === 201) {
- btn.textContent = 'Beigetreten ✓';
- setTimeout(loadMine, 500);
- } else {
- btn.disabled = false;
- btn.textContent = 'Beitreten';
- }
- } catch(e) { btn.disabled = false; btn.textContent = 'Beitreten'; }
- }
-
async function loadRequests() {
try {
const reqRes = await fetch('/gruppen/requests/mine');
@@ -333,42 +262,6 @@
} catch(e) { showCreateError('Fehler: ' + e.message); }
}
- // ── Join dialog ──
-
- let pendingJoinGruppeId = null;
- function openJoinDialog(gruppeId, name) {
- pendingJoinGruppeId = gruppeId;
- document.getElementById('joinDialogGroupName').textContent = name;
- document.getElementById('joinNachricht').value = '';
- document.getElementById('joinDialog').classList.add('visible');
- }
- function closeJoinDialog() { document.getElementById('joinDialog').classList.remove('visible'); pendingJoinGruppeId = null; }
-
- async function sendJoinRequest() {
- if (!pendingJoinGruppeId) return;
- document.getElementById('joinError').style.display = 'none';
- const nachricht = document.getElementById('joinNachricht').value.trim() || null;
- try {
- const res = await fetch('/gruppen/' + pendingJoinGruppeId + '/join', {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({ nachricht })
- });
- if (res.ok || res.status === 201) {
- closeJoinDialog();
- doSearch();
- } else {
- const el = document.getElementById('joinError');
- el.textContent = 'Fehler beim Senden der Anfrage.';
- el.style.display = 'block';
- }
- } catch(e) {
- const el = document.getElementById('joinError');
- el.textContent = 'Fehler: ' + e.message;
- el.style.display = 'block';
- }
- }
-
// ── Image preview ──
function previewBild(input, previewId, dataId) {
@@ -401,9 +294,6 @@
document.getElementById('createDialog').addEventListener('click', e => {
if (e.target === document.getElementById('createDialog')) closeCreateDialog();
});
- document.getElementById('joinDialog').addEventListener('click', e => {
- if (e.target === document.getElementById('joinDialog')) closeJoinDialog();
- });
loadMine();
diff --git a/bin/main/static/community/nachrichten.html b/bin/main/static/community/nachrichten.html
index a691a84..eae7222 100644
--- a/bin/main/static/community/nachrichten.html
+++ b/bin/main/static/community/nachrichten.html
@@ -360,7 +360,7 @@
const list = document.getElementById('convList');
list.innerHTML = '';
if (convs.length === 0) {
- list.innerHTML = 'Noch keine Nachrichten. Personen suchen';
+ list.innerHTML = 'Noch keine Nachrichten. Personen suchen';
return;
}
convs.forEach(c => {
diff --git a/bin/main/static/community/personen-suchen.html b/bin/main/static/community/personen-suchen.html
deleted file mode 100644
index 1cf76a7..0000000
--- a/bin/main/static/community/personen-suchen.html
+++ /dev/null
@@ -1,206 +0,0 @@
-
-
-
-
-
-
- Personen suchen – xXx Sphere
-
-
-
-
-
-
-
-
-
Personen suchen
-
-
-
-
-
-
-
-
Gib mindestens 2 Zeichen ein, um zu suchen.
-
-
-
-
-
-
-
-
-
diff --git a/bin/main/static/games/common/einladungen.html b/bin/main/static/games/common/einladungen.html
new file mode 100644
index 0000000..f198d67
--- /dev/null
+++ b/bin/main/static/games/common/einladungen.html
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+ Einladungen – xXx Sphere
+
+
+
+
+
+
+
+
Einladungen
+
+
+
+
+
+
+
+
+
Wird geladen…
+
+
+
+
+
+
+
+
+
Du hast aktuell keine offenen Einladungen.
+
+
+
+
+
+
Wird geladen…
+
+
+
+
+
+
+
+
+
Du hast aktuell keine gesendeten Einladungen.
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/main/static/games/vanilla/neuvanilla.html b/bin/main/static/games/vanilla/neuvanilla.html
index 559bed4..27a9699 100644
--- a/bin/main/static/games/vanilla/neuvanilla.html
+++ b/bin/main/static/games/vanilla/neuvanilla.html
@@ -1213,6 +1213,7 @@
const id = addPlayer(p.name, i === 0, i === 0, false, false);
if (id) { restorePlayer(id, p); if (p.userId) userIdToInfo[p.userId] = { playerId: id, name: p.name }; }
});
+ Object.entries(userIdToInfo).forEach(([userId, info]) => { pruefeChastityConstraint(info.playerId, userId); });
await ladeEinladungenAusDb(userIdToInfo);
restoredFromSetup = true;
} else {
@@ -1273,14 +1274,15 @@
if (draft.gruppenJson) { sessionStorage.setItem('vanilla-session-gruppen', draft.gruppenJson); savedGruppen = new Set(JSON.parse(draft.gruppenJson)); }
}
}
- const selfGeschlecht = user?.geschlecht || null;
- const selfWerkzeuge = selfGeschlecht ? (WERKZEUGE_DEFAULTS[selfGeschlecht] || []) : [];
+ const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
+ const selfGeschlecht = defaults.geschlecht || user?.geschlecht || null;
+ const selfWerkzeuge = defaults.werkzeuge?.length ? defaults.werkzeuge : (selfGeschlecht ? (WERKZEUGE_DEFAULTS[selfGeschlecht] || []) : []);
const selfId = addPlayer(user ? user.name : '', true, true, !!selfGeschlecht, false);
if (selfId) {
if (user?.profilePicture) { playerProfilePics[selfId] = user.profilePicture; updatePlayerHeader(selfId, user.name); }
if (playerIds.length < MAX_PLAYERS) addPlayer();
- restorePlayer(selfId, { geschlecht: selfGeschlecht, spieltMit: [], rollen: [], werkzeuge: selfWerkzeuge });
- pruefeChastityConstraint(selfId, myUserId);
+ restorePlayer(selfId, { geschlecht: selfGeschlecht, spieltMit: defaults.spieltMit || [], rollen: defaults.rollen || [], werkzeuge: selfWerkzeuge });
+ if (myUserId) pruefeChastityConstraint(selfId, myUserId);
}
await ladeEinladungenAusDb(null);
}
@@ -1324,8 +1326,9 @@
if (user?.profilePicture) { playerProfilePics[guestOwnPlayerId] = user.profilePicture; updatePlayerHeader(guestOwnPlayerId, user?.name || ''); }
- restorePlayer(guestOwnPlayerId, { geschlecht: user?.geschlecht || null, spieltMit: [], rollen: [], werkzeuge: [] });
- pruefeChastityConstraint(guestOwnPlayerId, myUserId);
+ const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
+ restorePlayer(guestOwnPlayerId, { geschlecht: defaults.geschlecht || user?.geschlecht || null, spieltMit: defaults.spieltMit || [], rollen: defaults.rollen || [], werkzeuge: defaults.werkzeuge || [] });
+ if (myUserId) pruefeChastityConstraint(guestOwnPlayerId, myUserId);
document.getElementById('acc-grundeinstellungen-btn').classList.add('is-open');
document.getElementById('acc-grundeinstellungen-body').classList.add('is-open');
diff --git a/bin/main/static/js/hashtag.js b/bin/main/static/js/hashtag.js
new file mode 100644
index 0000000..567df05
--- /dev/null
+++ b/bin/main/static/js/hashtag.js
@@ -0,0 +1,217 @@
+(function () {
+ 'use strict';
+
+ // ── Styles ──────────────────────────────────────────────────────────────
+ const style = document.createElement('style');
+ style.textContent = `
+ .post-hashtag {
+ color: var(--color-primary);
+ text-decoration: none;
+ font-weight: 600;
+ }
+ .post-hashtag:hover { text-decoration: underline; }
+
+ .hashtag-dropdown {
+ position: fixed;
+ z-index: 600;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 8px;
+ min-width: 170px;
+ max-width: 260px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.45);
+ overflow: hidden;
+ }
+ .hashtag-dropdown-item {
+ padding: 0.45rem 0.9rem;
+ cursor: pointer;
+ font-size: 0.88rem;
+ color: var(--color-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .hashtag-dropdown-item:hover,
+ .hashtag-dropdown-item.active {
+ background: var(--color-secondary);
+ color: var(--color-primary);
+ }
+ .hashtag-dropdown-create {
+ font-style: italic;
+ border-top: 1px solid var(--color-secondary);
+ color: var(--color-primary);
+ }
+ .hashtag-dropdown-hint {
+ padding: 0.45rem 0.9rem;
+ font-size: 0.82rem;
+ color: var(--color-text);
+ opacity: 0.6;
+ font-style: italic;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // ── Escape helper (works even if shared.js not yet loaded) ───────────────
+ function esc(s) {
+ return String(s ?? '')
+ .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ }
+
+ // ── renderTextWithHashtags ────────────────────────────────────────────────
+ window.renderTextWithHashtags = function (text) {
+ if (!text) return '';
+ const PATTERN = /#([\wäöüÄÖÜß]{1,100})/g;
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+ while ((match = PATTERN.exec(text)) !== null) {
+ parts.push(esc(text.slice(lastIndex, match.index)));
+ const tag = match[1].toLowerCase();
+ parts.push(
+ `#${esc(match[1])}`
+ );
+ lastIndex = match.index + match[0].length;
+ }
+ parts.push(esc(text.slice(lastIndex)));
+ return parts.join('');
+ };
+
+ // ── attachHashtagAutocomplete ─────────────────────────────────────────────
+ window.attachHashtagAutocomplete = function (textarea) {
+ let dropdownEl = null;
+ let suggestions = [];
+ let selectedIdx = -1;
+ let debounce = null;
+
+ // Returns { prefix, hashStart } if cursor is inside a #word, else null
+ function getHashAtCursor() {
+ const pos = textarea.selectionStart;
+ const text = textarea.value;
+ let i = pos - 1;
+ while (i >= 0 && /[\wäöüÄÖÜß]/.test(text[i])) i--;
+ if (i >= 0 && text[i] === '#') {
+ return { prefix: text.slice(i + 1, pos), hashStart: i };
+ }
+ if (i === -1 && text.length > 0 && text[0] === '#' && pos > 0) {
+ return { prefix: text.slice(1, pos), hashStart: 0 };
+ }
+ return null;
+ }
+
+ function positionDropdown() {
+ if (!dropdownEl) return;
+ const r = textarea.getBoundingClientRect();
+ dropdownEl.style.top = (r.bottom + window.scrollY + 2) + 'px';
+ dropdownEl.style.left = (r.left + window.scrollX) + 'px';
+ }
+
+ function highlight() {
+ if (!dropdownEl) return;
+ dropdownEl.querySelectorAll('.hashtag-dropdown-item').forEach((el, i) => {
+ el.classList.toggle('active', i === selectedIdx);
+ });
+ }
+
+ function removeDropdown() {
+ dropdownEl?.remove();
+ dropdownEl = null;
+ suggestions = [];
+ selectedIdx = -1;
+ }
+
+ function showDropdown(items, prefix) {
+ removeDropdown();
+ const lPrefix = prefix ? prefix.toLowerCase() : '';
+ const hasCreate = lPrefix.length > 0 && !items.some(t => t === lPrefix);
+
+ if (!items.length && !hasCreate) {
+ if (prefix !== undefined && prefix.length === 0) {
+ // Nur "#" getippt, noch keine populären Tags
+ dropdownEl = document.createElement('div');
+ dropdownEl.className = 'hashtag-dropdown';
+ const hint = document.createElement('div');
+ hint.className = 'hashtag-dropdown-hint';
+ hint.textContent = 'Tippe weiter um einen Hashtag zu erstellen';
+ dropdownEl.appendChild(hint);
+ document.body.appendChild(dropdownEl);
+ positionDropdown();
+ }
+ return;
+ }
+
+ suggestions = hasCreate ? [...items, lPrefix] : [...items];
+
+ dropdownEl = document.createElement('div');
+ dropdownEl.className = 'hashtag-dropdown';
+
+ suggestions.forEach((tag, i) => {
+ const isCreate = hasCreate && i === suggestions.length - 1;
+ const item = document.createElement('div');
+ item.className = 'hashtag-dropdown-item' + (isCreate ? ' hashtag-dropdown-create' : '');
+ item.textContent = (isCreate ? '+ #' : '#') + tag;
+ item.addEventListener('mousedown', e => { e.preventDefault(); insertTag(tag); });
+ item.addEventListener('mouseover', () => { selectedIdx = i; highlight(); });
+ dropdownEl.appendChild(item);
+ });
+
+ document.body.appendChild(dropdownEl);
+ positionDropdown();
+ }
+
+ function insertTag(tag) {
+ const info = getHashAtCursor();
+ if (!info) { removeDropdown(); return; }
+ const v = textarea.value;
+ const cursor = textarea.selectionStart;
+ const before = v.slice(0, info.hashStart);
+ const after = v.slice(cursor);
+ const inserted = '#' + tag + ' ';
+ textarea.value = before + inserted + after;
+ const newPos = before.length + inserted.length;
+ textarea.setSelectionRange(newPos, newPos);
+ textarea.focus();
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
+ removeDropdown();
+ }
+
+ textarea.addEventListener('input', () => {
+ clearTimeout(debounce);
+ const info = getHashAtCursor();
+ if (!info) { removeDropdown(); return; }
+ debounce = setTimeout(async () => {
+ try {
+ const url = info.prefix.length === 0
+ ? '/hashtags/popular?limit=6'
+ : '/hashtags/suggest?q=' + encodeURIComponent(info.prefix) + '&limit=6';
+ const res = await fetch(url);
+ if (res.ok) showDropdown(await res.json(), info.prefix);
+ else showDropdown([], info.prefix);
+ } catch { removeDropdown(); }
+ }, 150);
+ });
+
+ textarea.addEventListener('keydown', e => {
+ if (!dropdownEl) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ selectedIdx = Math.min(selectedIdx + 1, suggestions.length - 1);
+ highlight();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ selectedIdx = Math.max(selectedIdx - 1, 0);
+ highlight();
+ } else if ((e.key === 'Enter' || e.key === 'Tab') && selectedIdx >= 0) {
+ e.preventDefault();
+ insertTag(suggestions[selectedIdx]);
+ } else if (e.key === 'Escape') {
+ removeDropdown();
+ }
+ });
+
+ textarea.addEventListener('blur', () => setTimeout(removeDropdown, 150));
+ window.addEventListener('scroll', positionDropdown, { passive: true });
+ window.addEventListener('resize', positionDropdown, { passive: true });
+ };
+})();
diff --git a/bin/main/static/js/mobile-nav.js b/bin/main/static/js/mobile-nav.js
index 6fab66a..6e97bd7 100644
--- a/bin/main/static/js/mobile-nav.js
+++ b/bin/main/static/js/mobile-nav.js
@@ -19,7 +19,7 @@
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
z-index: 500;
align-items: center;
- padding: 0 0.25rem;
+ padding: 0 4px 0 0.25rem;
}
.mobile-topbar-logo {
position: absolute;
@@ -43,9 +43,9 @@
background: none;
border: none;
color: var(--color-text);
- font-size: 1.725rem;
+ font-size: 1.3rem;
line-height: 1;
- padding: 0.75rem 0.675rem;
+ padding: 0.55rem 0.6rem;
cursor: pointer;
display: flex;
align-items: center;
diff --git a/bin/main/static/js/nav.js b/bin/main/static/js/nav.js
index 70fa159..1a44fb6 100644
--- a/bin/main/static/js/nav.js
+++ b/bin/main/static/js/nav.js
@@ -301,7 +301,7 @@
'/community/gruppen.html', '/community/gruppe.html',
'/community/locations.html', '/community/location-detail.html',
'/community/events.html', '/community/event-detail.html',
- '/community/abonnements.html', '/community/personen-suchen.html',
+ '/community/abonnements.html',
'/community/benutzer.html',
])}
${column('colDating', 'Dating', col3Html, ['/dating/'])}
diff --git a/bin/main/static/js/section-nav.js b/bin/main/static/js/section-nav.js
index 4328936..41f1367 100644
--- a/bin/main/static/js/section-nav.js
+++ b/bin/main/static/js/section-nav.js
@@ -5,12 +5,19 @@
// ── Bereichs-Definitionen ────────────────────────────────────────────────
const SECTIONS = {
+ common: {
+ prefixes: ['/games/common/'],
+ items: [
+ { href: '/games/vanilla/neuvanilla.html', icon: 'VANILLA', label: 'Vanilla Game' },
+ { href: '/games/bdsm/neubdsm.html', icon: 'BDSM', label: 'BDSM Game' },
+ { href: '/games/chastity/neulock.html', icon: 'CHASTITY', label: 'Chastity Game' },
+ ],
+ },
social: {
prefixes: ['/community/'],
exclude: [
'/community/nachrichten.html',
'/community/benachrichtigungen.html',
- '/community/einladungen.html',
],
items: [
{ href: '/community/feed.html', icon: 'FEED', label: 'Feed' },
diff --git a/bin/main/static/js/topbar.js b/bin/main/static/js/topbar.js
index 7b88b4b..66d866d 100644
--- a/bin/main/static/js/topbar.js
+++ b/bin/main/static/js/topbar.js
@@ -168,18 +168,26 @@
async function doSearch(q, overlay) {
try {
- const res = await fetch('/search?q=' + encodeURIComponent(q) + '&limit=3');
- if (!res.ok) { overlay.innerHTML = 'Fehler bei der Suche.
'; return; }
- const data = await res.json();
- const { users = [], locations = [], events = [] } = data;
+ const tagQuery = q.startsWith('#') ? q.slice(1) : q;
+ const [searchRes, gruppenRes, hashtagRes] = await Promise.all([
+ fetch('/search?q=' + encodeURIComponent(q) + '&limit=3'),
+ fetch('/gruppen/search?q=' + encodeURIComponent(q)),
+ fetch('/hashtags/suggest?q=' + encodeURIComponent(tagQuery) + '&limit=4')
+ ]);
- if (!users.length && !locations.length && !events.length) {
- overlay.innerHTML = 'Keine Ergebnisse.
';
- return;
- }
+ const data = searchRes.ok ? await searchRes.json() : {};
+ const gruppen = gruppenRes.ok ? await gruppenRes.json() : [];
+ const hashtags = hashtagRes.ok ? await hashtagRes.json() : [];
+
+ const { users = [], locations = [], events = [] } = data;
+ const gruppenSlice = gruppen.slice(0, 3);
let html = '';
+ if (!users.length && !locations.length && !events.length && !gruppenSlice.length && !hashtags.length) {
+ html += 'Keine Ergebnisse.
';
+ }
+
if (users.length) {
html += `Personen
`;
html += users.map(u => {
@@ -213,6 +221,26 @@
}).join('');
}
+ if (gruppenSlice.length) {
+ html += `Gruppen
`;
+ html += gruppenSlice.map(g => {
+ const av = g.bild
+ ? `
`
+ : `👥`;
+ return `
+ ${av}${esc(g.name)}`;
+ }).join('');
+ }
+
+ if (hashtags.length) {
+ html += `Hashtags
`;
+ html += ``;
+ html += hashtags.map(tag =>
+ `
#${esc(tag)}`
+ ).join('');
+ html += `
`;
+ }
+
html += `Alle Ergebnisse anzeigen →`;
overlay.innerHTML = html;
} catch (e) {
diff --git a/bin/main/static/search.html b/bin/main/static/search.html
index 5d193a1..5e3c309 100644
--- a/bin/main/static/search.html
+++ b/bin/main/static/search.html
@@ -163,6 +163,33 @@
transition: border-color 0.15s, color 0.15s;
}
.search-load-more:hover { border-color: var(--color-primary); color: var(--color-primary); background: none; }
+
+ .hashtag-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.4rem 0.85rem;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 20px;
+ color: var(--color-primary);
+ font-weight: 600;
+ font-size: 0.9rem;
+ text-decoration: none;
+ transition: border-color 0.15s, background 0.15s;
+ }
+ .hashtag-chip:hover { border-color: var(--color-primary); background: var(--color-secondary); }
+
+ /* Dialog (Gruppen-Beitritt) */
+ .dialog-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:200; align-items:center; justify-content:center; }
+ .dialog-backdrop.visible { display:flex; }
+ .dialog { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:12px; padding:1.75rem; width:100%; max-width:420px; box-shadow:0 8px 32px rgba(0,0,0,0.6); max-height:90vh; overflow-y:auto; }
+ .dialog h3 { color:var(--color-primary); font-size:1.1rem; margin-bottom:1.25rem; }
+ .dialog label { display:block; font-size:0.8rem; color:#aaa; margin-bottom:0.3rem; margin-top:1rem; }
+ .dialog textarea { width:100%; padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:80px; box-sizing:border-box; }
+ .dialog textarea:focus { border-color:var(--color-primary); }
+ .dialog-actions { display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.5rem; }
+ .dialog-actions button { flex:none; margin:0; padding:0.55rem 1.1rem; font-size:0.9rem; width:auto; }
@@ -173,7 +200,7 @@
@@ -188,6 +215,12 @@
+
+
@@ -207,6 +240,31 @@
+
+
+
+
+
+
+
+
+
+
+
Beitrittsanfrage senden
+
+
+
+
+
+
+
+
@@ -279,10 +337,12 @@
document.getElementById('grid' + cap(t)).innerHTML = '';
document.getElementById('more' + cap(t)).style.display = 'none';
});
- // Alle drei Typen parallel laden
+ // Alle Typen parallel laden
loadChunk('users');
loadChunk('locations');
loadChunk('events');
+ loadGruppen(q);
+ loadHashtags(q);
}
function clearAll() {
@@ -295,6 +355,12 @@
document.getElementById('count' + cap(t)).textContent = '0';
document.getElementById('loading' + cap(t)).style.display = 'none';
});
+ document.getElementById('gridGruppen').innerHTML = '';
+ document.getElementById('countGruppen').textContent = '0';
+ document.getElementById('loadingGruppen').style.display = 'none';
+ document.getElementById('gridHashtags').innerHTML = '';
+ document.getElementById('countHashtags').textContent = '0';
+ document.getElementById('loadingHashtags').style.display = 'none';
const url = new URL(window.location);
url.searchParams.delete('q');
history.replaceState(null, '', url);
@@ -374,14 +440,185 @@
});
}
+ // ── Gruppen-Suche ──
+
+ async function loadGruppen(q) {
+ const loadingEl = document.getElementById('loadingGruppen');
+ const grid = document.getElementById('gridGruppen');
+ loadingEl.style.display = '';
+ grid.innerHTML = '';
+ try {
+ const res = await fetch('/gruppen/search?q=' + encodeURIComponent(q));
+ if (!res.ok) throw new Error();
+ const data = await res.json();
+ document.getElementById('countGruppen').textContent = data.length;
+ if (!data.length) {
+ grid.innerHTML = 'Keine Ergebnisse.
';
+ return;
+ }
+ data.forEach(g => grid.appendChild(buildGruppeCard(g)));
+ } catch (e) {
+ // ignore
+ } finally {
+ loadingEl.style.display = 'none';
+ }
+ }
+
+ function buildGruppeCard(g) {
+ const card = document.createElement('div');
+ card.className = 'search-card';
+ card.style.cssText = 'justify-content:space-between; cursor:pointer;';
+ card.addEventListener('click', () => { location.href = '/community/gruppe.html?gruppeId=' + g.gruppeId; });
+
+ const av = g.bild
+ ? ``
+ : `👥
`;
+ const privBadge = g.isPrivate ? ' 🔒' : '';
+ const sub = g.memberCount + ' Mitglied' + (g.memberCount !== 1 ? 'er' : '');
+
+ const info = document.createElement('div');
+ info.style.cssText = 'display:flex; align-items:center; gap:0.75rem; min-width:0; flex:1;';
+ info.innerHTML = `${av}${esc(g.name)}${privBadge}
${esc(sub)}
`;
+ card.appendChild(info);
+
+ if (!g.myRole) {
+ const btn = document.createElement('button');
+ btn.style.cssText = 'font-size:0.78rem; padding:0.3rem 0.65rem; width:auto; margin:0; white-space:nowrap; flex-shrink:0; margin-left:0.5rem;';
+ if (g.myRequestStatus === 'AUSSTEHEND') {
+ btn.disabled = true;
+ btn.style.opacity = '0.6';
+ btn.textContent = 'Anfrage ausstehend';
+ } else if (g.isPrivate) {
+ btn.textContent = 'Anfrage senden';
+ btn.addEventListener('click', e => { e.stopPropagation(); openSearchJoinDialog(g.gruppeId, g.name); });
+ } else {
+ btn.textContent = 'Beitreten';
+ btn.addEventListener('click', e => { e.stopPropagation(); joinGruppeSearch(g.gruppeId, btn); });
+ }
+ card.appendChild(btn);
+ }
+
+ return card;
+ }
+
+ // ── Hashtag-Suche ──
+
+ async function loadHashtags(q) {
+ const loadingEl = document.getElementById('loadingHashtags');
+ const grid = document.getElementById('gridHashtags');
+ loadingEl.style.display = '';
+ grid.innerHTML = '';
+ try {
+ const raw = q.startsWith('#') ? q.slice(1) : q;
+ const res = await fetch('/hashtags/suggest?q=' + encodeURIComponent(raw) + '&limit=20');
+ if (!res.ok) throw new Error();
+ const tags = await res.json();
+ document.getElementById('countHashtags').textContent = tags.length;
+ if (!tags.length) {
+ grid.innerHTML = 'Keine Hashtags gefunden.
';
+ return;
+ }
+ tags.forEach(tag => {
+ const a = document.createElement('a');
+ a.className = 'hashtag-chip';
+ a.href = '/community/feed.html?tag=' + encodeURIComponent(tag);
+ a.textContent = '#' + tag;
+ grid.appendChild(a);
+ });
+ } catch (e) {
+ // ignore
+ } finally {
+ loadingEl.style.display = 'none';
+ }
+ }
+
+ async function joinGruppeSearch(gruppeId, btn) {
+ btn.disabled = true;
+ btn.textContent = '…';
+ try {
+ const res = await fetch('/gruppen/' + gruppeId + '/join', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: '{}'
+ });
+ if (res.ok || res.status === 201) {
+ btn.textContent = 'Beigetreten ✓';
+ } else {
+ btn.disabled = false;
+ btn.textContent = 'Beitreten';
+ }
+ } catch (e) {
+ btn.disabled = false;
+ btn.textContent = 'Beitreten';
+ }
+ }
+
+ // ── Join-Dialog für Gruppen ──
+
+ let searchJoinGruppeId = null;
+
+ function openSearchJoinDialog(gruppeId, name) {
+ searchJoinGruppeId = gruppeId;
+ document.getElementById('searchJoinGroupName').textContent = name;
+ document.getElementById('searchJoinNachricht').value = '';
+ document.getElementById('searchJoinError').style.display = 'none';
+ document.getElementById('searchJoinDialog').classList.add('visible');
+ }
+
+ function closeSearchJoinDialog() {
+ document.getElementById('searchJoinDialog').classList.remove('visible');
+ searchJoinGruppeId = null;
+ }
+
+ async function sendSearchJoinRequest() {
+ if (!searchJoinGruppeId) return;
+ document.getElementById('searchJoinError').style.display = 'none';
+ const nachricht = document.getElementById('searchJoinNachricht').value.trim() || null;
+ try {
+ const res = await fetch('/gruppen/' + searchJoinGruppeId + '/join', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nachricht })
+ });
+ if (res.ok || res.status === 201) {
+ closeSearchJoinDialog();
+ if (currentQuery.length >= 2) loadGruppen(currentQuery);
+ } else {
+ const el = document.getElementById('searchJoinError');
+ el.textContent = 'Fehler beim Senden der Anfrage.';
+ el.style.display = 'block';
+ }
+ } catch (e) {
+ const el = document.getElementById('searchJoinError');
+ el.textContent = 'Fehler: ' + e.message;
+ el.style.display = 'block';
+ }
+ }
+
+ document.getElementById('searchJoinCancelBtn').addEventListener('click', closeSearchJoinDialog);
+ document.getElementById('searchJoinSendBtn').addEventListener('click', sendSearchJoinRequest);
+ document.getElementById('searchJoinDialog').addEventListener('click', e => {
+ if (e.target === document.getElementById('searchJoinDialog')) closeSearchJoinDialog();
+ });
+
function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
// ── URL-Parameter beim Start auslesen ──
- const initQ = new URLSearchParams(window.location.search).get('q') || '';
+ const params = new URLSearchParams(window.location.search);
+ const initQ = params.get('q') || '';
+ const initTab = params.get('tab') || '';
+
+ if (initTab) {
+ const tabBtn = document.querySelector(`.search-tab-btn[data-tab="${initTab}"]`);
+ if (tabBtn) tabBtn.click();
+ }
+
if (initQ.length >= 2) {
input.value = initQ;
- // Warten bis icons.js geladen ist
setTimeout(() => startSearch(initQ), 100);
+ } else if (initTab === 'hashtags') {
+ // Populäre Tags zeigen wenn kein Suchbegriff
+ setTimeout(() => loadHashtags(''), 100);
}
})();
diff --git a/bin/main/static/userhome.html b/bin/main/static/userhome.html
index fa94bba..fe813f9 100644
--- a/bin/main/static/userhome.html
+++ b/bin/main/static/userhome.html
@@ -243,6 +243,33 @@
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
@media (max-width:650px) { .lb-layout { flex-direction:column; height:95vh; } .lb-post-side { border-right:none; border-bottom:1px solid var(--color-secondary); max-height:55vh; } .lb-comments-panel { width:100%; } }
+ /* ── Spiel starten ── */
+ .start-game-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 0.75rem;
+ }
+ .start-game-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.6rem;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 12px;
+ padding: 1.25rem 1rem;
+ text-decoration: none;
+ color: var(--color-text);
+ transition: border-color 0.15s, background 0.15s;
+ text-align: center;
+ }
+ .start-game-card:hover {
+ border-color: var(--color-primary);
+ background: rgba(var(--color-primary-rgb,233,69,96),0.06);
+ }
+ .start-game-icon { font-size: 2rem; line-height: 1; }
+ .start-game-title { font-size: 0.9rem; font-weight: 600; }
+
/* ── Neue Mitglieder ── */
.new-members-strip {
display: flex;
@@ -306,6 +333,25 @@
+
+
+
Einladungen 📨
@@ -423,6 +469,7 @@
+
-
+
+
-
+
+
@@ -157,7 +128,7 @@
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
- function gruppeCard(g, showJoin = false) {
+ function gruppeCard(g) {
const img = g.bild
? `
`
: `
👥
`;
@@ -165,16 +136,6 @@
? `
${g.myRole === 'ADMIN' ? 'Admin' : 'Mitglied'}`
: '';
const privBadge = g.isPrivate ? ' 🔒' : '';
- let actions = '';
- if (showJoin && !g.myRole) {
- if (g.myRequestStatus === 'AUSSTEHEND') {
- actions = `
`;
- } else if (g.isPrivate) {
- actions = `
`;
- } else {
- actions = `
`;
- }
- }
return `
${img}
@@ -183,7 +144,6 @@
${g.memberCount} Mitglied${g.memberCount !== 1 ? 'er' : ''} · ${g.postCount} Beiträge
${g.beschreibung ? `
${esc(g.beschreibung.substring(0,80))}${g.beschreibung.length>80?'…':''}
` : ''}
- ${actions ? `
${actions}
` : ''}
`;
}
@@ -223,37 +183,6 @@
}));
}
- async function doSearch() {
- const q = document.getElementById('searchInput').value.trim();
- if (!q) return;
- try {
- const res = await fetch('/gruppen/search?q=' + encodeURIComponent(q));
- if (!res.ok) return;
- const data = await res.json();
- const grid = document.getElementById('discoverGrid');
- const hint = document.getElementById('discoverHint');
- grid.innerHTML = '';
- if (data.length === 0) { hint.textContent = 'Keine Gruppen gefunden.'; hint.style.display = ''; return; }
- hint.style.display = 'none';
- data.forEach(g => grid.insertAdjacentHTML('beforeend', gruppeCard(g, true)));
- } catch(e) { console.error(e); }
- }
-
- async function joinGruppe(gruppeId, btn) {
- btn.disabled = true;
- btn.textContent = '…';
- try {
- const res = await fetch('/gruppen/' + gruppeId + '/join', { method: 'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
- if (res.ok || res.status === 201) {
- btn.textContent = 'Beigetreten ✓';
- setTimeout(loadMine, 500);
- } else {
- btn.disabled = false;
- btn.textContent = 'Beitreten';
- }
- } catch(e) { btn.disabled = false; btn.textContent = 'Beitreten'; }
- }
-
async function loadRequests() {
try {
const reqRes = await fetch('/gruppen/requests/mine');
@@ -333,42 +262,6 @@
} catch(e) { showCreateError('Fehler: ' + e.message); }
}
- // ── Join dialog ──
-
- let pendingJoinGruppeId = null;
- function openJoinDialog(gruppeId, name) {
- pendingJoinGruppeId = gruppeId;
- document.getElementById('joinDialogGroupName').textContent = name;
- document.getElementById('joinNachricht').value = '';
- document.getElementById('joinDialog').classList.add('visible');
- }
- function closeJoinDialog() { document.getElementById('joinDialog').classList.remove('visible'); pendingJoinGruppeId = null; }
-
- async function sendJoinRequest() {
- if (!pendingJoinGruppeId) return;
- document.getElementById('joinError').style.display = 'none';
- const nachricht = document.getElementById('joinNachricht').value.trim() || null;
- try {
- const res = await fetch('/gruppen/' + pendingJoinGruppeId + '/join', {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({ nachricht })
- });
- if (res.ok || res.status === 201) {
- closeJoinDialog();
- doSearch();
- } else {
- const el = document.getElementById('joinError');
- el.textContent = 'Fehler beim Senden der Anfrage.';
- el.style.display = 'block';
- }
- } catch(e) {
- const el = document.getElementById('joinError');
- el.textContent = 'Fehler: ' + e.message;
- el.style.display = 'block';
- }
- }
-
// ── Image preview ──
function previewBild(input, previewId, dataId) {
@@ -401,9 +294,6 @@
document.getElementById('createDialog').addEventListener('click', e => {
if (e.target === document.getElementById('createDialog')) closeCreateDialog();
});
- document.getElementById('joinDialog').addEventListener('click', e => {
- if (e.target === document.getElementById('joinDialog')) closeJoinDialog();
- });
loadMine();
diff --git a/src/main/resources/static/community/nachrichten.html b/src/main/resources/static/community/nachrichten.html
index a691a84..eae7222 100644
--- a/src/main/resources/static/community/nachrichten.html
+++ b/src/main/resources/static/community/nachrichten.html
@@ -360,7 +360,7 @@
const list = document.getElementById('convList');
list.innerHTML = '';
if (convs.length === 0) {
- list.innerHTML = 'Noch keine Nachrichten. Personen suchen';
+ list.innerHTML = 'Noch keine Nachrichten. Personen suchen';
return;
}
convs.forEach(c => {
diff --git a/src/main/resources/static/community/personen-suchen.html b/src/main/resources/static/community/personen-suchen.html
deleted file mode 100644
index 1cf76a7..0000000
--- a/src/main/resources/static/community/personen-suchen.html
+++ /dev/null
@@ -1,206 +0,0 @@
-
-
-
-
-
-
- Personen suchen – xXx Sphere
-
-
-
-
-
-
-
-
-
Personen suchen
-
-
-
-
-
-
-
-
Gib mindestens 2 Zeichen ein, um zu suchen.
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/static/games/common/einladungen.html b/src/main/resources/static/games/common/einladungen.html
new file mode 100644
index 0000000..f198d67
--- /dev/null
+++ b/src/main/resources/static/games/common/einladungen.html
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+ Einladungen – xXx Sphere
+
+
+
+
+
+
+
+
Einladungen
+
+
+
+
+
+
+
+
+
Wird geladen…
+
+
+
+
+
+
+
+
+
Du hast aktuell keine offenen Einladungen.
+
+
+
+
+
+
Wird geladen…
+
+
+
+
+
+
+
+
+
Du hast aktuell keine gesendeten Einladungen.
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/static/games/vanilla/neuvanilla.html b/src/main/resources/static/games/vanilla/neuvanilla.html
index 559bed4..27a9699 100644
--- a/src/main/resources/static/games/vanilla/neuvanilla.html
+++ b/src/main/resources/static/games/vanilla/neuvanilla.html
@@ -1213,6 +1213,7 @@
const id = addPlayer(p.name, i === 0, i === 0, false, false);
if (id) { restorePlayer(id, p); if (p.userId) userIdToInfo[p.userId] = { playerId: id, name: p.name }; }
});
+ Object.entries(userIdToInfo).forEach(([userId, info]) => { pruefeChastityConstraint(info.playerId, userId); });
await ladeEinladungenAusDb(userIdToInfo);
restoredFromSetup = true;
} else {
@@ -1273,14 +1274,15 @@
if (draft.gruppenJson) { sessionStorage.setItem('vanilla-session-gruppen', draft.gruppenJson); savedGruppen = new Set(JSON.parse(draft.gruppenJson)); }
}
}
- const selfGeschlecht = user?.geschlecht || null;
- const selfWerkzeuge = selfGeschlecht ? (WERKZEUGE_DEFAULTS[selfGeschlecht] || []) : [];
+ const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
+ const selfGeschlecht = defaults.geschlecht || user?.geschlecht || null;
+ const selfWerkzeuge = defaults.werkzeuge?.length ? defaults.werkzeuge : (selfGeschlecht ? (WERKZEUGE_DEFAULTS[selfGeschlecht] || []) : []);
const selfId = addPlayer(user ? user.name : '', true, true, !!selfGeschlecht, false);
if (selfId) {
if (user?.profilePicture) { playerProfilePics[selfId] = user.profilePicture; updatePlayerHeader(selfId, user.name); }
if (playerIds.length < MAX_PLAYERS) addPlayer();
- restorePlayer(selfId, { geschlecht: selfGeschlecht, spieltMit: [], rollen: [], werkzeuge: selfWerkzeuge });
- pruefeChastityConstraint(selfId, myUserId);
+ restorePlayer(selfId, { geschlecht: selfGeschlecht, spieltMit: defaults.spieltMit || [], rollen: defaults.rollen || [], werkzeuge: selfWerkzeuge });
+ if (myUserId) pruefeChastityConstraint(selfId, myUserId);
}
await ladeEinladungenAusDb(null);
}
@@ -1324,8 +1326,9 @@
if (user?.profilePicture) { playerProfilePics[guestOwnPlayerId] = user.profilePicture; updatePlayerHeader(guestOwnPlayerId, user?.name || ''); }
- restorePlayer(guestOwnPlayerId, { geschlecht: user?.geschlecht || null, spieltMit: [], rollen: [], werkzeuge: [] });
- pruefeChastityConstraint(guestOwnPlayerId, myUserId);
+ const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
+ restorePlayer(guestOwnPlayerId, { geschlecht: defaults.geschlecht || user?.geschlecht || null, spieltMit: defaults.spieltMit || [], rollen: defaults.rollen || [], werkzeuge: defaults.werkzeuge || [] });
+ if (myUserId) pruefeChastityConstraint(guestOwnPlayerId, myUserId);
document.getElementById('acc-grundeinstellungen-btn').classList.add('is-open');
document.getElementById('acc-grundeinstellungen-body').classList.add('is-open');
diff --git a/src/main/resources/static/js/hashtag.js b/src/main/resources/static/js/hashtag.js
new file mode 100644
index 0000000..567df05
--- /dev/null
+++ b/src/main/resources/static/js/hashtag.js
@@ -0,0 +1,217 @@
+(function () {
+ 'use strict';
+
+ // ── Styles ──────────────────────────────────────────────────────────────
+ const style = document.createElement('style');
+ style.textContent = `
+ .post-hashtag {
+ color: var(--color-primary);
+ text-decoration: none;
+ font-weight: 600;
+ }
+ .post-hashtag:hover { text-decoration: underline; }
+
+ .hashtag-dropdown {
+ position: fixed;
+ z-index: 600;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 8px;
+ min-width: 170px;
+ max-width: 260px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.45);
+ overflow: hidden;
+ }
+ .hashtag-dropdown-item {
+ padding: 0.45rem 0.9rem;
+ cursor: pointer;
+ font-size: 0.88rem;
+ color: var(--color-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .hashtag-dropdown-item:hover,
+ .hashtag-dropdown-item.active {
+ background: var(--color-secondary);
+ color: var(--color-primary);
+ }
+ .hashtag-dropdown-create {
+ font-style: italic;
+ border-top: 1px solid var(--color-secondary);
+ color: var(--color-primary);
+ }
+ .hashtag-dropdown-hint {
+ padding: 0.45rem 0.9rem;
+ font-size: 0.82rem;
+ color: var(--color-text);
+ opacity: 0.6;
+ font-style: italic;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // ── Escape helper (works even if shared.js not yet loaded) ───────────────
+ function esc(s) {
+ return String(s ?? '')
+ .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ }
+
+ // ── renderTextWithHashtags ────────────────────────────────────────────────
+ window.renderTextWithHashtags = function (text) {
+ if (!text) return '';
+ const PATTERN = /#([\wäöüÄÖÜß]{1,100})/g;
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+ while ((match = PATTERN.exec(text)) !== null) {
+ parts.push(esc(text.slice(lastIndex, match.index)));
+ const tag = match[1].toLowerCase();
+ parts.push(
+ `#${esc(match[1])}`
+ );
+ lastIndex = match.index + match[0].length;
+ }
+ parts.push(esc(text.slice(lastIndex)));
+ return parts.join('');
+ };
+
+ // ── attachHashtagAutocomplete ─────────────────────────────────────────────
+ window.attachHashtagAutocomplete = function (textarea) {
+ let dropdownEl = null;
+ let suggestions = [];
+ let selectedIdx = -1;
+ let debounce = null;
+
+ // Returns { prefix, hashStart } if cursor is inside a #word, else null
+ function getHashAtCursor() {
+ const pos = textarea.selectionStart;
+ const text = textarea.value;
+ let i = pos - 1;
+ while (i >= 0 && /[\wäöüÄÖÜß]/.test(text[i])) i--;
+ if (i >= 0 && text[i] === '#') {
+ return { prefix: text.slice(i + 1, pos), hashStart: i };
+ }
+ if (i === -1 && text.length > 0 && text[0] === '#' && pos > 0) {
+ return { prefix: text.slice(1, pos), hashStart: 0 };
+ }
+ return null;
+ }
+
+ function positionDropdown() {
+ if (!dropdownEl) return;
+ const r = textarea.getBoundingClientRect();
+ dropdownEl.style.top = (r.bottom + window.scrollY + 2) + 'px';
+ dropdownEl.style.left = (r.left + window.scrollX) + 'px';
+ }
+
+ function highlight() {
+ if (!dropdownEl) return;
+ dropdownEl.querySelectorAll('.hashtag-dropdown-item').forEach((el, i) => {
+ el.classList.toggle('active', i === selectedIdx);
+ });
+ }
+
+ function removeDropdown() {
+ dropdownEl?.remove();
+ dropdownEl = null;
+ suggestions = [];
+ selectedIdx = -1;
+ }
+
+ function showDropdown(items, prefix) {
+ removeDropdown();
+ const lPrefix = prefix ? prefix.toLowerCase() : '';
+ const hasCreate = lPrefix.length > 0 && !items.some(t => t === lPrefix);
+
+ if (!items.length && !hasCreate) {
+ if (prefix !== undefined && prefix.length === 0) {
+ // Nur "#" getippt, noch keine populären Tags
+ dropdownEl = document.createElement('div');
+ dropdownEl.className = 'hashtag-dropdown';
+ const hint = document.createElement('div');
+ hint.className = 'hashtag-dropdown-hint';
+ hint.textContent = 'Tippe weiter um einen Hashtag zu erstellen';
+ dropdownEl.appendChild(hint);
+ document.body.appendChild(dropdownEl);
+ positionDropdown();
+ }
+ return;
+ }
+
+ suggestions = hasCreate ? [...items, lPrefix] : [...items];
+
+ dropdownEl = document.createElement('div');
+ dropdownEl.className = 'hashtag-dropdown';
+
+ suggestions.forEach((tag, i) => {
+ const isCreate = hasCreate && i === suggestions.length - 1;
+ const item = document.createElement('div');
+ item.className = 'hashtag-dropdown-item' + (isCreate ? ' hashtag-dropdown-create' : '');
+ item.textContent = (isCreate ? '+ #' : '#') + tag;
+ item.addEventListener('mousedown', e => { e.preventDefault(); insertTag(tag); });
+ item.addEventListener('mouseover', () => { selectedIdx = i; highlight(); });
+ dropdownEl.appendChild(item);
+ });
+
+ document.body.appendChild(dropdownEl);
+ positionDropdown();
+ }
+
+ function insertTag(tag) {
+ const info = getHashAtCursor();
+ if (!info) { removeDropdown(); return; }
+ const v = textarea.value;
+ const cursor = textarea.selectionStart;
+ const before = v.slice(0, info.hashStart);
+ const after = v.slice(cursor);
+ const inserted = '#' + tag + ' ';
+ textarea.value = before + inserted + after;
+ const newPos = before.length + inserted.length;
+ textarea.setSelectionRange(newPos, newPos);
+ textarea.focus();
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
+ removeDropdown();
+ }
+
+ textarea.addEventListener('input', () => {
+ clearTimeout(debounce);
+ const info = getHashAtCursor();
+ if (!info) { removeDropdown(); return; }
+ debounce = setTimeout(async () => {
+ try {
+ const url = info.prefix.length === 0
+ ? '/hashtags/popular?limit=6'
+ : '/hashtags/suggest?q=' + encodeURIComponent(info.prefix) + '&limit=6';
+ const res = await fetch(url);
+ if (res.ok) showDropdown(await res.json(), info.prefix);
+ else showDropdown([], info.prefix);
+ } catch { removeDropdown(); }
+ }, 150);
+ });
+
+ textarea.addEventListener('keydown', e => {
+ if (!dropdownEl) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ selectedIdx = Math.min(selectedIdx + 1, suggestions.length - 1);
+ highlight();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ selectedIdx = Math.max(selectedIdx - 1, 0);
+ highlight();
+ } else if ((e.key === 'Enter' || e.key === 'Tab') && selectedIdx >= 0) {
+ e.preventDefault();
+ insertTag(suggestions[selectedIdx]);
+ } else if (e.key === 'Escape') {
+ removeDropdown();
+ }
+ });
+
+ textarea.addEventListener('blur', () => setTimeout(removeDropdown, 150));
+ window.addEventListener('scroll', positionDropdown, { passive: true });
+ window.addEventListener('resize', positionDropdown, { passive: true });
+ };
+})();
diff --git a/src/main/resources/static/js/mobile-nav.js b/src/main/resources/static/js/mobile-nav.js
index 6fab66a..6e97bd7 100644
--- a/src/main/resources/static/js/mobile-nav.js
+++ b/src/main/resources/static/js/mobile-nav.js
@@ -19,7 +19,7 @@
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
z-index: 500;
align-items: center;
- padding: 0 0.25rem;
+ padding: 0 4px 0 0.25rem;
}
.mobile-topbar-logo {
position: absolute;
@@ -43,9 +43,9 @@
background: none;
border: none;
color: var(--color-text);
- font-size: 1.725rem;
+ font-size: 1.3rem;
line-height: 1;
- padding: 0.75rem 0.675rem;
+ padding: 0.55rem 0.6rem;
cursor: pointer;
display: flex;
align-items: center;
diff --git a/src/main/resources/static/js/nav.js b/src/main/resources/static/js/nav.js
index 70fa159..1a44fb6 100644
--- a/src/main/resources/static/js/nav.js
+++ b/src/main/resources/static/js/nav.js
@@ -301,7 +301,7 @@
'/community/gruppen.html', '/community/gruppe.html',
'/community/locations.html', '/community/location-detail.html',
'/community/events.html', '/community/event-detail.html',
- '/community/abonnements.html', '/community/personen-suchen.html',
+ '/community/abonnements.html',
'/community/benutzer.html',
])}
${column('colDating', 'Dating', col3Html, ['/dating/'])}
diff --git a/src/main/resources/static/js/section-nav.js b/src/main/resources/static/js/section-nav.js
index 4328936..41f1367 100644
--- a/src/main/resources/static/js/section-nav.js
+++ b/src/main/resources/static/js/section-nav.js
@@ -5,12 +5,19 @@
// ── Bereichs-Definitionen ────────────────────────────────────────────────
const SECTIONS = {
+ common: {
+ prefixes: ['/games/common/'],
+ items: [
+ { href: '/games/vanilla/neuvanilla.html', icon: 'VANILLA', label: 'Vanilla Game' },
+ { href: '/games/bdsm/neubdsm.html', icon: 'BDSM', label: 'BDSM Game' },
+ { href: '/games/chastity/neulock.html', icon: 'CHASTITY', label: 'Chastity Game' },
+ ],
+ },
social: {
prefixes: ['/community/'],
exclude: [
'/community/nachrichten.html',
'/community/benachrichtigungen.html',
- '/community/einladungen.html',
],
items: [
{ href: '/community/feed.html', icon: 'FEED', label: 'Feed' },
diff --git a/src/main/resources/static/js/topbar.js b/src/main/resources/static/js/topbar.js
index 7b88b4b..66d866d 100644
--- a/src/main/resources/static/js/topbar.js
+++ b/src/main/resources/static/js/topbar.js
@@ -168,18 +168,26 @@
async function doSearch(q, overlay) {
try {
- const res = await fetch('/search?q=' + encodeURIComponent(q) + '&limit=3');
- if (!res.ok) { overlay.innerHTML = 'Fehler bei der Suche.
'; return; }
- const data = await res.json();
- const { users = [], locations = [], events = [] } = data;
+ const tagQuery = q.startsWith('#') ? q.slice(1) : q;
+ const [searchRes, gruppenRes, hashtagRes] = await Promise.all([
+ fetch('/search?q=' + encodeURIComponent(q) + '&limit=3'),
+ fetch('/gruppen/search?q=' + encodeURIComponent(q)),
+ fetch('/hashtags/suggest?q=' + encodeURIComponent(tagQuery) + '&limit=4')
+ ]);
- if (!users.length && !locations.length && !events.length) {
- overlay.innerHTML = 'Keine Ergebnisse.
';
- return;
- }
+ const data = searchRes.ok ? await searchRes.json() : {};
+ const gruppen = gruppenRes.ok ? await gruppenRes.json() : [];
+ const hashtags = hashtagRes.ok ? await hashtagRes.json() : [];
+
+ const { users = [], locations = [], events = [] } = data;
+ const gruppenSlice = gruppen.slice(0, 3);
let html = '';
+ if (!users.length && !locations.length && !events.length && !gruppenSlice.length && !hashtags.length) {
+ html += 'Keine Ergebnisse.
';
+ }
+
if (users.length) {
html += `Personen
`;
html += users.map(u => {
@@ -213,6 +221,26 @@
}).join('');
}
+ if (gruppenSlice.length) {
+ html += `Gruppen
`;
+ html += gruppenSlice.map(g => {
+ const av = g.bild
+ ? `
`
+ : `👥`;
+ return `
+ ${av}${esc(g.name)}`;
+ }).join('');
+ }
+
+ if (hashtags.length) {
+ html += `Hashtags
`;
+ html += ``;
+ html += hashtags.map(tag =>
+ `
#${esc(tag)}`
+ ).join('');
+ html += `
`;
+ }
+
html += `Alle Ergebnisse anzeigen →`;
overlay.innerHTML = html;
} catch (e) {
diff --git a/src/main/resources/static/search.html b/src/main/resources/static/search.html
index 5d193a1..5e3c309 100644
--- a/src/main/resources/static/search.html
+++ b/src/main/resources/static/search.html
@@ -163,6 +163,33 @@
transition: border-color 0.15s, color 0.15s;
}
.search-load-more:hover { border-color: var(--color-primary); color: var(--color-primary); background: none; }
+
+ .hashtag-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.4rem 0.85rem;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 20px;
+ color: var(--color-primary);
+ font-weight: 600;
+ font-size: 0.9rem;
+ text-decoration: none;
+ transition: border-color 0.15s, background 0.15s;
+ }
+ .hashtag-chip:hover { border-color: var(--color-primary); background: var(--color-secondary); }
+
+ /* Dialog (Gruppen-Beitritt) */
+ .dialog-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:200; align-items:center; justify-content:center; }
+ .dialog-backdrop.visible { display:flex; }
+ .dialog { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:12px; padding:1.75rem; width:100%; max-width:420px; box-shadow:0 8px 32px rgba(0,0,0,0.6); max-height:90vh; overflow-y:auto; }
+ .dialog h3 { color:var(--color-primary); font-size:1.1rem; margin-bottom:1.25rem; }
+ .dialog label { display:block; font-size:0.8rem; color:#aaa; margin-bottom:0.3rem; margin-top:1rem; }
+ .dialog textarea { width:100%; padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:80px; box-sizing:border-box; }
+ .dialog textarea:focus { border-color:var(--color-primary); }
+ .dialog-actions { display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.5rem; }
+ .dialog-actions button { flex:none; margin:0; padding:0.55rem 1.1rem; font-size:0.9rem; width:auto; }
@@ -173,7 +200,7 @@
@@ -188,6 +215,12 @@
+
+
@@ -207,6 +240,31 @@
+
+
+
+
+
+
+
+
+
+
+
Beitrittsanfrage senden
+
+
+
+
+
+
+
+
@@ -279,10 +337,12 @@
document.getElementById('grid' + cap(t)).innerHTML = '';
document.getElementById('more' + cap(t)).style.display = 'none';
});
- // Alle drei Typen parallel laden
+ // Alle Typen parallel laden
loadChunk('users');
loadChunk('locations');
loadChunk('events');
+ loadGruppen(q);
+ loadHashtags(q);
}
function clearAll() {
@@ -295,6 +355,12 @@
document.getElementById('count' + cap(t)).textContent = '0';
document.getElementById('loading' + cap(t)).style.display = 'none';
});
+ document.getElementById('gridGruppen').innerHTML = '';
+ document.getElementById('countGruppen').textContent = '0';
+ document.getElementById('loadingGruppen').style.display = 'none';
+ document.getElementById('gridHashtags').innerHTML = '';
+ document.getElementById('countHashtags').textContent = '0';
+ document.getElementById('loadingHashtags').style.display = 'none';
const url = new URL(window.location);
url.searchParams.delete('q');
history.replaceState(null, '', url);
@@ -374,14 +440,185 @@
});
}
+ // ── Gruppen-Suche ──
+
+ async function loadGruppen(q) {
+ const loadingEl = document.getElementById('loadingGruppen');
+ const grid = document.getElementById('gridGruppen');
+ loadingEl.style.display = '';
+ grid.innerHTML = '';
+ try {
+ const res = await fetch('/gruppen/search?q=' + encodeURIComponent(q));
+ if (!res.ok) throw new Error();
+ const data = await res.json();
+ document.getElementById('countGruppen').textContent = data.length;
+ if (!data.length) {
+ grid.innerHTML = 'Keine Ergebnisse.
';
+ return;
+ }
+ data.forEach(g => grid.appendChild(buildGruppeCard(g)));
+ } catch (e) {
+ // ignore
+ } finally {
+ loadingEl.style.display = 'none';
+ }
+ }
+
+ function buildGruppeCard(g) {
+ const card = document.createElement('div');
+ card.className = 'search-card';
+ card.style.cssText = 'justify-content:space-between; cursor:pointer;';
+ card.addEventListener('click', () => { location.href = '/community/gruppe.html?gruppeId=' + g.gruppeId; });
+
+ const av = g.bild
+ ? ``
+ : `👥
`;
+ const privBadge = g.isPrivate ? ' 🔒' : '';
+ const sub = g.memberCount + ' Mitglied' + (g.memberCount !== 1 ? 'er' : '');
+
+ const info = document.createElement('div');
+ info.style.cssText = 'display:flex; align-items:center; gap:0.75rem; min-width:0; flex:1;';
+ info.innerHTML = `${av}${esc(g.name)}${privBadge}
${esc(sub)}
`;
+ card.appendChild(info);
+
+ if (!g.myRole) {
+ const btn = document.createElement('button');
+ btn.style.cssText = 'font-size:0.78rem; padding:0.3rem 0.65rem; width:auto; margin:0; white-space:nowrap; flex-shrink:0; margin-left:0.5rem;';
+ if (g.myRequestStatus === 'AUSSTEHEND') {
+ btn.disabled = true;
+ btn.style.opacity = '0.6';
+ btn.textContent = 'Anfrage ausstehend';
+ } else if (g.isPrivate) {
+ btn.textContent = 'Anfrage senden';
+ btn.addEventListener('click', e => { e.stopPropagation(); openSearchJoinDialog(g.gruppeId, g.name); });
+ } else {
+ btn.textContent = 'Beitreten';
+ btn.addEventListener('click', e => { e.stopPropagation(); joinGruppeSearch(g.gruppeId, btn); });
+ }
+ card.appendChild(btn);
+ }
+
+ return card;
+ }
+
+ // ── Hashtag-Suche ──
+
+ async function loadHashtags(q) {
+ const loadingEl = document.getElementById('loadingHashtags');
+ const grid = document.getElementById('gridHashtags');
+ loadingEl.style.display = '';
+ grid.innerHTML = '';
+ try {
+ const raw = q.startsWith('#') ? q.slice(1) : q;
+ const res = await fetch('/hashtags/suggest?q=' + encodeURIComponent(raw) + '&limit=20');
+ if (!res.ok) throw new Error();
+ const tags = await res.json();
+ document.getElementById('countHashtags').textContent = tags.length;
+ if (!tags.length) {
+ grid.innerHTML = 'Keine Hashtags gefunden.
';
+ return;
+ }
+ tags.forEach(tag => {
+ const a = document.createElement('a');
+ a.className = 'hashtag-chip';
+ a.href = '/community/feed.html?tag=' + encodeURIComponent(tag);
+ a.textContent = '#' + tag;
+ grid.appendChild(a);
+ });
+ } catch (e) {
+ // ignore
+ } finally {
+ loadingEl.style.display = 'none';
+ }
+ }
+
+ async function joinGruppeSearch(gruppeId, btn) {
+ btn.disabled = true;
+ btn.textContent = '…';
+ try {
+ const res = await fetch('/gruppen/' + gruppeId + '/join', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: '{}'
+ });
+ if (res.ok || res.status === 201) {
+ btn.textContent = 'Beigetreten ✓';
+ } else {
+ btn.disabled = false;
+ btn.textContent = 'Beitreten';
+ }
+ } catch (e) {
+ btn.disabled = false;
+ btn.textContent = 'Beitreten';
+ }
+ }
+
+ // ── Join-Dialog für Gruppen ──
+
+ let searchJoinGruppeId = null;
+
+ function openSearchJoinDialog(gruppeId, name) {
+ searchJoinGruppeId = gruppeId;
+ document.getElementById('searchJoinGroupName').textContent = name;
+ document.getElementById('searchJoinNachricht').value = '';
+ document.getElementById('searchJoinError').style.display = 'none';
+ document.getElementById('searchJoinDialog').classList.add('visible');
+ }
+
+ function closeSearchJoinDialog() {
+ document.getElementById('searchJoinDialog').classList.remove('visible');
+ searchJoinGruppeId = null;
+ }
+
+ async function sendSearchJoinRequest() {
+ if (!searchJoinGruppeId) return;
+ document.getElementById('searchJoinError').style.display = 'none';
+ const nachricht = document.getElementById('searchJoinNachricht').value.trim() || null;
+ try {
+ const res = await fetch('/gruppen/' + searchJoinGruppeId + '/join', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nachricht })
+ });
+ if (res.ok || res.status === 201) {
+ closeSearchJoinDialog();
+ if (currentQuery.length >= 2) loadGruppen(currentQuery);
+ } else {
+ const el = document.getElementById('searchJoinError');
+ el.textContent = 'Fehler beim Senden der Anfrage.';
+ el.style.display = 'block';
+ }
+ } catch (e) {
+ const el = document.getElementById('searchJoinError');
+ el.textContent = 'Fehler: ' + e.message;
+ el.style.display = 'block';
+ }
+ }
+
+ document.getElementById('searchJoinCancelBtn').addEventListener('click', closeSearchJoinDialog);
+ document.getElementById('searchJoinSendBtn').addEventListener('click', sendSearchJoinRequest);
+ document.getElementById('searchJoinDialog').addEventListener('click', e => {
+ if (e.target === document.getElementById('searchJoinDialog')) closeSearchJoinDialog();
+ });
+
function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
// ── URL-Parameter beim Start auslesen ──
- const initQ = new URLSearchParams(window.location.search).get('q') || '';
+ const params = new URLSearchParams(window.location.search);
+ const initQ = params.get('q') || '';
+ const initTab = params.get('tab') || '';
+
+ if (initTab) {
+ const tabBtn = document.querySelector(`.search-tab-btn[data-tab="${initTab}"]`);
+ if (tabBtn) tabBtn.click();
+ }
+
if (initQ.length >= 2) {
input.value = initQ;
- // Warten bis icons.js geladen ist
setTimeout(() => startSearch(initQ), 100);
+ } else if (initTab === 'hashtags') {
+ // Populäre Tags zeigen wenn kein Suchbegriff
+ setTimeout(() => loadHashtags(''), 100);
}
})();
diff --git a/src/main/resources/static/userhome.html b/src/main/resources/static/userhome.html
index fa94bba..fe813f9 100644
--- a/src/main/resources/static/userhome.html
+++ b/src/main/resources/static/userhome.html
@@ -243,6 +243,33 @@
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
@media (max-width:650px) { .lb-layout { flex-direction:column; height:95vh; } .lb-post-side { border-right:none; border-bottom:1px solid var(--color-secondary); max-height:55vh; } .lb-comments-panel { width:100%; } }
+ /* ── Spiel starten ── */
+ .start-game-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 0.75rem;
+ }
+ .start-game-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.6rem;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 12px;
+ padding: 1.25rem 1rem;
+ text-decoration: none;
+ color: var(--color-text);
+ transition: border-color 0.15s, background 0.15s;
+ text-align: center;
+ }
+ .start-game-card:hover {
+ border-color: var(--color-primary);
+ background: rgba(var(--color-primary-rgb,233,69,96),0.06);
+ }
+ .start-game-icon { font-size: 2rem; line-height: 1; }
+ .start-game-title { font-size: 0.9rem; font-weight: 600; }
+
/* ── Neue Mitglieder ── */
.new-members-strip {
display: flex;
@@ -306,6 +333,25 @@
+
+
+
Einladungen 📨
@@ -423,6 +469,7 @@
+