diff --git a/packages/web/src/booking/PublicBookingMonthGrid.tsx b/packages/web/src/booking/PublicBookingMonthGrid.tsx
index 2530ddbdd5..4cf4dd42d3 100644
--- a/packages/web/src/booking/PublicBookingMonthGrid.tsx
+++ b/packages/web/src/booking/PublicBookingMonthGrid.tsx
@@ -178,7 +178,6 @@ export function PublicBookingMonthGrid({
}
const { day } = cell;
const isSelected = selectedDateKey === day.dateKey;
- const label = formatBookingMonthDayLabel(day.dateKey, timeZone);
if (!day.available) {
return (
,
+ locale?: string,
+): ((timeZone: string) => Intl.DateTimeFormat) => {
+ const cache = new Map();
+ return (timeZone: string) => {
+ const cached = cache.get(timeZone);
+ if (cached) {
+ return cached;
+ }
+ const formatter = new Intl.DateTimeFormat(locale, { ...options, timeZone });
+ cache.set(timeZone, formatter);
+ return formatter;
+ };
+};
+
+const monthHeadingFormatter = perTimeZoneFormatter({
+ month: "long",
+ year: "numeric",
+});
+const timeZoneNameFormatter = perTimeZoneFormatter({ timeZoneName: "long" });
+const slotLabelFormatter = perTimeZoneFormatter({
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+});
+const slotTimeFormatter = perTimeZoneFormatter({
+ hour: "numeric",
+ minute: "2-digit",
+});
+const slotDateHeadingFormatter = perTimeZoneFormatter({
+ weekday: "long",
+ month: "long",
+ day: "numeric",
+});
+const monthDayLabelFormatter = perTimeZoneFormatter({
+ weekday: "long",
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+});
+const weekdayShortFormatter = perTimeZoneFormatter({ weekday: "short" });
+const weekdayLongFormatter = perTimeZoneFormatter({ weekday: "long" });
+/** `en-CA` prints ISO date order, which is what a YYYY-MM-DD key needs. */
+const dateKeyFormatter = perTimeZoneFormatter(
+ { year: "numeric", month: "2-digit", day: "2-digit" },
+ "en-CA",
+);
+
export const isBookingMonthKey = (value: string): boolean =>
MONTH_KEY_PATTERN.test(value);
@@ -40,11 +97,7 @@ export function formatBookingMonthHeading(
if (!monthStart) {
return monthKey;
}
- return new Intl.DateTimeFormat(undefined, {
- month: "long",
- year: "numeric",
- timeZone,
- }).format(monthStart.toDate());
+ return monthHeadingFormatter(timeZone).format(monthStart.toDate());
}
function parseBookingMonthStart(
@@ -104,10 +157,7 @@ export function isBookingMonthAvailable(
export function formatGuestTimeZoneLabel(timeZone: string): string {
try {
- const parts = new Intl.DateTimeFormat(undefined, {
- timeZone,
- timeZoneName: "long",
- }).formatToParts(new Date());
+ const parts = timeZoneNameFormatter(timeZone).formatToParts(new Date());
const name = parts.find((part) => part.type === "timeZoneName")?.value;
return name ?? timeZone;
} catch {
@@ -119,37 +169,21 @@ export function formatBookingSlotLabel(
slotStart: string,
timeZone: string,
): string {
- return new Intl.DateTimeFormat(undefined, {
- timeZone,
- weekday: "short",
- month: "short",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- }).format(new Date(slotStart));
+ return slotLabelFormatter(timeZone).format(new Date(slotStart));
}
export function formatBookingSlotTime(
slotStart: string,
timeZone: string,
): string {
- return new Intl.DateTimeFormat(undefined, {
- timeZone,
- hour: "numeric",
- minute: "2-digit",
- }).format(new Date(slotStart));
+ return slotTimeFormatter(timeZone).format(new Date(slotStart));
}
export function formatBookingSlotDateHeading(
slotStart: string,
timeZone: string,
): string {
- return new Intl.DateTimeFormat(undefined, {
- timeZone,
- weekday: "long",
- month: "long",
- day: "numeric",
- }).format(new Date(slotStart));
+ return slotDateHeadingFormatter(timeZone).format(new Date(slotStart));
}
export function formatDurationMinutes(minutes: number): string {
@@ -165,25 +199,7 @@ export function formatDurationMinutes(minutes: number): string {
return `${hourLabel} ${remainder} minutes`;
}
-/** Cache one formatter per timezone; constructing Intl is the expensive part. */
-const dateKeyFormatters = new Map();
-
-const dateKeyFormatter = (timeZone: string): Intl.DateTimeFormat => {
- const cached = dateKeyFormatters.get(timeZone);
- if (cached) {
- return cached;
- }
- const formatter = new Intl.DateTimeFormat("en-CA", {
- timeZone,
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- });
- dateKeyFormatters.set(timeZone, formatter);
- return formatter;
-};
-
-/** YYYY-MM-DD in `timeZone`. `en-CA` prints ISO date order. */
+/** YYYY-MM-DD in `timeZone`. */
export function formatBookingDateKey(
instant: Date | string | Dayjs,
timeZone: string,
@@ -319,14 +335,8 @@ export function listBookingWeekdayHeadings(
return Array.from({ length: 7 }, (_, index) => {
const date = sunday.add(index, "day").toDate();
return {
- short: new Intl.DateTimeFormat(undefined, {
- weekday: "short",
- timeZone,
- }).format(date),
- long: new Intl.DateTimeFormat(undefined, {
- weekday: "long",
- timeZone,
- }).format(date),
+ short: weekdayShortFormatter(timeZone).format(date),
+ long: weekdayLongFormatter(timeZone).format(date),
};
});
}
@@ -335,13 +345,9 @@ export function formatBookingMonthDayLabel(
dateKey: string,
timeZone: string,
): string {
- return new Intl.DateTimeFormat(undefined, {
- weekday: "long",
- month: "long",
- day: "numeric",
- year: "numeric",
- timeZone,
- }).format(dayjs.tz(dateKey, timeZone).toDate());
+ return monthDayLabelFormatter(timeZone).format(
+ dayjs.tz(dateKey, timeZone).toDate(),
+ );
}
export function listBookingAvailableDateKeysInMonth(
diff --git a/packages/web/src/booking/public-booking.query.ts b/packages/web/src/booking/public-booking.query.ts
index e72da5cf10..ee86687a16 100644
--- a/packages/web/src/booking/public-booking.query.ts
+++ b/packages/web/src/booking/public-booking.query.ts
@@ -26,23 +26,24 @@ const PUBLIC_BOOKING_SLOTS_STALE_TIME_MS = 60_000;
export const publicBookingQueryKeys = {
page: (slug: string) => ["public-booking", "page", slug] as const,
+ /** Prefix covering every month and timezone of one host's slots. */
+ slotsAll: (slug: string) => ["public-booking", "slots", slug] as const,
slots: (slug: string, monthKey: string, timeZone: string) =>
- ["public-booking", "slots", slug, monthKey, timeZone] as const,
+ [...publicBookingQueryKeys.slotsAll(slug), monthKey, timeZone] as const,
reservation: (reservationId: string) =>
["public-booking", "reservation", reservationId] as const,
};
+/** A page or reservation that is gone stays gone; retry once for anything else. */
+const retryUnlessNotFound = (failureCount: number, error: Error): boolean =>
+ !(error instanceof PublicBookingNotFoundError) && failureCount < 1;
+
function publicBookingPageQueryOptions(slug: string) {
return queryOptions({
queryKey: publicBookingQueryKeys.page(slug),
queryFn: () => PublicBookingApi.getPage(slug),
staleTime: PUBLIC_BOOKING_PAGE_STALE_TIME_MS,
- retry: (failureCount, error) => {
- if (error instanceof PublicBookingNotFoundError) {
- return false;
- }
- return failureCount < 1;
- },
+ retry: retryUnlessNotFound,
});
}
@@ -55,12 +56,7 @@ function publicBookingReservationQueryOptions(reservationId: string) {
queryKey: publicBookingQueryKeys.reservation(reservationId),
queryFn: () => PublicBookingApi.getReservation(reservationId),
staleTime: PUBLIC_BOOKING_PAGE_STALE_TIME_MS,
- retry: (failureCount, error) => {
- if (error instanceof PublicBookingNotFoundError) {
- return false;
- }
- return failureCount < 1;
- },
+ retry: retryUnlessNotFound,
enabled: Boolean(reservationId),
});
}
@@ -243,7 +239,7 @@ export function useCreatePublicBookingReservationMutation(slug: string) {
PublicBookingApi.createReservation(slug, input),
onSuccess: () => {
void queryClient.invalidateQueries({
- queryKey: ["public-booking", "slots", slug],
+ queryKey: publicBookingQueryKeys.slotsAll(slug),
});
},
});