- App is now called "wohnungsdidi" everywhere user-facing (page title,
nav brand, login header, notification subjects, report filename,
FastAPI titles, log messages)
- Brand dot replaced with an image of Didi (web/static/didi.webp),
rendered as a round 2.25rem avatar in _layout + login
- "Programmiert für Annika ♥" footer now shows for every logged-in user,
not only Annika
- Count-up shows only seconds ("vor 73 s") regardless of age — no
rollover to minutes/hours
- Data continuity: DB file stays /data/lazyflat.sqlite and the Docker
volume stays lazyflat_data so the rename doesn't strand existing data
- Session cookie renamed to wohnungsdidi_session (one-time logout on
rollout)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.4 KiB
JavaScript
97 lines
3.4 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";
|
|
})
|
|
.catch(() => {
|
|
pane.innerHTML = '<div class="px-4 py-5 text-sm text-slate-500">Detail konnte nicht geladen werden.</div>';
|
|
});
|
|
});
|