Feature: Suchfeld in Routen, Events und Places
Alle drei Seiten haben jetzt ein debounced Suchfeld (300ms) mit Lupe-Icon über der Liste. Die Suche filtert clientseitig: - Routen: Name, Beschreibung, Username (bereits vorhandener State verbessert) - Events: Titel, Ort, Beschreibung - Places: Name, Adresse, Typ Leerer Zustand zeigt passendes UI.emptyState() mit Suchbegriff. SW by-v209, APP_VER 178.
This commit is contained in:
parent
5acfa9d8f6
commit
6581a9a88c
3 changed files with 90 additions and 18 deletions
|
|
@ -35,6 +35,7 @@ window.Page_events = (() => {
|
||||||
let _events = [];
|
let _events = [];
|
||||||
let _filter = 'alle';
|
let _filter = 'alle';
|
||||||
let _quellFilter = 'alle'; // 'alle' | 'vdh' | 'nutzer'
|
let _quellFilter = 'alle'; // 'alle' | 'vdh' | 'nutzer'
|
||||||
|
let _search = '';
|
||||||
let _view = 'liste'; // liste | karte
|
let _view = 'liste'; // liste | karte
|
||||||
let _map = null;
|
let _map = null;
|
||||||
let _markers = [];
|
let _markers = [];
|
||||||
|
|
@ -78,6 +79,12 @@ window.Page_events = (() => {
|
||||||
${_state.user ? `<button class="btn btn-primary btn-sm" id="ev-new-btn">${UI.icon('plus')} Event</button>` : ''}
|
${_state.user ? `<button class="btn btn-primary btn-sm" id="ev-new-btn">${UI.icon('plus')} Event</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="diary-search-wrap" style="margin:var(--space-2) var(--space-3) 0" id="ev-search-wrap">
|
||||||
|
<svg class="ph-icon diary-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||||||
|
<input type="search" class="diary-search-input" id="ev-search"
|
||||||
|
placeholder="Events durchsuchen…" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="events-filter-bar by-tabs" id="ev-filter-bar">
|
<div class="events-filter-bar by-tabs" id="ev-filter-bar">
|
||||||
${TYPEN.map(t => `
|
${TYPEN.map(t => `
|
||||||
<button class="by-tab ${t.id === 'alle' ? 'active' : ''}" data-ev-typ="${t.id}">
|
<button class="by-tab ${t.id === 'alle' ? 'active' : ''}" data-ev-typ="${t.id}">
|
||||||
|
|
@ -99,6 +106,16 @@ window.Page_events = (() => {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
_container.addEventListener('click', _onClick);
|
_container.addEventListener('click', _onClick);
|
||||||
|
|
||||||
|
// Suche mit Debounce
|
||||||
|
let _searchTimer = null;
|
||||||
|
document.getElementById('ev-search')?.addEventListener('input', e => {
|
||||||
|
clearTimeout(_searchTimer);
|
||||||
|
_searchTimer = setTimeout(() => {
|
||||||
|
_search = e.target.value.trim().toLowerCase();
|
||||||
|
if (_view === 'karte') { _renderMap(_filtered()); } else { _renderList(); }
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -124,6 +141,14 @@ window.Page_events = (() => {
|
||||||
if (_quellFilter !== 'alle') {
|
if (_quellFilter !== 'alle') {
|
||||||
evs = evs.filter(e => (e.quelle || 'nutzer') === _quellFilter);
|
evs = evs.filter(e => (e.quelle || 'nutzer') === _quellFilter);
|
||||||
}
|
}
|
||||||
|
if (_search) {
|
||||||
|
const q = _search;
|
||||||
|
evs = evs.filter(e =>
|
||||||
|
(e.titel || '').toLowerCase().includes(q) ||
|
||||||
|
(e.ort_name || '').toLowerCase().includes(q) ||
|
||||||
|
(e.beschreibung|| '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
return evs;
|
return evs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,8 +163,10 @@ window.Page_events = (() => {
|
||||||
if (!filtered.length) {
|
if (!filtered.length) {
|
||||||
listEl.innerHTML = UI.emptyState({
|
listEl.innerHTML = UI.emptyState({
|
||||||
icon: UI.icon('calendar-blank'),
|
icon: UI.icon('calendar-blank'),
|
||||||
title: 'Keine Events in der Nähe',
|
title: _search ? 'Keine Events gefunden' : 'Keine Events in der Nähe',
|
||||||
text: 'Hier erscheinen Hundeveranstaltungen, Treffen und Aktivitäten in deiner Umgebung.',
|
text: _search
|
||||||
|
? `Keine Events passen zu „${UI.escape(_search)}".`
|
||||||
|
: 'Hier erscheinen Hundeveranstaltungen, Treffen und Aktivitäten in deiner Umgebung.',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ window.Page_places = (() => {
|
||||||
let _markers = [];
|
let _markers = [];
|
||||||
let _data = [];
|
let _data = [];
|
||||||
let _activeTyp = null; // null = alle
|
let _activeTyp = null; // null = alle
|
||||||
|
let _search = '';
|
||||||
let _userPos = null;
|
let _userPos = null;
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -61,6 +62,13 @@ window.Page_places = (() => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Suche -->
|
||||||
|
<div class="diary-search-wrap" style="margin:var(--space-2) var(--space-3) 0" id="places-search-wrap">
|
||||||
|
<svg class="ph-icon diary-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||||||
|
<input type="search" class="diary-search-input" id="places-search"
|
||||||
|
placeholder="Orte durchsuchen…" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Karte -->
|
<!-- Karte -->
|
||||||
<div id="places-map" class="places-map"></div>
|
<div id="places-map" class="places-map"></div>
|
||||||
|
|
||||||
|
|
@ -95,6 +103,16 @@ window.Page_places = (() => {
|
||||||
_showForm(null);
|
_showForm(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Suche mit Debounce
|
||||||
|
let _searchTimer = null;
|
||||||
|
document.getElementById('places-search')?.addEventListener('input', e => {
|
||||||
|
clearTimeout(_searchTimer);
|
||||||
|
_searchTimer = setTimeout(() => {
|
||||||
|
_search = e.target.value.trim().toLowerCase();
|
||||||
|
_applyFilter();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
UI.loadLeaflet().then(_initMap);
|
UI.loadLeaflet().then(_initMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,7 +171,16 @@ window.Page_places = (() => {
|
||||||
// Filter anwenden
|
// Filter anwenden
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
function _filtered() {
|
function _filtered() {
|
||||||
return _activeTyp ? _data.filter(p => p.typ === _activeTyp) : _data;
|
let list = _activeTyp ? _data.filter(p => p.typ === _activeTyp) : _data;
|
||||||
|
if (_search) {
|
||||||
|
const q = _search;
|
||||||
|
list = list.filter(p =>
|
||||||
|
(p.name || '').toLowerCase().includes(q) ||
|
||||||
|
(p.adresse|| '').toLowerCase().includes(q) ||
|
||||||
|
(p.typ || '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _applyFilter() {
|
function _applyFilter() {
|
||||||
|
|
@ -187,11 +214,12 @@ window.Page_places = (() => {
|
||||||
const items = _filtered();
|
const items = _filtered();
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
|
const msg = _search
|
||||||
|
? `Keine Orte gefunden für „${UI.escape(_search)}".`
|
||||||
|
: (_activeTyp ? 'Keine Orte in dieser Kategorie.' : 'Noch keine Orte eingetragen.');
|
||||||
list.innerHTML = `
|
list.innerHTML = `
|
||||||
<div class="places-list-inner">
|
<div class="places-list-inner">
|
||||||
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">
|
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">${msg}</p>
|
||||||
${_activeTyp ? 'Keine Orte in dieser Kategorie.' : 'Noch keine Orte eingetragen.'}
|
|
||||||
</p>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,11 @@ window.Page_routes = (() => {
|
||||||
<button class="rk-mode-btn${_browseMode==='discover'?' active':''}" id="rk-mode-discover">${UI.icon('compass')} Entdecken</button>
|
<button class="rk-mode-btn${_browseMode==='discover'?' active':''}" id="rk-mode-discover">${UI.icon('compass')} Entdecken</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="rk-search-row">
|
<div class="rk-search-row">
|
||||||
<input class="rk-search" id="rk-search" type="search"
|
<div class="diary-search-wrap" style="flex:1;min-width:0">
|
||||||
placeholder="🔍 Route suchen…" autocomplete="off">
|
<svg class="ph-icon diary-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||||||
|
<input class="diary-search-input" id="rk-search" type="search"
|
||||||
|
placeholder="Routen durchsuchen…" autocomplete="off">
|
||||||
|
</div>
|
||||||
<div class="rk-view-toggle">
|
<div class="rk-view-toggle">
|
||||||
<button class="rk-view-btn${_viewMode==='list'?' active':''}" id="rk-view-list" title="Liste">${UI.icon('list')}</button>
|
<button class="rk-view-btn${_viewMode==='list'?' active':''}" id="rk-view-list" title="Liste">${UI.icon('list')}</button>
|
||||||
<button class="rk-view-btn${_viewMode==='map'?' active':''}" id="rk-view-map" title="Karte">${UI.icon('map-trifold')}</button>
|
<button class="rk-view-btn${_viewMode==='map'?' active':''}" id="rk-view-map" title="Karte">${UI.icon('map-trifold')}</button>
|
||||||
|
|
@ -118,8 +121,13 @@ window.Page_routes = (() => {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
let _searchTimer = null;
|
||||||
document.getElementById('rk-search').addEventListener('input', e => {
|
document.getElementById('rk-search').addEventListener('input', e => {
|
||||||
_search = e.target.value.toLowerCase(); _applyFilter();
|
clearTimeout(_searchTimer);
|
||||||
|
_searchTimer = setTimeout(() => {
|
||||||
|
_search = e.target.value.toLowerCase().trim();
|
||||||
|
_applyFilter();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
document.getElementById('rk-view-list').addEventListener('click', () => _switchView('list'));
|
document.getElementById('rk-view-list').addEventListener('click', () => _switchView('list'));
|
||||||
document.getElementById('rk-view-map').addEventListener('click', () => _switchView('map'));
|
document.getElementById('rk-view-map').addEventListener('click', () => _switchView('map'));
|
||||||
|
|
@ -426,9 +434,12 @@ window.Page_routes = (() => {
|
||||||
if (!_filtered.length) {
|
if (!_filtered.length) {
|
||||||
if (_data.length) {
|
if (_data.length) {
|
||||||
// Filter aktiv aber kein Ergebnis
|
// Filter aktiv aber kein Ergebnis
|
||||||
|
const emptyMsg = _search
|
||||||
|
? `Keine Routen gefunden für „${UI.escape(_search)}".`
|
||||||
|
: 'Keine Routen passen zu deinen Filtern.';
|
||||||
grid.innerHTML = `<div class="rk-empty">
|
grid.innerHTML = `<div class="rk-empty">
|
||||||
<div class="rk-empty-icon">🔍</div>
|
<div class="rk-empty-icon">🔍</div>
|
||||||
<p>Keine Routen passen zu deinen Filtern.</p>
|
<p>${emptyMsg}</p>
|
||||||
<button class="btn btn-secondary" id="rk-empty-reset">Filter zurücksetzen</button>
|
<button class="btn btn-secondary" id="rk-empty-reset">Filter zurücksetzen</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.getElementById('rk-empty-reset')?.addEventListener('click', () => {
|
document.getElementById('rk-empty-reset')?.addEventListener('click', () => {
|
||||||
|
|
@ -667,14 +678,20 @@ window.Page_routes = (() => {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const footer = `
|
const footer = `
|
||||||
<button type="button" class="btn btn-secondary" id="rd-gpx">${UI.icon('download-simple')} GPX</button>
|
<div style="display:flex;gap:var(--space-2);width:100%;flex-wrap:wrap">
|
||||||
<button type="button" class="btn btn-secondary" id="rd-share" title="Route teilen">${UI.icon('share')}</button>
|
<div style="display:flex;gap:var(--space-2);flex:1">
|
||||||
<button type="button" class="btn btn-secondary" id="rd-send-friend" title="An Freund senden">${UI.icon('chat-circle-dots')} An Freund senden</button>
|
<button type="button" class="btn btn-secondary btn-sm" id="rd-gpx" title="GPX herunterladen">${UI.icon('download-simple')} GPX</button>
|
||||||
${isOwn ? `<button type="button" class="btn btn-ghost" id="rd-vis" title="${route.is_public?'Privat machen':'Öffentlich machen'}">
|
<button type="button" class="btn btn-secondary btn-sm" id="rd-share" title="Route teilen">${UI.icon('share')}</button>
|
||||||
${route.is_public ? UI.icon('lock')+' Privat' : UI.icon('globe')+' Öffentlich'}
|
<button type="button" class="btn btn-secondary btn-sm" id="rd-send-friend" title="An Freund senden">${UI.icon('chat-circle-dots')}</button>
|
||||||
</button>
|
${isOwn ? `
|
||||||
<button type="button" class="btn btn-ghost" id="rd-del" style="color:var(--c-danger)">${UI.icon('trash')}</button>` : ''}
|
<button type="button" class="btn btn-secondary btn-sm" id="rd-vis">
|
||||||
<button type="button" class="btn btn-primary flex-1" id="rd-close">Schließen</button>
|
${route.is_public ? UI.icon('lock')+' Privat' : UI.icon('globe')+' Öffentlich'}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" id="rd-del" style="color:var(--c-danger)" title="Route löschen">${UI.icon('trash')} Löschen</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" id="rd-close">Schließen</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
UI.modal.open({ title: `🥾 ${UI.escape(route.name)}`, body, footer });
|
UI.modal.open({ title: `🥾 ${UI.escape(route.name)}`, body, footer });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue