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:
@@ -0,0 +1,56 @@
|
||||
// Internes Place-Objekt: bleibt nah am KI-Import-JSON-Format aus
|
||||
// anforderung.md, ergänzt um intern generierte Felder (id, createdAt,
|
||||
// updatedAt). Die Kategorie wird beim Normalisieren immer auf eine
|
||||
// gültige Kategorie-id (Slug) aufgelöst, damit Styling (Farbe/Icon)
|
||||
// überall per getCategoryById() funktioniert.
|
||||
|
||||
(function () {
|
||||
const { generateId } = EventMap.utils;
|
||||
const { CATEGORIES, getCategoryById, getCategoryByLabel } = EventMap.config;
|
||||
const { createDefaultPlaceFields } = EventMap.models;
|
||||
|
||||
const CATEGORY_IDS = new Set(CATEGORIES.map((c) => c.id));
|
||||
|
||||
function resolveCategory(rawCategory) {
|
||||
if (typeof rawCategory === 'string' && CATEGORY_IDS.has(rawCategory)) {
|
||||
return rawCategory;
|
||||
}
|
||||
return getCategoryByLabel(rawCategory).id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisiert ein rohes Place-Objekt (z.B. aus data/seed.js oder einem
|
||||
* späteren KI-Import) zu einem vollständigen internen Place-Objekt.
|
||||
* Fehlende Felder werden mit Defaults aufgefüllt, id/createdAt/updatedAt
|
||||
* werden übernommen falls vorhanden, sonst neu generiert.
|
||||
*/
|
||||
function normalizePlace(raw = {}) {
|
||||
const defaults = createDefaultPlaceFields();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return {
|
||||
id: raw.id || generateId(),
|
||||
...defaults,
|
||||
...raw,
|
||||
category: resolveCategory(raw.category),
|
||||
ageRecommendation: { ...defaults.ageRecommendation, ...(raw.ageRecommendation || {}) },
|
||||
cost: { ...defaults.cost, ...(raw.cost || {}) },
|
||||
facilities: { ...defaults.facilities, ...(raw.facilities || {}) },
|
||||
tags: Array.isArray(raw.tags) ? [...raw.tags] : [],
|
||||
createdAt: raw.createdAt || now,
|
||||
updatedAt: raw.updatedAt || now,
|
||||
};
|
||||
}
|
||||
|
||||
function getPlaceCategory(place) {
|
||||
return getCategoryById(place.category);
|
||||
}
|
||||
|
||||
function getPlaceLatLng(place) {
|
||||
return { lat: place.latitude, lng: place.longitude };
|
||||
}
|
||||
|
||||
EventMap.models.normalizePlace = normalizePlace;
|
||||
EventMap.models.getPlaceCategory = getPlaceCategory;
|
||||
EventMap.models.getPlaceLatLng = getPlaceLatLng;
|
||||
})();
|
||||
@@ -0,0 +1,50 @@
|
||||
// Beschreibt die Feldstruktur eines Ausflugsziels (~27 Felder, siehe
|
||||
// anforderung.md) und liefert Default-Werte. Dient als einzige Quelle der
|
||||
// Wahrheit für die Form eines Place-Objekts; Validierungslogik für
|
||||
// Formulare folgt in einer späteren Phase.
|
||||
|
||||
(function () {
|
||||
/**
|
||||
* Liefert ein leeres/defaultes Place-Objekt ohne id/createdAt/updatedAt
|
||||
* (diese werden von place.js ergänzt).
|
||||
*/
|
||||
function createDefaultPlaceFields() {
|
||||
return {
|
||||
name: '',
|
||||
category: 'sonstige',
|
||||
shortDescription: '',
|
||||
description: '',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
address: '',
|
||||
city: '',
|
||||
region: '',
|
||||
ageRecommendation: { min: null, max: null },
|
||||
cost: { type: 'unknown', description: '' },
|
||||
openingHours: '',
|
||||
durationMinutes: null,
|
||||
facilities: {
|
||||
strollerAccessible: false,
|
||||
wheelchairAccessible: false,
|
||||
toilets: false,
|
||||
parking: false,
|
||||
restaurant: false,
|
||||
},
|
||||
environment: 'outdoor',
|
||||
website: '',
|
||||
image: '',
|
||||
rating: null,
|
||||
tags: [],
|
||||
source: '',
|
||||
sourceVerified: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Gültige Werte für die Enums des Datenmodells.
|
||||
const COST_TYPES = ['free', 'paid', 'mixed', 'unknown'];
|
||||
const ENVIRONMENT_TYPES = ['indoor', 'outdoor', 'mixed'];
|
||||
|
||||
EventMap.models.createDefaultPlaceFields = createDefaultPlaceFields;
|
||||
EventMap.models.COST_TYPES = COST_TYPES;
|
||||
EventMap.models.ENVIRONMENT_TYPES = ENVIRONMENT_TYPES;
|
||||
})();
|
||||
Reference in New Issue
Block a user