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

@ -1306,37 +1306,7 @@ window.Page_diary = (() => {
</div>
<div class="form-group" id="diary-location-group">
<label class="form-label">Ort <span class="text-secondary">(optional)</span></label>
<!-- Karte (Lesemodus, Edit per Button aktivierbar) -->
<div style="position:relative">
<div id="diary-map-wrap" style="border-radius:var(--radius-md);overflow:hidden;height:220px;background:var(--c-surface-2)"></div>
<button type="button" id="diary-map-edit-btn" class="btn btn-secondary btn-sm"
style="position:absolute;bottom:var(--space-2);right:var(--space-2);z-index:500">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pencil-simple"></use></svg>
<span id="diary-map-edit-label">Position ändern</span>
</button>
</div>
<!-- POI-Name + Aktionen -->
<div class="mt-2">
<div id="diary-location-chip-wrap" style="${entry?.location_name ? '' : 'display:none'}">
<div class="diary-location-chip">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>
<span id="diary-location-label">${UI.escape(entry?.location_name || '')}</span>
<button type="button" id="diary-location-clear" aria-label="Name entfernen">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
</div>
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
<button type="button" class="btn btn-danger" id="diary-coords-clear">Ort entfernen</button>
<button type="button" class="btn btn-secondary btn-sm" id="diary-location-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>
<span id="diary-location-btn-label">POI suchen</span>
</button>
</div>
<div id="diary-location-suggestions" style="display:none;margin-top:var(--space-2)"></div>
</div>
<div id="diary-location-picker"></div>
</div>
${dogPickerHtml}
<div class="form-group" style="margin-top:var(--space-5)">
@ -1538,140 +1508,15 @@ window.Page_diary = (() => {
let _locLat = (entry?.gps_lat != null) ? entry.gps_lat : null;
let _locLon = (entry?.gps_lon != null) ? entry.gps_lon : null;
let _locName = entry?.location_name || null;
let _miniMap = null, _miniMarker = null;
const _pinSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="40" viewBox="0 0 32 40"><path d="M16 0C7.163 0 0 7.163 0 16c0 10 16 24 16 24S32 26 32 16C32 7.163 24.837 0 16 0z" fill="#C4843A"/><circle cx="16" cy="16" r="7" fill="white"/></svg>';
const _mkIcon = () => L.divIcon({ html: _pinSvg, className: '', iconSize: [32,40], iconAnchor: [16,40] });
function _setName(name) {
_locName = name;
document.getElementById('diary-location-label').textContent = name;
document.getElementById('diary-location-chip-wrap').style.display = '';
document.getElementById('diary-location-suggestions').style.display = 'none';
}
function _placeMarker(lat, lon) {
if (_miniMarker) { _miniMarker.setLatLng([lat, lon]); return; }
_miniMarker = L.marker([lat, lon], { draggable: false, icon: _mkIcon() }).addTo(_miniMap);
_miniMarker.on('dragend', () => {
const p = _miniMarker.getLatLng(); _locLat = p.lat; _locLon = p.lng;
document.getElementById('diary-location-btn-label').textContent = 'POI suchen';
// Location Picker (gemeinsame UI-Komponente)
setTimeout(() => {
const _diaryPicker = UI.locationPicker({
containerId: 'diary-location-picker',
onSelect: (lat, lon, name) => { _locLat = lat; _locLon = lon; _locName = name; },
});
}
document.getElementById('diary-location-clear')?.addEventListener('click', () => {
_locName = null;
document.getElementById('diary-location-chip-wrap').style.display = 'none';
});
const _clearBtn = document.getElementById('diary-coords-clear');
let _clearPending = false;
_clearBtn?.addEventListener('click', () => {
if (!_clearPending) {
_clearPending = true;
_clearBtn.textContent = 'Wirklich entfernen?';
_clearBtn.style.color = 'var(--c-danger)';
setTimeout(() => {
if (_clearPending) {
_clearPending = false;
_clearBtn.textContent = 'Ort entfernen';
_clearBtn.style.color = 'var(--c-text-muted)';
}
}, 3000);
return;
}
_clearPending = false;
_clearBtn.textContent = 'Ort entfernen';
_clearBtn.style.color = 'var(--c-text-muted)';
_locLat = null; _locLon = null; _locName = null;
document.getElementById('diary-location-chip-wrap').style.display = 'none';
document.getElementById('diary-location-suggestions').style.display = 'none';
document.getElementById('diary-location-btn-label').textContent = 'POI suchen';
if (_miniMarker) { _miniMarker.remove(); _miniMarker = null; }
if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); _setMapEditing(false); }
});
let _mapEditing = false;
function _setMapEditing(on) {
_mapEditing = on;
const lbl = document.getElementById('diary-map-edit-label');
if (lbl) lbl.textContent = on ? 'Fertig' : 'Position ändern';
if (!_miniMap) return;
if (on) {
if (_miniMarker) _miniMarker.dragging.enable();
} else {
if (_miniMarker) _miniMarker.dragging.disable();
}
}
document.getElementById('diary-map-edit-btn')?.addEventListener('click', () => {
_setMapEditing(!_mapEditing);
});
// Karte beim Formular-Open automatisch laden
UI.loadLeaflet().then(() => {
setTimeout(() => {
const lat = _locLat || 48.0, lon = _locLon || 11.9, zoom = _locLat ? 15 : 7;
_miniMap = L.map('diary-map-wrap', {
zoomControl: true, attributionControl: false,
dragging: true, scrollWheelZoom: false,
}).setView([lat, lon], zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 })
.addTo(_miniMap);
_miniMap.invalidateSize();
if (_locLat) {
_placeMarker(lat, lon);
_miniMarker.dragging.disable(); // Lesemodus: kein Drag
}
// Klick nur im Edit-Modus
_miniMap.on('click', e => {
if (!_mapEditing) return;
_locLat = e.latlng.lat; _locLon = e.latlng.lng;
_placeMarker(_locLat, _locLon);
if (!_mapEditing) _miniMarker.dragging.disable();
document.getElementById('diary-location-btn-label').textContent = 'POI suchen';
});
}, 150);
});
async function _showSuggestions() {
const btn = document.getElementById('diary-location-btn');
UI.setLoading(btn, true);
try {
let lat = _locLat, lon = _locLon;
if (lat == null || lon == null) {
const pos = await API.getLocation();
lat = pos.lat; lon = pos.lon;
_locLat = lat; _locLon = lon;
if (_miniMap) { _miniMap.setView([lat, lon], 15); _placeMarker(lat, lon); }
document.getElementById('diary-location-btn-label').textContent = 'POI suchen';
}
const suggestions = await API.diary.nearby(_appState.activeDog.id, lat, lon);
const sugEl = document.getElementById('diary-location-suggestions');
if (suggestions.length === 0) {
sugEl.innerHTML = '<p style="font-size:var(--text-sm);color:var(--c-text-secondary);padding:var(--space-2) 0">Keine Orte in der Nähe gefunden.</p>';
} else {
sugEl.innerHTML = suggestions.map(s => `
<button type="button" class="diary-location-suggestion"
data-name="${UI.escape(s.name)}" data-lat="${s.lat}" data-lon="${s.lon}">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${_sourceIcon(s.source)}"></use></svg>
<span>${UI.escape(s.name)}</span>
<small>${s.distance_m < 1000 ? s.distance_m + ' m' : (s.distance_m / 1000).toFixed(1) + ' km'}</small>
</button>`).join('');
sugEl.querySelectorAll('.diary-location-suggestion').forEach(el => {
el.addEventListener('click', () => _setName(el.dataset.name));
});
}
sugEl.style.display = '';
} catch (err) {
UI.toast.error(err?.message?.includes('GPS') || lat == null
? 'GPS nicht verfügbar.' : 'Ortssuche fehlgeschlagen.');
} finally {
UI.setLoading(btn, false);
}
}
document.getElementById('diary-location-btn')?.addEventListener('click', _showSuggestions);
if (_locLat != null) _diaryPicker.setValue(_locLat, _locLon, _locName);
}, 50);
document.getElementById('diary-form-delete')?.addEventListener('click', async () => {
const ok = await UI.modal.confirm({