Make UI language, timestamps and live feedback consistent
This commit is contained in:
+30
-2
@@ -17,7 +17,7 @@ export function availableTimeZones(): string[] {
|
||||
return [...new Set([DEFAULT_TIME_ZONE, 'UTC', ...zones])].sort();
|
||||
}
|
||||
|
||||
type Timestamp = string | number | Date | null | undefined;
|
||||
export type Timestamp = string | number | Date | null | undefined;
|
||||
|
||||
/** Database timestamps without an offset are UTC, never browser-local time. */
|
||||
function parseTimestamp(value: Exclude<Timestamp, null | undefined>): Date {
|
||||
@@ -28,6 +28,12 @@ function parseTimestamp(value: Exclude<Timestamp, null | undefined>): Date {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
export function timestampISO(value: Timestamp): string | undefined {
|
||||
if (value === null || value === undefined || value === '') return undefined;
|
||||
const parsed = parseTimestamp(value);
|
||||
return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : undefined;
|
||||
}
|
||||
|
||||
export function createTimeFormatter(zone: string) {
|
||||
const timeZone = validTimeZone(zone) ? zone : DEFAULT_TIME_ZONE;
|
||||
const date: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||
@@ -40,12 +46,34 @@ export function createTimeFormatter(zone: string) {
|
||||
return Number.isFinite(parsed.getTime()) ? format.format(parsed) : '—';
|
||||
};
|
||||
}
|
||||
const absolute = formatter({ ...date, hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
|
||||
const clock = formatter({ hour: 'numeric', minute: '2-digit' });
|
||||
const calendar = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: 'numeric', day: 'numeric' });
|
||||
const calendarDay = (value: Date) => {
|
||||
const parts = Object.fromEntries(calendar.formatToParts(value).map(part => [part.type, part.value]));
|
||||
return Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day));
|
||||
};
|
||||
function relative(value: Timestamp, now = Date.now()): string {
|
||||
if (value === null || value === undefined || value === '') return '—';
|
||||
const parsed = parseTimestamp(value);
|
||||
if (!Number.isFinite(parsed.getTime()) || !Number.isFinite(now)) return '—';
|
||||
const age = now - parsed.getTime();
|
||||
if (age < 0) return absolute(parsed);
|
||||
if (age < 5_000) return 'Just now';
|
||||
if (age < 60_000) return `${Math.floor(age / 1000)} s ago`;
|
||||
if (age < 3_600_000) return `${Math.floor(age / 60_000)} min ago`;
|
||||
if (age < 86_400_000) return `${Math.floor(age / 3_600_000)} h ago`;
|
||||
// Calendar arithmetic keeps Yesterday correct across DST and year changes.
|
||||
if (calendarDay(new Date(now)) - calendarDay(parsed) === 86_400_000) return `Yesterday, ${clock(parsed)}`;
|
||||
return absolute(parsed);
|
||||
}
|
||||
return {
|
||||
relative,
|
||||
dateTime: formatter({ ...date, ...time }),
|
||||
date: formatter(date),
|
||||
time: formatter(time),
|
||||
clockTime: formatter({ hour: 'numeric', minute: '2-digit', second: '2-digit' }),
|
||||
minuteTime: formatter({ hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }),
|
||||
period: formatter({ ...date, hour: 'numeric', minute: '2-digit', timeZoneName: 'short' })
|
||||
period: absolute
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user