diff --git a/VERSION b/VERSION
index 96bf369..29001eb 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1205
\ No newline at end of file
+1206
\ No newline at end of file
diff --git a/backend/static/css/components.css b/backend/static/css/components.css
index e1ca1a3..366d2f7 100644
--- a/backend/static/css/components.css
+++ b/backend/static/css/components.css
@@ -2514,115 +2514,6 @@ html.modal-open {
text-align: center;
}
-/* ============================================================
- ORTE (places.js)
- ============================================================ */
-.places-layout {
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: hidden;
-}
-.places-toolbar {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: var(--space-3) var(--space-4);
- background: var(--c-surface);
- border-bottom: 1px solid var(--c-border-light);
- flex-shrink: 0;
- overflow-x: auto;
- scrollbar-width: none;
-}
-.places-toolbar::-webkit-scrollbar { display: none; }
-.places-filter {
- display: flex;
- gap: var(--space-2);
- flex: 1;
- overflow-x: auto;
- scrollbar-width: none;
-}
-.places-filter::-webkit-scrollbar { display: none; }
-.places-filter-btn {
- padding: var(--space-1) var(--space-3);
- border-radius: var(--radius-full);
- border: 1.5px solid var(--c-border);
- background: var(--c-surface);
- color: var(--c-text-secondary);
- font-size: var(--text-sm);
- cursor: pointer;
- white-space: nowrap;
- transition: all 0.15s;
- flex-shrink: 0;
-}
-.places-filter-btn.active {
- background: var(--c-primary);
- border-color: var(--c-primary);
- color: #fff;
-}
-.places-map {
- height: 42%;
- flex-shrink: 0;
- min-height: 180px;
-}
-.places-list {
- flex: 1;
- overflow-y: auto;
- scrollbar-width: thin;
- scrollbar-color: var(--c-primary) var(--c-surface);
-}
-.places-list-inner {
- padding: var(--space-3) var(--space-4);
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
-}
-.places-card {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: var(--space-3);
- background: var(--c-surface);
- border: 1.5px solid var(--c-border-light);
- border-left: 4px solid var(--typ-color, var(--c-primary));
- border-radius: var(--radius-lg);
- cursor: pointer;
- transition: box-shadow 0.15s;
-}
-.places-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
-.places-card-icon { font-size: 1.6rem; flex-shrink: 0; }
-.places-card-body { flex: 1; min-width: 0; }
-.places-card-name { font-weight: var(--weight-semibold); color: var(--c-text); }
-.places-card-meta { font-size: var(--text-sm); color: var(--c-text-secondary); margin-top: 2px; }
-.places-card-flags { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-1); }
-.places-card-arrow { color: var(--c-text-muted); font-size: 1.2rem; }
-.places-flag {
- font-size: var(--text-xs);
- padding: 2px 7px;
- border-radius: var(--radius-full);
- background: var(--c-surface-2);
- color: var(--c-text-secondary);
-}
-.places-flag--detail {
- font-size: var(--text-sm);
- padding: var(--space-1) var(--space-3);
-}
-.places-locate-btn {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: #C4843A;
- color: #fff;
- border: none;
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 10px;
-}
-.places-locate-btn:hover { background: #9E6520; }
-
/* ============================================================
ROUTEN — Komoot-Stil (routes.js)
============================================================ */
diff --git a/backend/static/index.html b/backend/static/index.html
index c9d56d4..55377a3 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -86,14 +86,14 @@
Ban Yaro
-
+
-
-
-
-
-
+
+
+
+
+
@@ -617,11 +617,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -631,7 +631,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index fba73c8..676179c 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 = '1205'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1206'; // ← 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/pages/forum.js b/backend/static/js/pages/forum.js
index 5615925..4431f75 100644
--- a/backend/static/js/pages/forum.js
+++ b/backend/static/js/pages/forum.js
@@ -13,8 +13,6 @@ window.Page_forum = (() => {
let _offset = 0;
let _searchTimer = null;
let _searching = false;
- let _mapLoaded = false;
- let _leafletLoaded = false;
let _map = null;
let _clusterGroup = null;
let _activeSection = 'list'; // 'list' | 'map'
@@ -1237,15 +1235,11 @@ function _fmtDate(iso) {
}
});
- await _loadLeaflet();
const mapEl = document.getElementById('forum-map');
if (!mapEl) return;
- _map = L.map(mapEl, { zoomControl: true }).setView([51.0, 10.0], 6);
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap',
- maxZoom: 18,
- }).addTo(_map);
+ // GL über die Facade (gleicher Style wie die zentrale Karte), Fallback Leaflet.
+ _map = await UI.map.create(mapEl, { center: [51.0, 10.0], zoom: 6, zoomControl: true, attributionControl: false });
_loadMembersOnMap();
}
@@ -1253,37 +1247,20 @@ function _fmtDate(iso) {
async function _loadMembersOnMap() {
if (!_map) return;
try {
- // MarkerCluster laden falls nicht vorhanden
- if (!window.L.markerClusterGroup) {
- await Promise.all([
- new Promise((res, rej) => {
- if (document.querySelector('link[href*="MarkerCluster"]')) { res(); return; }
- const l1 = document.createElement('link'); l1.rel='stylesheet'; l1.href='/css/MarkerCluster.css'; l1.onload=res; l1.onerror=rej; document.head.appendChild(l1);
- }),
- new Promise((res, rej) => {
- const s = document.createElement('script'); s.src='/js/leaflet.markercluster.js'; s.onload=res; s.onerror=rej; document.head.appendChild(s);
- }),
- ]);
- }
-
const members = await API.forum.membersMap();
- // Alte Cluster-Gruppe sauber entfernen
- if (_clusterGroup) { _map.removeLayer(_clusterGroup); _clusterGroup = null; }
+ // Alte Gruppe sauber entfernen
+ if (_clusterGroup) { try { _map.removeLayer(_clusterGroup); } catch (e) {} _clusterGroup = null; }
- _clusterGroup = L.markerClusterGroup({ maxClusterRadius: 60 });
+ _clusterGroup = UI.map.clusterGroup({ maxClusterRadius: 60 });
members.forEach(m => {
- const icon = L.divIcon({
- className: '',
- html: `${UI.escape((m.vorname||'?')[0].toUpperCase())}
`,
- iconSize: [32, 32], iconAnchor: [16, 16],
- });
+ border:2px solid rgba(255,255,255,0.8)">${UI.escape((m.vorname||'?')[0].toUpperCase())}`;
_clusterGroup.addLayer(
- L.marker([m.lat, m.lon], { icon })
+ UI.map.svgMarker(m.lat, m.lon, html, { size: 32, anchorY: 16 })
.bindPopup(`${UI.escape(m.vorname || '?')}`)
);
});
@@ -1293,30 +1270,6 @@ function _fmtDate(iso) {
}
}
- async function _loadLeaflet() {
- if (_leafletLoaded || window.L) { _leafletLoaded = true; return; }
-
- // CSS
- if (!document.querySelector('link[href*="leaflet.css"]')) {
- const lCss = document.createElement('link');
- lCss.rel = 'stylesheet';
- lCss.href = '/css/leaflet.css';
- document.head.appendChild(lCss);
- }
-
- // JS
- await new Promise((resolve, reject) => {
- if (window.L) { resolve(); return; }
- const s = document.createElement('script');
- s.src = '/js/leaflet.js';
- s.onload = resolve;
- s.onerror = reject;
- document.head.appendChild(s);
- });
-
- _leafletLoaded = true;
- }
-
// ----------------------------------------------------------
// Moderations-Panel
// ----------------------------------------------------------
@@ -1469,6 +1422,13 @@ function _fmtDate(iso) {
document.body.appendChild(lb);
}
- return { init, refresh, onDogChange, openNew, openThread: _openThread };
+ // Karte beim Verlassen freigeben (WebGL-Kontext-Leak vermeiden).
+ function destroy() {
+ try { _clusterGroup && _clusterGroup.remove && _clusterGroup.remove(); } catch (e) {}
+ try { _map && _map.remove && _map.remove(); } catch (e) {}
+ _map = null; _clusterGroup = null;
+ }
+
+ return { init, refresh, onDogChange, openNew, openThread: _openThread, destroy };
})();
diff --git a/backend/static/js/pages/places.js b/backend/static/js/pages/places.js
deleted file mode 100644
index 767f708..0000000
--- a/backend/static/js/pages/places.js
+++ /dev/null
@@ -1,469 +0,0 @@
-/* ============================================================
- BAN YARO — Orte (Hundefreundliche Orte)
- Karte + Liste, Eigene Orte anlegen/bearbeiten
- ============================================================ */
-
-window.Page_places = (() => {
-
- let _container = null;
- let _appState = null;
- let _map = null;
- let _markers = [];
- let _data = [];
- let _activeTyp = null; // null = alle
- let _search = '';
- let _userPos = null;
-
- // ----------------------------------------------------------
- // Typen-Konfiguration
- // ----------------------------------------------------------
- const TYPEN = {
- restaurant: { icon: '', label: 'Restaurant & Café', color: '#F97316' },
- freilauf: { icon: '', label: 'Freilauffläche', color: '#22C55E' },
- shop: { icon: '', label: 'Shop', color: '#3B82F6' },
- kotbeutel: { icon: '', label: 'Kotbeutel-Station', color: '#84A98C' },
- tierarzt: { icon: '', label: 'Tierarzt', color: '#EF4444' },
- hundeschule: { icon: '', label: 'Hundeschule', color: '#8B5CF6' },
- };
-
- // _esc ersetzt durch UI.escape()
-
- // ----------------------------------------------------------
- // INIT
- // ----------------------------------------------------------
- async function init(container, appState) {
- _container = container;
- _appState = appState;
- _render();
- _loadData();
- try { _userPos = await API.getLocation(); } catch {}
- }
-
- function refresh() { _loadData(); }
- function onDogChange() {}
-
- // ----------------------------------------------------------
- // RENDER — Grundstruktur
- // ----------------------------------------------------------
- function _render() {
- _container.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- // Events
- document.getElementById('places-filter').addEventListener('click', e => {
- const btn = e.target.closest('.places-filter-btn');
- if (!btn) return;
- document.querySelectorAll('.places-filter-btn').forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
- _activeTyp = btn.dataset.typ || null;
- _applyFilter();
- });
-
- document.getElementById('places-add-btn').addEventListener('click', () => {
- if (!_appState.user) {
- UI.toast.warning('Bitte zuerst anmelden.');
- App.navigate('settings');
- return;
- }
- _showForm(null);
- });
-
- // Suche mit Debounce
- let _searchTimer = null;
- document.getElementById('places-search')?.addEventListener('input', e => {
- clearTimeout(_searchTimer);
- _searchTimer = setTimeout(() => {
- _search = e.target.value.trim().toLowerCase();
- _applyFilter();
- }, 300);
- });
-
- UI.loadLeaflet().then(_initMap);
- }
-
- // ----------------------------------------------------------
- // Karte initialisieren
- // ----------------------------------------------------------
- function _initMap() {
- const el = document.getElementById('places-map');
- if (!el || !window.L || _map) return;
-
- const center = _userPos ? [_userPos.lat, _userPos.lon] : [51.1657, 10.4515];
- const zoom = _userPos ? 13 : 6;
-
- _map = L.map('places-map', { zoomControl: true, attributionControl: false })
- .setView(center, zoom);
-
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 })
- .addTo(_map);
-
- // GPS-Locate-Button
- L.Control.Locate = L.Control.extend({
- onAdd() {
- const btn = L.DomUtil.create('button', 'places-locate-btn');
- btn.innerHTML = '';
- btn.title = 'Meinen Standort';
- btn.onclick = async () => {
- try {
- const pos = await API.getLocation({ enableHighAccuracy: true });
- _userPos = pos;
- _map.setView([pos.lat, pos.lon], 14);
- } catch { UI.toast.error('Standort konnte nicht ermittelt werden.'); }
- };
- return btn;
- },
- onRemove() {},
- });
- new L.Control.Locate({ position: 'bottomright' }).addTo(_map);
-
- _renderMarkers();
- }
-
- // ----------------------------------------------------------
- // Daten laden
- // ----------------------------------------------------------
- async function _loadData() {
- try {
- _data = await API.places.list();
- _renderList();
- _renderMarkers();
- } catch (err) {
- UI.toast.error(err.message || 'Fehler beim Laden der Orte.');
- }
- }
-
- // ----------------------------------------------------------
- // Filter anwenden
- // ----------------------------------------------------------
- function _filtered() {
- let list = _activeTyp ? _data.filter(p => p.typ === _activeTyp) : _data;
- if (_search) {
- const q = _search;
- list = list.filter(p =>
- (p.name || '').toLowerCase().includes(q) ||
- (p.adresse|| '').toLowerCase().includes(q) ||
- (p.typ || '').toLowerCase().includes(q)
- );
- }
- return list;
- }
-
- function _applyFilter() {
- _renderList();
- _renderMarkers();
- }
-
- // ----------------------------------------------------------
- // Marker rendern
- // ----------------------------------------------------------
- function _renderMarkers() {
- if (!_map || !window.L) return;
- _markers.forEach(m => m.remove());
- _markers = [];
-
- _filtered().forEach(place => {
- const t = TYPEN[place.typ] || { icon: '', color: '#6B7280' };
- const marker = UI.leafletMarker({ lat: place.lat, lon: place.lon, color: t.color, icon: t.icon, size: 34 })
- .addTo(_map)
- .on('click', () => _openDetail(place));
- _markers.push(marker);
- });
- }
-
- // ----------------------------------------------------------
- // Liste rendern
- // ----------------------------------------------------------
- function _renderList() {
- const list = document.getElementById('places-list');
- if (!list) return;
- const items = _filtered();
-
- if (!items.length) {
- const msg = _search
- ? `Keine Orte gefunden für „${UI.escape(_search)}".`
- : (_activeTyp ? 'Keine Orte in dieser Kategorie.' : 'Noch keine Orte eingetragen.');
- list.innerHTML = `
- `;
- return;
- }
-
- list.innerHTML = `
-
- ${items.map(p => _cardHTML(p)).join('')}
-
`;
-
- list.querySelectorAll('.places-card').forEach(card => {
- const id = parseInt(card.dataset.id);
- const place = _data.find(p => p.id === id);
- if (place) card.addEventListener('click', () => _openDetail(place));
- });
- }
-
- function _cardHTML(p) {
- const t = TYPEN[p.typ] || { icon: '', label: p.typ, color: '#6B7280' };
- const flags = [
- p.hund_rein === true ? `${UI.icon('dog')} Hund rein` : null,
- p.leine_pflicht === true ? `${UI.icon('tag')} Leinenpflicht` : null,
- p.wasser_fuer_hunde === true ? `${UI.icon('drop')} Wasser` : null,
- ].filter(Boolean);
-
- return `
-
-
${t.icon}
-
-
${UI.escape(p.name)}
-
- ${t.label}
- ${p.adresse ? `· ${UI.escape(p.adresse)}` : ''}
-
- ${flags.length ? `
${flags.map(f => `${f}`).join('')}
` : ''}
-
-
${UI.icon('arrow-right')}
-
`;
- }
-
- // ----------------------------------------------------------
- // Detail-Modal
- // ----------------------------------------------------------
- function _openDetail(place) {
- const t = TYPEN[place.typ] || { icon: '', label: place.typ, color: '#6B7280' };
- const isOwn = _appState.user?.id === place.user_id;
-
- const flags = [
- place.hund_rein === true ? `${UI.icon('dog')} Hund erlaubt` : (place.hund_rein === false ? `${UI.icon('x')} Kein Hund` : null),
- place.leine_pflicht === true ? `${UI.icon('tag')} Leinenpflicht` : (place.leine_pflicht === false ? `${UI.icon('check')} Leine optional` : null),
- place.wasser_fuer_hunde === true ? `${UI.icon('drop')} Wasser vorhanden`: null,
- ].filter(Boolean);
-
- const body = `
-
-
${t.icon}
-
-
${UI.escape(place.name)}
-
${t.label}
-
-
- ${place.adresse ? `${UI.icon('map-pin')} ${UI.escape(place.adresse)}
` : ''}
- ${place.telefon ? `${UI.icon('phone')} ${UI.escape(place.telefon)}
` : ''}
- ${place.website ? `${UI.icon('arrow-square-out')} ${UI.escape(place.website)}
` : ''}
- ${flags.length ? `${flags.map(f => `${f}`).join('')}
` : ''}
-
-
- Eingetragen von ${UI.escape(place.user_name || 'Unbekannt')}
-
- `;
-
- const footer = isOwn ? `
-
-
- ` : `
-
- `;
-
- UI.modal.open({ title: `${t.icon} ${UI.escape(place.name)}`, body, footer });
-
- UI.ratingStars({
- containerId: `place-rating-${place.id}`,
- targetType: 'place',
- targetId: place.id,
- isLoggedIn: !!_appState.user,
- });
-
- document.getElementById('place-detail-close')?.addEventListener('click', UI.modal.close);
-
- document.getElementById('place-detail-edit')?.addEventListener('click', () => {
- UI.modal.close();
- _showForm(place);
- });
-
-
- // Auf Karte zentrieren
- if (_map) _map.setView([place.lat, place.lon], 15);
- }
-
- // ----------------------------------------------------------
- // Formular — Ort anlegen / bearbeiten
- // ----------------------------------------------------------
- function _showForm(place) {
- const isEdit = !!place;
-
- const typOpts = Object.entries(TYPEN)
- .map(([k, t]) => ``)
- .join('');
-
- const body = `
-
- `;
-
- const footer = `
-
-
-
- ${isEdit ? `` : ''}
-
-
-
- `;
-
- UI.modal.open({ title: isEdit ? `${UI.escape(place.name)} bearbeiten` : ' Neuer Ort', body, footer });
-
- document.getElementById('place-form-cancel')?.addEventListener('click', UI.modal.close);
-
- document.getElementById('place-form-delete')?.addEventListener('click', async () => {
- const ok = await UI.modal.confirm({
- title: 'Ort löschen?', message: `„${place.name}" wird dauerhaft entfernt.`, confirmText: 'Löschen', danger: true,
- });
- if (!ok) return;
- try {
- await API.places.delete(place.id);
- _data = _data.filter(p => p.id !== place.id);
- UI.modal.close();
- _renderList();
- _renderMarkers();
- UI.toast.success('Ort gelöscht.');
- } catch (err) { UI.toast.error(err.message || 'Fehler.'); }
- });
-
- // Location-Picker initialisieren
- const _picker = UI.locationPicker({ containerId: 'pf-location-picker' });
- if (place?.lat && place?.lon) {
- _picker.setValue(place.lat, place.lon, null);
- }
-
- document.getElementById('place-form')?.addEventListener('submit', async e => {
- e.preventDefault();
- const btn = document.querySelector('[form="place-form"][type="submit"]') || e.target.querySelector('[type="submit"]');
- const fd = UI.formData(e.target);
- const loc = _picker.getValue();
-
- if (!loc.lat || !loc.lon) {
- UI.toast.warning('Bitte GPS-Position ermitteln.');
- return;
- }
-
- await UI.asyncButton(btn, async () => {
- const payload = {
- name: fd.name?.trim(),
- typ: fd.typ,
- lat: loc.lat,
- lon: loc.lon,
- adresse: fd.adresse || null,
- website: fd.website || null,
- telefon: fd.telefon || null,
- hund_rein: 'hund_rein' in fd,
- leine_pflicht: 'leine_pflicht' in fd,
- wasser_fuer_hunde: 'wasser_fuer_hunde' in fd,
- };
-
- if (isEdit) {
- const updated = await API.places.update(place.id, payload);
- const idx = _data.findIndex(p => p.id === place.id);
- if (idx !== -1) _data[idx] = updated;
- UI.toast.success('Gespeichert.');
- } else {
- const created = await API.places.create(payload);
- _data.unshift(created);
- UI.toast.success('Ort hinzugefügt!');
- }
-
- UI.modal.close();
- _renderList();
- _renderMarkers();
- });
- });
- }
-
- return { init, refresh, onDogChange };
-
-})();
diff --git a/backend/static/landing.html b/backend/static/landing.html
index b53008f..91b71b8 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 db90cda..07cb52e 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 = '1205';
+const VER = '1206';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten