133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { locale, t } from '@/i18n'
|
||
|
||
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||
const MINUTE_MS = 60 * 1000
|
||
const HOUR_MS = 60 * MINUTE_MS
|
||
const ISO_DAY = /^(\d{4})-(\d{2})-(\d{2})$/
|
||
|
||
export interface DayGroup<T> {
|
||
key: string
|
||
label: string
|
||
items: T[]
|
||
}
|
||
|
||
function pad(value: number) {
|
||
return String(value).padStart(2, '0')
|
||
}
|
||
|
||
function monthShort(date: Date, timeZone?: 'UTC') {
|
||
if (locale.value === 'fr') {
|
||
return new Intl.DateTimeFormat('fr', {
|
||
month: 'short',
|
||
timeZone,
|
||
}).format(date)
|
||
}
|
||
return MONTHS[timeZone ? date.getUTCMonth() : date.getMonth()]
|
||
}
|
||
|
||
export function formatAbsoluteDay(date: Date, timeZone?: 'UTC') {
|
||
const day = timeZone ? date.getUTCDate() : date.getDate()
|
||
const year = timeZone ? date.getUTCFullYear() : date.getFullYear()
|
||
return `${day} ${monthShort(date, timeZone)} ${year}`
|
||
}
|
||
|
||
function clock(date: Date, withSeconds: boolean, utc: boolean) {
|
||
const hours = utc ? date.getUTCHours() : date.getHours()
|
||
const minutes = utc ? date.getUTCMinutes() : date.getMinutes()
|
||
const seconds = utc ? date.getUTCSeconds() : date.getSeconds()
|
||
const base = `${pad(hours)}:${pad(minutes)}`
|
||
return withSeconds ? `${base}:${pad(seconds)}` : base
|
||
}
|
||
|
||
export function localDayKey(date: Date) {
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||
}
|
||
|
||
function startOfDay(date: Date) {
|
||
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||
}
|
||
|
||
export function dayLabel(iso: string, now: Date = new Date()) {
|
||
const date = new Date(iso)
|
||
const today = startOfDay(now)
|
||
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
|
||
const key = localDayKey(date)
|
||
if (key === localDayKey(today)) return t('Today')
|
||
if (key === localDayKey(yesterday)) return t('Yesterday')
|
||
return formatAbsoluteDay(date)
|
||
}
|
||
|
||
export function whenLabel(iso: string, now: Date = new Date()) {
|
||
const date = new Date(iso)
|
||
if (localDayKey(date) !== localDayKey(now)) return clock(date, false, false)
|
||
const elapsed = Math.max(0, now.getTime() - date.getTime())
|
||
if (elapsed < HOUR_MS) {
|
||
return t('{0} min ago', [Math.max(1, Math.floor(elapsed / MINUTE_MS))])
|
||
}
|
||
return t('{0} h ago', [Math.floor(elapsed / HOUR_MS)])
|
||
}
|
||
|
||
export function recentWhenLabel(iso: string, now: Date = new Date()) {
|
||
const date = new Date(iso)
|
||
const key = localDayKey(date)
|
||
if (key === localDayKey(now)) return whenLabel(iso, now)
|
||
const today = startOfDay(now)
|
||
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
|
||
if (key === localDayKey(yesterday)) return `${t('Yesterday')}, ${clock(date, false, false)}`
|
||
return localDayTime(iso)
|
||
}
|
||
|
||
export function localDayTime(iso: string) {
|
||
const date = new Date(iso)
|
||
return `${formatAbsoluteDay(date)}, ${clock(date, false, false)}`
|
||
}
|
||
|
||
export function fullLocalTime(iso: string) {
|
||
const date = new Date(iso)
|
||
return `${formatAbsoluteDay(date)}, ${clock(date, true, false)}`
|
||
}
|
||
|
||
export function fullUtcTime(iso: string) {
|
||
const date = new Date(iso)
|
||
return `${formatAbsoluteDay(date, 'UTC')}, ${clock(date, true, true)}`
|
||
}
|
||
|
||
export function groupByDay<T extends { created_at: string }>(
|
||
items: T[],
|
||
now: Date = new Date(),
|
||
): DayGroup<T>[] {
|
||
const groups: DayGroup<T>[] = []
|
||
for (const item of items) {
|
||
const key = localDayKey(new Date(item.created_at))
|
||
const last = groups[groups.length - 1]
|
||
if (last && last.key === key) {
|
||
last.items.push(item)
|
||
continue
|
||
}
|
||
groups.push({
|
||
key,
|
||
label: dayLabel(item.created_at, now),
|
||
items: [item],
|
||
})
|
||
}
|
||
return groups
|
||
}
|
||
|
||
function parseIsoDay(value: string) {
|
||
const match = ISO_DAY.exec(value)
|
||
if (!match) return null
|
||
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12)
|
||
}
|
||
|
||
export function formatDateRange(from: string, to: string) {
|
||
const start = parseIsoDay(from)
|
||
const end = parseIsoDay(to)
|
||
if (!start || !end) return ''
|
||
const sameYear = start.getFullYear() === end.getFullYear()
|
||
const sameMonth = sameYear && start.getMonth() === end.getMonth()
|
||
if (sameMonth && start.getDate() === end.getDate()) return formatAbsoluteDay(end)
|
||
if (sameMonth) return `${start.getDate()} – ${formatAbsoluteDay(end)}`
|
||
if (sameYear) return `${start.getDate()} ${monthShort(start)} – ${formatAbsoluteDay(end)}`
|
||
return `${formatAbsoluteDay(start)} – ${formatAbsoluteDay(end)}`
|
||
}
|