Files
calendar/packages/features/embed/lib/EmbedCodes.tsx
T
acf3a7c54e feat: add "React (Atom)" to embed generator and rename rest (#20618)
* fix: trigger lingo.dev by removing duplicate value

* feat: add support for React Atom embed options

* fix: update locator for React code tab in embed code generator E2E tests

* fix: update embed tab names to clarify iframe usage

* fix failing test

* fix: indentation and installation commands for atoms

* fix: add note about iframe usage in Preview tab

* refactor: replace Cal component with BookerEmbed in EmbedCodes

* fix: Add EmbedConfig type import in Embed.tsx

- Added missing EmbedConfig type to the existing types import
- Resolves TypeScript error "Cannot find name 'EmbedConfig'"

* fix: update BookerEmbed usage in EmbedCodes component

* fix: enhance BookerEmbed implementation and update installation instructions in EmbedTabs

* fix: improve comments in EmbedCodes for clarity on layout options and styling references

* fix: streamline BookerEmbed usage in EmbedCodes with improved class handling and layout options

* fix: embed type selection

* fix: update layout selection in EmbedCodes to use dynamic configuration

* fix: update embed configuration to use theme instead of layout

* fix: conditionally render embed options based on if we are selecting atom

* fix: use simple props instead of embed specific logic to fix the active embed bug

* Fix double tabs activeness

* Revert isCurrent change

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-04-22 04:51:10 +00:00

338 lines
10 KiB
TypeScript

import { WEBSITE_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import type { PreviewState } from "../types";
import { embedLibUrl } from "./constants";
import { getApiNameForReactSnippet, getApiNameForVanillaJsSnippet } from "./getApiName";
import { getDimension } from "./getDimension";
export const doWeNeedCalOriginProp = (embedCalOrigin: string) => {
// If we are self hosted, calOrigin won't be app.cal.com so we need to pass it
// If we are not self hosted but it's still different from WEBAPP_URL and WEBSITE_URL, we need to pass it -> It happens for organization booking URL at the moment
return IS_SELF_HOSTED || (embedCalOrigin !== WEBAPP_URL && embedCalOrigin !== WEBSITE_URL);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Codes = {
react: {
inline: ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["inline"];
embedCalOrigin: string;
namespace: string;
}) => {
const width = getDimension(previewState.width);
const height = getDimension(previewState.height);
const namespaceProp = `${namespace ? `namespace="${namespace}"` : ""}`;
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(() => {
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
return <Cal ${namespaceProp}
calLink="${calLink}"
style={{width:"${width}",height:"${height}",overflow:"scroll"}}
config={${JSON.stringify(previewState.config)}}
${doWeNeedCalOriginProp(embedCalOrigin) ? `calOrigin="${embedCalOrigin}"` : ""}
${IS_SELF_HOSTED ? `embedJsUrl="${embedLibUrl}"` : ""}
/>;
};`;
},
"floating-popup": ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
embedCalOrigin: string;
uiInstructionCode: string;
namespace: string;
previewState: PreviewState["floatingPopup"];
}) => {
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
const floatingButtonArg = JSON.stringify({
calLink,
...(doWeNeedCalOriginProp(embedCalOrigin) ? { calOrigin: embedCalOrigin } : null),
...previewState,
});
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(() => {
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${getApiNameForReactSnippet({ mainApiName: "cal" })}("floatingButton", ${floatingButtonArg});
${uiInstructionCode}
})();
}, [])
};`;
},
"element-click": ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["elementClick"];
embedCalOrigin: string;
namespace: string;
}) => {
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(() => {
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
return <button data-cal-namespace="${namespace}"
data-cal-link="${calLink}"
${doWeNeedCalOriginProp(embedCalOrigin) ? `data-cal-origin="${embedCalOrigin}"` : ""}
${`data-cal-config='${JSON.stringify(previewState.config)}'`}
>Click me</button>;
};`;
},
},
"react-atom": {
inline: ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["inline"];
embedCalOrigin: string;
namespace: string;
}) => {
return code`
import { BookerEmbed } from "@calcom/atoms";
// You might need to define or import BookerProps depending on your setup
// For example: type BookerProps = { eventTypeSlug: string; calUsername: string; /* other props */ };
export default function Booker( props : BookerProps ) {
return (
<>
<BookerEmbed
// Use the parsed username and event slug from calLink
eventSlug={eventSlug}
// layout can be of three types: COLUMN_VIEW, MONTH_VIEW or WEEK_VIEW,
// you can choose whichever you prefer
view="${previewState.config?.layout || "MONTH_VIEW"}"
username={calUsername}
customClassNames={{
bookerContainer: "border-subtle border",
}}
onCreateBookingSuccess={() => {
console.log("booking created successfully");
}}
/>
</>
);
};`;
},
"floating-popup": ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
embedCalOrigin: string;
uiInstructionCode: string;
namespace: string;
previewState: PreviewState["floatingPopup"];
}) => {
return code`
import { BookerEmbed } from "@calcom/atoms";
// You might need to define or import BookerProps depending on your setup
export default function Booker( props : BookerProps ) {
return (
<>
<BookerEmbed
// Use the parsed username and event slug from calLink
eventSlug={eventSlug}
// layout can be of three types: COLUMN_VIEW, MONTH_VIEW or WEEK_VIEW,
// you can choose whichever you prefer
view="${previewState.config?.layout || "MONTH_VIEW"}"
username={calUsername}
customClassNames={{
bookerContainer: "border-subtle border",
}}
onCreateBookingSuccess={() => {
console.log("booking created successfully");
}}
/>
</>
);
};`;
},
"element-click": ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["elementClick"];
embedCalOrigin: string;
namespace: string;
}) => {
return code`
import { BookerEmbed } from "@calcom/atoms";
// You might need to define or import BookerProps depending on your setup
export default function Booker( props : BookerProps ) {
return (
<>
<BookerEmbed
// Use the parsed username and event slug from calLink
eventSlug={eventSlug}
// layout can be of three types: COLUMN_VIEW, MONTH_VIEW or WEEK_VIEW,
// you can choose whichever you prefer
view="${previewState.config?.layout || "MONTH_VIEW"}"
username={calUsername}
customClassNames={{
bookerContainer: "border-subtle border",
}}
onCreateBookingSuccess={() => {
console.log("booking created successfully");
}}
/>
</>
);
};`;
},
},
HTML: {
inline: ({
calLink,
uiInstructionCode,
previewState,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["inline"];
namespace: string;
}) => {
return code`${getApiNameForVanillaJsSnippet({ namespace, mainApiName: "Cal" })}("inline", {
elementOrSelector:"#my-cal-inline",
config: ${JSON.stringify(previewState.config)},
calLink: "${calLink}",
});
${uiInstructionCode}`;
},
"floating-popup": ({
calLink,
uiInstructionCode,
previewState,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["floatingPopup"];
namespace: string;
}) => {
const floatingButtonArg = JSON.stringify({
calLink,
...previewState,
});
return code`${getApiNameForVanillaJsSnippet({
namespace,
mainApiName: "Cal",
})}("floatingButton", ${floatingButtonArg});
${uiInstructionCode}`;
},
"element-click": ({
calLink,
uiInstructionCode,
previewState,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["elementClick"];
namespace: string;
}) => {
return code`
// Important: Please add the following attributes to the element that should trigger the calendar to open upon clicking.
// \`data-cal-link="${calLink}"\`
// data-cal-namespace="${namespace}"
// \`data-cal-config='${JSON.stringify(previewState.config)}'\`
${uiInstructionCode}`;
},
},
};
/**
* 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("");
};
function getArgumentForGetCalApi(namespace: string) {
const libUrl = IS_SELF_HOSTED ? embedLibUrl : undefined;
const argumentForGetCalApi = namespace ? { namespace, embedLibUrl: libUrl } : { embedLibUrl: libUrl };
return argumentForGetCalApi;
}