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 `
+
+
${iconHtml}
+
${escHtml(title)}
+ ${message ? `
${escHtml(message)}
` : ''}
+ ${retry ? `
+
+ ${_svgIcon('arrow-clockwise')} Erneut versuchen
+
+
` : ''}
+
+ `;
+ }
+
+ // ----------------------------------------------------------
+ // 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 ? `${escHtml(label)}${required ? ' *' : ''} ` : ''}
+
+ `;
+ }
+
+ // ----------------------------------------------------------
+ // 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