atmo.rsvp
1import { parseAbsolute, parseDateTime } from '@internationalized/date';
2
3/**
4 * Convert a datetime-local string (e.g. "2026-04-09T22:00") to a UTC ISO string,
5 * interpreting the wall-clock components in the given IANA timezone.
6 */
7export function datetimeLocalToISO(dt: string, tz: string): string {
8 return parseDateTime(dt).toDate(tz).toISOString();
9}
10
11/**
12 * Convert a UTC ISO string to a datetime-local string ("YYYY-MM-DDTHH:mm") whose
13 * components represent the wall-clock time in the given IANA timezone.
14 */
15export function isoToDatetimeLocalInTz(iso: string, tz: string): string {
16 const zdt = parseAbsolute(iso, tz);
17 const pad = (n: number) => n.toString().padStart(2, '0');
18 return `${zdt.year}-${pad(zdt.month)}-${pad(zdt.day)}T${pad(zdt.hour)}:${pad(zdt.minute)}`;
19}
20
21/**
22 * Format an ISO timestamp using Intl options, rendered in the event's timezone
23 * when available. Falls back to the viewer's local zone for legacy events that
24 * predate the timezone field.
25 */
26export function formatInTz(
27 iso: string,
28 tz: string | undefined,
29 options: Intl.DateTimeFormatOptions,
30 locale: string = 'en-US'
31): string {
32 const date = new Date(iso);
33 return new Intl.DateTimeFormat(locale, { ...options, timeZone: tz || undefined }).format(date);
34}
35
36/**
37 * Returns the parts of an ISO timestamp in the given timezone (or viewer-local
38 * when tz is falsy). Useful when the caller wants numeric components like the
39 * day-of-month rendered in the event's zone.
40 */
41export function partsInTz(
42 iso: string,
43 tz: string | undefined,
44 options: Intl.DateTimeFormatOptions,
45 locale: string = 'en-US'
46): Record<string, string> {
47 const date = new Date(iso);
48 const parts = new Intl.DateTimeFormat(locale, {
49 ...options,
50 timeZone: tz || undefined
51 }).formatToParts(date);
52 const out: Record<string, string> = {};
53 for (const p of parts) if (p.type !== 'literal') out[p.type] = p.value;
54 return out;
55}