AppStore CLI: Making video app creation a breeze with major cleanup of locations code throughout (#3825)

* Fix breadcrumb colors

* HorizontalTabs

* Team List Item WIP

* Horizontal Tabs

* Cards

* Remove team list item WIP

* Login Page

* Add welcome back i118n

* EventType page work

* Update EventType Icons

* WIP Availability

* Horizontal Tab Work

* Add build command for in root

* Update build DIr/command

* Add Edit Button + change buttons to v2

* Availablitiy page

* Fix IPAD

* Make mobile look a little nicer

* WIP bookingshell

* Remove list items from breaking build

* Add Embed ModalBox for routing forms

* Mian bulk of Booking Page.

* Few updates to components

* Fix chormatic feedback

* Add duplicate form support

* Fix duplication logic

* Change to feathericons everywhere and other fixes

* Dont allow routes for fallback route

* Fix banner

* Fix Empty Screen

* Text area + embded window fixes

* Semi fix avatar

* Fix all TS issues

* Fix tests

* Troubleshoot container + Active on count

* Support routing using query params

* Improve mobile

* NITS

* Fix padding on input

* Support multiselect in router endpoint

* Fix the issue where app goes in embed mode after viewing embed once

* Fix icons

* Add router url tests

* Add Responses download and form toggling tests

* Add required validation test

* Change Icons everywhere

* App typeform app

* Improvements in cli

* Starting to move event types settings to tabs

* Begin migration to single page form

* Single page tabs

* Limits Page

* Advanced tab

* Add RHF to dependancies

* Add typeform how-to-use page

* Add typeform how-to-use page and screenshots

* Most of advanced tab

* Solved RHF mismtach

* Build fixes

* RHF conditionals fixes

* Improved legibility

* Fix TS error

* Add missing image

* Update CliApp.tsx

* Major refactor/organisation into optional V2 UI

* Portal EditLocationModal

* Fix dialoug form

* Update imports

* Auto Animate + custom inputs WIP

* Custom Inputs

* WIP Apps

* Fixing stories imports

* Stripe app

* Remove duplicate dialog

* Remove duplicate dialog

* Major locations cleanup, 10s of bug fixes and app-store improvements

* Fix missing pieces

* More fixes

* Fix embed URL

* Fix app toggles + number of active apps

* Fix container padding on disabledBorder prop

* Removes strict

* more fixes

* EventType Team page WIP

* Fix embed

* NIT

* Add Darkmode gray color

* V2 Shell WIP

* Fix headings on shell V2

* Fix mobile layout with V2 shell

* V2 create event type button

* Checked Team Select

* Hidden to happen on save - not on toggle

* Team Attendee Select animation

* Fix scheduling type and remove multi select label

* Fix overflow on teams url

* Revert console

* Revert api

* Fix Embed TS errors

* Fix TS errors

* Fix Eslint errors

* Fix TS errors for UI

* Fix ESLINT error

* Fix TS errors

* Add missing import

* Fix CLI

* Add a default placeholder

* Remove hardcoded daily:integrations

* Fix message for payment page

* Revert api and console to main

* Update README

* Fix TS errors

* Fix Lint warnings

* Fix Tests

* Fix conflict issues

* Fix conflict issues

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
authored and GitHub committed 2022-08-25 18:48:50 -06:00
1 parent 87cce8eab6
commit 6c86317081
78 files changed
+1275 -2129

No files matched your search

@@ -0,0 +1,32 @@
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Props } from "./pages/AvailabilityPage";
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
return (
<div>
<div className="flex-warp mr-6 flex break-words text-sm text-gray-600 dark:text-white">
<p className="w-full">
{locations.map((location) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
return (
<span key={location.type} className="flex flex-row items-center pt-1">
<img
src={eventLocationType.iconUrl}
className="mr-[10px] ml-[2px] h-4 w-4"
alt={`${eventLocationType.label} icon`}
/>
<span key={location.type}>{locationKeyToString(location)} </span>
</span>
);
})}
</p>
</div>
</div>
);
}
@@ -3,6 +3,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useMutation } from "react-query";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -17,8 +18,6 @@ import { TextArea } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error";
import useMeQuery from "@lib/hooks/useMeQuery";
import { linkValueToString } from "@lib/linkValueToString";
import { LocationType } from "@lib/location";
import { extractRecurringDates } from "@lib/parseDate";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -182,17 +181,10 @@ function BookingListItem(booking: BookingItemProps) {
},
});
const saveLocation = (newLocationType: LocationType, details: { [key: string]: string }) => {
const saveLocation = (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => {
let newLocation = newLocationType as string;
if (
newLocationType === LocationType.InPerson ||
newLocationType === LocationType.Link ||
newLocationType === LocationType.UserPhone ||
newLocationType === LocationType.Riverside ||
newLocationType === LocationType.Around ||
newLocationType === LocationType.Whereby ||
newLocationType === LocationType.Ping
) {
const eventLocationType = getEventLocationType(newLocationType);
if (eventLocationType?.organizerInputType) {
newLocation = details[Object.keys(details)[0]];
}
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
@@ -215,17 +207,7 @@ function BookingListItem(booking: BookingItemProps) {
}
}
let location = booking.location || "";
if (location.includes("integration")) {
if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) {
location = t("web_conference");
} else if (isConfirmed) {
location = linkValueToString(booking.location, t);
} else {
location = t("web_conferencing_details_to_follow");
}
}
const location = booking.location || "";
const onClick = () => {
router.push({
@@ -8,7 +8,6 @@ import { useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { z } from "zod";
import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations";
import dayjs, { Dayjs } from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
@@ -46,41 +45,9 @@ import PoweredByCal from "@components/ui/PoweredByCal";
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
import { AvailableEventLocations } from "../AvailableEventLocations";
type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
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:
case LocationType.Ping:
case LocationType.Riverside:
case LocationType.Around:
case LocationType.Whereby:
return location.link || "Link"; // If disabled link won't exist on the object
case LocationType.Phone:
return t("your_number");
case LocationType.UserPhone:
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;
}
};
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
const router = useRouter();
@@ -457,40 +424,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
{t("requires_confirmation")}
</p>
)}
{eventType.locations.length === 1 && (
<p className="dark:text-darkgray-600 text-gray-600">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<Icon.FiVideo className="dark:text-darkgray-600 mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
) : (
<Icon.FiMapPin className="dark:text-darkgray-600 mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
)}
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
{eventType.locations.length > 1 && (
<div className="flex-warp dark:text-darkgray-600 flex text-gray-600">
<div className="mr-[10px] ml-[2px] -mt-1 ">
<Icon.FiMapPin className="dark:text-darkgray-600 inline-block h-4 w-4 text-gray-500" />
</div>
<p>
{eventType.locations.map((el, i, arr) => {
return (
<span key={el.type}>
{locationKeyToString(el, t)}{" "}
{arr.length - 1 !== i && (
<span className="font-light"> {t("or_lowercase")} </span>
)}
</span>
);
})}
</p>
</div>
)}
<p className="dark:text-darkgray-600 text-gray-600">
<Icon.FiClock className="dark:text-darkgray-600 mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
<AvailableEventLocations locations={eventType.locations} />
<p className="text-gray-600 dark:text-white">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
{eventType.length} {t("minutes")}
</p>
{eventType.price > 0 && (
@@ -590,40 +526,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
{t("requires_confirmation")}
</div>
)}
{eventType.locations.length === 1 && (
<p className="dark:text-darkgray-600 py-1 text-sm font-medium text-gray-600">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<Icon.FiVideo className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
) : (
<Icon.FiMapPin className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
)}
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
{eventType.locations.length > 1 && (
<div className="flex-warp dark:text-darkgray-600 flex items-center font-medium text-gray-600">
<div className="mr-[10px] ml-[2px] -mt-1 ">
<Icon.FiMapPin className="inline-block h-4 w-4 " />
</div>
<p className="dark:text-darkgray-600 py-1 text-sm font-medium ">
{eventType.locations.map((el, i, arr) => {
return (
<span key={el.type}>
{locationKeyToString(el, t)}{" "}
{arr.length - 1 !== i && (
<span className="text-sm font-light"> {t("or_lowercase")} </span>
)}
</span>
);
})}
</p>
</div>
)}
<p className="dark:text-darkgray-600 py-1 text-sm font-medium text-gray-600">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 " />
<AvailableEventLocations locations={eventType.locations} />
<p className="py-1 text-sm font-medium text-gray-600 dark:text-white">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
{eventType.length} {t("minutes")}
</p>
{!rescheduleUid && eventType.recurringEvent && (
@@ -13,7 +13,14 @@ import { useMutation } from "react-query";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import {
locationKeyToString,
getEventLocationValue,
getEventLocationType,
EventLocationType,
} from "@calcom/app-store/locations";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { LocationObject, LocationType } from "@calcom/core/location";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
@@ -37,7 +44,6 @@ import { EmailInput, Form } from "@calcom/ui/form/fields";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { ensureArray } from "@lib/ensureArray";
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";
@@ -58,15 +64,13 @@ declare global {
};
}
type BookingPageProps = (BookPageProps | TeamBookingPageProps | HashLinkPageProps) & {
locationLabels: Record<LocationType, string>;
};
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
type BookingFormValues = {
name: string;
email: string;
notes?: string;
locationType?: LocationType;
locationType?: EventLocationType["type"];
guests?: string[];
phone?: string;
hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers
@@ -83,7 +87,6 @@ const BookingPage = ({
profile,
isDynamicGroupBooking,
recurringEventCount,
locationLabels,
hasHashedBookingLink,
hashedLink,
}: BookingPageProps) => {
@@ -132,16 +135,6 @@ const BookingPage = ({
);
}
const location = (function humanReadableLocation(location) {
if (!location) {
return;
}
if (location.includes("integration")) {
return t("web_conferencing_details_to_follow");
}
return location;
})(responseData.location);
return router.push({
pathname: "/success",
query: {
@@ -152,7 +145,7 @@ const BookingPage = ({
reschedule: !!rescheduleUid,
name: attendees[0].name,
email: attendees[0].email,
location,
location: responseData.location,
eventName: profile.eventName || "",
bookingId: id,
isSuccessBookingPage: true,
@@ -213,7 +206,6 @@ const BookingPage = ({
}
}, [router.query.guest]);
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
const guestListEmails = !isDynamicGroupBooking
? booking?.attendees.slice(1).map((attendee) => attendee.email)
@@ -296,12 +288,12 @@ const BookingPage = ({
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
});
const selectedLocation = useWatch({
const selectedLocationType = useWatch({
control: bookingForm.control,
name: "locationType",
defaultValue: ((): LocationType | undefined => {
defaultValue: ((): EventLocationType["type"] | undefined => {
if (router.query.location) {
return router.query.location as LocationType;
return router.query.location as EventLocationType["type"];
}
if (locations.length === 1) {
return locations[0]?.type;
@@ -309,40 +301,13 @@ const BookingPage = ({
})(),
});
const getLocationValue = (
booking: Pick<BookingFormValues, "locationType" | "phone" | "hostPhoneNumber">
) => {
const { locationType } = booking;
switch (locationType) {
case LocationType.Phone: {
return booking.phone || "";
}
case LocationType.InPerson: {
return locationInfo(locationType)?.address || "";
}
case LocationType.Link: {
return locationInfo(locationType)?.link || "";
}
case LocationType.UserPhone: {
return locationInfo(locationType)?.hostPhoneNumber || "";
}
case LocationType.Around: {
return locationInfo(locationType)?.link || "";
}
case LocationType.Riverside: {
return locationInfo(locationType)?.link || "";
}
case LocationType.Whereby: {
return locationInfo(locationType)?.link || "";
}
case LocationType.Ping: {
return locationInfo(locationType)?.link || "";
}
// Catches all other location types, such as Google Meet, Zoom etc.
default:
return selectedLocation || "";
}
};
const selectedLocation = getEventLocationType(selectedLocationType);
const AttendeeInput =
selectedLocation?.attendeeInputType === "text"
? "input"
: selectedLocation?.attendeeInputType === "phone"
? PhoneInput
: null;
// Calculate the booking date(s)
let recurringStrings: string[] = [],
@@ -403,9 +368,10 @@ const BookingPage = ({
language: i18n.language,
rescheduleUid,
user: router.query.user,
location: getLocationValue(
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
),
location: getEventLocationValue(locations, {
type: booking.locationType ? booking.locationType : selectedLocationType || "",
phone: booking.phone,
}),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
@@ -414,7 +380,7 @@ const BookingPage = ({
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
}));
recurringMutation.mutate(recurringBookings);
} else {
@@ -430,9 +396,10 @@ const BookingPage = ({
rescheduleUid,
bookingUid: router.query.bookingUid as string,
user: router.query.user,
location: getLocationValue(
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
),
location: getEventLocationValue(locations, {
type: (booking.locationType ? booking.locationType : selectedLocationType) || "",
phone: booking.phone,
}),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
@@ -441,13 +408,14 @@ const BookingPage = ({
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
});
}
};
// Should be disabled when rescheduleUid is present and data was found in defaultUserValues name/email fields.
const disableInput = !!rescheduleUid && !!defaultUserValues.email && !!defaultUserValues.name;
const disableLocations = !!rescheduleUid;
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
const inputClassName =
"dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500";
@@ -656,21 +624,34 @@ const BookingPage = ({
<span className="block text-sm font-medium text-gray-700 dark:text-white">
{t("location")}
</span>
{locations.map((location, i) => (
<label key={i} className="block">
<input
type="radio"
className="location h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
{...bookingForm.register("locationType", { required: true })}
value={location.type}
defaultChecked={selectedLocation === location.type}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 ">{locationLabels[location.type]}</span>
</label>
))}
{locations.map((location, i) => {
const locationString = locationKeyToString(location);
// TODO: Right now selectedLocationType isn't send by getSSP. Once that's available defaultChecked should work and show the location in the original booking
const defaultChecked = rescheduleUid ? selectedLocationType === location.type : i === 0;
if (typeof locationString !== "string") {
// It's possible that location app got uninstalled
return null;
}
return (
<label key={i} className="block">
<input
type="radio"
disabled={!!disableLocations}
className="location h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
{...bookingForm.register("locationType", { required: true })}
value={location.type}
defaultChecked={defaultChecked}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-gray-500">
{locationKeyToString(location)}
</span>
</label>
);
})}
</div>
)}
{selectedLocation === LocationType.Phone && (
{/* TODO: Change name and id ="phone" to something generic */}
{AttendeeInput && (
<div className="mb-4">
<label
htmlFor="phone"
@@ -678,10 +659,10 @@ const BookingPage = ({
{t("phone_number")}
</label>
<div className="mt-1">
<PhoneInput<BookingFormValues>
<AttendeeInput<BookingFormValues>
control={bookingForm.control}
name="phone"
placeholder={t("enter_phone_number")}
placeholder={t(selectedLocation?.attendeeInputPlaceholder || "")}
id="phone"
required
disabled={disableInput}
@@ -828,7 +809,7 @@ const BookingPage = ({
)}
</div>
)}
{isSmsReminderNumberNeeded && selectedLocation !== LocationType.Phone && (
{isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
<div className="mb-4">
<label
htmlFor="smsReminderNumber"
+127 -350
View File
@@ -1,11 +1,18 @@
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { LocationOptionsToString } from "@calcom/app-store/locations";
import { LocationType } from "@calcom/core/location";
import {
LocationType,
getEventLocationType,
EventLocationType,
LocationObject,
} from "@calcom/app-store/locations";
import { getMessageForOrganizer } from "@calcom/app-store/locations";
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
@@ -15,7 +22,6 @@ import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { Form } from "@calcom/ui/form/fields";
import { QueryCell } from "@lib/QueryCell";
import { linkValueToString } from "@lib/linkValueToString";
import CheckboxField from "@components/ui/form/CheckboxField";
import Select from "@components/ui/form/Select";
@@ -24,33 +30,46 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
type LocationFormValues = {
locationType: LocationType;
locationAddress?: string;
locationLink?: string;
locationPhoneNumber?: string;
displayLocationPublicly?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
booking?: BookingItem;
defaultValues?: {
type: LocationType;
address?: string | undefined;
link?: string | undefined;
hostPhoneNumber?: string | undefined;
displayLocationPublicly?: boolean | undefined;
}[];
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
}
const LocationInput = (props: {
eventLocationType: EventLocationType;
locationFormMethods: ReturnType<typeof useForm>;
id: string;
required: boolean;
placeholder: string;
className?: string;
defaultValue?: string;
}): JSX.Element | null => {
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
<PhoneInput
name={eventLocationType.variable}
control={locationFormMethods.control}
{...remainingProps}
/>
);
}
return null;
};
export const EditLocationDialog = (props: ISetLocationDialog) => {
const {
saveLocation,
@@ -73,37 +92,52 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
const locationFormSchema = z.object({
locationType: z.string(),
phone: z.string().optional().nullable(),
locationAddress: z.string().optional(),
locationLink:
selection?.value === LocationType.Whereby
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?whereby.com\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Around
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?around.co\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Ping
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?ping.gg\/call\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Riverside
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?riverside.fm\/studio\/[a-zA-Z0-9]*/)
.optional()
: z.string().url().optional(),
locationLink: z
.string()
.optional()
.superRefine((val, ctx) => {
if (
eventLocationType &&
!eventLocationType.default &&
eventLocationType.linkType === "static" &&
eventLocationType.urlRegExp
) {
const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(val).success;
if (!valid) {
const sampleUrl = eventLocationType.organizerInputPlaceholder;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL for ${eventLocationType.label}. ${
sampleUrl ? "Sample URL: " + sampleUrl : ""
}`,
});
}
return;
}
const valid = z.string().url().optional().safeParse(val).success;
if (!valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL`,
});
}
return;
}),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.nullable()
.refine((val) => {
if (val === null) return false;
return isValidPhoneNumber(val);
})
.optional(),
});
const locationFormMethods = useForm<LocationFormValues>({
const locationFormMethods = useForm({
mode: "onSubmit",
resolver: zodResolver(locationFormSchema),
});
@@ -113,28 +147,43 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
name: "locationType",
});
const LocationOptions =
selectedLocation === LocationType.InPerson ? (
<>
const eventLocationType = getEventLocationType(selectedLocation);
const defaultLocation = defaultValues?.find(
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
);
const LocationOptions = (() => {
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
if (!eventLocationType.variable) {
console.error("eventLocationType.variable can't be undefined");
return null;
}
return (
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_address_place")}
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
{t(eventLocationType.messageForOrganizer || "")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationAddress")}
id="address"
<LocationInput
locationFormMethods={locationFormMethods}
eventLocationType={eventLocationType}
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-sm border-gray-300 text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.InPerson
)?.address
: undefined
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
}
/>
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
as="p"
/>
</div>
{!booking && (
<div className="mt-3">
@@ -143,12 +192,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.InPerson)
?.displayLocationPublicly
: undefined
}
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
@@ -160,286 +204,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Link ? (
<div>
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
{t("set_link_meeting")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
required
id="link"
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Link
)?.link
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
description={t("display_location_label")}
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Link)
?.displayLocationPublicly
: undefined
}
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
) : selectedLocation === LocationType.UserPhone ? (
<div>
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
{t("set_your_phone_number")}
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
</label>
<div className="mt-1">
<PhoneInput<LocationFormValues>
control={locationFormMethods.control}
name="locationPhoneNumber"
required
id="locationPhoneNumber"
placeholder={t("host_phone_number")}
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.UserPhone
)?.hostPhoneNumber
: undefined
}
/>
{locationFormMethods.formState.errors.locationPhoneNumber && (
<p className="mt-1 text-sm text-red-500">{t("invalid_number")}</p>
)}
</div>
</div>
) : selectedLocation === LocationType.Whereby ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_whereby_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="wherebylink"
placeholder="https://www.whereby.com/cal"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Whereby
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_whereby_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Whereby)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Around ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_around_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.around.co/rick"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Around
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_around_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Around)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Ping ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_ping_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="pinglink"
placeholder="https://www.ping.gg/call/theo"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Ping
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_ping_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Ping)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Riverside ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_riverside_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.riverside.fm/studio/rick"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Riverside
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_riverside_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Riverside)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : (
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
);
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
return (
<Dialog open={isOpenDialog}>
@@ -462,7 +231,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">{linkValueToString(booking.location, t)}</p>
<p className="mb-2 ml-1 text-sm text-black">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
)}
<Form
@@ -474,23 +245,29 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
displayLocationPublicly,
};
}
const eventLocationType = getEventLocationType(newLocation);
// TODO: There can be a property that tells if it is to be saved in `link`
if (
newLocation === LocationType.Link ||
newLocation === LocationType.Whereby ||
newLocation === LocationType.Around ||
newLocation === LocationType.Riverside ||
newLocation === LocationType.Ping
(!eventLocationType?.default && eventLocationType?.linkType === "static")
) {
details = { link: values.locationLink, displayLocationPublicly };
details = { link: values.locationLink };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
if (eventLocationType?.organizerInputType) {
details = {
...details,
displayLocationPublicly,
};
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);
@@ -1,11 +1,18 @@
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { LocationOptionsToString } from "@calcom/app-store/locations";
import { LocationType } from "@calcom/core/location";
import {
LocationType,
getEventLocationType,
EventLocationType,
getHumanReadableLocationValue,
getMessageForOrganizer,
LocationObject,
} from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
@@ -15,7 +22,6 @@ import { Button } from "@calcom/ui/v2";
import { Form } from "@calcom/ui/v2";
import { QueryCell } from "@lib/QueryCell";
import { linkValueToString } from "@lib/linkValueToString";
import CheckboxField from "@components/ui/form/CheckboxField";
import Select from "@components/ui/form/Select";
@@ -24,33 +30,46 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
type LocationFormValues = {
locationType: LocationType;
locationAddress?: string;
locationLink?: string;
locationPhoneNumber?: string;
displayLocationPublicly?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
booking?: BookingItem;
defaultValues?: {
type: LocationType;
address?: string | undefined;
link?: string | undefined;
hostPhoneNumber?: string | undefined;
displayLocationPublicly?: boolean | undefined;
}[];
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
}
const LocationInput = (props: {
eventLocationType: EventLocationType;
locationFormMethods: ReturnType<typeof useForm>;
id: string;
required: boolean;
placeholder: string;
className?: string;
defaultValue?: string;
}): JSX.Element | null => {
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
<PhoneInput
name={eventLocationType.variable}
control={locationFormMethods.control}
{...remainingProps}
/>
);
}
return null;
};
export const EditLocationDialog = (props: ISetLocationDialog) => {
const {
saveLocation,
@@ -73,29 +92,43 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
const locationFormSchema = z.object({
locationType: z.string(),
phone: z.string().optional(),
locationAddress: z.string().optional(),
locationLink:
selection?.value === LocationType.Whereby
? z
locationLink: z
.string()
.optional()
.superRefine((val, ctx) => {
if (
eventLocationType &&
!eventLocationType.default &&
eventLocationType.linkType === "static" &&
eventLocationType.urlRegExp
) {
const valid = z
.string()
.regex(/^http(s)?:\/\/(www\.)?whereby.com\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Around
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?around.co\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Ping
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?ping.gg\/call\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Riverside
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?riverside.fm\/studio\/[a-zA-Z0-9]*/)
.optional()
: z.string().url().optional(),
.regex(new RegExp(eventLocationType.urlRegExp || ""))
.safeParse(val).success;
if (!valid) {
const sampleUrl = eventLocationType.organizerInputPlaceholder;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL for ${eventLocationType.label}. ${
sampleUrl ? "Sample URL: " + sampleUrl : ""
}`,
});
}
return;
}
const valid = z.string().url().optional().safeParse(val).success;
if (!valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL`,
});
}
return;
}),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
@@ -103,7 +136,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
.optional(),
});
const locationFormMethods = useForm<LocationFormValues>({
const locationFormMethods = useForm({
mode: "onSubmit",
resolver: zodResolver(locationFormSchema),
});
@@ -113,28 +146,43 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
name: "locationType",
});
const LocationOptions =
selectedLocation === LocationType.InPerson ? (
<>
const eventLocationType = getEventLocationType(selectedLocation);
const defaultLocation = defaultValues?.find(
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
);
const LocationOptions = (() => {
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
if (!eventLocationType.variable) {
console.error("eventLocationType.variable can't be undefined");
return null;
}
return (
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_address_place")}
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
{t(eventLocationType.messageForOrganizer || "")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationAddress")}
id="address"
<LocationInput
locationFormMethods={locationFormMethods}
eventLocationType={eventLocationType}
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-md border-gray-300 text-sm"
className="block w-full rounded-sm border-gray-300 text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.InPerson
)?.address
: undefined
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
}
/>
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
as="p"
/>
</div>
{!booking && (
<div className="mt-3">
@@ -143,12 +191,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.InPerson)
?.displayLocationPublicly
: undefined
}
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
@@ -160,274 +203,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Link ? (
<div>
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
{t("set_link_meeting")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
required
id="link"
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Link
)?.link
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
description={t("display_location_label")}
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Link)
?.displayLocationPublicly
: undefined
}
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
) : selectedLocation === LocationType.UserPhone ? (
<div>
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
{t("set_your_phone_number")}
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
</label>
<div className="mt-1">
<PhoneInput<LocationFormValues>
control={locationFormMethods.control}
name="locationPhoneNumber"
required
id="locationPhoneNumber"
placeholder={t("host_phone_number")}
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.UserPhone
)?.hostPhoneNumber
: undefined
}
/>
{locationFormMethods.formState.errors.locationPhoneNumber && (
<p className="mt-1 text-sm text-red-500">{t("invalid_number")}</p>
)}
</div>
</div>
) : selectedLocation === LocationType.Whereby ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_whereby_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="wherebylink"
placeholder="https://www.whereby.com/cal"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Whereby
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Whereby)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Around ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_around_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.around.co/rick"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Around
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Around)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Ping ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_ping_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="pinglink"
placeholder="https://www.ping.gg/call/theo"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Ping
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Ping)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Riverside ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_riverside_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.riverside.fm/studio/rick"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Riverside
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Riverside)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : (
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
);
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
return (
<Dialog open={isOpenDialog}>
@@ -450,7 +230,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">{linkValueToString(booking.location, t)}</p>
<p className="mb-2 ml-1 text-sm text-black">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
)}
<Form
@@ -462,23 +244,29 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
displayLocationPublicly,
};
}
const eventLocationType = getEventLocationType(newLocation);
// TODO: There can be a property that tells if it is to be saved in `link`
if (
newLocation === LocationType.Link ||
newLocation === LocationType.Whereby ||
newLocation === LocationType.Around ||
newLocation === LocationType.Riverside ||
newLocation === LocationType.Ping
(!eventLocationType?.default && eventLocationType?.linkType === "static")
) {
details = { link: values.locationLink, displayLocationPublicly };
details = { link: values.locationLink };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
if (eventLocationType?.organizerInputType) {
details = {
...details,
displayLocationPublicly,
};
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);
@@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from "react";
import { Controller, useForm, useFormContext, useWatch } from "react-hook-form";
import { z } from "zod";
import { getEventLocationType, EventLocationType } from "@calcom/app-store/locations";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -15,7 +16,6 @@ import { Select, Label, TextField, Portal, Switch } from "@calcom/ui/v2";
import * as RadioArea from "@calcom/ui/v2/core/form/radio-area";
import { asStringOrUndefined } from "@lib/asStringOrNull";
import { LocationType } from "@lib/location";
import { slugify } from "@lib/slugify";
import CheckedSelect from "@components/ui/form/CheckedSelect";
@@ -23,7 +23,7 @@ import { EditLocationDialog } from "@components/v2/eventtype/EditLocationDialog"
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
@@ -37,7 +37,7 @@ export const EventSetupTab = (
const [showLocationModal, setShowLocationModal] = useState(false);
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const openLocationModal = (type: LocationType) => {
const openLocationModal = (type: EventLocationType["type"]) => {
setSelectedLocation(locationOptions.find((option) => option.value === type));
setShowLocationModal(true);
};
@@ -61,7 +61,7 @@ export const EventSetupTab = (
);
};
const addLocation = (newLocationType: LocationType, details = {}) => {
const addLocation = (newLocationType: EventLocationType["type"], details = {}) => {
const existingIdx = formMethods.getValues("locations").findIndex((loc) => newLocationType === loc.type);
if (existingIdx !== -1) {
const copy = formMethods.getValues("locations");
@@ -122,7 +122,7 @@ export const EventSetupTab = (
});
const locationFormMethods = useForm<{
locationType: LocationType;
locationType: EventLocationType["type"];
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://
@@ -139,9 +139,18 @@ export const EventSetupTab = (
animationRef.current && autoAnimate(animationRef.current);
}, [animationRef]);
const validLocations = formMethods.getValues("locations").filter((location) => {
const eventLocation = getEventLocationType(location.type);
if (!eventLocation) {
// It's possible that the location app in use got uninstalled.
return false;
}
return true;
});
return (
<div className="w-full">
{formMethods.getValues("locations").length === 0 && (
{validLocations.length === 0 && (
<div className="flex">
<Select
options={locationOptions}
@@ -149,16 +158,13 @@ export const EventSetupTab = (
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
onChange={(e) => {
if (e?.value) {
const newLocationType: LocationType = e.value;
const newLocationType: EventLocationType["type"] = e.value;
const eventLocationType = getEventLocationType(newLocationType);
if (!eventLocationType) {
return;
}
locationFormMethods.setValue("locationType", newLocationType);
if (
newLocationType === LocationType.InPerson ||
newLocationType === LocationType.Link ||
newLocationType === LocationType.UserPhone ||
newLocationType === LocationType.Whereby ||
newLocationType === LocationType.Around ||
newLocationType === LocationType.Riverside
) {
if (eventLocationType.organizerInputType) {
openLocationModal(newLocationType);
} else {
addLocation(newLocationType);
@@ -168,339 +174,57 @@ export const EventSetupTab = (
/>
</div>
)}
{formMethods.getValues("locations").length > 0 && (
{validLocations.length > 0 && (
<ul ref={animationRef}>
{formMethods.getValues("locations").map((location) => (
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 p-[10px]">
<div className="flex justify-between">
{location.type === LocationType.InPerson && (
<div className="flex flex-grow items-center">
<Icon.FiMapPin className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.address}
</span>
</div>
)}
{location.type === LocationType.Link && (
<div className="flex flex-grow items-center">
<Icon.FiGlobe className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.link}
</span>
</div>
)}
{location.type === LocationType.UserPhone && (
<div className="flex flex-grow items-center">
<Icon.FiPhone className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.hostPhoneNumber}
</span>
</div>
)}
{location.type === LocationType.Phone && (
<div className="flex flex-grow items-center">
<Icon.FiPhone className="h-6 w-6" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{t("phone_call")}</span>
</div>
)}
{location.type === LocationType.Whereby && (
<div className="flex flex-grow items-center">
<img src="/api/app-store/whereby/icon.svg" className="h-6 w-6" alt="whereby logo" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{location.link}</span>
</div>
)}
{location.type === LocationType.Around && (
<div className="flex flex-grow items-center">
<img src="/api/app-store/around/icon.svg" className="h-6 w-6" alt="whereby logo" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{location.link}</span>
</div>
)}
{location.type === LocationType.Riverside && (
<div className="flex flex-grow items-center">
<img src="/api/app-store/riverside/icon.svg" className="h-6 w-6" alt="whereby logo" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{location.link}</span>
</div>
)}
{location.type === LocationType.GoogleMeet && (
<div className="flex flex-grow items-center">
<svg
{validLocations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
return null;
}
return (
<li key={location.type} className="mb-2 rounded-sm border border-neutral-300 py-1.5 px-2">
<div className="flex justify-between">
<div key={index} className="flex flex-grow items-center">
<img
src={eventLocationType.iconUrl}
className="h-6 w-6"
viewBox="0 0 64 54"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M16 0V16H0" fill="#EA4335" />
<path
d="M16 0V16H37.3333V27.0222L53.3333 14.0444V5.33332C53.3333 1.77777 51.5555 0 47.9999 0"
fill="#FFBA00"
/>
<path
d="M15.6438 53.3341V37.3341H37.3326V26.6675L53.3326 39.2897V48.0008C53.3326 51.5563 51.5548 53.3341 47.9993 53.3341"
fill="#00AC47"
/>
<path d="M37.3335 26.6662L53.3335 13.6885V39.644" fill="#00832D" />
<path
d="M53.3335 13.6892L60.8001 7.64481C62.4001 6.40037 64.0001 6.40037 64.0001 8.88925V44.4447C64.0001 46.9336 62.4001 46.9336 60.8001 45.6892L53.3335 39.6447"
fill="#00AC47"
/>
<path
d="M0 36.9785V48.0007C0 51.5563 1.77777 53.334 5.33332 53.334H16V36.9785"
fill="#0066DA"
/>
<path d="M0 16H16V37.3333H0" fill="#2684FC" />
</svg>
<span className="text-sm ltr:ml-2 rtl:mr-2">Google Meet</span>
alt={`${eventLocationType.label} logo`}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2">{eventLocationType.label}</span>
</div>
)}
{location.type === LocationType.Huddle01 && (
<div className="flex flex-grow items-center">
<svg
width="1.25em"
height="1.25em"
viewBox="0 0 26 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M14.8607 0H4.04353C3.16693 0 2.32622 0.347292 1.70636 0.965476C1.08651 1.58366 0.738281 2.4221 0.738281 3.29634V14.0844C0.738281 14.9586 1.08651 15.7971 1.70636 16.4152C2.32622 17.0334 3.16693 17.3807 4.04353 17.3807H14.8607C15.7373 17.3807 16.578 17.0334 17.1979 16.4152C17.8177 15.7971 18.166 14.9586 18.166 14.0844V3.29634C18.166 2.4221 17.8177 1.58366 17.1979 0.965476C16.578 0.347292 15.7373 0 14.8607 0V0Z"
fill="#246BFD"
/>
<path
d="M24.1641 3.10754C24.0122 3.14004 23.8679 3.20106 23.7389 3.28734L21.1623 4.85161C20.7585 5.09889 20.4269 5.44766 20.2008 5.86299C19.9686 6.28713 19.8472 6.76272 19.8477 7.24595V10.1407C19.8475 10.6251 19.9694 11.1017 20.2023 11.5267C20.4295 11.9431 20.7627 12.2925 21.1683 12.5396L23.7645 14.1038C23.9325 14.2074 24.1202 14.2753 24.3158 14.3031C24.5103 14.3302 24.7084 14.3164 24.8973 14.2627C25.0881 14.2077 25.2659 14.1149 25.4201 13.99C25.5764 13.862 25.706 13.7047 25.8017 13.527C25.9321 13.2836 26.0003 13.0118 26 12.7359V4.62985C25.9995 4.39497 25.9483 4.16296 25.8498 3.94961C25.7523 3.73989 25.6097 3.55418 25.4321 3.40571C25.258 3.26046 25.0522 3.15784 24.8311 3.10604C24.6118 3.05359 24.3832 3.0541 24.1641 3.10754Z"
fill="#246BFD"
/>
<path
d="M7.07325 14.3165C6.26596 14.3165 5.64849 14.0822 5.22081 13.6138C4.79313 13.1453 4.57928 12.484 4.57928 11.63V6.0112C4.57928 5.15515 4.79313 4.49338 5.22081 4.0259C5.64849 3.55842 6.26596 3.32418 7.07325 3.32318C7.87452 3.32318 8.4915 3.55742 8.92419 4.0259C9.35687 4.49438 9.57071 5.15615 9.5657 6.0112V11.63C9.5657 12.484 9.35186 13.1453 8.92419 13.6138C8.49651 14.0822 7.87953 14.3165 7.07325 14.3165ZM7.07325 12.7897C7.63914 12.7897 7.92259 12.4401 7.9236 11.7408V5.90332C7.9236 5.20409 7.64015 4.85448 7.07325 4.85448C6.50635 4.85448 6.2224 5.20409 6.2214 5.90332V11.7363C6.2214 12.4396 6.50534 12.7907 7.07325 12.7897Z"
fill="white"
/>
<path
d="M12.6791 6.0112H10.9619V4.82002C11.3388 4.83087 11.7155 4.78952 12.0811 4.69716C12.3452 4.63341 12.5856 4.49564 12.7737 4.3001C12.9727 4.05484 13.1254 3.77563 13.2244 3.47601H14.3287V14.1637H12.6791V6.0112Z"
fill="white"
/>
</svg>
<span className="ml-2 text-sm">Huddle01 Web3 Video</span>
<div className="flex">
<button
type="button"
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
openLocationModal(location.type);
}}
aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
<Icon.FiEdit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
</button>
</div>
)}
{location.type === LocationType.Daily && (
<div className="flex flex-grow items-center">
<svg
id="svg"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="1.25em"
height="1.25em"
viewBox="0, 0, 400,400">
<g id="svgg">
<path
id="path0"
d="M100.400 142.062 C 99.630 142.280,98.394 143.076,97.654 143.830 C 96.914 144.583,95.997 145.200,95.616 145.200 C 94.776 145.200,93.802 146.248,93.389 147.598 C 93.221 148.147,92.560 149.054,91.919 149.613 C 90.024 151.267,90.020 151.390,90.010 199.645 C 89.999 248.545,90.014 248.945,91.940 250.744 C 92.571 251.334,93.229 252.262,93.401 252.808 C 93.751 253.916,95.054 255.200,95.829 255.200 C 96.107 255.200,96.710 255.808,97.169 256.550 C 98.373 258.498,94.832 258.400,164.273 258.400 C 231.741 258.400,231.099 258.418,231.949 256.552 C 232.208 255.983,233.149 255.250,234.197 254.801 C 235.357 254.304,236.005 253.774,236.014 253.314 C 236.021 252.921,236.375 251.880,236.800 251.000 C 237.225 250.120,237.579 249.119,237.586 248.776 C 237.594 248.434,237.864 247.804,238.187 247.376 C 238.696 246.704,238.776 240.392,238.787 200.426 C 238.801 149.852,238.967 154.051,236.799 149.949 C 236.610 149.591,236.332 148.647,236.183 147.850 C 235.956 146.640,235.591 146.227,233.964 145.342 C 232.893 144.759,231.907 143.938,231.774 143.518 C 231.641 143.098,231.052 142.539,230.466 142.277 C 229.079 141.657,102.567 141.447,100.400 142.062 "
stroke="none"
fill="#f9f9f9"
fillRule="evenodd"
/>
<path
id="path1"
d="M304.600 153.562 C 304.160 153.717,302.589 154.419,301.109 155.122 C 299.629 155.825,298.171 156.400,297.869 156.400 C 297.567 156.400,296.528 156.977,295.560 157.682 C 294.592 158.387,292.872 159.272,291.739 159.649 C 290.605 160.025,288.743 160.976,287.602 161.761 C 286.460 162.547,284.778 163.386,283.863 163.628 C 282.948 163.869,281.300 164.672,280.200 165.413 C 279.100 166.154,277.660 166.885,277.000 167.037 C 275.491 167.385,272.800 168.718,272.800 169.117 C 272.800 169.485,270.749 170.506,268.629 171.194 C 266.207 171.979,263.730 174.650,263.412 176.820 C 262.921 180.167,263.353 224.092,263.889 225.295 C 264.635 226.970,266.755 228.668,269.300 229.629 C 270.565 230.107,271.600 230.622,271.600 230.775 C 271.600 231.219,274.452 232.687,276.241 233.162 C 277.144 233.403,278.381 234.061,278.991 234.626 C 279.600 235.191,281.382 236.125,282.950 236.701 C 284.517 237.278,286.430 238.236,287.200 238.831 C 287.970 239.426,289.320 240.126,290.200 240.387 C 292.160 240.967,294.400 242.079,294.400 242.472 C 294.400 242.837,297.518 244.231,299.125 244.584 C 299.790 244.730,300.737 245.198,301.228 245.625 C 301.720 246.051,302.620 246.400,303.228 246.400 C 303.837 246.400,304.605 246.504,304.936 246.631 C 305.267 246.758,305.902 246.498,306.348 246.052 C 306.793 245.607,307.721 244.951,308.410 244.595 C 310.905 243.305,310.800 245.287,310.800 199.575 C 310.800 155.897,310.789 155.600,309.169 155.600 C 309.026 155.600,308.231 155.060,307.400 154.400 C 306.569 153.740,305.780 153.218,305.645 153.240 C 305.510 153.262,305.040 153.407,304.600 153.562 "
stroke="none"
fill="#1be7b8"
fillRule="evenodd"
/>
<path
id="path2"
d="M104.148 137.776 C 103.459 138.076,102.774 138.519,102.624 138.760 C 102.475 139.002,101.832 139.200,101.196 139.200 C 98.679 139.200,95.594 140.337,94.191 141.782 C 93.434 142.562,92.630 143.200,92.406 143.200 C 92.181 143.200,91.703 143.875,91.344 144.700 C 90.984 145.525,90.140 146.560,89.467 147.000 C 87.556 148.251,87.579 147.532,87.693 201.219 L 87.800 252.069 88.800 252.944 C 89.350 253.425,90.311 254.498,90.935 255.328 C 91.559 256.159,92.682 257.235,93.430 257.719 C 94.178 258.204,94.792 258.829,94.795 259.110 C 94.801 259.708,96.289 260.360,98.770 260.851 C 99.743 261.044,100.887 261.516,101.311 261.901 C 102.535 263.008,223.251 262.983,224.942 261.875 C 225.616 261.433,227.174 261.056,228.925 260.910 C 232.411 260.620,234.281 259.898,234.866 258.616 C 235.107 258.087,235.812 257.444,236.432 257.187 C 237.635 256.688,238.800 255.226,238.800 254.214 C 238.800 253.876,239.039 253.600,239.330 253.600 C 239.622 253.600,240.297 253.135,240.830 252.568 L 241.800 251.536 241.800 200.335 L 241.800 149.134 240.400 147.884 C 239.630 147.197,238.690 145.944,238.312 145.101 C 237.852 144.075,237.232 143.430,236.441 143.154 C 235.696 142.895,235.110 142.318,234.859 141.598 C 234.411 140.311,233.008 139.763,229.068 139.333 C 227.786 139.194,226.522 138.865,226.260 138.603 C 224.854 137.196,225.002 137.200,164.726 137.216 C 115.566 137.229,105.185 137.325,104.148 137.776 M230.299 140.581 C 231.013 140.751,232.363 141.600,233.299 142.466 C 234.235 143.333,235.488 144.338,236.085 144.699 C 236.684 145.061,237.282 145.862,237.419 146.487 C 237.556 147.110,238.076 148.110,238.574 148.710 C 240.721 151.291,240.592 148.280,240.713 198.600 C 240.829 246.814,240.750 249.650,239.248 251.152 C 238.800 251.600,238.071 252.676,237.629 253.543 C 237.187 254.410,236.187 255.514,235.407 255.995 C 234.628 256.477,233.798 257.231,233.563 257.670 C 232.125 260.355,229.256 260.458,160.200 260.300 C 96.040 260.154,98.009 260.223,96.185 258.055 C 95.663 257.435,94.598 256.495,93.818 255.964 C 93.037 255.434,92.310 254.730,92.202 254.400 C 92.094 254.070,91.396 253.117,90.652 252.283 C 88.728 250.126,88.809 252.440,88.804 199.526 C 88.800 148.835,88.746 150.246,90.767 148.075 C 91.445 147.347,92.000 146.583,92.000 146.379 C 92.000 145.965,94.367 143.600,94.781 143.600 C 94.926 143.600,95.721 142.979,96.550 142.220 C 97.645 141.217,98.567 140.772,99.928 140.589 C 100.958 140.450,101.980 140.273,102.200 140.195 C 103.020 139.904,229.052 140.284,230.299 140.581 M302.261 151.784 C 301.415 152.085,300.477 152.683,300.177 153.111 C 299.589 153.951,298.498 154.440,295.467 155.223 C 294.179 155.556,293.257 156.096,292.706 156.841 C 292.120 157.635,291.307 158.082,289.909 158.382 C 287.523 158.894,286.569 159.361,285.000 160.786 C 284.254 161.463,282.944 162.058,281.536 162.358 C 279.852 162.717,278.929 163.194,277.936 164.216 C 277.201 164.973,276.327 165.593,275.994 165.596 C 274.726 165.605,271.323 167.114,270.329 168.107 C 269.759 168.678,268.506 169.354,267.546 169.609 C 263.906 170.578,262.647 172.127,261.546 176.994 C 260.707 180.702,260.406 219.312,261.200 221.401 C 261.420 221.979,261.860 223.699,262.178 225.222 C 262.801 228.210,263.915 229.763,265.769 230.228 C 266.340 230.371,266.906 230.649,267.027 230.844 C 267.148 231.040,267.598 231.200,268.028 231.200 C 268.457 231.200,269.121 231.575,269.504 232.034 C 270.324 233.017,272.827 234.231,274.800 234.604 C 275.626 234.760,276.610 235.349,277.200 236.040 C 277.950 236.919,278.976 237.422,281.300 238.052 C 283.242 238.578,284.400 239.096,284.400 239.438 C 284.400 240.158,287.095 241.510,289.201 241.847 C 290.693 242.085,292.400 243.256,292.400 244.041 C 292.400 244.329,297.174 246.000,297.997 246.000 C 298.233 246.000,299.057 246.630,299.827 247.400 C 301.156 248.729,301.366 248.800,303.981 248.800 L 306.736 248.800 309.338 246.578 C 312.714 243.696,312.469 247.711,312.322 197.737 L 312.200 156.074 310.962 154.537 C 308.533 151.521,305.601 150.593,302.261 151.784 M307.400 154.400 C 308.231 155.060,309.026 155.600,309.169 155.600 C 310.789 155.600,310.800 155.897,310.800 199.575 C 310.800 245.287,310.905 243.305,308.410 244.595 C 307.721 244.951,306.793 245.607,306.348 246.052 C 305.902 246.498,305.267 246.758,304.936 246.631 C 304.605 246.504,303.837 246.400,303.228 246.400 C 302.620 246.400,301.720 246.051,301.228 245.625 C 300.737 245.198,299.790 244.730,299.125 244.584 C 297.518 244.231,294.400 242.837,294.400 242.472 C 294.400 242.079,292.160 240.967,290.200 240.387 C 289.320 240.126,287.970 239.426,287.200 238.831 C 286.430 238.236,284.517 237.278,282.950 236.701 C 281.382 236.125,279.600 235.191,278.991 234.626 C 278.381 234.061,277.144 233.403,276.241 233.162 C 274.452 232.687,271.600 231.219,271.600 230.775 C 271.600 230.622,270.565 230.107,269.300 229.629 C 266.755 228.668,264.635 226.970,263.889 225.295 C 263.353 224.092,262.921 180.167,263.412 176.820 C 263.730 174.650,266.207 171.979,268.629 171.194 C 270.749 170.506,272.800 169.485,272.80Line truncated
stroke="none"
fill="#4c545c"
fillRule="evenodd"
/>
<path
id="path3"
d="M102.200 140.195 C 101.980 140.273,100.958 140.450,99.928 140.589 C 98.567 140.772,97.645 141.217,96.550 142.220 C 95.721 142.979,94.926 143.600,94.781 143.600 C 94.367 143.600,92.000 145.965,92.000 146.379 C 92.000 146.583,91.445 147.347,90.767 148.075 C 88.746 150.246,88.800 148.835,88.804 199.526 C 88.809 252.440,88.728 250.126,90.652 252.283 C 91.396 253.117,92.094 254.070,92.202 254.400 C 92.310 254.730,93.037 255.434,93.818 255.964 C 94.598 256.495,95.663 257.435,96.185 258.055 C 98.009 260.223,96.040 260.154,160.200 260.300 C 229.256 260.458,232.125 260.355,233.563 257.670 C 233.798 257.231,234.628 256.477,235.407 255.995 C 236.187 255.514,237.187 254.410,237.629 253.543 C 238.071 252.676,238.800 251.600,239.248 251.152 C 240.750 249.650,240.829 246.814,240.713 198.600 C 240.592 148.280,240.721 151.291,238.574 148.710 C 238.076 148.110,237.556 147.110,237.419 146.487 C 237.282 145.862,236.684 145.061,236.085 144.699 C 235.488 144.338,234.235 143.333,233.299 142.466 C 232.363 141.600,231.013 140.751,230.299 140.581 C 229.052 140.284,103.020 139.904,102.200 140.195 M230.466 142.277 C 231.052 142.539,231.641 143.098,231.774 143.518 C 231.907 143.938,232.893 144.759,233.964 145.342 C 235.591 146.227,235.956 146.640,236.183 147.850 C 236.332 148.647,236.610 149.591,236.799 149.949 C 238.967 154.051,238.801 149.852,238.787 200.426 C 238.776 240.392,238.696 246.704,238.187 247.376 C 237.864 247.804,237.594 248.434,237.586 248.776 C 237.579 249.119,237.225 250.120,236.800 251.000 C 236.375 251.880,236.021 252.921,236.014 253.314 C 236.005 253.774,235.357 254.304,234.197 254.801 C 233.149 255.250,232.208 255.983,231.949 256.552 C 231.099 258.418,231.741 258.400,164.273 258.400 C 94.832 258.400,98.373 258.498,97.169 256.550 C 96.710 255.808,96.107 255.200,95.829 255.200 C 95.054 255.200,93.751 253.916,93.401 252.808 C 93.229 252.262,92.571 251.334,91.940 250.744 C 90.014 248.945,89.999 248.545,90.010 199.645 C 90.020 151.390,90.024 151.267,91.919 149.613 C 92.560 149.054,93.221 148.147,93.389 147.598 C 93.802 146.248,94.776 145.200,95.616 145.200 C 95.997 145.200,96.914 144.583,97.654 143.830 C 98.394 143.076,99.630 142.280,100.400 142.062 C 102.567 141.447,229.079 141.657,230.466 142.277 "
stroke="none"
fill="#949c9c"
fillRule="evenodd"
/>
<path
id="path4"
d="M35.200 0.984 C 35.200 1.947,35.121 1.971,31.700 2.084 L 28.200 2.200 28.077 3.900 L 27.954 5.600 25.403 5.600 C 21.914 5.600,20.903 6.043,20.590 7.712 C 20.367 8.902,20.142 9.103,18.669 9.430 C 17.102 9.777,16.988 9.898,16.800 11.400 C 16.605 12.956,16.554 13.003,14.922 13.122 C 13.260 13.243,13.243 13.260,13.122 14.922 C 13.003 16.554,12.956 16.605,11.400 16.800 C 9.898 16.988,9.777 17.102,9.430 18.669 C 9.103 20.142,8.902 20.367,7.712 20.590 C 6.043 20.903,5.600 21.914,5.600 25.403 L 5.600 27.954 3.900 28.077 L 2.200 28.200 2.084 31.700 C 1.971 35.121,1.947 35.200,0.984 35.200 L 0.000 35.200 0.000 200.000 L 0.000 364.800 0.984 364.800 C 1.947 364.800,1.971 364.879,2.084 368.300 L 2.200 371.800 3.900 372.177 L 5.600 372.554 5.600 374.851 C 5.600 378.083,6.072 379.102,7.712 379.410 C 8.902 379.633,9.103 379.858,9.430 381.331 C 9.777 382.898,9.898 383.012,11.400 383.200 C 12.953 383.394,13.004 383.449,13.121 385.059 C 13.247 386.786,13.757 387.181,15.876 387.195 C 16.598 387.199,16.773 387.463,16.876 388.700 C 16.992 390.104,17.107 390.224,18.669 390.570 C 20.142 390.897,20.367 391.098,20.590 392.288 C 20.903 393.957,21.914 394.400,25.403 394.400 L 27.954 394.400 28.077 396.100 L 28.200 397.800 31.700 397.916 C 35.121 398.029,35.200 398.053,35.200 399.016 L 35.200 400.000 200.000 400.000 L 364.800 400.000 364.800 399.016 C 364.800 398.053,364.879 398.029,368.300 397.916 L 371.800 397.800 372.177 396.100 L 372.554 394.400 375.103 394.400 C 378.233 394.400,379.094 393.974,379.414 392.265 C 379.633 391.101,379.865 390.896,381.331 390.570 C 382.893 390.224,383.008 390.104,383.124 388.700 C 383.241 387.288,383.327 387.200,384.596 387.200 C 386.308 387.200,387.200 386.308,387.200 384.596 C 387.200 383.327,387.288 383.241,388.700 383.124 C 390.104 383.008,390.224 382.893,390.570 381.331 C 390.896 379.865,391.101 379.633,392.265 379.414 C 393.974 379.094,394.400 378.233,394.400 375.103 L 394.400 372.554 396.100 372.177 L 397.800 371.800 397.916 368.300 C 398.029 364.879,398.053 364.800,399.016 364.800 L 400.000 364.800 400.000 200.000 L 400.000 35.200 399.016 35.200 C 398.053 35.200,398.029 35.121,397.916 31.700 L 397.800 28.200 396.100 28.077 L 394.400 27.954 394.400 25.403 C 394.400 21.914,393.957 20.903,392.288 20.590 C 391.098 20.367,390.897 20.142,390.570 18.669 C 390.224 17.107,390.104 16.992,388.700 16.876 C 387.463 16.773,387.199 16.598,387.195 15.876 C 387.181 13.757,386.786 13.247,385.059 13.121 C 383.452 13.004,383.396 12.953,383.275 11.480 C 383.121 9.617,382.265 9.200,378.597 9.200 L 376.046 9.200 375.923 7.500 C 375.802 5.821,375.779 5.798,374.173 5.681 C 372.616 5.566,372.529 5.488,372.173 3.881 L 371.800 2.200 368.300 2.084 C 364.879 1.971,364.800 1.947,364.800 0.984 L 364.800 0.000 200.000 0.000 L 35.200 0.000 35.200 0.984 M224.918 137.663 C 225.394 137.918,225.998 138.341,226.260 138.603 C 226.522 138.865,227.786 139.194,229.068 139.333 C 233.008 139.763,234.411 140.311,234.859 141.598 C 235.110 142.318,235.696 142.895,236.441 143.154 C 237.232 143.430,237.852 144.075,238.312 145.101 C 238.690 145.944,239.630 147.197,240.400 147.884 L 241.800 149.134 241.800 200.335 L 241.800 251.536 240.830 252.568 C 240.297 253.135,239.622 253.600,239.330 253.600 C 239.039 253.600,238.800 253.876,238.800 254.214 C 238.800 255.226,237.635 256.688,236.432 257.187 C 235.812 257.444,235.107 258.087,234.866 258.616 C 234.281 259.898,232.411 260.620,228.925 260.910 C 227.174 261.056,225.616 261.433,224.942 261.875 C 223.251 262.983,102.535 263.008,101.311 261.901 C 100.887 261.516,99.743 261.044,98.770 260.851 C 96.289 260.360,94.801 259.708,94.795 259.110 C 94.792 258.829,94.178 258.204,93.430 257.719 C 92.682 257.235,91.559 256.159,90.935 255.328 C 90.311 254.498,89.350 253.425,88.800 252.944 L 87.800 252.069 87.693 201.219 C 87.579 147.532,87.556 148.251,89.467 147.000 C 90.140 146.560,90.984 145.525,91.344 144.700 C 91.703 143.875,92.181 143.200,92.406 143.200 C 92.630 143.200,93.434 142.562,94.191 141.782 C 95.594 140.337,98.679 139.200,101.196 139.200 C 101.832 139.200,102.475 139.002,102.624 138.760 C 103.575 137.222,103.193 137.232,164.726 137.216 C 208.933 137.204,224.273 137.318,224.918 137.663 M308.162 152.107 C 309.021 152.598,310.281 153.692,310.962 154.537 L 312.200 156.074 312.322 197.737 C 312.469 247.711,312.714 243.696,309.338 246.578 L 306.736 248.800 303.981 248.800 C 301.366 248.800,301.156 248.729,299.827 247.400 C 299.057 246.630,298.233 246.000,297.997 246.000 C 297.174 246.000,292.400 244.329,292.400 244.041 C 292.400 243.256,290.693 242.085,289.201 241.847 C 287.095 241.510,284.400 240.158,284.400 239.438 C 284.400 239.096,283.242 238.578,281.300 238.052 C 278.976 237.422,277.950 236.919,277.200 236.040 C 276.610 235.349,275.626 234.760,274.800 234.604 C 272.827 234.231,270.324 233.017,269.504 232.034 C 269.121 231.575,268.457 231.200,268.028 231.200 C 267.598 231.200,267.148 231.040,267.027 230.844 C 266.906 230.649,266.340 230.371,265.769 230.228 C 263.915 22Line truncated
stroke="none"
fill="#141c24"
fillRule="evenodd"
/>
</g>
</svg>
<span className="text-sm ltr:ml-2 rtl:mr-2">Cal.com Video</span>
</div>
)}
{location.type === LocationType.Zoom && (
<div className="flex flex-grow items-center">
<svg
className="h-6 w-6"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M32 0C49.6733 0 64 14.3267 64 32C64 49.6733 49.6733 64 32 64C14.3267 64 0 49.6733 0 32C0 14.3267 14.3267 0 32 0Z"
fill="#E5E5E4"
/>
<path
d="M32.0002 0.623047C49.3292 0.623047 63.3771 14.6709 63.3771 31.9999C63.3771 49.329 49.3292 63.3768 32.0002 63.3768C14.6711 63.3768 0.623291 49.329 0.623291 31.9999C0.623291 14.6709 14.6716 0.623047 32.0002 0.623047Z"
fill="white"
/>
<path
d="M31.9998 3.14014C47.9386 3.14014 60.8597 16.0612 60.8597 32C60.8597 47.9389 47.9386 60.8599 31.9998 60.8599C16.0609 60.8599 3.13989 47.9389 3.13989 32C3.13989 16.0612 16.0609 3.14014 31.9998 3.14014Z"
fill="#4A8CFF"
/>
<path
d="M13.1711 22.9581V36.5206C13.1832 39.5875 15.6881 42.0558 18.743 42.0433H38.5125C39.0744 42.0433 39.5266 41.5911 39.5266 41.0412V27.4788C39.5145 24.4119 37.0096 21.9435 33.9552 21.956H14.1857C13.6238 21.956 13.1716 22.4082 13.1716 22.9581H13.1711ZM40.7848 28.2487L48.9469 22.2864C49.6557 21.6998 50.2051 21.8462 50.2051 22.9095V41.0903C50.2051 42.2999 49.5329 42.1536 48.9469 41.7134L40.7848 35.7631V28.2487Z"
fill="white"
/>
</svg>
<span className="text-sm ltr:ml-2 rtl:mr-2">Zoom Video</span>
</div>
)}
{location.type === LocationType.Tandem && (
<div className="flex flex-grow items-center">
<svg
width="1.25em"
height="1.25em"
viewBox="0 0 400 400"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M167.928 256.163L64 324V143.835L167.928 76V256.163Z"
fill="#4341DC"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M335.755 256.163L231.827 324V143.835L335.755 76V256.163Z"
fill="#00B6B6"
/>
</svg>
<span className="ml-2 text-sm">Tandem Video</span>
</div>
)}
{location.type === LocationType.Jitsi && (
<div className="flex flex-grow items-center">
<svg
className="h-6 w-6"
viewBox="0 0 60 60"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M45.4181 23.2445C45.0316 22.4169 44.4086 21.7223 43.6277 21.2487C42.6993 20.6602 41.5397 20.5354 40.7313 20.5354C40.4591 20.5354 40.1871 20.5495 39.9163 20.5776H39.8961C39.9105 20.4663 39.9393 20.3223 39.9633 20.2051C40.0037 19.9997 40.0497 19.7664 40.0757 19.5235C40.1832 18.4819 39.8126 17.0803 39.1301 15.9543C39.0216 15.7747 39.0254 15.7133 39.0254 15.7133C39.0596 15.6624 39.0995 15.6157 39.1445 15.5741L39.2069 15.5088C40.8206 13.7808 41.3083 11.881 40.6545 9.85058C39.4152 6.00002 39.083 6.00002 38.8401 6.00002C38.786 5.99948 38.7323 6.01041 38.6827 6.03209C38.633 6.05376 38.5886 6.08569 38.5521 6.12578C38.5118 6.1746 38.4823 6.23142 38.4656 6.29249C38.4489 6.35356 38.4454 6.41747 38.4552 6.48002C38.5809 7.75682 38.2929 9.58466 38.1009 10.2807C37.8657 11.1562 37.0449 12.4618 34.4529 13.7031C34.2709 13.7806 34.0851 13.8489 33.8961 13.9075C33.0504 14.1907 31.6373 14.664 30.852 15.745C30.2597 16.4391 30.1166 17.1341 29.8603 18.3888C29.6318 19.5043 29.4475 20.8627 29.8085 22.4842C29.8449 22.6445 29.8795 22.7789 29.9112 22.8989C29.9333 22.9843 29.9525 23.0583 29.9669 23.1226L29.9976 23.1159L29.9659 23.1245C29.9899 23.2205 29.9745 23.2435 29.94 23.2675C29.8153 23.3169 29.6869 23.3564 29.556 23.3856C29.172 23.4471 28.788 23.5152 28.4213 23.5872C27.2539 23.7879 23.7249 24.3965 22.0997 26.9837C21.0965 28.5792 20.9544 30.6634 21.6753 33.1853C22.4232 35.6813 23.9073 37.3968 24.923 37.7462L24.9393 37.752C25.0123 37.7846 25.0824 37.8125 25.1505 37.8355C25.1505 37.9056 25.1438 37.991 25.1352 38.087C25.1281 38.1293 25.1214 38.1754 25.115 38.2253C25.0037 39.0394 24.6197 39.9168 24.228 39.9398C24.0216 39.8995 23.2037 39.4358 22.6248 39.0557C22.4392 38.9347 22.2632 38.8189 22.0968 38.7082C20.4811 37.6387 19.5902 37.0502 18.2289 36.8986C18.1824 36.8936 18.1356 36.891 18.0888 36.8909C16.6488 36.8909 14.0568 39.3158 14.0021 44.1485C13.9742 46.5696 14.2277 48.7824 14.7576 50.7264C15.1358 52.1203 15.539 52.9142 15.6561 53.1264L16.1227 54L16.3358 53.04C17.459 47.952 18.8453 46.7261 21.1521 45.2592C22.8427 46.8432 25.0632 47.6784 27.5918 47.6784C29.723 47.6784 31.9531 47.0592 33.8693 45.9341C35.7518 44.8301 37.2053 43.3421 38.0933 41.6227C38.2411 41.7187 38.4197 41.8435 38.5377 41.9261C38.9601 42.2218 39.0494 42.2842 39.2001 42.2842H39.2136C39.3787 42.2774 40.9358 41.7552 42.5304 39.8074C43.4568 38.6755 44.2209 37.2758 44.8008 35.6448C45.5189 33.6288 45.9528 31.248 46.1006 28.5706C46.2638 26.3223 46.0353 24.5319 45.4219 23.2493L45.4181 23.2445ZM39.4449 10.3027C39.5788 10.8426 39.5962 11.4049 39.4958 11.952C39.3461 13.0157 38.8949 13.968 38.1249 14.8368C38.7811 13.3969 39.2257 11.8698 39.4449 10.3027V10.3027ZM38.3313 16.9047C38.5659 17.2594 38.7449 17.6479 38.8622 18.0567C38.9924 18.5927 38.9691 19.1546 38.795 19.6781C38.5195 20.3338 38.1499 20.665 37.6958 20.665H37.6353C37.4753 20.6409 37.3219 20.5847 37.1841 20.4999C36.6005 20.1581 36.2846 19.2519 36.5121 18.5875C36.5217 18.5683 36.5304 18.5482 36.539 18.5271C36.5765 18.4311 36.683 18.2467 36.9787 17.9616C37.4011 17.5882 38.0184 17.1168 38.3294 16.9056L38.3313 16.9047ZM31.5729 16.7242L31.5787 16.7136V16.704C31.7035 16.3968 32.5627 15.8995 33.1329 15.5703C33.204 15.528 33.275 15.4867 33.3461 15.4464C33.5958 15.3153 33.8522 15.1971 34.1141 15.0922C35.052 14.6928 36.5141 14.0707 37.7179 12.912C37.3685 14.137 36.5275 16.2 35.1451 17.449C34.8379 17.7264 34.2177 17.9722 33.4987 18.2563C32.724 18.5616 31.7928 18.9283 30.8894 19.4823C30.9595 18.7757 31.1774 17.5431 31.5681 16.7242H31.5729ZM30.9729 20.7159C31.4376 20.3319 32.2881 19.8115 35.075 18.8919C35.0942 18.9783 35.1163 19.0896 35.1374 19.2C35.1547 19.2835 35.1729 19.3786 35.1941 19.4823C35.3765 20.3827 35.5781 21.3946 37.4529 21.9562C37.2782 22.3402 36.9192 22.9891 36.7944 23.113L36.7531 23.1485L36.7243 23.1955C36.3595 23.7811 34.3877 25.0589 33.5045 25.0589C33.4665 25.0593 33.4286 25.0564 33.3912 25.0503C32.8593 24.9475 31.6382 23.8627 31.2792 23.0611C30.5851 21.4877 30.7272 20.9203 30.9681 20.7197L30.9729 20.7159ZM24.0293 35.1581C23.0078 33.6384 22.2341 30.983 22.7409 28.848V28.8384C22.9857 27.6768 23.7969 26.9933 24.132 26.7533L24.18 26.7178C24.6773 26.2839 25.6949 25.8259 27.0465 25.4256C27.2385 25.3757 27.3441 25.3536 27.3489 25.3536C27.5976 25.3018 27.8693 25.2394 28.1333 25.1789C28.6328 25.0513 29.1392 24.9523 29.6501 24.8823C29.7768 24.8688 29.9054 24.8448 30.0283 24.8208C30.1944 24.7844 30.3634 24.7625 30.5333 24.7555C30.6688 24.7483 30.8031 24.7839 30.9173 24.8573C31.4184 25.4525 32.6453 26.6275 33.4497 26.6967H33.4737H33.5006C34.4165 26.6477 35.8517 25.7539 37.7678 24.0384C38.0933 23.7504 38.3957 23.4989 38.6923 23.2791L38.7288 23.2512C39.2517 22.8546 39.8275 22.533 40.4395 22.296C40.2389 22.9527 40.0795 24.0749 40.7275 25.656C41.0645 26.4739 41.7115 28.3171 42.2481 30.1219C43.1717 33.2304 43.043 33.7776 43.0248 33.8285C42.9992 33.9341 42.9574 34.0351 42.9009 34.128C42.8289 34.1141 42.7593 34.0895 42.6945 34.055C42.2289 33.84 41.7067 33.456 41.0961 33.0048C39.9633 32.1744 38.555 31.1414 36.Line truncated
fill="#727272"
/>
</svg>
<span className="ml-2 text-sm">Jitsi Meet</span>
</div>
)}
{location.type === LocationType.Teams && (
<div className="flex flex-grow items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
viewBox="0 0 2228.833 2073.333">
<path
fill="#5059C9"
d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"
/>
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25" />
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917" />
<path
fill="#7B83EB"
d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"
/>
<path
opacity=".1"
d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"
/>
<path
opacity=".2"
d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
/>
<path
opacity=".2"
d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
/>
<path
opacity=".2"
d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"
/>
<path
opacity=".1"
d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"
/>
<path
opacity=".2"
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
/>
<path
opacity=".2"
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
/>
<path
opacity=".2"
d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"
/>
<linearGradient
id="a"
gradientUnits="userSpaceOnUse"
x1="198.099"
y1="1683.0726"
x2="942.2344"
y2="394.2607"
gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
<stop offset="0" stopColor="#5a62c3" />
<stop offset=".5" stopColor="#4d55bd" />
<stop offset="1" stopColor="#3940ab" />
</linearGradient>
<path
fill="url(#a)"
d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"
/>
<path
fill="#FFF"
d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"
/>
</svg>
<span className="ml-2 text-sm">MS Teams</span>
</div>
)}
<div className="flex">
<button
type="button"
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
openLocationModal(location.type);
}}
aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
<Icon.FiEdit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
</button>
</div>
</div>
</li>
))}
{formMethods.getValues("locations").length > 0 &&
formMethods.getValues("locations").length !== locationOptions.length && (
<li>
<button
type="button"
className="flex rounded-sm py-2 hover:bg-gray-100"
onClick={() => setShowLocationModal(true)}>
<Icon.FiPlus className="mt-0.5 h-4 w-4 text-neutral-900" />
<span className="ml-1 text-sm font-medium text-neutral-700">{t("add_location")}</span>
</button>
</li>
)}
);
})}
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
<li>
<button
type="button"
className="flex rounded-sm py-2 hover:bg-gray-100"
onClick={() => setShowLocationModal(true)}>
<Icon.FiPlus className="mt-0.5 h-4 w-4 text-neutral-900" />
<span className="ml-1 text-sm font-medium text-neutral-700">{t("add_location")}</span>
</button>
</li>
)}
</ul>
)}
</div>
-41
View File
@@ -1,41 +0,0 @@
import { TFunction } from "next-i18next";
import { LocationType } from "./location";
/**
* Use this function to translate booking location value to a readable string
* @param linkValue
* @param translationFunction
* @returns
*/
export const linkValueToString = (
linkValue: string | undefined | null,
translationFunction: TFunction
): string => {
const t = translationFunction;
if (!linkValue) {
return translationFunction("no_location");
}
switch (linkValue) {
case LocationType.InPerson:
return t("in_person_meeting");
case LocationType.UserPhone:
return t("user_phone");
case LocationType.GoogleMeet:
return `Google Meet: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Zoom:
return `Zoom: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Daily:
return `Cal Video: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Jitsi:
return `Jitsi: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Huddle01:
return `Huddle01t: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Tandem:
return `Tandem: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Teams:
return `Teams: ${t("meeting_url_in_conformation_email")}`;
default:
return linkValue || "";
}
};
+4 -5
View File
@@ -3,7 +3,7 @@ import { GetStaticPaths, GetStaticPropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import { locationHiddenFilter, LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -148,12 +148,12 @@ async function getUserPageProps(context: GetStaticPropsContext) {
if (!eventType) return { notFound: true };
//TODO: Use zodSchema to verify it instead of using Type Assertion
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: eventType.users.map((user) => ({
name: user.name,
username: user.username,
@@ -241,11 +241,10 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
}
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: users.map((user) => {
return {
name: user.name,
+5 -3
View File
@@ -1,7 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib";
import {
getDefaultEvent,
@@ -25,7 +25,6 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return props.away ? (
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
@@ -57,7 +56,7 @@ export default function Book(props: BookPageProps) {
</main>
</div>
) : (
<BookingPage {...props} locationLabels={locationLabels} />
<BookingPage {...props} />
);
}
@@ -138,8 +137,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
const eventTypeObject = [eventType].map((e) => {
let locations = eventTypeRaw.locations || [];
locations = privacyFilteredLocations(locations as LocationObject[]);
return {
...e,
locations: locations,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
schedulingType: null,
+7 -6
View File
@@ -5,9 +5,11 @@ import { RRule } from "rrule";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { getLocationValueForDB, LocationObject } from "@calcom/app-store/locations";
import { handlePayment } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { getEventName } from "@calcom/core/event";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import dayjs from "@calcom/dayjs";
import {
@@ -34,7 +36,6 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { getEventName } from "@lib/event";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscribers from "@lib/webhooks/subscriptions";
@@ -174,6 +175,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
return {
...eventType,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: (eventType.locations ?? []) as LocationObject[],
};
};
@@ -358,9 +360,8 @@ async function handler(req: NextApiRequest) {
const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const location = !!eventType.locations ? (eventType.locations as Array<{ type: string }>)[0] : "";
const locationType = !!location && location.type ? location.type : "";
const bookingLocation = getLocationValueForDB(reqBody.location, eventType.locations);
console.log(bookingLocation, reqBody.location, eventType.locations);
const customInputs = {} as NonNullable<CalendarEvent["customInputs"]>;
const teamMemberPromises =
@@ -387,7 +388,7 @@ async function handler(req: NextApiRequest) {
eventType: eventType.title,
eventName: eventType.eventName,
host: organizerUser.name || "Nameless",
location: locationType,
location: bookingLocation,
t: tOrganizer,
};
@@ -408,7 +409,7 @@ async function handler(req: NextApiRequest) {
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
},
attendees: attendeesList,
location: reqBody.location, // Will be processed by the EventManager later.
location: bookingLocation, // Will be processed by the EventManager later.
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
hideCalendarNotes: eventType.hideCalendarNotes,
+2 -1
View File
@@ -11,6 +11,7 @@ import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { DailyLocationType } from "@calcom/app-store/locations";
import { refund } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { deleteMeeting } from "@calcom/core/videoClient";
@@ -238,7 +239,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
/** TODO: Remove this without breaking functionality */
if (bookingToDelete.location === "integrations:daily") {
if (bookingToDelete.location === DailyLocationType) {
bookingToDelete.user.credentials.push(FAKE_DAILY_CREDENTIAL);
}
+1 -1
View File
@@ -70,7 +70,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
isGlobal={data.isGlobal}
type={data.type}
logo={data.logo}
categories={[data.category]}
categories={data.categories ?? [data.category]}
author={data.publisher}
feeType={data.feeType || "usage-based"}
price={data.price || 0}
+2 -2
View File
@@ -2,13 +2,13 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { getWorkingHours } from "@lib/availability";
import { GetBookingType } from "@lib/getBooking";
import { locationHiddenFilter, LocationObject } from "@lib/location";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
@@ -105,7 +105,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
periodStartDate: hashedLink.eventType.periodStartDate?.toString() ?? null,
periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null,
slug,
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: users.map((u) => ({
name: u.name,
username: u.username,
+1 -5
View File
@@ -1,7 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
@@ -17,10 +16,7 @@ import { ssrInit } from "@server/lib/ssr";
export type HashLinkPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: HashLinkPageProps) {
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return <BookingPage {...props} locationLabels={locationLabels} />;
return <BookingPage {...props} />;
}
Book.isThemeSupported = true;
+52 -338
View File
@@ -15,8 +15,10 @@ import { v5 as uuidv5 } from "uuid";
import { z } from "zod";
import { SelectGifInput } from "@calcom/app-store/giphy/components";
import { getEventLocationType, EventLocationType } from "@calcom/app-store/locations";
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { LocationObject, LocationType } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -40,7 +42,6 @@ import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { LocationObject, LocationType } from "@lib/location";
import { slugify } from "@lib/slugify";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -68,7 +69,7 @@ import { TRPCClientError } from "@trpc/client";
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
@@ -90,9 +91,10 @@ export type FormValues = {
hideCalendarNotes: boolean;
hashedLink: string | undefined;
locations: {
type: LocationType;
type: EventLocationType["type"];
address?: string;
link?: string;
phone?: string;
hostPhoneNumber?: string;
displayLocationPublicly?: boolean;
}[];
@@ -267,6 +269,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (message) {
showToast(message, "error");
}
showToast("Some error occured", "error");
},
});
@@ -332,7 +335,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
deleteMutation.mutate(payload);
}
const openLocationModal = (type: LocationType) => {
const openLocationModal = (type: EventLocationType["type"]) => {
setSelectedLocation(locationOptions.find((option) => option.value === type));
setShowLocationModal(true);
};
@@ -345,7 +348,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
);
};
const addLocation = (newLocationType: LocationType, details = {}) => {
const addLocation = (newLocationType: EventLocationType["type"], details = {}) => {
const existingIdx = formMethods.getValues("locations").findIndex((loc) => newLocationType === loc.type);
if (existingIdx !== -1) {
const copy = formMethods.getValues("locations");
@@ -435,7 +438,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
});
const locationFormMethods = useForm<{
locationType: LocationType;
locationType: EventLocationType["type"];
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://
@@ -455,17 +458,13 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
onChange={(e) => {
if (e?.value) {
const newLocationType: LocationType = e.value;
const newLocationType: EventLocationType["type"] = e.value;
const eventLocationType = getEventLocationType(newLocationType);
if (!eventLocationType) {
return;
}
locationFormMethods.setValue("locationType", newLocationType);
if (
newLocationType === LocationType.InPerson ||
newLocationType === LocationType.Link ||
newLocationType === LocationType.UserPhone ||
newLocationType === LocationType.Whereby ||
newLocationType === LocationType.Around ||
newLocationType === LocationType.Riverside ||
newLocationType === LocationType.Ping
) {
if (eventLocationType.organizerInputType) {
openLocationModal(newLocationType);
} else {
addLocation(newLocationType);
@@ -476,334 +475,49 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</div>
)}
{/* TODO: all of this should be generated from the App Store */}
{formMethods.getValues("locations").length > 0 && (
<ul>
{formMethods.getValues("locations").map((location) => (
<li key={location.type} className="mb-2 rounded-sm border border-neutral-300 py-1.5 px-2">
<div className="flex justify-between">
{location.type === LocationType.InPerson && (
<div className="flex flex-grow items-center">
<Icon.FiMapPin className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.address}
{formMethods.getValues("locations").map((location, index) => {
const eventLocation = getEventLocationType(location.type);
if (!eventLocation) {
// It's possible that the location app in use got uninstalled.
return null;
}
return (
<li key={location.type} className="mb-2 rounded-sm border border-neutral-300 py-1.5 px-2">
<div className="flex justify-between">
<div key={index} className="flex flex-grow items-center">
<img
src={eventLocation.iconUrl}
className="h-6 w-6"
alt={`${eventLocation.label} logo`}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2">
{location[eventLocation.defaultValueVariable] || eventLocation.label}
</span>
</div>
)}
{location.type === LocationType.Link && (
<div className="flex flex-grow items-center">
<Icon.FiGlobe className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.link}
</span>
<div className="flex">
<button
type="button"
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
openLocationModal(location.type);
}}
aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
<Icon.FiEdit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
</button>
</div>
)}
{location.type === LocationType.UserPhone && (
<div className="flex flex-grow items-center">
<Icon.FiPhone className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.hostPhoneNumber}
</span>
</div>
)}
{location.type === LocationType.Phone && (
<div className="flex flex-grow items-center">
<Icon.FiPhone className="h-6 w-6" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{t("phone_call")}</span>
</div>
)}
{location.type === LocationType.Whereby && (
<div className="flex flex-grow items-center">
<img src="/api/app-store/whereby/icon.svg" className="h-6 w-6" alt="Whereby logo" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{location.link}</span>
</div>
)}
{location.type === LocationType.Around && (
<div className="flex flex-grow items-center">
<img src="/api/app-store/around/icon.svg" className="h-6 w-6" alt="Around logo" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{location.link}</span>
</div>
)}
{location.type === LocationType.Riverside && (
<div className="flex flex-grow items-center">
<img src="/api/app-store/riverside/icon.svg" className="h-6 w-6" alt="Riverside logo" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{location.link}</span>
</div>
)}
{location.type === LocationType.Ping && (
<div className="flex flex-grow items-center">
<img src="/api/app-store/ping/icon.svg" className="h-6 w-6" alt="Ping logo" />
<span className="text-sm ltr:ml-2 rtl:mr-2">{location.link}</span>
</div>
)}
{location.type === LocationType.GoogleMeet && (
<div className="flex flex-grow items-center">
<svg
className="h-6 w-6"
viewBox="0 0 64 54"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M16 0V16H0" fill="#EA4335" />
<path
d="M16 0V16H37.3333V27.0222L53.3333 14.0444V5.33332C53.3333 1.77777 51.5555 0 47.9999 0"
fill="#FFBA00"
/>
<path
d="M15.6438 53.3341V37.3341H37.3326V26.6675L53.3326 39.2897V48.0008C53.3326 51.5563 51.5548 53.3341 47.9993 53.3341"
fill="#00AC47"
/>
<path d="M37.3335 26.6662L53.3335 13.6885V39.644" fill="#00832D" />
<path
d="M53.3335 13.6892L60.8001 7.64481C62.4001 6.40037 64.0001 6.40037 64.0001 8.88925V44.4447C64.0001 46.9336 62.4001 46.9336 60.8001 45.6892L53.3335 39.6447"
fill="#00AC47"
/>
<path
d="M0 36.9785V48.0007C0 51.5563 1.77777 53.334 5.33332 53.334H16V36.9785"
fill="#0066DA"
/>
<path d="M0 16H16V37.3333H0" fill="#2684FC" />
</svg>
<span className="text-sm ltr:ml-2 rtl:mr-2">Google Meet</span>
</div>
)}
{location.type === LocationType.Huddle01 && (
<div className="flex flex-grow items-center">
<svg
width="1.25em"
height="1.25em"
viewBox="0 0 26 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M14.8607 0H4.04353C3.16693 0 2.32622 0.347292 1.70636 0.965476C1.08651 1.58366 0.738281 2.4221 0.738281 3.29634V14.0844C0.738281 14.9586 1.08651 15.7971 1.70636 16.4152C2.32622 17.0334 3.16693 17.3807 4.04353 17.3807H14.8607C15.7373 17.3807 16.578 17.0334 17.1979 16.4152C17.8177 15.7971 18.166 14.9586 18.166 14.0844V3.29634C18.166 2.4221 17.8177 1.58366 17.1979 0.965476C16.578 0.347292 15.7373 0 14.8607 0V0Z"
fill="#246BFD"
/>
<path
d="M24.1641 3.10754C24.0122 3.14004 23.8679 3.20106 23.7389 3.28734L21.1623 4.85161C20.7585 5.09889 20.4269 5.44766 20.2008 5.86299C19.9686 6.28713 19.8472 6.76272 19.8477 7.24595V10.1407C19.8475 10.6251 19.9694 11.1017 20.2023 11.5267C20.4295 11.9431 20.7627 12.2925 21.1683 12.5396L23.7645 14.1038C23.9325 14.2074 24.1202 14.2753 24.3158 14.3031C24.5103 14.3302 24.7084 14.3164 24.8973 14.2627C25.0881 14.2077 25.2659 14.1149 25.4201 13.99C25.5764 13.862 25.706 13.7047 25.8017 13.527C25.9321 13.2836 26.0003 13.0118 26 12.7359V4.62985C25.9995 4.39497 25.9483 4.16296 25.8498 3.94961C25.7523 3.73989 25.6097 3.55418 25.4321 3.40571C25.258 3.26046 25.0522 3.15784 24.8311 3.10604C24.6118 3.05359 24.3832 3.0541 24.1641 3.10754Z"
fill="#246BFD"
/>
<path
d="M7.07325 14.3165C6.26596 14.3165 5.64849 14.0822 5.22081 13.6138C4.79313 13.1453 4.57928 12.484 4.57928 11.63V6.0112C4.57928 5.15515 4.79313 4.49338 5.22081 4.0259C5.64849 3.55842 6.26596 3.32418 7.07325 3.32318C7.87452 3.32318 8.4915 3.55742 8.92419 4.0259C9.35687 4.49438 9.57071 5.15615 9.5657 6.0112V11.63C9.5657 12.484 9.35186 13.1453 8.92419 13.6138C8.49651 14.0822 7.87953 14.3165 7.07325 14.3165ZM7.07325 12.7897C7.63914 12.7897 7.92259 12.4401 7.9236 11.7408V5.90332C7.9236 5.20409 7.64015 4.85448 7.07325 4.85448C6.50635 4.85448 6.2224 5.20409 6.2214 5.90332V11.7363C6.2214 12.4396 6.50534 12.7907 7.07325 12.7897Z"
fill="white"
/>
<path
d="M12.6791 6.0112H10.9619V4.82002C11.3388 4.83087 11.7155 4.78952 12.0811 4.69716C12.3452 4.63341 12.5856 4.49564 12.7737 4.3001C12.9727 4.05484 13.1254 3.77563 13.2244 3.47601H14.3287V14.1637H12.6791V6.0112Z"
fill="white"
/>
</svg>
<span className="ml-2 text-sm">Huddle01 Web3 Video</span>
</div>
)}
{location.type === LocationType.Daily && (
<div className="flex flex-grow items-center">
<svg
id="svg"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="1.25em"
height="1.25em"
viewBox="0, 0, 400,400">
<g id="svgg">
<path
id="path0"
d="M100.400 142.062 C 99.630 142.280,98.394 143.076,97.654 143.830 C 96.914 144.583,95.997 145.200,95.616 145.200 C 94.776 145.200,93.802 146.248,93.389 147.598 C 93.221 148.147,92.560 149.054,91.919 149.613 C 90.024 151.267,90.020 151.390,90.010 199.645 C 89.999 248.545,90.014 248.945,91.940 250.744 C 92.571 251.334,93.229 252.262,93.401 252.808 C 93.751 253.916,95.054 255.200,95.829 255.200 C 96.107 255.200,96.710 255.808,97.169 256.550 C 98.373 258.498,94.832 258.400,164.273 258.400 C 231.741 258.400,231.099 258.418,231.949 256.552 C 232.208 255.983,233.149 255.250,234.197 254.801 C 235.357 254.304,236.005 253.774,236.014 253.314 C 236.021 252.921,236.375 251.880,236.800 251.000 C 237.225 250.120,237.579 249.119,237.586 248.776 C 237.594 248.434,237.864 247.804,238.187 247.376 C 238.696 246.704,238.776 240.392,238.787 200.426 C 238.801 149.852,238.967 154.051,236.799 149.949 C 236.610 149.591,236.332 148.647,236.183 147.850 C 235.956 146.640,235.591 146.227,233.964 145.342 C 232.893 144.759,231.907 143.938,231.774 143.518 C 231.641 143.098,231.052 142.539,230.466 142.277 C 229.079 141.657,102.567 141.447,100.400 142.062 "
stroke="none"
fill="#f9f9f9"
fillRule="evenodd"
/>
<path
id="path1"
d="M304.600 153.562 C 304.160 153.717,302.589 154.419,301.109 155.122 C 299.629 155.825,298.171 156.400,297.869 156.400 C 297.567 156.400,296.528 156.977,295.560 157.682 C 294.592 158.387,292.872 159.272,291.739 159.649 C 290.605 160.025,288.743 160.976,287.602 161.761 C 286.460 162.547,284.778 163.386,283.863 163.628 C 282.948 163.869,281.300 164.672,280.200 165.413 C 279.100 166.154,277.660 166.885,277.000 167.037 C 275.491 167.385,272.800 168.718,272.800 169.117 C 272.800 169.485,270.749 170.506,268.629 171.194 C 266.207 171.979,263.730 174.650,263.412 176.820 C 262.921 180.167,263.353 224.092,263.889 225.295 C 264.635 226.970,266.755 228.668,269.300 229.629 C 270.565 230.107,271.600 230.622,271.600 230.775 C 271.600 231.219,274.452 232.687,276.241 233.162 C 277.144 233.403,278.381 234.061,278.991 234.626 C 279.600 235.191,281.382 236.125,282.950 236.701 C 284.517 237.278,286.430 238.236,287.200 238.831 C 287.970 239.426,289.320 240.126,290.200 240.387 C 292.160 240.967,294.400 242.079,294.400 242.472 C 294.400 242.837,297.518 244.231,299.125 244.584 C 299.790 244.730,300.737 245.198,301.228 245.625 C 301.720 246.051,302.620 246.400,303.228 246.400 C 303.837 246.400,304.605 246.504,304.936 246.631 C 305.267 246.758,305.902 246.498,306.348 246.052 C 306.793 245.607,307.721 244.951,308.410 244.595 C 310.905 243.305,310.800 245.287,310.800 199.575 C 310.800 155.897,310.789 155.600,309.169 155.600 C 309.026 155.600,308.231 155.060,307.400 154.400 C 306.569 153.740,305.780 153.218,305.645 153.240 C 305.510 153.262,305.040 153.407,304.600 153.562 "
stroke="none"
fill="#1be7b8"
fillRule="evenodd"
/>
<path
id="path2"
d="M104.148 137.776 C 103.459 138.076,102.774 138.519,102.624 138.760 C 102.475 139.002,101.832 139.200,101.196 139.200 C 98.679 139.200,95.594 140.337,94.191 141.782 C 93.434 142.562,92.630 143.200,92.406 143.200 C 92.181 143.200,91.703 143.875,91.344 144.700 C 90.984 145.525,90.140 146.560,89.467 147.000 C 87.556 148.251,87.579 147.532,87.693 201.219 L 87.800 252.069 88.800 252.944 C 89.350 253.425,90.311 254.498,90.935 255.328 C 91.559 256.159,92.682 257.235,93.430 257.719 C 94.178 258.204,94.792 258.829,94.795 259.110 C 94.801 259.708,96.289 260.360,98.770 260.851 C 99.743 261.044,100.887 261.516,101.311 261.901 C 102.535 263.008,223.251 262.983,224.942 261.875 C 225.616 261.433,227.174 261.056,228.925 260.910 C 232.411 260.620,234.281 259.898,234.866 258.616 C 235.107 258.087,235.812 257.444,236.432 257.187 C 237.635 256.688,238.800 255.226,238.800 254.214 C 238.800 253.876,239.039 253.600,239.330 253.600 C 239.622 253.600,240.297 253.135,240.830 252.568 L 241.800 251.536 241.800 200.335 L 241.800 149.134 240.400 147.884 C 239.630 147.197,238.690 145.944,238.312 145.101 C 237.852 144.075,237.232 143.430,236.441 143.154 C 235.696 142.895,235.110 142.318,234.859 141.598 C 234.411 140.311,233.008 139.763,229.068 139.333 C 227.786 139.194,226.522 138.865,226.260 138.603 C 224.854 137.196,225.002 137.200,164.726 137.216 C 115.566 137.229,105.185 137.325,104.148 137.776 M230.299 140.581 C 231.013 140.751,232.363 141.600,233.299 142.466 C 234.235 143.333,235.488 144.338,236.085 144.699 C 236.684 145.061,237.282 145.862,237.419 146.487 C 237.556 147.110,238.076 148.110,238.574 148.710 C 240.721 151.291,240.592 148.280,240.713 198.600 C 240.829 246.814,240.750 249.650,239.248 251.152 C 238.800 251.600,238.071 252.676,237.629 253.543 C 237.187 254.410,236.187 255.514,235.407 255.995 C 234.628 256.477,233.798 257.231,233.563 257.670 C 232.125 260.355,229.256 260.458,160.200 260.300 C 96.040 260.154,98.009 260.223,96.185 258.055 C 95.663 257.435,94.598 256.495,93.818 255.964 C 93.037 255.434,92.310 254.730,92.202 254.400 C 92.094 254.070,91.396 253.117,90.652 252.283 C 88.728 250.126,88.809 252.440,88.804 199.526 C 88.800 148.835,88.746 150.246,90.767 148.075 C 91.445 147.347,92.000 146.583,92.000 146.379 C 92.000 145.965,94.367 143.600,94.781 143.600 C 94.926 143.600,95.721 142.979,96.550 142.220 C 97.645 141.217,98.567 140.772,99.928 140.589 C 100.958 140.450,101.980 140.273,102.200 140.195 C 103.020 139.904,229.052 140.284,230.299 140.581 M302.261 151.784 C 301.415 152.085,300.477 152.683,300.177 153.111 C 299.589 153.951,298.498 154.440,295.467 155.223 C 294.179 155.556,293.257 156.096,292.706 156.841 C 292.120 157.635,291.307 158.082,289.909 158.382 C 287.523 158.894,286.569 159.361,285.000 160.786 C 284.254 161.463,282.944 162.058,281.536 162.358 C 279.852 162.717,278.929 163.194,277.936 164.216 C 277.201 164.973,276.327 165.593,275.994 165.596 C 274.726 165.605,271.323 167.114,270.329 168.107 C 269.759 168.678,268.506 169.354,267.546 169.609 C 263.906 170.578,262.647 172.127,261.546 176.994 C 260.707 180.702,260.406 219.312,261.200 221.401 C 261.420 221.979,261.860 223.699,262.178 225.222 C 262.801 228.210,263.915 229.763,265.769 230.228 C 266.340 230.371,266.906 230.649,267.027 230.844 C 267.148 231.040,267.598 231.200,268.028 231.200 C 268.457 231.200,269.121 231.575,269.504 232.034 C 270.324 233.017,272.827 234.231,274.800 234.604 C 275.626 234.760,276.610 235.349,277.200 236.040 C 277.950 236.919,278.976 237.422,281.300 238.052 C 283.242 238.578,284.400 239.096,284.400 239.438 C 284.400 240.158,287.095 241.510,289.201 241.847 C 290.693 242.085,292.400 243.256,292.400 244.041 C 292.400 244.329,297.174 246.000,297.997 246.000 C 298.233 246.000,299.057 246.630,299.827 247.400 C 301.156 248.729,301.366 248.800,303.981 248.800 L 306.736 248.800 309.338 246.578 C 312.714 243.696,312.469 247.711,312.322 197.737 L 312.200 156.074 310.962 154.537 C 308.533 151.521,305.601 150.593,302.261 151.784 M307.400 154.400 C 308.231 155.060,309.026 155.600,309.169 155.600 C 310.789 155.600,310.800 155.897,310.800 199.575 C 310.800 245.287,310.905 243.305,308.410 244.595 C 307.721 244.951,306.793 245.607,306.348 246.052 C 305.902 246.498,305.267 246.758,304.936 246.631 C 304.605 246.504,303.837 246.400,303.228 246.400 C 302.620 246.400,301.720 246.051,301.228 245.625 C 300.737 245.198,299.790 244.730,299.125 244.584 C 297.518 244.231,294.400 242.837,294.400 242.472 C 294.400 242.079,292.160 240.967,290.200 240.387 C 289.320 240.126,287.970 239.426,287.200 238.831 C 286.430 238.236,284.517 237.278,282.950 236.701 C 281.382 236.125,279.600 235.191,278.991 234.626 C 278.381 234.061,277.144 233.403,276.241 233.162 C 274.452 232.687,271.600 231.219,271.600 230.775 C 271.600 230.622,270.565 230.107,269.300 229.629 C 266.755 228.668,264.635 226.970,263.889 225.295 C 263.353 224.092,262.921 180.167,263.412 176.820 C 263.730 174.650,266.207 171.979,268.629 171.194 C 270.749 170.506,272.800 169.485,272.80Line truncated
stroke="none"
fill="#4c545c"
fillRule="evenodd"
/>
<path
id="path3"
d="M102.200 140.195 C 101.980 140.273,100.958 140.450,99.928 140.589 C 98.567 140.772,97.645 141.217,96.550 142.220 C 95.721 142.979,94.926 143.600,94.781 143.600 C 94.367 143.600,92.000 145.965,92.000 146.379 C 92.000 146.583,91.445 147.347,90.767 148.075 C 88.746 150.246,88.800 148.835,88.804 199.526 C 88.809 252.440,88.728 250.126,90.652 252.283 C 91.396 253.117,92.094 254.070,92.202 254.400 C 92.310 254.730,93.037 255.434,93.818 255.964 C 94.598 256.495,95.663 257.435,96.185 258.055 C 98.009 260.223,96.040 260.154,160.200 260.300 C 229.256 260.458,232.125 260.355,233.563 257.670 C 233.798 257.231,234.628 256.477,235.407 255.995 C 236.187 255.514,237.187 254.410,237.629 253.543 C 238.071 252.676,238.800 251.600,239.248 251.152 C 240.750 249.650,240.829 246.814,240.713 198.600 C 240.592 148.280,240.721 151.291,238.574 148.710 C 238.076 148.110,237.556 147.110,237.419 146.487 C 237.282 145.862,236.684 145.061,236.085 144.699 C 235.488 144.338,234.235 143.333,233.299 142.466 C 232.363 141.600,231.013 140.751,230.299 140.581 C 229.052 140.284,103.020 139.904,102.200 140.195 M230.466 142.277 C 231.052 142.539,231.641 143.098,231.774 143.518 C 231.907 143.938,232.893 144.759,233.964 145.342 C 235.591 146.227,235.956 146.640,236.183 147.850 C 236.332 148.647,236.610 149.591,236.799 149.949 C 238.967 154.051,238.801 149.852,238.787 200.426 C 238.776 240.392,238.696 246.704,238.187 247.376 C 237.864 247.804,237.594 248.434,237.586 248.776 C 237.579 249.119,237.225 250.120,236.800 251.000 C 236.375 251.880,236.021 252.921,236.014 253.314 C 236.005 253.774,235.357 254.304,234.197 254.801 C 233.149 255.250,232.208 255.983,231.949 256.552 C 231.099 258.418,231.741 258.400,164.273 258.400 C 94.832 258.400,98.373 258.498,97.169 256.550 C 96.710 255.808,96.107 255.200,95.829 255.200 C 95.054 255.200,93.751 253.916,93.401 252.808 C 93.229 252.262,92.571 251.334,91.940 250.744 C 90.014 248.945,89.999 248.545,90.010 199.645 C 90.020 151.390,90.024 151.267,91.919 149.613 C 92.560 149.054,93.221 148.147,93.389 147.598 C 93.802 146.248,94.776 145.200,95.616 145.200 C 95.997 145.200,96.914 144.583,97.654 143.830 C 98.394 143.076,99.630 142.280,100.400 142.062 C 102.567 141.447,229.079 141.657,230.466 142.277 "
stroke="none"
fill="#949c9c"
fillRule="evenodd"
/>
<path
id="path4"
d="M35.200 0.984 C 35.200 1.947,35.121 1.971,31.700 2.084 L 28.200 2.200 28.077 3.900 L 27.954 5.600 25.403 5.600 C 21.914 5.600,20.903 6.043,20.590 7.712 C 20.367 8.902,20.142 9.103,18.669 9.430 C 17.102 9.777,16.988 9.898,16.800 11.400 C 16.605 12.956,16.554 13.003,14.922 13.122 C 13.260 13.243,13.243 13.260,13.122 14.922 C 13.003 16.554,12.956 16.605,11.400 16.800 C 9.898 16.988,9.777 17.102,9.430 18.669 C 9.103 20.142,8.902 20.367,7.712 20.590 C 6.043 20.903,5.600 21.914,5.600 25.403 L 5.600 27.954 3.900 28.077 L 2.200 28.200 2.084 31.700 C 1.971 35.121,1.947 35.200,0.984 35.200 L 0.000 35.200 0.000 200.000 L 0.000 364.800 0.984 364.800 C 1.947 364.800,1.971 364.879,2.084 368.300 L 2.200 371.800 3.900 372.177 L 5.600 372.554 5.600 374.851 C 5.600 378.083,6.072 379.102,7.712 379.410 C 8.902 379.633,9.103 379.858,9.430 381.331 C 9.777 382.898,9.898 383.012,11.400 383.200 C 12.953 383.394,13.004 383.449,13.121 385.059 C 13.247 386.786,13.757 387.181,15.876 387.195 C 16.598 387.199,16.773 387.463,16.876 388.700 C 16.992 390.104,17.107 390.224,18.669 390.570 C 20.142 390.897,20.367 391.098,20.590 392.288 C 20.903 393.957,21.914 394.400,25.403 394.400 L 27.954 394.400 28.077 396.100 L 28.200 397.800 31.700 397.916 C 35.121 398.029,35.200 398.053,35.200 399.016 L 35.200 400.000 200.000 400.000 L 364.800 400.000 364.800 399.016 C 364.800 398.053,364.879 398.029,368.300 397.916 L 371.800 397.800 372.177 396.100 L 372.554 394.400 375.103 394.400 C 378.233 394.400,379.094 393.974,379.414 392.265 C 379.633 391.101,379.865 390.896,381.331 390.570 C 382.893 390.224,383.008 390.104,383.124 388.700 C 383.241 387.288,383.327 387.200,384.596 387.200 C 386.308 387.200,387.200 386.308,387.200 384.596 C 387.200 383.327,387.288 383.241,388.700 383.124 C 390.104 383.008,390.224 382.893,390.570 381.331 C 390.896 379.865,391.101 379.633,392.265 379.414 C 393.974 379.094,394.400 378.233,394.400 375.103 L 394.400 372.554 396.100 372.177 L 397.800 371.800 397.916 368.300 C 398.029 364.879,398.053 364.800,399.016 364.800 L 400.000 364.800 400.000 200.000 L 400.000 35.200 399.016 35.200 C 398.053 35.200,398.029 35.121,397.916 31.700 L 397.800 28.200 396.100 28.077 L 394.400 27.954 394.400 25.403 C 394.400 21.914,393.957 20.903,392.288 20.590 C 391.098 20.367,390.897 20.142,390.570 18.669 C 390.224 17.107,390.104 16.992,388.700 16.876 C 387.463 16.773,387.199 16.598,387.195 15.876 C 387.181 13.757,386.786 13.247,385.059 13.121 C 383.452 13.004,383.396 12.953,383.275 11.480 C 383.121 9.617,382.265 9.200,378.597 9.200 L 376.046 9.200 375.923 7.500 C 375.802 5.821,375.779 5.798,374.173 5.681 C 372.616 5.566,372.529 5.488,372.173 3.881 L 371.800 2.200 368.300 2.084 C 364.879 1.971,364.800 1.947,364.800 0.984 L 364.800 0.000 200.000 0.000 L 35.200 0.000 35.200 0.984 M224.918 137.663 C 225.394 137.918,225.998 138.341,226.260 138.603 C 226.522 138.865,227.786 139.194,229.068 139.333 C 233.008 139.763,234.411 140.311,234.859 141.598 C 235.110 142.318,235.696 142.895,236.441 143.154 C 237.232 143.430,237.852 144.075,238.312 145.101 C 238.690 145.944,239.630 147.197,240.400 147.884 L 241.800 149.134 241.800 200.335 L 241.800 251.536 240.830 252.568 C 240.297 253.135,239.622 253.600,239.330 253.600 C 239.039 253.600,238.800 253.876,238.800 254.214 C 238.800 255.226,237.635 256.688,236.432 257.187 C 235.812 257.444,235.107 258.087,234.866 258.616 C 234.281 259.898,232.411 260.620,228.925 260.910 C 227.174 261.056,225.616 261.433,224.942 261.875 C 223.251 262.983,102.535 263.008,101.311 261.901 C 100.887 261.516,99.743 261.044,98.770 260.851 C 96.289 260.360,94.801 259.708,94.795 259.110 C 94.792 258.829,94.178 258.204,93.430 257.719 C 92.682 257.235,91.559 256.159,90.935 255.328 C 90.311 254.498,89.350 253.425,88.800 252.944 L 87.800 252.069 87.693 201.219 C 87.579 147.532,87.556 148.251,89.467 147.000 C 90.140 146.560,90.984 145.525,91.344 144.700 C 91.703 143.875,92.181 143.200,92.406 143.200 C 92.630 143.200,93.434 142.562,94.191 141.782 C 95.594 140.337,98.679 139.200,101.196 139.200 C 101.832 139.200,102.475 139.002,102.624 138.760 C 103.575 137.222,103.193 137.232,164.726 137.216 C 208.933 137.204,224.273 137.318,224.918 137.663 M308.162 152.107 C 309.021 152.598,310.281 153.692,310.962 154.537 L 312.200 156.074 312.322 197.737 C 312.469 247.711,312.714 243.696,309.338 246.578 L 306.736 248.800 303.981 248.800 C 301.366 248.800,301.156 248.729,299.827 247.400 C 299.057 246.630,298.233 246.000,297.997 246.000 C 297.174 246.000,292.400 244.329,292.400 244.041 C 292.400 243.256,290.693 242.085,289.201 241.847 C 287.095 241.510,284.400 240.158,284.400 239.438 C 284.400 239.096,283.242 238.578,281.300 238.052 C 278.976 237.422,277.950 236.919,277.200 236.040 C 276.610 235.349,275.626 234.760,274.800 234.604 C 272.827 234.231,270.324 233.017,269.504 232.034 C 269.121 231.575,268.457 231.200,268.028 231.200 C 267.598 231.200,267.148 231.040,267.027 230.844 C 266.906 230.649,266.340 230.371,265.769 230.228 C 263.915 22Line truncated
stroke="none"
fill="#141c24"
fillRule="evenodd"
/>
</g>
</svg>
<span className="text-sm ltr:ml-2 rtl:mr-2">Cal.com Video</span>
</div>
)}
{location.type === LocationType.Zoom && (
<div className="flex flex-grow items-center">
<svg
className="h-6 w-6"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M32 0C49.6733 0 64 14.3267 64 32C64 49.6733 49.6733 64 32 64C14.3267 64 0 49.6733 0 32C0 14.3267 14.3267 0 32 0Z"
fill="#E5E5E4"
/>
<path
d="M32.0002 0.623047C49.3292 0.623047 63.3771 14.6709 63.3771 31.9999C63.3771 49.329 49.3292 63.3768 32.0002 63.3768C14.6711 63.3768 0.623291 49.329 0.623291 31.9999C0.623291 14.6709 14.6716 0.623047 32.0002 0.623047Z"
fill="white"
/>
<path
d="M31.9998 3.14014C47.9386 3.14014 60.8597 16.0612 60.8597 32C60.8597 47.9389 47.9386 60.8599 31.9998 60.8599C16.0609 60.8599 3.13989 47.9389 3.13989 32C3.13989 16.0612 16.0609 3.14014 31.9998 3.14014Z"
fill="#4A8CFF"
/>
<path
d="M13.1711 22.9581V36.5206C13.1832 39.5875 15.6881 42.0558 18.743 42.0433H38.5125C39.0744 42.0433 39.5266 41.5911 39.5266 41.0412V27.4788C39.5145 24.4119 37.0096 21.9435 33.9552 21.956H14.1857C13.6238 21.956 13.1716 22.4082 13.1716 22.9581H13.1711ZM40.7848 28.2487L48.9469 22.2864C49.6557 21.6998 50.2051 21.8462 50.2051 22.9095V41.0903C50.2051 42.2999 49.5329 42.1536 48.9469 41.7134L40.7848 35.7631V28.2487Z"
fill="white"
/>
</svg>
<span className="text-sm ltr:ml-2 rtl:mr-2">Zoom Video</span>
</div>
)}
{location.type === LocationType.Tandem && (
<div className="flex flex-grow items-center">
<svg
width="1.25em"
height="1.25em"
viewBox="0 0 400 400"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M167.928 256.163L64 324V143.835L167.928 76V256.163Z"
fill="#4341DC"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M335.755 256.163L231.827 324V143.835L335.755 76V256.163Z"
fill="#00B6B6"
/>
</svg>
<span className="ml-2 text-sm">Tandem Video</span>
</div>
)}
{location.type === LocationType.Jitsi && (
<div className="flex flex-grow items-center">
<svg
className="h-6 w-6"
viewBox="0 0 60 60"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M45.4181 23.2445C45.0316 22.4169 44.4086 21.7223 43.6277 21.2487C42.6993 20.6602 41.5397 20.5354 40.7313 20.5354C40.4591 20.5354 40.1871 20.5495 39.9163 20.5776H39.8961C39.9105 20.4663 39.9393 20.3223 39.9633 20.2051C40.0037 19.9997 40.0497 19.7664 40.0757 19.5235C40.1832 18.4819 39.8126 17.0803 39.1301 15.9543C39.0216 15.7747 39.0254 15.7133 39.0254 15.7133C39.0596 15.6624 39.0995 15.6157 39.1445 15.5741L39.2069 15.5088C40.8206 13.7808 41.3083 11.881 40.6545 9.85058C39.4152 6.00002 39.083 6.00002 38.8401 6.00002C38.786 5.99948 38.7323 6.01041 38.6827 6.03209C38.633 6.05376 38.5886 6.08569 38.5521 6.12578C38.5118 6.1746 38.4823 6.23142 38.4656 6.29249C38.4489 6.35356 38.4454 6.41747 38.4552 6.48002C38.5809 7.75682 38.2929 9.58466 38.1009 10.2807C37.8657 11.1562 37.0449 12.4618 34.4529 13.7031C34.2709 13.7806 34.0851 13.8489 33.8961 13.9075C33.0504 14.1907 31.6373 14.664 30.852 15.745C30.2597 16.4391 30.1166 17.1341 29.8603 18.3888C29.6318 19.5043 29.4475 20.8627 29.8085 22.4842C29.8449 22.6445 29.8795 22.7789 29.9112 22.8989C29.9333 22.9843 29.9525 23.0583 29.9669 23.1226L29.9976 23.1159L29.9659 23.1245C29.9899 23.2205 29.9745 23.2435 29.94 23.2675C29.8153 23.3169 29.6869 23.3564 29.556 23.3856C29.172 23.4471 28.788 23.5152 28.4213 23.5872C27.2539 23.7879 23.7249 24.3965 22.0997 26.9837C21.0965 28.5792 20.9544 30.6634 21.6753 33.1853C22.4232 35.6813 23.9073 37.3968 24.923 37.7462L24.9393 37.752C25.0123 37.7846 25.0824 37.8125 25.1505 37.8355C25.1505 37.9056 25.1438 37.991 25.1352 38.087C25.1281 38.1293 25.1214 38.1754 25.115 38.2253C25.0037 39.0394 24.6197 39.9168 24.228 39.9398C24.0216 39.8995 23.2037 39.4358 22.6248 39.0557C22.4392 38.9347 22.2632 38.8189 22.0968 38.7082C20.4811 37.6387 19.5902 37.0502 18.2289 36.8986C18.1824 36.8936 18.1356 36.891 18.0888 36.8909C16.6488 36.8909 14.0568 39.3158 14.0021 44.1485C13.9742 46.5696 14.2277 48.7824 14.7576 50.7264C15.1358 52.1203 15.539 52.9142 15.6561 53.1264L16.1227 54L16.3358 53.04C17.459 47.952 18.8453 46.7261 21.1521 45.2592C22.8427 46.8432 25.0632 47.6784 27.5918 47.6784C29.723 47.6784 31.9531 47.0592 33.8693 45.9341C35.7518 44.8301 37.2053 43.3421 38.0933 41.6227C38.2411 41.7187 38.4197 41.8435 38.5377 41.9261C38.9601 42.2218 39.0494 42.2842 39.2001 42.2842H39.2136C39.3787 42.2774 40.9358 41.7552 42.5304 39.8074C43.4568 38.6755 44.2209 37.2758 44.8008 35.6448C45.5189 33.6288 45.9528 31.248 46.1006 28.5706C46.2638 26.3223 46.0353 24.5319 45.4219 23.2493L45.4181 23.2445ZM39.4449 10.3027C39.5788 10.8426 39.5962 11.4049 39.4958 11.952C39.3461 13.0157 38.8949 13.968 38.1249 14.8368C38.7811 13.3969 39.2257 11.8698 39.4449 10.3027V10.3027ZM38.3313 16.9047C38.5659 17.2594 38.7449 17.6479 38.8622 18.0567C38.9924 18.5927 38.9691 19.1546 38.795 19.6781C38.5195 20.3338 38.1499 20.665 37.6958 20.665H37.6353C37.4753 20.6409 37.3219 20.5847 37.1841 20.4999C36.6005 20.1581 36.2846 19.2519 36.5121 18.5875C36.5217 18.5683 36.5304 18.5482 36.539 18.5271C36.5765 18.4311 36.683 18.2467 36.9787 17.9616C37.4011 17.5882 38.0184 17.1168 38.3294 16.9056L38.3313 16.9047ZM31.5729 16.7242L31.5787 16.7136V16.704C31.7035 16.3968 32.5627 15.8995 33.1329 15.5703C33.204 15.528 33.275 15.4867 33.3461 15.4464C33.5958 15.3153 33.8522 15.1971 34.1141 15.0922C35.052 14.6928 36.5141 14.0707 37.7179 12.912C37.3685 14.137 36.5275 16.2 35.1451 17.449C34.8379 17.7264 34.2177 17.9722 33.4987 18.2563C32.724 18.5616 31.7928 18.9283 30.8894 19.4823C30.9595 18.7757 31.1774 17.5431 31.5681 16.7242H31.5729ZM30.9729 20.7159C31.4376 20.3319 32.2881 19.8115 35.075 18.8919C35.0942 18.9783 35.1163 19.0896 35.1374 19.2C35.1547 19.2835 35.1729 19.3786 35.1941 19.4823C35.3765 20.3827 35.5781 21.3946 37.4529 21.9562C37.2782 22.3402 36.9192 22.9891 36.7944 23.113L36.7531 23.1485L36.7243 23.1955C36.3595 23.7811 34.3877 25.0589 33.5045 25.0589C33.4665 25.0593 33.4286 25.0564 33.3912 25.0503C32.8593 24.9475 31.6382 23.8627 31.2792 23.0611C30.5851 21.4877 30.7272 20.9203 30.9681 20.7197L30.9729 20.7159ZM24.0293 35.1581C23.0078 33.6384 22.2341 30.983 22.7409 28.848V28.8384C22.9857 27.6768 23.7969 26.9933 24.132 26.7533L24.18 26.7178C24.6773 26.2839 25.6949 25.8259 27.0465 25.4256C27.2385 25.3757 27.3441 25.3536 27.3489 25.3536C27.5976 25.3018 27.8693 25.2394 28.1333 25.1789C28.6328 25.0513 29.1392 24.9523 29.6501 24.8823C29.7768 24.8688 29.9054 24.8448 30.0283 24.8208C30.1944 24.7844 30.3634 24.7625 30.5333 24.7555C30.6688 24.7483 30.8031 24.7839 30.9173 24.8573C31.4184 25.4525 32.6453 26.6275 33.4497 26.6967H33.4737H33.5006C34.4165 26.6477 35.8517 25.7539 37.7678 24.0384C38.0933 23.7504 38.3957 23.4989 38.6923 23.2791L38.7288 23.2512C39.2517 22.8546 39.8275 22.533 40.4395 22.296C40.2389 22.9527 40.0795 24.0749 40.7275 25.656C41.0645 26.4739 41.7115 28.3171 42.2481 30.1219C43.1717 33.2304 43.043 33.7776 43.0248 33.8285C42.9992 33.9341 42.9574 34.0351 42.9009 34.128C42.8289 34.1141 42.7593 34.0895 42.6945 34.055C42.2289 33.84 41.7067 33.456 41.0961 33.0048C39.9633 32.1744 38.555 31.1414 36.Line truncated
fill="#727272"
/>
</svg>
<span className="ml-2 text-sm">Jitsi Meet</span>
</div>
)}
{location.type === LocationType.Teams && (
<div className="flex flex-grow items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
viewBox="0 0 2228.833 2073.333">
<path
fill="#5059C9"
d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"
/>
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25" />
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917" />
<path
fill="#7B83EB"
d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"
/>
<path
opacity=".1"
d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"
/>
<path
opacity=".2"
d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
/>
<path
opacity=".2"
d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
/>
<path
opacity=".2"
d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"
/>
<path
opacity=".1"
d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"
/>
<path
opacity=".2"
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
/>
<path
opacity=".2"
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
/>
<path
opacity=".2"
d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"
/>
<linearGradient
id="a"
gradientUnits="userSpaceOnUse"
x1="198.099"
y1="1683.0726"
x2="942.2344"
y2="394.2607"
gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
<stop offset="0" stopColor="#5a62c3" />
<stop offset=".5" stopColor="#4d55bd" />
<stop offset="1" stopColor="#3940ab" />
</linearGradient>
<path
fill="url(#a)"
d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"
/>
<path
fill="#FFF"
d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"
/>
</svg>
<span className="ml-2 text-sm">MS Teams</span>
</div>
)}
<div className="flex">
<button
type="button"
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
openLocationModal(location.type);
}}
aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
<Icon.FiEdit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
</button>
</div>
</div>
</li>
))}
</li>
);
})}
{formMethods.getValues("locations").length > 0 &&
formMethods.getValues("locations").length !== locationOptions.length && (
<li>
+21 -13
View File
@@ -1,3 +1,4 @@
import { Prisma } from "@prisma/client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { createEvent } from "ics";
@@ -9,6 +10,8 @@ import { useEffect, useRef, useState } from "react";
import { RRule } from "rrule";
import { z } from "zod";
import { getEventLocationValue, getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import { getEventName } from "@calcom/core/event";
import dayjs from "@calcom/dayjs";
import {
sdkActionManager,
@@ -26,13 +29,11 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import { isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { EmailInput } from "@calcom/ui/form/fields";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { getEventName } from "@lib/event";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -140,7 +141,15 @@ export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
const { location: _location, name, reschedule, listingStatus, status, isSuccessBookingPage } = router.query;
const location = Array.isArray(_location) ? _location[0] : _location;
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(_location)
? _location[0] || ""
: _location || "";
if (!location) {
// Can't use logger.error because it throws error on client. stdout isn't available to it.
console.error(`No location found `);
}
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const { data: session } = useSession();
@@ -155,16 +164,12 @@ export default function Success(props: SuccessProps) {
const attendeeName = typeof name === "string" ? name : "Nameless";
const locationFromEventType = !!eventType.locations
? (eventType.locations as Array<{ type: string }>)[0]
: "";
const locationType = !!locationFromEventType ? locationFromEventType.type : "";
const eventNameObject = {
attendeeName,
eventType: props.eventType.title,
eventName: (props.dynamicEventName as string) || props.eventType.eventName,
host: props.profile.name || "Nameless",
location: locationType,
location: location,
t,
};
const metadata = props.eventType?.metadata as { giphyThankYouPage: string };
@@ -252,6 +257,9 @@ export default function Success(props: SuccessProps) {
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
);
const customInputs = bookingInfo?.customInputs;
const locationToDisplay = getSuccessPageLocationMessage(location, t);
return (
<div className={isEmbed ? "" : "h-screen bg-neutral-100 dark:bg-neutral-900"} data-testid="success-page">
{userIsOwner && !isEmbed && (
@@ -355,16 +363,16 @@ export default function Success(props: SuccessProps) {
</div>
</>
)}
{location && (
{locationToDisplay && (
<>
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3">
{location.startsWith("http") ? (
<a title="Meeting Link" href={location}>
{location}
{locationToDisplay.startsWith("http") ? (
<a title="Meeting Link" href={locationToDisplay}>
{locationToDisplay}
</a>
) : (
location
locationToDisplay
)}
</div>
</>
+3 -3
View File
@@ -2,13 +2,13 @@ import { UserPlan } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
import getBooking, { GetBookingType } from "@lib/getBooking";
import { locationHiddenFilter, LocationObject } from "@lib/location";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
@@ -116,13 +116,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
eventType.schedule = null;
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
console.log("locations", locations);
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
periodStartDate: eventType.periodStartDate?.toString() ?? null,
periodEndDate: eventType.periodEndDate?.toString() ?? null,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: eventType.users.map((user) => ({
name: user.name,
username: user.username,
+4 -3
View File
@@ -1,7 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
@@ -16,9 +16,8 @@ export type TeamBookingPageProps = inferSSRProps<typeof getServerSideProps>;
export default function TeamBookingPage(props: TeamBookingPageProps) {
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return <BookingPage {...props} locationLabels={locationLabels} />;
return <BookingPage {...props} />;
}
TeamBookingPage.isThemeSupported = true;
@@ -88,6 +87,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
//TODO: Use zodSchema to verify it instead of using Type Assertion
locations: privacyFilteredLocations(eventTypeRaw.locations as LocationObject[]),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
@@ -8,6 +8,7 @@ import { z } from "zod";
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { LocationObject, EventLocationType } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -20,7 +21,6 @@ import { Button, showToast } from "@calcom/ui/v2";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { LocationObject, LocationType } from "@lib/location";
import { inferSSRProps } from "@lib/types/inferSSRProps";
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
@@ -52,7 +52,7 @@ export type FormValues = {
hideCalendarNotes: boolean;
hashedLink: string | undefined;
locations: {
type: LocationType;
type: EventLocationType["type"];
address?: string;
link?: string;
hostPhoneNumber?: string;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>

After

Width:  |  Height:  |  Size: 409 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-map-pin"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>

After

Width:  |  Height:  |  Size: 322 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>

After

Width:  |  Height:  |  Size: 520 B

@@ -453,7 +453,7 @@
"booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}",
"in_person_meeting": "In-person meeting",
"link_meeting": "Link meeting",
"phone_call": "Phone call",
"phone_call": "Attendee Phone Number",
"your_number": "Your phone number",
"phone_number": "Phone Number",
"attendee_phone_number": "Attendee Phone Number",
+1 -4
View File
@@ -21,10 +21,7 @@ If we rename all existing apps to their slug names, we can remove type and then
- Edit command Improvements
- Prefill fields in edit command -> It allows only that content to change which user wants to change.
- Don't override icon.svg
- For Video Apps
- Update app-store/locations.ts
- \_metadata.ts should have locationType and locationLabel props.
- Merge app-store:watch and app-store commands, introduce app-store --watch
- Merge app-store:watch and app-store commands; introduce app-store --watch
- Allow inputs in non interactive way as well - That would allow easily copy pasting commands.
- An app created through CLI should be able to completely skip API validation for testing purposes. Credentials should be created with no API specified specific to the app. It would allow us to test any app end to end not worrying about the corresponding API endpoint.
- Require assets path relative to app dir.
+119 -60
View File
@@ -46,6 +46,7 @@ const updatePackageJson = ({ slug, appDescription, appDirPath }) => {
const BaseAppFork = {
create: function* ({
category,
subCategory,
editMode = false,
appDescription,
appName,
@@ -66,6 +67,31 @@ const BaseAppFork = {
video: "conferencing",
};
const dataFromCategory =
category === "video"
? {
appData: {
location: {
type: `integrations:${slug}_video`,
label: `${appName}`,
},
},
}
: {};
const dataFromSubCategory =
category === "video" && subCategory === "static"
? {
appData: {
...dataFromCategory.appData,
location: {
...dataFromCategory.appData.location,
linkType: "static",
organizerInputPlaceholder: "https://anything.anything",
urlRegExp: "",
},
},
}
: {};
let config = {
"/*": "Don't modify slug - If required, do it using cli edit command",
name: appName,
@@ -82,6 +108,8 @@ const BaseAppFork = {
description: appDescription,
// TODO: Use this to avoid edit and delete on the apps created outside of cli
__createdUsingCli: true,
...dataFromCategory,
...dataFromSubCategory,
};
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
config = {
@@ -93,6 +121,10 @@ const BaseAppFork = {
`${appDirPath}/README.mdx`,
fs.readFileSync(`${appDirPath}/README.mdx`).toString().replace("_DESCRIPTION_", appDescription)
);
fs.writeFileSync(
`${appDirPath}/README.mdx`,
fs.readFileSync(`${appDirPath}/README.mdx`).toString().replace("_DESCRIPTION_", appDescription)
);
message = !editMode ? "Forked base app" : "Updated app";
yield message;
},
@@ -122,6 +154,10 @@ const Seed = {
});
}
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
@@ -151,41 +187,65 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
const [appInputData, setAppInputData] = useState({});
const [inputIndex, setInputIndex] = useState(0);
const fields = [
{ label: "App Title", name: "appName", type: "text" },
{ label: "App Description", name: "appDescription", type: "text" },
{ label: "App Title", name: "appName", type: "text", explainer: "Keep it very short" },
{
label: "App Description",
name: "appDescription",
type: "text",
explainer:
"A detailed description of your app. You can later modify README.mdx to add slider and other components",
},
{
label: "Category of App",
name: "appCategory",
type: "select",
options: [
{ label: "calendar", value: "calendar" },
{ label: "video", value: "video" },
{ label: "payment", value: "payment" },
{ label: "messaging", value: "messaging" },
{ label: "web3", value: "web3" },
{ label: "other", value: "other" },
{ label: "Calendar", value: "calendar" },
{
label: "Static Link - Video",
value: "video_static",
explainer:
"Apps like Ping.gg/Riverside/Whereby which require you to provide a link to join your room",
},
{ label: "Other - Video", value: "video_other" },
{ label: "Payment", value: "payment" },
{ label: "Messaging", value: "messaging" },
{ label: "Web3", value: "web3" },
{ label: "Other", value: "other" },
],
explainer: "This is how apps are categorized in App Store.",
},
{ label: "Publisher Name", name: "publisherName", type: "text", explainer: "Let users know who you are" },
{
label: "Publisher Email",
name: "publisherEmail",
type: "text",
explainer: "Let users know how they can contact you.",
},
{ label: "Publisher Name", name: "publisherName", type: "text" },
{ label: "Publisher Email", name: "publisherEmail", type: "text" },
];
const field = fields[inputIndex];
const fieldLabel = field?.label || "";
const fieldName = field?.name || "";
const fieldValue = appInputData[fieldName] || "";
const appName = appInputData["appName"];
const category = appInputData["appCategory"];
const rawCategory = appInputData["appCategory"] || "";
const appDescription = appInputData["appDescription"];
const publisherName = appInputData["publisherName"];
const publisherEmail = appInputData["publisherEmail"];
const [status, setStatus] = useState<"inProgress" | "done">("inProgress");
const allFieldsFilled = inputIndex === fields.length;
const [progressUpdate, setProgressUpdate] = useState("");
const category = rawCategory.split("_")[0];
const subCategory = rawCategory.split("_")[1];
if (!editMode) {
slug = getSlugFromAppName(appName);
}
useEffect(() => {
// When all fields have been filled
if (allFieldsFilled) {
const it = BaseAppFork.create({
category,
subCategory,
appDescription,
appName,
slug,
@@ -210,10 +270,6 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
return <Text>--slug is required</Text>;
}
if (!editMode) {
slug = getSlugFromAppName(appName);
}
if (allFieldsFilled) {
return (
<Box flexDirection="column">
@@ -226,8 +282,9 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
{status === "done" ? (
<Box flexDirection="column" paddingTop={2} paddingBottom={2}>
<Text bold italic>
Just wait for few seconds to process to exit and you are good to go. Your App code exists at $
{getAppDirPath(slug)}
Just wait for a few seconds for process to exit and then you are good to go. Your App code
exists at ${getAppDirPath(slug)}
Tip: Go and change the logo of your app by replacing {getAppDirPath(slug) + "/static/icon.svg"}
</Text>
<Text bold italic>
App Summary:
@@ -284,49 +341,51 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
}
return (
<Box flexDirection="column">
<Box>
<Text color="green">{`${fieldLabel}:`}</Text>
{field.type == "text" ? (
<TextInput
value={fieldValue}
onSubmit={(value) => {
if (!value) {
return;
}
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
) : (
<SelectInput<string>
items={field.options}
onSelect={(item) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: item.value,
};
});
setInputIndex((index) => {
return index + 1;
});
}}
/>
)}
</Box>
<Box>
<Text color="gray" italic>
{field.explainer}
</Text>
<Box flexDirection="column">
<Box>
<Text color="green">{`${fieldLabel}:`}</Text>
{field.type == "text" ? (
<TextInput
value={fieldValue}
onSubmit={(value) => {
if (!value) {
return;
}
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
) : (
<SelectInput<string>
items={field.options}
onSelect={(item) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: item.value,
};
});
setInputIndex((index) => {
return index + 1;
});
}}
/>
)}
</Box>
<Box>
<Text color="gray" italic>
{field.explainer}
</Text>
</Box>
</Box>
</Box>
);
+1
View File
@@ -133,6 +133,7 @@ function generateFiles() {
` ${app.name}: dynamic(() =>import("./${app.path}/components/InstallAppButton")),`,
})
);
const banner = `/**
This file is autogenerated using the command \`yarn app-store:build --watch\`.
Don't modify this file manually.
+8 -1
View File
@@ -32,8 +32,15 @@ export async function getAppRegistry() {
// Skip if app isn't installed
/* This is now handled from the DB */
// if (!app.installed) return apps;
const { rating, reviews, trending, verified, ...remainingAppProps } = app;
apps.push({
...app,
rating: rating || 0,
reviews: reviews || 0,
trending: trending || true,
verified: verified || true,
...remainingAppProps,
category: app.category || "other",
installed:
true /* All apps from DB are considered installed by default. @TODO: Add and filter our by `enabled` property */,
});
+2 -8
View File
@@ -1,16 +1,10 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,
trending: true,
verified: true,
...config,
} as App;
} as AppMeta;
export default metadata;
+2 -2
View File
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -21,6 +21,6 @@ export const metadata = {
variant: "conferencing",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -20,6 +20,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
+11 -11
View File
@@ -1,19 +1,19 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Around,
locationLabel: "Around Video",
appData: {
location: {
linkType: "static",
type: "integrations:around_video",
label: "Around Video",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?around.co\\/[a-zA-Z0-9]*",
organizerInputPlaceholder: "https://www.around.co/rick",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -20,6 +20,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
+9 -5
View File
@@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@@ -22,9 +21,14 @@ export const metadata = {
title: "Cal Video",
isGlobal: true,
email: "help@cal.com",
locationType: LocationType.Daily,
locationLabel: "Cal Video",
appData: {
location: {
linkType: "dynamic",
type: "integrations:daily",
label: "Cal Video",
},
},
key: { apikey: process.env.DAILY_API_KEY },
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@@ -13,6 +13,6 @@ export const metadata = {
licenseRequired: true,
isProOnly: true,
...config,
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -21,6 +21,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -21,6 +21,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
+2 -2
View File
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -21,6 +21,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -1,7 +1,6 @@
import { validJson } from "@calcom/lib/jsonUtils";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@@ -22,6 +21,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
+9 -5
View File
@@ -1,7 +1,6 @@
import { validJson } from "@calcom/lib/jsonUtils";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@@ -23,8 +22,13 @@ export const metadata = {
verified: true,
isGlobal: true,
email: "help@cal.com",
locationType: LocationType.GoogleMeet,
locationLabel: "Google Meet",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:google:meet",
label: "Google Meet",
},
},
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -21,6 +21,6 @@ export const metadata = {
title: "HubSpot CRM",
trending: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -1,7 +1,6 @@
import { randomString } from "@calcom/lib/random";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@@ -23,9 +22,14 @@ export const metadata = {
trending: true,
isGlobal: false,
email: "support@huddle01.com",
locationType: LocationType.Huddle01,
locationLabel: "Huddle01 Video",
appData: {
location: {
linkType: "dynamic",
type: "integrations:huddle01",
label: "Huddle01 Video",
},
},
key: { apikey: randomString(12) },
} as App;
} as AppMeta;
export default metadata;
+9 -5
View File
@@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@@ -22,8 +21,13 @@ export const metadata = {
trending: true,
isGlobal: false,
email: "help@cal.com",
locationType: LocationType.Jitsi,
locationLabel: "Jitsi Video",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:jitsi",
label: "Jitsi Video",
},
},
} as AppMeta;
export default metadata;
+2 -2
View File
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -20,6 +20,6 @@ export const metadata = {
url: "https://larksuite.com/",
verified: true,
email: "alan@larksuite.com",
} as App;
} as AppMeta;
export default metadata;
+302 -54
View File
@@ -1,51 +1,197 @@
import { BookingStatus } from "@prisma/client";
import type { TFunction } from "next-i18next";
export enum DefaultLocationType {
import logger from "@calcom/lib/logger";
import { Ensure, Optional } from "@calcom/types/utils";
import type { EventLocationTypeFromAppMeta } from "../types/App";
import { appStoreMetadata } from "./apps.browser.generated";
export type DefaultEventLocationType = {
default: true;
type: DefaultEventLocationTypeEnum;
label: string;
messageForOrganizer: string;
iconUrl: string;
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
variable: "locationType" | "locationAddress" | "locationLink" | "locationPhoneNumber" | "phone";
defaultValueVariable: "address" | "link" | "hostPhoneNumber" | "phone";
} & (
| {
organizerInputType: "phone" | "text" | null;
organizerInputPlaceholder?: string | null;
attendeeInputType?: null;
attendeeInputPlaceholder?: null;
}
| {
attendeeInputType: "phone" | "text" | null;
attendeeInputPlaceholder: string;
organizerInputType?: null;
organizerInputPlaceholder?: null;
}
);
type EventLocationTypeFromApp = Ensure<EventLocationTypeFromAppMeta, "defaultValueVariable" | "variable">;
export type EventLocationType = DefaultEventLocationType | EventLocationTypeFromApp;
export const DailyLocationType = "integrations:daily";
export enum DefaultEventLocationTypeEnum {
InPerson = "inPerson",
/**
* Booker Phone
*/
Phone = "phone",
/**
* Organizer Phone
*/
UserPhone = "userPhone",
Link = "link",
}
/** If your App has a location option, add it here */
export enum AppStoreLocationType {
GoogleMeet = "integrations:google:meet",
Zoom = "integrations:zoom",
Daily = "integrations:daily",
Jitsi = "integrations:jitsi",
Huddle01 = "integrations:huddle01",
Tandem = "integrations:tandem",
Teams = "integrations:office365_video",
Whereby = "integrations:whereby_video",
Around = "integrations:around_video",
Riverside = "integrations:riverside_video",
Ping = "integrations:ping_video",
}
export const defaultLocations: DefaultEventLocationType[] = [
{
default: true,
type: DefaultEventLocationTypeEnum.InPerson,
label: "In Person",
organizerInputType: "text",
messageForOrganizer: "Provide an Address or Place",
// HACK:
variable: "locationAddress",
defaultValueVariable: "address",
iconUrl: "/map-pin.svg",
},
{
default: true,
type: DefaultEventLocationTypeEnum.Link,
label: "Link",
organizerInputType: "text",
variable: "locationLink",
messageForOrganizer: "Provide a Meeting Link",
defaultValueVariable: "link",
iconUrl: "/globe.svg",
},
{
default: true,
type: DefaultEventLocationTypeEnum.Phone,
label: "Attendee Phone Number",
variable: "phone",
organizerInputType: null,
attendeeInputType: "phone",
attendeeInputPlaceholder: `enter_phone_number`,
defaultValueVariable: "phone",
messageForOrganizer: "Cal will ask your invitee to enter a phone number before scheduling.",
// This isn't inputType phone because organizer doesn't need to provide it.
// inputType: "phone"
iconUrl: "/phone.svg",
},
{
default: true,
type: DefaultEventLocationTypeEnum.UserPhone,
label: "Organizer Phone Number",
messageForOrganizer: "Provide your phone number",
organizerInputType: "phone",
variable: "locationPhoneNumber",
defaultValueVariable: "hostPhoneNumber",
iconUrl: "/phone.svg",
},
];
export type LocationObject = {
type: LocationType;
address?: string;
link?: string;
type: string;
displayLocationPublicly?: boolean;
hostPhoneNumber?: string;
} & Partial<Record<"address" | "link" | "hostPhoneNumber" | "phone", string>>;
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
export type BookingLocationValue = string;
export const AppStoreLocationType: Record<string, string> = {};
const locationsFromApps: EventLocationTypeFromApp[] = [];
for (const [appName, meta] of Object.entries(appStoreMetadata)) {
const location = meta.appData?.location;
if (location) {
const newLocation = {
...location,
messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`,
iconUrl: meta.logo,
// For All event location apps, locationLink is where we store the input
// TODO: locationLink and link seems redundant. We can modify the code to keep just one of them.
variable: location.variable || "locationLink",
defaultValueVariable: location.defaultValueVariable || "link",
};
// Static links always require organizer to input
if (newLocation.linkType === "static") {
newLocation.organizerInputType = location.organizerInputType || "text";
if (newLocation.organizerInputPlaceholder?.match(/https?:\/\//)) {
// HACK: Translation ends up removing https? if it's in the beginning :(
newLocation.organizerInputPlaceholder = ` ${newLocation.organizerInputPlaceholder}`;
}
} else {
newLocation.organizerInputType = null;
}
AppStoreLocationType[appName] = newLocation.type;
locationsFromApps.push({
...newLocation,
});
}
}
const locationsTypes = [...defaultLocations, ...locationsFromApps];
export const getStaticLinkBasedLocation = (locationType: string) =>
locationsFromApps.find((l) => l.linkType === "static" && l.type === locationType);
export const getEventLocationTypeFromApp = (locationType: string) =>
locationsFromApps.find((l) => l.type === locationType);
export const getEventLocationType = (locationType: string | undefined | null) =>
locationsTypes.find((l) => l.type === locationType);
export const getEventLocationTypeFromValue = (value: string | undefined | null) => {
if (!value) {
return null;
}
return locationsTypes.find((l) => {
if (l.default || l.linkType == "dynamic" || !l.urlRegExp) {
return;
}
return new RegExp(l.urlRegExp).test(value);
});
};
export const LocationType = { ...DefaultLocationType, ...AppStoreLocationType };
export type LocationType = DefaultLocationType | AppStoreLocationType;
export const guessEventLocationType = (locationTypeOrValue: string | undefined | null) =>
getEventLocationType(locationTypeOrValue) || getEventLocationTypeFromValue(locationTypeOrValue);
export const locationHiddenFilter = (locations: LocationObject[]) =>
locations.filter((el) => {
export const LocationType = { ...DefaultEventLocationTypeEnum, ...AppStoreLocationType };
type PrivacyFilteredLocationObject = Optional<LocationObject, "address" | "link">;
export const privacyFilteredLocations = (locations: LocationObject[]): PrivacyFilteredLocationObject[] => {
const locationsAfterPrivacyFilter = locations.map((location) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
logger.debug(`Couldn't find location type. App might be uninstalled: ${location.type} `);
}
// 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;
if (location.displayLocationPublicly || !eventLocationType) {
return location;
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { address: _1, link: _2, hostPhoneNumber: _3, ...privacyFilteredLocation } = location;
logger.debug("Applied Privacy Filter", location, privacyFilteredLocation);
return privacyFilteredLocation;
}
});
return locationsAfterPrivacyFilter;
};
/**
* Use this function for translating event location to a readable string
@@ -53,29 +199,131 @@ export const locationHiddenFilter = (locations: LocationObject[]) =>
* @param t
* @returns string
*/
export const LocationOptionsToString = (location: string, t: TFunction) => {
switch (location) {
case LocationType.InPerson:
return t("set_address_place");
case LocationType.Link:
return t("set_link_meeting");
case LocationType.Phone:
return t("cal_invitee_phone_number_scheduling");
case LocationType.GoogleMeet:
return t("cal_provide_google_meet_location");
case LocationType.Zoom:
return t("cal_provide_zoom_meeting_url");
case LocationType.Daily:
return t("cal_provide_video_meeting_url");
case LocationType.Jitsi:
return t("cal_provide_jitsi_meeting_url");
case LocationType.Huddle01:
return t("cal_provide_huddle01_meeting_url");
case LocationType.Tandem:
return t("cal_provide_tandem_meeting_url");
case LocationType.Teams:
return t("cal_provide_teams_meeting_url");
default:
return null;
export const getMessageForOrganizer = (location: string, t: TFunction) => {
const videoLocation = getEventLocationTypeFromApp(location);
const defaultLocation = defaultLocations.find((l) => l.type === location);
if (defaultLocation) {
return t(defaultLocation.messageForOrganizer);
}
if (videoLocation && videoLocation.linkType !== "static") {
return t(`Cal will provide a ${videoLocation.label} URL.`);
}
return "";
};
/**
* Use this function to translate booking location value to a readable string
* @param linkValue
* @param translationFunction
* @returns
*/
export const getHumanReadableLocationValue = (
linkValue: string | undefined | null,
translationFunction: TFunction
): string => {
if (!linkValue) {
return translationFunction("no_location");
}
// Just in case linkValue is a `locationType.type`(for old bookings)
const eventLocationType = getEventLocationType(linkValue);
if (eventLocationType) {
// If we can find a video location based on linkValue then it means that the linkValue is something like integrations:google-meet and in that case we don't have the meeting URL to show.
// Show a generic message in that case.
return `${eventLocationType.label}`;
}
// Otherwise just show the available link value which can be a Phone number, a URL or a physical address of a place.
return linkValue || "";
};
export const locationKeyToString = (location: LocationObject) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
return null;
}
const defaultValueVariable = eventLocationType.defaultValueVariable;
if (!defaultValueVariable) {
console.error(`defaultValueVariable not set for ${location.type}`);
return "";
}
return location[defaultValueVariable] || eventLocationType.label;
};
export const getEventLocationWithType = (
locations: LocationObject[],
locationType: EventLocationType["type"] | undefined
) => {
const location = locations.find((location) => location.type === locationType);
return location;
};
// FIXME: It assumes that type would be sent mostly now. If just in case a value and not type is sent(when old frontend sends requests to new backend), below forEach won't be able to find a match and thus bookingLocation would still be correct equal to reqBody.location
// We must handle the situation where frontend doesn't send us the value because it doesn't have it(displayLocationPublicly not set)
// But we want to store the actual location(except dynamic URL based location type) so that Emails, Calendars pick the value only.
// TODO: We must store both type as well as value so that we know the type of data that we are having. Is it an address or a phone number? This is to be done post v2.0
export const getLocationValueForDB = (
bookingLocationTypeOrValue: EventLocationType["type"],
eventLocations: LocationObject[]
) => {
let bookingLocation = bookingLocationTypeOrValue;
eventLocations.forEach((location) => {
if (location.type === bookingLocationTypeOrValue) {
const eventLocationType = getEventLocationType(bookingLocationTypeOrValue);
if (!eventLocationType) {
return;
}
if (!eventLocationType.default && eventLocationType.linkType === "dynamic") {
// Dynamic link based locations should still be saved as type. The beyond logic generates meeting URL based on the type.
// This difference can be avoided when we start storing both type and value of a location
return;
}
bookingLocation = location[eventLocationType.defaultValueVariable] || bookingLocation;
}
});
return bookingLocation;
};
export const getEventLocationValue = (eventLocations: LocationObject[], bookingLocation: LocationObject) => {
const eventLocationType = getEventLocationType(bookingLocation?.type);
if (!eventLocationType) {
return "";
}
const defaultValueVariable = eventLocationType.defaultValueVariable;
if (!defaultValueVariable) {
console.error(`${defaultValueVariable} not set for ${bookingLocation.type}`);
return "";
}
const eventLocation = getEventLocationWithType(eventLocations, bookingLocation?.type);
if (!eventLocation) {
console.error(`Could not find eventLocation for ${bookingLocation}`);
return "";
}
// Must send .type here if value isn't available due to privacy setting.
// For Booker Phone Number, it would be a value always. For others, value is either autogenerated or provided by Organizer and thus it's possible that organizer doesn't want it to show
// Backend checks for `integration` to generate link
// TODO: use zodSchema to ensure the type of data is correct
return (
bookingLocation[defaultValueVariable] || eventLocation[defaultValueVariable] || eventLocationType.type
);
};
export function getSuccessPageLocationMessage(location: EventLocationType["type"], t: TFunction) {
const eventLocationType = getEventLocationType(location);
let locationToDisplay = location;
if (eventLocationType && !eventLocationType.default && eventLocationType.linkType === "dynamic") {
const isConfirmed = status === BookingStatus.ACCEPTED;
if (status === BookingStatus.CANCELLED || status === BookingStatus.REJECTED) {
locationToDisplay == t("web_conference");
} else if (isConfirmed) {
locationToDisplay =
getHumanReadableLocationValue(location, t) + ": " + t("meeting_url_in_conformation_email");
} else {
locationToDisplay = t("web_conferencing_details_to_follow");
}
}
return locationToDisplay;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -21,6 +21,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -19,6 +19,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@@ -20,8 +19,13 @@ export const metadata = {
title: "MS Teams (Requires work/school account)",
trending: true,
email: "help@cal.com",
locationType: LocationType.Teams,
locationLabel: "MS Teams",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:office365_video",
label: "MS Teams",
},
},
} as AppMeta;
export default metadata;
+11 -5
View File
@@ -1,5 +1,4 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@@ -11,9 +10,16 @@ export const metadata = {
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Ping,
locationLabel: "Ping.gg",
appData: {
location: {
linkType: "static",
type: "integrations:ping_video",
label: "Ping.gg",
organizerInputPlaceholder: "https://www.ping.gg/call/theo",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?ping.gg\\/call\\/[a-zA-Z0-9]*",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;
+11 -5
View File
@@ -1,5 +1,4 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@@ -11,9 +10,16 @@ export const metadata = {
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Riverside,
locationLabel: "Riverside Video",
locationPlaceholder: "https://www.riverside.fm/studio/rick",
appData: {
location: {
label: "Riverside Video",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?riverside.fm\\/studio\\/[a-zA-Z0-9]*",
type: "integrations:riverside_video",
linkType: "static",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -20,6 +20,6 @@ export const metadata = {
variant: "conferencing",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -2,6 +2,7 @@ import { WebClient } from "@slack/web-api";
import { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { DailyLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import db from "@calcom/prisma";
@@ -94,7 +95,7 @@ export default async function createEvent(req: NextApiRequest, res: NextApiRespo
email: foundUser?.email ?? "",
name: foundUser?.username ?? "",
guests: await Promise.all(invitedGuestsEmails),
location: "integrations:daily", // Defaulting to daily video to make this a bit more usefull than in-person
location: DailyLocationType, // Defaulting to daily video to make this a bit more usefull than in-person
timeZone: foundUser?.timeZone ?? "",
language: foundUser?.locale ?? "en",
customInputs: [{ label: "", value: "" }],
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -25,6 +25,6 @@ export const metadata = {
variant: "payment",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
+9 -5
View File
@@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@@ -21,8 +20,13 @@ export const metadata = {
reviews: 0,
isGlobal: false,
email: "help@cal.com",
locationType: LocationType.Tandem,
locationLabel: "Tandem Video",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:tandem",
label: "Tandem Video",
},
},
} as AppMeta;
export default metadata;
+2 -8
View File
@@ -1,16 +1,10 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,
trending: true,
verified: true,
...config,
} as App;
} as AppMeta;
export default metadata;
+15 -48
View File
@@ -1,17 +1,17 @@
import { Prisma } from "@prisma/client";
import { TFunction } from "next-i18next";
import type { App } from "@calcom/types/App";
import { defaultLocations, EventLocationType, LocationType } from "@calcom/app-store/locations";
import type { App, AppMeta } from "@calcom/types/App";
// If you import this file on any app it should produce circular dependency
// import appStore from "./index";
import { appStoreMetadata } from "./apps.browser.generated";
import { LocationType } from "./locations";
const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
store[key] = appStoreMetadata[key as keyof typeof appStoreMetadata];
return store;
}, {} as Record<string, App>);
}, {} as Record<string, AppMeta>);
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
select: { id: true, type: true, key: true, userId: true, appId: true },
@@ -23,7 +23,7 @@ export const ALL_APPS = Object.values(ALL_APPS_MAP);
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
@@ -33,15 +33,15 @@ function translateLocations(locations: OptionTypeBase[], t: TFunction) {
label: t(l.label),
}));
}
const defaultLocations: OptionTypeBase[] = [
{ value: LocationType.InPerson, label: "in_person_meeting" },
{ value: LocationType.Link, label: "link_meeting" },
{ value: LocationType.Phone, label: "attendee_phone_number" },
{ value: LocationType.UserPhone, label: "host_phone_number" },
];
export function getLocationOptions(integrations: AppMeta, t: TFunction) {
const locations = [...defaultLocations];
export function getLocationOptions(integrations: ReturnType<typeof getApps>, t: TFunction) {
const locations: OptionTypeBase[] = [];
defaultLocations.forEach((l) => {
locations.push({
label: l.label,
value: l.type,
});
});
integrations.forEach((app) => {
if (app.locationOption) {
locations.push(app.locationOption);
@@ -72,10 +72,10 @@ function getApps(userCredentials: CredentialData[]) {
}
/** Check if app has location option AND add it if user has credentials for it */
if (credentials.length > 0 && appMeta?.locationType) {
if (credentials.length > 0 && appMeta?.appData?.location) {
locationOption = {
value: appMeta.locationType,
label: appMeta.locationLabel || "No label set",
value: appMeta.appData.location.type,
label: appMeta.appData.location.label || "No label set",
disabled: false,
};
}
@@ -96,43 +96,10 @@ function getApps(userCredentials: CredentialData[]) {
return apps;
}
export type AppMeta = ReturnType<typeof getApps>;
export function hasIntegrationInstalled(type: App["type"]): boolean {
return ALL_APPS.some((app) => app.type === type && !!app.installed);
}
export function getLocationTypes(): string[] {
return ALL_APPS.reduce((locations, app) => {
if (typeof app.locationType === "string") {
locations.push(app.locationType);
}
return locations;
}, [] as string[]);
}
export function getLocationLabels(t: TFunction) {
const defaultLocationLabels = defaultLocations.reduce((locations, location) => {
if (location.label === "attendee_phone_number") {
locations[location.value] = t("your_number");
return locations;
}
if (location.label === "host_phone_number") {
locations[location.value] = `${t("phone_call")} (${t("number_provided")})`;
return locations;
}
locations[location.value] = t(location.label);
return locations;
}, {} as Record<LocationType, string>);
return ALL_APPS.reduce((locations, app) => {
if (typeof app.locationType === "string") {
locations[app.locationType] = t(app.locationLabel || "No label set");
}
return locations;
}, defaultLocationLabels);
}
export function getAppName(name: string): string | null {
return ALL_APPS_MAP[name as keyof typeof ALL_APPS_MAP]?.name ?? null;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -22,6 +22,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "support@tryvital.io",
} as App;
} as AppMeta;
export default metadata;
+11 -5
View File
@@ -1,5 +1,4 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@@ -11,9 +10,16 @@ export const metadata = {
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Whereby,
locationLabel: "Whereby Video",
appData: {
location: {
linkType: "static",
type: "integrations:whereby_video",
label: "Whereby Video",
organizerInputPlaceholder: "https://www.whereby.com/cal",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?whereby.com\\/[a-zA-Z0-9]*",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -21,6 +21,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
+2 -2
View File
@@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -20,6 +20,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;
@@ -55,10 +55,10 @@ export default function ZapierSetup(props: IZapierSetupProps) {
<div className="m-auto rounded bg-white p-10">
<div className="flex flex-row">
<div className="mr-5">
<img className="h-11" src="/api/app-store/zapier/icon.svg" alt="Zapier Logo" />
<img className="h-11" src="/api/app-store/typeform/icon.svg" alt="Zapier Logo" />
</div>
<div className="ml-5">
<div className="text-gray-600">{t("setting_up_zapier")}</div>
<div className="text-gray-600">How to integrate a Typeform with Routing Form</div>
{!newApiKey ? (
<>
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
+11 -5
View File
@@ -1,9 +1,9 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
linkType: "dynamic",
name: "Zoom Video",
description: _package.description,
type: "zoom_video",
@@ -20,8 +20,14 @@ export const metadata = {
title: "Zoom Video",
trending: true,
email: "help@cal.com",
locationType: LocationType.Zoom,
locationLabel: "Zoom Video",
} as App;
appData: {
location: {
default: false,
linkType: "dynamic",
type: "integrations:zoom",
label: "Zoom Video",
},
},
} as AppMeta;
export default metadata;
+6 -43
View File
@@ -4,6 +4,7 @@ import merge from "lodash/merge";
import { v5 as uuidv5 } from "uuid";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
@@ -16,55 +17,15 @@ import type {
} from "@calcom/types/EventManager";
import { createEvent, updateEvent } from "./CalendarManager";
import { LocationType } from "./location";
import { createMeeting, updateMeeting } from "./videoClient";
export const isZoom = (location: string): boolean => {
return location === "integrations:zoom";
};
export const isDaily = (location: string): boolean => {
return location === "integrations:daily";
};
export const isHuddle01 = (location: string): boolean => {
return location === "integrations:huddle01";
};
export const isTandem = (location: string): boolean => {
return location === "integrations:tandem";
};
export const isTeams = (location: string): boolean => {
return location === "integrations:office365_video";
};
export const isJitsi = (location: string): boolean => {
return location === "integrations:jitsi";
};
export const isDedicatedIntegration = (location: string): boolean => {
return (
isZoom(location) ||
isDaily(location) ||
isHuddle01(location) ||
isTandem(location) ||
isJitsi(location) ||
isTeams(location)
);
return location !== "integrations:google:meet" && location.includes("integrations:");
};
export const getLocationRequestFromIntegration = (location: string) => {
if (
/** TODO: Handle this dynamically */
location === LocationType.GoogleMeet.valueOf() ||
location === LocationType.Zoom.valueOf() ||
location === LocationType.Daily.valueOf() ||
location === LocationType.Jitsi.valueOf() ||
location === LocationType.Huddle01.valueOf() ||
location === LocationType.Tandem.valueOf() ||
location === LocationType.Teams.valueOf()
) {
const eventLocationType = getEventLocationTypeFromApp(location);
if (eventLocationType) {
const requestId = uuidv5(location, uuidv5.URL);
return {
@@ -84,6 +45,8 @@ export const processLocation = (event: CalendarEvent): CalendarEvent => {
// If location is set to an integration location
// Build proper transforms for evt object
// Extend evt object with those transformations
// TODO: Rely on linkType:"dynamic" here. static links don't send their type. They send their URL directly.
if (event.location?.includes("integration")) {
const maybeLocationRequestObject = getLocationRequestFromIntegration(event.location);
@@ -1,5 +1,7 @@
import { TFunction } from "next-i18next";
import { guessEventLocationType } from "@calcom/app-store/locations";
type EventNameObjectType = {
attendeeName: string;
eventType: string;
@@ -18,35 +20,12 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
});
let eventName = eventNameObj.eventName;
let locationString = "";
let locationString = eventNameObj.location || "";
if (eventNameObj.eventName.includes("{LOCATION}")) {
switch (eventNameObj.location) {
case "inPerson":
locationString = "In Person";
break;
case "userPhone":
case "phone":
locationString = "Phone";
break;
case "integrations:daily":
locationString = "Cal Video";
break;
case "integrations:zoom":
locationString = "Zoom";
break;
case "integrations:huddle01":
locationString = "Huddle01";
break;
case "integrations:tandem":
locationString = "Tandem";
break;
case "integrations:office365_video":
locationString = "MS Teams";
break;
case "integrations:jitsi":
locationString = "Jitsi";
break;
const eventLocationType = guessEventLocationType(eventNameObj.location);
if (eventLocationType) {
locationString = eventLocationType.label;
}
eventName = eventName.replace("{LOCATION}", locationString);
}
+47 -61
View File
@@ -1,7 +1,8 @@
import type { TFunction } from "next-i18next";
import { getAppName } from "@calcom/app-store/utils";
import { getVideoCallPassword, getVideoCallUrl, getProviderName } from "@calcom/lib/CalEventParser";
import { guessEventLocationType } from "@calcom/app-store/locations";
import { getVideoCallPassword, getVideoCallUrl } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { Info } from "./Info";
@@ -9,89 +10,74 @@ import { LinkIcon } from "./LinkIcon";
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
const { t } = props;
const providerName =
(props.calEvent.location && getAppName(props.calEvent.location)) || getProviderName(props.calEvent);
if (props.calEvent.videoCallData) {
const meetingId = props.calEvent.videoCallData.id;
const meetingPassword = getVideoCallPassword(props.calEvent);
const meetingUrl = getVideoCallUrl(props.calEvent);
// We would not be able to determine provider name for DefaultEventLocationTypes
const providerName = guessEventLocationType(props.calEvent.location)?.label;
logger.debug(`LocationInfo: ${JSON.stringify(props.calEvent)} ${providerName}`);
return (
<Info
label={t("where")}
withSpacer
description={
meetingUrl ? (
<a
href={meetingUrl}
target="_blank"
title={t("meeting_url")}
style={{ color: "#3E3E3E" }}
rel="noreferrer">
{providerName} <LinkIcon />
</a>
) : (
<>{t("something_went_wrong")}</>
)
}
extraInfo={
<>
{meetingId && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_id")}: <span>{meetingId}</span>
</>
</div>
)}
{meetingPassword && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_password")}: <span>{meetingPassword}</span>
</>
</div>
)}
{meetingUrl && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_url")}:{" "}
<a href={meetingUrl} title={t("meeting_url")} style={{ color: "#3E3E3E" }}>
{meetingUrl}
</a>
</>
</div>
)}
</>
}
/>
);
const location = props.calEvent.location;
let meetingUrl = location?.search(/^https?:/) !== -1 ? location : undefined;
if (props.calEvent) {
meetingUrl = getVideoCallUrl(props.calEvent) || meetingUrl;
}
if (props.calEvent.additionalInformation?.hangoutLink) {
const hangoutLink: string = props.calEvent.additionalInformation.hangoutLink;
const isPhone = location?.startsWith("+");
// Because of location being a value here, we can determine the app that generated the location only for Dynamic Link based apps where the value is integrations:*
// For static link based location apps, the value is that URL itself. So, it is not straightforward to determine the app that generated the location.
// If we know the App we can always provide the name of the app like we do it for Google Hangout/Google Meet
if (meetingUrl) {
return (
<Info
label={t("where")}
withSpacer
description={
<a
href={hangoutLink}
href={meetingUrl}
target="_blank"
title={t("meeting_url")}
style={{ color: "#3E3E3E" }}
rel="noreferrer">
Google <LinkIcon />
{providerName || "Link"} <LinkIcon />
</a>
}
extraInfo={
meetingUrl && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_url")}:{" "}
<a href={meetingUrl} title={t("meeting_url")} style={{ color: "#3E3E3E" }}>
{meetingUrl}
</a>
</>
</div>
)
}
/>
);
}
if (isPhone) {
return (
<Info
label={t("where")}
withSpacer
description={
<a href={"tel:" + location} title="Phone" style={{ color: "#3E3E3E" }}>
{location}
</a>
}
/>
);
}
return (
<Info
label={t("where")}
withSpacer
description={providerName || props.calEvent.location}
description={providerName || location}
extraInfo={
(providerName === "Zoom" || providerName === "Google") && props.calEvent.requiresConfirmation ? (
<p style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
@@ -4,7 +4,7 @@ import Head from "next/head";
import { FC, useEffect, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { LocationOptionsToString } from "@calcom/app-store/locations";
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import dayjs from "@calcom/dayjs";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
@@ -103,7 +103,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
<>
<div className="font-medium">{t("where")}</div>
<div className="col-span-2 mb-6">
{LocationOptionsToString(props.booking.location, t)}
{getSuccessPageLocationMessage(props.booking.location, t)}
</div>
</>
)}
@@ -1,3 +1,4 @@
import { guessEventLocationType } from "@calcom/app-store/locations";
import { Dayjs } from "@calcom/dayjs";
import { Prisma } from "@calcom/prisma/client";
@@ -18,38 +19,7 @@ const customTemplate = async (text: string, variables: VariablesType, locale: st
let locationString = variables.location || "";
if (text.includes("{LOCATION}")) {
switch (variables.location) {
case "integrations:google:meet":
locationString = "Google Meet";
break;
case "integrations:daily":
locationString = "Cal Video";
break;
case "integrations:zoom":
locationString = "Zoom";
break;
case "integrations:huddle01":
locationString = "Huddle01";
break;
case "integrations:tandem":
locationString = "Tandem";
break;
case "integrations:office365_video":
locationString = "MS Teams";
break;
case "integrations:jitsi":
locationString = "Jitsi";
break;
case "integrations:whereby_video":
locationString = "Whereby";
break;
case "integrations:around_video":
locationString = "Around";
break;
case "integrations:riverside_video":
locationString = "Riverside";
break;
}
locationString = guessEventLocationType(locationString)?.label || "";
}
let dynamicText = text
+2 -1
View File
@@ -1,6 +1,7 @@
import type { EventTypeCustomInput } from "@prisma/client";
import { PeriodType, Prisma, SchedulingType, UserPlan } from "@prisma/client";
import { DailyLocationType } from "@calcom/app-store/locations";
import { userSelect } from "@calcom/prisma/selects";
type User = Prisma.UserGetPayload<typeof userSelect>;
@@ -59,7 +60,7 @@ const commons = {
periodType: PeriodType.UNLIMITED,
periodDays: null,
slotInterval: null,
locations: [{ type: "integrations:daily" }],
locations: [{ type: DailyLocationType }],
customInputs,
disableGuests: true,
minimumBookingNotice: 120,
+1
View File
@@ -15,6 +15,7 @@ function defaultResponder<T>(f: Handle<T>) {
ok = true;
if (result) res.json(result);
} catch (err) {
console.error(err);
const error = getServerErrorFromUnknown(err);
res.statusCode = error.statusCode;
res.json({ message: error.message });
+19 -7
View File
@@ -2,38 +2,50 @@
{
"/*": "This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually",
"dirName": "routing_forms",
"categories": ["other"],
"categories": [
"other"
],
"slug": "routing_forms",
"type": "routing_forms_other"
},
{
"dirName": "whereby",
"categories": ["video"],
"categories": [
"video"
],
"slug": "whereby",
"type": "whereby_video"
},
{
"dirName": "around",
"categories": ["video"],
"categories": [
"video"
],
"slug": "around",
"type": "around_video"
},
{
"dirName": "riverside",
"categories": ["video"],
"categories": [
"video"
],
"slug": "riverside",
"type": "riverside_video"
},
{
"dirName": "typeform",
"categories": ["other"],
"categories": [
"other"
],
"slug": "typeform",
"type": "typeform_other"
},
{
"dirName": "ping",
"categories": ["video"],
"categories": [
"video"
],
"slug": "ping",
"type": "ping_video"
}
]
]
+6 -3
View File
@@ -1,6 +1,9 @@
import { BookingStatus, MembershipRole, Prisma, UserPermissionRole, UserPlan } from "@prisma/client";
import { uuid } from "short-uuid";
import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
import dayjs from "@calcom/dayjs";
import { hashPassword } from "@calcom/lib/auth";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
@@ -263,19 +266,19 @@ async function main() {
title: "Zoom Event",
slug: "zoom",
length: 60,
locations: [{ type: "integrations:zoom" }],
locations: [{ type: zoomMeta.appData?.location.type }],
},
{
title: "Daily Event",
slug: "daily",
length: 60,
locations: [{ type: "integrations:daily" }],
locations: [{ type: dailyMeta.appData?.location.type }],
},
{
title: "Google Meet",
slug: "google-meet",
length: 60,
locations: [{ type: "integrations:google:meet" }],
locations: [{ type: googleMeetMeta.appData?.location.type }],
},
{
title: "Yoga class",
+3 -1
View File
@@ -17,7 +17,9 @@ export enum Frequency {
export const eventTypeLocations = z.array(
z.object({
type: z.nativeEnum(LocationType),
// TODO: Couldn't find a way to make it a union of types from App Store locations
// Creating a dynamic union by iterating over the object doesn't seem to make TS happy
type: z.string(),
address: z.string().optional(),
link: z.string().url().optional(),
displayLocationPublicly: z.boolean().optional(),
+2 -1
View File
@@ -8,6 +8,7 @@ import stripe, { closePayments } from "@calcom/app-store/stripepayment/lib/serve
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { DailyLocationType } from "@calcom/core/location";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
@@ -1031,7 +1032,7 @@ const loggedInViewerRouter = createProtectedRouter()
const updatedLocations = locations.map((location: { type: string }) => {
if (location.type.includes(integrationQuery)) {
return { type: "integrations:daily" };
return { type: DailyLocationType };
}
return location;
});
@@ -1,7 +1,7 @@
import { SchedulingType } from "@prisma/client";
import { z } from "zod";
import { LocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/locations";
import EventManager from "@calcom/core/EventManager";
import dayjs from "@calcom/dayjs";
import { sendLocationChangeEmails } from "@calcom/emails";
@@ -74,7 +74,7 @@ export const bookingsRouter = createProtectedRouter()
})
.mutation("editLocation", {
input: commonBookingSchema.extend({
newLocation: z.string().transform((val) => val || LocationType.Daily),
newLocation: z.string().transform((val) => val || DailyLocationType),
}),
async resolve({ ctx, input }) {
const { bookingId, newLocation: location } = input;
@@ -3,6 +3,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { DailyLocationType } from "@calcom/app-store/locations";
import { stripeDataSchema } from "@calcom/app-store/stripepayment/lib/server";
import { _DestinationCalendarModel, _EventTypeCustomInputModel, _EventTypeModel } from "@calcom/prisma/zod";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
@@ -122,7 +123,7 @@ export const eventTypesRouter = createProtectedRouter()
const appKeys = await getAppKeysFromSlug("daily-video");
if (typeof appKeys.api_key === "string") {
data.locations = [{ type: "integrations:daily" }];
data.locations = [{ type: DailyLocationType }];
}
if (teamId && schedulingType) {
+45 -5
View File
@@ -1,6 +1,38 @@
import type { Prisma } from "@prisma/client";
import type { LocationType } from "@calcom/app-store/locations";
import { Optional } from "./utils";
type CommonProperties = {
default?: false;
type: string;
label: string;
messageForOrganizer?: string;
iconUrl?: string;
variable?: "locationLink";
defaultValueVariable?: "link";
attendeeInputType?: null;
attendeeInputPlaceholder?: null;
};
type StaticLinkBasedEventLocation = {
linkType: "static";
urlRegExp: string;
organizerInputPlaceholder?: string;
organizerInputType?: "text" | "phone";
} & CommonProperties;
type DynamicLinkBasedEventLocation = {
linkType: "dynamic";
urlRegExp?: null;
organizerInputType?: null;
organizerInputPlaceholder?: null;
} & CommonProperties;
export type EventLocationTypeFromAppMeta = StaticLinkBasedEventLocation | DynamicLinkBasedEventLocation;
type EventLocationAppData = {
location: EventLocationTypeFromAppMeta;
};
/**
* This is the definition for an app store's app metadata.
@@ -37,8 +69,16 @@ export interface App {
variant: "calendar" | "payment" | "conferencing" | "video" | "other" | "other_calendar";
/** The slug for the app store public page inside `/apps/[slug] */
slug: string;
/** The category to which this app belongs, currently we have `calendar`, `payment` or `video` */
/*
* @deprecated Use categories
*/
category: string;
/** The category to which this app belongs, currently we have `calendar`, `payment` or `video` */
categories?: string[];
/** An absolute url to the app logo */
logo: string;
/** Company or individual publishing this app */
@@ -62,10 +102,7 @@ export interface App {
isGlobal?: boolean;
/** A contact email, mainly to ask for support */
email: string;
/** Add this value as a posible location option in event types */
locationType?: LocationType;
/** If the app adds a location, how should it be displayed? */
locationLabel?: string;
/** Needed API Keys (usually for global apps) */
key?: Prisma.JsonValue;
/** Needed API Keys (usually for global apps) */
@@ -78,4 +115,7 @@ export interface App {
commission?: number;
licenseRequired?: boolean;
isProOnly?: boolean;
appData?: EventLocationAppData;
}
export type AppMeta = Optional<App, "rating" | "trending" | "reviews" | "verified">;
-1
View File
@@ -4,7 +4,6 @@ import React from "react";
import classNames from "@calcom/lib/classNames";
import { Badge } from "./Badge";
import Button from "./Button";
export type BaseCardProps = {
+2 -2
View File
@@ -7116,12 +7116,12 @@ acorn@^8.0.0, acorn@^8.0.4, acorn@^8.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
acorn@^8.5.0, acorn@^8.7.1:
acorn@^8.5.0:
version "8.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
acorn@^8.6.0, acorn@^8.8.0:
acorn@^8.6.0, acorn@^8.7.1, acorn@^8.8.0:
version "8.8.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==