From dac952fbf947c6aafe2f4f614b74192d7bd3b111 Mon Sep 17 00:00:00 2001 From: Hermann Date: Fri, 24 Jul 2026 12:21:07 +0200 Subject: [PATCH] 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
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). --- css/components.css | 6 + css/filters.css | 183 +++++++++++++++++ css/responsive.css | 12 ++ docs/plan.md | 35 +++- index.html | 5 + js/filters/filterEngine.js | 130 ++++++++++++ js/filters/filterPanel.js | 395 +++++++++++++++++++++++++++++++++++++ js/main.js | 3 +- js/map/mapController.js | 29 ++- js/store/store.js | 22 +++ js/ui/list.js | 64 +++--- js/utils/dom.js | 1 + 12 files changed, 843 insertions(+), 42 deletions(-) create mode 100644 css/filters.css create mode 100644 js/filters/filterEngine.js create mode 100644 js/filters/filterPanel.js diff --git a/css/components.css b/css/components.css index 22139f3..055b088 100644 --- a/css/components.css +++ b/css/components.css @@ -98,6 +98,12 @@ line-height: 1.4; } +.place-card__distance { + color: var(--color-text-muted); + font-size: 0.8rem; + font-weight: 600; +} + /* Karten-Marker (L.divIcon) */ .eventmap-marker { diff --git a/css/filters.css b/css/filters.css new file mode 100644 index 0000000..2417887 --- /dev/null +++ b/css/filters.css @@ -0,0 +1,183 @@ +/* Filter- und Suchpanel (Phase 4). Struktur wird von + js/filters/filterPanel.js in #filter-panel eingehängt. + + Mobile:
/ als einfaches, JS-freies Auf-/Zuklappen. + Desktop (>=1024px, siehe css/responsive.css): immer sichtbar/offen. */ + +.filter-panel { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); +} + +.filter-panel__details { + padding: 0 var(--space-md); +} + +.filter-panel__details[open] { + padding-bottom: var(--space-md); +} + +.filter-panel__summary { + padding: var(--space-md) 0; + font-weight: 700; + cursor: pointer; + list-style: none; +} + +.filter-panel__summary::-webkit-details-marker { + display: none; +} + +.filter-panel__summary::before { + content: '▸ '; +} + +.filter-panel__details[open] > .filter-panel__summary::before { + content: '▾ '; +} + +.filter-panel__body { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.filter-panel__section { + display: flex; + flex-direction: column; + gap: var(--space-sm); + border: none; + border-top: 1px solid var(--color-border); + padding: var(--space-sm) 0 0; + margin: 0; +} + +.filter-panel__section-label { + padding: 0; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.02em; + color: var(--color-text-muted); +} + +.filter-panel__field { + display: flex; + flex-direction: column; + gap: var(--space-xs); + flex: 1 1 160px; +} + +.filter-panel__label { + font-size: 0.85rem; + font-weight: 600; +} + +.filter-panel__field input, +.filter-panel__field select, +.filter-panel__distance input, +.filter-panel__distance select { + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: var(--space-sm); + font: inherit; + background: var(--color-surface); + color: var(--color-text); + width: 100%; +} + +.filter-panel__row { + display: flex; + flex-wrap: wrap; + gap: var(--space-md); +} + +.filter-panel__categories { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm) var(--space-md); + max-height: 180px; + overflow-y: auto; + padding: var(--space-xs) 0; +} + +.filter-panel__category-option, +.filter-panel__checkbox { + display: flex; + align-items: center; + gap: var(--space-xs); + font-size: 0.85rem; + cursor: pointer; +} + +.filter-panel__category-swatch { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.filter-panel__checkbox-group { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm) var(--space-md); +} + +.filter-panel__distance { + display: flex; + flex-direction: column; + gap: var(--space-xs); + max-width: 220px; +} + +.filter-panel__hint { + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.filter-panel__hint[hidden] { + display: none; +} + +.filter-panel__location-btn[hidden] { + display: none; +} + +.filter-panel__actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-sm) var(--space-md); + padding-top: var(--space-md); + border-top: 1px solid var(--color-border); +} + +.filter-panel__count { + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-muted); + margin-right: auto; +} + +@media (min-width: 1024px) { + .filter-panel { + border-bottom: none; + overflow-y: auto; + } + + .filter-panel__summary { + cursor: default; + } + + /* Details-Inhalt unabhängig vom open-Attribut immer anzeigen, damit das + Panel auf Desktop nicht eingeklappt werden kann/muss (überschreibt die + UA-Default-Regel "details:not([open]) > *:not(summary){display:none}"). */ + .filter-panel__body { + display: grid !important; + grid-template-columns: repeat(2, 1fr); + } + + .filter-panel__actions { + grid-column: 1 / -1; + } +} diff --git a/css/responsive.css b/css/responsive.css index dbb77e0..91c7bb1 100644 --- a/css/responsive.css +++ b/css/responsive.css @@ -4,15 +4,27 @@ .app-layout { display: grid; grid-template-columns: 65% 35%; + grid-template-rows: auto 1fr; + grid-template-areas: + 'map filter' + 'map list'; height: calc(100vh - var(--header-height)); } .map-panel { + grid-area: map; height: 100%; min-height: 0; } + .filter-panel { + grid-area: filter; + border-left: 1px solid var(--color-border); + max-height: 45vh; + } + .list-panel { + grid-area: list; height: 100%; overflow-y: auto; border-left: 1px solid var(--color-border); diff --git a/docs/plan.md b/docs/plan.md index 78c482e..8c70575 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -156,13 +156,32 @@ Seed-Daten dadurch synchron direkt aus dem globalen Namespace, ganz ohne |---|---|---| | 0 | Scaffold (Ordnerstruktur, vendored Leaflet, Grundgerüst) | ✅ umgesetzt | | 1 | Datenmodell, Karte, Liste (read-only, Seed-Daten) | ✅ umgesetzt | -| 2 | Detailansicht (Titelbild, vollständige Beschreibung, Mini-Karte, Navigation-Button, Bearbeiten/Löschen) | offen | -| 3 | Formular zum manuellen Anlegen/Bearbeiten (Validierung, Koordinatenprüfung) | offen | -| 4 | Filter- und Suchfunktion (Kategorie, Entfernung, Alter, Kosten, Indoor/Outdoor, Ausstattung, Bewertung, Region) inkl. Anwendung auf Karte + Liste | offen | -| 5 | Route/Navigation-Integration (z.B. Link zu externem Kartendienst) | offen | +| 2 | Detailansicht (Titelbild, vollständige Beschreibung, Mini-Karte, Navigation-Button, Bearbeiten/Löschen) | ✅ umgesetzt | +| 3 | Formular zum manuellen Anlegen/Bearbeiten (Validierung, Koordinatenprüfung) | ✅ umgesetzt | +| 4 | Filter- und Suchfunktion (Kategorie, Entfernung, Alter, Kosten, Indoor/Outdoor, Ausstattung, Bewertung, Region) inkl. Anwendung auf Karte + Liste | ✅ umgesetzt | +| 5 | Route/Navigation-Integration | ✅ bereits in Phase 2 erledigt (OSM-Directions-Button in der Detailansicht) | | 6 | KI-Prompt-Generator (Eingabeformular, Promptgenerierung, Copy/Download) | offen | -| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) | offen | +| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import + Politur | offen | -Nicht Teil von Phase 0/1 und daher bewusst noch nicht angelegt: -Filter-Modul, Detail-Modul, Formular-Modul, Prompt-Generator-Modul, -Import-Modul. +## Filter- und Suchfunktion (Phase 4) + +- `js/filters/filterEngine.js`: reine Funktion `applyFilters(places, filters, userLocation)` + plus `getDistanceKm()`. Leerer/`null`/`[]`-Wert je Filterfeld = kein Filter aktiv. + Places ohne Bewertung/Dauer werden bei aktivem Bewertungs-/Dauer-Filter nicht + automatisch ausgeschlossen; Places ohne gültige Koordinaten werden bei aktivem + Entfernungsfilter (mit vorhandenem Standort) ausgeschlossen, da ihre Entfernung + nicht verifizierbar ist. Fehlt der Nutzerstandort komplett, wird der + Entfernungsfilter ignoriert statt die Liste leerzuräumen. +- `js/filters/filterPanel.js`: UI für alle Filter aus `anforderung.md` Abschnitt 3 + (Kategorie, Entfernung inkl. "Standort verwenden"-Button, Alter, Kosten, + Indoor/Outdoor, 5 Ausstattungsmerkmale, Aufenthaltsdauer, Mindestbewertung, + Freitextsuche über Name/Ort/Region/Adresse), Trefferzähler "X von Y", + "Filter zurücksetzen" und "Auf Ergebnisse zoomen". +- Store: `EventMap.store.updateFilters(patch)` und `EventMap.store.getFilteredPlaces()` + (Selector). `js/ui/list.js` und `js/map/mapController.js` rendern seitdem die + gefilterte statt die volle Liste. +- Layout: Mobile (< 1024px) als `
/` einklappbar oberhalb der + Liste; Desktop (≥ 1024px) als eigener Grid-Bereich neben der Karte, über der + Liste (`css/responsive.css`). +- Nicht umgesetzt (bewusst kein Muss laut Plan): Persistierung der Filter in + `settings.lastFilters`. diff --git a/index.html b/index.html index b9e6d27..961d66f 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,7 @@ + @@ -40,6 +41,8 @@ +
+