Refactor: UI.loadLeaflet, leafletMarker, escape, emptyState, locationPicker zentralisiert

- Task 1: UI.loadLeaflet() in ui.js (mit Cluster-Option), lokale _loadLeaflet() in
  diary/walks/routes/places/poison/events.js entfernt
- Task 2: UI.escape() ersetzt lokale _esc()/_escape() in allen 5 Seiten-Modulen
- Task 3: UI.emptyState() ersetzt lokale _emptyState() in diary/routes/events.js
- Task 4: _fmtDate/_fmtDateShort in walks/poison bewusst behalten (anderes Format),
  Kommentare ergänzt
- Task 5: UI.locationPicker() eingebaut in places/poison/events (ersetzt manuelle
  GPS-Input-Blöcke)
- Task 6: UI.leafletMarker() factory in ui.js, Kreis-divIcon-Blöcke in walks/places/
  poison ersetzt; events.js behält Diamant-Marker (andere Form)
- SW by-v207, APP_VER 175
This commit is contained in:
rene 2026-04-18 14:34:35 +02:00
parent 066b722c5e
commit e98ce0d232
9 changed files with 761 additions and 471 deletions

View file

@ -15,7 +15,6 @@ window.Page_poison = (() => {
let _userMarker = null;
let _reports = [];
let _userPos = null;
let _leafletLoaded = false;
const TYPEN = {
unbekannt: { label: 'Unbekannt', icon: '❓', color: '#e67e22' },
@ -97,43 +96,13 @@ window.Page_poison = (() => {
document.getElementById('poison-btn-report')
?.addEventListener('click', _showReportForm);
await _loadLeaflet();
await UI.loadLeaflet();
_initMap();
// Leaflet muss nach CSS-Load die Container-Größe neu berechnen
setTimeout(() => _map?.invalidateSize(), 100);
await _locateAndLoad();
}
// ----------------------------------------------------------
// LEAFLET DYNAMISCH LADEN
// ----------------------------------------------------------
async function _loadLeaflet() {
if (_leafletLoaded || window.L) { _leafletLoaded = true; return; }
// CSS lokal (kein CDN)
await new Promise(resolve => {
if (document.querySelector('link[href*="leaflet"]')) { resolve(); return; }
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/css/leaflet.css';
link.onload = resolve;
link.onerror = resolve;
document.head.appendChild(link);
});
// JS lokal (kein CDN)
await new Promise((resolve, reject) => {
if (document.querySelector('script[src*="leaflet"]')) { resolve(); return; }
const s = document.createElement('script');
s.src = '/js/leaflet.js';
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
_leafletLoaded = true;
}
// ----------------------------------------------------------
// KARTE INITIALISIEREN
// ----------------------------------------------------------
@ -229,27 +198,16 @@ window.Page_poison = (() => {
_reports.forEach(r => {
const typ = TYPEN[r.typ] || TYPEN.unbekannt;
const icon = L.divIcon({
className : '',
html : `<div style="
background:${typ.color};color:#fff;border-radius:50%;
width:34px;height:34px;
display:flex;align-items:center;justify-content:center;
font-size:17px;box-shadow:0 2px 6px rgba(0,0,0,.35);
border:2px solid #fff">${typ.icon}</div>`,
iconSize : [34, 34],
iconAnchor : [17, 17],
});
const distStr = r.distanz_m < 1000
? `${r.distanz_m} m`
: `${(r.distanz_m / 1000).toFixed(1)} km`;
const marker = L.marker([r.lat, r.lon], { icon })
const marker = UI.leafletMarker({ lat: r.lat, lon: r.lon, color: typ.color, icon: typ.icon, size: 34 })
.addTo(_map)
.bindPopup(`
<b>${typ.icon} ${typ.label}</b><br>
${r.beschreibung ? _escape(r.beschreibung.slice(0, 80)) + '<br>' : ''}
${r.beschreibung ? UI.escape(r.beschreibung.slice(0, 80)) + '<br>' : ''}
<small>📍 ${distStr} entfernt</small><br>
<small>📅 ${_fmtDate(r.created_at)}</small>
${r.bestaetigt ? '<br><small>✅ Bestätigt</small>' : ''}
@ -316,7 +274,7 @@ window.Page_poison = (() => {
${r.beschreibung
? `<p style="margin:0 0 var(--space-1);font-size:var(--text-sm);
color:var(--c-text)">
${_escape(r.beschreibung.slice(0, 120))}${r.beschreibung.length > 120 ? '…' : ''}
${UI.escape(r.beschreibung.slice(0, 120))}${r.beschreibung.length > 120 ? '…' : ''}
</p>`
: ''}
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
@ -357,7 +315,7 @@ window.Page_poison = (() => {
</div>
${r.beschreibung
? `<p style="white-space:pre-wrap;margin-bottom:var(--space-3)">${_escape(r.beschreibung)}</p>`
? `<p style="white-space:pre-wrap;margin-bottom:var(--space-3)">${UI.escape(r.beschreibung)}</p>`
: ''}
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);
@ -365,7 +323,7 @@ window.Page_poison = (() => {
<div>📍 ${r.lat.toFixed(5)}, ${r.lon.toFixed(5)}</div>
<div>📅 Gemeldet: ${_fmtDate(r.created_at)}</div>
<div> Läuft ab: ${_fmtDate(r.expires_at)}</div>
${r.melder_name ? `<div>👤 Gemeldet von: ${_escape(r.melder_name)}</div>` : ''}
${r.melder_name ? `<div>👤 Gemeldet von: ${UI.escape(r.melder_name)}</div>` : ''}
</div>
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
@ -482,21 +440,7 @@ window.Page_poison = (() => {
<div class="form-group">
<label class="form-label">Standort</label>
<div style="display:flex;gap:var(--space-2);align-items:center">
<input class="form-control" type="text" id="pf-lat-disp"
placeholder="Breite" readonly style="flex:1">
<input class="form-control" type="text" id="pf-lon-disp"
placeholder="Länge" readonly style="flex:1">
<button type="button" class="btn btn-secondary" id="pf-gps-btn"
title="GPS-Standort ermitteln">📍</button>
</div>
<input type="hidden" name="lat" id="pf-lat">
<input type="hidden" name="lon" id="pf-lon">
<small id="pf-gps-hint" style="color:var(--c-text-secondary)">
${_userPos
? '✅ Aktueller Standort vorausgefüllt'
: 'GPS-Button drücken um Standort zu ermitteln'}
</small>
<div id="poison-location-picker"></div>
</div>
<div class="form-group">
@ -532,32 +476,12 @@ window.Page_poison = (() => {
UI.modal.open({ title: '⚠️ Giftköder melden', body, footer });
// Standort vorausfüllen wenn bekannt
// Location-Picker initialisieren + ggf. bekannten Standort vorausfüllen
const _picker = UI.locationPicker({ containerId: 'poison-location-picker' });
if (_userPos) {
document.getElementById('pf-lat').value = _userPos.lat;
document.getElementById('pf-lon').value = _userPos.lon;
document.getElementById('pf-lat-disp').value = _userPos.lat.toFixed(6);
document.getElementById('pf-lon-disp').value = _userPos.lon.toFixed(6);
_picker.setValue(_userPos.lat, _userPos.lon, null);
}
// GPS-Button
document.getElementById('pf-gps-btn')?.addEventListener('click', async () => {
const btn = document.getElementById('pf-gps-btn');
UI.setLoading(btn, true);
try {
const pos = await API.getLocation({ timeout: 10000, enableHighAccuracy: true });
document.getElementById('pf-lat').value = pos.lat;
document.getElementById('pf-lon').value = pos.lon;
document.getElementById('pf-lat-disp').value = pos.lat.toFixed(6);
document.getElementById('pf-lon-disp').value = pos.lon.toFixed(6);
document.getElementById('pf-gps-hint').textContent = '✅ Standort aktualisiert';
_userPos = pos;
} catch {
UI.toast.error('GPS-Standort konnte nicht ermittelt werden.');
}
UI.setLoading(btn, false);
});
// Foto-Vorschau
const photoInput = document.querySelector('#poison-form [name="photo"]');
const photoPreview = document.getElementById('pf-photo-preview');
@ -576,16 +500,17 @@ window.Page_poison = (() => {
e.preventDefault();
const submitBtn = document.querySelector('[form="poison-form"][type="submit"]') || e.target.querySelector('[type="submit"]');
const fd = UI.formData(e.target);
const loc = _picker.getValue();
if (!fd.lat || !fd.lon) {
if (!loc.lat || !loc.lon) {
UI.toast.warning('Bitte zuerst den GPS-Standort ermitteln (📍).');
return;
}
await UI.asyncButton(submitBtn, async () => {
const payload = {
lat : parseFloat(fd.lat),
lon : parseFloat(fd.lon),
lat : loc.lat,
lon : loc.lon,
typ : fd.typ,
beschreibung : fd.beschreibung || null,
};
@ -605,6 +530,8 @@ window.Page_poison = (() => {
}
// Distanz client-seitig berechnen (für sofortige Anzeige)
// _userPos aktualisieren falls Picker neuen Standort geliefert hat
if (loc.lat && loc.lon) _userPos = { lat: loc.lat, lon: loc.lon };
created.distanz_m = _userPos
? Math.round(_haversine(_userPos.lat, _userPos.lon, created.lat, created.lon))
: 0;
@ -644,6 +571,8 @@ window.Page_poison = (() => {
return 2 * R * Math.asin(Math.sqrt(a));
}
// _fmtDate: bewusst lokal behalten — UI.time.format() liefert langen Monats-Namen
// und behandelt kein SQLite-Leerzeichen-Format ("2026-04-12 00:00:00")
function _fmtDate(isoStr) {
if (!isoStr) return '';
// SQLite speichert "2026-04-12T00:00:00" oder "2026-04-12 00:00:00"
@ -653,15 +582,6 @@ window.Page_poison = (() => {
});
}
function _escape(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------