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
+98
View File
@@ -0,0 +1,98 @@
# ADR 0001: Kein Build-Tooling, klassische Scripts mit globalem Namespace, Leaflet lokal vendored
## Status
Akzeptiert (überarbeitet siehe "Revision" unten; ursprüngliche
Entscheidung für native ES-Module wurde revidiert)
## Kontext
`anforderung.md` fordert explizit "möglichst einfache und gut verständliche
Technologien" (HTML5, CSS3, JavaScript, eine Open-Source-Kartenbibliothek,
LocalStorage) und stellt keinen Backend-Zwang für die erste Version. Die
Anwendung soll außerdem vollständig responsiv sein und ohne größere
Infrastruktur betrieben und gewartet werden können.
Die tatsächliche Zielumgebung des Nutzers wurde präzisiert: Die App wird
per **Doppelklick auf `index.html`** direkt aus einem lokal synchronisierten
Nextcloud-Ordner geöffnet (`file://...`). Es steht **kein lokaler
HTTP-Server** zur Verfügung und ein solcher soll vom Endnutzer auch nicht
manuell gestartet werden müssen Priorität hat maximale Einfachheit für den
Endnutzer, nicht Entwicklerkomfort.
## Revision (2026-07-24)
Die ursprüngliche Entscheidung, JavaScript als native ES-Module
(`<script type="module">` mit `import`/`export`) umzusetzen, war für diesen
Anwendungsfall **falsch**: Browser blockieren das Nachladen von ES-Modulen
über `file://` per CORS-Policy (`Access to script ... has been blocked by
CORS policy`). Die App ließ sich damit nur über einen lokalen HTTP-Server
starten was der Anforderung "Doppelklick, kein Server" widerspricht.
**Neue Entscheidung:** Alle JavaScript-Dateien wurden von ES-Modulen auf
**klassische, nicht-modulare `<script>`-Tags** umgebaut. Statt
`import`/`export` hängt jede Datei ihre öffentliche API an ein einziges
globales Namespace-Objekt `window.EventMap` (mit Sub-Namespaces `utils`,
`config`, `models`, `store`, `map`, `ui`) an. Jede Datei kapselt ihre
internen Hilfsvariablen weiterhin in einer IIFE
(`(function () { ... })();`), sodass nur das explizit an `EventMap.*`
angehängte gemeinsam genutzt wird. Die Ladereihenfolge in `index.html`
(Utils → Config → Models → Store → Map → UI → `main.js`) stellt sicher,
dass jede Datei beim Ausführen bereits auf die Namespaces ihrer
Abhängigkeiten zugreifen kann.
## Entscheidung
- Es wird **kein Build-Tooling** (kein Bundler wie Webpack/Vite, kein
Transpiler, kein Package-Manager-Setup für die App selbst) eingesetzt.
- JavaScript wird als **klassische Scripts** (`<script src="...">`, ohne
`type="module"`) eingebunden, organisiert über einen gemeinsamen globalen
Namespace `window.EventMap` statt über `import`/`export`. Jede Datei ist
in eine IIFE gekapselt.
- **Leaflet** und **Leaflet.markercluster** werden **lokal vendored** unter
`vendor/leaflet/` bzw. `vendor/leaflet.markercluster/` (JS, CSS und
Marker-Bildassets), nicht per CDN eingebunden.
## Begründung
- Entspricht direkt der Anforderung nach einfachen, gut verständlichen
Technologien ohne zusätzliche Lernkurve (kein Framework, kein Build-Schritt).
- Ermöglicht das Öffnen der App per Doppelklick aus dem synchronisierten
Ordner heraus, ohne dass der Endnutzer einen Server starten oder
irgendetwas installieren muss das ist für die Zielgruppe wichtiger als
natives Modul-Tooling.
- Lokal vendorte Bibliotheken machen die App **offline-fähig** und
unabhängig von der Verfügbarkeit externer CDNs; das passt zum
Offline-First-Ansatz der LocalStorage-Datenhaltung.
- Geringere Angriffsfläche/Komplexität: keine Abhängigkeit von Node-Toolchain-
Versionen, keine Build-Artefakte, die aktuell gehalten werden müssten.
## Konsequenzen
- **Trade-off gegenüber ES-Modulen:** Es gibt kein natives Modul-Scoping
mehr. Stattdessen dient der globale `window.EventMap`-Namespace plus
IIFE-Kapselung pro Datei als Ersatz. Das erfordert Disziplin beim
Vergeben von Namen unter `EventMap.*` (keine versehentlichen
Überschreibungen) und macht Abhängigkeiten weniger explizit als
`import`-Statements sie ergeben sich aus der Script-Reihenfolge in
`index.html`, die entsprechend sorgfältig gepflegt werden muss.
- **Behobenes Restrisiko:** Ursprünglich lud `js/store/store.js` die
Beispieldaten per `fetch('data/seed.json')`, was unter `file://`
(insbesondere in Chrome/Chromium) von der Same-Origin-Policy blockiert
wird. Die Seed-Daten wurden daher von `data/seed.json` nach
`data/seed.js` verschoben und liegen dort als eingebettetes
`<script>`-Objekt (`EventMap.data.SEED_PLACES`), das wie jede andere
JS-Datei klassisch eingebunden wird. `initStore()` ist dadurch wieder
synchron; `fetch()` kommt aktuell an keiner Stelle der Anwendung mehr
vor. Für spätere Phasen (z.B. KI-JSON-Import) ist zu beachten: Datei-Uploads
über `<input type="file">` + `FileReader` sind unproblematisch unter
`file://`, ein `fetch()` auf lokale Dateien dagegen nicht.
- Es gibt keinen automatischen Minify-/Tree-Shaking-Schritt; die
Dateigröße im Produktivbetrieb ist etwas größer als mit Bundler, was für
den Umfang dieser Anwendung akzeptabel ist.
- Updates der vendorten Bibliotheken müssen manuell durchgeführt werden
(Dateien in `vendor/` austauschen), es gibt keinen automatisierten
Dependency-Update-Mechanismus (z.B. Dependabot) für diese Assets.
- Sollte die Anwendung später deutlich wachsen (z.B. TypeScript, größere
Testinfrastruktur, Bereitstellung über einen echten Webserver), kann
diese Entscheidung revidiert und zurück auf ES-Module umgestellt werden.
+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.