feat: activity screen in the SPA, route /activity
This commit is contained in:
1 parent
e37a46a027
commit
35db442cd3
47 files changed
+7582
-11
No files matched your search
@@ -0,0 +1,110 @@
|
||||
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 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)}`
|
||||
}
|
||||
Reference in new issue
Block a user