open-navigator / web_app /src /utils /dateTime.ts
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
Raw
History Blame Contribute Delete
2.37 kB
/** Parse ISO / Postgres timestamptz strings from APIs. */
export function parseApiDateTime(value: string | null | undefined): Date | null {
if (!value?.trim() || value === 'β€”') return null
const d = new Date(value.trim())
return Number.isNaN(d.getTime()) ? null : d
}
const absoluteFormatter = new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
})
/** Local calendar + clock, e.g. "May 25, 2026, 8:55 PM". */
export function formatDateTimeAbsolute(value: string | null | undefined): string {
const d = parseApiDateTime(value)
if (!d) return value?.trim() || 'β€”'
return absoluteFormatter.format(d)
}
function formatRelativeToNow(date: Date): string {
const diffMs = date.getTime() - Date.now()
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
const absSec = Math.abs(Math.round(diffMs / 1000))
if (absSec < 45) return rtf.format(Math.round(diffMs / 1000), 'second')
if (absSec < 45 * 60) return rtf.format(Math.round(diffMs / 60_000), 'minute')
if (absSec < 22 * 3_600) return rtf.format(Math.round(diffMs / 3_600_000), 'hour')
if (absSec < 6 * 86_400) return rtf.format(Math.round(diffMs / 86_400_000), 'day')
return absoluteFormatter.format(date)
}
/** Compact age for live dashboards, e.g. ``5m ago``, ``2h ago``. */
export function formatAgoCompact(
value: string | null | undefined,
nowMs = Date.now(),
): string {
const d = parseApiDateTime(value)
if (!d) return 'β€”'
const sec = Math.max(0, Math.floor((nowMs - d.getTime()) / 1000))
if (sec < 60) return `${Math.max(1, sec)}s ago`
const m = Math.floor(sec / 60)
if (m < 60) return `${m}m ago`
const h = Math.floor(m / 60)
if (h < 48) return `${h}h ago`
const days = Math.floor(h / 24)
return `${days}d ago`
}
/** "Updated" line: relative when within a week, otherwise absolute. */
export function formatUpdatedAt(value: string | null | undefined): {
display: string
title: string
} {
const d = parseApiDateTime(value)
if (!d) {
const fallback = value?.trim() || 'β€”'
return { display: fallback, title: fallback }
}
const absolute = formatDateTimeAbsolute(value)
const ageDays = Math.abs(d.getTime() - Date.now()) / 86_400_000
return {
display: ageDays < 7 ? formatRelativeToNow(d) : absolute,
title: absolute,
}
}