@@ -574,6 +582,7 @@ const EventTypesPage = () => {
{data.eventTypeGroups.length === 0 && (
)}
+
>
)}
/>
diff --git a/apps/web/playwright/embed-code-generator.test.ts b/apps/web/playwright/embed-code-generator.test.ts
new file mode 100644
index 00000000..8f8e442c
--- /dev/null
+++ b/apps/web/playwright/embed-code-generator.test.ts
@@ -0,0 +1,188 @@
+import { expect, Page, test } from "@playwright/test";
+
+function chooseEmbedType(page: Page, embedType: string) {
+ page.locator(`[data-testid=${embedType}]`).click();
+}
+
+async function gotToPreviewTab(page: Page) {
+ await page.locator("[data-testid=embed-tabs]").locator("text=Preview").click();
+}
+
+async function clickEmbedButton(page: Page) {
+ const embedButton = page.locator("[data-testid=event-type-embed]");
+ const eventTypeId = await embedButton.getAttribute("data-test-eventtype-id");
+ embedButton.click();
+ return eventTypeId;
+}
+
+async function clickFirstEventTypeEmbedButton(page: Page) {
+ const menu = page.locator("[data-testid*=event-type-options]").first();
+ await menu.click();
+ const eventTypeId = await clickEmbedButton(page);
+ return eventTypeId;
+}
+
+async function expectToBeNavigatingToEmbedTypesDialog(
+ page: Page,
+ { eventTypeId, basePage }: { eventTypeId: string | null; basePage: string }
+) {
+ if (!eventTypeId) {
+ throw new Error("Couldn't find eventTypeId");
+ }
+ await page.waitForNavigation({
+ url: (url) => {
+ return (
+ url.pathname === basePage &&
+ url.searchParams.get("dialog") === "embed" &&
+ url.searchParams.get("eventTypeId") === eventTypeId
+ );
+ },
+ });
+}
+
+async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
+ page: Page,
+ { eventTypeId, embedType, basePage }: { eventTypeId: string | null; embedType: string; basePage: string }
+) {
+ if (!eventTypeId) {
+ throw new Error("Couldn't find eventTypeId");
+ }
+ await page.waitForNavigation({
+ url: (url) => {
+ return (
+ url.pathname === basePage &&
+ url.searchParams.get("dialog") === "embed" &&
+ url.searchParams.get("eventTypeId") === eventTypeId &&
+ url.searchParams.get("embedType") === embedType &&
+ url.searchParams.get("tabName") === "embed-code"
+ );
+ },
+ });
+}
+
+async function expectToContainValidCode(page: Page, { embedType }: { embedType: string }) {
+ const embedCode = await page.locator("[data-testid=embed-code]").inputValue();
+ expect(embedCode.includes("(function (C, A, L)")).toBe(true);
+ expect(embedCode.includes(`Cal ${embedType} embed code begins`)).toBe(true);
+ return {
+ message: () => `passed`,
+ pass: true,
+ };
+}
+
+async function expectToContainValidPreviewIframe(
+ page: Page,
+ { embedType, calLink }: { embedType: string; calLink: string }
+) {
+ expect(await page.locator("[data-testid=embed-preview]").getAttribute("src")).toContain(
+ `/preview.html?embedType=${embedType}&calLink=${calLink}`
+ );
+}
+
+test.describe("Embed Code Generator Tests", () => {
+ test.use({ storageState: "playwright/artifacts/proStorageState.json" });
+
+ test.describe("Event Types Page", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/event-types");
+ });
+
+ test("open Embed Dialog and choose Inline for First Event Type", async ({ page }) => {
+ const eventTypeId = await clickFirstEventTypeEmbedButton(page);
+ await expectToBeNavigatingToEmbedTypesDialog(page, {
+ eventTypeId,
+ basePage: "/event-types",
+ });
+
+ chooseEmbedType(page, "inline");
+
+ await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
+ eventTypeId,
+ embedType: "inline",
+ basePage: "/event-types",
+ });
+
+ await expectToContainValidCode(page, { embedType: "inline" });
+
+ await gotToPreviewTab(page);
+
+ await expectToContainValidPreviewIframe(page, { embedType: "inline", calLink: "pro/30min" });
+ });
+
+ test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page }) => {
+ const eventTypeId = await clickFirstEventTypeEmbedButton(page);
+
+ await expectToBeNavigatingToEmbedTypesDialog(page, {
+ eventTypeId,
+ basePage: "/event-types",
+ });
+
+ chooseEmbedType(page, "floating-popup");
+
+ await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
+ eventTypeId,
+ embedType: "floating-popup",
+ basePage: "/event-types",
+ });
+ await expectToContainValidCode(page, { embedType: "floating-popup" });
+
+ await gotToPreviewTab(page);
+ await expectToContainValidPreviewIframe(page, { embedType: "floating-popup", calLink: "pro/30min" });
+ });
+
+ test("open Embed Dialog and choose element-click for First Event Type", async ({ page }) => {
+ const eventTypeId = await clickFirstEventTypeEmbedButton(page);
+
+ await expectToBeNavigatingToEmbedTypesDialog(page, {
+ eventTypeId,
+ basePage: "/event-types",
+ });
+
+ chooseEmbedType(page, "element-click");
+
+ await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
+ eventTypeId,
+ embedType: "element-click",
+ basePage: "/event-types",
+ });
+ await expectToContainValidCode(page, { embedType: "element-click" });
+
+ await gotToPreviewTab(page);
+ await expectToContainValidPreviewIframe(page, { embedType: "element-click", calLink: "pro/30min" });
+ });
+ });
+
+ test.describe("Event Type Edit Page", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/event-types/3");
+ });
+
+ test("open Embed Dialog for the Event Type", async ({ page }) => {
+ const eventTypeId = await clickEmbedButton(page);
+
+ await expectToBeNavigatingToEmbedTypesDialog(page, {
+ eventTypeId,
+ basePage: "/event-types/3",
+ });
+
+ chooseEmbedType(page, "inline");
+
+ await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
+ eventTypeId,
+ basePage: "/event-types/3",
+ embedType: "inline",
+ });
+
+ await expectToContainValidCode(page, {
+ embedType: "inline",
+ });
+
+ gotToPreviewTab(page);
+
+ await expectToContainValidPreviewIframe(page, {
+ embedType: "inline",
+ calLink: "pro/30min",
+ });
+ });
+ });
+});
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 470d7425..02806885 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -774,6 +774,11 @@
"impersonate_user_tip":"All uses of this feature is audited.",
"impersonating_user_warning":"Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "<0>Click Here to stop0>.",
+ "place_where_cal_widget_appear": "Place this code in your HTML where you want your Cal widget to appear.",
+ "copy_code": "Copy Code",
+ "code_copied": "Code copied!",
+ "how_you_want_add_cal_site":"How do you want to add Cal to your site?",
+ "choose_ways_put_cal_site":"Choose one of the following ways to put Cal on your site.",
"setting_up_zapier": "Setting up your Zapier integration",
"generate_api_key": "Generate Api Key",
"your_unique_api_key": "Your unique API key",
diff --git a/apps/web/server/routers/viewer/eventTypes.tsx b/apps/web/server/routers/viewer/eventTypes.tsx
index bbc2822a..738e68ea 100644
--- a/apps/web/server/routers/viewer/eventTypes.tsx
+++ b/apps/web/server/routers/viewer/eventTypes.tsx
@@ -211,6 +211,40 @@ export const eventTypesRouter = createProtectedRouter()
return next();
})
+ .query("get", {
+ input: z.object({
+ id: z.number(),
+ }),
+ async resolve({ ctx, input }) {
+ const user = await ctx.prisma.user.findUnique({
+ where: {
+ id: ctx.user.id,
+ },
+ select: {
+ id: true,
+ username: true,
+ name: true,
+ startTime: true,
+ endTime: true,
+ bufferTime: true,
+ avatar: true,
+ plan: true,
+ },
+ });
+ if (!user) {
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+ }
+ return await ctx.prisma.eventType.findUnique({
+ where: {
+ id: input.id,
+ },
+ include: {
+ team: true,
+ users: true,
+ },
+ });
+ },
+ })
.mutation("update", {
input: EventTypeUpdateInput.strict(),
async resolve({ ctx, input }) {
diff --git a/packages/embeds/embed-core/README.md b/packages/embeds/embed-core/README.md
index 23795f3b..9b68c9a9 100644
--- a/packages/embeds/embed-core/README.md
+++ b/packages/embeds/embed-core/README.md
@@ -56,6 +56,7 @@ Make `dist/embed.umd.js` servable on URL
- Automation Tests
- Run automation tests in CI
+ - Automation Tests are using snapshots of Booking Page which has current month which requires us to regenerate snapshots every month.
- Bundling Related
- Comments in CSS aren't stripped off
@@ -72,13 +73,8 @@ Make `dist/embed.umd.js` servable on URL
- Dev Experience/Ease of Installation
- 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.
-
-- Performance Improvements
- - 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
@@ -87,8 +83,7 @@ Make `dist/embed.umd.js` servable on URL
- Quick Solution: Serve embed.js also from app, so that they go live together and there is only a slight chance of compatibility issues on going live. Note, that they can still occur as 2 different requests are sent at different times to fetch the libraries and deployments can go live in between,
- UI Config Features
- - Theme switch dynamically - If user switches the theme on website, he should be able to do it on embed. Add a demo for the API. Also, test system theme handling.
- - How would the user add on hover styles just using style attribute ?
+ - How would the user add on hover styles just using style attribute ?
- If just iframe refreshes due to some reason, embed script can't replay the applied instructions.
diff --git a/packages/embeds/embed-core/index.html b/packages/embeds/embed-core/index.html
index cf7842da..016f88a6 100644
--- a/packages/embeds/embed-core/index.html
+++ b/packages/embeds/embed-core/index.html
@@ -1,6 +1,7 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/embeds/embed-core/src/FloatingButton/FloatingButton.ts b/packages/embeds/embed-core/src/FloatingButton/FloatingButton.ts
index 01d3e25b..fbba8459 100644
--- a/packages/embeds/embed-core/src/FloatingButton/FloatingButton.ts
+++ b/packages/embeds/embed-core/src/FloatingButton/FloatingButton.ts
@@ -1,11 +1,73 @@
import { CalWindow } from "@calcom/embed-snippet";
-import floatingButtonHtml from "./FloatingButtonHtml";
+import getFloatingButtonHtml from "./FloatingButtonHtml";
export class FloatingButton extends HTMLElement {
+ static updatedClassString(position: string, classString: string) {
+ return [
+ classString.replace(/hidden|md:right-10|md:left-10|left-4|right-4/g, ""),
+ position === "bottom-right" ? "md:right-10 right-4" : "md:left-10 left-4",
+ ].join(" ");
+ }
+
+ //@ts-ignore
+ static get observedAttributes() {
+ return [
+ "data-button-text",
+ "data-hide-button-icon",
+ "data-button-position",
+ "data-button-color",
+ "data-button-text-color",
+ ];
+ }
+
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
+ if (name === "data-button-text") {
+ const buttonEl = this.shadowRoot?.querySelector("#button");
+ if (!buttonEl) {
+ throw new Error("Button not found");
+ }
+ buttonEl.innerHTML = newValue;
+ } else if (name === "data-hide-button-icon") {
+ const buttonIconEl = this.shadowRoot?.querySelector("#button-icon") as HTMLElement;
+ if (!buttonIconEl) {
+ throw new Error("Button not found");
+ }
+ buttonIconEl.style.display = newValue == "true" ? "none" : "block";
+ } else if (name === "data-button-position") {
+ const buttonEl = this.shadowRoot?.querySelector("button") as HTMLElement;
+ if (!buttonEl) {
+ throw new Error("Button not found");
+ }
+ buttonEl.className = FloatingButton.updatedClassString(newValue, buttonEl.className);
+ } else if (name === "data-button-color") {
+ const buttonEl = this.shadowRoot?.querySelector("button") as HTMLElement;
+ if (!buttonEl) {
+ throw new Error("Button not found");
+ }
+ buttonEl.style.backgroundColor = newValue;
+ } else if (name === "data-button-text-color") {
+ const buttonEl = this.shadowRoot?.querySelector("button") as HTMLElement;
+ if (!buttonEl) {
+ throw new Error("Button not found");
+ }
+ buttonEl.style.color = newValue;
+ }
+ }
constructor() {
super();
- const buttonHtml = ` ${floatingButtonHtml}`;
+ const buttonText = this.dataset["buttonText"];
+ const buttonPosition = this.dataset["buttonPosition"];
+ const buttonColor = this.dataset["buttonColor"];
+ const buttonTextColor = this.dataset["buttonTextColor"];
+
+ //TODO: Logic is duplicated over HTML generation and attribute change, keep it at one place
+ const buttonHtml = ` ${getFloatingButtonHtml({
+ buttonText: buttonText!,
+ buttonClasses: [FloatingButton.updatedClassString(buttonPosition!, "")],
+ buttonColor: buttonColor!,
+ buttonTextColor: buttonTextColor!,
+ })}`;
this.attachShadow({ mode: "open" });
this.shadowRoot!.innerHTML = buttonHtml;
}
diff --git a/packages/embeds/embed-core/src/FloatingButton/FloatingButtonHtml.ts b/packages/embeds/embed-core/src/FloatingButton/FloatingButtonHtml.ts
index 792032d8..e015f9c6 100644
--- a/packages/embeds/embed-core/src/FloatingButton/FloatingButtonHtml.ts
+++ b/packages/embeds/embed-core/src/FloatingButton/FloatingButtonHtml.ts
@@ -1,8 +1,23 @@
-const html = `
-
+cus:ring-4 focus:ring-gray-600 focus:ring-opacity-50 active:scale-95"
+style="background-color:${buttonColor}; color:${buttonTextColor} z-index: 10001">
+
-
Book my Cal
+
${buttonText}
`;
+};
-export default html;
+export default getHtml;
diff --git a/packages/embeds/embed-core/src/Inline/inline.ts b/packages/embeds/embed-core/src/Inline/inline.ts
index e17e9291..f0e101e6 100644
--- a/packages/embeds/embed-core/src/Inline/inline.ts
+++ b/packages/embeds/embed-core/src/Inline/inline.ts
@@ -1,6 +1,7 @@
import { CalWindow } from "@calcom/embed-snippet";
import loaderCss from "../loader.css";
+import { getErrorString } from "../utils";
import inlineHtml from "./inlineHtml";
export class Inline extends HTMLElement {
@@ -9,8 +10,16 @@ export class Inline extends HTMLElement {
return ["loading"];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
- if (name === "loading" && newValue == "done") {
- (this.shadowRoot!.querySelector("#loader")! as HTMLElement).style.display = "none";
+ if (name === "loading") {
+ if (newValue == "done") {
+ (this.shadowRoot!.querySelector(".loader")! as HTMLElement).style.display = "none";
+ } else if (newValue === "failed") {
+ (this.shadowRoot!.querySelector(".loader")! as HTMLElement).style.display = "none";
+ (this.shadowRoot!.querySelector("#error")! as HTMLElement).style.display = "block";
+ (this.shadowRoot!.querySelector("slot")! as HTMLElement).style.visibility = "hidden";
+ const errorString = getErrorString(this.dataset.errorCode);
+ (this.shadowRoot!.querySelector("#error")! as HTMLElement).innerText = errorString;
+ }
}
}
constructor() {
diff --git a/packages/embeds/embed-core/src/Inline/inlineHtml.ts b/packages/embeds/embed-core/src/Inline/inlineHtml.ts
index 80a87b2a..44c4c7e3 100644
--- a/packages/embeds/embed-core/src/Inline/inlineHtml.ts
+++ b/packages/embeds/embed-core/src/Inline/inlineHtml.ts
@@ -1,7 +1,10 @@
-const html = `
+const html = `
+
+Something went wrong.
+
`;
export default html;
diff --git a/packages/embeds/embed-core/src/ModalBox/ModalBox.ts b/packages/embeds/embed-core/src/ModalBox/ModalBox.ts
index a6e6a2b2..9a7b4dfa 100644
--- a/packages/embeds/embed-core/src/ModalBox/ModalBox.ts
+++ b/packages/embeds/embed-core/src/ModalBox/ModalBox.ts
@@ -1,6 +1,7 @@
import { CalWindow } from "@calcom/embed-snippet";
import loaderCss from "../loader.css";
+import { getErrorString } from "../utils";
import modalBoxHtml from "./ModalBoxHtml";
export class ModalBox extends HTMLElement {
@@ -28,11 +29,16 @@ export class ModalBox extends HTMLElement {
}
if (newValue == "loaded") {
- (this.shadowRoot!.querySelector("#loader")! as HTMLElement).style.display = "none";
+ (this.shadowRoot!.querySelector(".loader")! as HTMLElement).style.display = "none";
} else if (newValue === "started") {
this.show(true);
} else if (newValue == "closed") {
this.show(false);
+ } else if (newValue === "failed") {
+ (this.shadowRoot!.querySelector(".loader")! as HTMLElement).style.display = "none";
+ (this.shadowRoot!.querySelector("#error")! as HTMLElement).style.display = "inline-block";
+ const errorString = getErrorString(this.dataset.errorCode);
+ (this.shadowRoot!.querySelector("#error")! as HTMLElement).innerText = errorString;
}
}
diff --git a/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts b/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts
index 6222e91a..9e2178cd 100644
--- a/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts
+++ b/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts
@@ -59,11 +59,12 @@ const html = `