activity.ts
typescript
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923
fix(issues): use issue number as pagination cursor, not cre…
Sonnet 4.6
patch
8 days ago
| 1 | /** |
| 2 | * Activity feed – progressive enhancement. |
| 3 | * |
| 4 | * Responsibilities: |
| 5 | * 1. Row entrance animations (staggered fade-in via IntersectionObserver). |
| 6 | * 2. Keep the "latest event" timestamp chip up-to-date without a full reload |
| 7 | * by re-rendering relative timestamps every 60 s. |
| 8 | */ |
| 9 | |
| 10 | declare global { |
| 11 | interface Window { |
| 12 | __activityCfg?: { base: string }; |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | // ── Relative-time refresh ───────────────────────────────────────────────── |
| 17 | |
| 18 | /** Parse an ISO datetime string and return a human-readable relative label. */ |
| 19 | function relativeLabel(iso: string): string { |
| 20 | const ms = Date.now() - new Date(iso).getTime(); |
| 21 | const s = Math.floor(ms / 1000); |
| 22 | if (s < 60) return "just now"; |
| 23 | const m = Math.floor(s / 60); |
| 24 | if (m < 60) return `${m}m ago`; |
| 25 | const h = Math.floor(m / 60); |
| 26 | if (h < 24) return `${h}h ago`; |
| 27 | const d = Math.floor(h / 24); |
| 28 | return `${d}d ago`; |
| 29 | } |
| 30 | |
| 31 | function refreshTimestamps(): void { |
| 32 | document.querySelectorAll<HTMLElement>("[data-iso]").forEach(el => { |
| 33 | const iso = el.dataset.iso; |
| 34 | if (iso) el.textContent = relativeLabel(iso); |
| 35 | }); |
| 36 | } |
| 37 | |
| 38 | // ── Row entrance animation ──────────────────────────────────────────────── |
| 39 | |
| 40 | function attachRowAnimations(root: Element = document.body): void { |
| 41 | const rows = root.querySelectorAll<HTMLElement>(".av-row, .av-date-header"); |
| 42 | if (!rows.length) return; |
| 43 | |
| 44 | const io = new IntersectionObserver((entries) => { |
| 45 | entries.forEach((entry, i) => { |
| 46 | if (!entry.isIntersecting) return; |
| 47 | const el = entry.target as HTMLElement; |
| 48 | el.style.animationDelay = `${i * 30}ms`; |
| 49 | el.classList.add("av-row--visible"); |
| 50 | io.unobserve(el); |
| 51 | }); |
| 52 | }, { threshold: 0.05 }); |
| 53 | |
| 54 | rows.forEach(row => { |
| 55 | row.classList.add("av-row--hidden"); |
| 56 | io.observe(row); |
| 57 | }); |
| 58 | } |
| 59 | |
| 60 | // ── HTMX post-swap re-init ──────────────────────────────────────────────── |
| 61 | |
| 62 | function bindHtmxSwap(): void { |
| 63 | document.body.addEventListener("htmx:afterSwap", (e: Event) => { |
| 64 | const target = (e as CustomEvent).detail?.target as HTMLElement | undefined; |
| 65 | if (!target) return; |
| 66 | if (target.id === "av-feed" || target.closest("#av-feed")) { |
| 67 | attachRowAnimations(target); |
| 68 | refreshTimestamps(); |
| 69 | } |
| 70 | }); |
| 71 | } |
| 72 | |
| 73 | // ── Entry point ─────────────────────────────────────────────────────────── |
| 74 | |
| 75 | export function initActivity(): void { |
| 76 | attachRowAnimations(); |
| 77 | refreshTimestamps(); |
| 78 | setInterval(refreshTimestamps, 60_000); |
| 79 | bindHtmxSwap(); |
| 80 | } |
File History
1 commit
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923
fix(issues): use issue number as pagination cursor, not cre…
Sonnet 4.6
patch
8 days ago