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

@ -27,8 +27,6 @@ window.Page_routes = (() => {
// Mini-Karten auf den Route-Cards
let _miniMaps = new Map(); // routeId → L.map
let _leafletReady = false;
const DIFFICULTY_LABEL = { leicht: '🟢 Leicht', mittel: '🟡 Mittel', anspruchsvoll: '🔴 Anspruchsvoll' };
const TERRAIN_LABEL = { wald: '🌲 Wald', asphalt: '🛣️ Asphalt', wiese: '🌿 Wiese', mix: '🔀 Mix' };
const HUNDE_LABEL = { eingeschränkt: '🐾', gut: '🐾🐾', sehr_gut: '🐾🐾🐾', premium: '🐾🐾🐾🐾' };
@ -41,26 +39,13 @@ window.Page_routes = (() => {
{ type: 'bank', icon: '🪑', label: 'Bank' },
];
function _esc(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function _emptyState(icon, title, text, cta = '') {
return `<div class="empty-state">
<svg class="ph-icon empty-state-icon" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
<div class="empty-state-title">${title}</div>
${text ? `<p class="empty-state-text">${text}</p>` : ''}
${cta ? `<div class="empty-state-cta">${cta}</div>` : ''}
</div>`;
}
// _esc und _emptyState ersetzt durch UI.escape() / UI.emptyState()
async function init(container, appState) {
_container = container;
_appState = appState;
_render();
_loadLeaflet(); // fire & forget — bereit wenn Cards gerendert werden
UI.loadLeaflet(); // fire & forget — bereit wenn Cards gerendert werden
try { _userPos = await API.getLocation(); } catch {}
await _loadData();
@ -72,21 +57,6 @@ window.Page_routes = (() => {
}
}
async function _loadLeaflet() {
if (_leafletReady || window.L) { _leafletReady = true; return; }
if (!document.querySelector('link[href="/css/leaflet.css"]')) {
const l = document.createElement('link');
l.rel = 'stylesheet'; l.href = '/css/leaflet.css';
document.head.appendChild(l);
}
await new Promise((res, rej) => {
const s = document.createElement('script');
s.src = '/js/leaflet.js'; s.onload = res; s.onerror = rej;
document.head.appendChild(s);
});
_leafletReady = true;
}
function refresh() { _loadData(); }
function onDogChange() {}
@ -362,7 +332,7 @@ window.Page_routes = (() => {
}).addTo(_searchMap);
// Tooltip mit Namen und Distanz
const tip = `<b>${_esc(route.name)}</b>${route.distanz_km ? ` · ${route.distanz_km.toFixed(1)} km` : ''}`;
const tip = `<b>${UI.escape(route.name)}</b>${route.distanz_km ? ` · ${route.distanz_km.toFixed(1)} km` : ''}`;
line.bindTooltip(tip, { sticky: true, className: 'rk-map-tooltip' });
// Hover-Highlight
@ -409,7 +379,7 @@ window.Page_routes = (() => {
_applyFilter();
} catch (err) {
document.getElementById('rk-grid').innerHTML =
`<p style="color:var(--c-danger);padding:var(--space-6)">Fehler: ${_esc(err.message)}</p>`;
`<p style="color:var(--c-danger);padding:var(--space-6)">Fehler: ${UI.escape(err.message)}</p>`;
}
}
@ -478,12 +448,12 @@ window.Page_routes = (() => {
</div>`;
} else {
// Noch gar keine eigenen Routen
grid.innerHTML = _emptyState(
'map-trifold',
'Noch keine Routen',
'Zeichne Lieblingsrouten auf oder importiere GPX-Dateien. Teile Routen mit Freunden.',
`<button class="btn btn-primary" id="rk-empty-rec">${UI.icon('path')} Route aufzeichnen</button>`
);
grid.innerHTML = UI.emptyState({
icon: UI.icon('map-trifold'),
title: 'Noch keine Routen',
text: 'Zeichne Lieblingsrouten auf oder importiere GPX-Dateien. Teile Routen mit Freunden.',
action: `<button class="btn btn-primary" id="rk-empty-rec">${UI.icon('path')} Route aufzeichnen</button>`,
});
document.getElementById('rk-empty-rec')?.addEventListener('click', () => {
App.navigate('map');
setTimeout(() => window.Page_map?.startRecording?.(), 600);
@ -530,13 +500,13 @@ window.Page_routes = (() => {
const dur = r.dauer_min ? _fmtDur(r.dauer_min) : '';
const firstPhoto = (r.foto_urls || [])[0];
const previewContent = firstPhoto
? `<img src="${_esc(firstPhoto)}" style="width:100%;height:100%;object-fit:cover">`
? `<img src="${UI.escape(firstPhoto)}" style="width:100%;height:100%;object-fit:cover">`
: `<div class="rk-mini-map" data-id="${r.id}"
data-track='${JSON.stringify(r.preview_track||[])}'
style="width:100%;height:100%"></div>`;
const authorLine = isDiscover
? `<div class="rk-card-creator">${UI.icon('user')} ${_esc(r.user_name||'Anonym')}</div>`
? `<div class="rk-card-creator">${UI.icon('user')} ${UI.escape(r.user_name||'Anonym')}</div>`
: '';
return `
@ -544,7 +514,7 @@ window.Page_routes = (() => {
<div class="rk-card-preview">${previewContent}</div>
<div class="rk-card-body">
${authorLine}
<div class="rk-card-name">${_esc(r.name)}</div>
<div class="rk-card-name">${UI.escape(r.name)}</div>
<div class="rk-card-stats">
${dist ? `<span>${UI.icon('map-trifold')} ${dist}</span>` : ''}
${dur ? `<span>${UI.icon('timer')} ${dur}</span>` : ''}
@ -560,7 +530,7 @@ window.Page_routes = (() => {
<div class="rk-card-footer">
<div class="rk-stars">${_starsHTML(r.id, r.bewertung||0, r.anz_bewertungen||0)}</div>
<div class="rk-card-actions">
${isDiscover ? '' : `<span class="rk-card-author">${_esc(r.user_name||'Anonym')}</span>`}
${isDiscover ? '' : `<span class="rk-card-author">${UI.escape(r.user_name||'Anonym')}</span>`}
<button class="rk-dl-btn" data-id="${r.id}">${UI.icon('download-simple')} GPX</button>
</div>
</div>
@ -661,7 +631,7 @@ window.Page_routes = (() => {
const photoGallery = photos.length ? `
<div class="rk-photo-gallery">
${photos.map(u => `<img src="${_esc(u)}" class="rk-photo-thumb" onclick="window.open('${_esc(u)}','_blank')">`).join('')}
${photos.map(u => `<img src="${UI.escape(u)}" class="rk-photo-thumb" onclick="window.open('${UI.escape(u)}','_blank')">`).join('')}
${isOwn ? `<label class="rk-photo-add" title="Foto hinzufügen">
<span>+</span>
<input type="file" id="rk-photo-input" accept="image/*" style="display:none">
@ -686,13 +656,13 @@ window.Page_routes = (() => {
${route.leine_empfohlen ? `<span class="rk-badge">${UI.icon('link')} Leine empfohlen</span>` : ''}
${!route.is_public ? `<span class="rk-badge rk-badge--private">${UI.icon('lock')} Privat</span>` : ''}
</div>
${route.beschreibung ? `<p style="color:var(--c-text-secondary);margin-bottom:var(--space-3)">${_esc(route.beschreibung)}</p>` : ''}
${route.beschreibung ? `<p style="color:var(--c-text-secondary);margin-bottom:var(--space-3)">${UI.escape(route.beschreibung)}</p>` : ''}
<div id="rk-nearby" class="rk-nearby-section">
<div style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt Orte entlang der Route</div>
</div>
<div id="rk-rating-${route.id}"></div>
<p style="color:var(--c-text-muted);font-size:0.75rem;margin-top:var(--space-2)">
${track.length} GPS-Punkte · von ${_esc(route.user_name||'Anonym')}
${route.bewertung ? ` · ${UI.icon('star')} ${route.bewertung.toFixed(1)} (${route.anz_bewertungen})` : ''}
${track.length} GPS-Punkte · von ${UI.escape(route.user_name||'Anonym')}
</p>
`;
@ -707,7 +677,14 @@ window.Page_routes = (() => {
<button type="button" class="btn btn-primary flex-1" id="rd-close">Schließen</button>
`;
UI.modal.open({ title: `🥾 ${_esc(route.name)}`, body, footer });
UI.modal.open({ title: `🥾 ${UI.escape(route.name)}`, body, footer });
UI.ratingStars({
containerId: `rk-rating-${route.id}`,
targetType: 'route',
targetId: route.id,
isLoggedIn: !!_appState.user,
});
document.getElementById('rd-close')?.addEventListener('click', UI.modal.close);
document.getElementById('rd-gpx')?.addEventListener('click', () => _downloadGpxDirect(route));
@ -855,12 +832,12 @@ window.Page_routes = (() => {
<div class="rk-nearby-title">${UI.icon('map-pin')} Entlang der Route</div>
${Object.values(byType).map(group => `
<div class="rk-nearby-group">
<div class="rk-nearby-group-label">${group.icon} ${_esc(group.label)} (${group.items.length})</div>
<div class="rk-nearby-group-label">${group.icon} ${UI.escape(group.label)} (${group.items.length})</div>
${group.items.slice(0, 5).map(p => `
<div class="rk-nearby-item">
<span class="rk-nearby-name">${_esc(p.name || group.label)}</span>
${p.opening_hours ? `<span class="rk-nearby-detail">${UI.icon('clock')} ${_esc(p.opening_hours)}</span>` : ''}
${p.phone ? `<a href="tel:${_esc(p.phone)}" class="rk-nearby-detail rk-nearby-phone">${UI.icon('phone')} ${_esc(p.phone)}</a>` : ''}
<span class="rk-nearby-name">${UI.escape(p.name || group.label)}</span>
${p.opening_hours ? `<span class="rk-nearby-detail">${UI.icon('clock')} ${UI.escape(p.opening_hours)}</span>` : ''}
${p.phone ? `<a href="tel:${UI.escape(p.phone)}" class="rk-nearby-detail rk-nearby-phone">${UI.icon('phone')} ${UI.escape(p.phone)}</a>` : ''}
</div>
`).join('')}
${group.items.length > 5 ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 0">+${group.items.length-5} weitere</div>` : ''}
@ -898,7 +875,7 @@ window.Page_routes = (() => {
const pts = track.map(p => ` <trkpt lat="${p.lat}" lon="${p.lon}"></trkpt>`).join('\n');
const gpx = `<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Ban Yaro" xmlns="http://www.topografix.com/GPX/1/1">
<trk><name>${_esc(route.name)}</name><trkseg>\n${pts}\n </trkseg></trk>
<trk><name>${UI.escape(route.name)}</name><trkseg>\n${pts}\n </trkseg></trk>
</gpx>`;
const blob = new Blob([gpx], { type: 'application/gpx+xml' });
const url = URL.createObjectURL(blob);
@ -1093,7 +1070,7 @@ window.Page_routes = (() => {
<form id="rk-import-form" style="display:flex;flex-direction:column;gap:var(--space-3);margin-top:var(--space-4)">
<div class="form-group">
<label class="form-label">Name *</label>
<input class="form-input" id="ri-name" value="${_esc(name)}" required maxlength="120">
<input class="form-input" id="ri-name" value="${UI.escape(name)}" required maxlength="120">
</div>
<div class="form-group">
<label class="form-label">Beschreibung</label>
@ -1225,15 +1202,15 @@ window.Page_routes = (() => {
const friendRows = friends.map(f => {
const initial = (f.name || '?')[0].toUpperCase();
return `<div class="rk-friend-row" data-id="${f.id}" data-name="${_esc(f.name || 'Anonym')}"
return `<div class="rk-friend-row" data-id="${f.id}" data-name="${UI.escape(f.name || 'Anonym')}"
style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);
cursor:pointer;border-radius:var(--radius-md);transition:background .15s"
onmouseover="this.style.background='var(--c-surface-2)'"
onmouseout="this.style.background=''">
<div style="width:36px;height:36px;border-radius:50%;background:var(--c-primary);
color:#fff;display:flex;align-items:center;justify-content:center;
font-weight:600;flex-shrink:0">${_esc(initial)}</div>
<span>${_esc(f.name || 'Anonym')}</span>
font-weight:600;flex-shrink:0">${UI.escape(initial)}</div>
<span>${UI.escape(f.name || 'Anonym')}</span>
</div>`;
}).join('');