import { DateTime } from 'luxon' export type DateFormatOptions = { format?: 'full' | 'long' | 'medium' | 'short' relative?: boolean includeTime?: boolean locale?: string } export function formatRelativeDate(dateString: string): string { const dateTime = DateTime.fromISO(dateString).setLocale('en-US') if (!dateTime.isValid) { return 'Invalid date' } const now = DateTime.now().setLocale('en-US') if (dateTime.hasSame(now, 'day')) { return new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' }).format(0, 'day') } return dateTime.toRelative() ?? '' } export function formatDate(date: string | Date, options: DateFormatOptions = {}) { const { format = 'medium', relative = false, includeTime = false, locale } = options const baseDateTime = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date) const dateTime = locale ? baseDateTime.setLocale(locale) : baseDateTime if (!dateTime.isValid) { return 'Invalid date' } if (relative) { return dateTime.toRelative() ?? '' } switch (format) { case 'full': return dateTime.toLocaleString({ ...DateTime.DATE_HUGE, ...(includeTime && DateTime.TIME_SIMPLE) }) case 'long': return dateTime.toLocaleString({ ...DateTime.DATE_FULL, ...(includeTime && DateTime.TIME_SIMPLE) }) case 'short': return dateTime.toLocaleString({ ...DateTime.DATE_SHORT, ...(includeTime && DateTime.TIME_SIMPLE) }) default: return dateTime.toLocaleString({ ...DateTime.DATE_MED, ...(includeTime && DateTime.TIME_SIMPLE), }) } } export function minimalFormatDate(date: Date): string { const now = new Date() const isSameDay = date.getDate() === now.getDate() && date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear() if (isSameDay) { return new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit', }).format(date) } const isSameYear = date.getFullYear() === now.getFullYear() if (isSameYear) { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', }).format(date) } return new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'numeric', year: '2-digit', }).format(date) } export function narrowTimeDelta(isoTimestamp: string) { return DateTime.fromISO(isoTimestamp) .toRelative({ style: 'narrow' }) ?.replace(/\.?\sago/, '') } export const isoDateRegex = /^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+\-]\d{2}:\d{2})?)?$/ // Truncates to this format: 43m ago, 20h ago, 20 days ago, 1 year ago export function timeAgo(date: string | Date): string { const dateTime = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date) if (!dateTime.isValid) { return 'Invalid date' } const now = DateTime.now() const diff = now.diff(dateTime, ['years', 'months', 'days', 'hours', 'minutes']).toObject() if (diff.years && diff.years >= 1) { return `${Math.floor(diff.years)} year${Math.floor(diff.years) > 1 ? 's' : ''} ago` } if (diff.months && diff.months >= 1) { return `${Math.floor(diff.months)} month${Math.floor(diff.months) > 1 ? 's' : ''} ago` } if (diff.days && diff.days >= 1) { return `${Math.floor(diff.days)} day${Math.floor(diff.days) > 1 ? 's' : ''} ago` } if (diff.hours && diff.hours >= 1) { return `${Math.floor(diff.hours)}h ago` } if (diff.minutes && diff.minutes >= 1) { return `${Math.floor(diff.minutes)}m ago` } return 'just now' }