Files
xxx-sphere-web/bin/main/static/search.html
Mario 2b0ce62d33
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Menp überarbeitet
2026-04-08 16:52:43 +02:00

390 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>