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>
64 lines
2.3 KiB
JavaScript
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',
|
|
})
|
|
}
|