From f0b1767b3c3743baa8e295d68442a4a9e334d02d Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Sun, 13 Mar 2022 15:56:56 +0000 Subject: [PATCH] Link/In person location (#2104) --- apps/web/pages/event-types/[type].tsx | 63 +++++++++++++++++-- apps/web/public/static/locales/en/common.json | 4 +- packages/lib/location.ts | 1 + packages/prisma/zod-utils.ts | 6 +- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index 6b466b9b..fbbd1b80 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -1,4 +1,4 @@ -import { PhoneIcon, XIcon } from "@heroicons/react/outline"; +import { GlobeAltIcon, PhoneIcon, XIcon } from "@heroicons/react/outline"; import { ChevronRightIcon, ClockIcon, @@ -12,6 +12,7 @@ import { UserAddIcon, UsersIcon, } from "@heroicons/react/solid"; +import { zodResolver } from "@hookform/resolvers/zod"; import { MembershipRole } from "@prisma/client"; import { Availability, EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; @@ -26,6 +27,7 @@ import { Controller, useForm } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; import Select from "react-select"; import { JSONObject } from "superjson/dist/types"; +import { z } from "zod"; import { StripeData } from "@calcom/stripe/server"; import Switch from "@calcom/ui/Switch"; @@ -119,6 +121,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { const defaultLocations = [ { value: LocationType.InPerson, label: t("in_person_meeting") }, + { value: LocationType.Link, label: t("link_meeting") }, { value: LocationType.Jitsi, label: "Jitsi Meet" }, { value: LocationType.Phone, label: t("phone_call") }, ]; @@ -273,6 +276,32 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { </div> </div> ); + case LocationType.Link: + return ( + <div> + <label htmlFor="address" className="block text-sm font-medium text-gray-700"> + {t("set_link_meeting")} + </label> + <div className="mt-1"> + <input + type="text" + {...locationFormMethods.register("locationLink")} + id="address" + required + className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm" + defaultValue={ + formMethods.getValues("locations").find((location) => location.type === LocationType.Link) + ?.link + } + /> + {locationFormMethods.formState.errors.locationLink && ( + <p className="mt-1 text-red-500"> + {locationFormMethods.formState.errors.locationLink.message} + </p> + )} + </div> + </div> + ); case LocationType.Phone: return <p className="text-sm">{t("cal_invitee_phone_number_scheduling")}</p>; case LocationType.GoogleMeet: @@ -351,7 +380,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { schedulingType: SchedulingType | null; price: number; hidden: boolean; - locations: { type: LocationType; address?: string }[]; + locations: { type: LocationType; address?: string; link?: string }[]; customInputs: EventTypeCustomInput[]; users: string[]; availability: { @@ -381,11 +410,19 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { }, }); + const locationFormSchema = z.object({ + locationType: z.string(), + locationAddress: z.string().optional(), + locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field + }); + const locationFormMethods = useForm<{ locationType: LocationType; - locationAddress: 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:// + }>({ + resolver: zodResolver(locationFormSchema), + }); const Locations = () => { return ( <div className="w-full"> @@ -422,6 +459,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { /> </div> )} + {location.type === LocationType.Link && ( + <div className="flex flex-grow items-center"> + <GlobeAltIcon className="h-6 w-6" /> + <input + disabled + className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2" + value={location.link} + /> + </div> + )} {location.type === LocationType.Phone && ( <div className="flex flex-grow items-center"> <PhoneIcon className="h-6 w-6" /> @@ -690,10 +737,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { smartContractAddress, beforeBufferTime, afterBufferTime, + locations, ...input } = values; updateMutation.mutate({ ...input, + locations, availability: availabilityState, periodStartDate: periodDates.startDate, periodEndDate: periodDates.endDate, @@ -1525,6 +1574,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { details = { address: values.locationAddress }; } + if (newLocation === LocationType.Link) { + details = { link: values.locationLink }; + } const existingIdx = formMethods .getValues("locations") .findIndex((loc) => values.locationType === loc.type); @@ -1541,7 +1593,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => { formMethods.getValues("locations").concat({ type: values.locationType, ...details }) ); } - setShowLocationModal(false); }}> <Controller diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 6774c69f..64967e59 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -405,7 +405,8 @@ "share_additional_notes": "Please share anything that will help prepare for our meeting.", "booking_confirmation": "Confirm your {{eventTypeTitle}} with {{profileName}}", "booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}", - "in_person_meeting": "Link or In-person meeting", + "in_person_meeting": "In-person meeting", + "link_meeting":"Link meeting", "phone_call": "Phone call", "phone_number": "Phone Number", "enter_phone_number": "Enter phone number", @@ -574,6 +575,7 @@ "calendar_days": "calendar days", "business_days": "business days", "set_address_place": "Set an address or place", + "set_link_meeting": "Set a link to the meeting", "cal_invitee_phone_number_scheduling": "Cal will ask your invitee to enter a phone number before scheduling.", "cal_provide_google_meet_location": "Cal will provide a Google Meet location.", "cal_provide_zoom_meeting_url": "Cal will provide a Zoom meeting URL.", diff --git a/packages/lib/location.ts b/packages/lib/location.ts index 5401d888..0c296031 100644 --- a/packages/lib/location.ts +++ b/packages/lib/location.ts @@ -1,6 +1,7 @@ export enum LocationType { InPerson = "inPerson", Phone = "phone", + Link = "link", GoogleMeet = "integrations:google:meet", Zoom = "integrations:zoom", Daily = "integrations:daily", diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 7fb41fb1..293cc304 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -4,7 +4,11 @@ import { LocationType } from "@calcom/lib/location"; import { slugify } from "@calcom/lib/slugify"; export const eventTypeLocations = z.array( - z.object({ type: z.nativeEnum(LocationType), address: z.string().optional() }) + z.object({ + type: z.nativeEnum(LocationType), + address: z.string().optional(), + link: z.string().url().optional(), + }) ); export const eventTypeSlug = z.string().transform((val) => slugify(val.trim()));