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,95 @@
|
||||
// Kleine DOM-Helfer. Wichtig: Daten aus Places werden NIE per innerHTML
|
||||
// eingefügt, sondern immer über textContent bzw. DOM-APIs (el()).
|
||||
//
|
||||
// Klassisches Script (kein ES-Modul): Die App wird per file:// direkt aus
|
||||
// einem synchronisierten Ordner geöffnet, daher können keine ES-Module
|
||||
// (import/export) verwendet werden (Browser blockieren deren Nachladen
|
||||
// unter file:// per CORS-Policy). Stattdessen hängen alle Dateien ihre
|
||||
// öffentliche API an das globale Namespace-Objekt window.EventMap.
|
||||
// Diese Datei wird als erste geladen und legt den Namespace an.
|
||||
|
||||
window.EventMap = window.EventMap || {};
|
||||
window.EventMap.utils = window.EventMap.utils || {};
|
||||
window.EventMap.config = window.EventMap.config || {};
|
||||
window.EventMap.models = window.EventMap.models || {};
|
||||
window.EventMap.store = window.EventMap.store || {};
|
||||
window.EventMap.map = window.EventMap.map || {};
|
||||
window.EventMap.ui = window.EventMap.ui || {};
|
||||
|
||||
(function () {
|
||||
/**
|
||||
* Erzeugt ein DOM-Element.
|
||||
* @param {string} tag Tag-Name, z.B. 'div'
|
||||
* @param {object} [props] Eigenschaften: className, attrs{}, dataset{}, style{},
|
||||
* text (setzt textContent), on{ click: fn, ... } für Event-Listener
|
||||
* @param {Array<Node|string>} [children] Kindknoten oder Strings (werden als Text angehängt)
|
||||
*/
|
||||
function el(tag, props = {}, children = []) {
|
||||
const node = document.createElement(tag);
|
||||
|
||||
if (props.className) {
|
||||
node.className = props.className;
|
||||
}
|
||||
if (props.text !== undefined) {
|
||||
node.textContent = props.text;
|
||||
}
|
||||
if (props.attrs) {
|
||||
for (const [key, value] of Object.entries(props.attrs)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
node.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (props.dataset) {
|
||||
for (const [key, value] of Object.entries(props.dataset)) {
|
||||
node.dataset[key] = value;
|
||||
}
|
||||
}
|
||||
if (props.style) {
|
||||
Object.assign(node.style, props.style);
|
||||
}
|
||||
if (props.on) {
|
||||
for (const [event, handler] of Object.entries(props.on)) {
|
||||
node.addEventListener(event, handler);
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
if (child === null || child === undefined) continue;
|
||||
if (typeof child === 'string') {
|
||||
node.appendChild(document.createTextNode(child));
|
||||
} else {
|
||||
node.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escaped HTML-Sonderzeichen. Nur als Sicherheitsnetz gedacht, falls an
|
||||
* einzelnen Stellen doch einmal mit HTML-Strings gearbeitet werden muss
|
||||
* (z.B. Leaflet-Popups, die intern innerHTML nutzen). Für normales
|
||||
* Rendering bitte el()/textContent verwenden.
|
||||
*/
|
||||
function escapeHtml(value) {
|
||||
const str = String(value ?? '');
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/** Entfernt alle Kindknoten eines Elements. */
|
||||
function clearChildren(node) {
|
||||
while (node.firstChild) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
EventMap.utils.el = el;
|
||||
EventMap.utils.escapeHtml = escapeHtml;
|
||||
EventMap.utils.clearChildren = clearChildren;
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
// Geografische Hilfsfunktionen.
|
||||
|
||||
(function () {
|
||||
const EARTH_RADIUS_KM = 6371;
|
||||
|
||||
function toRad(deg) {
|
||||
return (deg * Math.PI) / 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernung zwischen zwei Koordinaten in Kilometern (Haversine-Formel).
|
||||
* @param {{lat:number,lng:number}} a
|
||||
* @param {{lat:number,lng:number}} b
|
||||
*/
|
||||
function haversineDistanceKm(a, b) {
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLng = toRad(b.lng - a.lng);
|
||||
const lat1 = toRad(a.lat);
|
||||
const lat2 = toRad(b.lat);
|
||||
|
||||
const h =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
|
||||
const c = 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
||||
|
||||
return EARTH_RADIUS_KM * c;
|
||||
}
|
||||
|
||||
function isValidCoordinate(lat, lng) {
|
||||
return (
|
||||
typeof lat === 'number' &&
|
||||
typeof lng === 'number' &&
|
||||
Number.isFinite(lat) &&
|
||||
Number.isFinite(lng) &&
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lng >= -180 &&
|
||||
lng <= 180
|
||||
);
|
||||
}
|
||||
|
||||
EventMap.utils.haversineDistanceKm = haversineDistanceKm;
|
||||
EventMap.utils.isValidCoordinate = isValidCoordinate;
|
||||
})();
|
||||
@@ -0,0 +1,13 @@
|
||||
// Erzeugt eindeutige IDs für Ausflugsziele.
|
||||
|
||||
(function () {
|
||||
function generateId() {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// Fallback für Umgebungen ohne crypto.randomUUID (z.B. sehr alte Browser).
|
||||
return 'id-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
EventMap.utils.generateId = generateId;
|
||||
})();
|
||||
Reference in New Issue
Block a user