Fix: Routen-Cards mit OSM-Mini-Karte statt SVG, Username ohne Prefix

- Jede Route-Card zeigt echte OSM-Tiles als Kartenvorschau
  (Leaflet lazy-laden + IntersectionObserver → tiles erst wenn sichtbar)
- Track (orange), Start (grün), Ziel (rot) als Overlay
- Interaktion komplett deaktiviert (drag/zoom/click off)
- Username ohne "von " — kürzer, kein redundanter Text
- _svgPreview() bleibt als interner Fallback erhalten
This commit is contained in:
rene 2026-04-15 16:37:26 +02:00
parent ebe4ce20cf
commit 96bd57f0ad

View file

@ -17,6 +17,10 @@ window.Page_routes = (() => {
let _sortBy = 'newest';
let _onlyMine = false;
// 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: '🐾🐾🐾🐾' };
@ -37,10 +41,26 @@ window.Page_routes = (() => {
_container = container;
_appState = appState;
_render();
_loadLeaflet(); // fire & forget — bereit wenn Cards gerendert werden
try { _userPos = await API.getLocation(); } catch {}
_loadData();
}
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() {}
@ -205,7 +225,12 @@ window.Page_routes = (() => {
}
return;
}
// Alte Mini-Maps zerstören bevor DOM neu geschrieben wird
_miniMaps.forEach(m => m.remove());
_miniMaps.clear();
grid.innerHTML = _filtered.map(r => _cardHTML(r)).join('');
_initMiniMaps();
grid.querySelectorAll('.rk-card').forEach(card => {
card.addEventListener('click', e => {
if (e.target.closest('.rk-stars,.rk-dl-btn')) return;
@ -236,16 +261,16 @@ window.Page_routes = (() => {
const paws = HUNDE_LABEL[r.hunde_tauglichkeit] || '';
const dist = r.distanz_km ? `${r.distanz_km.toFixed(1)} km` : '';
const dur = r.dauer_min ? _fmtDur(r.dauer_min) : '';
const preview = _svgPreview(r.preview_track || []);
const firstPhoto = (r.foto_urls || [])[0];
const previewContent = firstPhoto
? `<img src="${_esc(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>`;
return `
<div class="rk-card" data-id="${r.id}">
<div class="rk-card-preview">
${firstPhoto
? `<img src="${_esc(firstPhoto)}" style="width:100%;height:100%;object-fit:cover">`
: preview}
</div>
<div class="rk-card-preview">${previewContent}</div>
<div class="rk-card-body">
<div class="rk-card-name">${_esc(r.name)}</div>
<div class="rk-card-stats">
@ -263,7 +288,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">
<span class="rk-card-author">von ${_esc(r.user_name||'Anonym')}</span>
<span class="rk-card-author">${_esc(r.user_name||'Anonym')}</span>
<button class="rk-dl-btn" data-id="${r.id}"> GPX</button>
</div>
</div>
@ -279,7 +304,51 @@ window.Page_routes = (() => {
}
// ----------------------------------------------------------
// SVG-Vorschau
// Mini-Karten (OSM-Tiles via Leaflet, lazy per IntersectionObserver)
// ----------------------------------------------------------
function _initMiniMaps() {
const init = () => {
const obs = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
obs.unobserve(entry.target);
_buildMiniMap(entry.target);
});
}, { rootMargin: '150px' });
document.querySelectorAll('.rk-mini-map').forEach(el => obs.observe(el));
};
if (window.L) { init(); return; }
// Leaflet noch am Laden — kurz pollen
let tries = 0;
const poll = setInterval(() => {
if (window.L || ++tries > 30) { clearInterval(poll); if (window.L) init(); }
}, 100);
}
function _buildMiniMap(el) {
const track = JSON.parse(el.dataset.track || '[]');
const routeId = parseInt(el.dataset.id);
if (track.length < 2) {
el.innerHTML = '<div class="rk-preview-empty">🗺️</div>';
return;
}
const lls = track.map(p => [p.lat, p.lon]);
const m = L.map(el, {
zoomControl: false, attributionControl: false,
dragging: false, touchZoom: false, scrollWheelZoom: false,
doubleClickZoom: false, keyboard: false, boxZoom: false,
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 17 }).addTo(m);
const poly = L.polyline(lls, { color: '#C4843A', weight: 3, opacity: 0.9 }).addTo(m);
L.circleMarker(lls[0], { radius: 5, color: '#22C55E', fillColor: '#22C55E', fillOpacity: 1, weight: 1.5 }).addTo(m);
L.circleMarker(lls.at(-1), { radius: 5, color: '#EF4444', fillColor: '#EF4444', fillOpacity: 1, weight: 1.5 }).addTo(m);
m.fitBounds(poly.getBounds(), { padding: [8, 8] });
_miniMaps.set(routeId, m);
}
// ----------------------------------------------------------
// SVG-Vorschau (Fallback, wird nicht mehr direkt genutzt)
// ----------------------------------------------------------
function _svgPreview(track) {
if (!track || track.length < 2)