|
| 1 | +/** |
| 2 | + * Local time rendering — show UTC timestamps in the viewer's own timezone. |
| 3 | + * |
| 4 | + * The server can only emit UTC (it has no idea where the viewer is), so a raw |
| 5 | + * `2026-06-23T11:39:40.328499Z` is ambiguous and hard to read. The server |
| 6 | + * renders `<time data-ts="<iso>" data-format="...">` with a UTC fallback as |
| 7 | + * its text (legible even without JS); these helpers rewrite the text to the |
| 8 | + * browser's locale + timezone and add a full-detail tooltip. |
| 9 | + * |
| 10 | + * Wired through LiveSocket's `dom` callbacks in `main.ts` rather than a |
| 11 | + * per-element hook, so timestamps inside lists don't each need a unique id: |
| 12 | + * one pass localizes the initial render, `onNodeAdded` catches streamed-in |
| 13 | + * nodes, and `onBeforeElUpdated` re-localizes elements LiveView re-renders. |
| 14 | + * |
| 15 | + * Formats (via `data-format`): |
| 16 | + * - "time" 11:39:40.328 local time of day, ms precision |
| 17 | + * - "datetime" Jun 23, 2026, 11:39:40 local date + time |
| 18 | + * - "date" Jun 23, 2026 |
| 19 | + */ |
| 20 | + |
| 21 | +type Fmt = "time" | "datetime" | "date"; |
| 22 | + |
| 23 | +/** Localize a single `<time data-ts>` element in place. */ |
| 24 | +export function localizeTime(el: HTMLElement): void { |
| 25 | + const iso = el.dataset.ts; |
| 26 | + if (!iso) return; |
| 27 | + |
| 28 | + const d = new Date(iso); |
| 29 | + if (Number.isNaN(d.getTime())) return; |
| 30 | + |
| 31 | + // Relative elements ("2m ago" / "in 2h") keep their server-computed text; |
| 32 | + // only the hover tooltip is localized to the viewer's timezone so the exact |
| 33 | + // moment is legible without exposing a confusing UTC string. |
| 34 | + if (el.dataset.rel) { |
| 35 | + el.setAttribute("title", format(d, "datetime")); |
| 36 | + return; |
| 37 | + } |
| 38 | + |
| 39 | + el.textContent = format(d, (el.dataset.format as Fmt) || "datetime"); |
| 40 | + el.setAttribute("title", d.toString()); |
| 41 | +} |
| 42 | + |
| 43 | +/** Localize every `<time data-ts>` under (and including) `root`. */ |
| 44 | +export function localizeTimes(root: ParentNode | Element): void { |
| 45 | + if (root instanceof HTMLElement && root.matches("time[data-ts]")) { |
| 46 | + localizeTime(root); |
| 47 | + } |
| 48 | + for (const el of root.querySelectorAll<HTMLElement>("time[data-ts]")) { |
| 49 | + localizeTime(el); |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +function format(d: Date, fmt: Fmt): string { |
| 54 | + switch (fmt) { |
| 55 | + case "time": |
| 56 | + return d.toLocaleTimeString([], { |
| 57 | + hour: "2-digit", |
| 58 | + minute: "2-digit", |
| 59 | + second: "2-digit", |
| 60 | + fractionalSecondDigits: 3, |
| 61 | + hour12: false, |
| 62 | + }); |
| 63 | + case "date": |
| 64 | + return d.toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" }); |
| 65 | + default: |
| 66 | + return d.toLocaleString([], { |
| 67 | + year: "numeric", |
| 68 | + month: "short", |
| 69 | + day: "numeric", |
| 70 | + hour: "2-digit", |
| 71 | + minute: "2-digit", |
| 72 | + second: "2-digit", |
| 73 | + hour12: false, |
| 74 | + }); |
| 75 | + } |
| 76 | +} |
0 commit comments