diff --git a/apps/docs/pages/integrations/embed.mdx b/apps/docs/pages/integrations/embed.mdx index 4503cb3b..694f41c0 100644 --- a/apps/docs/pages/integrations/embed.mdx +++ b/apps/docs/pages/integrations/embed.mdx @@ -1,8 +1,180 @@ --- -title: Embed Snippet +title: Embed --- -# Embed Snippet +# Embed -The Embed Snippet allows your website visitors to book a meeting with you directly from your website. It works by you installing a small Javascript Snippet to your website. -[Mention possiblity of installation through tag managers as well] +The Embed allows your website visitors to book a meeting with you directly from your website. + +## Install on any website + +TODO: Mention possibility of installation through tag managers as well + +- _Step-1._ Install the Vanilla JS Snippet + + ```javascript + (function (C, A, L) { + let p = function (a, ar) { + a.q.push(ar); + }; + let d = C.document; + C.Cal = + C.Cal || + function () { + let cal = C.Cal; + let ar = arguments; + if (!cal.loaded) { + cal.ns = {}; + cal.q = cal.q || []; + d.head.appendChild(d.createElement("script")).src = A; + cal.loaded = true; + } + if (ar[0] === L) { + const api = function () { + p(api, arguments); + }; + const namespace = ar[1]; + api.q = api.q || []; + typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar); + return; + } + p(cal, ar); + }; + })(window, "https://cal.com/embed.js", "init"); + ``` + +- _Step-2_. Initialize it + + ```javascript + Cal("init) + ``` + +## Install with a Framework + +### embed-react + +It provides a react component `` that can be used to show the embed inline at that place. + +```bash +yarn add @calcom/embed-react +``` + +### Any XYZ Framework + +You can use Vanilla JS Snippet to install + +## Popular ways in which you can embed on your website + +Assuming that you have followed the steps of installing and initializing the snippet, you can add show the embed in following ways: + +### Inline + +Show the embed inline inside a container element. It would take the width and height of the container element. + +
+ _Vanilla JS_ + +```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 + }, +}); +``` + +
+ +#### + +
+_React_ + +```jsx +import Cal from "@calcom/embed-react"; + +const MyComponent = () => ( + +); +``` + +
+ +### Popup on any existing element + +To show the embed as a popup on clicking an element, add `data-cal-link` attribute to the element. + +
+ +Vanilla JS + +To show the embed as a popup on clicking an element, simply add `data-cal-link` attribute to the element. + + +
+ +
+ React + ```jsx + import "@calcom/embed-react"; + + const MyComponent = ()=> { + return + } + +```` + +
+### Full Screen + +## Supported Instructions + +Consider an instruction as a function with that name and that would be called with the given arguments. + +### `inline` + +Appends embed inline as the child of the element. + +```javascript +Cal("inline", { elementOrSelector, calLink }); +```` + +- `elementOrSelector` - Give it either a valid CSS selector or an HTMLElement instance directly + +- `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL [https://cal.com/john](). It makes it easy to configure the calendar host once and use as many links you want with just usernames + +### `ui` + +Configure UI for embed. Make it look part of your webpage. + +```javascript +Cal("inline", { styles }); +``` + +- `styles` - It supports styling for `body` and `eventTypeListItem`. Right now we support just background on these two. + +### preload + +Usage: + +If you want to open cal link on some action. Make it pop open instantly by preloading it. + +```javascript +Cal("preload", { calLink }); +``` + +- `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]() diff --git a/apps/web/components/CustomBranding.tsx b/apps/web/components/CustomBranding.tsx index 66377f8f..314443e3 100644 --- a/apps/web/components/CustomBranding.tsx +++ b/apps/web/components/CustomBranding.tsx @@ -1,5 +1,7 @@ import { useEffect } from "react"; +import { useBrandColors } from "@calcom/embed-core"; + const brandColor = "#292929"; const brandTextColor = "#ffffff"; const darkBrandColor = "#fafafa"; @@ -220,6 +222,8 @@ const BrandColor = ({ lightVal: string | undefined | null; darkVal: string | undefined | null; }) => { + const embedBrandingColors = useBrandColors(); + lightVal = embedBrandingColors.brandColor || lightVal; // convert to 6 digit equivalent if 3 digit code is entered lightVal = normalizeHexCode(lightVal, false); darkVal = normalizeHexCode(darkVal, true); @@ -235,6 +239,34 @@ const BrandColor = ({ : "#" + darkVal : fallBackHex(darkVal, true); useEffect(() => { + document.documentElement.style.setProperty( + "--booking-highlight-color", + embedBrandingColors.highlightColor || "#10B981" // green--500 + ); + document.documentElement.style.setProperty( + "--booking-lightest-color", + embedBrandingColors.lightestColor || "#E1E1E1" // gray--200 + ); + document.documentElement.style.setProperty( + "--booking-lighter-color", + embedBrandingColors.lighterColor || "#ACACAC" // gray--400 + ); + document.documentElement.style.setProperty( + "--booking-light-color", + embedBrandingColors.lightColor || "#888888" // gray--500 + ); + document.documentElement.style.setProperty( + "--booking-median-color", + embedBrandingColors.medianColor || "#494949" // gray--600 + ); + document.documentElement.style.setProperty( + "--booking-dark-color", + embedBrandingColors.darkColor || "#313131" // gray--800 + ); + document.documentElement.style.setProperty( + "--booking-darker-color", + embedBrandingColors.darkerColor || "#292929" // gray--900 + ); document.documentElement.style.setProperty("--brand-color", lightVal); document.documentElement.style.setProperty("--brand-text-color", getContrastingTextColor(lightVal, true)); document.documentElement.style.setProperty("--brand-color-dark-mode", darkVal); diff --git a/apps/web/components/booking/AvailableTimes.tsx b/apps/web/components/booking/AvailableTimes.tsx index 08dbac1c..bedb05a6 100644 --- a/apps/web/components/booking/AvailableTimes.tsx +++ b/apps/web/components/booking/AvailableTimes.tsx @@ -66,9 +66,9 @@ const AvailableTimes: FC = ({ return (
- + {nameOfDay(i18n.language, Number(date.format("d")))} - + {date.format(", D ")} {date.toDate().toLocaleString(i18n.language, { month: "long" })} @@ -105,7 +105,7 @@ const AvailableTimes: FC = ({ diff --git a/apps/web/components/booking/DatePicker.tsx b/apps/web/components/booking/DatePicker.tsx index 3877ef79..8cfd5bd9 100644 --- a/apps/web/components/booking/DatePicker.tsx +++ b/apps/web/components/booking/DatePicker.tsx @@ -89,7 +89,6 @@ function DatePicker({ const [browsingDate, setBrowsingDate] = useState(date); const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton"); const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton"); - const [month, setMonth] = useState(""); const [year, setYear] = useState(""); const [isFirstMonth, setIsFirstMonth] = useState(false); @@ -238,17 +237,17 @@ function DatePicker({ ? "w-full sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 " : "w-full sm:pl-4") }> -
- - {month}{" "} - {year} +
+ + {month}{" "} + {year} -
+
-
+
{weekdayNames(i18n.language, weekStart === "Sunday" ? 0 : 1, "short").map((weekDay) => ( -
+
{weekDay}
))} @@ -286,7 +285,9 @@ function DatePicker({ className={classNames( "absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm text-center", "hover:border-brand hover:border dark:hover:border-white", - day.disabled ? "cursor-default font-light text-gray-400 hover:border-0" : "font-medium", + day.disabled + ? "text-bookinglighter cursor-default font-light hover:border-0" + : "font-medium", date && date.isSame(browsingDate.date(day.date), "day") ? "bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast" : !day.disabled diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index 7205db33..9a29f279 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -16,6 +16,9 @@ import { useRouter } from "next/router"; import { useEffect, useMemo, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; +import { useEmbedStyles, useIsEmbed, useIsBackgroundTransparent } from "@calcom/embed-core"; +import classNames from "@calcom/lib/classNames"; + import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { BASE_URL } from "@lib/config/constants"; @@ -44,10 +47,13 @@ type Props = AvailabilityTeamPageProps | AvailabilityPageProps; const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage }: Props) => { const router = useRouter(); + const isEmbed = useIsEmbed(); const { rescheduleUid } = router.query; const { isReady, Theme } = useTheme(profile.theme); const { t } = useLocale(); const { contracts } = useContracts(); + const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker"); + let isBackgroundTransparent = useIsBackgroundTransparent(); useExposePlanGlobally(plan); useEffect(() => { if (eventType.metadata.smartContractAddress) { @@ -129,11 +135,19 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
{isReady && ( -
+
{/* mobile: details */}
@@ -156,7 +170,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage />

{profile.name}

-
+
{eventType.title}
@@ -203,16 +217,16 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage size={10} truncateAfter={3} /> -

{profile.name}

-

+

{profile.name}

+

{eventType.title}

-

+

{eventType.length} {t("minutes")}

{eventType.price > 0 && ( -

+

)} - {(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && } + {(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && }
@@ -282,7 +296,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage function TimezoneDropdown() { return ( - + {timeZone()} {isTimeOptionsOpen ? ( diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index fc0dda4b..bf8d2599 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -12,6 +12,8 @@ import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { useMutation } from "react-query"; +import { useIsEmbed, useEmbedStyles, useIsBackgroundTransparent } from "@calcom/embed-core"; +import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { createPaymentLink } from "@calcom/stripe/client"; @@ -63,9 +65,12 @@ const BookingPage = ({ locationLabels, }: BookingPageProps) => { const { t, i18n } = useLocale(); + const isEmbed = useIsEmbed(); const router = useRouter(); const { contracts } = useContracts(); const { data: session } = useSession(); + const isBackgroundTransparent = useIsBackgroundTransparent(); + useEffect(() => { if (eventType.metadata.smartContractAddress) { const eventOwner = eventType.users[0]; @@ -283,9 +288,18 @@ const BookingPage = ({ -
+
{isReady && ( -
+
-

{profile.name}

-

+

+ {profile.name} +

+

{eventType.title}

-

+

{eventType.length} {t("minutes")}

{eventType.price > 0 && ( -

+

)} -

+

{parseDate(date)}

{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && ( -

+

{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}

)} diff --git a/apps/web/components/ui/PoweredByCal.tsx b/apps/web/components/ui/PoweredByCal.tsx index cce37c66..4ac80352 100644 --- a/apps/web/components/ui/PoweredByCal.tsx +++ b/apps/web/components/ui/PoweredByCal.tsx @@ -1,13 +1,16 @@ import Link from "next/link"; +import { useIsEmbed } from "@calcom/embed-core"; + import { useLocale } from "@lib/hooks/useLocale"; const PoweredByCal = () => { const { t } = useLocale(); + const isEmbed = useIsEmbed(); return ( -
+
- + {t("powered_by")}{" "} (
{time.format("HH:mm")} diff --git a/apps/web/lib/hooks/useExposePlanGlobally.ts b/apps/web/lib/hooks/useExposePlanGlobally.ts index ea59dce7..2e4132f4 100644 --- a/apps/web/lib/hooks/useExposePlanGlobally.ts +++ b/apps/web/lib/hooks/useExposePlanGlobally.ts @@ -2,6 +2,9 @@ import { useEffect } from "react"; import { UserPlan } from "@calcom/prisma/client"; +/** + * TODO: It should be exposed at a single place. + */ export function useExposePlanGlobally(plan: UserPlan) { // Don't wait for component to mount. Do it ASAP. Delaying it would delay UI Configuration. if (typeof window !== "undefined") { diff --git a/apps/web/lib/hooks/useTheme.tsx b/apps/web/lib/hooks/useTheme.tsx index bd725947..fbcec078 100644 --- a/apps/web/lib/hooks/useTheme.tsx +++ b/apps/web/lib/hooks/useTheme.tsx @@ -2,6 +2,8 @@ import Head from "next/head"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; +import { useEmbedTheme } from "@calcom/embed-core"; + import { Maybe } from "@trpc/server"; // This method is stringified and executed only on client. So, @@ -28,10 +30,9 @@ function applyThemeAndAddListener(theme: string) { // makes sure the ui doesn't flash export default function useTheme(theme?: Maybe) { const [isReady, setIsReady] = useState(false); - const router = useRouter(); - + const embedTheme = useEmbedTheme(); // Embed UI configuration takes more precedence over App Configuration - theme = (router.query.theme as string | null) || theme; + theme = embedTheme || theme; useEffect(() => { // TODO: isReady doesn't seem required now. This is also impacting PSI Score for pages which are using isReady. diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index e26cfff5..672841db 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from "react"; import { Toaster } from "react-hot-toast"; import { JSONObject } from "superjson/dist/types"; -import { sdkActionManager, useEmbedStyles } from "@calcom/embed-core"; +import { sdkActionManager, useEmbedStyles, useIsEmbed } from "@calcom/embed-core"; import defaultEvents, { getDynamicEventDescription, getUsernameList, @@ -107,6 +107,7 @@ export default function User(props: inferSSRProps) { useExposePlanGlobally("PRO"); const nameOrUsername = user.name || user.username || ""; const [evtsToVerify, setEvtsToVerify] = useState({}); + const isEmbed = useIsEmbed(); return ( <> @@ -119,7 +120,7 @@ export default function User(props: inferSSRProps) { username={isDynamicGroup ? dynamicUsernames.join(", ") : (user.username as string) || ""} // avatar={user.avatar || undefined} /> -
+
{isSingleUser && ( // When we deal with a single user, not dynamic group
diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index eb98e4cb..77b39982 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,5 +1,6 @@ import { DefaultSeo } from "next-seo"; import Head from "next/head"; +import { useEffect } from "react"; // import { ReactQueryDevtools } from "react-query/devtools"; import superjson from "superjson"; @@ -22,13 +23,20 @@ import "../styles/fonts.css"; import "../styles/globals.css"; function MyApp(props: AppProps) { - const { Component, pageProps, err } = props; + const { Component, pageProps, err, router } = props; + let pageStatus = "200"; + if (router.pathname === "/404") { + pageStatus = "404"; + } else if (router.pathname === "/500") { + pageStatus = "500"; + } return ( + diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 974a6597..2d509522 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -1,5 +1,6 @@ import { CheckIcon } from "@heroicons/react/outline"; import { ClockIcon, XIcon } from "@heroicons/react/solid"; +import classNames from "classnames"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import toArray from "dayjs/plugin/toArray"; @@ -10,6 +11,7 @@ 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 { sdkActionManager } from "@calcom/embed-core"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -136,6 +138,8 @@ export default function Success(props: inferSSRProps) const { isReady, Theme } = useTheme(props.profile.theme); const { eventType } = props; + const isBackgroundTransparent = useIsBackgroundTransparent(); + const isEmbed = useIsEmbed(); const attendeeName = typeof name === "string" ? name : "Nameless"; const eventNameObject = { @@ -199,25 +203,34 @@ export default function Success(props: inferSSRProps) return ( (isReady && ( -
+
-
-
+
+
{isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? ( ) : null}{" "}
-