Refactor: UI.loadLeaflet, leafletMarker, escape, emptyState, locationPicker zentralisiert

- Task 1: UI.loadLeaflet() in ui.js (mit Cluster-Option), lokale _loadLeaflet() in
  diary/walks/routes/places/poison/events.js entfernt
- Task 2: UI.escape() ersetzt lokale _esc()/_escape() in allen 5 Seiten-Modulen
- Task 3: UI.emptyState() ersetzt lokale _emptyState() in diary/routes/events.js
- Task 4: _fmtDate/_fmtDateShort in walks/poison bewusst behalten (anderes Format),
  Kommentare ergänzt
- Task 5: UI.locationPicker() eingebaut in places/poison/events (ersetzt manuelle
  GPS-Input-Blöcke)
- Task 6: UI.leafletMarker() factory in ui.js, Kreis-divIcon-Blöcke in walks/places/
  poison ersetzt; events.js behält Diamant-Marker (andere Form)
- SW by-v207, APP_VER 175
This commit is contained in:
rene 2026-04-18 14:34:35 +02:00
parent 066b722c5e
commit e98ce0d232
9 changed files with 761 additions and 471 deletions

View file

@ -17,26 +17,6 @@ window.Page_diary = (() => {
let _filterMilestone = false;
const LIMIT = 20;
function _loadLeaflet() {
if (window.L) return Promise.resolve();
return new Promise((resolve, reject) => {
const cssLoaded = document.querySelector('link[href*="leaflet"]')
? Promise.resolve()
: new Promise(res => {
const link = document.createElement('link');
link.rel = 'stylesheet'; link.href = '/css/leaflet.css';
link.onload = res; link.onerror = res;
document.head.appendChild(link);
});
cssLoaded.then(() => {
if (document.querySelector('script[src*="leaflet.js"]')) { resolve(); return; }
const s = document.createElement('script');
s.src = '/js/leaflet.js'; s.onload = resolve; s.onerror = reject;
document.head.appendChild(s);
});
});
}
function _sourceIcon(source) {
if (source === 'places') return 'star';
if (source === 'osm') return 'map-pin';
@ -70,7 +50,7 @@ window.Page_diary = (() => {
async function init(container, appState) {
_container = container;
_appState = appState;
_loadLeaflet(); // Leaflet im Hintergrund vorladen — kein await
UI.loadLeaflet(); // Leaflet im Hintergrund vorladen — kein await
await _render();
}
@ -139,14 +119,14 @@ window.Page_diary = (() => {
const cards = _appState.dogs.map(dog => {
const isActive = dog.id === activeDogId;
const av = dog.foto_url
? `<img src="${_escape(dog.foto_url)}" alt="${_escape(dog.name)}">`
? `<img src="${UI.escape(dog.foto_url)}" alt="${UI.escape(dog.name)}">`
: `<span>${UI.icon('dog')}</span>`;
return `
<div class="diary-picker-card${isActive ? ' diary-picker-card--active' : ''}"
data-dog-id="${dog.id}">
<div class="diary-picker-av">${av}</div>
<div class="diary-picker-name">${_escape(dog.name)}</div>
${dog.rasse ? `<div class="diary-picker-rasse">${_escape(dog.rasse)}</div>` : ''}
<div class="diary-picker-name">${UI.escape(dog.name)}</div>
${dog.rasse ? `<div class="diary-picker-rasse">${UI.escape(dog.rasse)}</div>` : ''}
</div>`;
}).join('');
@ -260,20 +240,6 @@ window.Page_diary = (() => {
UI.setLoading(btn, false);
}
// ----------------------------------------------------------
// EMPTY-STATE HELPER
// ----------------------------------------------------------
function _emptyState(icon, title, text, cta = '') {
return `<div class="empty-state">
<svg class="ph-icon empty-state-icon" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
<div class="empty-state-title">${title}</div>
${text ? `<p class="empty-state-text">${text}</p>` : ''}
${cta ? `<div class="empty-state-cta">${cta}</div>` : ''}
</div>`;
}
// ----------------------------------------------------------
// LISTE RENDERN — Timeline gruppiert nach Monat
// ----------------------------------------------------------
@ -282,12 +248,12 @@ window.Page_diary = (() => {
if (!listEl) return;
if (_entries.length === 0) {
listEl.innerHTML = _emptyState(
'book-open',
'Noch keine Tagebucheinträge',
'Halte besondere Momente mit deinem Hund fest — Spaziergänge, Erlebnisse, Erinnerungen.',
`<button class="btn btn-primary" id="diary-first-entry">Ersten Eintrag schreiben</button>`
);
listEl.innerHTML = UI.emptyState({
icon: UI.icon('book-open'),
title: 'Noch keine Tagebucheinträge',
text: 'Halte besondere Momente mit deinem Hund fest — Spaziergänge, Erlebnisse, Erinnerungen.',
action: `<button class="btn btn-primary" id="diary-first-entry">Ersten Eintrag schreiben</button>`,
});
listEl.querySelector('#diary-first-entry')
?.addEventListener('click', () => _showForm(null));
return;
@ -339,11 +305,11 @@ window.Page_diary = (() => {
: '';
const locationHtml = e.location_name
? `<p class="diary-card-location"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>${_escape(e.location_name)}</p>`
? `<p class="diary-card-location"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>${UI.escape(e.location_name)}</p>`
: '';
const textPreview = e.text
? `<p class="diary-card-text">${_escape(e.text.slice(0, 140))}${e.text.length > 140 ? '…' : ''}</p>`
? `<p class="diary-card-text">${UI.escape(e.text.slice(0, 140))}${e.text.length > 140 ? '…' : ''}</p>`
: '';
// Meilenstein-Badge (nur bei is_milestone=1, nicht bei manuell gewähltem Typ 'meilenstein')
@ -363,7 +329,7 @@ window.Page_diary = (() => {
<span class="diary-card-type">${typ.icon} ${typ.label}</span>
<span class="diary-card-date">${dateStr}</span>
</div>
${e.titel ? `<div class="diary-card-title">${_escape(e.titel)}</div>` : ''}
${e.titel ? `<div class="diary-card-title">${UI.escape(e.titel)}</div>` : ''}
${locationHtml}
${textPreview}
${tagsHtml}
@ -378,8 +344,8 @@ window.Page_diary = (() => {
const avatars = dogIds.map(did => {
const dog = _appState.dogs.find(d => d.id === did);
if (!dog) return '';
return `<div class="diary-dog-av" title="${_escape(dog.name)}">
${dog.foto_url ? `<img src="${_escape(dog.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
return `<div class="diary-dog-av" title="${UI.escape(dog.name)}">
${dog.foto_url ? `<img src="${UI.escape(dog.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
</div>`;
}).join('');
return `<div class="diary-dog-row">${avatars}</div>`;
@ -408,9 +374,9 @@ window.Page_diary = (() => {
const dog = _appState.dogs.find(d => d.id === did);
return dog ? `<div class="diary-dog-chip">
<div class="diary-dog-av">
${dog.foto_url ? `<img src="${_escape(dog.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
${dog.foto_url ? `<img src="${UI.escape(dog.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
</div>
<span>${_escape(dog.name)}</span>
<span>${UI.escape(dog.name)}</span>
</div>` : '';
}).join('')}
</div>`
@ -428,11 +394,11 @@ window.Page_diary = (() => {
${entry.location_name ? `
<div class="diary-detail-location">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>
${entry.gps_lat ? `<a href="https://maps.apple.com/?q=${encodeURIComponent(entry.location_name)}&ll=${entry.gps_lat},${entry.gps_lon}" target="_blank" rel="noopener" style="color:inherit">${_escape(entry.location_name)}</a>` : _escape(entry.location_name)}
${entry.gps_lat ? `<a href="https://maps.apple.com/?q=${encodeURIComponent(entry.location_name)}&ll=${entry.gps_lat},${entry.gps_lon}" target="_blank" rel="noopener" style="color:inherit">${UI.escape(entry.location_name)}</a>` : UI.escape(entry.location_name)}
</div>` : ''}
${dogsHtml}
${entry.text
? `<p style="white-space:pre-wrap;line-height:1.6;color:var(--c-text)">${_escape(entry.text)}</p>`
? `<p style="white-space:pre-wrap;line-height:1.6;color:var(--c-text)">${UI.escape(entry.text)}</p>`
: ''}
${tags.length
? `<div style="display:flex;flex-wrap:wrap;gap:var(--space-1);margin-top:var(--space-3)">
@ -486,9 +452,9 @@ window.Page_diary = (() => {
<input type="checkbox" name="extra_dog" value="${d.id}"
${entryDogIds.includes(d.id) ? 'checked' : ''}>
<div class="diary-dog-av">
${d.foto_url ? `<img src="${_escape(d.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
${d.foto_url ? `<img src="${UI.escape(d.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
</div>
<span>${_escape(d.name)}</span>
<span>${UI.escape(d.name)}</span>
</label>`).join('')}
</div>
</div>` : '';
@ -507,12 +473,12 @@ window.Page_diary = (() => {
<div class="form-group">
<label class="form-label">Titel <span style="color:var(--c-text-secondary)">(optional)</span></label>
<input class="form-control" type="text" name="titel"
value="${_escape(entry?.titel || '')}" placeholder="z.B. Erster Schultag">
value="${UI.escape(entry?.titel || '')}" placeholder="z.B. Erster Schultag">
</div>
<div class="form-group">
<label class="form-label">Text</label>
<textarea class="form-control" name="text" rows="5"
placeholder="Was ist passiert? Besonderheiten, Gedanken…">${_escape(entry?.text || '')}</textarea>
placeholder="Was ist passiert? Besonderheiten, Gedanken…">${UI.escape(entry?.text || '')}</textarea>
</div>
<div class="form-group" id="diary-location-group">
<label class="form-label">Ort <span style="color:var(--c-text-secondary)">(optional)</span></label>
@ -532,7 +498,7 @@ window.Page_diary = (() => {
<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">${_escape(entry?.location_name || '')}</span>
<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>
@ -549,7 +515,7 @@ window.Page_diary = (() => {
</div>
</div>
${dogPickerHtml}
<div class="form-group">
<div class="form-group" style="margin-top:var(--space-5)">
<input type="checkbox" name="is_milestone" id="diary-milestone-cb"
${entry?.is_milestone ? 'checked' : ''} style="display:none">
<button type="button" id="diary-milestone-btn"
@ -810,7 +776,7 @@ window.Page_diary = (() => {
});
// Karte beim Formular-Open automatisch laden
_loadLeaflet().then(() => {
UI.loadLeaflet().then(() => {
setTimeout(() => {
const lat = _locLat || 48.0, lon = _locLon || 11.9, zoom = _locLat ? 15 : 7;
_miniMap = L.map('diary-map-wrap', {
@ -854,9 +820,9 @@ window.Page_diary = (() => {
} else {
sugEl.innerHTML = suggestions.map(s => `
<button type="button" class="diary-location-suggestion"
data-name="${_escape(s.name)}" data-lat="${s.lat}" data-lon="${s.lon}">
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>${_escape(s.name)}</span>
<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 => {
@ -983,12 +949,6 @@ window.Page_diary = (() => {
.format(new Date(+y, +m - 1, 1));
}
function _escape(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ----------------------------------------------------------
// IMPORT
// ----------------------------------------------------------
@ -998,7 +958,7 @@ window.Page_diary = (() => {
body: `
<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-4)">
Importiere Einträge aus einer anderen App in das Tagebuch von
<strong>${_escape(_appState.activeDog?.name || 'deinem Hund')}</strong>.
<strong>${UI.escape(_appState.activeDog?.name || 'deinem Hund')}</strong>.
</p>
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
@ -1076,7 +1036,7 @@ window.Page_diary = (() => {
const errHtml = res.errors?.length
? `<details style="margin-top:var(--space-2)"><summary style="font-size:var(--text-xs);cursor:pointer">${res.errors.length} Fehler anzeigen</summary>
<pre style="font-size:var(--text-xs);white-space:pre-wrap;margin-top:var(--space-1)">${_escape(res.errors.join('\n'))}</pre></details>`
<pre style="font-size:var(--text-xs);white-space:pre-wrap;margin-top:var(--space-1)">${UI.escape(res.errors.join('\n'))}</pre></details>`
: '';
resultEl.innerHTML = `
@ -1100,7 +1060,7 @@ window.Page_diary = (() => {
resultEl.innerHTML = `
<div style="background:var(--c-danger-subtle);border-radius:var(--radius-md);
padding:var(--space-3) var(--space-4);color:var(--c-danger)">
Fehler: ${_escape(e.message || String(e))}
Fehler: ${UI.escape(e.message || String(e))}
</div>`;
resultEl.style.display = 'block';
UI.setLoading(btn, false);