Neues Modul js/ai/promptBuilder.js: reine Funktion buildPrompt(), deckt alle 10 Pflichtpunkte aus anforderung.md explizit ab (Region+Radius, Kategorien, Alter, wichtige Eigenschaften, Anzahl Ergebnisse, keine erfundenen Informationen, unsichere Angaben kennzeichnen, ausschliesslich gueltiges JSON, genaue Koordinaten, direkte Importierbarkeit) und bettet das exakte JSON-Zielschema aus anforderung.md unveraendert ein. UI (js/ui/promptGenerator.js): Modal mit allen Eingabefeldern aus der Anforderung (Region, Radius, Kategorien, Altersgruppe, Indoor/Outdoor, Kostenrahmen, max. Ergebnisse, besondere Anforderungen als Checkboxen + Freitext, Ausgabesprache), Live-Vorschau des generierten Prompts, Copy-to-Clipboard mit execCommand-Fallback (js/utils/clipboard.js) und Speichern als Textdatei (js/utils/download.js). Neuer Toolbar-Button oeffnet den Generator. JSON-Import der KI-Antwort ist bewusst nicht Teil dieser Phase, sondern folgt in Phase 7. docs/plan.md entsprechend fortgeschrieben.
207 lines
11 KiB
Markdown
207 lines
11 KiB
Markdown
# EventMap – Gesamtplan
|
||
|
||
Dieses Dokument fasst den vom architect-Agenten erarbeiteten Plan zusammen
|
||
und wird phasenweise fortgeschrieben. Aktuell umgesetzt: **Phase 0 (Scaffold)**
|
||
und **Phase 1 (Datenmodell + Karte + Liste, read-only mit Beispieldaten)**.
|
||
|
||
## Tech-Stack
|
||
|
||
- Vanilla HTML5/CSS3/JavaScript, klassische Scripts (kein `type="module"`);
|
||
Module-Ersatz über einen globalen Namespace `window.EventMap` +
|
||
IIFE-Kapselung pro Datei, siehe `docs/adr/0001-vanilla-esm.md`. Grund:
|
||
die App wird per Doppelklick direkt aus `file://` geöffnet, native
|
||
ES-Module würden dort per CORS-Policy blockiert.
|
||
- Kein Build-Tool, kein Bundler, kein Framework
|
||
- Karte: [Leaflet](https://leafletjs.com/) 1.9.4 + [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster) 1.5.3, lokal vendored unter `vendor/`
|
||
- Kartengrundlage: OpenStreetMap-Daten, ausgeliefert über CARTO-Basemap-Tiles
|
||
(`basemaps.cartocdn.com`, Stil "light_all") statt des offiziellen
|
||
`tile.openstreetmap.org`. Grund: dessen Tile Usage Policy verlangt seit
|
||
2024 einen gültigen HTTP-Referer, den `file://`-Seiten (Doppelklick-Start)
|
||
grundsätzlich nicht mitsenden können ("Access blocked: Referer is
|
||
required"). CARTO liefert dieselben OSM-Daten kartographisch auf, ohne
|
||
diese Einschränkung; Attribution zeigt weiterhin OpenStreetMap + CARTO.
|
||
- Datenhaltung: LocalStorage (Offline-first), vorbereitet für spätere Migration auf Backend/API
|
||
|
||
## Ordnerstruktur
|
||
|
||
```
|
||
index.html
|
||
css/ reset, variables (Design-Tokens), layout, components, responsive
|
||
js/config/ constants.js, categories.js
|
||
js/store/ store.js (Pub/Sub-State), storage.js (LocalStorage-IO), migrations.js
|
||
js/models/ place.js (Normalisierung), schema.js (Feldstruktur/Defaults)
|
||
js/map/ mapController.js, markers.js, geolocation.js
|
||
js/ui/ list.js (Ergebnisliste; weitere UI-Module folgen in späteren Phasen)
|
||
js/utils/ dom.js, geo.js, id.js
|
||
data/seed.js Beispieldaten (10 Ausflugsziele, wird nur beim allerersten Start geladen)
|
||
vendor/ lokal vendorte Leaflet-Bibliotheken (JS/CSS/Bildassets)
|
||
docs/ dieser Plan, ADRs
|
||
```
|
||
|
||
## Datenmodell (Ausflugsziel / Place)
|
||
|
||
Das interne Place-Objekt orientiert sich eng am verbindlichen KI-Import-JSON-Format
|
||
aus `anforderung.md` (siehe dortiger Abschnitt "Vorgabe für das KI-JSON"):
|
||
|
||
- Basisfelder: `name`, `category`, `shortDescription`, `description`, `latitude`,
|
||
`longitude`, `address`, `city`, `region`
|
||
- `ageRecommendation: { min, max }`
|
||
- `cost: { type: 'free'|'paid'|'mixed'|'unknown', description }`
|
||
- `openingHours`, `durationMinutes`
|
||
- `facilities: { strollerAccessible, wheelchairAccessible, toilets, parking, restaurant }`
|
||
- `environment: 'indoor'|'outdoor'|'mixed'`
|
||
- `website`, `image`, `rating`, `tags[]`, `source`, `sourceVerified`
|
||
- Intern generiert: `id` (UUID), `createdAt`, `updatedAt` (ISO-Strings)
|
||
|
||
`js/models/place.js` normalisiert rohe Objekte (aus `data/seed.js` oder späteren
|
||
Importen) zu diesem vollständigen internen Format; die Kategorie wird dabei
|
||
immer auf eine gültige Kategorie-`id` (Slug, siehe `js/config/categories.js`)
|
||
aufgelöst – entweder direkt (falls schon ein Slug vorliegt) oder über das
|
||
Label (z.B. `"Spielplatz"` → `spielplatz`), wie es die KI liefern würde.
|
||
|
||
### Kategorien
|
||
|
||
14 Kategorien aus der Anforderung + "Sonstige" als generischer Fallback
|
||
(insgesamt 15), jeweils mit `id` (Slug), `label`, `color` (Hex) und `icon`
|
||
(Emoji-Glyph) – siehe `js/config/categories.js`.
|
||
|
||
## Store-Architektur
|
||
|
||
`js/store/store.js` ist ein zentraler Pub/Sub-Store:
|
||
|
||
```js
|
||
state = { places, filters: null, userLocation: null, selectedPlaceId: null }
|
||
```
|
||
|
||
- `subscribe(fn)` registriert Listener, `setState(patch)` merged den State und
|
||
benachrichtigt alle Subscriber.
|
||
- `initStore()` lädt beim Start persistierte Daten aus LocalStorage
|
||
(`js/store/storage.js`). Ist noch nichts gespeichert, wird `data/seed.js`
|
||
geladen, normalisiert und einmalig persistiert.
|
||
- Einstellungen (`lastFilters`, `lastMapView`, `userConsentGeo`, `defaultCenter`)
|
||
werden getrennt über `getSettings()`/`updateSettings()` verwaltet und im
|
||
Key `eventmap.settings.v1` persistiert.
|
||
|
||
### LocalStorage-Schema
|
||
|
||
- `eventmap.data.v1` → `{ schemaVersion: 1, places: [...], updatedAt }`
|
||
- `eventmap.settings.v1` → `{ lastFilters, lastMapView, userConsentGeo, defaultCenter }`
|
||
|
||
`js/store/migrations.js` stellt eine schlanke, sequenziell anwendbare
|
||
Migrations-Infrastruktur bereit (`migrate(data)`). Aktuell existiert nur
|
||
Schema-Version 1, das Migrationsarray ist entsprechend leer.
|
||
|
||
`defaultCenter` (frei wählbares Fallback-Kartenzentrum) ist als Datenfeld
|
||
vorbereitet, aber in Phase 1 noch nicht über die UI einstellbar. Solange kein
|
||
`defaultCenter` gesetzt ist, verwendet die Karte den hartcodierten Default aus
|
||
`js/config/constants.js` (geografische Mitte Deutschlands, lat 51.1657,
|
||
lng 10.4515, zoom 6).
|
||
|
||
## Karte
|
||
|
||
- `js/map/mapController.js`: initialisiert Leaflet-Karte + OSM-Tile-Layer
|
||
(inkl. korrekter Attribution), verwaltet ein `L.markerClusterGroup`,
|
||
reagiert per `subscribe()` auf Änderungen an `state.places` (Marker neu
|
||
rendern) und `state.selectedPlaceId` (Karte zentrieren + Popup öffnen).
|
||
`fitToPlaces()` passt die Ansicht auf alle vorhandenen Places an, mit
|
||
Fallback auf das Default-Center falls keine Places mit gültigen
|
||
Koordinaten vorhanden sind.
|
||
- `js/map/markers.js`: erzeugt je Kategorie ein `L.divIcon` (Farbe + Emoji,
|
||
kein PNG-Sprite nötig) und den Popup-Inhalt (Name, Kategorie,
|
||
Kurzbeschreibung) als DOM-Node (nicht als HTML-String, siehe
|
||
Sicherheitshinweis unten).
|
||
- `js/map/geolocation.js`: fragt den Standort nur auf explizite
|
||
Nutzeraktion (Button "Meinen Standort verwenden") ab, setzt bei Erfolg
|
||
`userLocation` im Store und zentriert die Karte; bei Ablehnung/Fehler
|
||
bleibt das Default-Center erhalten und ein Hinweis wird angezeigt.
|
||
|
||
## Ergebnisliste
|
||
|
||
`js/ui/list.js` rendert `state.places` als Cards (Name, Kategorie mit
|
||
Farbe/Icon, Kurzbeschreibung). Ein Klick auf eine Card setzt
|
||
`selectedPlaceId` im Store; `mapController.js` reagiert darauf und
|
||
zentriert die Karte auf den zugehörigen Marker. Filterung (Phase 4) ist
|
||
hier noch nicht implementiert – aktuell werden immer alle Places gezeigt.
|
||
|
||
**Sicherheitshinweis:** Da künftig Fremddaten per KI-Import in die App
|
||
gelangen, wird an keiner Stelle `innerHTML` mit Place-Daten befüllt.
|
||
`js/utils/dom.js` stellt dafür `el()` (DOM-Erzeugung über `textContent`/
|
||
Properties) sowie `escapeHtml()` als zusätzliches Sicherheitsnetz bereit.
|
||
|
||
## Layout
|
||
|
||
- Header mit Titel "EventMap".
|
||
- Mobile/Tablet (< 1024px): Karte oben (fixe Höhe ca. 50vh), Liste darunter,
|
||
normal scrollbar.
|
||
- Desktop (≥ 1024px): Grid mit Karte (~65%) und Liste-Sidebar (~35%)
|
||
nebeneinander, siehe `css/responsive.css`.
|
||
|
||
## Kein Dev-Server nötig
|
||
|
||
Die App verwendet klassische `<script>`-Tags statt ES-Module (siehe
|
||
`docs/adr/0001-vanilla-esm.md`) und lässt sich daher **per Doppelklick auf
|
||
`index.html`** direkt aus dem lokal synchronisierten Ordner öffnen
|
||
(`file://...`) – ein lokaler HTTP-Server ist dafür nicht mehr nötig.
|
||
|
||
Die Beispieldaten liegen in `data/seed.js` als eingebettetes
|
||
`<script>`-Objekt (`EventMap.data.SEED_PLACES`) statt als `.json`-Datei,
|
||
da `fetch()` auf lokale Dateien über `file://` in manchen Browsern
|
||
(insbesondere Chrome/Chromium) blockiert wird. `initStore()` liest die
|
||
Seed-Daten dadurch synchron direkt aus dem globalen Namespace, ganz ohne
|
||
`fetch()`.
|
||
|
||
## Phasenübersicht
|
||
|
||
| Phase | Inhalt | Status |
|
||
|---|---|---|
|
||
| 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) | ✅ 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) | ✅ umgesetzt |
|
||
| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import + Politur | offen |
|
||
|
||
## KI-Prompt-Generator (Phase 6)
|
||
|
||
- `js/ai/promptBuilder.js`: reine Funktion `buildPrompt(formValues)`, deckt alle
|
||
10 Pflichtpunkte aus `anforderung.md` explizit nummeriert ab und bettet das
|
||
exakte JSON-Zielschema aus `anforderung.md` ("Vorgabe für das KI-JSON")
|
||
1:1 als Konstante `JSON_TEMPLATE` ein (per Skript byte-genau abgeglichen).
|
||
- Der Anweisungstext des Prompts ist immer Deutsch; das Formularfeld "Sprache
|
||
der Ausgabe" steuert separat, in welcher Sprache die KI ihre `name`/
|
||
`shortDescription`/`description`-Texte verfassen soll.
|
||
- `js/ui/promptGenerator.js`: Modal mit Live-Vorschau (`<textarea readonly>`,
|
||
aktualisiert sich bei jeder Eingabe), Buttons "In Zwischenablage kopieren"
|
||
(`js/utils/clipboard.js`, Clipboard API + `execCommand`-Fallback für
|
||
`file://`-Einschränkungen) und "Als Textdatei speichern"
|
||
(`js/utils/download.js`, clientseitiger Blob-Download).
|
||
- Einstiegspunkt: Button in der Karten-Toolbar, öffnet
|
||
`EventMap.ui.openPromptGenerator()`.
|
||
- Import der KI-Antwort (JSON zurück in die App) ist bewusst nicht Teil
|
||
dieser Phase, sondern folgt in Phase 7.
|
||
|
||
## 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 `<details>/<summary>` 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`.
|