Karten-Ausbau (OSM), Forum-Erweiterung, UI-Komponenten, Refactor Tagebuch/Gassi (DRY), Landing/SEO — APP_VER 1155

This commit is contained in:
rene 2026-06-03 17:24:47 +02:00
parent 2d907f6370
commit 10e39ed135
18 changed files with 871 additions and 405 deletions

View file

@ -59,6 +59,7 @@ window.Page_map = (() => {
treffpunkt: [],
community: [],
zuechter: [],
hotel: [],
};
const VISIBLE_KEY = 'by_map_visible_v1';
@ -130,6 +131,10 @@ window.Page_map = (() => {
interactive: false,
};
// Orts-Suche
let _searchTimer = null;
let _searchMarker = null;
let _overpassTimer = null;
let _overpassActive = false;
let _ringClosing = false;
@ -210,13 +215,50 @@ window.Page_map = (() => {
</div>
</div>
<div class="map-fabs">
<button class="map-fab map-fab--pin" id="map-pin-btn" title="Marker setzen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg></button>
${App.hasPro(_appState?.user) ? `
<button class="map-fab" id="map-radar-btn" title="Regenradar ein-/ausblenden"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#cloud-rain"></use></svg></button>
<button class="map-fab" id="map-temp-btn" title="Temperatur ein-/ausblenden"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#thermometer"></use></svg></button>
` : ''}
<button class="map-fab" id="map-locate-btn" title="Meinen Standort"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
<!-- Orts-Suche Panel (von oben einschiebend, geschlossen per default) -->
<div class="map-search-wrap" id="map-search-wrap">
<div class="map-search-row">
<svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px;flex-shrink:0;color:#888"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
<input type="search" id="map-search-input" class="map-search-input"
placeholder="Ort oder Adresse…" autocomplete="off" autocorrect="off" spellcheck="false">
<button class="map-search-clear" id="map-search-clear" aria-label="Suche schließen">
<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
<div class="map-search-results" id="map-search-results" style="display:none"></div>
</div>
<!-- Speed Dial -->
<div class="map-speed-dial" id="map-speed-dial">
<div class="map-sd-items">
<!-- DOM-Reihenfolge = Aufklappreihenfolge von unten nach oben -->
<div class="map-sd-item">
<span class="map-sd-label">Mein Standort</span>
<button class="map-sd-btn" id="map-locate-btn" title="Mein Standort"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
</div>
<div class="map-sd-item">
<span class="map-sd-label">Ort suchen</span>
<button class="map-sd-btn" id="map-search-btn" title="Ort suchen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg></button>
</div>
<div class="map-sd-item">
<span class="map-sd-label">Marker setzen</span>
<button class="map-sd-btn map-fab--pin" id="map-pin-btn" title="Marker setzen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg></button>
</div>
${App.hasPro(_appState?.user) ? `
<div class="map-sd-item">
<span class="map-sd-label">Regenradar</span>
<button class="map-sd-btn" id="map-radar-btn" title="Regenradar"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#cloud-rain"></use></svg></button>
</div>
<div class="map-sd-item">
<span class="map-sd-label">Temperatur</span>
<button class="map-sd-btn" id="map-temp-btn" title="Temperatur"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#thermometer"></use></svg></button>
</div>
` : ''}
</div>
<button class="map-fab map-sd-trigger" id="map-sd-trigger" title="Karten-Aktionen">
<svg class="ph-icon map-sd-icon-open" aria-hidden="true"><use href="/icons/phosphor.svg#dots-three-vertical"></use></svg>
<svg class="ph-icon map-sd-icon-close" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
<div class="map-statusbar" id="map-statusbar">
@ -289,7 +331,19 @@ window.Page_map = (() => {
_saveVisible();
});
// Speed Dial
const _sdEl = document.getElementById('map-speed-dial');
document.getElementById('map-sd-trigger')?.addEventListener('click', e => {
e.stopPropagation();
_sdEl?.classList.toggle('open');
});
// Klick auf Karte / außerhalb schließt Speed Dial
document.getElementById('central-map')?.addEventListener('pointerdown', () => {
_sdEl?.classList.remove('open');
});
document.getElementById('map-locate-btn').addEventListener('click', () => {
_sdEl?.classList.remove('open');
if (_userPos) {
_map?.setView([_userPos.lat, _userPos.lon], 16);
} else {
@ -297,9 +351,54 @@ window.Page_map = (() => {
}
});
document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode);
document.getElementById('map-radar-btn')?.addEventListener('click', _toggleRadar);
document.getElementById('map-temp-btn')?.addEventListener('click', _toggleTemp);
document.getElementById('map-pin-btn').addEventListener('click', () => {
_sdEl?.classList.remove('open');
_togglePlacementMode();
});
document.getElementById('map-radar-btn')?.addEventListener('click', () => {
_sdEl?.classList.remove('open');
_toggleRadar();
});
document.getElementById('map-temp-btn')?.addEventListener('click', () => {
_sdEl?.classList.remove('open');
_toggleTemp();
});
// Suche — FAB öffnet Panel
document.getElementById('map-search-btn')?.addEventListener('click', () => {
document.getElementById('map-speed-dial')?.classList.remove('open');
const wrap = document.getElementById('map-search-wrap');
const isOpen = wrap?.classList.contains('active');
if (isOpen) {
_clearSearch();
} else {
wrap?.classList.add('active');
setTimeout(() => document.getElementById('map-search-input')?.focus(), 60);
document.getElementById('map-search-btn')?.classList.add('active');
}
});
const searchInput = document.getElementById('map-search-input');
const searchResults = document.getElementById('map-search-results');
searchInput?.addEventListener('input', () => {
const q = searchInput.value.trim();
clearTimeout(_searchTimer);
if (q.length < 2) { searchResults.style.display = 'none'; return; }
_searchTimer = setTimeout(() => _runSearch(q), 400);
});
searchInput?.addEventListener('keydown', e => {
if (e.key === 'Escape') _clearSearch();
});
document.getElementById('map-search-clear')?.addEventListener('click', _clearSearch);
// Klick auf Karte schließt Ergebnisse (aber behält Marker)
document.getElementById('central-map')?.addEventListener('pointerdown', () => {
searchResults.style.display = 'none';
searchInput?.blur();
});
}
// ----------------------------------------------------------
@ -907,7 +1006,7 @@ window.Page_map = (() => {
const params = new URLSearchParams({ type: osmType, ...bbox });
try {
const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json());
const osmCount = _layers[layerKey].filter(m => !m._ownPlace).length;
const osmCount = (_layers[layerKey] || []).filter(m => !m._ownPlace).length;
if (pois.length !== osmCount) _replaceOsmMarkers(layerKey, pois);
_done++;
const pct = Math.round(20 + _done / _total * 80);
@ -919,11 +1018,14 @@ window.Page_map = (() => {
const pct = Math.round(20 + _done / _total * 80);
const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length;
_setOsmStatus(pct < 100 ? `Scanne…` : `${total} Marker`, pct);
return _layers[layerKey].filter(m => !m._ownPlace).length;
return (_layers[layerKey] || []).filter(m => !m._ownPlace).length;
}
});
await Promise.all(freshTasks);
_overpassActive = false;
try {
await Promise.all(freshTasks);
} finally {
_overpassActive = false;
}
const totalLoaded = Object.values(_layers).flat().filter(m => !m._ownPlace).length;
const allHidden = Object.keys(OSM_LAYER_MAP).every(k => _visible[k] === false);
@ -931,10 +1033,13 @@ window.Page_map = (() => {
_setOsmStatus('Layer deaktiviert — Liste antippen', 100);
}
// Wenn 0 OSM-Marker: Hintergrund-Fetch läuft noch — max 3× automatisch nachfragen
if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 3) {
// Wenn 0 OSM-Marker: Hintergrund-Overpass-Fetch läuft noch — bis zu 8× nachfragen
// Overpass für alle Layer sequential: bis zu ~4min → Retries müssen das abdecken
if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 8) {
_autoRetryCount++;
const delay = _autoRetryCount * 30000; // 30s, 60s, 90s
// 10s, 20s, 35s, 50s, 70s, 90s, 120s, 150s
const delays = [10000, 20000, 35000, 50000, 70000, 90000, 120000, 150000];
const delay = delays[_autoRetryCount - 1] || 120000;
_setOsmStatus(`Neue Umgebung Daten werden geladen…`);
setTimeout(() => { if (!_overpassActive) _scheduleOsmLoad(); }, delay);
}
@ -1944,6 +2049,92 @@ window.Page_map = (() => {
} catch { /* still */ }
}
// ----------------------------------------------------------
// Orts-Suche (Nominatim-Proxy)
// ----------------------------------------------------------
async function _runSearch(q) {
const resultsEl = document.getElementById('map-search-results');
if (!resultsEl) return;
resultsEl.innerHTML = '<div class="map-search-loading">Suche…</div>';
resultsEl.style.display = '';
try {
const data = await API.get(`/osm/geocode?q=${encodeURIComponent(q)}`);
if (!data.length) {
resultsEl.innerHTML = '<div class="map-search-empty">Keine Ergebnisse</div>';
return;
}
resultsEl.innerHTML = data.map((r, i) =>
`<div class="map-search-item" data-i="${i}">
<div class="map-search-item-name">${UI.escape(r.name)}</div>
${r.subtitle ? `<div class="map-search-item-sub">${UI.escape(r.subtitle)}</div>` : ''}
</div>`
).join('');
resultsEl.querySelectorAll('.map-search-item').forEach(el => {
el.addEventListener('pointerdown', e => {
e.stopPropagation();
const r = data[+el.dataset.i];
_flyToResult(r);
document.getElementById('map-search-input').value = r.name;
document.getElementById('map-search-clear').style.display = '';
resultsEl.style.display = 'none';
});
});
} catch {
resultsEl.innerHTML = '<div class="map-search-empty">Suche nicht verfügbar</div>';
}
}
function _flyToResult(r) {
if (!_map || !window.L) return;
_searchMarker?.remove();
_map.flyTo([r.lat, r.lon], 15, { duration: 1.0 });
_searchMarker = L.marker([r.lat, r.lon], {
icon: L.divIcon({
className: '',
html: `<div style="background:#C4843A;color:#fff;font-size:15px;
width:32px;height:32px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);
display:flex;align-items:center;justify-content:center;
box-shadow:0 2px 8px rgba(0,0,0,0.4)">
<span style="transform:rotate(45deg)">
<svg style="width:16px;height:16px" viewBox="0 0 256 256" fill="currentColor">
<path d="M128,16a96,96,0,1,0,96,96A96.11,96.11,0,0,0,128,16Zm0,48a32,32,0,1,1-32,32A32,32,0,0,1,128,64Zm0,144a80,80,0,0,1-56.37-23.37C74.18,170.06,98.65,160,128,160s53.82,10.06,56.37,24.63A80,80,0,0,1,128,208Z"/>
</svg>
</span></div>`,
iconSize: [32, 32],
iconAnchor: [16, 32],
}),
zIndexOffset: 1000,
})
.addTo(_map)
.bindPopup(`<div style="font-size:13px;font-weight:600">${UI.escape(r.name)}</div>
${r.subtitle ? `<div style="font-size:11px;color:#888">${UI.escape(r.subtitle)}</div>` : ''}
<button class="btn btn-secondary btn-sm" id="search-marker-close" style="margin-top:8px">
Marker entfernen
</button>`, { maxWidth: 240 })
.openPopup();
setTimeout(() => {
document.getElementById('search-marker-close')?.addEventListener('click', () => {
_clearSearch();
_searchMarker?.closePopup();
});
}, 50);
}
function _clearSearch() {
const input = document.getElementById('map-search-input');
const results = document.getElementById('map-search-results');
const wrap = document.getElementById('map-search-wrap');
const btn = document.getElementById('map-search-btn');
if (input) { input.value = ''; input.blur(); }
if (results) results.style.display = 'none';
wrap?.classList.remove('active');
btn?.classList.remove('active');
_searchMarker?.remove();
_searchMarker = null;
clearTimeout(_searchTimer);
}
return { init, refresh, onDogChange, startRecording: _startRecording, stopRecording: _stopRecording, isRecording: () => _recActive };
})();