From 08d4c800e9c197797791e0cee532bb64e11ff5f6 Mon Sep 17 00:00:00 2001 From: Rajiv Sahal Date: Thu, 20 Mar 2025 19:35:45 +0530 Subject: [PATCH] feat: allow timezones selection in `Booker` atom via prop (#20044) * expose props to pass in timezones and booking dry run values from outside * fix merge conflicts * update typings * add props values to test * fixup: PR feedback * PR feedback * fix wrong prop name * fix merge conflicts * fixup * update docs * update docs examples * fix props naming * fix timezone component logic and typo * extract typings of timeZones * update typings for Timezone * update docs * resolve merge conflicts * fix typings --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> --- docs/platform/atoms/booker.mdx | 1 + packages/features/bookings/Booker/Booker.tsx | 2 + .../bookings/Booker/components/EventMeta.tsx | 4 + packages/features/bookings/Booker/types.ts | 5 + .../timezone-select/TimezoneSelect.tsx | 10 +- packages/lib/timeZones.ts | 419 ++++++++++++++++++ .../atoms/booker/BookerPlatformWrapper.tsx | 8 +- packages/platform/atoms/booker/types.ts | 3 + packages/platform/atoms/src/lib/timeZones.ts | 20 + packages/platform/atoms/timezone/index.tsx | 31 +- .../examples/base/src/pages/booking.tsx | 2 + 11 files changed, 488 insertions(+), 17 deletions(-) create mode 100644 packages/lib/timeZones.ts create mode 100644 packages/platform/atoms/src/lib/timeZones.ts diff --git a/docs/platform/atoms/booker.mdx b/docs/platform/atoms/booker.mdx index 44c891b62b..dd748a2155 100644 --- a/docs/platform/atoms/booker.mdx +++ b/docs/platform/atoms/booker.mdx @@ -123,6 +123,7 @@ Below is a list of props that can be passed to the booker atom. | onBookerStateChange | No | Callback function that is triggered when the state of the booker atom changes. | | allowUpdatingUrlParams | No | Boolean indicating if the URL parameters should be updated, defaults to false. | | confirmButtonDisabled | No | Boolean indicating if the submit button should be disabled, defaults to false. | +| timeZones | No | Array of valid IANA timezones to be used in the booker. Eg. ["Asia/Kolkata", "Europe/London"] | ## Styling diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 4b0b890829..e36281c2c8 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -83,6 +83,7 @@ const BookerComponent = ({ renderCaptcha, hashedLink, confirmButtonDisabled, + timeZones, }: BookerProps & WrappedBookerProps) => { const searchParams = useCompatSearchParams(); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); @@ -404,6 +405,7 @@ const BookerComponent = ({ isPlatform={isPlatform} isPrivateLink={!!hashedLink} locale={userLocale} + timeZones={timeZones} /> {layout !== BookerLayouts.MONTH_VIEW && !(layout === "mobile" && bookerState === "booking") && ( diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index ce08086910..c4e2c4d9dd 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -6,6 +6,7 @@ import { shallow } from "zustand/shallow"; import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone"; import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings"; +import type { Timezone } from "@calcom/features/bookings/Booker/types"; import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText"; import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details"; import { useTimePreferences } from "@calcom/features/bookings/lib"; @@ -51,6 +52,7 @@ export const EventMeta = ({ isPrivateLink, classNames, locale, + timeZones, }: { event?: Pick< BookerEvent, @@ -83,6 +85,7 @@ export const EventMeta = ({ eventMetaTimezoneSelect?: string; }; locale?: string | null; + timeZones?: Timezone[]; }) => { const { timeFormat, timezone } = useBookerTime(); const [setTimezone] = useTimePreferences((state) => [state.setTimezone]); @@ -211,6 +214,7 @@ export const EventMeta = ({ event.lockTimeZoneToggleOnBookingPage ? "cursor-not-allowed" : "" }`}> ({ label: city, timezone })); + return ( ({ label: city, timezone }))} + data={[...cityTimezonesFormatted, ...SELECT_SEARCH_DATA]} isPending={isPending} {...props} /> @@ -64,6 +66,7 @@ export type TimezoneSelectComponentProps = SelectProps & { timezoneSelectCustomClassname?: string; size?: "sm" | "md"; grow?: boolean; + isWebTimezoneSelect?: boolean; }; // TODO: I wonder if we move this to ui package, and keep the TRPC version in features @@ -77,9 +80,10 @@ export function TimezoneSelectComponent({ value, size = "md", grow = false, + isWebTimezoneSelect = true, ...props }: TimezoneSelectComponentProps) { - const data = [...(props.data || []), ...SELECT_SEARCH_DATA]; + const data = [...(props.data || [])]; /* * we support multiple timezones for the different labels * e.g. 'Sao Paulo' and 'Brazil Time' both being 'America/Sao_Paulo' @@ -108,7 +112,7 @@ export function TimezoneSelectComponent({ {...reactSelectProps} timezones={{ ...(props.data ? addTimezonesToDropdown(data) : {}), - ...addTimezonesToDropdown(additionalTimezones), + ...(isWebTimezoneSelect ? addTimezonesToDropdown(additionalTimezones) : {}), }} styles={{ control: (base) => ({ diff --git a/packages/lib/timeZones.ts b/packages/lib/timeZones.ts new file mode 100644 index 0000000000..b23176e1f5 --- /dev/null +++ b/packages/lib/timeZones.ts @@ -0,0 +1,419 @@ +export const IntlSupportedTimeZones = [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/La_Rioja", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Monticello", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Chita", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Katmandu", + "Asia/Kolkata", + "Asia/Khandyga", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zurich", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", +] as const; diff --git a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx index 538b11d0ab..87e87d75f9 100644 --- a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx +++ b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx @@ -59,6 +59,7 @@ export const BookerPlatformWrapper = ( onBookerStateChange, allowUpdatingUrlParams = false, confirmButtonDisabled, + isBookingDryRun, } = props; const layout = BookerLayouts[view]; @@ -292,7 +293,9 @@ export const BookerPlatformWrapper = ( const _cacheParam = searchParams?.get("cal.cache"); const shouldServeCache = _cacheParam ? _cacheParam === "true" : undefined; - const isBookingDryRun = searchParams?.get("cal.isBookingDryRun")?.toLowerCase() === "true"; + const isBookingDryRun = + searchParams?.get("cal.isBookingDryRun")?.toLowerCase() === "true" || + searchParams?.get("cal.sandbox")?.toLowerCase() === "true"; setRoutingParams({ ...(skipContactOwner ? { skipContactOwner } : {}), ...(routedTeamMemberIds ? { routedTeamMemberIds } : {}), @@ -487,6 +490,7 @@ export const BookerPlatformWrapper = ( return ( ); diff --git a/packages/platform/atoms/booker/types.ts b/packages/platform/atoms/booker/types.ts index 8b4a14f6a7..050a9bb433 100644 --- a/packages/platform/atoms/booker/types.ts +++ b/packages/platform/atoms/booker/types.ts @@ -1,5 +1,6 @@ import type { BookerProps } from "@calcom/features/bookings/Booker"; import type { BookerStore } from "@calcom/features/bookings/Booker/store"; +import type { Timezone } from "@calcom/features/bookings/Booker/types"; import type { BookingResponse } from "@calcom/platform-libraries"; import type { ApiSuccessResponse, @@ -74,6 +75,8 @@ export type BookerPlatformWrapperAtomProps = Omit< preventEventTypeRedirect?: boolean; allowUpdatingUrlParams?: boolean; confirmButtonDisabled?: boolean; + timeZones?: Timezone[]; + isBookingDryRun?: boolean; }; type VIEW_TYPE = keyof typeof BookerLayouts; diff --git a/packages/platform/atoms/src/lib/timeZones.ts b/packages/platform/atoms/src/lib/timeZones.ts new file mode 100644 index 0000000000..689f87a7a1 --- /dev/null +++ b/packages/platform/atoms/src/lib/timeZones.ts @@ -0,0 +1,20 @@ +export const formatTimezones = ( + timeZones: { + city: string; + timezone: string; + }[] +) => { + return timeZones.map(({ city, timezone }) => ({ + label: city, + timezone, + })); +}; + +export const filterPropsTimezones = ( + propsTimezones: string[], + availableTimezones: { city: string; timezone: string }[] +) => { + return availableTimezones.filter((availableTimezone) => + propsTimezones.includes(availableTimezone.timezone) + ); +}; diff --git a/packages/platform/atoms/timezone/index.tsx b/packages/platform/atoms/timezone/index.tsx index 28a770fea3..96f4aa3e79 100644 --- a/packages/platform/atoms/timezone/index.tsx +++ b/packages/platform/atoms/timezone/index.tsx @@ -1,23 +1,30 @@ +import { useMemo } from "react"; + import { TimezoneSelectComponent as TimezoneSelect } from "@calcom/features/components/timezone-select"; import type { TimezoneSelectProps } from "@calcom/features/components/timezone-select"; - +import type { Timezone } from "@calcom/features/bookings/Booker/types"; import useGetCityTimezones from "../hooks/useGetCityTimezones"; +import { filterPropsTimezones, formatTimezones } from "../src/lib/timeZones"; -export function Timezone(props: TimezoneSelectProps) { - const { isLoading, data } = useGetCityTimezones(); +export function Timezone(props: TimezoneSelectProps & { timeZones?: Timezone[] }) { + const { isLoading: isLoadingAvailableCityTimezones, data: availableCityTimezones } = useGetCityTimezones(); + const cityTimeZones = useMemo(() => { + if (props.timeZones) { + const filteredTimeZones = filterPropsTimezones(props.timeZones, availableCityTimezones ?? []); + return formatTimezones(filteredTimeZones); + } else if (availableCityTimezones && !isLoadingAvailableCityTimezones) { + return formatTimezones(availableCityTimezones); + } + + return []; + }, [availableCityTimezones, props.timeZones, isLoadingAvailableCityTimezones]); return ( ({ - label: city, - timezone, - })) - : [] - } - isPending={isLoading} + data={cityTimeZones} + isPending={isLoadingAvailableCityTimezones} + isWebTimezoneSelect={false} /> ); } diff --git a/packages/platform/examples/base/src/pages/booking.tsx b/packages/platform/examples/base/src/pages/booking.tsx index eb56c0d0db..502365ea6d 100644 --- a/packages/platform/examples/base/src/pages/booking.tsx +++ b/packages/platform/examples/base/src/pages/booking.tsx @@ -91,6 +91,8 @@ export default function Bookings(props: { calUsername: string; calEmail: string {!bookingTitle && eventTypeSlug && !rescheduleUid && ( <> {