From a9e519f643aa739a60cbf8ca7b54551769bf6e9b Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 9 Jun 2022 10:35:18 +0530 Subject: [PATCH] Improvement: Add React to Embed Snippet Generator (#3018) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/components/Embed.tsx | 570 +++++++++++++----- apps/web/public/static/locales/en/common.json | 1 + packages/embeds/embed-react/src/Cal.tsx | 19 +- packages/embeds/embed-react/src/index.ts | 2 + packages/embeds/embed-react/test-cal.tsx | 1 + 5 files changed, 426 insertions(+), 167 deletions(-) diff --git a/apps/web/components/Embed.tsx b/apps/web/components/Embed.tsx index b48eea541f..94c23ee087 100644 --- a/apps/web/components/Embed.tsx +++ b/apps/web/components/Embed.tsx @@ -2,7 +2,7 @@ import { ArrowLeftIcon, ChevronRightIcon, CodeIcon, EyeIcon, SunIcon } from "@he import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import classNames from "classnames"; import { useRouter } from "next/router"; -import { useRef, useState } from "react"; +import { forwardRef, MutableRefObject, useRef, useState } from "react"; import { components, ControlProps } from "react-select"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -20,8 +20,253 @@ import ColorPicker from "@components/ui/colorpicker"; import Select from "@components/ui/form/Select"; type EmbedType = "inline" | "floating-popup" | "element-click"; +type EmbedFramework = "react" | "HTML"; + +const enum Theme { + auto = "auto", + light = "light", + dark = "dark", +} + +type PreviewState = { + inline: { + width: string; + height: string; + }; + theme: Theme; + floatingPopup: Record; + elementClick: Record; + palette: { + brandColor: string; + }; +}; const queryParamsForDialog = ["embedType", "tabName", "eventTypeId"]; +const getDimension = (dimension: string) => { + if (dimension.match(/^\d+$/)) { + dimension = `${dimension}%`; + } + return dimension; +}; + +/** + * 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, +}: { + apiName: string; + theme?: string; + brandColor: string; +}) => { + theme = theme !== "auto" ? theme : undefined; + return getInstructionString({ + apiName, + instructionName: "ui", + instructionArg: { + theme, + styles: { + branding: { + brandColor, + }, + }, + }, + }); +}; + +// 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"; + +function MyComponent() { + useEffect(()=>{ + (async function () { + const cal = await getCalApi(); + ${uiInstructionCode} + })(); + }, []) + return ; +};`; + }, + "floating-popup": ({ + floatingButtonArg, + uiInstructionCode, + }: { + floatingButtonArg: string; + uiInstructionCode: string; + }) => { + return code` +import Cal, { getCalApi } from "@calcom/embed-react"; + +function MyComponent() { + useEffect(()=>{ + (async function () { + const cal = await getCalApi(); + Cal("floatingButton", ${floatingButtonArg}); + ${uiInstructionCode} + })(); + }, []) +};`; + }, + "element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => { + return code` +import Cal, { getCalApi } from "@calcom/embed-react"; + +function MyComponent() { + useEffect(()=>{ + (async function () { + const cal = await getCalApi(); + ${uiInstructionCode} + })(); + }, []) + return