From 4a58da62d6fc8343ae336ea8868a81a4fc6fcae8 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 31 Mar 2022 14:15:47 +0530 Subject: [PATCH] [Feature]Booking Embed (#2227) --- apps/web/components/booking/DatePicker.tsx | 11 +- apps/web/components/ui/form/PhoneInput.tsx | 1 - apps/web/lib/hooks/useTheme.tsx | 5 + apps/web/next.config.js | 1 + apps/web/pages/[user].tsx | 15 +- apps/web/pages/_app.tsx | 2 + apps/web/pages/_document.tsx | 8 +- apps/web/pages/success.tsx | 35 +- package.json | 1 + packages/embeds/README.md | 8 + packages/embeds/embed-core/README.md | 95 +++++ packages/embeds/embed-core/env.d.ts | 9 + packages/embeds/embed-core/index.html | 225 +++++++++++ packages/embeds/embed-core/index.ts | 2 + packages/embeds/embed-core/package.json | 17 + packages/embeds/embed-core/src/ModalBox.ts | 78 ++++ .../embeds/embed-core/src/embed-iframe.ts | 152 +++++++ packages/embeds/embed-core/src/embed.css | 6 + packages/embeds/embed-core/src/embed.ts | 382 ++++++++++++++++++ .../embed-core/src/sdk-action-manager.ts | 58 +++ packages/embeds/embed-core/src/sdk-event.ts | 10 + packages/embeds/embed-core/tsconfig.json | 10 + packages/embeds/embed-core/vite.config.js | 15 + packages/embeds/embed-core/yarn.lock | 215 ++++++++++ packages/embeds/embed-react/README.md | 14 + packages/embeds/embed-react/env.d.ts | 1 + packages/embeds/embed-react/index.html | 10 + packages/embeds/embed-react/package.json | 17 + packages/embeds/embed-react/src/Cal.tsx | 33 ++ packages/embeds/embed-react/src/useEmbed.ts | 13 + packages/embeds/embed-react/test-cal.tsx | 24 ++ packages/embeds/embed-react/tsconfig.json | 11 + packages/embeds/embed-snippet/README.md | 11 + packages/embeds/embed-snippet/env.d.ts | 1 + packages/embeds/embed-snippet/package.json | 15 + packages/embeds/embed-snippet/src/index.ts | 58 +++ packages/embeds/embed-snippet/tsconfig.json | 9 + packages/embeds/embed-snippet/vite.config.js | 12 + yarn.lock | 172 +++++++- 39 files changed, 1740 insertions(+), 22 deletions(-) create mode 100644 packages/embeds/README.md create mode 100644 packages/embeds/embed-core/README.md create mode 100644 packages/embeds/embed-core/env.d.ts create mode 100644 packages/embeds/embed-core/index.html create mode 100644 packages/embeds/embed-core/index.ts create mode 100644 packages/embeds/embed-core/package.json create mode 100644 packages/embeds/embed-core/src/ModalBox.ts create mode 100644 packages/embeds/embed-core/src/embed-iframe.ts create mode 100644 packages/embeds/embed-core/src/embed.css create mode 100644 packages/embeds/embed-core/src/embed.ts create mode 100644 packages/embeds/embed-core/src/sdk-action-manager.ts create mode 100644 packages/embeds/embed-core/src/sdk-event.ts create mode 100644 packages/embeds/embed-core/tsconfig.json create mode 100644 packages/embeds/embed-core/vite.config.js create mode 100644 packages/embeds/embed-core/yarn.lock create mode 100644 packages/embeds/embed-react/README.md create mode 100644 packages/embeds/embed-react/env.d.ts create mode 100644 packages/embeds/embed-react/index.html create mode 100644 packages/embeds/embed-react/package.json create mode 100644 packages/embeds/embed-react/src/Cal.tsx create mode 100644 packages/embeds/embed-react/src/useEmbed.ts create mode 100644 packages/embeds/embed-react/test-cal.tsx create mode 100644 packages/embeds/embed-react/tsconfig.json create mode 100644 packages/embeds/embed-snippet/README.md create mode 100644 packages/embeds/embed-snippet/env.d.ts create mode 100644 packages/embeds/embed-snippet/package.json create mode 100644 packages/embeds/embed-snippet/src/index.ts create mode 100644 packages/embeds/embed-snippet/tsconfig.json create mode 100644 packages/embeds/embed-snippet/vite.config.js diff --git a/apps/web/components/booking/DatePicker.tsx b/apps/web/components/booking/DatePicker.tsx index 488e93236a..d4a68ba322 100644 --- a/apps/web/components/booking/DatePicker.tsx +++ b/apps/web/components/booking/DatePicker.tsx @@ -5,13 +5,15 @@ import dayjsBusinessTime from "dayjs-business-time"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import { memoize } from "lodash"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; + +import { useEmbedStyles } from "@calcom/embed-core"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import classNames from "@lib/classNames"; import { timeZone } from "@lib/clock"; import { weekdayNames } from "@lib/core/i18n/weekday"; import { doWorkAsync } from "@lib/doWorkAsync"; -import { useLocale } from "@lib/hooks/useLocale"; import getSlots from "@lib/slots"; import { WorkingHours } from "@lib/types/schedule"; @@ -85,6 +87,8 @@ function DatePicker({ }: DatePickerProps): JSX.Element { const { i18n } = useLocale(); const [browsingDate, setBrowsingDate] = useState(date); + const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton"); + const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton"); const [month, setMonth] = useState(""); const [year, setYear] = useState(""); @@ -274,6 +278,9 @@ function DatePicker({ +
+ Corresponding Cal Link is being preloaded. Assuming that it would take you some time to click this + as you are reading this text, it would open up super fast[If you are running a production build on + local]. Try switching to slow 3G or create a custom Network configuration which is impossibly + slow +
+ +
+
+

Default Namespace(Cal)[Black Theme][Guests(janedoe@gmail.com and test@gmail.com)]

+ + You would see last Booking page action in my place +
+
+ if you render booking embed in me, I would not let it be more than 30vh in height. So you would + have to scroll to see the entire content +
+
Loading .....
+
+
+
+

Namespace "second"(Cal.ns.second)[Custom Styling]

+ + + You would see last Booking page action in my place + +
+
If you render booking embed in me, I won't restrict you. The entire page is yours.
+ + +
Loading .....
+
+
+ +
+

Namespace "third"(Cal.ns.third)

+ + + You would see last Booking page action in my place + +
+
If you render booking embed in me, I would not let you be more than 30% wide
+
Loading .....
+
+
+
+ + + + + diff --git a/packages/embeds/embed-core/index.ts b/packages/embeds/embed-core/index.ts new file mode 100644 index 0000000000..f1811a9314 --- /dev/null +++ b/packages/embeds/embed-core/index.ts @@ -0,0 +1,2 @@ +export * from "./src/embed-iframe"; +export * from "./src/sdk-event"; diff --git a/packages/embeds/embed-core/package.json b/packages/embeds/embed-core/package.json new file mode 100644 index 0000000000..538c4ac409 --- /dev/null +++ b/packages/embeds/embed-core/package.json @@ -0,0 +1,17 @@ +{ + "name": "@calcom/embed-core", + "version": "0.1.0", + "description": "The core script adds the booking embed", + "main": "./index.ts", + "scripts": { + "build": "vite build", + "vite": "vite", + "dev": "run-p 'build --watch' 'vite --port 3002 --strict-port --open'", + "type-check": "tsc --pretty --noEmit", + "lint": "eslint --ext .ts,.js src" + }, + "devDependencies": { + "vite": "^2.8.6", + "eslint": "^8.10.0" + } +} diff --git a/packages/embeds/embed-core/src/ModalBox.ts b/packages/embeds/embed-core/src/ModalBox.ts new file mode 100644 index 0000000000..723425e64b --- /dev/null +++ b/packages/embeds/embed-core/src/ModalBox.ts @@ -0,0 +1,78 @@ +export class ModalBox extends HTMLElement { + connectedCallback() { + const closeEl = this.shadowRoot!.querySelector(".close") as HTMLElement; + closeEl.onclick = () => { + this.shadowRoot!.host.remove(); + }; + } + + constructor() { + super(); + //FIXME: this styling goes as is as it's a JS string. That's a lot of unnecessary whitespaces over the wire. + const modalHtml = ` + +
+
+ × +
+ +
+ `; + this.attachShadow({ mode: "open" }); + this.shadowRoot!.innerHTML = modalHtml; + } +} diff --git a/packages/embeds/embed-core/src/embed-iframe.ts b/packages/embeds/embed-core/src/embed-iframe.ts new file mode 100644 index 0000000000..8659406f78 --- /dev/null +++ b/packages/embeds/embed-core/src/embed-iframe.ts @@ -0,0 +1,152 @@ +import { useState, useEffect, CSSProperties } from "react"; + +import { sdkActionManager } from "./sdk-event"; + +// Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted. +// Keep this list to minimum, only adding those styles which are really needed. +interface EmbedStyles { + body?: Pick; + eventTypeListItem?: Pick; + enabledDateButton?: Pick; + disabledDateButton?: Pick; +} + +type ElementName = keyof EmbedStyles; + +type ReactEmbedStylesSetter = React.Dispatch>; + +export interface UiConfig { + theme: string; + styles: EmbedStyles; +} + +const embedStore = { + // Store all embed styles here so that as and when new elements are mounted, styles can be applied to it. + styles: {}, + // Store all React State setters here. + reactStylesStateSetters: {} as Record, +}; + +const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => { + embedStore.styles = stylesConfig; + for (let [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) { + setEmbedStyle((styles) => { + return { + ...styles, + ...stylesConfig, + }; + }); + } +}; + +const registerNewSetter = (elementName: ElementName, setStyles: ReactEmbedStylesSetter) => { + embedStore.reactStylesStateSetters[elementName] = setStyles; + // It's possible that 'ui' instruction has already been processed and the registration happened due to some action by the user in iframe. + // So, we should call the setter immediately with available embedStyles + setStyles(embedStore.styles); +}; + +const removeFromEmbedStylesSetterMap = (elementName: ElementName) => { + delete embedStore.reactStylesStateSetters[elementName]; +}; + +// TODO: Make it usable as an attribute directly instead of styles value. It would allow us to go beyond styles e.g. for debugging we can add a special attribute indentifying the element on which UI config has been applied +export const useEmbedStyles = (elementName: ElementName) => { + const [styles, setStyles] = useState({} as EmbedStyles); + + useEffect(() => { + registerNewSetter(elementName, setStyles); + // It's important to have an element's embed style be required in only one component. If due to any reason it is required in multiple components, we would override state setter. + return () => { + // Once the component is unmounted, we can remove that state setter. + removeFromEmbedStylesSetterMap(elementName); + }; + }, []); + + return styles[elementName] || {}; +}; + +// If you add a method here, give type safety to parent manually by adding it to embed.ts. Look for "parentKnowsIframeReady" in it +export const methods = { + ui: function style(uiConfig: UiConfig) { + // TODO: Create automatic logger for all methods. Useful for debugging. + console.log("Method: ui called", uiConfig); + const stylesConfig = uiConfig.styles; + + // In case where parent gives instructions before setEmbedStyles is set. + if (!setEmbedStyles) { + return requestAnimationFrame(() => { + style(uiConfig); + }); + } + + // body can't be styled using React state hook as it is generated by _document.tsx which doesn't support hooks. + if (stylesConfig.body?.background) { + document.body.style.background = stylesConfig.body.background as string; + } + + setEmbedStyles(stylesConfig); + }, + parentKnowsIframeReady: () => { + document.body.style.display = "block"; + sdkActionManager?.fire("linkReady", {}); + }, +}; + +const messageParent = (data: any) => { + parent.postMessage( + { + originator: "CAL", + ...data, + }, + "*" + ); +}; + +function keepParentInformedAboutDimensionChanges() { + let knownHiddenHeight: Number | null = null; + let numDimensionChanges = 0; + requestAnimationFrame(function informAboutScroll() { + // Because of scroll="no", this much is hidden from the user. + const hiddenHeight = document.documentElement.scrollHeight - window.innerHeight; + // TODO: Handle width as well. + if (knownHiddenHeight !== hiddenHeight) { + knownHiddenHeight = hiddenHeight; + numDimensionChanges++; + // FIXME: This event shouldn't be subscribable by the user. Only by the SDK. + sdkActionManager?.fire("dimension-changed", { + hiddenHeight, + }); + } + // Parent Counterpart would change the dimension of iframe and thus page's dimension would be impacted which is recursive. + // It should stop ideally by reaching a hiddenHeight value of 0. + // FIXME: If 0 can't be reached we need to just abandon our quest for perfect iframe and let scroll be there. Such case can be logged in the wild and fixed later on. + if (numDimensionChanges > 50) { + console.warn("Too many dimension changes detected."); + return; + } + requestAnimationFrame(informAboutScroll); + }); +} + +if (typeof window !== "undefined" && !location.search.includes("prerender=true")) { + sdkActionManager?.on("*", (e) => { + const detail = e.detail; + //console.log(detail.fullType, detail.type, detail.data); + messageParent(detail); + }); + + window.addEventListener("message", (e) => { + const data: Record = e.data; + if (!data) { + return; + } + const method: keyof typeof methods = data.method; + if (data.originator === "CAL" && typeof method === "string") { + methods[method]?.(data.arg); + } + }); + + keepParentInformedAboutDimensionChanges(); + sdkActionManager?.fire("iframeReady", {}); +} diff --git a/packages/embeds/embed-core/src/embed.css b/packages/embeds/embed-core/src/embed.css new file mode 100644 index 0000000000..5f8425dce9 --- /dev/null +++ b/packages/embeds/embed-core/src/embed.css @@ -0,0 +1,6 @@ +/** + These styles are applied to the entire page, so the selectors need to be specific +*/ +.cal-embed { + border: 0px; +} diff --git a/packages/embeds/embed-core/src/embed.ts b/packages/embeds/embed-core/src/embed.ts new file mode 100644 index 0000000000..f22f3098c6 --- /dev/null +++ b/packages/embeds/embed-core/src/embed.ts @@ -0,0 +1,382 @@ +import type { CalWindow } from "@calcom/embed-snippet"; + +import { ModalBox } from "./ModalBox"; +import { methods, UiConfig } from "./embed-iframe"; +import css from "./embed.css"; +import { SdkActionManager } from "./sdk-action-manager"; + +declare module "*.css"; + +type Namespace = string; +type Config = Record<"origin", "string">; + +const globalCal = (window as CalWindow).Cal; + +if (!globalCal || !globalCal.q) { + throw new Error("Cal is not defined. This shouldn't happen"); +} + +document.head.appendChild(document.createElement("style")).innerHTML = css; + +function log(...args: any[]) { + console.log(...args); +} +/** + * A very simple data validator written with intention of keeping payload size low. + * Extend the functionality of it as required by the embed. + * @param data + * @param schema + */ +function validate(data: any, schema: Record<"props" | "required", any>) { + function checkType(value: any, expectedType: any) { + if (typeof expectedType === "string") { + return typeof value == expectedType; + } else { + return value instanceof expectedType; + } + } + + function isUndefined(data: any) { + return typeof data === "undefined"; + } + + if (schema.required && isUndefined(data)) { + throw new Error("Argument is required"); + } + + for (let [prop, propSchema] of Object.entries>(schema.props)) { + if (propSchema.required && isUndefined(data[prop])) { + throw new Error(`"${prop}" is required`); + } + let typeCheck = true; + if (propSchema.type && !isUndefined(data[prop])) { + if (propSchema.type instanceof Array) { + propSchema.type.forEach((type) => { + typeCheck = typeCheck || checkType(data[prop], type); + }); + } else { + typeCheck = checkType(data[prop], propSchema.type); + } + } + if (!typeCheck) { + throw new Error(`"${prop}" is of wrong type.Expected type "${propSchema.type}"`); + } + } +} + +export type Instruction = [method: string, argument: any] | [method: string, argument: any][]; +export type InstructionQueue = Instruction[]; + +export class Cal { + iframe?: HTMLIFrameElement; + + __config: any; + + namespace: string; + + actionManager: SdkActionManager; + + iframeReady!: boolean; + + iframeDoQueue: { method: keyof typeof methods; arg: any }[] = []; + + static actionsManagers: Record; + + static getQueryObject(config: Record) { + config = config || {}; + return { + ...config, + // guests is better for API but Booking Page accepts guest. So do the mapping + guest: config.guests ?? "", + }; + } + + processInstruction(instruction: Instruction) { + instruction = [].slice.call(instruction, 0); + if (instruction[0] instanceof Array) { + // It is an instruction + instruction.forEach((instruction) => { + this.processInstruction(instruction); + }); + return; + } + const [method, ...args] = instruction; + if (!this[method]) { + // Instead of throwing error, log and move forward in the queue + log(`Instruction ${method} not FOUND`); + } + try { + (this[method] as Function)(...args); + } catch (e) { + // Instead of throwing error, log and move forward in the queue + log(`Instruction couldn't be executed`, e); + } + return instruction; + } + + processQueue(queue: InstructionQueue) { + queue.forEach((instruction) => { + this.processInstruction(instruction); + }); + + queue.splice(0); + + /** @ts-ignore */ // We changed the definition of push here. + queue.push = (instruction) => { + this.processInstruction(instruction); + }; + } + + createIframe({ + calLink, + queryObject = {}, + }: { + calLink: string; + queryObject?: Record; + }) { + const iframe = (this.iframe = document.createElement("iframe")); + // FIXME: scrolling seems deprecated, though it works on Chrome. What's the recommended way to do it? + iframe.scrolling = "no"; + iframe.className = "cal-embed"; + const config = this.getConfig(); + + // Prepare searchParams from config + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(queryObject)) { + if (value instanceof Array) { + value.forEach((val) => searchParams.append(key, val)); + } else { + searchParams.set(key, value); + } + } + + const urlInstance = new URL(`${config.origin}/${calLink}`); + urlInstance.searchParams.set("embed", this.namespace); + + // Merge searchParams from config onto the URL which might have query params already + //@ts-ignore + for (let [key, value] of searchParams) { + urlInstance.searchParams.append(key, value); + } + iframe.src = urlInstance.toString(); + return iframe; + } + + init(namespaceOrConfig: string | Config, config: Config = {} as Config) { + if (namespaceOrConfig.hasOwnProperty("origin")) { + config = namespaceOrConfig as Config; + } + if (config?.origin) { + this.__config.origin = config.origin; + } + } + + getConfig() { + return this.__config; + } + + // TODO: Maintain exposed methods in a separate namespace, so that unexpected methods don't become instructions + + /** + * It is an instruction that adds embed iframe inline as last child of the element + */ + inline({ + calLink, + elementOrSelector, + config, + }: { + calLink: string; + elementOrSelector: string | HTMLElement; + config: Record; + }) { + validate(arguments[0], { + required: true, + props: { + calLink: { + required: true, + type: "string", + }, + elementOrSelector: { + required: true, + type: ["string", HTMLElement], + }, + config: { + required: false, + type: Object, + }, + }, + }); + const iframe = this.createIframe({ calLink, queryObject: Cal.getQueryObject(config) }); + iframe.style.height = "100%"; + iframe.style.width = "100%"; + let element = + elementOrSelector instanceof HTMLElement + ? elementOrSelector + : document.querySelector(elementOrSelector); + if (!element) { + throw new Error("Element not found"); + } + element.appendChild(iframe); + } + + modal({ calLink }: { calLink: string }) { + const iframe = this.createIframe({ calLink }); + iframe.style.height = "100%"; + iframe.style.width = "100%"; + const template = document.createElement("template"); + template.innerHTML = ``; + template.content.children[0].appendChild(iframe); + document.body.appendChild(template.content); + } + + on({ + action, + callback, + }: { + action: Parameters[0]; + callback: Parameters[1]; + }) { + validate(arguments[0], { + required: true, + props: { + action: { + required: true, + type: "string", + }, + callback: { + required: true, + type: Function, + }, + }, + }); + this.actionManager.on(action, callback); + } + + preload({ calLink }: { calLink: string }) { + validate(arguments[0], { + required: true, + props: { + calLink: { + type: "string", + required: true, + }, + }, + }); + const iframe = document.body.appendChild(document.createElement("iframe")); + const config = this.getConfig(); + + const urlInstance = new URL(`${config.origin}/${calLink}`); + urlInstance.searchParams.set("prerender", "true"); + iframe.src = urlInstance.toString(); + iframe.style.width = "0"; + iframe.style.height = "0"; + iframe.style.display = "none"; + } + + ui(uiConfig: UiConfig) { + validate(uiConfig, { + required: true, + props: { + theme: { + required: false, + type: "string", + }, + styles: { + required: false, + type: Object, + }, + }, + }); + + this.doInIframe({ method: "ui", arg: uiConfig }); + } + + doInIframe({ + method, + arg, + }: // TODO: Need some TypeScript magic here to remove hardcoded types + | { method: "ui"; arg: Parameters[0] } + | { method: "parentKnowsIframeReady"; arg: undefined }) { + if (!this.iframeReady) { + this.iframeDoQueue.push({ method, arg }); + return; + } + // TODO: Ensure that origin is as defined by user. Generally it would be cal.com but in case of self hosting it can be anything. + this.iframe!.contentWindow!.postMessage({ originator: "CAL", method, arg }, "*"); + } + + constructor(namespace: string, q: InstructionQueue) { + this.__config = { + origin: import.meta.env.NEXT_PUBLIC_WEBSITE_URL || "https://cal.com", + }; + this.namespace = namespace; + this.actionManager = new SdkActionManager(namespace); + + Cal.actionsManagers = Cal.actionsManagers || {}; + Cal.actionsManagers[namespace] = this.actionManager; + + this.processQueue(q); + + // 1. Initial iframe width and height would be according to 100% value of the parent element + // 2. Once webpage inside iframe renders, it would tell how much iframe height should be increased so that my entire content is visible without iframe scroll + // 3. Parent window would check what iframe height can be set according to parent Element + this.actionManager.on("dimension-changed", (e) => { + const { data } = e.detail; + const iframe = this.iframe!; + if (!iframe) { + // Iframe might be pre-rendering + return; + } + let proposedHeightByIframeWebsite = parseFloat(getComputedStyle(iframe).height) + data.hiddenHeight; + iframe.style.height = proposedHeightByIframeWebsite; + }); + + this.actionManager.on("iframeReady", (e) => { + this.iframeReady = true; + this.doInIframe({ method: "parentKnowsIframeReady", arg: undefined }); + this.iframeDoQueue.forEach(({ method, arg }) => { + this.doInIframe({ method, arg }); + }); + }); + } +} + +globalCal.instance = new Cal("", globalCal.q!); + +for (let [ns, api] of Object.entries(globalCal.ns!)) { + api.instance = new Cal(ns, api.q!); +} + +/** + * Intercepts all postmessages and fires action in corresponding actionManager + */ +window.addEventListener("message", (e) => { + const detail = e.data; + const fullType = detail.fullType; + const parsedAction = SdkActionManager.parseAction(fullType); + if (!parsedAction) { + return; + } + const actionManager = Cal.actionsManagers[parsedAction.ns]; + if (!actionManager) { + throw new Error("Unhandled Action" + parsedAction); + } + actionManager.fire(parsedAction.type, detail.data); +}); + +document.addEventListener("click", (e) => { + const htmlElement = e.target; + if (!(htmlElement instanceof HTMLElement)) { + return; + } + const path = htmlElement.dataset.calLink; + if (!path) { + return; + } + // TODO: Add an option to check which cal instance should be used for this. + globalCal("modal", { + calLink: path, + }); +}); + +customElements.define("cal-modal-box", ModalBox); diff --git a/packages/embeds/embed-core/src/sdk-action-manager.ts b/packages/embeds/embed-core/src/sdk-action-manager.ts new file mode 100644 index 0000000000..68b4b95925 --- /dev/null +++ b/packages/embeds/embed-core/src/sdk-action-manager.ts @@ -0,0 +1,58 @@ +type Namespace = string; +type CustomEventDetail = Record; + +function _fireEvent(fullName: string, detail: CustomEventDetail) { + const event = new window.CustomEvent(fullName, { + detail: detail, + }); + + window.dispatchEvent(event); +} + +export class SdkActionManager { + namespace: Namespace; + + static parseAction(fullType: string) { + if (!fullType) { + return null; + } + //FIXME: Ensure that any action if it has :, it is properly encoded. + const [cal, calNamespace, type] = fullType.split(":"); + if (cal !== "CAL") { + return null; + } + return { + ns: calNamespace, + type, + }; + } + + getFullActionName(name: string) { + return this.namespace ? `CAL:${this.namespace}:${name}` : `CAL::${name}`; + } + + fire(name: string, data: CustomEventDetail) { + const fullName = this.getFullActionName(name); + const detail = { + type: name, + namespace: this.namespace, + fullType: fullName, + data, + }; + + _fireEvent(fullName, detail); + + // Wildcard Event + _fireEvent(this.getFullActionName("*"), detail); + } + + on(name: string, callback: (arg0: CustomEvent) => void) { + const fullName = this.getFullActionName(name); + window.addEventListener(fullName, callback as EventListener); + } + + constructor(ns: string | null) { + ns = ns || ""; + this.namespace = ns; + } +} diff --git a/packages/embeds/embed-core/src/sdk-event.ts b/packages/embeds/embed-core/src/sdk-event.ts new file mode 100644 index 0000000000..3d33d9f819 --- /dev/null +++ b/packages/embeds/embed-core/src/sdk-event.ts @@ -0,0 +1,10 @@ +/** + * @file + * This module is supposed to instantiate the SDK with appropriate namespace + */ +import { SdkActionManager } from "./sdk-action-manager"; + +export let sdkActionManager: SdkActionManager | null = null; +if (typeof window !== "undefined") { + sdkActionManager = new SdkActionManager(new URL(document.URL).searchParams.get("embed")); +} diff --git a/packages/embeds/embed-core/tsconfig.json b/packages/embeds/embed-core/tsconfig.json new file mode 100644 index 0000000000..7e6b6ec8e2 --- /dev/null +++ b/packages/embeds/embed-core/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@calcom/tsconfig/base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "baseUrl": "." + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/embeds/embed-core/vite.config.js b/packages/embeds/embed-core/vite.config.js new file mode 100644 index 0000000000..f30bc285ac --- /dev/null +++ b/packages/embeds/embed-core/vite.config.js @@ -0,0 +1,15 @@ +require("dotenv").config({ path: "../../../.env" }); + +const path = require("path"); +const { defineConfig } = require("vite"); + +module.exports = defineConfig({ + envPrefix: "NEXT_PUBLIC_", + build: { + lib: { + entry: path.resolve(__dirname, "src/embed.ts"), + name: "embed", + fileName: (format) => `embed.${format}.js`, + }, + }, +}); diff --git a/packages/embeds/embed-core/yarn.lock b/packages/embeds/embed-core/yarn.lock new file mode 100644 index 0000000000..8b1ed8b2cf --- /dev/null +++ b/packages/embeds/embed-core/yarn.lock @@ -0,0 +1,215 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +esbuild-android-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz#b868bbd9955a92309c69df628d8dd1945478b45c" + integrity sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ== + +esbuild-android-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz#e7d6430555e8e9c505fd87266bbc709f25f1825c" + integrity sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ== + +esbuild-darwin-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz#4dc7484127564e89b4445c0a560a3cb50b3d68e1" + integrity sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g== + +esbuild-darwin-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz#469e59c665f84a8ed323166624c5e7b9b2d22ac1" + integrity sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ== + +esbuild-freebsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz#895df03bf5f87094a56c9a5815bf92e591903d70" + integrity sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA== + +esbuild-freebsd-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz#0b72a41a6b8655e9a8c5608f2ec1afdcf6958441" + integrity sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA== + +esbuild-linux-32@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz#43b8ba3803b0bbe7f051869c6a8bf6de1e95de28" + integrity sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw== + +esbuild-linux-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz#dc8072097327ecfadba1735562824ce8c05dd0bd" + integrity sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg== + +esbuild-linux-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz#c52b58cbe948426b1559910f521b0a3f396f10b8" + integrity sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ== + +esbuild-linux-arm@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz#df869dbd67d4ee3a04b3c7273b6bd2b233e78a18" + integrity sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw== + +esbuild-linux-mips64le@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz#a2b646d9df368b01aa970a7b8968be6dd6b01d19" + integrity sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A== + +esbuild-linux-ppc64le@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz#9a21af766a0292578a3009c7408b8509cac7cefd" + integrity sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA== + +esbuild-linux-riscv64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz#344a27f91568056a5903ad5841b447e00e78d740" + integrity sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg== + +esbuild-linux-s390x@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz#73a7309bd648a07ef58f069658f989a5096130db" + integrity sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg== + +esbuild-netbsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz#482a587cdbd18a6c264a05136596927deb46c30a" + integrity sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q== + +esbuild-openbsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz#e99f8cdc63f1628747b63edd124d53cf7796468d" + integrity sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw== + +esbuild-sunos-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz#8611d825bcb8239c78d57452e83253a71942f45c" + integrity sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg== + +esbuild-windows-32@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz#c06374206d4d92dd31d4fda299b09f51a35e82f6" + integrity sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw== + +esbuild-windows-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz#756631c1d301dfc0d1a887deed2459ce4079582f" + integrity sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg== + +esbuild-windows-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz#ad7e187193dcd18768b16065a950f4441d7173f4" + integrity sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg== + +esbuild@^0.14.14: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.27.tgz#41fe0f1b6b68b9f77cac025009bc54bb96e616f1" + integrity sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q== + optionalDependencies: + esbuild-android-64 "0.14.27" + esbuild-android-arm64 "0.14.27" + esbuild-darwin-64 "0.14.27" + esbuild-darwin-arm64 "0.14.27" + esbuild-freebsd-64 "0.14.27" + esbuild-freebsd-arm64 "0.14.27" + esbuild-linux-32 "0.14.27" + esbuild-linux-64 "0.14.27" + esbuild-linux-arm "0.14.27" + esbuild-linux-arm64 "0.14.27" + esbuild-linux-mips64le "0.14.27" + esbuild-linux-ppc64le "0.14.27" + esbuild-linux-riscv64 "0.14.27" + esbuild-linux-s390x "0.14.27" + esbuild-netbsd-64 "0.14.27" + esbuild-openbsd-64 "0.14.27" + esbuild-sunos-64 "0.14.27" + esbuild-windows-32 "0.14.27" + esbuild-windows-64 "0.14.27" + esbuild-windows-arm64 "0.14.27" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +nanoid@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@^8.4.6: + version "8.4.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" + integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== + dependencies: + nanoid "^3.3.1" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +resolve@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^2.59.0: + version "2.70.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.1.tgz#824b1f1f879ea396db30b0fc3ae8d2fead93523e" + integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== + optionalDependencies: + fsevents "~2.3.2" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +vite@^2.8.6: + version "2.8.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.6.tgz#32d50e23c99ca31b26b8ccdc78b1d72d4d7323d3" + integrity sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug== + dependencies: + esbuild "^0.14.14" + postcss "^8.4.6" + resolve "^1.22.0" + rollup "^2.59.0" + optionalDependencies: + fsevents "~2.3.2" diff --git a/packages/embeds/embed-react/README.md b/packages/embeds/embed-react/README.md new file mode 100644 index 0000000000..38db8aeadc --- /dev/null +++ b/packages/embeds/embed-react/README.md @@ -0,0 +1,14 @@ +# cal-react + +Makes the embed available as a React component. + +To add the embed on a webpage built using React. Follow the steps + +```bash +yarn add @calcom/embed-react +``` + +```jsx +import Cal from "@calcom/embed-react" + +``` diff --git a/packages/embeds/embed-react/env.d.ts b/packages/embeds/embed-react/env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/embeds/embed-react/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/embeds/embed-react/index.html b/packages/embeds/embed-react/index.html new file mode 100644 index 0000000000..e70064fa31 --- /dev/null +++ b/packages/embeds/embed-react/index.html @@ -0,0 +1,10 @@ + + + + + + + +
+ + diff --git a/packages/embeds/embed-react/package.json b/packages/embeds/embed-react/package.json new file mode 100644 index 0000000000..26693a53c5 --- /dev/null +++ b/packages/embeds/embed-react/package.json @@ -0,0 +1,17 @@ +{ + "name": "@calcom/embed-react", + "version": "0.1.0", + "description": "Embed Cal Booking anywhere", + "scripts": { + "dev": "vite --port=3003 --open", + "build": "vite build", + "preview": "vite preview", + "type-check": "tsc --pretty --noEmit", + "lint": "eslint --ext .ts,.js,.tsx,.jsx ./src" + }, + "main": "src/Cal.tsx", + "devDependencies": { + "vite": "^2.8.6", + "eslint": "^8.10.0" + } +} diff --git a/packages/embeds/embed-react/src/Cal.tsx b/packages/embeds/embed-react/src/Cal.tsx new file mode 100644 index 0000000000..0cf976c5c8 --- /dev/null +++ b/packages/embeds/embed-react/src/Cal.tsx @@ -0,0 +1,33 @@ +import { useEffect, useRef } from "react"; + +import useEmbed from "./useEmbed"; + +export default function Cal({ + calLink, + config, + embedJsUrl, +}: { + calLink: string; + config?: any; + embedJsUrl?: string; +}) { + const Cal = useEmbed(embedJsUrl); + const ref = useRef(null); + useEffect(() => { + if (!Cal) { + return; + } + Cal("init"); + Cal("inline", { + elementOrSelector: ref.current, + calLink, + config, + }); + }, [Cal, calLink, config]); + + if (!Cal) { + return
Loading {calLink}
; + } + + return
; +} diff --git a/packages/embeds/embed-react/src/useEmbed.ts b/packages/embeds/embed-react/src/useEmbed.ts new file mode 100644 index 0000000000..be11ad74a1 --- /dev/null +++ b/packages/embeds/embed-react/src/useEmbed.ts @@ -0,0 +1,13 @@ +import { useEffect, useState } from "react"; + +import EmbedSnippet from "@calcom/embed-snippet"; + +export default function useEmbed(embedJsUrl?: string) { + const [globalCal, setGlobalCal] = useState>(); + useEffect(() => { + setGlobalCal(() => { + return EmbedSnippet(embedJsUrl); + }); + }, []); + return globalCal; +} diff --git a/packages/embeds/embed-react/test-cal.tsx b/packages/embeds/embed-react/test-cal.tsx new file mode 100644 index 0000000000..dfe7aa9f45 --- /dev/null +++ b/packages/embeds/embed-react/test-cal.tsx @@ -0,0 +1,24 @@ +import ReactDom from "react-dom"; + +import Cal from "@calcom/embed-react"; + +function App() { + return ( + <> +

+ There is Cal component below me +

+ + + ); +} +ReactDom.render(, document.getElementById("root")); diff --git a/packages/embeds/embed-react/tsconfig.json b/packages/embeds/embed-react/tsconfig.json new file mode 100644 index 0000000000..2674008254 --- /dev/null +++ b/packages/embeds/embed-react/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@calcom/tsconfig/base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "baseUrl": ".", + "jsx": "preserve" + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/embeds/embed-snippet/README.md b/packages/embeds/embed-snippet/README.md new file mode 100644 index 0000000000..0bde031132 --- /dev/null +++ b/packages/embeds/embed-snippet/README.md @@ -0,0 +1,11 @@ +# embed-snippet + +This is the code snippet that is to be installed by the user on his website. + +## Development + +`yarn build` will generate dist/snippet.es.js + +- which can be used as `` + diff --git a/packages/embeds/embed-snippet/env.d.ts b/packages/embeds/embed-snippet/env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/embeds/embed-snippet/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/embeds/embed-snippet/package.json b/packages/embeds/embed-snippet/package.json new file mode 100644 index 0000000000..8525e2d04c --- /dev/null +++ b/packages/embeds/embed-snippet/package.json @@ -0,0 +1,15 @@ +{ + "name": "@calcom/embed-snippet", + "version": "0.1.0", + "main": "src/index.ts", + "scripts": { + "dev": "vite --port=3002", + "build": "vite build", + "preview": "vite preview", + "type-check": "tsc --pretty --noEmit", + "lint": "eslint --ext .ts,.js src" + }, + "devDependencies": { + "eslint": "^8.10.0" + } +} diff --git a/packages/embeds/embed-snippet/src/index.ts b/packages/embeds/embed-snippet/src/index.ts new file mode 100644 index 0000000000..78c90d5003 --- /dev/null +++ b/packages/embeds/embed-snippet/src/index.ts @@ -0,0 +1,58 @@ +/** + * As we want to keep control on the size of this snippet but we want some portion of it to be still readable. + * So, write the code that you need directly but keep it short. + */ +import { Cal as CalClass, Instruction, InstructionQueue } from "@calcom/embed-core/src/embed"; + +export interface GlobalCal { + (methodName: string, arg?: any): void; + /** Marks that the embed.js is loaded. Avoids re-downloading it. */ + loaded?: boolean; + /** Maintains a queue till the time embed.js isn't loaded */ + q?: InstructionQueue; + /** If user registers multiple namespaces, those are available here */ + ns?: Record; + instance?: CalClass; +} + +export interface CalWindow extends Window { + Cal?: GlobalCal; +} + +export default function EmbedSnippet(url = "https://cal.com/embed.js") { + /*! Copy the code below and paste it in script tag of your website */ + (function (C: CalWindow, A, L) { + let d = C.document; + C.Cal = + C.Cal || + function () { + let cal = C.Cal!; + let ar = arguments; + if (!cal.loaded) { + cal.ns = {}; + cal.q = cal.q || []; + d.head.appendChild(d.createElement("script")).src = A; + cal.loaded = true; + } + + if (ar[0] === L) { + const api: { (): void; q: any[] } = function () { + api.q.push(arguments); + }; + const namespace = arguments[1]; + api.q = api.q || []; + namespace ? (cal.ns![namespace] = api) : null; + return; + } + cal.q!.push(ar as unknown as Instruction); + }; + })( + window, + //! Replace it with "https://cal.com/embed.js" or the URL where you have embed.js installed + url, + "init" + ); + /*! Copying ends here. */ + + return (window as CalWindow).Cal; +} diff --git a/packages/embeds/embed-snippet/tsconfig.json b/packages/embeds/embed-snippet/tsconfig.json new file mode 100644 index 0000000000..42b3877672 --- /dev/null +++ b/packages/embeds/embed-snippet/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@calcom/tsconfig/base.json", + "compilerOptions": { + "baseUrl": ".", + "module": "esnext" + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/embeds/embed-snippet/vite.config.js b/packages/embeds/embed-snippet/vite.config.js new file mode 100644 index 0000000000..81ba9a24df --- /dev/null +++ b/packages/embeds/embed-snippet/vite.config.js @@ -0,0 +1,12 @@ +const path = require("path"); +const { defineConfig } = require("vite"); + +module.exports = defineConfig({ + build: { + lib: { + entry: path.resolve(__dirname, "index.ts"), + name: "snippet", + fileName: (format) => `snippet.${format}.js`, + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index cc080cf043..a7c3640e6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3357,11 +3357,16 @@ dependencies: prop-types "^15.7.2" -"@stripe/stripe-js@^1.16.0", "@stripe/stripe-js@^1.17.1": +"@stripe/stripe-js@^1.16.0": version "1.24.0" resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.24.0.tgz#d23977f364565981f8ab30b1b540e367f72abc5c" integrity sha512-8CEILOpzoRhGwvgcf6y+BlPyEq1ZqxAv3gsX7LvokFYvbcyH72GRcHQMGXuZS3s7HqfYQuTSFrvZNL/qdkgA9Q== +"@stripe/stripe-js@^1.17.1": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.26.0.tgz#45670924753c01e18d0544ea1f1067b474aaa96f" + integrity sha512-4R1vC75yKaCVFARW3bhelf9+dKt4NP4iZY/sIjGK7AAMBVvZ47eG74NvsAIUdUnhOXSWFMjdFWqv+etk5BDW4g== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -5897,7 +5902,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.3, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1: +debug@4, debug@4.3.3, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== @@ -5911,6 +5916,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@~4.3.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -6453,6 +6465,132 @@ es6-symbol@^3.1.1, es6-symbol@^3.1.3: d "^1.0.1" ext "^1.1.2" +esbuild-android-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz#b868bbd9955a92309c69df628d8dd1945478b45c" + integrity sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ== + +esbuild-android-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz#e7d6430555e8e9c505fd87266bbc709f25f1825c" + integrity sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ== + +esbuild-darwin-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz#4dc7484127564e89b4445c0a560a3cb50b3d68e1" + integrity sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g== + +esbuild-darwin-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz#469e59c665f84a8ed323166624c5e7b9b2d22ac1" + integrity sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ== + +esbuild-freebsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz#895df03bf5f87094a56c9a5815bf92e591903d70" + integrity sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA== + +esbuild-freebsd-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz#0b72a41a6b8655e9a8c5608f2ec1afdcf6958441" + integrity sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA== + +esbuild-linux-32@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz#43b8ba3803b0bbe7f051869c6a8bf6de1e95de28" + integrity sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw== + +esbuild-linux-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz#dc8072097327ecfadba1735562824ce8c05dd0bd" + integrity sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg== + +esbuild-linux-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz#c52b58cbe948426b1559910f521b0a3f396f10b8" + integrity sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ== + +esbuild-linux-arm@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz#df869dbd67d4ee3a04b3c7273b6bd2b233e78a18" + integrity sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw== + +esbuild-linux-mips64le@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz#a2b646d9df368b01aa970a7b8968be6dd6b01d19" + integrity sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A== + +esbuild-linux-ppc64le@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz#9a21af766a0292578a3009c7408b8509cac7cefd" + integrity sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA== + +esbuild-linux-riscv64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz#344a27f91568056a5903ad5841b447e00e78d740" + integrity sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg== + +esbuild-linux-s390x@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz#73a7309bd648a07ef58f069658f989a5096130db" + integrity sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg== + +esbuild-netbsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz#482a587cdbd18a6c264a05136596927deb46c30a" + integrity sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q== + +esbuild-openbsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz#e99f8cdc63f1628747b63edd124d53cf7796468d" + integrity sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw== + +esbuild-sunos-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz#8611d825bcb8239c78d57452e83253a71942f45c" + integrity sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg== + +esbuild-windows-32@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz#c06374206d4d92dd31d4fda299b09f51a35e82f6" + integrity sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw== + +esbuild-windows-64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz#756631c1d301dfc0d1a887deed2459ce4079582f" + integrity sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg== + +esbuild-windows-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz#ad7e187193dcd18768b16065a950f4441d7173f4" + integrity sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg== + +esbuild@^0.14.14: + version "0.14.27" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.27.tgz#41fe0f1b6b68b9f77cac025009bc54bb96e616f1" + integrity sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q== + optionalDependencies: + esbuild-android-64 "0.14.27" + esbuild-android-arm64 "0.14.27" + esbuild-darwin-64 "0.14.27" + esbuild-darwin-arm64 "0.14.27" + esbuild-freebsd-64 "0.14.27" + esbuild-freebsd-arm64 "0.14.27" + esbuild-linux-32 "0.14.27" + esbuild-linux-64 "0.14.27" + esbuild-linux-arm "0.14.27" + esbuild-linux-arm64 "0.14.27" + esbuild-linux-mips64le "0.14.27" + esbuild-linux-ppc64le "0.14.27" + esbuild-linux-riscv64 "0.14.27" + esbuild-linux-s390x "0.14.27" + esbuild-netbsd-64 "0.14.27" + esbuild-openbsd-64 "0.14.27" + esbuild-sunos-64 "0.14.27" + esbuild-windows-32 "0.14.27" + esbuild-windows-64 "0.14.27" + esbuild-windows-arm64 "0.14.27" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -12954,7 +13092,12 @@ react-fit@^1.4.0: prop-types "^15.6.0" tiny-warning "^1.0.0" -react-hook-form@^7.16.2, react-hook-form@^7.20.4: +react-hook-form@^7.16.2: + version "7.29.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.29.0.tgz#5e7e41a483b70731720966ed8be52163ea1fecf1" + integrity sha512-NcJqWRF6el5HMW30fqZRt27s+lorvlCCDbTpAyHoodQeYWXgQCvZJJQLC1kRMKdrJknVH0NIg3At6TUzlZJFOQ== + +react-hook-form@^7.20.4: version "7.28.0" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.28.0.tgz#40f385da1f31a3c26bb7491d7d77c111b6ad6909" integrity sha512-mmLpT86BkMGPr0r6ca8zxV0WH4Y1FW5MKs7Rq1+uHLVeeg5pSWbF5Z/qLCnM5vPVblHNM6lRBRRotnfEAf0ALA== @@ -13610,6 +13753,13 @@ rollup-plugin-polyfill-node@0.8.0: dependencies: "@rollup/plugin-inject" "^4.0.0" +rollup@^2.59.0: + version "2.70.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.1.tgz#824b1f1f879ea396db30b0fc3ae8d2fead93523e" + integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== + optionalDependencies: + fsevents "~2.3.2" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -15550,6 +15700,18 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +vite@^2.8.6: + version "2.8.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.6.tgz#32d50e23c99ca31b26b8ccdc78b1d72d4d7323d3" + integrity sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug== + dependencies: + esbuild "^0.14.14" + postcss "^8.4.6" + resolve "^1.22.0" + rollup "^2.59.0" + optionalDependencies: + fsevents "~2.3.2" + void-elements@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" @@ -16315,12 +16477,12 @@ zod-prisma@^0.5.4: parenthesis "^3.1.8" ts-morph "^13.0.2" -zod@^3.14.2: +zod@^3.14.2, zod@^3.9.5: version "3.14.3" resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.3.tgz#60e86341c05883c281fe96a0e79acea48a09f123" integrity sha512-OzwRCSXB1+/8F6w6HkYHdbuWysYWnAF4fkRgKDcSFc54CE+Sv0rHXKfeNUReGCrHukm1LNpi6AYeXotznhYJbQ== -zod@^3.8.2, zod@^3.9.5: +zod@^3.8.2: version "3.13.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.13.4.tgz#5d6fe03ef4824a637d7ef50b5441cf6ab3acede0" integrity sha512-LZRucWt4j/ru5azOkJxCfpR87IyFDn8h2UODdqvXzZLb3K7bb9chUrUIGTy3BPsr8XnbQYfQ5Md5Hu2OYIo1mg==