* Alarm-Status ist jetzt nur 'aktiv' wenn ein echter Push-Channel (Telegram mit Token+Chat oder E-Mail mit Adresse) konfiguriert ist. UI-only zählt nicht mehr als eingerichteter Alarm. * Ablehnen-Button in der Wohnungsliste: flat_rejections (migration v4) speichert pro-User-Ablehnungen, abgelehnte Flats fallen aus Liste und Karte raus. Wiederholbar pro User unabhängig. * Footer 'Programmiert für Annika ♥' erscheint nur auf Seiten, wenn annika angemeldet ist. * Map: Hinweistext unter leerer Karte entfernt; alle Zoom-Mechanismen deaktiviert (Scrollrad, Doppelklick, Box, Touch, Tastatur, +/- Buttons). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
2.9 KiB
JavaScript
101 lines
2.9 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, {
|
|
zoomControl: false,
|
|
scrollWheelZoom: false,
|
|
doubleClickZoom: false,
|
|
boxZoom: false,
|
|
touchZoom: false,
|
|
keyboard: false,
|
|
}).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, "<");
|
|
const safeLink = (f.link || "#").replace(/"/g, """);
|
|
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);
|