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()));