lazyflat/web/static/map.js
Moritz 376551213a map view (Leaflet + OSM), iOS switches, Alarm → Benachrichtigungen
* flats: new lat/lng columns (migration v3); alert geocodes every new flat
  through googlemaps and ships coords in the payload
* web: CSP extended for unpkg (leaflet.css) + tile.openstreetmap.org
* Wohnungen tab: Liste/Karte view toggle (segmented, CSS-only via :has(),
  selection persisted in localStorage). Karte shows passende flats as Pins
  on an OSM tile map; Popup per Pin mit Adresse, Zimmer/m²/€ und Link
* Top-strip toggles are now proper iOS-style toggle switches (single
  rounded knob sliding in a pill, red when on), no descriptive subtitle
* Alarm-Karte verlinkt jetzt auf /einstellungen/benachrichtigungen
  (Filter-Karte bleibt /einstellungen/filter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:02:40 +02:00

94 lines
2.8 KiB
JavaScript

// lazyflat — Leaflet flat map
// A single Leaflet map instance; re-initialised after every HTMX swap of
// the Wohnungen body. Also flushes size when the view toggle flips from
// list to map (Leaflet needs invalidateSize on a hidden-then-shown map).
let mapInstance = null;
const BERLIN_CENTER = [52.52, 13.405];
const BERLIN_ZOOM = 11;
function initFlatsMap() {
const el = document.getElementById("flats-map");
if (!el || typeof L === "undefined") return;
if (mapInstance) {
try { mapInstance.remove(); } catch (e) {}
mapInstance = null;
}
mapInstance = L.map(el).setView(BERLIN_CENTER, BERLIN_ZOOM);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap",
maxZoom: 18,
}).addTo(mapInstance);
let data = [];
try {
data = JSON.parse(el.dataset.flats || "[]");
} catch (e) {
console.warn("flats-map: bad JSON in data-flats", e);
}
const bounds = [];
data.forEach((f) => {
if (typeof f.lat !== "number" || typeof f.lng !== "number") return;
const m = L.marker([f.lat, f.lng]).addTo(mapInstance);
const rent = f.rent ? Math.round(f.rent) + " €" : "";
const rooms = f.rooms ? f.rooms + " Zi" : "";
const size = f.size ? Math.round(f.size) + " m²" : "";
const meta = [rooms, size, rent].filter(Boolean).join(" · ");
const safeAddr = (f.address || "").replace(/</g, "&lt;");
const safeLink = (f.link || "#").replace(/"/g, "&quot;");
m.bindPopup(
`<b>${safeAddr}</b>` +
(meta ? `<br>${meta}` : "") +
`<br><a href="${safeLink}" target="_blank" rel="noopener">Zur Anzeige →</a>`,
);
bounds.push([f.lat, f.lng]);
});
if (bounds.length === 1) {
mapInstance.setView(bounds[0], 14);
} else if (bounds.length > 1) {
mapInstance.fitBounds(bounds, { padding: [30, 30] });
}
}
function flushMapSize() {
if (mapInstance) {
setTimeout(() => mapInstance.invalidateSize(), 50);
}
}
function wireViewToggle() {
document.querySelectorAll('input[name="view_mode"]').forEach((r) => {
if (r.dataset.wired === "1") return;
r.dataset.wired = "1";
r.addEventListener("change", (e) => {
try {
localStorage.setItem("lazyflat_view_mode", e.target.value);
} catch (err) {}
flushMapSize();
});
});
}
function restoreView() {
let stored = null;
try {
stored = localStorage.getItem("lazyflat_view_mode");
} catch (err) {}
if (!stored) return;
const el = document.querySelector(
`input[name="view_mode"][value="${stored}"]`,
);
if (el && !el.checked) {
el.checked = true;
flushMapSize();
}
}
function onReady() {
initFlatsMap();
wireViewToggle();
restoreView();
}
document.addEventListener("DOMContentLoaded", onReady);
document.body && document.body.addEventListener("htmx:afterSwap", onReady);