diff --git a/apps/web/components/booking/TimezoneDropdown.tsx b/apps/web/components/booking/TimezoneDropdown.tsx
index 195a9acec7..dcffd1904c 100644
--- a/apps/web/components/booking/TimezoneDropdown.tsx
+++ b/apps/web/components/booking/TimezoneDropdown.tsx
@@ -17,7 +17,7 @@ export function TimezoneDropdown({
return (
<>
-
+
diff --git a/apps/web/pages/api/trpc/[trpc].ts b/apps/web/pages/api/trpc/[trpc].ts
index 81a1e4598d..d00d2ebb3e 100644
--- a/apps/web/pages/api/trpc/[trpc].ts
+++ b/apps/web/pages/api/trpc/[trpc].ts
@@ -65,6 +65,7 @@ export default trpcNext.createNextApiHandler({
"viewer.public.i18n": `no-cache`,
// Revalidation time here should be 1 second, per https://github.com/calcom/cal.com/pull/6823#issuecomment-1423215321
"viewer.public.slots.getSchedule": `no-cache`, // FIXME
+ "viewer.public.cityTimezones": `max-age=${ONE_DAY_IN_SECONDS}, stale-while-revalidate`,
} as const;
// Find which element above is an exact match for this group of paths
diff --git a/apps/web/test/lib/getTimezone.test.ts b/apps/web/test/lib/getTimezone.test.ts
new file mode 100644
index 0000000000..841c777a5c
--- /dev/null
+++ b/apps/web/test/lib/getTimezone.test.ts
@@ -0,0 +1,78 @@
+import { expect, it } from "@jest/globals";
+
+import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
+
+const cityData = [
+ {
+ city: "San Francisco",
+ timezone: "America/Argentina/Cordoba",
+ },
+ {
+ city: "Sao Francisco do Sul",
+ timezone: "America/Sao_Paulo",
+ },
+ {
+ city: "San Francisco de Macoris",
+ timezone: "America/Santo_Domingo",
+ },
+ {
+ city: "San Francisco Gotera",
+ timezone: "America/El_Salvador",
+ },
+ {
+ city: "San Francisco",
+ timezone: "America/Los_Angeles",
+ },
+];
+
+const option = {
+ value: "America/Los_Angeles",
+ label: "(GMT-8:00) San Francisco",
+ offset: -8,
+ abbrev: "PST",
+ altName: "Pacific Standard Time",
+};
+
+it("should return empty array for an empty string", () => {
+ expect(filterByCities("", cityData)).toMatchInlineSnapshot(`Array []`);
+});
+
+it("should filter cities for a valid city name", () => {
+ expect(filterByCities("San Francisco", cityData)).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "city": "San Francisco",
+ "timezone": "America/Argentina/Cordoba",
+ },
+ Object {
+ "city": "San Francisco de Macoris",
+ "timezone": "America/Santo_Domingo",
+ },
+ Object {
+ "city": "San Francisco Gotera",
+ "timezone": "America/El_Salvador",
+ },
+ Object {
+ "city": "San Francisco",
+ "timezone": "America/Los_Angeles",
+ },
+ ]
+ `);
+});
+
+it("should return appropriate timezone(s) for a given city name array", () => {
+ expect(addCitiesToDropdown(cityData)).toMatchInlineSnapshot(`
+ Object {
+ "America/Los_Angeles": "San Francisco",
+ "America/Sao_Paulo": "Sao Francisco do Sul",
+ }
+ `);
+});
+
+it("should render city name as option label if cityData is not empty", () => {
+ expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
+});
+
+it("should return timezone as option label if cityData is empty", () => {
+ expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los_Angeles GMT -8:00"`);
+});
diff --git a/package.json b/package.json
index 7d83fb89df..d894cb5b64 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
"ts-jest": "^28.0.8"
},
"dependencies": {
+ "city-timezones": "^1.2.1",
"turbo": "^1.4.3"
},
"resolutions": {
diff --git a/packages/lib/timezone.ts b/packages/lib/timezone.ts
new file mode 100644
index 0000000000..60c9b5a59c
--- /dev/null
+++ b/packages/lib/timezone.ts
@@ -0,0 +1,38 @@
+import type { ITimezoneOption } from "react-timezone-select";
+import { allTimezones } from "react-timezone-select";
+
+import type { ICity } from "@calcom/ui/components/form/timezone-select";
+
+function findPartialMatch(itemsToSearch: string, searchString: string) {
+ const searchItems = searchString.split(" ");
+ return searchItems.every((i) => itemsToSearch.toLowerCase().indexOf(i.toLowerCase()) >= 0);
+}
+
+function findFromCity(searchString: string, data: ICity[]): ICity[] {
+ if (searchString) {
+ const cityLookup = data.filter((o) => findPartialMatch(o.city, searchString));
+ return cityLookup?.length ? cityLookup : [];
+ }
+ return [];
+}
+
+export const filterByCities = (tz: string, data: ICity[]): ICity[] => {
+ const cityLookup = findFromCity(tz, data);
+ return cityLookup.map(({ city, timezone }) => ({ city, timezone }));
+};
+
+export const addCitiesToDropdown = (cities: ICity[]) => {
+ const cityTimezones = cities?.reduce((acc: { [key: string]: string }, city: ICity) => {
+ if (Object.keys(allTimezones).includes(city.timezone)) {
+ acc[city.timezone] = city.city;
+ }
+ return acc;
+ }, {});
+ return cityTimezones || {};
+};
+
+export const handleOptionLabel = (option: ITimezoneOption, cities: ICity[]) => {
+ const timezoneValue = option.label.split(")")[0].replace("(", " ").replace("T", "T ");
+ const cityName = option.label.split(") ")[1];
+ return cities.length > 0 ? `${cityName}${timezoneValue}` : `${option.value}${timezoneValue}`;
+};
diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx
index e87ccd1669..640df42d15 100644
--- a/packages/trpc/server/routers/viewer.tsx
+++ b/packages/trpc/server/routers/viewer.tsx
@@ -1,5 +1,6 @@
import type { DestinationCalendar, Prisma } from "@prisma/client";
import { AppCategories, BookingStatus, IdentityProvider } from "@prisma/client";
+import { cityMapping } from "city-timezones";
import _ from "lodash";
import { authenticator } from "otplib";
import z from "zod";
@@ -143,6 +144,7 @@ const publicViewerRouter = router({
}),
// REVIEW: This router is part of both the public and private viewer router?
slots: slotsRouter,
+ cityTimezones: publicProcedure.query(() => cityMapping),
});
// routes only available to authenticated users
diff --git a/packages/ui/components/form/timezone-select/TimezoneSelect.tsx b/packages/ui/components/form/timezone-select/TimezoneSelect.tsx
index 40430460dd..9d9a0542d2 100644
--- a/packages/ui/components/form/timezone-select/TimezoneSelect.tsx
+++ b/packages/ui/components/form/timezone-select/TimezoneSelect.tsx
@@ -1,28 +1,44 @@
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
import type { ITimezoneOption, ITimezone, Props as SelectProps } from "react-timezone-select";
import BaseSelect, { allTimezones } from "react-timezone-select";
+import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
+import { trpc } from "@calcom/trpc/react";
+
import { getReactSelectProps } from "../select";
+export interface ICity {
+ city: string;
+ timezone: string;
+}
+
export function TimezoneSelect({ className, components, ...props }: SelectProps) {
+ const [cities, setCities] = useState
([]);
+ const { data, isLoading } = trpc.viewer.public.cityTimezones.useQuery(undefined, {
+ trpc: { context: { skipBatch: true } },
+ });
+ const handleInputChange = (tz: string) => {
+ if (data) setCities(filterByCities(tz, data));
+ };
+
const reactSelectProps = useMemo(() => {
return getReactSelectProps({ className, components: components || {} });
}, [className, components]);
return (
{(option as ITimezoneOption).value}
}
- getOptionLabel={(data) => {
- const option = data as ITimezoneOption;
- const formatedLabel = option.label.split(")")[0].replace("(", " ").replace("T", "T ");
- return `${option.value}${formatedLabel}`;
- }}
+ getOptionLabel={(option) => handleOptionLabel(option as ITimezoneOption, cities)}
/>
);
}
diff --git a/packages/ui/components/form/timezone-select/index.ts b/packages/ui/components/form/timezone-select/index.ts
index 21a295f9ce..025fb331c0 100644
--- a/packages/ui/components/form/timezone-select/index.ts
+++ b/packages/ui/components/form/timezone-select/index.ts
@@ -1,2 +1,2 @@
export { TimezoneSelect } from "./TimezoneSelect";
-export type { ITimezone, ITimezoneOption } from "./TimezoneSelect";
+export type { ITimezone, ITimezoneOption, ICity } from "./TimezoneSelect";
diff --git a/yarn.lock b/yarn.lock
index d9450e895c..c10da465f8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11624,6 +11624,13 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"
+city-timezones@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/city-timezones/-/city-timezones-1.2.1.tgz#7087d1719dd599f1e88ebc2f7fb96e091201d318"
+ integrity sha512-hruuB611QFoUFMsan7xd9B2VPMrA8XC716O/999WW34kmaJUT1hxKF2W8TSXAWkhSqgvbu70DjcDv7/wpM6vow==
+ dependencies:
+ lodash "^4.17.21"
+
cjs-module-lexer@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"