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
+37
View File
@@ -0,0 +1,37 @@
// Schlanke Migrations-Infrastruktur für das LocalStorage-Datenschema.
// Aktuell existiert nur Schema-Version 1, daher ist MIGRATIONS leer.
// Neue Migrationen werden als { from, to, migrate(data) } ergänzt und
// von migrate() sequenziell angewendet.
(function () {
const { CURRENT_SCHEMA_VERSION } = EventMap.config;
const MIGRATIONS = [
// Beispiel für zukünftige Migrationen:
// { from: 1, to: 2, migrate: (data) => ({ ...data, neuesFeld: 'default' }) },
];
/**
* Wendet alle nötigen Migrationsschritte an, bis schemaVersion der
* aktuellen CURRENT_SCHEMA_VERSION entspricht.
*/
function migrate(data) {
let current = data ?? {};
let version = current.schemaVersion ?? 1;
while (version < CURRENT_SCHEMA_VERSION) {
const step = MIGRATIONS.find((m) => m.from === version);
if (!step) {
// Keine passende Migration gefunden: auf aktuelle Version springen,
// um die App nicht zu blockieren.
break;
}
current = step.migrate(current);
version = step.to;
}
return { ...current, schemaVersion: CURRENT_SCHEMA_VERSION };
}
EventMap.store.migrate = migrate;
})();
+63
View File
@@ -0,0 +1,63 @@
// Persistenzschicht: liest/schreibt die beiden LocalStorage-Keys
// eventmap.data.v1 und eventmap.settings.v1.
(function () {
const { STORAGE_KEYS, CURRENT_SCHEMA_VERSION } = EventMap.config;
const { migrate } = EventMap.store;
function createDefaultSettings() {
return {
lastFilters: null,
lastMapView: null,
userConsentGeo: null,
defaultCenter: null,
};
}
/** Liest die persistierten Places. Gibt null zurück, falls noch nichts gespeichert ist. */
function loadData() {
const raw = window.localStorage.getItem(STORAGE_KEYS.DATA);
if (!raw) {
return null;
}
try {
const parsed = JSON.parse(raw);
return migrate(parsed);
} catch (err) {
console.warn('EventMap: Gespeicherte Daten sind beschädigt und werden verworfen.', err);
return null;
}
}
function saveData(places) {
const payload = {
schemaVersion: CURRENT_SCHEMA_VERSION,
places,
updatedAt: new Date().toISOString(),
};
window.localStorage.setItem(STORAGE_KEYS.DATA, JSON.stringify(payload));
}
function loadSettings() {
const raw = window.localStorage.getItem(STORAGE_KEYS.SETTINGS);
if (!raw) {
return createDefaultSettings();
}
try {
return { ...createDefaultSettings(), ...JSON.parse(raw) };
} catch (err) {
console.warn('EventMap: Gespeicherte Einstellungen sind beschädigt, Defaults werden verwendet.', err);
return createDefaultSettings();
}
}
function saveSettings(settings) {
window.localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(settings));
}
EventMap.store.createDefaultSettings = createDefaultSettings;
EventMap.store.loadData = loadData;
EventMap.store.saveData = saveData;
EventMap.store.loadSettings = loadSettings;
EventMap.store.saveSettings = saveSettings;
})();
+88
View File
@@ -0,0 +1,88 @@
// Zentraler Pub/Sub-Store der Anwendung. Hält den Laufzeitzustand
// (places, filters, userLocation, selectedPlaceId) und benachrichtigt
// Subscriber bei Änderungen. Lädt beim Start aus LocalStorage bzw. beim
// allerersten Start aus data/seed.js und persistiert Änderungen zurück.
(function () {
const { loadData, saveData, loadSettings, saveSettings } = EventMap.store;
const { normalizePlace } = EventMap.models;
let state = {
places: [],
filters: null,
userLocation: null,
selectedPlaceId: null,
};
let settings = loadSettings();
const subscribers = new Set();
function notify() {
for (const fn of subscribers) {
fn(state);
}
}
/** Registriert einen Listener, der bei jeder Zustandsänderung aufgerufen wird. */
function subscribe(fn) {
subscribers.add(fn);
return () => subscribers.delete(fn);
}
function getState() {
return state;
}
function setState(patch) {
state = { ...state, ...patch };
notify();
}
function getSettings() {
return settings;
}
function updateSettings(patch) {
settings = { ...settings, ...patch };
saveSettings(settings);
}
function loadSeedPlaces() {
const raw = (EventMap.data && EventMap.data.SEED_PLACES) || [];
return raw.map((item) => normalizePlace(item));
}
/**
* Initialisiert den Store: lädt persistierte Places aus LocalStorage oder,
* falls noch keine vorhanden sind, die eingebetteten Beispieldaten
* (data/seed.js). Gibt den finalen State zurück.
*/
function initStore() {
const persisted = loadData();
let places = [];
if (persisted && Array.isArray(persisted.places) && persisted.places.length > 0) {
places = persisted.places.map((p) => normalizePlace(p));
} else {
places = loadSeedPlaces();
saveData(places);
}
setState({ places });
return state;
}
/** Persistiert state.places nach Änderungen (Erstellen/Bearbeiten/Löschen). */
function persistPlaces() {
saveData(state.places);
}
EventMap.store.subscribe = subscribe;
EventMap.store.getState = getState;
EventMap.store.setState = setState;
EventMap.store.getSettings = getSettings;
EventMap.store.updateSettings = updateSettings;
EventMap.store.initStore = initStore;
EventMap.store.persistPlaces = persistPlaces;
})();