lazyflat/web/static/app.js
EiSiMo 0b73bafa81 debug(lightbox): trace IIFE init, partial-fetch contents, tile clicks
Lightbox still not opening on the user's side after the style.display
switch. Wire console.log() at every checkpoint so we can read off
DevTools where the chain breaks:

- partial fetch logs how many .flat-gallery-tile / a.flat-gallery-tile
  elements arrived and the first 200 chars of HTML — catches stale
  partial caches and template regressions.
- IIFE init logs whether the overlay element and each child were found.
- The delegated click handler logs every tile click, the gallery
  tile/url counts, and the open() call. A sibling branch logs clicks
  *inside* the gallery that don't match a tile (catches markup drift).
- open() logs the final computed display value so we can tell whether
  CSS still hides the overlay after the style change.
- A window.error listener catches any uncaught exception that would
  abort app.js before our IIFE registers its handlers.

All log lines are prefixed `[lazyflat.lightbox]` and tagged
`DEBUG(lightbox):` in source for easy removal once it's confirmed
working.

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

247 lines
9.6 KiB
JavaScript

// lazyflat — live time helpers.
// [data-rel-utc="<iso>"] → "vor 3 min" (relative-past, updated every 5 s)
// [data-counter-up-utc="<iso>"] → "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/<id> 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 = '<div class="px-4 py-5 text-sm text-slate-500">lädt…</div>';
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";
// DEBUG(lightbox): confirm fetched partial actually contains <button class="flat-gallery-tile">
const tileCount = pane.querySelectorAll(".flat-gallery-tile").length;
const aTagCount = pane.querySelectorAll("a.flat-gallery-tile").length;
console.log("[lazyflat.lightbox] partial loaded flat=%s tiles=%d (a-tags=%d, html-snippet=%o)",
flatId, tileCount, aTagCount, html.slice(0, 200));
})
.catch(() => {
pane.innerHTML = '<div class="px-4 py-5 text-sm text-slate-500">Detail konnte nicht geladen werden.</div>';
});
});
// Deep-link landing: ?flat=<id> 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 () {
// DEBUG(lightbox): start-up trace, remove once the viewer is confirmed working.
console.log("[lazyflat.lightbox] IIFE start");
const overlay = document.getElementById("lazyflat-lightbox");
console.log("[lazyflat.lightbox] overlay element:", overlay);
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]");
console.log("[lazyflat.lightbox] children:",
{ imgEl: !!imgEl, counterEl: !!counterEl, prevBtn: !!prevBtn,
nextBtn: !!nextBtn, closeBtn: !!closeBtn });
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) {
console.log("[lazyflat.lightbox] open() called", { count: list && list.length, startIdx, list });
if (!list.length) {
console.warn("[lazyflat.lightbox] open() bailed — empty url list");
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();
console.log("[lazyflat.lightbox] open() done — overlay computed display:",
getComputedStyle(overlay).display, "src:", imgEl.src);
}
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) {
// DEBUG(lightbox): only log clicks somewhere INSIDE a gallery so we don't spam.
if (ev.target.closest && ev.target.closest(".flat-gallery")) {
console.log("[lazyflat.lightbox] click inside gallery but no .flat-gallery-tile ancestor",
{ target: ev.target, tagName: ev.target.tagName });
}
return;
}
console.log("[lazyflat.lightbox] tile clicked", { tile, target: ev.target,
tagName: tile.tagName, fullSrc: tile.dataset.fullSrc });
const gallery = tile.closest(".flat-gallery");
if (!gallery) {
console.warn("[lazyflat.lightbox] tile has no .flat-gallery ancestor");
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);
console.log("[lazyflat.lightbox] gallery has", tiles.length, "tiles,", list.length, "valid urls");
open(list, tiles.indexOf(tile));
});
console.log("[lazyflat.lightbox] click delegate attached");
})();
// DEBUG(lightbox): catch any uncaught error so we know if app.js stopped
// running before the IIFE attached its handlers.
window.addEventListener("error", (ev) => {
console.error("[lazyflat.lightbox] window error:", ev.message, ev.filename + ":" + ev.lineno);
});