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

@ -453,6 +453,10 @@ const UI = (() => {
const isDark = document.documentElement.dataset.theme === 'dark';
if (isDark) tiles.getContainer().style.filter = 'brightness(0.7) invert(1) contrast(0.9) hue-rotate(200deg)';
}
// Safety-Net: Container-Größe nach Layout neu vermessen. Verhindert
// grau bleibende Bereiche wenn die Karte vor dem finalen Layout erstellt
// wird (z.B. in frisch eingefügten Overlays mit flex:1).
requestAnimationFrame(() => m.invalidateSize());
return m;
},
@ -873,12 +877,35 @@ const UI = (() => {
coordsClear: `${p}-coords-clear`,
suggestions: `${p}-suggestions`,
pinHere: `${p}-pin-here`,
geoInput: `${p}-geo-input`,
geoClear: `${p}-geo-clear`,
geoResults: `${p}-geo-results`,
};
// HTML in den Container rendern
function _render(container) {
container.innerHTML = `
<div style="position:relative">
<!-- Geocoding-Suchfeld als Overlay oben left:46px lässt Zoom-Control frei -->
<div style="position:absolute;top:8px;left:46px;right:8px;z-index:1001">
<div style="display:flex;align-items:center;gap:7px;background:rgba(255,255,255,0.96);
border-radius:var(--radius-full);padding:6px 11px;
box-shadow:0 2px 8px rgba(0,0,0,0.22);backdrop-filter:blur(4px)">
<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px;flex-shrink:0;color:#aaa"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
<input type="search" id="${ids.geoInput}" placeholder="Ort oder Adresse suchen…"
autocomplete="off" autocorrect="off" spellcheck="false"
style="flex:1;border:none;outline:none;font-size:13px;background:transparent;
font-family:inherit;color:var(--c-text);min-width:0">
<button type="button" id="${ids.geoClear}" aria-label="Suche löschen"
style="display:none;background:none;border:none;padding:2px;cursor:pointer;
color:#bbb;line-height:1">
<svg class="ph-icon" aria-hidden="true" style="width:13px;height:13px"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
<div id="${ids.geoResults}" style="display:none;background:rgba(255,255,255,0.98);
border-radius:10px;box-shadow:0 4px 14px rgba(0,0,0,0.18);
margin-top:5px;overflow:hidden;max-height:190px;overflow-y:auto"></div>
</div>
<div id="${ids.mapWrap}" style="border-radius:var(--radius-md);overflow:hidden;height:200px;background:var(--c-surface-2)"></div>
<button type="button" id="${ids.pinHere}" style="
position:absolute;bottom:10px;left:50%;transform:translateX(-50%);
@ -1102,6 +1129,75 @@ const UI = (() => {
}
_getEl(ids.locBtn)?.addEventListener('click', _showSuggestions);
// Geocoding-Suche
let _geoTimer = null;
const geoInput = _getEl(ids.geoInput);
const geoClear = _getEl(ids.geoClear);
const geoResults = _getEl(ids.geoResults);
geoInput?.addEventListener('input', () => {
const q = geoInput.value.trim();
if (geoClear) geoClear.style.display = q ? '' : 'none';
clearTimeout(_geoTimer);
if (q.length < 2) { if (geoResults) geoResults.style.display = 'none'; return; }
_geoTimer = setTimeout(async () => {
if (geoResults) {
geoResults.innerHTML = '<div style="padding:9px 13px;font-size:12px;color:var(--c-text-secondary)">Suche…</div>';
geoResults.style.display = '';
}
try {
const data = await API.get(`/osm/geocode?q=${encodeURIComponent(q)}`);
if (!geoResults) return;
if (!data.length) {
geoResults.innerHTML = '<div style="padding:9px 13px;font-size:12px;color:var(--c-text-secondary)">Keine Ergebnisse</div>';
return;
}
geoResults.innerHTML = data.map((r, i) => `
<div data-i="${i}" style="padding:9px 13px;cursor:pointer;border-bottom:1px solid rgba(0,0,0,0.05)">
<div style="font-size:13px;font-weight:600;color:var(--c-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escape(r.name)}</div>
${r.subtitle ? `<div style="font-size:11px;color:var(--c-text-secondary)">${escape(r.subtitle)}</div>` : ''}
</div>`).join('');
geoResults.querySelectorAll('[data-i]').forEach(el => {
el.addEventListener('pointerdown', e => {
e.preventDefault();
const r = data[+el.dataset.i];
_setCoords(r.lat, r.lon);
_setName(r.name);
if (_map) {
_map.flyTo([r.lat, r.lon], 15, { duration: 0.8 });
_placeMarker(r.lat, r.lon);
}
const lbl = _getEl(ids.locBtnLabel);
if (lbl) lbl.textContent = 'POI suchen';
geoInput.value = '';
if (geoClear) geoClear.style.display = 'none';
geoResults.style.display = 'none';
onSelect?.(_lat, _lon, _name);
});
});
} catch {
if (geoResults) geoResults.innerHTML = '<div style="padding:9px 13px;font-size:12px;color:var(--c-text-secondary)">Suche nicht verfügbar</div>';
}
}, 400);
});
geoInput?.addEventListener('keydown', e => {
if (e.key === 'Escape') {
geoInput.value = '';
if (geoClear) geoClear.style.display = 'none';
if (geoResults) geoResults.style.display = 'none';
}
});
geoClear?.addEventListener('click', () => {
geoInput.value = '';
geoClear.style.display = 'none';
if (geoResults) geoResults.style.display = 'none';
});
_getEl(ids.mapWrap)?.addEventListener('pointerdown', () => {
if (geoResults) geoResults.style.display = 'none';
geoInput?.blur();
});
}
// Container initialisieren