[Feature]Booking Embed (#2227)

This commit is contained in:
Hariom Balhara 2022-03-31 14:15:47 +05:30 committed by GitHub
parent 4e9c3be598
commit 4a58da62d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1740 additions and 22 deletions

View File

@ -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<Dayjs | null>(date);
const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton");
const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton");
const [month, setMonth] = useState<string>("");
const [year, setYear] = useState<string>("");
@ -274,6 +278,9 @@ function DatePicker({
<button
onClick={() => onDatePicked(browsingDate.date(day.date))}
disabled={day.disabled}
style={
day.disabled ? { ...disabledDateButtonEmbedStyles } : { ...enabledDateButtonEmbedStyles }
}
className={classNames(
"absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm text-center",
"hover:border-brand hover:border dark:hover:border-white",

View File

@ -1,5 +1,4 @@
import React from "react";
import { Control } from "react-hook-form";
import BasePhoneInput, { Props } from "react-phone-number-input/react-hook-form";
import "react-phone-number-input/style.css";

View File

@ -1,4 +1,5 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Maybe } from "@trpc/server";
@ -27,6 +28,10 @@ function applyThemeAndAddListener(theme: string) {
// makes sure the ui doesn't flash
export default function useTheme(theme?: Maybe<string>) {
const [isReady, setIsReady] = useState(false);
const router = useRouter();
// Embed UI configuration takes more precedence over App Configuration
theme = (router.query.theme as string | null) || theme;
useEffect(() => {
// TODO: isReady doesn't seem required now. This is also impacting PSI Score for pages which are using isReady.

View File

@ -8,6 +8,7 @@ const withTM = require("next-transpile-modules")([
"@calcom/prisma",
"@calcom/stripe",
"@calcom/ui",
"@calcom/embed-core",
]);
const { i18n } = require("./next-i18next.config");

View File

@ -8,7 +8,9 @@ import React, { useState } from "react";
import { Toaster } from "react-hot-toast";
import { JSONObject } from "superjson/dist/types";
import { useLocale } from "@lib/hooks/useLocale";
import { sdkActionManager, useEmbedStyles } from "@calcom/embed-core";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -30,6 +32,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const { user, eventTypes } = props;
const { t } = useLocale();
const router = useRouter();
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
@ -48,9 +51,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="mb-8 text-center">
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername}></AvatarSSR>
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername} />
<h1 className="font-cal mb-1 text-3xl text-neutral-900 dark:text-white">
{nameOrUsername}
<span>{nameOrUsername}</span>
{user.verified && (
<BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
)}
@ -71,7 +74,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
eventTypes.map((type) => (
<div
key={type.id}
style={{ display: "flex" }}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="hover:border-brand group relative rounded-sm border border-neutral-200 bg-white hover:bg-gray-50 dark:border-neutral-700 dark:bg-gray-800 dark:hover:border-neutral-600">
<ArrowRightIcon className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
@ -91,6 +94,10 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
"You must verify a wallet with a token belonging to the specified smart contract first",
"error"
);
} else {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}
}}
className="block w-full px-6 py-4"

View File

@ -3,6 +3,8 @@ import Head from "next/head";
// import { ReactQueryDevtools } from "react-query/devtools";
import superjson from "superjson";
import "@calcom/embed-core/src/embed-iframe";
import AppProviders, { AppProps } from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config";

View File

@ -5,10 +5,12 @@ type Props = Record<string, unknown> & DocumentProps;
class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
const isEmbed = ctx.req?.url?.includes("embed");
return { ...initialProps, isEmbed };
}
render() {
const props = this.props;
const { locale } = this.props.__NEXT_DATA__;
const dir = locale === "ar" || locale === "he" ? "rtl" : "ltr";
@ -23,7 +25,9 @@ class MyDocument extends Document<Props> {
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" content="#ffffff" />
</Head>
<body className="bg-gray-100 dark:bg-neutral-900">
{/* Keep the embed hidden till parent initializes and gives it the appropriate styles */}
<body className="bg-gray-100 dark:bg-neutral-900" style={props.isEmbed ? { display: "none" } : {}}>
<Main />
<NextScript />
</body>

View File

@ -10,12 +10,13 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { sdkActionManager } from "@calcom/embed-core";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { EmailInput } from "@calcom/ui/form/fields";
import { asStringOrThrow, asStringOrNull } from "@lib/asStringOrNull";
import { getEventName } from "@lib/event";
import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import prisma from "@lib/prisma";
@ -40,11 +41,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
const { isReady, Theme } = useTheme(props.profile.theme);
useEffect(() => {
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
}, []);
const { eventType } = props;
const attendeeName = typeof name === "string" ? name : "Nameless";
@ -57,6 +54,26 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
};
const eventName = getEventName(eventNameObject);
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
useEffect(() => {
const users = eventType.users;
// TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
sdkActionManager!.fire("bookingSuccessful", {
eventType,
date: date.toString(),
duration: eventType.length,
organizer: {
name: users[0].name || "Nameless",
email: users[0].email || "Email-less",
timeZone: users[0].timeZone,
},
confirmed: !needsConfirmation,
// TODO: Add payment details
});
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
}, [eventType, needsConfirmation]);
function eventLink(): string {
const optional: { location?: string } = {};
@ -87,8 +104,6 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
return encodeURIComponent(event.value ? event.value : false);
}
const needsConfirmation = props.eventType.requiresConfirmation && reschedule != "true";
return (
(isReady && (
<div className="h-screen bg-neutral-100 dark:bg-neutral-900" data-testid="success-page">
@ -322,6 +337,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
},
},
team: {
@ -351,6 +368,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
},
});
if (user) {

View File

@ -5,6 +5,7 @@
"workspaces": [
"apps/*",
"packages/*",
"packages/embeds/*",
"packages/app-store/*"
],
"scripts": {

View File

@ -0,0 +1,8 @@
# Embeds
This folder contains all the various flavours of embeds.
`core` contains the core library written in vanilla JS that manages the embed.
`snippet` contains the Vanilla JS Code Snippet that can be installed on any website and would automatically fetch the `core` library.
Please see the respective folder READMEs for details on them.

View File

@ -0,0 +1,95 @@
# embed-core
See [index.html](index.html) to understand how it can be used.
## Features
- The Embed SDK can be added asynchronously
- You can add it through any tag manager like GTM if you like[Need to Test]
- Available configurations are
- `theme`
- Prefilling of
- `name`
- `email`
- `notes`
- `guests`
## How to use embed on any webpage no matter what framework.
- _Step-1._ Install the snippet
```javascript
(function (C, 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 = function () {
api.q.push(arguments);
};
const namespace = arguments[1];
api.q = api.q || [];
namespace ? (cal.ns[namespace] = api) : null;
return;
}
cal.q.push(ar);
};
})(window, "https://cal.com/embed.js", "init");
```
- _Step-2_. Give `init` instruction to it. It creates a queue so that even without embed.js being fetched, you can give instructions to embed.
```javascript
Cal("init) // Creates default instance. Give instruction to it as Cal("instruction")
```
**Optionally** if you want to install another instance of embed you can do
```javascript
Cal("init", "NAME_YOUR_OTHER_INSTANCE"); // Creates a named instance. Give instructions to it as Cal.ns.NAME_YOUR_OTHER_INSTANCE("instruction")
```
- Step-1 and Step-2 must be followed in same order. After that you can give various instructions to embed as you like.
## Supported Instructions
Consider an instruction as a function with that name and that would be called with the given argument.
`inline` - Appends embed inline as the child of the element.
- `elementOrSelector` - Give it either a valid CSS selector or an HTMLElement instance directly
- `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL <https://cal.com/john>. It makes it easy to configure the calendar host once and use as many links you want with just usernames
`ui` - Configure UI for embed. Make it look part of your webpage.
- `styles` - It supports styling for `body` and `eventTypeListItem`. Right now we support just background on these two.
`preload` - If you want to open cal link on some action. Make it pop open instantly by preloading it.
- `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL <https://cal.com/john>
## Development
Run the following command and then you can test the embed in the automatically opened page `http://localhost:3002`
```bash
yarn dev
```
## Shipping to Production
```bash
yarn build
```
Make `dist/embed.umd.js` servable on URL http://cal.com/embed.js

9
packages/embeds/embed-core/env.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly NEXT_PUBLIC_WEBSITE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -0,0 +1,225 @@
<html>
<head>
<!-- <link rel="prerender" href="http://localhost:3000/free"> -->
<script>
if (!location.search.includes("nonResponsive")) {
document.write('<meta name="viewport" content="width=device-width"/>');
}
</script>
<script>
(function (C, 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 = function () {
api.q.push(arguments);
};
const namespace = arguments[1];
api.q = api.q || [];
namespace ? (cal.ns[namespace] = api) : null;
}
cal.q.push(ar);
};
})(window, "//localhost:3002/dist/embed.umd.js", "init");
</script>
<script>
Cal("init");
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
Cal("init", "second");
// Create a namespace "third". It can be accessed as Cal.ns.second with the exact same API as Cal
Cal("init", "third");
</script>
<style>
.debug {
/* border: 1px solid black; */
margin-bottom: 5px;
}
.loader {
color: green;
}
</style>
</head>
<body>
<h3>This page has a non responsive version accessible <a href="?nonResponsive">here</a></h3>
<h3>Pre-render test page available at <a href="?prerender-test">here</a></h3>
<div>
<button data-cal-link="free">Book with Free User</button>
<div>
<i
>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</i
>
</div>
</div>
<div id="namespaces-test">
<div class="debug">
<h2>Default Namespace(Cal)<i>[Black Theme][Guests(janedoe@gmail.com and test@gmail.com)]</i></h2>
<div>
<i><a href="?only=ns:default">Test in Zen Mode</a></i>
</div>
<i id="booking-status-"> You would see last Booking page action in my place </i>
<div id="cal-booking-place-" style="max-height: 30vh; overflow: scroll">
<div>
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
</div>
<div class="loader" id="cal-booking-loader-">Loading .....</div>
</div>
</div>
<div class="debug">
<h2>Namespace "second"(Cal.ns.second)[Custom Styling]</h2>
<div>
<i><a href="?only=ns:second">Test in Zen Mode</a></i>
</div>
<i id="booking-status-second">
<i>You would see last Booking page action in my place</i>
</i>
<div id="cal-booking-place-second">
<div>If you render booking embed in me, I won't restrict you. The entire page is yours.</div>
<button
onclick="(function () {Cal.ns.second('ui', {styles:{eventTypeListItem:{backgroundColor:'blue'}}})})()">
Change <code>eventTypeListItem</code> bg color
</button>
<button onclick="(function () {Cal.ns.second('ui', {styles:{body:{background:'red'}}})})()">
Change <code>body</code> bg color
</button>
<div class="loader" id="cal-booking-loader-second">Loading .....</div>
</div>
</div>
<div class="debug">
<h2>Namespace "third"(Cal.ns.third)</h2>
<div>
<i><a href="?only=ns:third">Test in Zen Mode</a></i>
</div>
<i id="booking-status-third">
<i>You would see last Booking page action in my place</i>
</i>
<div id="cal-booking-place-third" style="width: 30%">
<div>If you render booking embed in me, I would not let you be more than 30% wide</div>
<div class="loader" id="cal-booking-loader-third">Loading .....</div>
</div>
</div>
</div>
<script>
const searchParams = new URL(document.URL).searchParams;
const only = searchParams.get("only");
// In prerender-test, we would want to test just the prerender case and nothing else.
if (searchParams.get("prerender-test") === null) {
if (!only || only === "ns:default") {
Cal("inline", {
elementOrSelector: "#cal-booking-place-",
calLink: "pro?case=1",
config: {
name: "John Doe",
email: "johndoe@gmail.com",
notes: "Test Meeting",
guests: ["janedoe@gmail.com", "test@gmail.com"],
theme: "dark",
},
});
}
if (!only || only === "ns:second") {
// Bulk API is supported - Keep all configuration at one place.
Cal.ns.second(
[
"inline",
{
elementOrSelector: "#cal-booking-place-second",
calLink: "pro?case=2",
},
],
[
"ui",
{
styles: {
body: {
background: "white",
},
eventTypeListItem: {
backgroundColor: "#D3D3D3",
},
enabledDateButton: {
backgroundColor: "#D3D3D3",
},
disabledDateButton: {
backgroundColor: "lightslategray",
},
},
},
]
);
}
if (!only || only === "ns:third") {
Cal.ns.third(
[
"inline",
{
elementOrSelector: "#cal-booking-place-third",
calLink: "pro?case=3",
},
],
[
"ui",
{
styles: {
body: {
background: "white",
},
},
},
]
);
}
} else {
document.getElementById("namespaces-test").style.display = "none";
}
Cal("preload", {
calLink: "free",
});
</script>
<script>
const callback = function (e) {
const detail = e.detail;
const namespace = detail.namespace;
if (detail.type === "linkReady") {
document.getElementById("cal-booking-loader-" + namespace).remove();
}
document.getElementById(`booking-status-${namespace}`).innerHTML = JSON.stringify(e.detail);
};
Cal("on", {
action: "*",
callback,
});
Cal.ns.second("on", {
action: "*",
callback,
});
Cal.ns.third("on", {
action: "*",
callback,
});
</script>
</body>
</html>

View File

@ -0,0 +1,2 @@
export * from "./src/embed-iframe";
export * from "./src/sdk-event";

View File

@ -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"
}
}

View File

@ -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 = `
<style>
.backdrop {
position:fixed;
width:100%;
height:100%;
top:0;
left:0;
z-index:99999999;
display:block;
background-color:rgb(5,5,5, 0.8)
}
@media only screen and (min-width:600px) {
.modal-box {
margin:0 auto;
margin-top:20px;
margin-bottom:20px;
position:absolute;
width:50%;
height: 80%;
top:50%;
left:50%;
transform: translateY(-50%) translateX(-50%);
overflow: scroll;
}
}
@media only screen and (max-width:600px) {
.modal-box {
width: 100%;
height: 80%;
position:fixed;
top:50px;
left:0;
right: 0;
margin: 0;
}
}
.header {
position: relative;
float:right;
top: 10px;
}
.close {
font-size: 30px;
left: -20px;
position: relative;
color:white;
cursor: pointer;
}
</style>
<div class="backdrop">
<div class="header">
<span class="close">&times;</span>
</div>
<div class="modal-box">
<div class="body">
<slot></slot>
</div>
</div>
</div>
`;
this.attachShadow({ mode: "open" });
this.shadowRoot!.innerHTML = modalHtml;
}
}

View File

@ -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<CSSProperties, "background" | "backgroundColor">;
eventTypeListItem?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
enabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
disabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
}
type ElementName = keyof EmbedStyles;
type ReactEmbedStylesSetter = React.Dispatch<React.SetStateAction<EmbedStyles>>;
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<ElementName, ReactEmbedStylesSetter>,
};
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<string, any> = 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", {});
}

View File

@ -0,0 +1,6 @@
/**
These styles are applied to the entire page, so the selectors need to be specific
*/
.cal-embed {
border: 0px;
}

View File

@ -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<Record<"type" | "required", any>>(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<Namespace, SdkActionManager>;
static getQueryObject(config: Record<string, string>) {
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<string, string | string[]>;
}) {
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<string, string>;
}) {
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 = `<cal-modal-box></cal-modal-box>`;
template.content.children[0].appendChild(iframe);
document.body.appendChild(template.content);
}
on({
action,
callback,
}: {
action: Parameters<SdkActionManager["on"]>[0];
callback: Parameters<SdkActionManager["on"]>[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<typeof methods["ui"]>[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);

View File

@ -0,0 +1,58 @@
type Namespace = string;
type CustomEventDetail = Record<string, any>;
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<CustomEventDetail>) => void) {
const fullName = this.getFullActionName(name);
window.addEventListener(fullName, callback as EventListener);
}
constructor(ns: string | null) {
ns = ns || "";
this.namespace = ns;
}
}

View File

@ -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"));
}

View File

@ -0,0 +1,10 @@
{
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"baseUrl": "."
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -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`,
},
},
});

View File

@ -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"

View File

@ -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"
<Cal></Cal>
```

1
packages/embeds/embed-react/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,10 @@
<html>
<head>
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script type="module" src="./test-cal.tsx"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -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"
}
}

View File

@ -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<HTMLDivElement>(null);
useEffect(() => {
if (!Cal) {
return;
}
Cal("init");
Cal("inline", {
elementOrSelector: ref.current,
calLink,
config,
});
}, [Cal, calLink, config]);
if (!Cal) {
return <div>Loading {calLink}</div>;
}
return <div ref={ref}></div>;
}

View File

@ -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<ReturnType<typeof EmbedSnippet>>();
useEffect(() => {
setGlobalCal(() => {
return EmbedSnippet(embedJsUrl);
});
}, []);
return globalCal;
}

View File

@ -0,0 +1,24 @@
import ReactDom from "react-dom";
import Cal from "@calcom/embed-react";
function App() {
return (
<>
<h1>
There is <code>Cal</code> component below me
</h1>
<Cal
embedJsUrl="//localhost:3002/dist/embed.umd.js"
calLink="pro"
config={{
name: "John Doe",
email: "johndoe@gmail.com",
notes: "Test Meeting",
guests: ["janedoe@gmail.com"],
theme: "dark",
}}></Cal>
</>
);
}
ReactDom.render(<App></App>, document.getElementById("root"));

View File

@ -0,0 +1,11 @@
{
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"baseUrl": ".",
"jsx": "preserve"
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -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 `<script type="module" src=...`
- You can also copy the appropriate portion of the code and install it directly as `<script>CODE_SUGGESTED_TO_BE_COPIED</script>`

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -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"
}
}

View File

@ -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<string, GlobalCal>;
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;
}

View File

@ -0,0 +1,9 @@
{
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"baseUrl": ".",
"module": "esnext"
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -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`,
},
},
});

172
yarn.lock
View File

@ -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==