import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible"; import classNames from "classnames"; import type { TFunction } from "next-i18next"; import type { NextRouter } from "next/router"; import { useRouter } from "next/router"; import type { MutableRefObject, RefObject } from "react"; import { createRef, forwardRef, useRef, useState } from "react"; import type { ControlProps } from "react-select"; import { components } from "react-select"; import type { BookerLayout } from "@calcom/features/bookings/Booker/types"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { APP_NAME, EMBED_LIB_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; import { Button, Dialog, DialogClose, DialogContent, HorizontalTabs, Label, showToast, Switch, TextArea, TextField, ColorPicker, Select, } from "@calcom/ui"; import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon"; type EmbedType = "inline" | "floating-popup" | "element-click"; type EmbedFramework = "react" | "HTML"; const enum Theme { auto = "auto", light = "light", dark = "dark", } const EMBED_CAL_ORIGIN = WEBAPP_URL; const EMBED_CAL_JS_URL = `${WEBAPP_URL}/embed/embed.js`; type PreviewState = { inline: { width: string; height: string; }; theme: Theme; floatingPopup: { config?: { layout: BookerLayouts; }; [key: string]: string | boolean | undefined | Record; }; elementClick: Record; palette: { brandColor: string; }; hideEventTypeDetails: boolean; layout: BookerLayouts; }; const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"]; const getDimension = (dimension: string) => { if (dimension.match(/^\d+$/)) { dimension = `${dimension}%`; } return dimension; }; const goto = (router: NextRouter, searchParams: Record) => { const newQuery = new URLSearchParams(router.asPath.split("?")[1]); Object.keys(searchParams).forEach((key) => { newQuery.set(key, searchParams[key]); }); router.push(`${router.asPath.split("?")[0]}?${newQuery.toString()}`, undefined, { shallow: true, }); }; const removeQueryParams = (router: NextRouter, queryParams: string[]) => { const params = new URLSearchParams(window.location.search); queryParams.forEach((param) => { params.delete(param); }); router.push(`${router.asPath.split("?")[0]}?${params.toString()}`); }; /** * It allows us to show code with certain reusable blocks indented according to the block variable placement * So, if you add a variable ${abc} with indentation of 4 spaces, it will automatically indent all newlines in `abc` with the same indent before constructing the final string * `A${var}C` with var = "B" -> partsWithoutBlock=['A','C'] blocksOrVariables=['B'] */ const code = (partsWithoutBlock: TemplateStringsArray, ...blocksOrVariables: string[]) => { const constructedCode: string[] = []; for (let i = 0; i < partsWithoutBlock.length; i++) { const partWithoutBlock = partsWithoutBlock[i]; // blocksOrVariables length would always be 1 less than partsWithoutBlock // So, last item should be concatenated as is. if (i >= blocksOrVariables.length) { constructedCode.push(partWithoutBlock); continue; } const block = blocksOrVariables[i]; const indentedBlock: string[] = []; let indent = ""; block.split("\n").forEach((line) => { indentedBlock.push(line); }); // non-null assertion is okay because we know that we are referencing last element. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const indentationMatch = partWithoutBlock .split("\n") .at(-1)! .match(/(^[\t ]*).*$/); if (indentationMatch) { indent = indentationMatch[1]; } constructedCode.push(partWithoutBlock + indentedBlock.join("\n" + indent)); } return constructedCode.join(""); }; const getInstructionString = ({ apiName, instructionName, instructionArg, }: { apiName: string; instructionName: string; instructionArg: Record; }) => { return `${apiName}("${instructionName}", ${JSON.stringify(instructionArg)});`; }; const getEmbedUIInstructionString = ({ apiName, theme, brandColor, hideEventTypeDetails, layout, }: { apiName: string; theme?: string; brandColor: string; hideEventTypeDetails: boolean; layout?: string; }) => { theme = theme !== "auto" ? theme : undefined; return getInstructionString({ apiName, instructionName: "ui", instructionArg: { theme, styles: { branding: { brandColor, }, }, hideEventTypeDetails: hideEventTypeDetails, layout, }, }); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const Codes: Record string>> = { react: { inline: ({ calLink, uiInstructionCode, previewState, }: { calLink: string; uiInstructionCode: string; previewState: PreviewState; }) => { const width = getDimension(previewState.inline.width); const height = getDimension(previewState.inline.height); return code` import Cal, { getCalApi } from "@calcom/embed-react"; import { useEffect } from "react"; export default function MyApp() { useEffect(()=>{ (async function () { const cal = await getCalApi(); ${uiInstructionCode} })(); }, []) return ; };`; }, "floating-popup": ({ floatingButtonArg, uiInstructionCode, }: { floatingButtonArg: string; uiInstructionCode: string; }) => { return code` import { getCalApi } from "@calcom/embed-react"; import { useEffect } from "react"; export default function App() { useEffect(()=>{ (async function () { const cal = await getCalApi(${IS_SELF_HOSTED ? `"${EMBED_CAL_JS_URL}"` : ""}); cal("floatingButton", ${floatingButtonArg}); ${uiInstructionCode} })(); }, []) };`; }, "element-click": ({ calLink, uiInstructionCode, previewState, }: { calLink: string; uiInstructionCode: string; previewState: PreviewState; }) => { return code` import { getCalApi } from "@calcom/embed-react"; import { useEffect } from "react"; export default function App() { useEffect(()=>{ (async function () { const cal = await getCalApi(${IS_SELF_HOSTED ? `"${EMBED_CAL_JS_URL}"` : ""}); ${uiInstructionCode} })(); }, []) return ; };`; }, }, HTML: { inline: ({ calLink, uiInstructionCode, previewState, }: { calLink: string; uiInstructionCode: string; previewState: PreviewState; }) => { return code`Cal("inline", { elementOrSelector:"#my-cal-inline", calLink: "${calLink}", layout: "${previewState.layout}" }); ${uiInstructionCode}`; }, "floating-popup": ({ floatingButtonArg, uiInstructionCode, }: { floatingButtonArg: string; uiInstructionCode: string; }) => { return code`Cal("floatingButton", ${floatingButtonArg}); ${uiInstructionCode}`; }, "element-click": ({ calLink, uiInstructionCode, previewState, }: { calLink: string; uiInstructionCode: string; previewState: PreviewState; }) => { return code` // Important: Please add following attributes to the element you want to open Cal on click // \`data-cal-link="${calLink}"\` // \`data-cal-config='${JSON.stringify({ layout: previewState.layout, })}'\` ${uiInstructionCode}`; }, }, }; const getEmbedTypeSpecificString = ({ embedFramework, embedType, calLink, previewState, }: { embedFramework: EmbedFramework; embedType: EmbedType; calLink: string; previewState: PreviewState; }) => { const frameworkCodes = Codes[embedFramework]; if (!frameworkCodes) { throw new Error(`No code available for the framework:${embedFramework}`); } let uiInstructionStringArg: { apiName: string; theme: PreviewState["theme"]; brandColor: string; hideEventTypeDetails: boolean; layout?: BookerLayout; }; if (embedFramework === "react") { uiInstructionStringArg = { apiName: "cal", theme: previewState.theme, brandColor: previewState.palette.brandColor, hideEventTypeDetails: previewState.hideEventTypeDetails, layout: previewState.layout, }; } else { uiInstructionStringArg = { apiName: "Cal", theme: previewState.theme, brandColor: previewState.palette.brandColor, hideEventTypeDetails: previewState.hideEventTypeDetails, layout: previewState.layout, }; } if (!frameworkCodes[embedType]) { throw new Error(`Code not available for framework:${embedFramework} and embedType:${embedType}`); } if (embedType === "inline") { return frameworkCodes[embedType]({ calLink, uiInstructionCode: getEmbedUIInstructionString(uiInstructionStringArg), previewState, }); } else if (embedType === "floating-popup") { const floatingButtonArg = { calLink, ...(IS_SELF_HOSTED ? { calOrigin: EMBED_CAL_ORIGIN } : null), ...previewState.floatingPopup, }; return frameworkCodes[embedType]({ floatingButtonArg: JSON.stringify(floatingButtonArg), uiInstructionCode: getEmbedUIInstructionString(uiInstructionStringArg), previewState, }); } else if (embedType === "element-click") { return frameworkCodes[embedType]({ calLink, uiInstructionCode: getEmbedUIInstructionString(uiInstructionStringArg), previewState, }); } return ""; }; const embeds = (t: TFunction) => (() => { return [ { title: t("inline_embed"), subtitle: t("load_inline_content"), type: "inline", illustration: ( ), }, { title: t("floating_pop_up_button"), subtitle: t("floating_button_trigger_modal"), type: "floating-popup", illustration: ( ), }, { title: t("pop_up_element_click"), subtitle: t("open_dialog_with_element_click"), type: "element-click", illustration: ( ), }, ]; })(); const tabs = [ { name: "HTML", href: "embedTabName=embed-code", icon: Code, type: "code", Component: forwardRef< HTMLTextAreaElement | HTMLIFrameElement | null, { embedType: EmbedType; calLink: string; previewState: PreviewState } >(function EmbedHtml({ embedType, calLink, previewState }, ref) { const { t } = useLocale(); if (ref instanceof Function || !ref) { return null; } if (ref.current && !(ref.current instanceof HTMLTextAreaElement)) { return null; } return ( <>
{t("place_where_cal_widget_appear", { appName: APP_NAME })}