// lazyflat — live time helpers. // [data-rel-utc=""] → "vor 3 min" (relative-past, updated every 5 s) // [data-counter-up-utc=""] → "vor X s" counting up each second (used for // "aktualisiert vor X s"; resets when the // server ships a newer timestamp in the swap) function fmtRelative(iso) { const ts = Date.parse(iso); if (!iso || Number.isNaN(ts)) return "—"; const diff = Math.max(0, Math.floor((Date.now() - ts) / 1000)); if (diff < 5) return "gerade eben"; if (diff < 60) return `vor ${diff} s`; if (diff < 3600) return `vor ${Math.floor(diff / 60)} min`; if (diff < 86400) return `vor ${Math.floor(diff / 3600)} h`; return `vor ${Math.floor(diff / 86400)} Tagen`; } function fmtCountUp(iso) { const ts = Date.parse(iso); if (!iso || Number.isNaN(ts)) return "—"; const diff = Math.max(0, Math.floor((Date.now() - ts) / 1000)); return `vor ${diff} s`; } function updateRelativeTimes() { document.querySelectorAll("[data-rel-utc]").forEach((el) => { el.textContent = fmtRelative(el.dataset.relUtc); }); } function updateCounterUps() { document.querySelectorAll("[data-counter-up-utc]").forEach((el) => { el.textContent = fmtCountUp(el.dataset.counterUpUtc); }); } // After the HTMX swap rebuilds the list, the open-chevron class is gone even // though the corresponding .flat-detail pane is preserved. Re-sync by looking // at pane visibility. function syncFlatExpandState() { document.querySelectorAll(".flat-detail").forEach((pane) => { const row = pane.closest(".flat-row"); if (!row) return; const btn = row.querySelector(".flat-expand-btn"); if (!btn) return; const open = pane.dataset.loaded === "1" && pane.style.display !== "none"; btn.classList.toggle("open", open); }); } function tick() { updateRelativeTimes(); updateCounterUps(); } document.addEventListener("DOMContentLoaded", tick); document.addEventListener("DOMContentLoaded", syncFlatExpandState); if (document.body) { document.body.addEventListener("htmx:afterSwap", tick); document.body.addEventListener("htmx:afterSwap", syncFlatExpandState); } setInterval(updateCounterUps, 1000); setInterval(updateRelativeTimes, 5000); // Flat detail expand — lazily fetches /partials/wohnung/ into the sibling // .flat-detail container on first open, toggles visibility on subsequent clicks. // Event delegation survives HTMX swaps without re-binding on each poll. document.addEventListener("click", (ev) => { const btn = ev.target.closest(".flat-expand-btn"); if (!btn) return; const row = btn.closest(".flat-row"); if (!row) return; const pane = row.querySelector(".flat-detail"); if (!pane) return; if (btn.classList.contains("open")) { pane.style.display = "none"; btn.classList.remove("open"); return; } btn.classList.add("open"); pane.style.display = "block"; if (pane.dataset.loaded) return; pane.innerHTML = '
lädt…
'; const flatId = btn.dataset.flatId || ""; fetch("/partials/wohnung/" + encodeURIComponent(flatId), { headers: { "HX-Request": "true" } }) .then((r) => r.text()) .then((html) => { pane.innerHTML = html; pane.dataset.loaded = "1"; }) .catch(() => { pane.innerHTML = '
Detail konnte nicht geladen werden.
'; }); }); // Deep-link landing: ?flat= on first load expands + highlights that row. // Used from Telegram match notifications so "Zur lazyflat Seite" jumps // straight to the relevant card. function openDeepLinkedFlat() { const params = new URLSearchParams(location.search); const targetId = params.get("flat"); if (!targetId) return; let found = null; for (const btn of document.querySelectorAll(".flat-expand-btn")) { if (btn.dataset.flatId === targetId) { found = btn; break; } } if (!found) { // Flat not in the visible list (filter/age/rejected). Drop the param so // a reload doesn't keep failing silently. params.delete("flat"); const qs = params.toString(); history.replaceState(null, "", location.pathname + (qs ? "?" + qs : "") + location.hash); return; } const row = found.closest(".flat-row"); if (!found.classList.contains("open")) found.click(); if (row) { row.scrollIntoView({ behavior: "smooth", block: "center" }); row.classList.add("flat-highlight"); setTimeout(() => row.classList.remove("flat-highlight"), 3000); } params.delete("flat"); const qs = params.toString(); history.replaceState(null, "", location.pathname + (qs ? "?" + qs : "") + location.hash); } document.addEventListener("DOMContentLoaded", openDeepLinkedFlat); // Image lightbox — single global modal in base.html, opened by clicking any // .flat-gallery-tile. Click handler is delegated so it survives HTMX swaps. // Visibility is driven by inline style.display rather than a `hidden` class // because Tailwind's CDN injects its own `.hidden { display: none }` rule // at runtime, which conflicted with our class toggle and kept the modal // invisible after open(). (function () { const overlay = document.getElementById("lazyflat-lightbox"); if (!overlay) { console.warn("[lazyflat.lightbox] #lazyflat-lightbox not in DOM; viewer disabled"); return; } const imgEl = overlay.querySelector(".lightbox-image"); const counterEl = overlay.querySelector(".lightbox-counter"); const prevBtn = overlay.querySelector("[data-lightbox-prev]"); const nextBtn = overlay.querySelector("[data-lightbox-next]"); const closeBtn = overlay.querySelector("[data-lightbox-close]"); let urls = []; let idx = 0; function render() { imgEl.src = urls[idx]; imgEl.alt = `Foto ${idx + 1} von ${urls.length}`; counterEl.textContent = urls.length > 1 ? `${idx + 1} / ${urls.length}` : ""; prevBtn.hidden = idx <= 0; nextBtn.hidden = idx >= urls.length - 1; } function open(list, startIdx) { if (!list.length) return; urls = list; idx = Math.max(0, Math.min(startIdx | 0, urls.length - 1)); overlay.style.display = "flex"; overlay.setAttribute("aria-hidden", "false"); document.body.classList.add("lightbox-open"); render(); } function close() { overlay.style.display = "none"; overlay.setAttribute("aria-hidden", "true"); document.body.classList.remove("lightbox-open"); imgEl.removeAttribute("src"); urls = []; } function step(delta) { const next = idx + delta; if (next < 0 || next >= urls.length) return; idx = next; render(); } prevBtn.addEventListener("click", () => step(-1)); nextBtn.addEventListener("click", () => step(1)); closeBtn.addEventListener("click", close); overlay.addEventListener("click", (ev) => { if (ev.target === overlay) close(); }); document.addEventListener("keydown", (ev) => { if (overlay.style.display === "none") return; if (ev.key === "Escape") close(); else if (ev.key === "ArrowLeft") step(-1); else if (ev.key === "ArrowRight") step(1); }); document.addEventListener("click", (ev) => { const tile = ev.target.closest(".flat-gallery-tile"); if (!tile) return; const gallery = tile.closest(".flat-gallery"); if (!gallery) return; ev.preventDefault(); const tiles = Array.from(gallery.querySelectorAll(".flat-gallery-tile")); const list = tiles .map((t) => t.dataset.fullSrc || (t.querySelector("img") || {}).src || "") .filter(Boolean); open(list, tiles.indexOf(tile)); }); })();