diff --git a/VERSION b/VERSION index 4d64262..0da1d63 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1102 \ No newline at end of file +1103 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 1800abd..249f2d3 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,13 +86,13 @@ Ban Yaro - + - - - - + + + + @@ -616,11 +616,11 @@ - - - - - + + + + + @@ -630,7 +630,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index aeb7b90..2af1e81 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 = '1102'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1103'; // ← 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/ui.js b/backend/static/js/ui.js index e624c55..0d43666 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -291,11 +291,162 @@ const UI = (() => { ${icon ? `
${icon}
` : ''} ${title ? `
${title}
` : ''} ${text ? `
${text}
` : ''} - ${action ? `
${action}
` : ''} + ${action ? `
${action}
` : ''} `; } + // ---------------------------------------------------------- + // ERROR-STATE (dedizierte Error-UI, optional mit Retry-Button) + // ---------------------------------------------------------- + // Verwendung: + // container.innerHTML = UI.errorState({ + // title: 'Fehler beim Laden', + // message: err.message, + // retry: async () => { await _loadData(); } + // }); + let _errorRetryHandlers = new Map(); + function errorState({ icon, title = 'Etwas ist schiefgelaufen', message = '', retry = null } = {}) { + const iconHtml = icon || _svgIcon('warning-circle'); + const retryId = retry ? `err-retry-${Date.now()}-${Math.random().toString(36).slice(2,7)}` : ''; + if (retry) _errorRetryHandlers.set(retryId, retry); + + setTimeout(() => { + if (!retryId) return; + const btn = document.getElementById(retryId); + const fn = _errorRetryHandlers.get(retryId); + if (btn && fn) { + btn.addEventListener('click', () => asyncButton(btn, fn)); + _errorRetryHandlers.delete(retryId); + } + }, 0); + + return ` + + `; + } + + // ---------------------------------------------------------- + // SKELETON LIST — Karten-Skeleton für Listen-Loading + // ---------------------------------------------------------- + // Verwendung: container.innerHTML = UI.skeletonList(5); + function skeletonList(count = 4) { + return `
${ + Array.from({ length: count }, () => ` +
+
+
+
+
+
+
+ `).join('') + }
+ `; + } + + // ---------------------------------------------------------- + // MONEY-INPUT (Euro-Input mit Locale-Format) + // ---------------------------------------------------------- + // Verwendung: UI.moneyInput({ name: 'betrag', value: 12.50, required: true }) + // Rendert:
+ function moneyInput({ name, value = '', placeholder = '0,00', required = false, currency = '€' } = {}) { + const val = (value === '' || value == null) ? '' : Number(value).toFixed(2).replace('.', ','); + return ` +
+ ${currency} + +
+ `; + } + + // Money parser: Frontend-Helper für Form-Submit + function parseMoney(str) { + if (str == null || str === '') return null; + const cleaned = String(str).replace(',', '.').replace(/[^0-9.]/g, ''); + const n = parseFloat(cleaned); + return isNaN(n) ? null : Math.round(n * 100) / 100; + } + + // ---------------------------------------------------------- + // DATE-PICKER (Wrapper für mit Label) + // ---------------------------------------------------------- + // Verwendung: UI.datePicker({ name: 'datum', label: 'Datum', value: '2026-05-27', max: 'today' }) + function datePicker({ name, label = '', value = '', min = '', max = '', required = false } = {}) { + const today = new Date().toISOString().slice(0, 10); + const _min = min === 'today' ? today : min; + const _max = max === 'today' ? today : max; + const id = `dp-${name}-${Date.now().toString(36)}`; + return ` + ${label ? `` : ''} + + `; + } + + // ---------------------------------------------------------- + // MAP — Leaflet-Karte zentralisiert erstellen + // ---------------------------------------------------------- + // Verwendung: + // const map = await UI.map.create('mein-map', { center:[51,10], zoom:6 }); + // Optional: { darkFilter: true } für CSS-Filter im Dark-Mode + const map = { + OSM_URL: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + OSM_MAX_ZOOM: 19, + + async create(containerId, options = {}) { + await loadLeaflet(); + const { + center = [51.1657, 10.4515], + zoom = 6, + zoomControl = true, + attributionControl = false, + darkFilter = false, + } = options; + const m = L.map(containerId, { zoomControl, attributionControl }).setView(center, zoom); + const tiles = L.tileLayer(this.OSM_URL, { maxZoom: this.OSM_MAX_ZOOM }).addTo(m); + if (darkFilter) { + const isDark = document.documentElement.dataset.theme === 'dark'; + if (isDark) tiles.getContainer().style.filter = 'brightness(0.7) invert(1) contrast(0.9) hue-rotate(200deg)'; + } + return m; + }, + + // SVG-Marker mit eigenem HTML (z.B. mit Pulse-Animation, Rotation, etc.) + svgMarker(lat, lon, html, { size = 32, anchorY = null, className = '' } = {}) { + const icon = L.divIcon({ + className, + html, + iconSize: [size, size], + iconAnchor: [size / 2, anchorY != null ? anchorY : size / 2], + }); + return L.marker([lat, lon], { icon }); + }, + }; + // ---------------------------------------------------------- // DATUM-FORMATIERUNG (Deutsch, relativ) // ---------------------------------------------------------- @@ -1100,19 +1251,19 @@ const UI = (() => { toast, modal, setLoading, asyncButton, formData, setFormError, clearFormErrors, - emptyState, time, - setupPhotoPreview, scrollTop, skeleton, + emptyState, errorState, time, + setupPhotoPreview, scrollTop, skeleton, skeletonList, + moneyInput, parseMoney, datePicker, icon: _svgIcon, escape, escHtml, help, pageInfo, saveToAlbum, loadLeaflet, leafletMarker, locationPicker, + map, ratingStars, dogChip, bindDogChip, - dogChip, - bindDogChip, }; })(); diff --git a/backend/static/landing.html b/backend/static/landing.html index 8a56643..e9018f3 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 cd866e4..410846c 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 = '1102'; +const VER = '1103'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten