Embed Miscellaneous Improvements and Fixes (#2499)

This commit is contained in:
Hariom Balhara 2022-04-25 10:03:00 +05:30 committed by GitHub
parent 53d7e57142
commit 93c75b5fef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 1139 additions and 746 deletions

View File

@ -9,8 +9,8 @@ The Embed allows your website visitors to book a meeting with you directly from
## Install on any website
- _Step-1._ Install the Vanilla JS Snippet
```javascript
```html
<script>
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
@ -38,14 +38,10 @@ The Embed allows your website visitors to book a meeting with you directly from
}
p(cal, ar);
};
})(window, "https://cal.com/embed.js", "init");
```
- _Step-2_. Initialize it
```javascript
Cal("init")
```
})(window, "https://cal.com/embed.js", "init");
Cal("init")
</script>
```
## Install with a Framework
@ -72,18 +68,20 @@ Show the embed inline inside a container element. It would take the width and he
<details>
<summary>_Vanilla JS_</summary>
```javascript
Cal("inline", {
elementOrSelector: "Your Embed Container Selector Path", // You can also provide an element directly
calLink: "jane", // The link that you want to embed. It would open https://cal.com/jane in embed
config: {
name: "John Doe", // Prefill Name
email: "johndoe@gmail.com", // Prefill Email
notes: "Test Meeting", // Prefill Notes
guests: ["janedoe@gmail.com", "test@gmail.com"], // Prefill Guests
theme: "dark", // "dark" or "light" theme
},
});
```html
<script>
Cal("inline", {
elementOrSelector: "Your Embed Container Selector Path", // You can also provide an element directly
calLink: "jane", // The link that you want to embed. It would open https://cal.com/jane in embed
config: {
name: "John Doe", // Prefill Name
email: "johndoe@gmail.com", // Prefill Email
notes: "Test Meeting", // Prefill Notes
guests: ["janedoe@gmail.com", "test@gmail.com"], // Prefill Guests
theme: "dark", // "dark" or "light" theme
},
});
</script>
```
</details>
@ -146,8 +144,10 @@ Consider an instruction as a function with that name and that would be called wi
Appends embed inline as the child of the element.
```javascript
```html
<script>
Cal("inline", { elementOrSelector, calLink });
</script>
````
- `elementOrSelector` - Give it either a valid CSS selector or an HTMLElement instance directly
@ -158,8 +158,10 @@ Cal("inline", { elementOrSelector, calLink });
Configure UI for embed. Make it look part of your webpage.
```javascript
```html
<script>
Cal("inline", { styles });
</script>
```
- `styles` - It supports styling for `body` and `eventTypeListItem`. Right now we support just background on these two.
@ -170,15 +172,18 @@ Usage:
If you want to open cal link on some action. Make it pop open instantly by preloading it.
```javascript
```html
<script>
Cal("preload", { calLink });
</script>
```
- `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]()
## Actions
You can listen to an action that occurs in embedded cal link as follows. You can think of them as DOM events. We are avoiding the term "events" to not confuse it with Cal Events.
```javascript
```html
<script>
Cal("on", {
action: "ANY_ACTION_NAME",
callback: (e)=>{
@ -188,6 +193,7 @@ Cal("on", {
const {data, type, namespace} = e.detail;
}
})
</script>
```
Following are the list of supported actions.

View File

@ -18,7 +18,14 @@ import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { useEmbedStyles, useIsEmbed, useIsBackgroundTransparent, sdkActionManager } from "@calcom/embed-core";
import {
useEmbedStyles,
useIsEmbed,
useIsBackgroundTransparent,
sdkActionManager,
useEmbedType,
useEmbedNonStylesConfig,
} from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -56,6 +63,8 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
const { t, i18n } = useLocale();
const { contracts } = useContracts();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
let isBackgroundTransparent = useIsBackgroundTransparent();
useExposePlanGlobally(plan);
useEffect(() => {
@ -146,18 +155,19 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div>
<main
className={
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed
? classNames("m-auto", selectedDate ? "max-w-5xl" : "max-w-3xl")
? classNames(selectedDate ? "max-w-5xl" : "max-w-3xl")
: "transition-max-width mx-auto my-0 duration-500 ease-in-out md:my-24 " +
(selectedDate ? "max-w-5xl" : "max-w-3xl")
}>
(selectedDate ? "max-w-5xl" : "max-w-3xl")
)}>
{isReady && (
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent ? "" : "bg-white dark:bg-gray-800 sm:dark:border-gray-600",
"border-bookinglightest rounded-sm md:border",
"border-bookinglightest rounded-md md:border",
isEmbed ? "mx-auto" : selectedDate ? "max-w-5xl" : "max-w-3xl"
)}>
{/* mobile: details */}

View File

@ -18,7 +18,13 @@ import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { useMutation } from "react-query";
import { useIsEmbed, useIsBackgroundTransparent } from "@calcom/embed-core";
import {
useIsEmbed,
useEmbedStyles,
useIsBackgroundTransparent,
useEmbedType,
useEmbedNonStylesConfig,
} from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
@ -71,6 +77,8 @@ const BookingPage = ({
}: BookingPageProps) => {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
const { contracts } = useContracts();
const { data: session } = useSession();
@ -298,16 +306,17 @@ const BookingPage = ({
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<main
className={classNames(
isEmbed ? "mx-auto" : "mx-auto my-0 rounded-sm sm:my-24",
"max-w-3xl sm:border sm:dark:border-gray-600"
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "" : "sm:my-24",
"my-0 max-w-3xl "
)}>
{isReady && (
<div
className={classNames(
"overflow-hidden",
"main overflow-hidden",
isEmbed ? "" : "border border-gray-200",
isBackgroundTransparent ? "" : "bg-white dark:border-0 dark:bg-gray-800",
"sm:rounded-sm"
isBackgroundTransparent ? "" : "dark:border-1 bg-white dark:bg-gray-800",
"rounded-md sm:border sm:dark:border-gray-600"
)}>
<div className="px-4 py-5 sm:flex sm:p-4">
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-700">

View File

@ -1,5 +1,6 @@
import { CreditCardIcon } from "@heroicons/react/solid";
import { Elements } from "@stripe/react-stripe-js";
import classNames from "classnames";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
@ -8,6 +9,7 @@ import Head from "next/head";
import React, { FC, useEffect, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core";
import getStripe from "@calcom/stripe/client";
import PaymentComponent from "@ee/components/stripe/Payment";
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
@ -26,16 +28,33 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const [date, setDate] = useState(dayjs.utc(props.booking.startTime));
const { isReady, Theme } = useTheme(props.profile.theme);
const isEmbed = useIsEmbed();
useEffect(() => {
let embedIframeWidth = 0;
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
}, []);
if (isEmbed) {
requestAnimationFrame(function fixStripeIframe() {
// HACK: Look for stripe iframe and center position it just above the embed content
const stripeIframeWrapper = document.querySelector(
'iframe[src*="https://js.stripe.com/v3/authorize-with-url-inner"]'
)?.parentElement;
if (stripeIframeWrapper) {
stripeIframeWrapper.style.margin = "0 auto";
stripeIframeWrapper.style.width = embedIframeWidth + "px";
}
requestAnimationFrame(fixStripeIframe);
});
sdkActionManager?.on("__dimensionChanged", (e) => {
embedIframeWidth = e.detail.data.iframeWidth as number;
});
}
}, [isEmbed]);
const eventName = props.booking.title;
return isReady ? (
<div className="h-screen bg-neutral-50 dark:bg-neutral-900">
<div className="h-screen">
<Theme />
<Head>
<title>
@ -51,7 +70,10 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
&#8203;
</span>
<div
className="inline-block transform overflow-hidden rounded-sm border border-neutral-200 bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:my-8 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle"
className={classNames(
"main inline-block transform overflow-hidden rounded-lg border border-neutral-200 bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle",
isEmbed ? "" : "sm:my-8"
)}
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">

View File

@ -18,6 +18,8 @@ function applyThemeAndAddListener(theme: string) {
document.documentElement.classList.remove("dark");
}
} else {
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove("light");
document.documentElement.classList.add(theme);
}
};
@ -33,15 +35,16 @@ export default function useTheme(theme?: Maybe<string>) {
const embedTheme = useEmbedTheme();
// Embed UI configuration takes more precedence over App Configuration
theme = embedTheme || theme;
const [_theme, setTheme] = useState<Maybe<string>>(null);
useEffect(() => {
// TODO: isReady doesn't seem required now. This is also impacting PSI Score for pages which are using isReady.
setIsReady(true);
setTheme(theme);
}, []);
function Theme() {
const code = applyThemeAndAddListener.toString();
const themeStr = theme ? `"${theme}"` : null;
const themeStr = _theme ? `"${_theme}"` : null;
return (
<Head>
<script dangerouslySetInnerHTML={{ __html: `(${code})(${themeStr})` }}></script>

View File

@ -1,6 +1,7 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { BadgeCheckIcon } from "@heroicons/react/solid";
import { UserPlan } from "@prisma/client";
import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import Link from "next/link";
@ -9,9 +10,10 @@ import React, { useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import { JSONObject } from "superjson/dist/types";
import { sdkActionManager, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
import defaultEvents, {
getDynamicEventDescription,
getGroupName,
getUsernameList,
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
@ -23,6 +25,7 @@ import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
import AvatarGroup from "@components/ui/AvatarGroup";
import { AvatarSSR } from "@components/ui/AvatarSSR";
@ -37,7 +40,7 @@ interface EvtsToVerify {
}
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const { users } = props;
const { users, profile } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
const { Theme } = useTheme(user.theme);
const { t } = useLocale();
@ -102,13 +105,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
))}
</ul>
);
const isEmbed = useIsEmbed();
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
useExposePlanGlobally("PRO");
const nameOrUsername = user.name || user.username || "";
const [evtsToVerify, setEvtsToVerify] = useState<EvtsToVerify>({});
const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
useEffect(() => {
@ -128,8 +133,17 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
username={isDynamicGroup ? dynamicUsernames.join(", ") : (user.username as string) || ""}
// avatar={user.avatar || undefined}
/>
<div className={"h-screen dark:bg-neutral-900" + isEmbed ? " bg:white m-auto max-w-3xl" : ""}>
<main className="mx-auto max-w-3xl px-4 py-24">
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed
? " border-bookinglightest rounded-md border bg-white dark:bg-neutral-900 sm:dark:border-gray-600"
: "",
"max-w-3xl py-24 px-4"
)}>
{isSingleUser && ( // When we deal with a single user, not dynamic group
<div className="mb-8 text-center">
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername}></AvatarSSR>
@ -284,6 +298,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
email: true,
name: true,
bio: true,
brandColor: true,
darkBrandColor: true,
avatar: true,
theme: true,
plan: true,
@ -298,10 +314,36 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true,
};
}
const isDynamicGroup = users.length > 1;
const dynamicNames = isDynamicGroup
? users.map((user) => {
return user.name || "";
})
: [];
const [user] = users; //to be used when dealing with single user, not dynamic group
const profile = isDynamicGroup
? {
name: getGroupName(dynamicNames),
image: null,
theme: null,
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: users.some((user) => {
return !user.allowDynamicBooking;
})
? false
: true,
}
: {
name: user.name || user.username,
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
const usersIds = users.map((user) => user.id);
const credentials = await prisma.credential.findMany({
where: {
@ -337,6 +379,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
users,
profile,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},

View File

@ -5,7 +5,7 @@ type Props = Record<string, unknown> & DocumentProps;
class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
const isEmbed = ctx.req?.url?.includes("embed");
const isEmbed = ctx.req?.url?.includes("embed=");
return { ...initialProps, isEmbed };
}
@ -27,7 +27,9 @@ class MyDocument extends Document<Props> {
</Head>
{/* 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" } : {}}>
<body
className={props.isEmbed ? "bg-transparent" : "bg-gray-100 dark:bg-neutral-900"}
style={props.isEmbed ? { display: "none" } : {}}>
<Main />
<NextScript />
</body>

View File

@ -12,7 +12,12 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState, useRef } from "react";
import { useIsEmbed, useEmbedStyles, useIsBackgroundTransparent } from "@calcom/embed-core";
import {
useIsEmbed,
useEmbedStyles,
useIsBackgroundTransparent,
useEmbedNonStylesConfig,
} from "@calcom/embed-core";
import { sdkActionManager } from "@calcom/embed-core";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -88,7 +93,7 @@ function RedirectionToast({ url }: { url: string }) {
return (
<>
<div className="relative inset-x-0 top-0 z-[60] pb-2 sm:fixed sm:top-2 sm:pb-5">
<div className="relative z-[60] pb-2 sm:pb-5">
<div className="mx-auto w-full sm:max-w-7xl sm:px-2 lg:px-8">
<div className="border border-green-600 bg-green-500 p-2 sm:p-3">
<div className="flex flex-wrap items-center justify-between">
@ -142,6 +147,9 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
const isBackgroundTransparent = useIsBackgroundTransparent();
const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const attendeeName = typeof name === "string" ? name : "Nameless";
const eventNameObject = {
@ -214,19 +222,22 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
description={needsConfirmation ? t("booking_submitted") : t("booking_confirmed")}
/>
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames("mx-auto", isEmbed ? "" : "max-w-3xl py-24")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "fixed inset-0 z-50 ")}>
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl}></RedirectionToast>
) : null}{" "}
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div
className={classNames(
shouldAlignCentrally ? "text-center" : "",
"flex items-end justify-center px-4 pt-4 pb-20 sm:block sm:p-0"
)}>
<div
className={classNames("my-4 transition-opacity sm:my-0", isEmbed ? "" : "fixed inset-0")}
className={classNames("my-4 transition-opacity sm:my-0", isEmbed ? "" : " inset-0")}
aria-hidden="true">
<div
className={classNames(
"inline-block transform overflow-hidden rounded-sm",
isEmbed ? "" : "border sm:my-8 sm:max-w-lg ",
"inline-block transform overflow-hidden rounded-md border sm:my-8 sm:max-w-lg",
isBackgroundTransparent ? "" : "bg-white dark:border-neutral-700 dark:bg-gray-800",
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-6 sm:align-middle"
)}
@ -404,7 +415,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
</form>
</div>
)}
{userIsOwner && (
{userIsOwner && !isEmbed && (
<div className="mt-4">
<Link href="/bookings">
<a className="flex items-center text-black dark:text-white">

View File

@ -86,7 +86,7 @@ function TeamPage({ team }: TeamPageProps) {
<div>
<Theme />
<HeadSeo title={teamName} description={teamName} />
<div className="px-4 pt-24 pb-12">
<div className="rounded-md bg-white px-4 pt-24 pb-12 dark:bg-gray-800 md:border">
<div className="max-w-96 mx-auto mb-8 text-center">
<Avatar
alt={teamName}

View File

@ -33,6 +33,8 @@
"start": "turbo run start --scope=\"@calcom/web\"",
"test": "turbo run test",
"test-playwright": "yarn playwright test --config=tests/config/playwright.config.ts",
"embed-tests-quick": "turbo run embed-tests-quick",
"embed-tests": "turbo run embed-tests",
"test-e2e": "turbo run test-e2e --concurrency=1",
"type-check": "turbo run type-check"
},

1
packages/embeds/embed-core/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
src/tailwind.generated.css

View File

@ -9,7 +9,7 @@ You can also see various example usages [here](https://github.com/calcom/cal.com
## Development
Run the following command and then you can test the embed in the automatically opened page `http://localhost:3002`
Run the following command and then you can test the embed in the automatically opened page `http://localhost:3100`
```bash
yarn dev
@ -38,22 +38,21 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
## Known Bugs and Upcoming Improvements
- Unsupported Browsers and versions. Documenting them and gracefully handling that.
- Need to create a booking Shell so that common changes for embed can be applied there.
- Accessibility and UI/UX Issues
- let user choose the loader for ModalBox
- If website owner links the booking page directly for an event, should the user be able to go to events-listing page using back button ?
- Let user specify both dark and light theme colors. Right now the colors specified are for light theme.
- Embed doesn't adapt to screen size without page refresh.
- Try opening in portrait mode and then go to landscape mode.
- In inline mode, due to changing height of iframe, the content goes beyond the fold. Automatic scroll needs to be implemented.
- On Availability page, when selecting date, width doesn't increase. max-width is there but because of strict width restriction with iframe, it doesn't allow it to expand.
- Transparent support is not properly done for team links
- Maybe don't set border radius in inline mode or give option to configure border radius.
- Branding
- Powered by Cal.com and 'Try it for free'. Should they be shown only for FREE account.
- Branding at the bottom has been removed for UI improvements, need to see where to add it.
- API
- Allow loader color customization using UI command itself too.
- Allow loader color customization using UI command itself too. Right now it's possible using CSS only.
- Automation Tests
- Run automation tests in CI
@ -71,8 +70,6 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
- Need to reduce the number of colors on booking page, so that UI configuration is simpler
- Dev Experience/Ease of Installation
- Improved Demo
- Seeding might be done for team event so that such an example is also available readily in index.html
- Do we need a one liner(like `window.dataLayer.push`) to inform SDK of something even if snippet is not yet on the page but would be there e.g. through GTM it would come late on the page ?
- Might be better to pass all configuration using a single base64encoded query param to booking page.
@ -81,7 +78,7 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
- Custom written Tailwind CSS is sent multiple times for different custom elements.
- Embed Code Generator
- Option to disable redirect banner and let parent handle redirect.
- Release Issues
- Compatibility Issue - When embed-iframe.js is updated in such a way that it is not compatible with embed.js, doing a release might break the embed for some time. e.g. iframeReady event let's say get's changed to something else
- Best Case scenario - App and Website goes live at the same time. A website using embed loads the same updated and thus compatible versions of embed.js and embed-iframe.js

View File

@ -5,6 +5,15 @@
if (!location.search.includes("nonResponsive")) {
document.write('<meta name="viewport" content="width=device-width"/>');
}
(()=> {
const url = new URL(document.URL);
// Only run the example specified by only=, avoids distraction and faster to test.
const only = url.searchParams.get("only");
const namespace = only ? only.replace("ns:",""): null
if (namespace) {
location.hash="#cal-booking-place-" + namespace + "-iframe"
}
})()
</script>
<script>
(function (C, A, L) {
@ -34,7 +43,7 @@
}
p(cal, ar);
};
})(window, "//localhost:3002/dist/embed.umd.js", "init");
})(window, "//localhost:3100/dist/embed.umd.js", "init");
</script>
<style>
@ -55,8 +64,7 @@
color: green;
}
* {
--cal-brand-border-color: blue;
--cal-brand-background-color: blue;
--cal-brand-color: gray;
}
</style>
</head>
@ -64,7 +72,7 @@
<h3>This page has a non responsive version accessible <a href="?nonResponsive">here</a></h3>
<h3>Pre-render test page available at <a href="?only=prerender-test">here</a></h3>
<div>
<button data-cal-namespace="prerendertestLightTheme" data-cal-link="free?light&popup">Book with Free User[Light Theme]</button>
<button data-cal-namespace="prerendertestLightTheme" data-cal-config='{"theme":"light"}' data-cal-link="free?light&popup">Book with Free User[Light Theme]</button>
<div>
<i
>Corresponding Cal Link is being preloaded. Assuming that it would take you some time to click this
@ -74,9 +82,12 @@
>
</div>
<h2>Other Popup Examples</h2>
<button data-cal-namespace="popupDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="free?dark&popup">Book with Free User[Dark Theme]</button>
<button data-cal-namespace="popupTeamLinkLightTheme" data-cal-config='{"theme":"light"}' data-cal-link="team/test-team?team&light&popup">Book with Test Team[Light Theme]</button>
<button data-cal-namespace="popupTeamLinkDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="team/test-team?team&dark&popup">Book with Test Team[Dark Theme]</button>
<button data-cal-namespace="popupAutoTheme" data-cal-link="free">Book with Free User[Auto Theme]</button>
<button data-cal-namespace="popupDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="free">Book with Free User[Dark Theme]</button>
<button data-cal-namespace="popupTeamLinkLightTheme" data-cal-config='{"theme":"light"}' data-cal-link="team/seeded-team/collective-seeded-team-event">Book with Test Team[Light Theme]</button>
<button data-cal-namespace="popupTeamLinkDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="team/seeded-team/collective-seeded-team-event">Book with Test Team[Dark Theme]</button>
<button data-cal-namespace="popupTeamLinksList" data-cal-link="team/seeded-team/">See Team Links [Auto Theme]</button>
<button data-cal-namespace="popupPaidEvent" data-cal-link="pro/paid">Book Paid Event [Auto Theme]</button>
<div>
<h2>Embed for Pages behind authentication</h2>
<button data-cal-namespace="upcomingBookings" data-cal-config='{"theme":"dark"}' data-cal-link="bookings/upcoming">Show Upcoming Bookings</button>
@ -85,18 +96,14 @@
<div id="namespaces-test">
<div class="debug" id="cal-booking-place-default">
<h2>
Default Namespace(Cal)<i>[Dark Theme][inline][Guests(janedoe@gmail.com and test@gmail.com)]</i>
Default Namespace(Cal)<i>[Dark Theme][inline][Guests(janedoe@example.com and test@example.com)]</i>
</h2>
<div>
<i><a href="?only=ns:default">Test in Zen Mode</a></i>
</div>
<i class="last-action"> You would see last Booking page action in my place </i>
<div >
<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="place" style="width:50%; max-height: 30vh; overflow: scroll"></div>
<div class="place" style="width:50%;"></div>
<div class="loader" id="cal-booking-loader-">Loading .....</div>
</div>
</div>
@ -109,7 +116,7 @@
<i>You would see last Booking page action in my place</i>
</i>
<div class="place">
<div>If you render booking embed in me, I won't restrict you. The entire page is yours.</div>
<div>If you render booking embed in me, I won't restrict you. The entire page is yours. Content is by default aligned center</div>
<button
onclick="(function () {Cal.ns.second('ui', {styles:{eventTypeListItem:{backgroundColor:'blue'}}})})()">
Change <code>eventTypeListItem</code> bg color
@ -117,6 +124,21 @@
<button onclick="(function () {Cal.ns.second('ui', {styles:{body:{background:'red'}}})})()">
Change <code>body</code> bg color
</button>
<button onclick="(function () {Cal.ns.second('ui', {styles:{align:'left'}})})()">
Align left
</button>
<button onclick="(function () {Cal.ns.second('ui', {styles:{align:'center'}})})()">
Align Center
</button>
<button onclick="(function () {Cal.ns.second('ui', {styles:{enabledDateButton: {
backgroundColor: '#D3D3D3',
},
disabledDateButton: {
backgroundColor: 'lightslategray',
},}})})()">
Change Date Button Color
</button>
<div class="loader" id="cal-booking-loader-second">Loading .....</div>
</div>
</div>
@ -136,7 +158,7 @@
</div>
<div class="debug" id="cal-booking-place-fourth">
<h2>Namespace "fourth"(Cal.ns.fourth)[Team Event Test][inline]</h2>
<h2>Namespace "fourth"(Cal.ns.fourth)[Team Event Test][inline taking entire width]</h2>
<div>
<i><a href="?only=ns:fourth">Test in Zen Mode</a></i>
</div>
@ -150,6 +172,23 @@
</div>
</div>
<div class="debug" id="cal-booking-place-fifth">
<h2>Namespace "fifth"(Cal.ns.fifth)[Team Event Test][inline along with some content]</h2>
<div>
<i><a href="?only=ns:fifth">Test in Zen Mode</a></i>
</div>
<i class="last-action">
<i>You would see last Booking page action in my place</i>
</i>
<div style="display:flex;align-items: center;">
<h2 style="width: 30%">
On the right side you can book a team meeting =>
</h2>
<div style="width: 70%" class="place">
</div>
</div>
</div>
<script>
const callback = function (e) {
const detail = e.detail;
@ -176,10 +215,14 @@
elementOrSelector: "#cal-booking-place-default .place",
calLink: "pro?case=1",
config: {
name: "John Doe",
__autoScroll:true,
iframeAttrs: {
id: "cal-booking-place-default-iframe"
},
name: "John",
email: "johndoe@gmail.com",
notes: "Test Meeting",
guests: ["janedoe@gmail.com", "test@gmail.com"],
guests: ["janedoe@example.com", "test@example.com"],
theme: "dark",
},
});
@ -202,25 +245,11 @@
{
elementOrSelector: "#cal-booking-place-second .place",
calLink: "pro?case=2",
},
],
[
"ui",
{
styles: {
body: {
background: "white",
config: {
iframeAttrs: {
id: "cal-booking-place-second-iframe"
},
eventTypeListItem: {
backgroundColor: "#D3D3D3",
},
enabledDateButton: {
backgroundColor: "#D3D3D3",
},
disabledDateButton: {
backgroundColor: "lightslategray",
},
},
}
},
]
);
@ -243,6 +272,11 @@
{
elementOrSelector: "#cal-booking-place-third .place",
calLink: "pro/30min",
config: {
iframeAttrs: {
id: "cal-booking-place-third-iframe"
},
}
},
],
[
@ -280,7 +314,12 @@
"inline",
{
elementOrSelector: "#cal-booking-place-fourth .place",
calLink: "team/test-team",
calLink: "team/seeded-team",
config: {
iframeAttrs: {
id: "cal-booking-place-fourth-iframe"
},
}
},
],
[
@ -307,7 +346,30 @@
callback,
});
}
if (!only || only === "ns:fifth") {
Cal("init", "fifth", {
debug: 1,
origin: "http://localhost:3000",
});
Cal.ns.fifth(
[
"inline",
{
elementOrSelector: "#cal-booking-place-fifth .place",
calLink: "team/seeded-team/collective-seeded-team-event",
config: {
iframeAttrs: {
id: "cal-booking-place-fifth-iframe"
},
}
},
],
);
Cal.ns.fifth("on", {
action: "*",
callback,
});
}
if (!only || only === "prerender-test") {
Cal('init', 'prerendertestLightTheme', {
debug: 1,
@ -321,6 +383,10 @@
debug: 1,
origin: "http://localhost:3000",
})
Cal('init', 'popupAutoTheme', {
debug: 1,
origin: "http://localhost:3000",
})
Cal('init', 'popupTeamLinkLightTheme', {
debug: 1,
origin: "http://localhost:3000",
@ -330,7 +396,17 @@
origin: "http://localhost:3000",
})
Cal('init', 'upcomingBookings', {
Cal('init', 'popupTeamLinkDarkTheme', {
debug: 1,
origin: "http://localhost:3000",
})
Cal('init', 'popupTeamLinksList', {
debug: 1,
origin: "http://localhost:3000",
})
Cal('init', 'popupPaidEvent', {
debug: 1,
origin: "http://localhost:3000",
})

View File

@ -7,13 +7,30 @@
"build": "vite build",
"build:cal": "NEXT_PUBLIC_WEBSITE_URL='https://cal.com' yarn build",
"vite": "vite",
"dev": "run-p 'build --watch' 'vite --port 3002 --strict-port --open'",
"tailwind": "yarn tailwindcss -i ./src/styles.css -o ./src/tailwind.generated.css --watch",
"buildWatchAndServer": "run-p 'build --watch' 'vite --port 3100 --strict-port --open'",
"dev": "run-p 'tailwind' 'buildWatchAndServer'",
"type-check": "tsc --pretty --noEmit",
"lint": "eslint --ext .ts,.js src",
"test-playwright": "yarn playwright test --config=playwright/config/playwright.config.ts"
"embed-tests": "yarn playwright test --config=playwright/config/playwright.config.ts",
"embed-tests-quick": "QUICK=true yarn embed-tests"
},
"postcss": {
"map": false,
"plugins": {
"tailwindcss": {},
"autoprefixer": {}
}
},
"devDependencies": {
"vite": "^2.8.6",
"eslint": "^8.10.0"
"autoprefixer": "^10.4.4",
"eslint": "^8.10.0",
"postcss": "^8.4.12",
"vite": "^2.8.6"
},
"dependencies": {
"tailwindcss": "^3.0.24",
"tsc": "^2.0.4",
"typescript": "^4.6.3"
}
}

View File

@ -3,10 +3,10 @@ import * as path from "path";
const outputDir = path.join("../results");
const testDir = path.join("../tests");
const quickMode = process.env.QUICK === "true";
const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI,
retries: 1,
retries: quickMode ? 0 : 1,
workers: 1,
timeout: 60_000,
reporter: [
@ -19,15 +19,21 @@ const config: PlaywrightTestConfig = {
],
globalSetup: require.resolve("./globalSetup"),
outputDir,
expect: {
toMatchSnapshot: {
// Opacity transitions can cause small differences
maxDiffPixels: 50,
},
},
webServer: {
// Start App Server manually - Can't be handled here. See https://github.com/microsoft/playwright/issues/8206
command: "yarn workspace @calcom/embed-core dev",
port: 3002,
port: 3100,
timeout: 60_000,
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: "http://localhost:3002",
baseURL: "http://localhost:3100",
locale: "en-US",
trace: "retain-on-failure",
headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
@ -38,16 +44,20 @@ const config: PlaywrightTestConfig = {
testDir,
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
testDir,
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
testDir,
use: { ...devices["Desktop Safari"] },
},
quickMode
? {}
: {
name: "firefox",
testDir,
use: { ...devices["Desktop Firefox"] },
},
quickMode
? {}
: {
name: "webkit",
testDir,
use: { ...devices["Desktop Safari"] },
},
],
};
export type ExpectedUrlDetails = {
@ -123,9 +133,16 @@ expect.extend({
}
}
const iframeReadyEventDetail = await getActionFiredDetails({
calNamespace,
actionType: "__iframeReady",
const iframeReadyEventDetail = await new Promise(async (resolve) => {
setInterval(async () => {
const iframeReadyEventDetail = await getActionFiredDetails({
calNamespace,
actionType: "linkReady",
});
if (iframeReadyEventDetail) {
resolve(iframeReadyEventDetail);
}
}, 500);
});
if (!iframeReadyEventDetail) {

View File

@ -11,18 +11,26 @@ export const test = base.extend<Fixtures>({
({ calNamespace }: { calNamespace: string }) => {
//@ts-ignore
window.eventsFiredStoreForPlaywright = window.eventsFiredStoreForPlaywright || {};
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("DOMContentLoaded", function tryAddingListener() {
if (parent !== window) {
// Firefox seems to execute this snippet for iframe as well. Avoid that. It must be executed only for parent frame.
return;
}
console.log("PlaywrightTest:", "Adding listener for __iframeReady");
//@ts-ignore
let api = window.Cal;
if (!api) {
setTimeout(tryAddingListener, 500);
return;
}
if (calNamespace) {
//@ts-ignore
api = window.Cal.ns[calNamespace];
}
console.log("PlaywrightTest:", "Adding listener for __iframeReady");
if (!api) {
throw new Error(`namespace "${calNamespace}" not found`);
}
api("on", {
action: "*",
callback: (e: any) => {
@ -41,13 +49,15 @@ export const test = base.extend<Fixtures>({
},
getActionFiredDetails: async ({ page }, use) => {
await use(async ({ calNamespace, actionType }) => {
return await page.evaluate(
({ actionType, calNamespace }) => {
//@ts-ignore
return window.eventsFiredStoreForPlaywright[`${actionType}-${calNamespace}`];
},
{ actionType, calNamespace }
);
if (!page.isClosed()) {
return await page.evaluate(
({ actionType, calNamespace }) => {
//@ts-ignore
return window.eventsFiredStoreForPlaywright[`${actionType}-${calNamespace}`];
},
{ actionType, calNamespace }
);
}
});
},
});

View File

@ -1,8 +1,35 @@
import { Page, test } from "@playwright/test";
import { Page, Frame, test, expect } from "@playwright/test";
import prisma from "@lib/prisma";
export function todo(title: string) {
test.skip(title, () => {});
}
export const deleteAllBookingsByEmail = async (email: string) =>
await prisma.booking.deleteMany({
where: {
attendees: {
some: {
email: email,
},
},
},
});
export const getBooking = async (bookingId: string) => {
const booking = await prisma.booking.findUnique({
where: {
uid: bookingId,
},
include: {
attendees: true,
},
});
if (!booking) {
throw new Error("Booking not found");
}
return booking;
};
export const getEmbedIframe = async ({ page, pathname }: { page: Page; pathname: string }) => {
// FIXME: Need to wait for the iframe to be properly added to shadow dom. There should be a no time boundation way to do it.
@ -19,3 +46,48 @@ export const getEmbedIframe = async ({ page, pathname }: { page: Page; pathname:
}
return null;
};
async function selectFirstAvailableTimeSlotNextMonth(frame: Frame, page: Page) {
await frame.click('[data-testid="incrementMonth"]');
// @TODO: Find a better way to make test wait for full month change render to end
// so it can click up on the right day, also when resolve remove other todos
// Waiting for full month increment
await frame.waitForTimeout(1000);
expect(await page.screenshot()).toMatchSnapshot("availability-page-2.png");
// TODO: Find out why the first day is always booked on tests
await frame.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
await frame.click('[data-testid="time"]');
}
export async function bookFirstEvent(username: string, frame: Frame, page: Page) {
// Click first event type
await frame.click('[data-testid="event-type-link"]');
await frame.waitForNavigation({
url(url) {
return !!url.pathname.match(new RegExp(`/${username}/.*$`));
},
});
expect(await page.screenshot()).toMatchSnapshot("availability-page-1.png");
await selectFirstAvailableTimeSlotNextMonth(frame, page);
await frame.waitForNavigation({
url(url) {
return url.pathname.includes(`/${username}/book`);
},
});
expect(await page.screenshot()).toMatchSnapshot("booking-page.png");
// --- fill form
await frame.fill('[name="name"]', "Embed User");
await frame.fill('[name="email"]', "embed-user@example.com");
await frame.press('[name="email"]', "Enter");
const response = await page.waitForResponse("**/api/book/event");
const responseObj = await response.json();
const bookingId = responseObj.uid;
// Make sure we're navigated to the success page
await frame.waitForNavigation({
url(url) {
return url.pathname.endsWith("/success");
},
});
expect(await page.screenshot()).toMatchSnapshot("success-page.png");
return bookingId;
}

View File

@ -1,9 +1,15 @@
import { expect } from "@playwright/test";
import { test } from "../fixtures/fixtures";
import { todo, getEmbedIframe } from "../lib/testUtils";
import { todo, getEmbedIframe, bookFirstEvent, getBooking, deleteAllBookingsByEmail } from "../lib/testUtils";
test("should open embed iframe on click - Configured with light theme", async ({
page,
addEmbedListeners,
getActionFiredDetails,
}) => {
await deleteAllBookingsByEmail("embed-user@example.com");
test("should open embed iframe on click", async ({ page, addEmbedListeners, getActionFiredDetails }) => {
const calNamespace = "prerendertestLightTheme";
await addEmbedListeners(calNamespace);
await page.goto("/?only=prerender-test");
@ -17,6 +23,15 @@ test("should open embed iframe on click", async ({ page, addEmbedListeners, getA
expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
pathname: "/free",
});
expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
const bookingId = await bookFirstEvent("free", embedIframe, page);
const booking = await getBooking(bookingId);
expect(booking.attendees.length).toBe(1);
await deleteAllBookingsByEmail("embed-user@example.com");
});
todo("Floating Button Test with Dark Theme");

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -1,13 +1,14 @@
import { expect, Frame } from "@playwright/test";
import { test } from "../fixtures/fixtures";
import { todo, getEmbedIframe } from "../lib/testUtils";
import { todo, getEmbedIframe, bookFirstEvent, deleteAllBookingsByEmail } from "../lib/testUtils";
test("Inline Iframe - Configured with Dark Theme", async ({
page,
getActionFiredDetails,
addEmbedListeners,
}) => {
await deleteAllBookingsByEmail("embed-user@example.com");
await addEmbedListeners("");
await page.goto("/?only=ns:default");
const embedIframe = await getEmbedIframe({ page, pathname: "/pro" });
@ -17,6 +18,12 @@ test("Inline Iframe - Configured with Dark Theme", async ({
theme: "dark",
},
});
expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
await bookFirstEvent("pro", embedIframe, page);
await deleteAllBookingsByEmail("embed-user@example.com");
});
todo(

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -1,33 +0,0 @@
import tailwindCss from "./tailwind.css";
export class FloatingButton extends HTMLElement {
constructor() {
super();
const buttonHtml = `
<style>
${tailwindCss}
</style>
<button
class="fixed bottom-4 right-4 flex h-16 origin-center transform cursor-pointer items-center rounded-full py-4 px-6 text-base outline-none drop-shadow-md transition transition-all focus:outline-none focus:ring-4 focus:ring-gray-600 focus:ring-opacity-50 active:scale-95 md:bottom-6 md:right-10"
style="background-color: rgb(255, 202, 0); color: rgb(20, 30, 47); z-index: 10001">
<div class="mr-3 flex items-center justify-center">
<svg
class="h-7 w-7"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
<div class="font-semibold leading-5 antialiased">Book my Cal</div>
</button>`;
this.attachShadow({ mode: "open" });
this.shadowRoot!.innerHTML = buttonHtml;
}
}

View File

@ -0,0 +1,12 @@
import { CalWindow } from "@calcom/embed-snippet";
import floatingButtonHtml from "./FloatingButtonHtml";
export class FloatingButton extends HTMLElement {
constructor() {
super();
const buttonHtml = `<style>${(window as CalWindow).Cal!.__css}</style> ${floatingButtonHtml}`;
this.attachShadow({ mode: "open" });
this.shadowRoot!.innerHTML = buttonHtml;
}
}

View File

@ -0,0 +1,22 @@
const html = `<button class="fixed bottom-4 right-4 flex h-16 origin-center bg-red-50 transform cursor-pointer items-center
rounded-full py-4 px-6 text-base outline-none drop-shadow-md transition focus:outline-none fo
cus:ring-4 focus:ring-gray-600 focus:ring-opacity-50 active:scale-95 md:bottom-6 md:right-10"
style="background-color: rgb(255, 202, 0); color: rgb(20, 30, 47); z-index: 10001">
<div class="mr-3 flex items-center justify-center">
<svg
class="h-7 w-7"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
<div class="font-semibold leading-5 antialiased">Book my Cal</div>
</button>`;
export default html;

View File

@ -1,5 +1,7 @@
import loaderCss from "./loader.css";
import tailwindCss from "./tailwind.css";
import { CalWindow } from "@calcom/embed-snippet";
import loaderCss from "../loader.css";
import inlineHtml from "./inlineHtml";
export class Inline extends HTMLElement {
//@ts-ignore
@ -14,14 +16,8 @@ export class Inline extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot!.innerHTML = `
<style> ${tailwindCss}${loaderCss}</style>
<div id="loader" style="left:0;right:0" class="absolute z-highest flex h-screen w-full items-center">
<div class="loader border-brand dark:border-darkmodebrand">
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
</div>
</div>
<slot></slot>
`;
this.shadowRoot!.innerHTML = `<style>${
(window as CalWindow).Cal!.__css
}</style><style>${loaderCss}</style>${inlineHtml}`;
}
}

View File

@ -0,0 +1,7 @@
const html = `<div id="loader" style="top:calc(50% - 30px); left:calc(50% - 30px)" class="absolute z-highest">
<div class="loader border-brand dark:border-darkmodebrand">
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
</div>
</div>
<slot></slot>`;
export default html;

View File

@ -1,126 +0,0 @@
import loaderCss from "./loader.css";
import tailwindCss from "./tailwind.css";
export class ModalBox extends HTMLElement {
static htmlOverflow: string;
//@ts-ignore
static get observedAttributes() {
return ["state"];
}
show(show: boolean) {
// We can't make it display none as that takes iframe width and height calculations to 0
(this.shadowRoot!.host as unknown as any).style.visibility = show ? "visible" : "hidden";
}
close() {
this.show(false);
document.body.style.overflow = ModalBox.htmlOverflow;
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (name !== "state") {
return;
}
if (newValue == "loaded") {
(this.shadowRoot!.querySelector("#loader")! as HTMLElement).style.display = "none";
} else if (newValue === "started") {
this.show(true);
}
}
connectedCallback() {
const closeEl = this.shadowRoot!.querySelector(".close") as HTMLElement;
this.shadowRoot!.host.addEventListener("click", (e) => {
this.close();
});
closeEl.onclick = () => {
this.close();
};
}
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> ${tailwindCss}
.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%;
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;
}
.loader {
--cal-brand-border-color: white;
--cal-brand-background-color: white;
}
${loaderCss}
</style>
<div class="backdrop">
<div class="header">
<span class="close">&times;</span>
</div>
<div class="modal-box">
<div class="body">
<div id="loader" class="absolute z-highest flex h-screen w-full items-center">
<div class="loader border-brand dark:border-darkmodebrand">
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
</div>
</div>
<slot></slot>
</div>
</div>
</div>
`;
this.attachShadow({ mode: "open" });
ModalBox.htmlOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
this.shadowRoot!.innerHTML = modalHtml;
}
}

View File

@ -0,0 +1,71 @@
import { CalWindow } from "@calcom/embed-snippet";
import loaderCss from "../loader.css";
import modalBoxHtml from "./ModalBoxHtml";
export class ModalBox extends HTMLElement {
static htmlOverflow: string;
//@ts-ignore
static get observedAttributes() {
return ["state"];
}
show(show: boolean) {
// We can't make it display none as that takes iframe width and height calculations to 0
(this.shadowRoot!.host as unknown as any).style.visibility = show ? "visible" : "hidden";
if (!show) {
document.body.style.overflow = ModalBox.htmlOverflow;
}
}
close() {
this.show(false);
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (name !== "state") {
return;
}
if (newValue == "loaded") {
(this.shadowRoot!.querySelector("#loader")! as HTMLElement).style.display = "none";
} else if (newValue === "started") {
this.show(true);
} else if (newValue == "closed") {
this.show(false);
}
}
connectedCallback() {
const closeEl = this.shadowRoot!.querySelector(".close") as HTMLElement;
document.addEventListener(
"keydown",
(e) => {
if (e.key === "Escape") {
this.close();
}
},
{
once: true,
}
);
this.shadowRoot!.host.addEventListener("click", (e) => {
this.close();
});
closeEl.onclick = () => {
this.close();
};
}
constructor() {
super();
const modalHtml = `<style>${
(window as CalWindow).Cal!.__css
}</style><style>${loaderCss}</style>${modalBoxHtml}`;
this.attachShadow({ mode: "open" });
ModalBox.htmlOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
this.shadowRoot!.innerHTML = modalHtml;
}
}

View File

@ -0,0 +1,72 @@
const html = `<style>
.my-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:100%;
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;
}
/*Modal background is black only, so hardcode white */
.loader {
--cal-brand-color:white;
}
</style>
<div class="my-backdrop">
<div class="header">
<span class="close">&times;</span>
</div>
<div class="modal-box">
<div class="body">
<div id="loader" class="z-[999999999999] absolute flex w-full items-center">
<div class="loader modal-loader border-brand dark:border-darkmodebrand">
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
</div>
</div>
<slot></slot>
</div>
</div>
</div>`;
export default html;

View File

@ -12,6 +12,7 @@ const embedStore = {
// Store all embed styles here so that as and when new elements are mounted, styles can be applied to it.
styles: {},
namespace: null,
embedType: undefined,
theme: null,
// Store all React State setters here.
reactStylesStateSetters: {},
@ -21,6 +22,7 @@ const embedStore = {
styles: UiConfig["styles"];
namespace: string | null;
theme: string | null;
embedType: undefined | null | string;
reactStylesStateSetters: any;
parentInformedAboutContentHeight: boolean;
windowLoadEventFired: boolean;
@ -84,7 +86,9 @@ interface EmbedStyles {
disabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
availabilityDatePicker?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
}
interface EmbedStylesBranding {
interface EmbedNonStylesConfig {
/** Default would be center */
align: "left";
branding?: {
brandColor?: string;
lightColor?: string;
@ -97,7 +101,7 @@ interface EmbedStylesBranding {
};
}
type ReactEmbedStylesSetter = React.Dispatch<React.SetStateAction<EmbedStyles | EmbedStylesBranding>>;
type ReactEmbedStylesSetter = React.Dispatch<React.SetStateAction<EmbedStyles | EmbedNonStylesConfig>>;
const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
embedStore.styles = stylesConfig;
@ -111,14 +115,14 @@ const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
}
};
const registerNewSetter = (elementName: keyof EmbedStyles | keyof EmbedStylesBranding, setStyles: any) => {
const registerNewSetter = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig, setStyles: any) => {
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: keyof EmbedStyles | keyof EmbedStylesBranding) => {
const removeFromEmbedStylesSetterMap = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig) => {
delete embedStore.reactStylesStateSetters[elementName];
};
@ -128,6 +132,12 @@ function isValidNamespace(ns: string | null | undefined) {
export const useEmbedTheme = () => {
const router = useRouter();
useEffect(() => {
router.events.on("routeChangeComplete", () => {
sdkActionManager?.fire("__routeChanged", {});
});
}, [router.events]);
if (embedStore.theme) {
return embedStore.theme;
}
@ -151,8 +161,8 @@ export const useEmbedStyles = (elementName: keyof EmbedStyles) => {
return styles[elementName] || {};
};
export const useEmbedBranding = (elementName: keyof EmbedStylesBranding) => {
const [styles, setStyles] = useState({} as EmbedStylesBranding);
export const useEmbedNonStylesConfig = (elementName: keyof EmbedNonStylesConfig) => {
const [styles, setStyles] = useState({} as EmbedNonStylesConfig);
useEffect(() => {
registerNewSetter(elementName, setStyles);
@ -171,7 +181,7 @@ export const useIsBackgroundTransparent = () => {
// TODO: Background should be read as ui.background and not ui.body.background
const bodyEmbedStyles = useEmbedStyles("body");
if (bodyEmbedStyles?.background === "transparent") {
if (bodyEmbedStyles.background === "transparent") {
isBackgroundTransparent = true;
}
return isBackgroundTransparent;
@ -179,8 +189,8 @@ export const useIsBackgroundTransparent = () => {
export const useBrandColors = () => {
// TODO: Branding shouldn't be part of ui.styles. It should exist as ui.branding.
const brandingColors = useEmbedBranding("branding");
return brandingColors;
const brandingColors = useEmbedNonStylesConfig("branding") as EmbedNonStylesConfig["branding"];
return brandingColors || {};
};
function getNamespace() {
@ -196,6 +206,17 @@ function getNamespace() {
}
}
function getEmbedType() {
if (embedStore.embedType) {
return embedStore.embedType;
}
if (isBrowser) {
const url = new URL(document.URL);
const embedType = (embedStore.embedType = url.searchParams.get("embedType"));
return embedType;
}
}
const isEmbed = () => {
const namespace = getNamespace();
const _isValidNamespace = isValidNamespace(namespace);
@ -218,6 +239,14 @@ export const useIsEmbed = () => {
return _isEmbed;
};
export const useEmbedType = () => {
const [state, setState] = useState<string | null | undefined>(null);
useEffect(() => {
setState(getEmbedType());
}, []);
return state;
};
function unhideBody() {
document.body.style.display = "block";
}
@ -300,9 +329,13 @@ function keepParentInformedAboutDimensionChanges() {
embedStore.windowLoadEventFired = true;
// Use the dimensions of main element as in most places there is max-width restriction on it and we just want to show the main content.
// It avoids the unwanted padding outside main tag.
const mainElement = document.getElementsByTagName("main")[0] || document.documentElement;
const mainElement =
(document.getElementsByClassName("main")[0] as HTMLElement) ||
document.getElementsByTagName("main")[0] ||
document.documentElement;
const documentScrollHeight = document.documentElement.scrollHeight;
const documentScrollWidth = document.documentElement.scrollWidth;
const contentHeight = mainElement.offsetHeight;
const contentWidth = mainElement.offsetWidth;
@ -331,10 +364,6 @@ function keepParentInformedAboutDimensionChanges() {
// 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;
}
runAsap(informAboutScroll);
});
}
@ -361,10 +390,16 @@ if (isBrowser) {
// Because on cal-iframe we set explicty width to make it look inline and part of page, there is never space available for content to automatically expand
// This is a HACK to quickly tell iframe to go full width and let iframe content adapt to that and set new width.
sdkActionManager?.on("__refreshWidth", () => {
sdkActionManager?.fire("__dimensionChanged", {
iframeWidth: 100,
__unit: "%",
});
// sdkActionManager?.fire("__dimensionChanged", {
// iframeWidth: 100,
// __unit: "%",
// });
// runAsap(() => {
// sdkActionManager?.fire("__dimensionChanged", {
// iframeWidth: 100,
// __unit: "%",
// });
// });
});
window.addEventListener("message", (e) => {
@ -378,6 +413,19 @@ if (isBrowser) {
}
});
document.addEventListener("click", (e) => {
if (!e.target) {
return;
}
const mainElement =
(document.getElementsByClassName("main")[0] as HTMLElement) ||
document.getElementsByTagName("main")[0] ||
document.documentElement;
if ((e.target as HTMLElement).contains(mainElement)) {
sdkActionManager?.fire("__closeIframe", {});
}
});
if (!pageStatus || pageStatus == "200") {
keepParentInformedAboutDimensionChanges();
sdkActionManager?.fire("__iframeReady", {});

View File

@ -4,4 +4,6 @@
.cal-embed {
border: 0px;
min-height: 300px;
margin: 0 auto;
width: 100%;
}

View File

@ -1,26 +1,29 @@
import type { CalWindow } from "@calcom/embed-snippet";
import { FloatingButton } from "./FloatingButton";
import { ModalBox } from "./ModalBox";
import { FloatingButton } from "./FloatingButton/FloatingButton";
import { Inline } from "./Inline/inline";
import { ModalBox } from "./ModalBox/ModalBox";
import { methods, UiConfig } from "./embed-iframe";
import css from "./embed.css";
import { Inline } from "./inline";
import { SdkActionManager } from "./sdk-action-manager";
import allCss from "./tailwind.generated.css";
customElements.define("cal-modal-box", ModalBox);
customElements.define("cal-floating-button", FloatingButton);
customElements.define("cal-inline", Inline);
declare module "*.css";
type Namespace = string;
type Config = {
origin: string;
debug: 1;
debug?: boolean;
};
const globalCal = (window as CalWindow).Cal;
if (!globalCal || !globalCal.q) {
throw new Error("Cal is not defined. This shouldn't happen");
}
globalCal.__css = allCss;
document.head.appendChild(document.createElement("style")).innerHTML = css;
function log(...args: any[]) {
@ -75,7 +78,7 @@ export type InstructionQueue = Instruction[];
export class Cal {
iframe?: HTMLIFrameElement;
__config: any;
__config: Config;
modalBox!: Element;
@ -96,7 +99,7 @@ export class Cal {
return {
...config,
// guests is better for API but Booking Page accepts guest. So do the mapping
guest: config.guests ?? "",
guest: config.guests ?? undefined,
};
}
@ -141,27 +144,35 @@ export class Cal {
queryObject = {},
}: {
calLink: string;
queryObject?: Record<string, string | string[]>;
queryObject?: Record<string, string | string[] | Record<string, string>>;
}) {
const iframe = (this.iframe = document.createElement("iframe"));
iframe.className = "cal-embed";
iframe.name = "cal-embed";
const config = this.getConfig();
const { iframeAttrs, ...restQueryObject } = queryObject;
if (iframeAttrs && typeof iframeAttrs !== "string" && !(iframeAttrs instanceof Array)) {
iframe.setAttribute("id", iframeAttrs.id);
}
// Prepare searchParams from config
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(queryObject)) {
for (const [key, value] of Object.entries(restQueryObject)) {
if (value === undefined) {
continue;
}
if (value instanceof Array) {
value.forEach((val) => searchParams.append(key, val));
} else {
searchParams.set(key, value);
searchParams.set(key, value as string);
}
}
const urlInstance = new URL(`${config.origin}/${calLink}`);
urlInstance.searchParams.set("embed", this.namespace);
if (config.debug) {
urlInstance.searchParams.set("debug", config.debug);
urlInstance.searchParams.set("debug", "" + config.debug);
}
// Merge searchParams from config onto the URL which might have query params already
@ -219,6 +230,16 @@ export class Cal {
},
},
});
config = config || {};
// Keeping auto-scroll disabled for two reasons:
// - If user scrolls the content to an appropriate position, it again resets it to default position which might not be for the liking of the user
// - Sometimes, the position can be wrong(e.g. if there is a fixed position header on top coming above the iframe content).
// Best solution might be to autoscroll only if the iframe is not fully visible, detection of full visibility might be tough
// We need to keep in mind that autoscroll is meant to solve the problem when on a certain view(which is availability page right now), the height goes too high and then suddenly it becomes normal
(config as unknown as any).__autoScroll = !!(config as unknown as any).__autoScroll;
config.embedType = "inline";
const iframe = this.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
iframe.style.height = "100%";
iframe.style.width = "100%";
@ -230,8 +251,9 @@ export class Cal {
throw new Error("Element not found");
}
const template = document.createElement("template");
template.innerHTML = `<cal-inline style="max-height:inherit;height:inherit;min-height:inherit;display:block;position:relative"></cal-inline>`;
template.innerHTML = `<cal-inline style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap"></cal-inline>`;
this.inlineEl = template.content.children[0];
(this.inlineEl as unknown as any).__CalAutoScroll = config.__autoScroll;
this.inlineEl.appendChild(iframe);
element.appendChild(template.content);
}
@ -247,7 +269,7 @@ export class Cal {
},
});
const template = document.createElement("template");
template.innerHTML = `<cal-floating-button data-cal-namespace=${this.namespace} data-cal-link=${calLink}></cal-floating-button>`;
template.innerHTML = `<cal-floating-button data-cal-namespace="${this.namespace}" data-cal-link="${calLink}"></cal-floating-button>`;
document.body.appendChild(template.content);
}
@ -257,6 +279,7 @@ export class Cal {
existingModalEl.setAttribute("state", "started");
return;
}
config.embedType = "modal";
const iframe = this.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
iframe.style.borderRadius = "8px";
@ -264,8 +287,12 @@ export class Cal {
iframe.style.width = "100%";
const template = document.createElement("template");
template.innerHTML = `<cal-modal-box uid="${uid}"></cal-modal-box>`;
this.modalBox = template.content.children[0];
this.modalBox.appendChild(iframe);
this.actionManager.on("__closeIframe", () => {
this.modalBox.setAttribute("state", "closed");
});
document.body.appendChild(template.content);
}
@ -348,7 +375,7 @@ export class Cal {
constructor(namespace: string, q: InstructionQueue) {
this.__config = {
// Keep cal.com hardcoded till the time embed.js deployment to cal.com/embed.js is automated. This is to prevent accidentally pushing of localhost domain to production
origin: /*import.meta.env.NEXT_PUBLIC_WEBSITE_URL || */ "https://cal.com",
origin: /*import.meta.env.NEXT_PUBLIC_WEBSITE_URL || */ "https://app.cal.com",
};
this.namespace = namespace;
this.actionManager = new SdkActionManager(namespace);
@ -377,9 +404,9 @@ export class Cal {
iframe.style.height = data.iframeHeight + unit;
}
if (data.iframeWidth) {
iframe.style.width = data.iframeWidth + unit;
}
// if (data.iframeWidth) {
// iframe.style.width = data.iframeWidth + unit;
// }
if (this.modalBox) {
// It ensures that if the iframe is so tall that it can't fit in the parent window without scroll. Then force the scroll by restricting the max-height to innerHeight
@ -399,6 +426,13 @@ export class Cal {
this.doInIframe({ method, arg });
});
});
this.actionManager.on("__routeChanged", () => {
if (this.inlineEl && (this.inlineEl as unknown as any).__CalAutoScroll) {
this.inlineEl.scrollIntoView();
}
});
this.actionManager.on("linkReady", (e) => {
this.modalBox?.setAttribute("state", "loaded");
this.inlineEl?.setAttribute("loading", "done");
@ -455,13 +489,12 @@ document.addEventListener("click", (e) => {
if (namespace) {
api = globalCal.ns![namespace];
}
if (!api) {
throw new Error(`Namespace ${namespace} isn't defined`);
}
api("modal", {
calLink: path,
config,
uid: modalUniqueId,
});
});
customElements.define("cal-modal-box", ModalBox);
customElements.define("cal-floating-button", FloatingButton);
customElements.define("cal-inline", Inline);

View File

@ -53,10 +53,13 @@
display: block;
width: 30px;
height: 30px;
margin: 60px auto;
position: relative;
border-width: 4px;
border-style: solid;
-webkit-animation: loader 2s infinite ease;
animation: loader 2s infinite ease;
}
}
.loader.modal-loader {
margin: 60px auto;
}

View File

@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Cal Sans';
src: url("https://cal.com/cal.ttf");
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Cal Sans';
font-weight: normal;
letter-spacing: normal;
}
html, body, :host {
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji
}

View File

@ -1,203 +0,0 @@
* {
-tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(248 248 248 / var(--tw-bg-opacity));
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.leading-5 {
line-height: 1.25rem;
}
.w-7 {
width: 1.75rem;
}
.h-7 {
height: 1.75rem;
}
.font-semibold {
font-weight: 600;
}
.flex {
display: flex;
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.leading-5 {
line-height: 1.25rem;
}
.font-semibold {
font-weight: 600;
}
.mr-3 {
margin-right: 0.75rem;
}
.items-center {
align-items: center;
}
.w-full {
width: 100%;
}
.h-screen {
height: 100%;
}
.flex {
display: flex;
}
.z-highest {
z-index: 500000000;
}
.absolute {
position: absolute;
}
.border-brand {
border-color:var(--cal-brand-border-color);
}
.bg-brand {
background-color: var(--cal-brand-background-color);
}
@media (min-width: 768px) {
.md\:right-10 {
right: 2.5rem;
}
.md\:bottom-6 {
bottom: 1.5rem;
}
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition {
transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform,
filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity,
box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity,
box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.drop-shadow-md {
--tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate)
var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.outline-none {
outline: 2px solid transparent;
outline-offset: 2px;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.rounded-full {
border-radius: 9999px;
}
.items-center {
align-items: center;
}
.cursor-pointer {
cursor: pointer;
}
.transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.origin-center {
transform-origin: center;
}
.h-16 {
height: 4rem;
}
.flex {
display: flex;
}
.right-4 {
right: 1rem;
}
.bottom-4 {
bottom: 1rem;
}
.fixed {
position: fixed;
}
.relative {
position: relative;
}

View File

@ -0,0 +1,17 @@
const base = require("@calcom/config/tailwind-preset");
module.exports = {
...base,
content: ["**/*Html.ts"],
theme: {
...base.theme,
extend: {
...base.theme.extend,
colors: {
...base.theme.extend.colors,
// Set default as black
brand: "var(--cal-brand-color, black)",
},
},
},
};

View File

@ -2,6 +2,9 @@
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"module": "esnext",
"paths": {
"@lib/*": ["../../../apps/web/lib/*"]
},
"moduleResolution": "Node",
"baseUrl": "."
},

View File

@ -2,7 +2,6 @@ require("dotenv").config({ path: "../../../.env" });
const path = require("path");
const { defineConfig } = require("vite");
module.exports = defineConfig({
envPrefix: "NEXT_PUBLIC_",
build: {

View File

@ -0,0 +1,2 @@
.turbo
dist

View File

@ -2,4 +2,13 @@
Embed Cal Link as a React Component
To know how to use it, follow the steps at <https://docs.cal.com/integrations/embed>
To know how to use it, follow the steps at <https://docs.cal.com/integrations/embed>
TODO
- Playwright tests.
- Need to what these tests should be as embed-core already have tests. We probably just need to verify that embed-core API is called appropriately.
- It would probably be better if Playwright tests exist at one place for all embeds.
- Distribution
- It would be better DX to serve the unbuilt version with JSX, instead of built version with React.createElement calls. But because of WebPack loaders not running on node_modules automatically, it doesn't work automatically.
- Right now if a typescript project uses the package, VSCode takes the user to .d.ts files instead of the functions definitions. How to solve it ?

View File

@ -3,18 +3,42 @@
"version": "1.0.1",
"description": "Embed Cal Link as a React Component",
"scripts": {
"dev": "vite --port=3003 --open",
"build": "vite build",
"dev": "vite --port=3101 --open",
"tsc": "tsc",
"build": "vite build && yarn tsc --emitDeclarationOnly --declarationDir dist",
"preview": "vite preview",
"prepare": "yarn build",
"type-check": "tsc --pretty --noEmit",
"lint": "eslint --ext .ts,.js,.tsx,.jsx ./src"
"lint": "eslint --ext .ts,.js,.tsx,.jsx ./src",
"embed-tests": "yarn playwright test --config=./playwright/config/playwright.config.ts",
"embed-tests-quick": "QUICK=true yarn embed-tests"
},
"main": "src/Cal.tsx",
"dependencies": {
"@calcom/embed-snippet": "^1.0.0"
"main": "./dist/Cal.umd.js",
"module": "./dist/Cal.es.js",
"types": "./dist/src/index.d.ts",
"peerDependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0"
},
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/Cal.es.js",
"require": "./dist/Cal.umd.js"
}
},
"devDependencies": {
"vite": "^2.8.6",
"eslint": "^8.10.0"
"@calcom/embed-snippet": "^1.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@vitejs/plugin-react": "^1.3.0",
"eslint": "^8.10.0",
"vite": "^2.9.5"
},
"dependencies": {
"playwright": "^1.21.1",
"typescript": "^4.6.3"
}
}

View File

@ -0,0 +1,34 @@
import { PlaywrightTestConfig, devices } from "@playwright/test";
import path from "path";
//TODO: Move the common config to embed-playwright-config and let core and react use the base. Along with config there would be base fixtures and expect custom matchers as well.
import baseConfig from "@calcom/embed-core/playwright/config/playwright.config";
const testDir = path.join("../tests");
const projects = baseConfig.projects?.map((project) => {
if (!project.name) {
return {};
}
return {
...project,
testDir,
};
});
const config: PlaywrightTestConfig = {
...baseConfig,
webServer: {
// Start App Server manually - Can't be handled here. See https://github.com/microsoft/playwright/issues/8206
command: "yarn workspace @calcom/embed-react dev",
port: 3101,
timeout: 60_000,
reuseExistingServer: true,
},
use: {
...baseConfig.use,
baseURL: "http://localhost:3101",
},
projects,
};
export default config;

View File

@ -0,0 +1,18 @@
import { expect } from "@playwright/test";
import { test } from "@calcom/embed-core/playwright/fixtures/fixtures";
import { getEmbedIframe } from "@calcom/embed-core/playwright/lib/testUtils";
test("Inline Usage Snapshot", async ({ page, getActionFiredDetails, addEmbedListeners }) => {
//TODO: Do it with page.goto automatically
await addEmbedListeners("");
await page.goto("/");
const embedIframe = await getEmbedIframe({ page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink("", getActionFiredDetails, {
pathname: "/pro",
searchParams: {
theme: "dark",
},
});
expect(await page.screenshot()).toMatchSnapshot("react-component-inline.png");
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@ -13,12 +13,17 @@ export default function Cal({
config?: any;
embedJsUrl?: string;
}) {
if (!calLink) {
throw new Error("calLink is required");
}
const initializedRef = useRef(false);
const Cal = useEmbed(embedJsUrl);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!Cal) {
if (!Cal || initializedRef.current) {
return;
}
initializedRef.current = true;
const element = ref.current;
let initConfig = {};
if (calOrigin) {
@ -30,9 +35,6 @@ export default function Cal({
calLink,
config,
});
return () => {
element?.querySelector(".cal-embed")?.remove();
};
}, [Cal, calLink, config, calOrigin]);
if (!Cal) {

View File

@ -0,0 +1,3 @@
import Cal from "./Cal";
export default Cal;

View File

@ -1,8 +1,15 @@
import { useEffect } from "react";
import { useState } from "react";
import ReactDom from "react-dom";
import Cal from "@calcom/embed-react";
import Cal from "./src/index";
function App() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
// Simulate state change causing config object to change, causing rerender of Cal
setTimeout(setLoaded.bind(true), 1000);
}, []);
return (
<>
<h1>
@ -10,7 +17,7 @@ function App() {
</h1>
<Cal
calOrigin="http://localhost:3000"
embedJsUrl="//localhost:3002/dist/embed.umd.js"
embedJsUrl="//localhost:3100/dist/embed.umd.js"
calLink="pro"
config={{
name: "John Doe",

View File

@ -4,8 +4,12 @@
"module": "ESNext",
"moduleResolution": "Node",
"baseUrl": ".",
"jsx": "preserve"
"declaration": true,
"jsx": "preserve",
"paths": {
"@lib/*": ["../../../apps/web/lib/*"]
},
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
"exclude": ["dist", "build", "node_modules", "test-cal.tsx"]
}

View File

@ -0,0 +1,28 @@
import react from "@vitejs/plugin-react";
import path from "path";
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "Cal",
fileName: (format) => `Cal.${format}.js`,
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ["react", "react-dom"],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
react: "React",
"react-dom": "ReactDOM",
},
},
},
},
});

View File

@ -2,7 +2,7 @@
* 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";
import type { Cal as CalClass, InstructionQueue } from "@calcom/embed-core/src/embed";
export interface GlobalCal {
(methodName: string, arg?: any): void;
@ -13,6 +13,7 @@ export interface GlobalCal {
/** If user registers multiple namespaces, those are available here */
ns?: Record<string, GlobalCal>;
instance?: CalClass;
__css?: string;
}
export interface CalWindow extends Window {

View File

@ -122,6 +122,12 @@
},
"postinstall": {},
"start": {},
"embed-tests": {
"cache": false
},
"embed-tests-quick": {
"cache": false
},
"test": {
"dependsOn": ["^test"]
},

326
yarn.lock
View File

@ -117,7 +117,7 @@
json5 "^2.1.2"
semver "^6.3.0"
"@babel/core@^7.7.2", "@babel/core@^7.8.0":
"@babel/core@^7.17.9", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe"
integrity sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==
@ -626,7 +626,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
"@babel/plugin-syntax-jsx@^7.12.13":
"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665"
integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==
@ -891,6 +891,38 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-transform-react-jsx-development@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz#43a00724a3ed2557ed3f276a01a929e6686ac7b8"
integrity sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==
dependencies:
"@babel/plugin-transform-react-jsx" "^7.16.7"
"@babel/plugin-transform-react-jsx-self@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.16.7.tgz#f432ad0cba14c4a1faf44f0076c69e42a4d4479e"
integrity sha512-oe5VuWs7J9ilH3BCCApGoYjHoSO48vkjX2CbA5bFVhIuO2HKxA3vyF7rleA4o6/4rTDbk6r8hBW7Ul8E+UZrpA==
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-transform-react-jsx-source@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz#1879c3f23629d287cc6186a6c683154509ec70c0"
integrity sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-transform-react-jsx@^7.16.7", "@babel/plugin-transform-react-jsx@^7.17.3":
version "7.17.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1"
integrity sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==
dependencies:
"@babel/helper-annotate-as-pure" "^7.16.7"
"@babel/helper-module-imports" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-jsx" "^7.16.7"
"@babel/types" "^7.17.0"
"@babel/plugin-transform-regenerator@^7.16.7":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz#0a33c3a61cf47f45ed3232903683a0afd2d3460c"
@ -1337,7 +1369,7 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
"@eslint/eslintrc@^1.2.0", "@eslint/eslintrc@^1.2.1":
"@eslint/eslintrc@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6"
integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==
@ -1619,7 +1651,7 @@
resolved "https://registry.yarnpkg.com/@glidejs/glide/-/glide-3.5.2.tgz#7012c5920ecf202bbda44d8526fc979984b6dd54"
integrity sha512-7jGciNJ2bQ4eZLSNlSZ+VAyW63kALf420CvkEpK4lEsUfWJq9odqimci0YCiyNyMUFB+pWHwLYyNc57dijYsCg==
"@headlessui/react@^1.4.1", "@headlessui/react@^1.5.0":
"@headlessui/react@^1.4.1":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.5.0.tgz#483b44ba2c8b8d4391e1d2c863898d7dd0cc0296"
integrity sha512-aaRnYxBb3MU2FNJf3Ut9RMTUqqU3as0aI1lQhgo2n9Fa67wRu14iOGqx93xB+uMNVfNwZ5B3y/Ndm7qZGuFeMQ==
@ -2434,11 +2466,6 @@
dependencies:
webpack-bundle-analyzer "4.3.0"
"@next/env@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.0.tgz#73713399399b34aa5a01771fb73272b55b22c314"
integrity sha512-nrIgY6t17FQ9xxwH3jj0a6EOiQ/WDHUos35Hghtr+SWN/ntHIQ7UpuvSi0vaLzZVHQWaDupKI+liO5vANcDeTQ==
"@next/env@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.4.tgz#5af629b43075281ecd7f87938802b7cf5b67e94b"
@ -2466,11 +2493,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.5.tgz#36729ab3dfd7743e82cfe536b43254dcb146620c"
integrity sha512-SKnGTdYcoN04Y2DvE0/Y7/MjkA+ltsmbuH/y/hR7Ob7tsj+8ZdOYuk+YvW1B8dY20nDPHP58XgDTSm2nA8BzzA==
"@next/swc-android-arm64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39"
integrity sha512-/280MLdZe0W03stA69iL+v6I+J1ascrQ6FrXBlXGCsGzrfMaGr7fskMa0T5AhQIVQD4nA/46QQWxG//DYuFBcA==
"@next/swc-android-arm64@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.4.tgz#f320d60639e19ecffa1f9034829f2d95502a9a51"
@ -2481,11 +2503,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.5.tgz#52578f552305c92d0b9b81d603c9643fb71e0835"
integrity sha512-YXiqgQ/9Rxg1dXp6brXbeQM1JDx9SwUY/36JiE+36FXqYEmDYbxld9qkX6GEzkc5rbwJ+RCitargnzEtwGW0mw==
"@next/swc-darwin-arm64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.0.tgz#08e8b411b8accd095009ed12efbc2f1d4d547135"
integrity sha512-R8vcXE2/iONJ1Unf5Ptqjk6LRW3bggH+8drNkkzH4FLEQkHtELhvcmJwkXcuipyQCsIakldAXhRbZmm3YN1vXg==
"@next/swc-darwin-arm64@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.4.tgz#fd578278312613eddcf3aee26910100509941b63"
@ -2496,11 +2513,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.5.tgz#3d5b53211484c72074f4975ba0ec2b1107db300e"
integrity sha512-y8mhldb/WFZ6lFeowkGfi0cO/lBdiBqDk4T4LZLvCpoQp4Or/NzUN6P5NzBQZ5/b4oUHM/wQICEM+1wKA4qIVw==
"@next/swc-darwin-x64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.0.tgz#fcd684497a76e8feaca88db3c394480ff0b007cd"
integrity sha512-ieAz0/J0PhmbZBB8+EA/JGdhRHBogF8BWaeqR7hwveb6SYEIJaDNQy0I+ZN8gF8hLj63bEDxJAs/cEhdnTq+ug==
"@next/swc-darwin-x64@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.4.tgz#ace5f80d8c8348efe194f6d7074c6213c52b3944"
@ -2511,11 +2523,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.5.tgz#adcabb732d226453777c0d37d58eaff9328b66fd"
integrity sha512-wqJ3X7WQdTwSGi0kIDEmzw34QHISRIQ5uvC+VXmsIlCPFcMA+zM5723uh8NfuKGquDMiEMS31a83QgkuHMYbwQ==
"@next/swc-linux-arm-gnueabihf@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.0.tgz#9ec6380a27938a5799aaa6035c205b3c478468a7"
integrity sha512-njUd9hpl6o6A5d08dC0cKAgXKCzm5fFtgGe6i0eko8IAdtAPbtHxtpre3VeSxdZvuGFh+hb0REySQP9T1ttkog==
"@next/swc-linux-arm-gnueabihf@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.4.tgz#2bf2c83863635f19c71c226a2df936e001cce29c"
@ -2526,11 +2533,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.5.tgz#82a7cde67482b756bc65fbebf1dfa8a782074e93"
integrity sha512-WnhdM5duONMvt2CncAl+9pim0wBxDS2lHoo7ub/o/i1bRbs11UTzosKzEXVaTDCUkCX2c32lIDi1WcN2ZPkcdw==
"@next/swc-linux-arm64-gnu@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.0.tgz#7f4196dff1049cea479607c75b81033ae2dbd093"
integrity sha512-OqangJLkRxVxMhDtcb7Qn1xjzFA3s50EIxY7mljbSCLybU+sByPaWAHY4px97ieOlr2y4S0xdPKkQ3BCAwyo6Q==
"@next/swc-linux-arm64-gnu@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.4.tgz#d577190f641c9b4b463719dd6b8953b6ba9be8d9"
@ -2541,11 +2543,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.5.tgz#f82ca014504950aab751e81f467492e9be0bad5d"
integrity sha512-Jq2H68yQ4bLUhR/XQnbw3LDW0GMQn355qx6rU36BthDLeGue7YV7MqNPa8GKvrpPocEMW77nWx/1yI6w6J07gw==
"@next/swc-linux-arm64-musl@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.0.tgz#b445f767569cdc2dddee785ca495e1a88c025566"
integrity sha512-hB8cLSt4GdmOpcwRe2UzI5UWn6HHO/vLkr5OTuNvCJ5xGDwpPXelVkYW/0+C3g5axbDW2Tym4S+MQCkkH9QfWA==
"@next/swc-linux-arm64-musl@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.4.tgz#e70ffe70393d8f9242deecdb282ce5a8fd588b14"
@ -2556,11 +2553,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.5.tgz#f811ec9f4b12a978426c284c95ab2f515ddf7f9e"
integrity sha512-KgPjwdbhDqXI7ghNN8V/WAiLquc9Ebe8KBrNNEL0NQr+yd9CyKJ6KqjayVkmX+hbHzbyvbui/5wh/p3CZQ9xcQ==
"@next/swc-linux-x64-gnu@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.0.tgz#67610e9be4fbc987de7535f1bcb17e45fe12f90e"
integrity sha512-OKO4R/digvrVuweSw/uBM4nSdyzsBV5EwkUeeG4KVpkIZEe64ZwRpnFB65bC6hGwxIBnTv5NMSnJ+0K/WmG78A==
"@next/swc-linux-x64-gnu@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.4.tgz#91498a130387fb1961902f2bee55863f8e910cff"
@ -2571,11 +2563,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.5.tgz#d44857257e6d20dc841998951d584ab1f25772c3"
integrity sha512-O2ErUTvCJ6DkNTSr9pbu1n3tcqykqE/ebty1rwClzIYdOgpB3T2MfEPP+K7GhUR87wmN/hlihO9ch7qpVFDGKw==
"@next/swc-linux-x64-musl@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.0.tgz#ea19a23db08a9f2e34ac30401f774cf7d1669d31"
integrity sha512-JohhgAHZvOD3rQY7tlp7NlmvtvYHBYgY0x5ZCecUT6eCCcl9lv6iV3nfu82ErkxNk1H893fqH0FUpznZ/H3pSw==
"@next/swc-linux-x64-musl@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.4.tgz#78057b03c148c121553d41521ad38f6c732762ff"
@ -2586,11 +2573,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.5.tgz#3cc523abadc9a2a6de680593aff06e71cc29ecef"
integrity sha512-1eIlZmlO/VRjxxzUBcVosf54AFU3ltAzHi+BJA+9U/lPxCYIsT+R4uO3QksRzRjKWhVQMRjEnlXyyq5SKJm7BA==
"@next/swc-win32-arm64-msvc@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.0.tgz#eadf054fc412085659b98e145435bbba200b5283"
integrity sha512-T/3gIE6QEfKIJ4dmJk75v9hhNiYZhQYAoYm4iVo1TgcsuaKLFa+zMPh4056AHiG6n9tn2UQ1CFE8EoybEsqsSw==
"@next/swc-win32-arm64-msvc@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.4.tgz#05bbaabacac23b8edf6caa99eb86b17550a09051"
@ -2601,11 +2583,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.5.tgz#c62232d869f1f9b22e8f24e4e7f05307c20f30ca"
integrity sha512-oromsfokbEuVb0CBLLE7R9qX3KGXucZpsojLpzUh1QJjuy1QkrPJncwr8xmWQnwgtQ6ecMWXgXPB+qtvizT9Tw==
"@next/swc-win32-ia32-msvc@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.0.tgz#68faeae10c89f698bf9d28759172b74c9c21bda1"
integrity sha512-iwnKgHJdqhIW19H9PRPM9j55V6RdcOo6rX+5imx832BCWzkDbyomWnlzBfr6ByUYfhohb8QuH4hSGEikpPqI0Q==
"@next/swc-win32-ia32-msvc@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.4.tgz#8fd2fb48f04a2802e51fc320878bf6b411c1c866"
@ -2616,11 +2593,6 @@
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.5.tgz#2bd9b28a9ba730d12a493e7d9d18e150fe89d496"
integrity sha512-a/51L5KzBpeZSW9LbekMo3I3Cwul+V+QKwbEIMA+Qwb2qrlcn1L9h3lt8cHqNTFt2y72ce6aTwDTw1lyi5oIRA==
"@next/swc-win32-x64-msvc@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.0.tgz#d27e7e76c87a460a4da99c5bfdb1618dcd6cd064"
integrity sha512-aBvcbMwuanDH4EMrL2TthNJy+4nP59Bimn8egqv6GHMVj0a44cU6Au4PjOhLNqEh9l+IpRGBqMTzec94UdC5xg==
"@next/swc-win32-x64-msvc@12.1.4":
version "12.1.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.4.tgz#a72ed44c9b1f850986a30fe36c59e01f8a79b5f3"
@ -3385,6 +3357,14 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^4.2.0":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@rushstack/eslint-patch@1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.8.tgz#be3e914e84eacf16dbebd311c0d0b44aa1174c64"
@ -4017,11 +3997,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/node@17.0.21":
version "17.0.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644"
integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==
"@types/node@^12.12.6":
version "12.20.47"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.47.tgz#ca9237d51f2a2557419688511dab1c8daf475188"
@ -4087,6 +4062,13 @@
dependencies:
"@types/react" "*"
"@types/react-dom@^17.0.0":
version "17.0.15"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.15.tgz#f2c8efde11521a4b7991e076cb9c70ba3bb0d156"
integrity sha512-Tr9VU9DvNoHDWlmecmcsE5ZZiUkYx+nKBzum4Oxe1K0yJVyBlfbq7H3eXjxXqJczBKqPGq3EgfTru4MgKb9+Yw==
dependencies:
"@types/react" "^17"
"@types/react-phone-number-input@^3.0.13":
version "3.0.13"
resolved "https://registry.yarnpkg.com/@types/react-phone-number-input/-/react-phone-number-input-3.0.13.tgz#4eb7dcd278dcf9eb2a8d2ce2cb304657cbf1b4e5"
@ -4143,10 +4125,10 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@17.0.40":
version "17.0.40"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.40.tgz#dc010cee6254d5239a138083f3799a16638e6bad"
integrity sha512-UrXhD/JyLH+W70nNSufXqMZNuUD2cXHu6UjCllC6pmOQgBX4SGXOH8fjRka0O0Ee0HrFxapDD8Bwn81Kmiz6jQ==
"@types/react@^17", "@types/react@^17.0.0":
version "17.0.44"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.44.tgz#c3714bd34dd551ab20b8015d9d0dbec812a51ec7"
integrity sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@ -4383,6 +4365,20 @@
clsx "^1.1.1"
next-transpile-modules "^8.0.0"
"@vitejs/plugin-react@^1.3.0":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-1.3.1.tgz#bf008adf33e713215cd4a6b94a75146dd6891975"
integrity sha512-qQS8Y2fZCjo5YmDUplEXl3yn+aueiwxB7BaoQ4nWYJYR+Ai8NXPVLlkLobVMs5+DeyFyg9Lrz6zCzdX1opcvyw==
dependencies:
"@babel/core" "^7.17.9"
"@babel/plugin-transform-react-jsx" "^7.17.3"
"@babel/plugin-transform-react-jsx-development" "^7.16.7"
"@babel/plugin-transform-react-jsx-self" "^7.16.7"
"@babel/plugin-transform-react-jsx-source" "^7.16.7"
"@rollup/pluginutils" "^4.2.0"
react-refresh "^0.12.0"
resolve "^1.22.0"
"@wojtekmaj/date-utils@^1.0.2", "@wojtekmaj/date-utils@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz#2dcfd92881425c5923e429c2aec86fb3609032a1"
@ -4798,7 +4794,7 @@ autolinker@^3.11.0:
dependencies:
tslib "^2.3.0"
autoprefixer@^10.3.4, autoprefixer@^10.4.0, autoprefixer@^10.4.2:
autoprefixer@^10.3.4, autoprefixer@^10.4.0, autoprefixer@^10.4.4:
version "10.4.4"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.4.tgz#3e85a245b32da876a893d3ac2ea19f01e7ea5a1e"
integrity sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==
@ -5627,11 +5623,6 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chart.js@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.1.tgz#0516f690c6a8680c6c707e31a4c1807a6f400ada"
integrity sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA==
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@ -7175,47 +7166,6 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@8.10.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d"
integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==
dependencies:
"@eslint/eslintrc" "^1.2.0"
"@humanwhocodes/config-array" "^0.9.2"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.1.1"
eslint-utils "^3.0.0"
eslint-visitor-keys "^3.3.0"
espree "^9.3.1"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^6.0.1"
globals "^13.6.0"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
regexpp "^3.2.0"
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
eslint@^8.10.0, eslint@^8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.11.0.tgz#88b91cfba1356fc10bb9eb592958457dfe09fb37"
@ -12312,29 +12262,6 @@ next-validations@^0.1.11:
resolved "https://registry.yarnpkg.com/next-validations/-/next-validations-0.1.11.tgz#fcc62dea5be8f9793d410de175f96e3fc1dac54d"
integrity sha512-rdyRgZ3f3jwhLigdi9MC5R74BvRpB3cewa8LVnMHDiDRnSThvX0CdZ5KHK4t/SgrIGaVXiXOQ59KtvBqjcm5pA==
next@12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/next/-/next-12.1.0.tgz#c33d753b644be92fc58e06e5a214f143da61dd5d"
integrity sha512-s885kWvnIlxsUFHq9UGyIyLiuD0G3BUC/xrH0CEnH5lHEWkwQcHOORgbDF0hbrW9vr/7am4ETfX4A7M6DjrE7Q==
dependencies:
"@next/env" "12.1.0"
caniuse-lite "^1.0.30001283"
postcss "8.4.5"
styled-jsx "5.0.0"
use-subscription "1.5.1"
optionalDependencies:
"@next/swc-android-arm64" "12.1.0"
"@next/swc-darwin-arm64" "12.1.0"
"@next/swc-darwin-x64" "12.1.0"
"@next/swc-linux-arm-gnueabihf" "12.1.0"
"@next/swc-linux-arm64-gnu" "12.1.0"
"@next/swc-linux-arm64-musl" "12.1.0"
"@next/swc-linux-x64-gnu" "12.1.0"
"@next/swc-linux-x64-musl" "12.1.0"
"@next/swc-win32-arm64-msvc" "12.1.0"
"@next/swc-win32-ia32-msvc" "12.1.0"
"@next/swc-win32-x64-msvc" "12.1.0"
next@12.1.4, next@^12.1.0:
version "12.1.4"
resolved "https://registry.yarnpkg.com/next/-/next-12.1.4.tgz#597a9bdec7aec778b442c4f6d41afd2c64a54b23"
@ -12622,6 +12549,11 @@ object-hash@^2.0.1, object-hash@^2.2.0:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
object-hash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
object-inspect@^1.12.0, object-inspect@^1.9.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
@ -13374,6 +13306,37 @@ playwright-core@1.20.2:
yauzl "2.10.0"
yazl "2.5.1"
playwright-core@1.21.1:
version "1.21.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.21.1.tgz#2757be7921576f047c0a622194dc45f4e1962e17"
integrity sha512-SbK5dEsai9ZUKlxcinqegorBq4GnftXd4/GfW+pLsdQIQWrLCM/JNh6YQ2Rf2enVykXCejtoXW8L5vJXBBVSJQ==
dependencies:
colors "1.4.0"
commander "8.3.0"
debug "4.3.3"
extract-zip "2.0.1"
https-proxy-agent "5.0.0"
jpeg-js "0.4.3"
mime "3.0.0"
pixelmatch "5.2.1"
pngjs "6.0.0"
progress "2.0.3"
proper-lockfile "4.1.2"
proxy-from-env "1.1.0"
rimraf "3.0.2"
socks-proxy-agent "6.1.1"
stack-utils "2.0.5"
ws "8.4.2"
yauzl "2.10.0"
yazl "2.5.1"
playwright@^1.21.1:
version "1.21.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.21.1.tgz#62bdefc0e8baba192d93d8daf0c0eb9213869d76"
integrity sha512-Of0h1XAvsqK1XfHVZ8sL2PjJVoQUu9gTmmMTtLS7MEyWMRD0kn8myeI90xj1ncJhUysQxGboH64S5v+lL2USrg==
dependencies:
playwright-core "1.21.1"
pngjs@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
@ -13406,7 +13369,7 @@ postcss-js@^4.0.0:
dependencies:
camelcase-css "^2.0.1"
postcss-load-config@^3.1.0:
postcss-load-config@^3.1.0, postcss-load-config@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
@ -13421,7 +13384,7 @@ postcss-nested@5.0.6:
dependencies:
postcss-selector-parser "^6.0.6"
postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9:
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
@ -13443,7 +13406,7 @@ postcss@8.4.5:
picocolors "^1.0.0"
source-map-js "^1.0.1"
postcss@^8.3.6, postcss@^8.4.12, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.8:
postcss@^8.3.6, postcss@^8.4.12, postcss@^8.4.4, postcss@^8.4.6:
version "8.4.12"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
@ -13868,11 +13831,6 @@ react-calendar@^3.3.1:
merge-class-names "^1.1.1"
prop-types "^15.6.0"
react-chartjs-2@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz#2a123df16d3a987c54eb4e810ed766d3c03adf8d"
integrity sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==
react-colorful@^5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
@ -14068,6 +14026,11 @@ react-redux@^7.2.4:
prop-types "^15.7.2"
react-is "^17.0.2"
react-refresh@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.12.0.tgz#28ac0a2c30ef2bb3433d5fd0621e69a6d774c3a4"
integrity sha512-suLIhrU2IHKL5JEKR/fAwJv7bbeq4kJ+pJopf77jHwuR+HmJS/HbrPIGsTBUVfw7tXPOmYv7UJ7PCaN49e8x4A==
react-remove-scroll-bar@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.2.0.tgz#d4d545a7df024f75d67e151499a6ab5ac97c8cdd"
@ -15543,11 +15506,6 @@ style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
styled-jsx@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.0.tgz#816b4b92e07b1786c6b7111821750e0ba4d26e77"
integrity sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA==
styled-jsx@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.1.tgz#78fecbbad2bf95ce6cd981a08918ce4696f5fc80"
@ -15712,11 +15670,6 @@ swarm-js@^0.1.40:
tar "^4.0.2"
xhr-request "^1.0.1"
swr@^1.2.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -15749,6 +15702,33 @@ tailwindcss@^3.0.23:
quick-lru "^5.1.1"
resolve "^1.22.0"
tailwindcss@^3.0.24:
version "3.0.24"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.24.tgz#22e31e801a44a78a1d9a81ecc52e13b69d85704d"
integrity sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==
dependencies:
arg "^5.0.1"
chokidar "^3.5.3"
color-name "^1.1.4"
detective "^5.2.0"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.11"
glob-parent "^6.0.2"
is-glob "^4.0.3"
lilconfig "^2.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.4.12"
postcss-js "^4.0.0"
postcss-load-config "^3.1.4"
postcss-nested "5.0.6"
postcss-selector-parser "^6.0.10"
postcss-value-parser "^4.2.0"
quick-lru "^5.1.1"
resolve "^1.22.0"
tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
@ -16074,6 +16054,11 @@ ts-node@^10.6.0:
v8-compile-cache-lib "^3.0.0"
yn "3.1.1"
tsc@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/tsc/-/tsc-2.0.4.tgz#5f6499146abea5dca4420b451fa4f2f9345238f5"
integrity sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==
tsconfig-paths@^3.11.0, tsconfig-paths@^3.12.0, tsconfig-paths@^3.9.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
@ -16631,13 +16616,6 @@ use-sidecar@^1.0.1:
detect-node-es "^1.1.0"
tslib "^1.9.3"
use-subscription@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==
dependencies:
object-assign "^4.1.1"
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
@ -16831,6 +16809,18 @@ vite@^2.8.6:
optionalDependencies:
fsevents "~2.3.2"
vite@^2.9.5:
version "2.9.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.5.tgz#08ef37ac7a6d879c96f328b791732c9a00ea25ea"
integrity sha512-dvMN64X2YEQgSXF1lYabKXw3BbN6e+BL67+P3Vy4MacnY+UzT1AfkHiioFSi9+uiDUiaDy7Ax/LQqivk6orilg==
dependencies:
esbuild "^0.14.27"
postcss "^8.4.12"
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"