Phase 4: Filter- und Suchfunktion

Neues Modul js/filters/ mit reiner Filterlogik (filterEngine.js:
applyFilters(), getDistanceKm()) und UI (filterPanel.js) fuer alle Filter
aus anforderung.md Abschnitt 3: Kategorie, Entfernung (mit "Standort
verwenden"-Button), Alter der Kinder, Kosten, Indoor/Outdoor, 5
Ausstattungsmerkmale, Aufenthaltsdauer, Mindestbewertung sowie
Freitextsuche ueber Name/Ort/Region/Adresse. Trefferzaehler, "Filter
zuruecksetzen" und "Auf Ergebnisse zoomen" ergaenzt.

Store erweitert um updateFilters()/getFilteredPlaces() (Selector).
Karte und Liste rendern jetzt die gefilterte statt die volle
Ausflugsziel-Liste; die Karten-Rerender-Optimierung vergleicht dafuer
die Trefferliste per Id-Schluessel statt nur state.places, damit reine
Filter-/Standortaenderungen ohne Places-Aenderung erkannt werden.

Layout: Filter-Panel als aufklappbares <details> auf Mobile, eigener
Grid-Bereich neben der Karte auf Desktop. docs/plan.md entsprechend
fortgeschrieben (Phase 2-5 als umgesetzt markiert, Phase 5
Navigation war bereits Teil von Phase 2).
This commit is contained in:
2026-07-24 12:21:07 +02:00
parent 10f9170d61
commit dac952fbf9
12 changed files with 843 additions and 42 deletions
+20 -9
View File
@@ -4,7 +4,7 @@
(function () {
const { DEFAULT_MAP_CENTER } = EventMap.config;
const { subscribe, getState, setState } = EventMap.store;
const { subscribe, setState, getFilteredPlaces } = EventMap.store;
const { getPlaceLatLng } = EventMap.models;
const { createMarkerForPlace } = EventMap.map;
@@ -12,9 +12,14 @@
let clusterGroup = null;
const markersByPlaceId = new Map();
let lastPlacesRef = null;
let lastFilteredKey = null;
let lastSelectedPlaceId = null;
/** Günstiger Vergleichsschlüssel für eine gefilterte Trefferliste (Reihenfolge + IDs). */
function computeFilteredKey(places) {
return places.map((place) => place.id).join('|');
}
function hasValidCoords(place) {
const { lat, lng } = getPlaceLatLng(place);
return typeof lat === 'number' && typeof lng === 'number' && !Number.isNaN(lat) && !Number.isNaN(lng);
@@ -101,15 +106,21 @@
clusterGroup = L.markerClusterGroup();
map.addLayer(clusterGroup);
const initialState = getState();
renderMarkers(initialState.places);
fitToPlaces(initialState.places);
lastPlacesRef = initialState.places;
const initialFiltered = getFilteredPlaces();
renderMarkers(initialFiltered);
fitToPlaces(initialFiltered);
lastFilteredKey = computeFilteredKey(initialFiltered);
subscribe((state) => {
if (state.places !== lastPlacesRef) {
lastPlacesRef = state.places;
renderMarkers(state.places);
// Filter/Standort können sich ändern, ohne dass sich state.places
// ändert - daher bei jeder Store-Änderung neu filtern und nur bei
// tatsächlich geänderter Trefferliste neu rendern (vermeidet
// unnötiges Marker-Flackern z.B. bei jedem Freitext-Tastendruck).
const filtered = getFilteredPlaces();
const key = computeFilteredKey(filtered);
if (key !== lastFilteredKey) {
lastFilteredKey = key;
renderMarkers(filtered);
}
if (state.selectedPlaceId !== lastSelectedPlaceId) {
lastSelectedPlaceId = state.selectedPlaceId;