Files
eventmap/docs/plan.md
T
dapelza ee55fb1cf9 Phase 0+1: Scaffold, Datenmodell, Karte und Liste
Reines HTML5/CSS3/JavaScript ohne Build-Tooling, lauffaehig direkt per
Doppelklick auf index.html (file://, kein Server noetig):

- Datenmodell fuer Ausflugsziele mit allen Feldern aus der Anforderung,
  15 feste Kategorien inkl. Farbe/Icon
- Zentraler Pub/Sub-Store mit LocalStorage-Persistenz und
  Migrations-Grundgeruest
- Interaktive Karte (Leaflet + Leaflet.markercluster, lokal vendored)
  mit kategoriefarbenen Markern, Clustering, Popups, Geolocation-Button
- Kartengrundlage ueber CARTO-Basemaps statt tile.openstreetmap.org, da
  dessen Referer-Pflicht file://-Aufrufe blockiert
- Seed-Daten als eingebettetes Script (data/seed.js) statt JSON-Datei,
  da fetch() auf lokale Dateien unter file:// nicht zuverlaessig
  funktioniert
- Ergebnisliste mit 10 Beispiel-Ausflugszielen, sicher gerendert ueber
  textContent/DOM-APIs statt innerHTML (Vorbereitung fuer spaeteren
  KI-JSON-Import mit Fremddaten)
- Responsives Layout (Desktop nebeneinander, Mobile gestapelt)
2026-07-24 11:24:47 +02:00

169 lines
8.3 KiB
Markdown
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.
# 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) | 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 |
| 6 | KI-Prompt-Generator (Eingabeformular, Promptgenerierung, Copy/Download) | offen |
| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) | offen |
Nicht Teil von Phase 0/1 und daher bewusst noch nicht angelegt:
Filter-Modul, Detail-Modul, Formular-Modul, Prompt-Generator-Modul,
Import-Modul.