From 73872e2c21cac55c28cf0c6d367f3d7c955210d7 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 27 May 2026 08:17:06 +0200 Subject: [PATCH] Sprint D: Karten-Familie auf UI.map.create+svgMarker konsolidiert, SW by-v1107 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue zentrale Helper (in Sprint B vorbereitet) jetzt von 5 Seiten genutzt: walks.js (1 Karten-Init): - L.map+L.tileLayer → await UI.map.create('walks-map', {...}) - _initMap zu async, Aufrufer in _switchView und _loadData angepasst - Mini-Karte im Walk-Formular (Modal) bleibt unverändert (braucht eigene dragging/scrollWheelZoom-Options) - view-toggle nicht migriert (responsive CSS-Konflikt mit Desktop) poison.js (1 Karten-Init): - L.map+L.tileLayer → await UI.map.create('poison-map', {...}) - _initMap zu async, manueller UI.loadLeaflet entfernt - DangerCircle + User-Marker unverändert events.js (1 Karten-Init + Diamant-Marker): - await UI.map.create('ev-map', {...}) - Rotierter Diamant: L.divIcon+L.marker → UI.map.svgMarker (HTML 1:1 erhalten) lost.js (1 Karten-Init + Puls-Marker): - Eigene async _loadLeaflet() Funktion komplett entfernt — UI.map.create übernimmt das jetzt zentral - await UI.map.create('lost-map', {...}) - Puls-Animation 🐕: L.divIcon+L.marker → UI.map.svgMarker - _initMap zu async routes.js (6 von 7 Karten-Inits): - _suggestMap, _recMap, _searchMap, _navMap, trimMap, _buildDetailMap alle auf UI.map.create umgestellt + zu async - _buildMiniMap (Route-Card-Preview) bleibt unverändert (braucht 6 spezifische Interaction-Disable Options) - View-Toggle auf neue .map-list-toggle Klasse umgestellt (Border-Inline-Styles raus) NEUE CSS-KLASSE in components.css: - .map-list-toggle (vereinheitlichter Karten/Listen-Umschalter) - Verwendet von routes.js; walks/events können später folgen Tests 19/19 grün. GPS-Tracking-Logik (Polylines, Recording, Trim) komplett unangetastet. Marker-Cluster-Logik unverändert. --- VERSION | 2 +- backend/static/css/components.css | 41 ++++++++++++ backend/static/index.html | 24 +++---- backend/static/js/app.js | 2 +- backend/static/js/pages/events.js | 16 +++-- backend/static/js/pages/lost.js | 59 ++++-------------- backend/static/js/pages/poison.js | 20 +++--- backend/static/js/pages/routes.js | 100 ++++++++++++------------------ backend/static/js/pages/walks.js | 14 ++--- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 11 files changed, 130 insertions(+), 152 deletions(-) diff --git a/VERSION b/VERSION index 5642d7e..6a333d3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1106 \ No newline at end of file +1107 \ No newline at end of file diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 69113a5..309f5e4 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -8944,3 +8944,44 @@ svg.empty-state-icon { .offline-status-row .osr-text { flex: 1; min-width: 0; } .offline-status-row .osr-title { font-weight: 600; } .offline-status-row .osr-detail { font-size: var(--text-xs); color: var(--c-text-muted); margin-top: 2px; } + +/* ============================================================ + .map-list-toggle — vereinheitlichter Karten/Listen-Umschalter + Verwendet von walks.js, events.js, routes.js, etc. + +
+ + +
+ ============================================================ */ +.map-list-toggle { + display: flex; + border: 1.5px solid var(--c-border); + border-radius: var(--radius-md); + overflow: hidden; + background: var(--c-surface); +} +.map-list-toggle button { + flex: 1; + height: 44px; + border: none; + background: transparent; + color: var(--c-text-secondary); + cursor: pointer; + font-size: var(--text-sm); + font-weight: var(--weight-medium); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-1); + transition: background 0.15s, color 0.15s; + -webkit-tap-highlight-color: transparent; +} +.map-list-toggle button.active { + background: var(--c-primary); + color: #fff; +} +.map-list-toggle button:not(.active):hover { + background: var(--c-surface-2); + color: var(--c-text); +} diff --git a/backend/static/index.html b/backend/static/index.html index 113e542..8306b18 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -617,11 +617,11 @@ - - - - - + + + + + @@ -631,7 +631,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 08770c9..20fe464 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1106'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1107'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/pages/events.js b/backend/static/js/pages/events.js index a170280..afb7e62 100644 --- a/backend/static/js/pages/events.js +++ b/backend/static/js/pages/events.js @@ -248,8 +248,10 @@ window.Page_events = (() => { await UI.loadLeaflet(true); // true = mit MarkerCluster if (!_map) { - _map = L.map('ev-map', { zoomControl: true }).setView([51.1657, 10.4515], 6); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_map); + _map = await UI.map.create('ev-map', { + center: [51.1657, 10.4515], zoom: 6, + zoomControl: true, attributionControl: false, + }); } // Cluster-Gruppe aufräumen und neu befüllen @@ -266,12 +268,8 @@ window.Page_events = (() => { const typ = TYPEN.find(t => t.id === ev.typ) || TYPEN[TYPEN.length - 1]; const d = new Date(ev.datum + 'T00:00:00'); const datum = d.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric' }); - // Events nutzen rotierten Diamant-Marker (nicht Kreis) — UI.leafletMarker() nicht anwendbar - const icon = L.divIcon({ - className: '', - html: `
`, - iconSize: [32, 32], iconAnchor: [16, 32], - }); + // Events nutzen rotierten Diamant-Marker (nicht Kreis) — UI.map.svgMarker mit custom HTML + const html = `
`; const popup = `
${UI.escape(ev.titel)}
@@ -282,7 +280,7 @@ window.Page_events = (() => { style="font-size:12px;color:var(--c-primary,#2563eb)">Details
`; - const m = L.marker([ev.lat, ev.lon], { icon }).bindPopup(popup); + const m = UI.map.svgMarker(ev.lat, ev.lon, html, { size: 32, anchorY: 32 }).bindPopup(popup); _clusterGroup.addLayer(m); _markers.push(m); bounds.push([ev.lat, ev.lon]); diff --git a/backend/static/js/pages/lost.js b/backend/static/js/pages/lost.js index ad6de69..10b96d3 100644 --- a/backend/static/js/pages/lost.js +++ b/backend/static/js/pages/lost.js @@ -130,54 +130,24 @@ window.Page_lost = (() => { document.getElementById('lost-btn-report') ?.addEventListener('click', _showReportForm); - await _loadLeaflet(); - _initMap(); + await _initMap(); setTimeout(() => _map?.invalidateSize(), 100); await _locateAndLoad(); } // ---------------------------------------------------------- - // LEAFLET DYNAMISCH LADEN + // KARTE INITIALISIEREN (lädt Leaflet via UI.map.create) // ---------------------------------------------------------- - async function _loadLeaflet() { - if (_leafletLoaded || window.L) { _leafletLoaded = true; return; } - - 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); - }); - - 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 - // ---------------------------------------------------------- - function _initMap() { + async function _initMap() { _injectStyles(); const mapEl = document.getElementById('lost-map'); - if (!mapEl || !window.L || _map) return; + if (!mapEl || _map) return; - _map = L.map('lost-map', { zoomControl: true, attributionControl: false }) - .setView([51.1657, 10.4515], 6); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - }).addTo(_map); + _map = await UI.map.create('lost-map', { + center: [51.1657, 10.4515], zoom: 6, + zoomControl: true, attributionControl: false, + }); + _leafletLoaded = true; } // ---------------------------------------------------------- @@ -303,22 +273,17 @@ window.Page_lost = (() => { _reports.forEach(r => { const dotColor = r._isPending ? '#d97706' : '#e74c3c'; const anim = r._isPending ? 'by-lost-pulse-p' : 'by-lost-pulse-r'; - const icon = L.divIcon({ - className : '', - html : `
🐕
`, - iconSize : [34, 34], - iconAnchor : [17, 17], - }); + animation:${anim} 1.8s ease-in-out infinite">🐕`; const distStr = r.distanz_m !== undefined ? (r.distanz_m < 1000 ? `${Math.round(r.distanz_m)} m` : `${(r.distanz_m / 1000).toFixed(1)} km`) : ''; - const marker = L.marker([r.lat, r.lon], { icon }) + const marker = UI.map.svgMarker(r.lat, r.lon, html, { size: 34, anchorY: 17 }) .addTo(_map) .bindPopup(` 🔍 ${_escape(r.name)}
diff --git a/backend/static/js/pages/poison.js b/backend/static/js/pages/poison.js index 6547a25..f9d1151 100644 --- a/backend/static/js/pages/poison.js +++ b/backend/static/js/pages/poison.js @@ -94,8 +94,7 @@ window.Page_poison = (() => { document.getElementById('poison-btn-erstehilfe') ?.addEventListener('click', () => App.navigate('erste-hilfe', true, { tab: 'lebensgefahr' })); - await UI.loadLeaflet(); - _initMap(); + await _initMap(); // Leaflet muss nach CSS-Load die Container-Größe neu berechnen setTimeout(() => _map?.invalidateSize(), 100); await _locateAndLoad(); @@ -104,17 +103,16 @@ window.Page_poison = (() => { // ---------------------------------------------------------- // KARTE INITIALISIEREN // ---------------------------------------------------------- - function _initMap() { + async function _initMap() { const mapEl = document.getElementById('poison-map'); - if (!mapEl || !window.L || _map) return; - - _map = L.map('poison-map', { zoomControl: true, attributionControl: false }) - .setView([51.1657, 10.4515], 6); // Deutschland-Mitte - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - }).addTo(_map); + if (!mapEl || _map) return; + _map = await UI.map.create('poison-map', { + center: [51.1657, 10.4515], // Deutschland-Mitte + zoom: 6, + zoomControl: true, + attributionControl: false, + }); } // ---------------------------------------------------------- diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 1f5bc33..acb48c6 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -193,20 +193,15 @@ window.Page_routes = (() => { background:var(--c-surface);color:var(--c-text);outline:none; box-sizing:border-box;"> -
+
@@ -602,16 +597,18 @@ window.Page_routes = (() => {
`; - const _initMap = () => { + const _initMap = async () => { const mapEl = document.getElementById('rks-map'); - if (!mapEl || !window.L) return; + if (!mapEl) return; if (_suggestMap) { _suggestMap.remove(); _suggestMap = null; } const track = result.gps_track || []; if (track.length < 2) return; const lls = track.map(p => [p.lat, p.lon]); - _suggestMap = L.map(mapEl, { zoomControl: false, attributionControl: false, - dragging: true, touchZoom: true, scrollWheelZoom: false }); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_suggestMap); + _suggestMap = await UI.map.create(mapEl, { + center: lls[0], zoom: 14, + zoomControl: false, attributionControl: false, + }); + _suggestMap.scrollWheelZoom.disable(); const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap); L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap); L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap); @@ -619,12 +616,7 @@ window.Page_routes = (() => { _suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] }); setTimeout(() => _suggestMap?.invalidateSize(), 120); }; - if (window.L) { _initMap(); } else { - let tries = 0; - const poll = setInterval(() => { - if (window.L || ++tries > 40) { clearInterval(poll); if (window.L) _initMap(); } - }, 100); - } + _initMap(); document.getElementById('rks-nav-btn')?.addEventListener('click', () => { _openNavOverlay({ id: 'suggest-' + Date.now(), name: result.name, @@ -668,8 +660,6 @@ window.Page_routes = (() => { if (!_appState.user) { UI.toast.warning('Bitte anmelden.'); return; } if (_recOvl) return; - try { await (UI.loadLeaflet?.() ?? Promise.resolve()); } - catch { UI.toast.warning('Karte offline nicht verfügbar — GPS-Aufzeichnung läuft trotzdem.'); } const ovl = document.createElement('div'); ovl.id = 'rk-rec-ovl'; @@ -733,10 +723,10 @@ window.Page_routes = (() => { // Map-Setup: Leaflet könnte offline fehlen → alles in try/catch const pos = _userPos || { lat: 48.1, lon: 11.5 }; try { - if (!window.L) throw new Error('Leaflet not loaded'); - _recMap = L.map(ovl.querySelector('#rk-rec-map-wrap'), { zoomControl: false, attributionControl: false }) - .setView([pos.lat, pos.lon], 15); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_recMap); + _recMap = await UI.map.create(ovl.querySelector('#rk-rec-map-wrap'), { + center: [pos.lat, pos.lon], zoom: 15, + zoomControl: false, attributionControl: false, + }); _recLocMarker = L.circleMarker([pos.lat, pos.lon], { radius: 8, color: '#fff', weight: 2.5, fillColor: '#3b82f6', fillOpacity: 1 }).addTo(_recMap); @@ -1127,8 +1117,7 @@ window.Page_routes = (() => { document.body.appendChild(sec); document.getElementById('rk-map-back')?.addEventListener('click', () => _switchView('list')); - // Wie _initMiniMaps: pollen bis window.L bereit ist - _pollAndInitSearchMap(); + _initSearchMap(); } else { document.getElementById('rk-map-section')?.remove(); @@ -1140,26 +1129,16 @@ window.Page_routes = (() => { // ---------------------------------------------------------- // Suchkarte // ---------------------------------------------------------- - function _pollAndInitSearchMap() { - if (window.L) { _initSearchMap(); return; } - let tries = 0; - const poll = setInterval(() => { - if (window.L || ++tries > 40) { - clearInterval(poll); - if (window.L) _initSearchMap(); - } - }, 100); - } - - function _initSearchMap() { + async function _initSearchMap() { if (!document.getElementById('rk-search-map')) return; const center = _userPos ? [_userPos.lat, _userPos.lon] : [51.1, 10.4]; const zoom = _userPos ? 13 : 6; - _searchMap = L.map('rk-search-map', { zoomControl: true, attributionControl: false }) - .setView(center, zoom); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_searchMap); + _searchMap = await UI.map.create('rk-search-map', { + center, zoom, + zoomControl: true, attributionControl: false, + }); setTimeout(() => _searchMap?.invalidateSize(), 100); setTimeout(() => _searchMap?.invalidateSize(), 600); _renderRoutesOnMap(); @@ -1586,8 +1565,6 @@ window.Page_routes = (() => { try { await DeviceOrientationEvent.requestPermission(); } catch {} } - await UI.loadLeaflet?.() ?? Promise.resolve(); - const ovl = document.createElement('div'); ovl.id = 'rk-nav-ovl'; ovl.style.cssText = 'position:fixed;inset:0;z-index:850;display:flex;flex-direction:column;background:var(--c-bg)'; @@ -1700,9 +1677,10 @@ window.Page_routes = (() => { // Karte initialisieren const mapEl = document.getElementById('rk-nav-map'); const mid = track[Math.floor(track.length / 2)]; - _navMap = L.map(mapEl, { zoomControl: false, attributionControl: false }) - .setView([mid.lat, mid.lon], 15); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_navMap); + _navMap = await UI.map.create(mapEl, { + center: [mid.lat, mid.lon], zoom: 15, + zoomControl: false, attributionControl: false, + }); // Route-Polylines: erledigt (grün) + ausstehend (orange) const doneLine = L.polyline([], { color: '#22c55e', weight: 5, opacity: 0.85 }).addTo(_navMap); @@ -2106,12 +2084,12 @@ window.Page_routes = (() => { document.body.appendChild(ovl); // Map initialisieren - await UI.loadLeaflet?.() ?? Promise.resolve(); const mapEl = document.getElementById('rk-trim-map'); const center = fullTrack[Math.floor(fullTrack.length/2)]; - const trimMap = L.map(mapEl, { zoomControl: false, attributionControl: false }) - .setView([center.lat, center.lon], 14); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(trimMap); + const trimMap = await UI.map.create(mapEl, { + center: [center.lat, center.lon], zoom: 14, + zoomControl: false, attributionControl: false, + }); // Marker & Polylines let greyBefore = L.polyline([], { color: '#9ca3af', weight: 3, opacity: 0.5 }).addTo(trimMap); @@ -2368,9 +2346,9 @@ window.Page_routes = (() => { route.gps_track = [...route.gps_track].reverse(); // Karte neu aufbauen mit umgekehrtem Track const el = document.getElementById('rk-detail-map'); - if (el && window.L) { + if (el) { if (_detailMap) { _detailMap.remove(); _detailMap = null; } - _detailMap = _buildDetailMap(el, route.gps_track); + _detailMap = await _buildDetailMap(el, route.gps_track); } UI.toast.success('Route dauerhaft umgekehrt'); } catch (err) { UI.toast.error(err.message); } @@ -2424,10 +2402,10 @@ window.Page_routes = (() => { // Mini-Map let _detailMap = null; - setTimeout(() => { + setTimeout(async () => { const el = document.getElementById('rk-detail-map'); if (!el || !track.length) return; - if (window.L) _detailMap = _buildDetailMap(el, track); + _detailMap = await _buildDetailMap(el, track); }, 80); // Nearby POIs laden @@ -2546,10 +2524,12 @@ window.Page_routes = (() => { } } - function _buildDetailMap(el, track) { - const m = L.map(el, { zoomControl: false, attributionControl: false }); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(m); + async function _buildDetailMap(el, track) { const lls = track.map(p => [p.lat, p.lon]); + const m = await UI.map.create(el, { + center: lls[0], zoom: 14, + zoomControl: false, attributionControl: false, + }); const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.85 }).addTo(m); _addRouteArrows(m, track, '#3b82f6'); L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1 }).addTo(m); diff --git a/backend/static/js/pages/walks.js b/backend/static/js/pages/walks.js index 8b6dc24..e56f569 100644 --- a/backend/static/js/pages/walks.js +++ b/backend/static/js/pages/walks.js @@ -222,8 +222,7 @@ window.Page_walks = (() => { document.getElementById('walks-map-view').style.display = view === 'karte' ? '' : 'none'; if (view === 'karte') { - UI.loadLeaflet().then(() => { - _initMap(); + _initMap().then(() => { setTimeout(() => _map?.invalidateSize(), 150); setTimeout(() => _map?.invalidateSize(), 400); }); @@ -245,8 +244,7 @@ window.Page_walks = (() => { _renderList(); _renderMarkers(); if (window.innerWidth >= 1024) { - UI.loadLeaflet().then(() => { - _initMap(); + _initMap().then(() => { setTimeout(() => _map?.invalidateSize(), 150); setTimeout(() => _map?.invalidateSize(), 400); }); @@ -363,13 +361,11 @@ window.Page_walks = (() => { // ---------------------------------------------------------- // Leaflet + Karte // ---------------------------------------------------------- - function _initMap() { + async function _initMap() { const el = document.getElementById('walks-map'); - if (!el || !window.L || _map) return; + if (!el || _map) return; const center = _userPos ? [_userPos.lat, _userPos.lon] : [51.1657, 10.4515]; - _map = L.map('walks-map', { zoomControl: true, attributionControl: false }) - .setView(center, _userPos ? 12 : 6); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_map); + _map = await UI.map.create('walks-map', { center, zoom: _userPos ? 12 : 6 }); _renderMarkers(); } diff --git a/backend/static/landing.html b/backend/static/landing.html index f5e3ad8..568d8bd 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index d0ef7bf..d02565b 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1106'; +const VER = '1107'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten