diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index d76aeee407..2b295d4a06 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -8,18 +8,22 @@ import { CreditCardIcon, GlobeIcon, InformationCircleIcon, + LocationMarkerIcon, RefreshIcon, + VideoCameraIcon, } from "@heroicons/react/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; import { useContracts } from "contexts/contractsContext"; import dayjs, { Dayjs } from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; import utc from "dayjs/plugin/utc"; +import { TFunction } from "next-i18next"; import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; import { Frequency as RRuleFrequency } from "rrule"; +import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations"; import { useEmbedStyles, useIsEmbed, @@ -57,6 +61,33 @@ dayjs.extend(customParseFormat); type Props = AvailabilityTeamPageProps | AvailabilityPageProps; +export const locationKeyToString = (location: LocationObject, t: TFunction) => { + switch (location.type) { + case LocationType.InPerson: + return location.address || "In Person"; // If disabled address won't exist on the object + case LocationType.Link: + return location.link || "Link"; // If disabled link won't exist on the object + case LocationType.Phone: + return t("phone_call"); + case LocationType.GoogleMeet: + return "Google Meet"; + case LocationType.Zoom: + return "Zoom"; + case LocationType.Daily: + return "Cal Video"; + case LocationType.Jitsi: + return "Jitsi"; + case LocationType.Huddle01: + return "Huddle Video"; + case LocationType.Tandem: + return "Tandem"; + case LocationType.Teams: + return "Microsoft Teams"; + default: + return null; + } +}; + const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage, booking }: Props) => { const router = useRouter(); const isEmbed = useIsEmbed(); @@ -225,6 +256,25 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage {eventType.description}

)} + {eventType.locations.length === 1 && ( +

+ + {locationKeyToString(eventType.locations[0], t)} +

+ )} + {eventType.locations.length === 1 && ( +

+ {Object.values(AppStoreLocationType).includes( + eventType.locations[0].type as unknown as AppStoreLocationType + ) ? ( + + ) : ( + + )} + + {locationKeyToString(eventType.locations[0], t)} +

+ )}

{eventType.length} {t("minutes")} @@ -297,6 +347,36 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage {eventType.description}

)} + {eventType.locations.length === 1 && ( +

+ {Object.values(AppStoreLocationType).includes( + eventType.locations[0].type as unknown as AppStoreLocationType + ) ? ( + + ) : ( + + )} + + {locationKeyToString(eventType.locations[0], t)} +

+ )} + {eventType.locations.length > 1 && ( +
+
+ +
+

+ {eventType.locations.map((el, i, arr) => { + return ( + + {locationKeyToString(el, t)}{" "} + {arr.length - 1 !== i && or } + + ); + })} +

+
+ )}

{eventType.length} {t("minutes")} @@ -340,7 +420,6 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage

)} - {previousPage === `${WEBAPP_URL}/${profile.slug}` && (
diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index cae3c9c5f9..22b31977f6 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -37,7 +37,7 @@ import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { ensureArray } from "@lib/ensureArray"; import useTheme from "@lib/hooks/useTheme"; -import { LocationType } from "@lib/location"; +import { LocationObject, LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; import { parseDate, parseRecurringDates } from "@lib/parseDate"; @@ -203,10 +203,9 @@ const BookingPage = ({ const eventTypeDetail = { isWeb3Active: false, ...eventType }; - type Location = { type: LocationType; address?: string; link?: string; hostPhoneNumber?: string }; // it would be nice if Prisma at some point in the future allowed for Json; as of now this is not the case. - const locations: Location[] = useMemo( - () => (eventType.locations as Location[]) || [], + const locations: LocationObject[] = useMemo( + () => (eventType.locations as LocationObject[]) || [], [eventType.locations] ); diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index d46db6244e..c6107c7625 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -3,6 +3,7 @@ import { UserPlan } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; import { JSONObject } from "superjson/dist/types"; +import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@calcom/app-store/locations"; import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { RecurringEvent } from "@calcom/types/Calendar"; @@ -84,6 +85,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => periodEndDate: true, periodDays: true, periodCountCalendarDays: true, + locations: true, schedulingType: true, recurringEvent: true, schedule: { @@ -254,12 +256,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => } as const; } } + const locations = eventType.locations ? (eventType.locations as LocationObject[]) : []; const eventTypeObject = Object.assign({}, eventType, { metadata: (eventType.metadata || {}) as JSONObject, periodStartDate: eventType.periodStartDate?.toString() ?? null, periodEndDate: eventType.periodEndDate?.toString() ?? null, recurringEvent: (eventType.recurringEvent || {}) as RecurringEvent, + locations: locationHiddenFilter(locations), }); const schedule = eventType.schedule diff --git a/apps/web/pages/d/[link]/[slug].tsx b/apps/web/pages/d/[link]/[slug].tsx index 6721a5c8c3..147e8072c1 100644 --- a/apps/web/pages/d/[link]/[slug].tsx +++ b/apps/web/pages/d/[link]/[slug].tsx @@ -7,6 +7,7 @@ import { RecurringEvent } from "@calcom/types/Calendar"; import { asStringOrNull } from "@lib/asStringOrNull"; import { getWorkingHours } from "@lib/availability"; import { GetBookingType } from "@lib/getBooking"; +import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@lib/location"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -54,6 +55,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => minimumBookingNotice: true, beforeEventBuffer: true, afterEventBuffer: true, + locations: true, timeZone: true, metadata: true, slotInterval: true, @@ -132,6 +134,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => notFound: true, }; } + + const locations = hashedLink.eventType.locations + ? (hashedLink.eventType.locations as LocationObject[]) + : []; + const [user] = users; const eventTypeObject = Object.assign({}, hashedLink.eventType, { metadata: {} as JSONObject, @@ -139,6 +146,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => periodStartDate: hashedLink.eventType.periodStartDate?.toString() ?? null, periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null, slug, + locations: locationHiddenFilter(locations), }); const schedule = { diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index 9ddd425979..ad38708139 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -50,7 +50,7 @@ import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import { HttpError } from "@lib/core/http/error"; import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable"; -import { LocationType } from "@lib/location"; +import { LocationObject, LocationType } from "@lib/location"; import prisma from "@lib/prisma"; import { slugify } from "@lib/slugify"; import { trpc } from "@lib/trpc"; @@ -118,7 +118,13 @@ export type FormValues = { hidden: boolean; hideCalendarNotes: boolean; hashedLink: string | undefined; - locations: { type: LocationType; address?: string; link?: string; hostPhoneNumber?: string }[]; + locations: { + type: LocationType; + address?: string; + link?: string; + hostPhoneNumber?: string; + displayLocationPublicly?: boolean; + }[]; customInputs: EventTypeCustomInput[]; users: string[]; schedule: number; @@ -422,6 +428,7 @@ const EventTypePage = (props: inferSSRProps) => { if (!selectedLocation) { return null; } + switch (selectedLocation.value) { case LocationType.InPerson: return ( @@ -435,7 +442,7 @@ const EventTypePage = (props: inferSSRProps) => { {...locationFormMethods.register("locationAddress")} id="address" required - className=" block w-full rounded-sm border-gray-300 text-sm" + className="block w-full rounded-sm border-gray-300 text-sm" defaultValue={ formMethods .getValues("locations") @@ -443,6 +450,18 @@ const EventTypePage = (props: inferSSRProps) => { } />
+
+ ( + onChange(e.target.checked)} + infomationIconText={t("display_location_info_badge")}> + )} + /> +
); case LocationType.Link: @@ -469,6 +488,18 @@ const EventTypePage = (props: inferSSRProps) => {

)} +
+ ( + onChange(e.target.checked)} + infomationIconText={t("display_location_info_badge")}> + )} + /> +
); case LocationType.UserPhone: @@ -584,6 +615,7 @@ const EventTypePage = (props: inferSSRProps) => { const locationFormSchema = z.object({ locationType: z.string(), locationAddress: z.string().optional(), + displayLocationPublicly: z.boolean().optional(), locationPhoneNumber: z .string() .refine((val) => isValidPhoneNumber(val)) @@ -596,6 +628,7 @@ const EventTypePage = (props: inferSSRProps) => { locationPhoneNumber?: string; locationAddress?: string; // TODO: We should validate address or fetch the address from googles api to see if its valid? locationLink?: string; // Currently this only accepts links that are HTTPS:// + displayLocationPublicly?: boolean; }>({ resolver: zodResolver(locationFormSchema), }); @@ -2131,14 +2164,16 @@ const EventTypePage = (props: inferSSRProps) => {
{ - const newLocation = values.locationType; - + const { locationType: newLocation, displayLocationPublicly } = values; let details = {}; if (newLocation === LocationType.InPerson) { - details = { address: values.locationAddress }; + details = { + address: values.locationAddress, + displayLocationPublicly, + }; } if (newLocation === LocationType.Link) { - details = { link: values.locationLink }; + details = { link: values.locationLink, displayLocationPublicly }; } if (newLocation === LocationType.UserPhone) { details = { hostPhoneNumber: values.locationPhoneNumber }; @@ -2373,11 +2408,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => if (!rawEventType) throw Error("Event type not found"); - type Location = { - type: LocationType; - address?: string; - }; - const credentials = await prisma.credential.findMany({ where: { userId: session.user.id, @@ -2396,7 +2426,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const eventType = { ...restEventType, recurringEvent: (restEventType.recurringEvent || {}) as RecurringEvent, - locations: locations as unknown as Location[], + locations: locations as unknown as LocationObject[], metadata: (metadata || {}) as JSONObject, isWeb3Active: web3Credentials && web3Credentials.key diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 5083749fba..a740a70e17 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -7,6 +7,7 @@ import { RecurringEvent } from "@calcom/types/Calendar"; import { asStringOrNull } from "@lib/asStringOrNull"; import { getWorkingHours } from "@lib/availability"; import getBooking, { GetBookingType } from "@lib/getBooking"; +import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@lib/location"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -71,6 +72,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => beforeEventBuffer: true, afterEventBuffer: true, recurringEvent: true, + locations: true, price: true, currency: true, timeZone: true, @@ -107,11 +109,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => eventType.schedule = null; + const locations = eventType.locations ? (eventType.locations as LocationObject[]) : []; + const eventTypeObject = Object.assign({}, eventType, { metadata: (eventType.metadata || {}) as JSONObject, periodStartDate: eventType.periodStartDate?.toString() ?? null, periodEndDate: eventType.periodEndDate?.toString() ?? null, recurringEvent: (eventType.recurringEvent || {}) as RecurringEvent, + locations: locationHiddenFilter(locations), }); eventTypeObject.availability = []; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index d436894040..67ca80eedb 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -836,6 +836,8 @@ "go_to_app_store": "Go to App Store", "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions", "calendar_no_busy_slots": "There are no busy slots", + "display_location_label":"Display on booking page", + "display_location_info_badge":"Location will be visible before the booking is confirmed", "share_feedback": "Share feedback", "resources": "Resources", "support_documentation": "Support documentation", diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 6fb2adf0a5..f7801d67bd 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -16,5 +16,27 @@ export enum AppStoreLocationType { Teams = "integrations:office365_video", } +export type LocationObject = { + type: LocationType; + address?: string; + link?: string; + displayLocationPublicly?: boolean; + hostPhoneNumber?: string; +}; + export const LocationType = { ...DefaultLocationType, ...AppStoreLocationType }; export type LocationType = DefaultLocationType | AppStoreLocationType; + +export const locationHiddenFilter = (locations: LocationObject[]) => + locations.filter((el) => { + // Filter out locations that are not to be displayed publicly + const values = Object.values(AppStoreLocationType); + // Display if the location can be set to public - and also display all locations like google meet etc + if (el.displayLocationPublicly || values.includes(el["type"] as unknown as AppStoreLocationType)) + return el; + else { + delete el.address; + delete el.link; + return el; + } + }); diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 9148f54dbc..d926fdecb7 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -9,6 +9,7 @@ export const eventTypeLocations = z.array( type: z.nativeEnum(LocationType), address: z.string().optional(), link: z.string().url().optional(), + displayLocationPublicly: z.boolean().optional(), hostPhoneNumber: z.string().optional(), }) );