Files
FabledCurator/frontend/src/utils/date.js
T
bvandeusenandClaude Opus 4.7 c87e8e0932 fix(ui): render absolute timestamps in the viewer's local timezone
Download event times showed raw UTC wall-clock (iso.slice). Added
formatDateTime()/formatLocalDate() (local tz, robust to naive vs tz-aware
ISO) and applied them to the download row + detail modal datetimes and the
credential/artist date displays. formatPostDate stays UTC (date-only,
locale-stable, unit-tested).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:36:42 -04:00

64 lines
2.3 KiB
JavaScript

// Locale-independent post-date formatting. Uses a fixed month table and
// UTC getters so the rendered string never varies by browser/CI locale
// or timezone (toLocaleDateString would make tests flaky).
const MONTHS = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
export function formatPostDate(iso) {
if (!iso) return null
const d = new Date(iso)
if (isNaN(d.getTime())) return null
return `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`
}
// Compact relative formatter: "5m ago" (past) or "in 3h" (future). Null /
// invalid input returns `nullText`. Pass { future: true } for upcoming
// timestamps; a future time already elapsed reads as "imminent".
export function formatRelative(iso, opts = {}) {
const { future = false, nullText = future ? '—' : 'Never' } = opts
if (!iso) return nullText
const then = new Date(iso).getTime()
if (isNaN(then)) return nullText
const diff = (then - Date.now()) / 1000 // +ve = future
const abs = Math.abs(diff)
let body
if (abs < 60) body = `${Math.floor(abs)}s`
else if (abs < 3600) body = `${Math.floor(abs / 60)}m`
else if (abs < 86400) body = `${Math.floor(abs / 3600)}h`
else body = `${Math.floor(abs / 86400)}d`
if (future) return diff <= 0 ? 'imminent' : `in ${body}`
return `${body} ago`
}
// Parse a backend timestamp as a UTC instant. Backend serializes tz-aware
// UTC (…+00:00); if a value ever arrives without a tz designator we treat
// it as UTC so it still converts to the viewer's zone correctly.
function _parseUtc (iso) {
if (typeof iso !== 'string' || !iso) return null
const hasTz = /([zZ])$|([+-]\d{2}:?\d{2})$/.test(iso)
const d = new Date(hasTz ? iso : `${iso}Z`)
return isNaN(d.getTime()) ? null : d
}
// Local-timezone date + time, e.g. "May 28, 7:30 PM". Use for absolute
// timestamps that were previously shown as raw UTC ISO.
export function formatDateTime (iso) {
const d = _parseUtc(iso)
if (!d) return ''
return d.toLocaleString(undefined, {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
})
}
// Local-timezone date only, e.g. "May 28, 2026".
export function formatLocalDate (iso) {
const d = _parseUtc(iso)
if (!d) return ''
return d.toLocaleDateString(undefined, {
year: 'numeric', month: 'short', day: 'numeric',
})
}