Menp überarbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-08 16:52:43 +02:00
parent a13b8e894f
commit 2b0ce62d33
124 changed files with 2306 additions and 148 deletions

389
bin/main/static/search.html Normal file
View File

@@ -0,0 +1,389 @@
<!DOCTYPE html>
<html lang="de">
<head>
<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>Suche xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.search-hero {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.search-hero-input-wrap {
flex: 1;
position: relative;
display: flex;
align-items: center;
}
.search-hero-icon {
position: absolute;
left: 0.85rem;
color: var(--color-muted);
display: flex;
align-items: center;
pointer-events: none;
}
#searchInput {
width: 100%;
padding: 0.65rem 1rem 0.65rem 2.6rem;
border: 1px solid var(--color-secondary);
border-radius: 8px;
background: var(--color-card);
color: var(--color-text);
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
box-sizing: border-box;
}
#searchInput:focus { border-color: var(--color-primary); }
.search-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--color-secondary);
margin-bottom: 1.25rem;
}
.search-tab-btn {
background: none;
border: none;
border-bottom: 3px solid transparent;
border-radius: 0;
padding: 0.55rem 1.1rem;
font-size: 0.9rem;
font-weight: 600;
color: var(--color-muted);
cursor: pointer;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
}
.search-tab-btn:hover { color: var(--color-text); background: none; }
.search-tab-btn.active { color: var(--color-primary); border-bottom-color: var(--color-primary); }
.search-tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--color-secondary);
color: var(--color-muted);
border-radius: 10px;
font-size: 0.7rem;
font-weight: 700;
min-width: 1.2em;
padding: 0 0.35em;
margin-left: 0.3em;
vertical-align: middle;
}
.search-tab-btn.active .search-tab-count {
background: var(--color-primary);
color: #fff;
}
.search-panel { display: none; }
.search-panel.active { display: block; }
.search-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.85rem;
}
.search-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 0.85rem;
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: var(--color-text);
transition: border-color 0.15s;
min-width: 0;
}
.search-card:hover { border-color: var(--color-primary); }
.search-card-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
object-fit: cover;
background: var(--color-secondary);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
overflow: hidden;
}
.search-card-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.search-card-avatar--square { border-radius: 8px; }
.search-card-avatar--square img { border-radius: 6px; }
.search-card-body { min-width: 0; }
.search-card-name {
font-size: 0.9rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-card-sub {
font-size: 0.75rem;
color: var(--color-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-empty {
text-align: center;
padding: 2.5rem 1rem;
color: var(--color-muted);
font-size: 0.9rem;
}
.search-loading {
text-align: center;
padding: 2rem 1rem;
color: var(--color-muted);
font-size: 0.9rem;
}
.search-load-more {
display: block;
width: 100%;
margin-top: 1rem;
padding: 0.6rem;
background: none;
border: 1px solid var(--color-secondary);
border-radius: 8px;
color: var(--color-muted);
cursor: pointer;
font-size: 0.88rem;
transition: border-color 0.15s, color 0.15s;
}
.search-load-more:hover { border-color: var(--color-primary); color: var(--color-primary); background: none; }
</style>
</head>
<body>
<div class="main">
<div class="content">
<h1 style="margin-bottom:1rem;">Suche</h1>
<div class="search-hero">
<div class="search-hero-input-wrap">
<span class="search-hero-icon" id="searchIcon"></span>
<input type="text" id="searchInput" placeholder="Suchen nach Personen, Locations, Veranstaltungen…"
autocomplete="off" spellcheck="false">
</div>
</div>
<div class="search-tabs">
<button class="search-tab-btn active" data-tab="users">
Personen <span class="search-tab-count" id="countUsers">0</span>
</button>
<button class="search-tab-btn" data-tab="locations">
Locations <span class="search-tab-count" id="countLocations">0</span>
</button>
<button class="search-tab-btn" data-tab="events">
Veranstaltungen <span class="search-tab-count" id="countEvents">0</span>
</button>
</div>
<div class="search-panel active" id="panel-users">
<div class="search-loading" id="loadingUsers" style="display:none;">Wird geladen…</div>
<div class="search-grid" id="gridUsers"></div>
<button class="search-load-more" id="moreUsers" style="display:none;">Mehr laden</button>
</div>
<div class="search-panel" id="panel-locations">
<div class="search-loading" id="loadingLocations" style="display:none;">Wird geladen…</div>
<div class="search-grid" id="gridLocations"></div>
<button class="search-load-more" id="moreLocations" style="display:none;">Mehr laden</button>
</div>
<div class="search-panel" id="panel-events">
<div class="search-loading" id="loadingEvents" style="display:none;">Wird geladen…</div>
<div class="search-grid" id="gridEvents"></div>
<button class="search-load-more" id="moreEvents" style="display:none;">Mehr laden</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
(function () {
const PAGE_SIZE = 24;
function esc(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function IC(key) { return window.IC ? window.IC(key) : (window.ICONS?.[key]?.value || ''); }
// ── Suchicon setzen ──
document.addEventListener('DOMContentLoaded', () => {
const icon = document.getElementById('searchIcon');
if (icon) icon.innerHTML = IC('SEARCH') || '🔍';
});
setTimeout(() => {
const icon = document.getElementById('searchIcon');
if (icon && !icon.innerHTML) icon.innerHTML = IC('SEARCH') || '🔍';
}, 200);
// ── State ──
const state = {
users: { offset: 0, total: 0, loading: false },
locations: { offset: 0, total: 0, loading: false },
events: { offset: 0, total: 0, loading: false }
};
let currentQuery = '';
let debounceTimer;
// ── Tabs ──
document.querySelectorAll('.search-tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.search-tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.search-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
});
});
// ── "Mehr laden" Buttons ──
document.getElementById('moreUsers').addEventListener('click', () => loadMore('users'));
document.getElementById('moreLocations').addEventListener('click', () => loadMore('locations'));
document.getElementById('moreEvents').addEventListener('click', () => loadMore('events'));
// ── Suche starten ──
const input = document.getElementById('searchInput');
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
const q = input.value.trim();
if (q.length < 2) { clearAll(); return; }
debounceTimer = setTimeout(() => startSearch(q), 300);
});
function startSearch(q) {
currentQuery = q;
// URL aktualisieren
const url = new URL(window.location);
url.searchParams.set('q', q);
history.replaceState(null, '', url);
// State zurücksetzen
['users', 'locations', 'events'].forEach(t => {
state[t].offset = 0;
state[t].total = 0;
document.getElementById('grid' + cap(t)).innerHTML = '';
document.getElementById('more' + cap(t)).style.display = 'none';
});
// Alle drei Typen parallel laden
loadChunk('users');
loadChunk('locations');
loadChunk('events');
}
function clearAll() {
currentQuery = '';
['users', 'locations', 'events'].forEach(t => {
state[t].offset = 0;
state[t].total = 0;
document.getElementById('grid' + cap(t)).innerHTML = '';
document.getElementById('more' + cap(t)).style.display = 'none';
document.getElementById('count' + cap(t)).textContent = '0';
document.getElementById('loading' + cap(t)).style.display = 'none';
});
const url = new URL(window.location);
url.searchParams.delete('q');
history.replaceState(null, '', url);
}
function loadMore(type) {
if (state[type].loading) return;
loadChunk(type);
}
async function loadChunk(type) {
if (!currentQuery || state[type].loading) return;
state[type].loading = true;
const loadingEl = document.getElementById('loading' + cap(type));
const moreBtn = document.getElementById('more' + cap(type));
loadingEl.style.display = '';
moreBtn.style.display = 'none';
try {
const url = `/search?q=${encodeURIComponent(currentQuery)}&limit=${PAGE_SIZE}&offset=${state[type].offset}&type=${type}`;
const res = await fetch(url);
if (!res.ok) throw new Error();
const data = await res.json();
const items = data[type] || [];
state[type].offset += items.length;
state[type].total = data.total?.[type] ?? (items.length < PAGE_SIZE ? state[type].offset : state[type].offset + 1);
appendItems(type, items);
// Zähler aktualisieren
document.getElementById('count' + cap(type)).textContent = state[type].offset;
// "Mehr laden" zeigen wenn noch mehr vorhanden
moreBtn.style.display = items.length === PAGE_SIZE ? '' : 'none';
} catch (e) {
// ignore
} finally {
state[type].loading = false;
loadingEl.style.display = 'none';
}
}
function appendItems(type, items) {
const grid = document.getElementById('grid' + cap(type));
if (!items.length && state[type].offset === 0) {
if (!grid.innerHTML) {
grid.innerHTML = `<div class="search-empty" style="grid-column:1/-1;">Keine Ergebnisse.</div>`;
}
return;
}
items.forEach(item => {
const card = document.createElement('a');
if (type === 'users') {
card.href = `/community/benutzer.html?userId=${esc(item.userId)}`;
const av = item.profilePicture
? `<div class="search-card-avatar"><img src="data:image/png;base64,${esc(item.profilePicture)}" alt=""></div>`
: `<div class="search-card-avatar">${IC('PROFILE') || '👤'}</div>`;
card.innerHTML = `${av}<div class="search-card-body"><div class="search-card-name">${esc(item.name)}</div></div>`;
} else if (type === 'locations') {
card.href = `/community/location-detail.html?id=${esc(item.locationId)}`;
const av = item.profilePicture
? `<div class="search-card-avatar search-card-avatar--square"><img src="data:image/png;base64,${esc(item.profilePicture)}" alt=""></div>`
: `<div class="search-card-avatar search-card-avatar--square">📍</div>`;
card.innerHTML = `${av}<div class="search-card-body"><div class="search-card-name">${esc(item.name)}</div></div>`;
} else {
card.href = `/community/event-detail.html?eventId=${esc(item.eventId)}`;
const av = item.imageData
? `<div class="search-card-avatar search-card-avatar--square"><img src="${esc(item.imageData)}" alt=""></div>`
: `<div class="search-card-avatar search-card-avatar--square">🗓</div>`;
const sub = item.startAt ? new Date(item.startAt).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }) : '';
card.innerHTML = `${av}<div class="search-card-body"><div class="search-card-name">${esc(item.title)}</div><div class="search-card-sub">${esc(sub)}</div></div>`;
}
card.className = 'search-card';
grid.appendChild(card);
});
}
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') || '';
if (initQ.length >= 2) {
input.value = initQ;
// Warten bis icons.js geladen ist
setTimeout(() => startSearch(initQ), 100);
}
})();
</script>
</body>
</html>