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)
This commit is contained in:
2026-07-24 11:24:47 +02:00
parent 6b0d10c4fb
commit ee55fb1cf9
32 changed files with 2557 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
# 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.