Embed Snippet Generator (#2597)
* Add support to dynamically change the theme * Add Embed UI in app * Update UI as per Figma * Dynamicaly update Embed Code * Get differnet modes working in preview * Support Embed on EventType Edit, Team Link Fix and Mobile unsupported * Fix auto theme switch in Embed Snippet generator * Fix types * Self Review fixes * Remove Embed from App section * Move get query after the middleware to let middleware work on it * Add sandboxes in the document * Add error handling for embed loading * Fix types * Update snapshots and fix bug identified by tests * UI Fixes * Add Embed Tests * Respond in preview to width and height Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
21
apps/docs/components/Anchor.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
function getAnchor(text) {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9 ]/g, "")
|
||||||
|
.replace(/[ ]/g, "-")
|
||||||
|
.replace(/ /g, "%20");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Anchor({ as, children }) {
|
||||||
|
const anchor = getAnchor(children);
|
||||||
|
const link = `#${anchor}`;
|
||||||
|
const Component = as || "div";
|
||||||
|
return (
|
||||||
|
<Component id={anchor}>
|
||||||
|
<a href={link} className="anchor-link">
|
||||||
|
§
|
||||||
|
</a>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,13 +2,16 @@
|
||||||
title: Embed
|
title: Embed
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import Anchor from "../../components/Anchor"
|
||||||
|
|
||||||
# Embed
|
# Embed
|
||||||
|
|
||||||
The Embed allows your website visitors to book a meeting with you directly from your website.
|
The Embed allows your website visitors to book a meeting with you directly from your website.
|
||||||
|
|
||||||
## Install on any website
|
## Install on any website
|
||||||
|
|
||||||
- _Step-1._ Install the Vanilla JS Snippet
|
Install the following Vanilla JS Snippet to get embed to work on any website. After that you can <a href="#popular-ways-in-which-you-can-embed-on-your-website">choose any of the ways</a> to show your Cal Link embedded on your website.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script>
|
<script>
|
||||||
(function (C, A, L) {
|
(function (C, A, L) {
|
||||||
|
@ -57,7 +60,7 @@ yarn add @calcom/embed-react
|
||||||
|
|
||||||
You can use Vanilla JS Snippet to install
|
You can use Vanilla JS Snippet to install
|
||||||
|
|
||||||
## Popular ways in which you can embed on your website
|
<Anchor as="H2">Popular ways in which you can embed on your website</Anchor>
|
||||||
|
|
||||||
Assuming that you have followed the steps of installing and initializing the snippet, you can show the embed in following ways:
|
Assuming that you have followed the steps of installing and initializing the snippet, you can show the embed in following ways:
|
||||||
|
|
||||||
|
@ -82,8 +85,15 @@ Show the embed inline inside a container element. It would take the width and he
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
```
|
|
||||||
|
|
||||||
|
*Sample sandbox*
|
||||||
|
```
|
||||||
|
<iframe src="https://codesandbox.io/embed/vanilla-js-inline-embed-r27n67?fontsize=14&hidenavigation=1&theme=dark"
|
||||||
|
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
|
||||||
|
title="Cal Component - Embed Inline Demo[React][TypeScript]"
|
||||||
|
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||||
|
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||||
|
></iframe>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
####
|
####
|
||||||
|
@ -108,6 +118,14 @@ const MyComponent = () => (
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
*Sample sandbox*
|
||||||
|
|
||||||
|
<iframe src="https://codesandbox.io/embed/cal-component-embed-inline-demo-react-typescript-d1zlcn?fontsize=14&hidenavigation=1&theme=dark"
|
||||||
|
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
|
||||||
|
title="Cal Component - Embed Inline Demo[React][TypeScript]"
|
||||||
|
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||||
|
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||||
|
></iframe>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Popup on any existing element
|
### Popup on any existing element
|
||||||
|
@ -120,9 +138,16 @@ To show the embed as a popup on clicking an element, add `data-cal-link` attribu
|
||||||
|
|
||||||
To show the embed as a popup on clicking an element, simply add `data-cal-link` attribute to the element.
|
To show the embed as a popup on clicking an element, simply add `data-cal-link` attribute to the element.
|
||||||
|
|
||||||
|
*Sample sandbox*
|
||||||
|
<iframe src="https://codesandbox.io/embed/popup-on-click-of-an-existing-element-y9lcuo?fontsize=14&hidenavigation=1&theme=dark"
|
||||||
|
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
|
||||||
|
title="Cal Component - Embed Inline Demo[React][TypeScript]"
|
||||||
|
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||||
|
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
<button data-cal-link="jane" data-cal-config="A valid config JSON"></button>
|
<button data-cal-link="jane" data-cal-config="A valid config JSON"></button>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>React</summary>
|
<summary>React</summary>
|
||||||
```jsx
|
```jsx
|
||||||
|
@ -131,11 +156,37 @@ To show the embed as a popup on clicking an element, simply add `data-cal-link`
|
||||||
const MyComponent = ()=> {
|
const MyComponent = ()=> {
|
||||||
return <button data-cal-link="jane" data-cal-config='A valid config JSON'></button>
|
return <button data-cal-link="jane" data-cal-config='A valid config JSON'></button>
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
````
|
*Sample sandbox*
|
||||||
|
<iframe src="https://codesandbox.io/embed/embed-popup-on-click-of-an-existing-element-demo-react-sc967e?fontsize=14&hidenavigation=1&theme=dark"
|
||||||
|
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
|
||||||
|
title="Cal Component - Embed Inline Demo[React][TypeScript]"
|
||||||
|
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||||
|
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Floating pop-up button
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
Cal("floatingButton", {
|
||||||
|
// The link that you want to embed. It would open https://cal.com/jane in embed
|
||||||
|
calLink: "jane",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
*Sample sandbox*
|
||||||
|
<iframe src="https://codesandbox.io/embed/embed-floating-button-popup-all-websites-cg7pru?fontsize=14&hidenavigation=1&theme=dark"
|
||||||
|
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
|
||||||
|
title="Cal Component - Embed Inline Demo[React][TypeScript]"
|
||||||
|
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||||
|
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
## Supported Instructions
|
## Supported Instructions
|
||||||
|
|
||||||
Consider an instruction as a function with that name and that would be called with the given arguments.
|
Consider an instruction as a function with that name and that would be called with the given arguments.
|
||||||
|
|
905
apps/web/components/Embed.tsx
Normal file
|
@ -0,0 +1,905 @@
|
||||||
|
import { CodeIcon, EyeIcon, SunIcon, ChevronRightIcon, ArrowLeftIcon } from "@heroicons/react/solid";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { components, ControlProps, SingleValue } from "react-select";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
import { Button, Switch } from "@calcom/ui";
|
||||||
|
import { Dialog, DialogContent, DialogClose } from "@calcom/ui/Dialog";
|
||||||
|
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import NavTabs from "@components/NavTabs";
|
||||||
|
import ColorPicker from "@components/ui/colorpicker";
|
||||||
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
|
type EmbedType = "inline" | "floating-popup" | "element-click";
|
||||||
|
const queryParamsForDialog = ["embedType", "tabName", "eventTypeId"];
|
||||||
|
|
||||||
|
const embeds: {
|
||||||
|
illustration: React.ReactElement;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
type: EmbedType;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
title: "Inline Embed",
|
||||||
|
subtitle: "Loads your Cal scheduling page directly inline with your other website content",
|
||||||
|
type: "inline",
|
||||||
|
illustration: (
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
className="rounded-md"
|
||||||
|
viewBox="0 0 308 265"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
||||||
|
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="48" y="86.5" width="48" height="4" rx="1" fill="#E1E1E1" />
|
||||||
|
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="85" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="97" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="109" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="85" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="97" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="109" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="49" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<path
|
||||||
|
d="M61 124.5H67V122.5H61V124.5ZM68 125.5V131.5H70V125.5H68ZM67 132.5H61V134.5H67V132.5ZM60 131.5V125.5H58V131.5H60ZM61 132.5C60.4477 132.5 60 132.052 60 131.5H58C58 133.157 59.3431 134.5 61 134.5V132.5ZM68 131.5C68 132.052 67.5523 132.5 67 132.5V134.5C68.6569 134.5 70 133.157 70 131.5H68ZM67 124.5C67.5523 124.5 68 124.948 68 125.5H70C70 123.843 68.6569 122.5 67 122.5V124.5ZM61 122.5C59.3431 122.5 58 123.843 58 125.5H60C60 124.948 60.4477 124.5 61 124.5V122.5Z"
|
||||||
|
fill="#3E3E3E"
|
||||||
|
/>
|
||||||
|
<rect x="73" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="85" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="97" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="109" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="49" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="61" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="73" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="85" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="97" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="121" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="49" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="61" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="73" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="85" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="97" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="121" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="49" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="61" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="73" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="24.5" y="51" width="139" height="163" rx="1.5" stroke="#292929" />
|
||||||
|
<rect x="176" y="50.5" width="108" height="164" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||||
|
{/* <path
|
||||||
|
d="M2 1H306V-1H2V1ZM307 2V263H309V2H307ZM306 264H2V266H306V264ZM1 263V1.99999H-1V263H1ZM2 264C1.44772 264 1 263.552 1 263H-1C-1 264.657 0.343147 266 2 266V264ZM307 263C307 263.552 306.552 264 306 264V266C307.657 266 309 264.657 309 263H307ZM306 1C306.552 1 307 1.44772 307 2H309C309 0.343145 307.657 -1 306 -1V1ZM2 -1C0.343151 -1 -1 0.343133 -1 1.99999H1C1 1.44771 1.44771 1 2 1V-1Z"
|
||||||
|
fill="#CFCFCF"
|
||||||
|
/> */}
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Floating pop-up button",
|
||||||
|
subtitle: "Adds a floating button on your site that launches Cal in a dialog.",
|
||||||
|
type: "floating-popup",
|
||||||
|
illustration: (
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
className="rounded-md"
|
||||||
|
viewBox="0 0 308 265"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="226" y="223.5" width="66" height="26" rx="2" fill="#292929" />
|
||||||
|
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
||||||
|
{/* <path
|
||||||
|
d="M2 1H306V-1H2V1ZM307 2V263H309V2H307ZM306 264H2V266H306V264ZM1 263V1.99999H-1V263H1ZM2 264C1.44772 264 1 263.552 1 263H-1C-1 264.657 0.343147 266 2 266V264ZM307 263C307 263.552 306.552 264 306 264V266C307.657 266 309 264.657 309 263H307ZM306 1C306.552 1 307 1.44772 307 2H309C309 0.343145 307.657 -1 306 -1V1ZM2 -1C0.343151 -1 -1 0.343133 -1 1.99999H1C1 1.44771 1.44771 1 2 1V-1Z"
|
||||||
|
fill="#CFCFCF"
|
||||||
|
/> */}
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pop up via element click",
|
||||||
|
subtitle: "Open your Cal dialog when someone clicks an element.",
|
||||||
|
type: "element-click",
|
||||||
|
illustration: (
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
className="rounded-md"
|
||||||
|
viewBox="0 0 308 265"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" fill="#F8F8F8" />
|
||||||
|
<rect opacity="0.8" x="108" y="85" width="80" height="8" rx="2" fill="#E1E1E1" />
|
||||||
|
<rect x="108" y="97" width="48" height="4" rx="1" fill="#E1E1E1" />
|
||||||
|
<rect x="109" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="110" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="133" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="145" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="157" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="169" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="181" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="193" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="145" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="157" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="169" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="181" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="193" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="109" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="136" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<path
|
||||||
|
d="M121 135H127V133H121V135ZM128 136V142H130V136H128ZM127 143H121V145H127V143ZM120 142V136H118V142H120ZM121 143C120.448 143 120 142.552 120 142H118C118 143.657 119.343 145 121 145V143ZM128 142C128 142.552 127.552 143 127 143V145C128.657 145 130 143.657 130 142H128ZM127 135C127.552 135 128 135.448 128 136H130C130 134.343 128.657 133 127 133V135ZM121 133C119.343 133 118 134.343 118 136H120C120 135.448 120.448 135 121 135V133Z"
|
||||||
|
fill="#3E3E3E"
|
||||||
|
/>
|
||||||
|
<rect x="133" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="145" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="157" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="169" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="181" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="193" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="109" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="145" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="157" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="169" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="181" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="193" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="109" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="145" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="157" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="169" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="181" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="193" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="109" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="121" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="133" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="145" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="157" y="172" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
|
<rect x="169" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
|
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" stroke="#292929" />
|
||||||
|
{/* <path
|
||||||
|
d="M2 1H306V-1H2V1ZM307 2V263H309V2H307ZM306 264H2V266H306V264ZM1 263V1.99999H-1V263H1ZM2 264C1.44772 264 1 263.552 1 263H-1C-1 264.657 0.343147 266 2 266V264ZM307 263C307 263.552 306.552 264 306 264V266C307.657 266 309 264.657 309 263H307ZM306 1C306.552 1 307 1.44772 307 2H309C309 0.343145 307.657 -1 306 -1V1ZM2 -1C0.343151 -1 -1 0.343133 -1 1.99999H1C1 1.44771 1.44771 1 2 1V-1Z"
|
||||||
|
fill="#CFCFCF"
|
||||||
|
/> */}
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getEmbedSnippetString() {
|
||||||
|
let embedJsUrl = "https://cal.com/embed.js";
|
||||||
|
let isLocal = false;
|
||||||
|
if (location.hostname === "localhost") {
|
||||||
|
embedJsUrl = "http://localhost:3100/dist/embed.umd.js";
|
||||||
|
isLocal = true;
|
||||||
|
}
|
||||||
|
// TODO: Import this string from @calcom/embed-snippet
|
||||||
|
return `
|
||||||
|
(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, "${embedJsUrl}", "init");
|
||||||
|
Cal("init"${isLocal ? ', {origin:"http://localhost:3000/"}' : ""});
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmbedNavBar = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: t("Embed"),
|
||||||
|
tabName: "embed-code",
|
||||||
|
icon: CodeIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t("Preview"),
|
||||||
|
tabName: "embed-preview",
|
||||||
|
icon: EyeIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <NavTabs data-testid="embed-tabs" tabs={tabs} linkProps={{ shallow: true }} />;
|
||||||
|
};
|
||||||
|
const ThemeSelectControl = ({ children, ...props }: ControlProps<any, false>) => {
|
||||||
|
return (
|
||||||
|
<components.Control {...props}>
|
||||||
|
<SunIcon className="h-[32px] w-[32px] text-gray-500" />
|
||||||
|
{children}
|
||||||
|
</components.Control>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChooseEmbedTypesDialogContent = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<DialogContent size="lg">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
|
{t("how_you_want_add_cal_site")}
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{embeds.map((embed, index) => (
|
||||||
|
<button
|
||||||
|
className="mr-2 w-1/3 p-3 text-left hover:rounded-md hover:border hover:bg-neutral-100"
|
||||||
|
key={index}
|
||||||
|
data-testid={embed.type}
|
||||||
|
onClick={() => {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
embedType: embed.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
||||||
|
{embed.illustration}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 font-medium text-neutral-900">{embed.title}</div>
|
||||||
|
<p className="text-sm text-gray-500">{embed.subtitle}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||||
|
eventTypeId,
|
||||||
|
embedType,
|
||||||
|
}: {
|
||||||
|
eventTypeId: EventType["id"];
|
||||||
|
embedType: EmbedType;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const embedCode = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const embed = embeds.find((embed) => embed.type === embedType);
|
||||||
|
|
||||||
|
const { data: eventType, isLoading } = trpc.useQuery([
|
||||||
|
"viewer.eventTypes.get",
|
||||||
|
{
|
||||||
|
id: +eventTypeId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||||
|
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||||
|
const [previewState, setPreviewState] = useState({
|
||||||
|
inline: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
theme: "auto",
|
||||||
|
floatingPopup: {},
|
||||||
|
elementClick: {},
|
||||||
|
palette: {
|
||||||
|
brandColor: "#000000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
const noPopupQuery = {
|
||||||
|
...router.query,
|
||||||
|
};
|
||||||
|
|
||||||
|
delete noPopupQuery.dialog;
|
||||||
|
|
||||||
|
queryParamsForDialog.forEach((queryParam) => {
|
||||||
|
delete noPopupQuery[queryParam];
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
query: noPopupQuery,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use embed-code as default tab
|
||||||
|
if (!router.query.tabName) {
|
||||||
|
router.query.tabName = "embed-code";
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!embed || !eventType) {
|
||||||
|
close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calLink = `${eventType.team ? `team/${eventType.team.slug}` : eventType.users[0].username}/${
|
||||||
|
eventType.slug
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// TODO: Not sure how to make these template strings look better formatted.
|
||||||
|
// This exact formatting is required to make the code look nicely formatted together.
|
||||||
|
const getEmbedUIInstructionString = () =>
|
||||||
|
`Cal("ui", {
|
||||||
|
${getThemeForSnippet() ? 'theme: "' + previewState.theme + '",\n ' : ""}styles: {
|
||||||
|
branding: ${JSON.stringify(previewState.palette)}
|
||||||
|
}
|
||||||
|
})`;
|
||||||
|
|
||||||
|
const getEmbedTypeSpecificString = () => {
|
||||||
|
if (embedType === "inline") {
|
||||||
|
return `
|
||||||
|
Cal("inline", {
|
||||||
|
elementOrSelector:"#my-cal-inline",
|
||||||
|
calLink: "${calLink}"
|
||||||
|
});
|
||||||
|
${getEmbedUIInstructionString().trim()}`;
|
||||||
|
} else if (embedType === "floating-popup") {
|
||||||
|
let floatingButtonArg = {
|
||||||
|
calLink,
|
||||||
|
...previewState.floatingPopup,
|
||||||
|
};
|
||||||
|
return `
|
||||||
|
Cal("floatingButton", ${JSON.stringify(floatingButtonArg)});
|
||||||
|
${getEmbedUIInstructionString().trim()}`;
|
||||||
|
} else if (embedType === "element-click") {
|
||||||
|
return `//Important: Also, add data-cal-link="${calLink}" attribute to the element you want to open Cal on click
|
||||||
|
${getEmbedUIInstructionString().trim()}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getThemeForSnippet = () => {
|
||||||
|
return previewState.theme !== "auto" ? previewState.theme : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDimension = (dimension: string) => {
|
||||||
|
if (dimension.match(/^\d+$/)) {
|
||||||
|
dimension = `${dimension}%`;
|
||||||
|
}
|
||||||
|
return dimension;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToPalette = (update: typeof previewState["palette"]) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
palette: {
|
||||||
|
...previewState.palette,
|
||||||
|
...update,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewInstruction = (instruction: { name: string; arg: any }) => {
|
||||||
|
iframeRef.current?.contentWindow?.postMessage(
|
||||||
|
{
|
||||||
|
mode: "cal:preview",
|
||||||
|
type: "instruction",
|
||||||
|
instruction,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inlineEmbedDimensionUpdate = ({ width, height }: { width: string; height: string }) => {
|
||||||
|
iframeRef.current?.contentWindow?.postMessage(
|
||||||
|
{
|
||||||
|
mode: "cal:preview",
|
||||||
|
type: "inlineEmbedDimensionUpdate",
|
||||||
|
data: {
|
||||||
|
width: getDimension(width),
|
||||||
|
height: getDimension(height),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
previewInstruction({
|
||||||
|
name: "ui",
|
||||||
|
arg: {
|
||||||
|
theme: previewState.theme,
|
||||||
|
styles: {
|
||||||
|
branding: {
|
||||||
|
...previewState.palette,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (embedType === "floating-popup") {
|
||||||
|
previewInstruction({
|
||||||
|
name: "floatingButton",
|
||||||
|
arg: {
|
||||||
|
attributes: {
|
||||||
|
id: "my-floating-button",
|
||||||
|
},
|
||||||
|
...previewState.floatingPopup,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embedType === "inline") {
|
||||||
|
inlineEmbedDimensionUpdate({
|
||||||
|
width: previewState.inline.width,
|
||||||
|
height: previewState.inline.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeOptions = [
|
||||||
|
{ value: "auto", label: "Auto Theme" },
|
||||||
|
{ value: "dark", label: "Dark Theme" },
|
||||||
|
{ value: "light", label: "Light Theme" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FloatingPopupPositionOptions = [
|
||||||
|
{
|
||||||
|
value: "bottom-right",
|
||||||
|
label: "Bottom Right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "bottom-left",
|
||||||
|
label: "Bottom Left",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent size="xl">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex w-1/3 flex-col bg-white p-6">
|
||||||
|
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newQuery = { ...router.query };
|
||||||
|
delete newQuery.embedType;
|
||||||
|
delete newQuery.tabName;
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...newQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<ArrowLeftIcon className="mr-4 w-4"></ArrowLeftIcon>
|
||||||
|
</button>
|
||||||
|
{embed.title}
|
||||||
|
</h3>
|
||||||
|
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")}></hr>
|
||||||
|
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
|
<Collapsible
|
||||||
|
open={isEmbedCustomizationOpen}
|
||||||
|
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||||
|
<CollapsibleTrigger
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center text-base font-medium text-neutral-900">
|
||||||
|
<div>
|
||||||
|
{embedType === "inline"
|
||||||
|
? "Inline Embed Customization"
|
||||||
|
: embedType === "floating-popup"
|
||||||
|
? "Floating Popup Customization"
|
||||||
|
: "Element Click Customization"}
|
||||||
|
</div>
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={`${
|
||||||
|
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
|
} ml-auto h-5 w-5 text-neutral-500`}
|
||||||
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="text-sm">
|
||||||
|
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
||||||
|
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||||
|
<div className="text-sm">Embed Window Sizing</div>
|
||||||
|
<div className="justify-left flex items-center">
|
||||||
|
<TextField
|
||||||
|
name="width"
|
||||||
|
labelProps={{ className: "hidden" }}
|
||||||
|
required
|
||||||
|
value={previewState.inline.width}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
let width = e.target.value || "100%";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
inline: {
|
||||||
|
...previewState.inline,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
addOnLeading={<InputLeading>W</InputLeading>}
|
||||||
|
/>
|
||||||
|
<span className="p-2">x</span>
|
||||||
|
<TextField
|
||||||
|
labelProps={{ className: "hidden" }}
|
||||||
|
name="height"
|
||||||
|
value={previewState.inline.height}
|
||||||
|
required
|
||||||
|
onChange={(e) => {
|
||||||
|
const height = e.target.value || "100%";
|
||||||
|
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
inline: {
|
||||||
|
...previewState.inline,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
addOnLeading={<InputLeading>H</InputLeading>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4 items-center justify-between",
|
||||||
|
embedType === "floating-popup" ? "flex" : "hidden"
|
||||||
|
)}>
|
||||||
|
<div className="text-sm">Button Text</div>
|
||||||
|
{/* Default Values should come from preview iframe */}
|
||||||
|
<TextField
|
||||||
|
name="buttonText"
|
||||||
|
labelProps={{ className: "hidden" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
floatingPopup: {
|
||||||
|
...previewState.floatingPopup,
|
||||||
|
buttonText: e.target.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
defaultValue="Book my Cal"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4 flex items-center justify-between",
|
||||||
|
embedType === "floating-popup" ? "flex" : "hidden"
|
||||||
|
)}>
|
||||||
|
<div className="text-sm">Display Calendar Icon Button</div>
|
||||||
|
<Switch
|
||||||
|
defaultChecked={true}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
floatingPopup: {
|
||||||
|
...previewState.floatingPopup,
|
||||||
|
hideButtonIcon: !checked,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}></Switch>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4 flex items-center justify-between",
|
||||||
|
embedType === "floating-popup" ? "flex" : "hidden"
|
||||||
|
)}>
|
||||||
|
<div>Position of Button</div>
|
||||||
|
<Select
|
||||||
|
onChange={(position) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
floatingPopup: {
|
||||||
|
...previewState.floatingPopup,
|
||||||
|
buttonPosition: position?.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
defaultValue={FloatingPopupPositionOptions[0]}
|
||||||
|
options={FloatingPopupPositionOptions}></Select>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4 flex items-center justify-between",
|
||||||
|
embedType === "floating-popup" ? "flex" : "hidden"
|
||||||
|
)}>
|
||||||
|
<div>Button Color</div>
|
||||||
|
<div className="w-36">
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue="#000000"
|
||||||
|
onChange={(color) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
floatingPopup: {
|
||||||
|
...previewState.floatingPopup,
|
||||||
|
buttonColor: color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}></ColorPicker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4 flex items-center justify-between",
|
||||||
|
embedType === "floating-popup" ? "flex" : "hidden"
|
||||||
|
)}>
|
||||||
|
<div>Text Color</div>
|
||||||
|
<div className="w-36">
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue="#000000"
|
||||||
|
onChange={(color) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
floatingPopup: {
|
||||||
|
...previewState.floatingPopup,
|
||||||
|
buttonTextColor: color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}></ColorPicker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4 items-center justify-between",
|
||||||
|
embedType === "floating-popup" ? "flex" : "hidden"
|
||||||
|
)}>
|
||||||
|
<div>Button Color on Hover</div>
|
||||||
|
<div className="w-36">
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue="#000000"
|
||||||
|
onChange={(color) => {
|
||||||
|
addToPalette({
|
||||||
|
"floating-popup-button-color-hover": color,
|
||||||
|
});
|
||||||
|
}}></ColorPicker>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
<hr className="mt-4"></hr>
|
||||||
|
<div className="mt-4 font-medium">
|
||||||
|
<Collapsible
|
||||||
|
open={isBookingCustomizationOpen}
|
||||||
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
|
<CollapsibleTrigger className="flex w-full" type="button">
|
||||||
|
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={`${
|
||||||
|
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
|
} ml-auto h-5 w-5 text-neutral-500`}
|
||||||
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="mt-6 text-sm">
|
||||||
|
<Label className="flex items-center justify-between">
|
||||||
|
<div>Theme</div>
|
||||||
|
<Select
|
||||||
|
className="w-36"
|
||||||
|
defaultValue={ThemeOptions[0]}
|
||||||
|
components={{
|
||||||
|
Control: ThemeSelectControl,
|
||||||
|
}}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
theme: option.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
options={ThemeOptions}></Select>
|
||||||
|
</Label>
|
||||||
|
{[
|
||||||
|
{ name: "brandColor", title: "Brand Color" },
|
||||||
|
// { name: "lightColor", title: "Light Color" },
|
||||||
|
// { name: "lighterColor", title: "Lighter Color" },
|
||||||
|
// { name: "lightestColor", title: "Lightest Color" },
|
||||||
|
// { name: "highlightColor", title: "Highlight Color" },
|
||||||
|
// { name: "medianColor", title: "Median Color" },
|
||||||
|
].map((palette) => (
|
||||||
|
<Label key={palette.name} className="flex items-center justify-between">
|
||||||
|
<div>{palette.title}</div>
|
||||||
|
<div className="w-36">
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue="#000000"
|
||||||
|
onChange={(color) => {
|
||||||
|
//@ts-ignore - How to support dynamic palette names?
|
||||||
|
addToPalette({
|
||||||
|
[palette.name]: color,
|
||||||
|
});
|
||||||
|
}}></ColorPicker>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-2/3 bg-gray-50 p-6">
|
||||||
|
<EmbedNavBar />
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={classNames(router.query.tabName === "embed-code" ? "block" : "hidden", "h-[75vh]")}>
|
||||||
|
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small>
|
||||||
|
<TextArea
|
||||||
|
data-testid="embed-code"
|
||||||
|
ref={embedCode}
|
||||||
|
name="embed-code"
|
||||||
|
className="h-[36rem]"
|
||||||
|
readOnly
|
||||||
|
value={
|
||||||
|
`<!-- Cal ${embedType} embed code begins -->\n` +
|
||||||
|
(embedType === "inline"
|
||||||
|
? `<div style="width:${getDimension(previewState.inline.width)};height:${getDimension(
|
||||||
|
previewState.inline.height
|
||||||
|
)};overflow:scroll" id="my-cal-inline"></div>\n`
|
||||||
|
: "") +
|
||||||
|
`<script type="text/javascript">
|
||||||
|
${getEmbedSnippetString().trim()}
|
||||||
|
${getEmbedTypeSpecificString().trim()}
|
||||||
|
</script>
|
||||||
|
<!-- Cal ${embedType} embed code ends -->`
|
||||||
|
}></TextArea>
|
||||||
|
<p className="hidden text-sm text-gray-500">
|
||||||
|
{t(
|
||||||
|
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"}>
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
data-testid="embed-preview"
|
||||||
|
className="border-1 h-[75vh] border"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
src={`http://localhost:3100/preview.html?embedType=${embedType}&calLink=${calLink}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
if (!embedCode.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(embedCode.current.value);
|
||||||
|
showToast(t("code_copied"), "success");
|
||||||
|
}}>
|
||||||
|
{t("copy_code")}
|
||||||
|
</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button color="secondary">{t("Close")}</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbedDialog = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const eventTypeId: EventType["id"] = +(router.query.eventTypeId as string);
|
||||||
|
return (
|
||||||
|
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
|
||||||
|
{!router.query.embedType ? (
|
||||||
|
<ChooseEmbedTypesDialogContent />
|
||||||
|
) : (
|
||||||
|
<EmbedTypeCodeAndPreviewDialogContent
|
||||||
|
eventTypeId={eventTypeId}
|
||||||
|
embedType={router.query.embedType as EmbedType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbedButton = ({
|
||||||
|
eventTypeId,
|
||||||
|
className = "",
|
||||||
|
dark,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
eventTypeId: EventType["id"];
|
||||||
|
className: string;
|
||||||
|
dark?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
className = classNames(className, "hidden lg:flex");
|
||||||
|
const openEmbedModal = () => {
|
||||||
|
const query = {
|
||||||
|
...router.query,
|
||||||
|
dialog: "embed",
|
||||||
|
eventTypeId,
|
||||||
|
};
|
||||||
|
router.push(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="minimal"
|
||||||
|
size="sm"
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
data-test-eventtype-id={eventTypeId}
|
||||||
|
data-testid={"event-type-embed"}
|
||||||
|
onClick={() => openEmbedModal()}>
|
||||||
|
<CodeIcon
|
||||||
|
className={classNames("h-4 w-4 ltr:mr-2 rtl:ml-2", dark ? "" : "text-neutral-500")}></CodeIcon>
|
||||||
|
{t("Embed")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,32 +1,62 @@
|
||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import Link, { LinkProps } from "next/link";
|
import Link, { LinkProps } from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ElementType, FC, Fragment } from "react";
|
import React, { ElementType, FC, Fragment, MouseEventHandler } from "react";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
export interface NavTabProps {
|
export interface NavTabProps {
|
||||||
tabs: {
|
tabs: {
|
||||||
name: string;
|
name: string;
|
||||||
href: string;
|
/** If you want to change the path as per current tab */
|
||||||
|
href?: string;
|
||||||
|
/** If you want to change query param tabName as per current tab */
|
||||||
|
tabName?: string;
|
||||||
icon?: ElementType;
|
icon?: ElementType;
|
||||||
adminRequired?: boolean;
|
adminRequired?: boolean;
|
||||||
}[];
|
}[];
|
||||||
linkProps?: Omit<LinkProps, "href">;
|
linkProps?: Omit<LinkProps, "href">;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps }) => {
|
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="-mb-px flex space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse" aria-label="Tabs">
|
<nav
|
||||||
|
className="-mb-px flex space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse"
|
||||||
|
aria-label="Tabs"
|
||||||
|
{...props}>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isCurrent = router.asPath === tab.href;
|
let href: string;
|
||||||
|
let isCurrent;
|
||||||
|
if ((tab.tabName && tab.href) || (!tab.tabName && !tab.href)) {
|
||||||
|
throw new Error("Use either tabName or href");
|
||||||
|
}
|
||||||
|
if (tab.href) {
|
||||||
|
href = tab.href;
|
||||||
|
isCurrent = router.asPath === tab.href;
|
||||||
|
} else if (tab.tabName) {
|
||||||
|
href = "";
|
||||||
|
isCurrent = router.query.tabName === tab.tabName;
|
||||||
|
}
|
||||||
|
const onClick: MouseEventHandler = tab.tabName
|
||||||
|
? (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
tabName: tab.tabName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: () => {};
|
||||||
|
|
||||||
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
||||||
return (
|
return (
|
||||||
<Component key={tab.name}>
|
<Component key={tab.name}>
|
||||||
<Link href={tab.href} {...linkProps}>
|
<Link key={tab.name} href={href!} {...linkProps}>
|
||||||
<a
|
<a
|
||||||
|
onClick={onClick}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent
|
isCurrent
|
||||||
? "border-neutral-900 text-neutral-900"
|
? "border-neutral-900 text-neutral-900"
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default function useTheme(theme?: Maybe<string>) {
|
||||||
// TODO: isReady doesn't seem required now. This is also impacting PSI Score for pages which are using isReady.
|
// TODO: isReady doesn't seem required now. This is also impacting PSI Score for pages which are using isReady.
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
setTheme(theme);
|
setTheme(theme);
|
||||||
}, []);
|
}, [theme]);
|
||||||
|
|
||||||
function Theme() {
|
function Theme() {
|
||||||
const code = applyThemeAndAddListener.toString();
|
const code = applyThemeAndAddListener.toString();
|
||||||
|
|
|
@ -9,6 +9,7 @@ const withTM = require("next-transpile-modules")([
|
||||||
"@calcom/stripe",
|
"@calcom/stripe",
|
||||||
"@calcom/ui",
|
"@calcom/ui",
|
||||||
"@calcom/embed-core",
|
"@calcom/embed-core",
|
||||||
|
"@calcom/embed-snippet",
|
||||||
]);
|
]);
|
||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
|
|
||||||
|
|
|
@ -26,87 +26,6 @@ import IntegrationListItem from "@components/integrations/IntegrationListItem";
|
||||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||||
import WebhookListContainer from "@components/webhook/WebhookListContainer";
|
import WebhookListContainer from "@components/webhook/WebhookListContainer";
|
||||||
|
|
||||||
function IframeEmbedContainer() {
|
|
||||||
const { t } = useLocale();
|
|
||||||
// doesn't need suspense as it should already be loaded
|
|
||||||
const user = trpc.useQuery(["viewer.me"]).data;
|
|
||||||
|
|
||||||
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user?.username}" frameborder="0" allowfullscreen></iframe>`;
|
|
||||||
const htmlTemplate = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${t(
|
|
||||||
"schedule_a_meeting"
|
|
||||||
)}</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body>${iframeTemplate}</body></html>`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShellSubHeading title={t("iframe_embed")} subtitle={t("embed_calcom")} className="mt-10" />
|
|
||||||
<div className="lg:col-span-9 lg:pb-8">
|
|
||||||
<List>
|
|
||||||
<ListItem className={classNames("flex-col")}>
|
|
||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
|
|
||||||
<Image width={40} height={40} src="/apps/embed.svg" alt="Embed" />
|
|
||||||
<div className="flex-grow truncate pl-2">
|
|
||||||
<ListItemTitle component="h3">{t("standard_iframe")}</ListItemTitle>
|
|
||||||
<ListItemText component="p">{t("embed_your_calendar")}</ListItemText>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<input
|
|
||||||
id="iframe"
|
|
||||||
className="px-2 py-1 text-sm text-gray-500 "
|
|
||||||
placeholder={t("loading")}
|
|
||||||
defaultValue={iframeTemplate}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(iframeTemplate);
|
|
||||||
showToast("Copied to clipboard", "success");
|
|
||||||
}}>
|
|
||||||
<ClipboardIcon className="-mb-0.5 h-4 w-4 text-gray-800 ltr:mr-2 rtl:ml-2" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem className={classNames("flex-col")}>
|
|
||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
|
|
||||||
<Image width={40} height={40} src="/apps/embed.svg" alt="Embed" />
|
|
||||||
<div className="flex-grow truncate pl-2">
|
|
||||||
<ListItemTitle component="h3">{t("responsive_fullscreen_iframe")}</ListItemTitle>
|
|
||||||
<ListItemText component="p">A fullscreen scheduling experience on your website</ListItemText>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
id="fullscreen"
|
|
||||||
className="px-2 py-1 text-sm text-gray-500 "
|
|
||||||
placeholder={t("loading")}
|
|
||||||
defaultValue={htmlTemplate}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(htmlTemplate);
|
|
||||||
showToast("Copied to clipboard", "success");
|
|
||||||
}}>
|
|
||||||
<ClipboardIcon className="-mb-0.5 h-4 w-4 text-gray-800 ltr:mr-2 rtl:ml-2" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
<div className="grid grid-cols-2 space-x-4 rtl:space-x-reverse">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="iframe" className="block text-sm font-medium text-gray-700"></label>
|
|
||||||
<div className="mt-1"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="fullscreen" className="block text-sm font-medium text-gray-700"></label>
|
|
||||||
<div className="mt-1"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConnectOrDisconnectIntegrationButton(props: {
|
function ConnectOrDisconnectIntegrationButton(props: {
|
||||||
//
|
//
|
||||||
credentialIds: number[];
|
credentialIds: number[];
|
||||||
|
@ -342,7 +261,6 @@ export default function IntegrationsPage() {
|
||||||
<IntegrationsContainer />
|
<IntegrationsContainer />
|
||||||
<CalendarListContainer />
|
<CalendarListContainer />
|
||||||
<WebhookListContainer title={t("webhooks")} subtitle={t("receive_cal_meeting_data")} />
|
<WebhookListContainer title={t("webhooks")} subtitle={t("receive_cal_meeting_data")} />
|
||||||
<IframeEmbedContainer />
|
|
||||||
<Web3Container />
|
<Web3Container />
|
||||||
</ClientSuspense>
|
</ClientSuspense>
|
||||||
</AppsShell>
|
</AppsShell>
|
||||||
|
|
|
@ -52,6 +52,7 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { ClientSuspense } from "@components/ClientSuspense";
|
import { ClientSuspense } from "@components/ClientSuspense";
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||||
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import { Tooltip } from "@components/Tooltip";
|
import { Tooltip } from "@components/Tooltip";
|
||||||
|
@ -1822,6 +1823,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<LinkIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<LinkIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||||
{t("copy_link")}
|
{t("copy_link")}
|
||||||
</button>
|
</button>
|
||||||
|
<EmbedButton
|
||||||
|
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900"
|
||||||
|
eventTypeId={eventType.id}
|
||||||
|
/>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-red-500 hover:bg-gray-200">
|
<DialogTrigger className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-red-500 hover:bg-gray-200">
|
||||||
<TrashIcon className="h-4 w-4 text-red-500 ltr:mr-2 rtl:ml-2" />
|
<TrashIcon className="h-4 w-4 text-red-500 ltr:mr-2 rtl:ml-2" />
|
||||||
|
@ -1969,6 +1974,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ClientSuspense>
|
</ClientSuspense>
|
||||||
|
<EmbedDialog />
|
||||||
</Shell>
|
</Shell>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,13 +10,14 @@ import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
|
CodeIcon,
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
import { UsersIcon } from "@heroicons/react/solid";
|
import { UsersIcon } from "@heroicons/react/solid";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { Fragment, useEffect, useState } from "react";
|
import React, { Fragment, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -36,6 +37,7 @@ import classNames from "@lib/classNames";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
import EmptyScreen from "@components/EmptyScreen";
|
import EmptyScreen from "@components/EmptyScreen";
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import { Tooltip } from "@components/Tooltip";
|
import { Tooltip } from "@components/Tooltip";
|
||||||
|
@ -299,6 +301,12 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
{t("duplicate")}
|
{t("duplicate")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<EmbedButton
|
||||||
|
dark
|
||||||
|
className="w-full rounded-none"
|
||||||
|
eventTypeId={type.id}></EmbedButton>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
|
@ -519,9 +527,9 @@ const CTA = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const WithQuery = withQuery(["viewer.eventTypes"]);
|
const WithQuery = withQuery(["viewer.eventTypes"]);
|
||||||
|
|
||||||
const EventTypesPage = () => {
|
const EventTypesPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -574,6 +582,7 @@ const EventTypesPage = () => {
|
||||||
{data.eventTypeGroups.length === 0 && (
|
{data.eventTypeGroups.length === 0 && (
|
||||||
<CreateFirstEventTypeView profiles={data.profiles} canAddEvents={data.viewer.canAddEvents} />
|
<CreateFirstEventTypeView profiles={data.profiles} canAddEvents={data.viewer.canAddEvents} />
|
||||||
)}
|
)}
|
||||||
|
<EmbedDialog></EmbedDialog>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
188
apps/web/playwright/embed-code-generator.test.ts
Normal file
|
@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -774,6 +774,11 @@
|
||||||
"impersonate_user_tip":"All uses of this feature is audited.",
|
"impersonate_user_tip":"All uses of this feature is audited.",
|
||||||
"impersonating_user_warning":"Impersonating username \"{{user}}\".",
|
"impersonating_user_warning":"Impersonating username \"{{user}}\".",
|
||||||
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
|
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
|
||||||
|
"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",
|
"setting_up_zapier": "Setting up your Zapier integration",
|
||||||
"generate_api_key": "Generate Api Key",
|
"generate_api_key": "Generate Api Key",
|
||||||
"your_unique_api_key": "Your unique API key",
|
"your_unique_api_key": "Your unique API key",
|
||||||
|
|
|
@ -211,6 +211,40 @@ export const eventTypesRouter = createProtectedRouter()
|
||||||
|
|
||||||
return next();
|
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", {
|
.mutation("update", {
|
||||||
input: EventTypeUpdateInput.strict(),
|
input: EventTypeUpdateInput.strict(),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
|
|
|
@ -56,6 +56,7 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
|
||||||
|
|
||||||
- Automation Tests
|
- Automation Tests
|
||||||
- Run automation tests in CI
|
- 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
|
- Bundling Related
|
||||||
- Comments in CSS aren't stripped off
|
- Comments in CSS aren't stripped off
|
||||||
|
@ -72,13 +73,8 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
|
||||||
- Dev Experience/Ease of Installation
|
- 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 ?
|
- 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.
|
- Option to disable redirect banner and let parent handle redirect.
|
||||||
|
|
||||||
- Release Issues
|
- 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
|
- 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
|
- 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,7 +83,6 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
|
||||||
- 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,
|
- 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
|
- 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.
|
- If just iframe refreshes due to some reason, embed script can't replay the applied instructions.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!-- <link rel="prerender" href="http://localhost:3000/free"> -->
|
<!-- <link rel="prerender" href="http://localhost:3000/free"> -->
|
||||||
|
<!-- <script src="./src/embed.ts" type="module"></script> -->
|
||||||
<script>
|
<script>
|
||||||
if (!location.search.includes("nonResponsive")) {
|
if (!location.search.includes("nonResponsive")) {
|
||||||
document.write('<meta name="viewport" content="width=device-width"/>');
|
document.write('<meta name="viewport" content="width=device-width"/>');
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
"description": "This is the vanilla JS core script that embeds Cal Link",
|
"description": "This is the vanilla JS core script that embeds Cal Link",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "NEXT_PUBLIC_EMBED_FINGER_PRINT=$(git rev-parse --short HEAD) vite build && cp dist/embed.umd.js ../../../apps/website/public/embed.js && echo 'You need to commit the newly generated embed.js in apps/website'",
|
||||||
"build:cal": "NEXT_PUBLIC_WEBSITE_URL='https://cal.com' yarn build",
|
"build:cal": "NEXT_PUBLIC_WEBSITE_URL='https://cal.com' yarn build",
|
||||||
"vite": "vite",
|
"vite": "vite",
|
||||||
"tailwind": "yarn tailwindcss -i ./src/styles.css -o ./src/tailwind.generated.css --watch",
|
"tailwind": "yarn tailwindcss -i ./src/styles.css -o ./src/tailwind.generated.css",
|
||||||
"buildWatchAndServer": "run-p 'build --watch' 'vite --port 3100 --strict-port --open'",
|
"buildWatchAndServer": "run-p 'build --watch' 'vite --port 3100 --strict-port --open'",
|
||||||
"dev": "run-p 'tailwind' 'buildWatchAndServer'",
|
"dev": "yarn tailwind && run-p 'tailwind --watch' 'buildWatchAndServer'",
|
||||||
|
"dev-real": "vite dev --port 3100",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
"lint": "eslint --ext .ts,.js src",
|
"lint": "eslint --ext .ts,.js src",
|
||||||
"embed-tests": "yarn playwright test --config=playwright/config/playwright.config.ts",
|
"embed-tests": "yarn playwright test --config=playwright/config/playwright.config.ts",
|
||||||
|
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 217 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 199 KiB |
Before Width: | Height: | Size: 987 KiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 200 KiB |
Before Width: | Height: | Size: 996 KiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 233 KiB |
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 217 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 200 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 265 KiB After Width: | Height: | Size: 257 KiB |
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 221 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 259 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 329 KiB After Width: | Height: | Size: 353 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
80
packages/embeds/embed-core/preview.html
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
(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, "//localhost:3100/dist/embed.umd.js", "init");
|
||||||
|
Cal("init", {
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
display:flex;
|
||||||
|
}
|
||||||
|
.cell-1 {
|
||||||
|
border-right:1px solid #ded9d9;
|
||||||
|
padding-right:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-2 {
|
||||||
|
margin:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
const searchParams= new URL(document.URL).searchParams;
|
||||||
|
const embedType = searchParams.get("embedType");
|
||||||
|
const calLink = searchParams.get("calLink");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<script type="module" src="./src/preview.ts"></script>
|
||||||
|
<body>
|
||||||
|
<div id="my-embed" style="width:100%;height:100%;overflow:scroll"></div>
|
||||||
|
<script>
|
||||||
|
if (embedType === "inline") {
|
||||||
|
Cal("inline", {
|
||||||
|
elementOrSelector: "#my-embed",
|
||||||
|
calLink,
|
||||||
|
});
|
||||||
|
} else if (embedType === "floating-popup") {
|
||||||
|
Cal("floatingButton", {
|
||||||
|
calLink,
|
||||||
|
attributes: {
|
||||||
|
id: "my-floating-button"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (embedType === "element-click") {
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.setAttribute("data-cal-link", calLink)
|
||||||
|
button.innerHTML = 'I am a button that exists on your website'
|
||||||
|
document.body.appendChild(button);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,11 +1,73 @@
|
||||||
import { CalWindow } from "@calcom/embed-snippet";
|
import { CalWindow } from "@calcom/embed-snippet";
|
||||||
|
|
||||||
import floatingButtonHtml from "./FloatingButtonHtml";
|
import getFloatingButtonHtml from "./FloatingButtonHtml";
|
||||||
|
|
||||||
export class FloatingButton extends HTMLElement {
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const buttonHtml = `<style>${(window as CalWindow).Cal!.__css}</style> ${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 = `<style>${(window as CalWindow).Cal!.__css}</style> ${getFloatingButtonHtml({
|
||||||
|
buttonText: buttonText!,
|
||||||
|
buttonClasses: [FloatingButton.updatedClassString(buttonPosition!, "")],
|
||||||
|
buttonColor: buttonColor!,
|
||||||
|
buttonTextColor: buttonTextColor!,
|
||||||
|
})}`;
|
||||||
this.attachShadow({ mode: "open" });
|
this.attachShadow({ mode: "open" });
|
||||||
this.shadowRoot!.innerHTML = buttonHtml;
|
this.shadowRoot!.innerHTML = buttonHtml;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,23 @@
|
||||||
const html = `<button class="fixed bottom-4 right-4 flex h-16 origin-center bg-red-50 transform cursor-pointer items-center
|
const getHtml = ({
|
||||||
|
buttonText,
|
||||||
|
buttonClasses,
|
||||||
|
buttonColor,
|
||||||
|
buttonTextColor,
|
||||||
|
}: {
|
||||||
|
buttonText: string;
|
||||||
|
buttonClasses: string[];
|
||||||
|
buttonColor: string;
|
||||||
|
buttonTextColor: string;
|
||||||
|
}) => {
|
||||||
|
// IT IS A REQUIREMENT THAT ALL POSSIBLE CLASSES ARE HERE OTHERWISE TAILWIND WONT GENERATE THE CSS FOR CONDITIONAL CLASSES
|
||||||
|
// To not let all these classes apply and visible, keep it hidden initially
|
||||||
|
return `<button class="hidden fixed md:bottom-6 bottom-4 md:right-10 right-4 md:left-10 left-4 ${buttonClasses.join(
|
||||||
|
" "
|
||||||
|
)} 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
|
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"
|
cus:ring-4 focus:ring-gray-600 focus:ring-opacity-50 active:scale-95"
|
||||||
style="background-color: rgb(255, 202, 0); color: rgb(20, 30, 47); z-index: 10001">
|
style="background-color:${buttonColor}; color:${buttonTextColor} z-index: 10001">
|
||||||
<div class="mr-3 flex items-center justify-center">
|
<div id="button-icon" class="mr-3 flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
@ -16,7 +31,8 @@ style="background-color: rgb(255, 202, 0); color: rgb(20, 30, 47); z-index: 1000
|
||||||
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>
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-semibold leading-5 antialiased">Book my Cal</div>
|
<div id="button" class="font-semibold leading-5 antialiased">${buttonText}</div>
|
||||||
</button>`;
|
</button>`;
|
||||||
|
};
|
||||||
|
|
||||||
export default html;
|
export default getHtml;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { CalWindow } from "@calcom/embed-snippet";
|
import { CalWindow } from "@calcom/embed-snippet";
|
||||||
|
|
||||||
import loaderCss from "../loader.css";
|
import loaderCss from "../loader.css";
|
||||||
|
import { getErrorString } from "../utils";
|
||||||
import inlineHtml from "./inlineHtml";
|
import inlineHtml from "./inlineHtml";
|
||||||
|
|
||||||
export class Inline extends HTMLElement {
|
export class Inline extends HTMLElement {
|
||||||
|
@ -9,8 +10,16 @@ export class Inline extends HTMLElement {
|
||||||
return ["loading"];
|
return ["loading"];
|
||||||
}
|
}
|
||||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||||
if (name === "loading" && newValue == "done") {
|
if (name === "loading") {
|
||||||
(this.shadowRoot!.querySelector("#loader")! as HTMLElement).style.display = "none";
|
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() {
|
constructor() {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
const html = `<div id="loader" style="top:calc(50% - 30px); left:calc(50% - 30px)" class="absolute z-highest">
|
const html = `<div id="wrapper" style="top:calc(50% - 30px); left:calc(50% - 30px)" class="absolute z-highest">
|
||||||
<div class="loader border-brand dark:border-darkmodebrand">
|
<div class="loader border-brand dark:border-darkmodebrand">
|
||||||
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
|
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="error" class="hidden">
|
||||||
|
Something went wrong.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>`;
|
<slot></slot>`;
|
||||||
export default html;
|
export default html;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { CalWindow } from "@calcom/embed-snippet";
|
import { CalWindow } from "@calcom/embed-snippet";
|
||||||
|
|
||||||
import loaderCss from "../loader.css";
|
import loaderCss from "../loader.css";
|
||||||
|
import { getErrorString } from "../utils";
|
||||||
import modalBoxHtml from "./ModalBoxHtml";
|
import modalBoxHtml from "./ModalBoxHtml";
|
||||||
|
|
||||||
export class ModalBox extends HTMLElement {
|
export class ModalBox extends HTMLElement {
|
||||||
|
@ -28,11 +29,16 @@ export class ModalBox extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue == "loaded") {
|
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") {
|
} else if (newValue === "started") {
|
||||||
this.show(true);
|
this.show(true);
|
||||||
} else if (newValue == "closed") {
|
} else if (newValue == "closed") {
|
||||||
this.show(false);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,11 +59,12 @@ const html = `<style>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div id="loader" class="z-[999999999999] absolute flex w-full items-center">
|
<div id="wrapper" class="z-[999999999999] absolute flex w-full items-center">
|
||||||
<div class="loader modal-loader border-brand dark:border-darkmodebrand">
|
<div class="loader modal-loader border-brand dark:border-darkmodebrand">
|
||||||
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
|
<span class="loader-inner bg-brand dark:bg-darkmodebrand"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="error" class="hidden left-1/2 -translate-x-1/2 relative text-white"></div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { useState, useEffect, CSSProperties } from "react";
|
||||||
import { sdkActionManager } from "./sdk-event";
|
import { sdkActionManager } from "./sdk-event";
|
||||||
|
|
||||||
export interface UiConfig {
|
export interface UiConfig {
|
||||||
theme: string;
|
theme?: "dark" | "light" | "auto";
|
||||||
styles: EmbedStyles;
|
styles?: EmbedStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedStore = {
|
const embedStore = {
|
||||||
|
@ -13,7 +13,6 @@ const embedStore = {
|
||||||
styles: {},
|
styles: {},
|
||||||
namespace: null,
|
namespace: null,
|
||||||
embedType: undefined,
|
embedType: undefined,
|
||||||
theme: null,
|
|
||||||
// Store all React State setters here.
|
// Store all React State setters here.
|
||||||
reactStylesStateSetters: {},
|
reactStylesStateSetters: {},
|
||||||
parentInformedAboutContentHeight: false,
|
parentInformedAboutContentHeight: false,
|
||||||
|
@ -21,11 +20,12 @@ const embedStore = {
|
||||||
} as {
|
} as {
|
||||||
styles: UiConfig["styles"];
|
styles: UiConfig["styles"];
|
||||||
namespace: string | null;
|
namespace: string | null;
|
||||||
theme: string | null;
|
|
||||||
embedType: undefined | null | string;
|
embedType: undefined | null | string;
|
||||||
reactStylesStateSetters: any;
|
reactStylesStateSetters: any;
|
||||||
parentInformedAboutContentHeight: boolean;
|
parentInformedAboutContentHeight: boolean;
|
||||||
windowLoadEventFired: boolean;
|
windowLoadEventFired: boolean;
|
||||||
|
theme?: UiConfig["theme"];
|
||||||
|
setTheme: (arg0: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let isSafariBrowser = false;
|
let isSafariBrowser = false;
|
||||||
|
@ -132,17 +132,14 @@ function isValidNamespace(ns: string | null | undefined) {
|
||||||
|
|
||||||
export const useEmbedTheme = () => {
|
export const useEmbedTheme = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
let [theme, setTheme] = useState(embedStore.theme || (router.query.theme as string));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.events.on("routeChangeComplete", () => {
|
router.events.on("routeChangeComplete", () => {
|
||||||
sdkActionManager?.fire("__routeChanged", {});
|
sdkActionManager?.fire("__routeChanged", {});
|
||||||
});
|
});
|
||||||
}, [router.events]);
|
}, [router.events]);
|
||||||
|
embedStore.setTheme = setTheme;
|
||||||
if (embedStore.theme) {
|
return theme === "auto" ? null : theme;
|
||||||
return embedStore.theme;
|
|
||||||
}
|
|
||||||
const theme = (embedStore.theme = router.query.theme as string);
|
|
||||||
return theme;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Make it usable as an attribute directly instead of styles value. It would allow us to go beyond styles e.g. for debugging we can add a special attribute indentifying the element on which UI config has been applied
|
// TODO: Make it usable as an attribute directly instead of styles value. It would allow us to go beyond styles e.g. for debugging we can add a special attribute indentifying the element on which UI config has been applied
|
||||||
|
@ -271,11 +268,16 @@ export const methods = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// body can't be styled using React state hook as it is generated by _document.tsx which doesn't support hooks.
|
// body can't be styled using React state hook as it is generated by _document.tsx which doesn't support hooks.
|
||||||
if (stylesConfig.body?.background) {
|
if (stylesConfig?.body?.background) {
|
||||||
document.body.style.background = stylesConfig.body.background as string;
|
document.body.style.background = stylesConfig.body.background as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEmbedStyles(stylesConfig);
|
if (uiConfig.theme) {
|
||||||
|
embedStore.theme = uiConfig.theme as UiConfig["theme"];
|
||||||
|
embedStore.setTheme(uiConfig.theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEmbedStyles(stylesConfig || {});
|
||||||
},
|
},
|
||||||
parentKnowsIframeReady: () => {
|
parentKnowsIframeReady: () => {
|
||||||
log("Method: `parentKnowsIframeReady` called");
|
log("Method: `parentKnowsIframeReady` called");
|
||||||
|
@ -370,6 +372,7 @@ function keepParentInformedAboutDimensionChanges() {
|
||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
const url = new URL(document.URL);
|
const url = new URL(document.URL);
|
||||||
|
embedStore.theme = (url.searchParams.get("theme") || "auto") as UiConfig["theme"];
|
||||||
if (url.searchParams.get("prerender") !== "true" && isEmbed()) {
|
if (url.searchParams.get("prerender") !== "true" && isEmbed()) {
|
||||||
log("Initializing embed-iframe");
|
log("Initializing embed-iframe");
|
||||||
// HACK
|
// HACK
|
||||||
|
|
|
@ -23,6 +23,11 @@ const globalCal = (window as CalWindow).Cal;
|
||||||
if (!globalCal || !globalCal.q) {
|
if (!globalCal || !globalCal.q) {
|
||||||
throw new Error("Cal is not defined. This shouldn't happen");
|
throw new Error("Cal is not defined. This shouldn't happen");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store Commit Hash to know exactly what version of the code is running
|
||||||
|
// TODO: Ideally it should be the version as per package.json and then it can be renamed to version.
|
||||||
|
// But because it is built on local machine right now, it is much more reliable to have the commit hash.
|
||||||
|
globalCal.fingerprint = import.meta.env.NEXT_PUBLIC_EMBED_FINGER_PRINT as string;
|
||||||
globalCal.__css = allCss;
|
globalCal.__css = allCss;
|
||||||
document.head.appendChild(document.createElement("style")).innerHTML = css;
|
document.head.appendChild(document.createElement("style")).innerHTML = css;
|
||||||
|
|
||||||
|
@ -30,6 +35,7 @@ function log(...args: any[]) {
|
||||||
console.log(...args);
|
console.log(...args);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
* //TODO: Warn about extra properties not part of schema. Helps in fixing wrong expectations
|
||||||
* A very simple data validator written with intention of keeping payload size low.
|
* A very simple data validator written with intention of keeping payload size low.
|
||||||
* Extend the functionality of it as required by the embed.
|
* Extend the functionality of it as required by the embed.
|
||||||
* @param data
|
* @param data
|
||||||
|
@ -258,21 +264,55 @@ export class Cal {
|
||||||
element.appendChild(template.content);
|
element.appendChild(template.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
floatingButton({ calLink }: { calLink: string }) {
|
floatingButton({
|
||||||
validate(arguments[0], {
|
calLink,
|
||||||
required: true,
|
buttonText = "Book my Cal",
|
||||||
props: {
|
hideButtonIcon = false,
|
||||||
calLink: {
|
attributes,
|
||||||
required: true,
|
buttonPosition = "bottom-right",
|
||||||
type: "string",
|
buttonColor = "rgb(255, 202, 0)",
|
||||||
},
|
buttonTextColor = "rgb(20, 30, 47)",
|
||||||
},
|
}: {
|
||||||
});
|
calLink: string;
|
||||||
|
buttonText?: string;
|
||||||
|
attributes?: Record<string, string>;
|
||||||
|
hideButtonIcon?: boolean;
|
||||||
|
buttonPosition?: "bottom-left" | "bottom-right";
|
||||||
|
buttonColor: string;
|
||||||
|
buttonTextColor: string;
|
||||||
|
}) {
|
||||||
|
// validate(arguments[0], {
|
||||||
|
// required: true,
|
||||||
|
// props: {
|
||||||
|
// calLink: {
|
||||||
|
// required: true,
|
||||||
|
// type: "string",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
let attributesString = "";
|
||||||
|
let existingEl = null;
|
||||||
|
if (attributes?.id) {
|
||||||
|
attributesString += ` id="${attributes.id}"`;
|
||||||
|
existingEl = document.getElementById(attributes.id);
|
||||||
|
}
|
||||||
|
let el = existingEl;
|
||||||
|
if (!existingEl) {
|
||||||
const template = document.createElement("template");
|
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 ${attributesString} data-cal-namespace="${this.namespace}" data-cal-link="${calLink}"></cal-floating-button>`;
|
||||||
|
el = template.content.children[0] as HTMLElement;
|
||||||
document.body.appendChild(template.content);
|
document.body.appendChild(template.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buttonText) {
|
||||||
|
el!.setAttribute("data-button-text", buttonText);
|
||||||
|
}
|
||||||
|
el!.setAttribute("data-hide-button-icon", "" + hideButtonIcon);
|
||||||
|
el!.setAttribute("data-button-position", "" + buttonPosition);
|
||||||
|
el!.setAttribute("data-button-color", "" + buttonColor);
|
||||||
|
el!.setAttribute("data-button-text-color", "" + buttonTextColor);
|
||||||
|
}
|
||||||
|
|
||||||
modal({ calLink, config = {}, uid }: { calLink: string; config?: Record<string, string>; uid: number }) {
|
modal({ calLink, config = {}, uid }: { calLink: string; config?: Record<string, string>; uid: number }) {
|
||||||
const existingModalEl = document.querySelector(`cal-modal-box[uid="${uid}"]`);
|
const existingModalEl = document.querySelector(`cal-modal-box[uid="${uid}"]`);
|
||||||
if (existingModalEl) {
|
if (existingModalEl) {
|
||||||
|
@ -437,8 +477,16 @@ export class Cal {
|
||||||
this.modalBox?.setAttribute("state", "loaded");
|
this.modalBox?.setAttribute("state", "loaded");
|
||||||
this.inlineEl?.setAttribute("loading", "done");
|
this.inlineEl?.setAttribute("loading", "done");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actionManager.on("linkFailed", (e) => {
|
this.actionManager.on("linkFailed", (e) => {
|
||||||
this.iframe?.remove();
|
const iframe = this.iframe;
|
||||||
|
if (!iframe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inlineEl?.setAttribute("data-error-code", e.detail.data.code);
|
||||||
|
this.modalBox?.setAttribute("data-error-code", e.detail.data.code);
|
||||||
|
this.inlineEl?.setAttribute("loading", "failed");
|
||||||
|
this.modalBox?.setAttribute("state", "failed");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
packages/embeds/embed-core/src/preview.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { CalWindow } from "@calcom/embed-snippet";
|
||||||
|
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
const data = e.data;
|
||||||
|
if (data.mode !== "cal:preview") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalCal = (window as CalWindow).Cal;
|
||||||
|
if (!globalCal) {
|
||||||
|
throw new Error("Cal is not defined yet");
|
||||||
|
}
|
||||||
|
if (data.type == "instruction") {
|
||||||
|
globalCal(data.instruction.name, data.instruction.arg);
|
||||||
|
}
|
||||||
|
if (data.type == "inlineEmbedDimensionUpdate") {
|
||||||
|
const inlineEl = document.querySelector("#my-embed") as HTMLElement;
|
||||||
|
if (inlineEl) {
|
||||||
|
inlineEl.style.width = data.data.width;
|
||||||
|
inlineEl.style.height = data.data.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
7
packages/embeds/embed-core/src/utils.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const getErrorString = (errorCode: string | undefined) => {
|
||||||
|
if (errorCode === "404") {
|
||||||
|
return `Error Code: 404. Cal Link seems to be wrong.`;
|
||||||
|
} else {
|
||||||
|
return `Error Code: ${errorCode}. Something went wrong.`;
|
||||||
|
}
|
||||||
|
};
|
|
@ -6,6 +6,9 @@ module.exports = defineConfig({
|
||||||
envPrefix: "NEXT_PUBLIC_",
|
envPrefix: "NEXT_PUBLIC_",
|
||||||
build: {
|
build: {
|
||||||
minify: "terser",
|
minify: "terser",
|
||||||
|
watch: {
|
||||||
|
include: ["src/**"],
|
||||||
|
},
|
||||||
terserOptions: {
|
terserOptions: {
|
||||||
format: {
|
format: {
|
||||||
comments: false,
|
comments: false,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@calcom/embed-react",
|
"name": "@calcom/embed-react",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "Embed Cal Link as a React Component",
|
"description": "Embed Cal Link as a React Component",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port=3101 --open",
|
"dev": "vite --port=3101 --open",
|
||||||
|
|
|
@ -14,6 +14,7 @@ export interface GlobalCal {
|
||||||
ns?: Record<string, GlobalCal>;
|
ns?: Record<string, GlobalCal>;
|
||||||
instance?: CalClass;
|
instance?: CalClass;
|
||||||
__css?: string;
|
__css?: string;
|
||||||
|
fingerprint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalWindow extends Window {
|
export interface CalWindow extends Window {
|
||||||
|
@ -21,7 +22,6 @@ export interface CalWindow extends Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmbedSnippet(url = "https://cal.com/embed.js") {
|
export default function EmbedSnippet(url = "https://cal.com/embed.js") {
|
||||||
/*! Copy the code below and paste it in script tag of your website */
|
|
||||||
(function (C: CalWindow, A, L) {
|
(function (C: CalWindow, A, L) {
|
||||||
let p = function (a: any, ar: any) {
|
let p = function (a: any, ar: any) {
|
||||||
a.q.push(ar);
|
a.q.push(ar);
|
||||||
|
@ -60,3 +60,5 @@ export default function EmbedSnippet(url = "https://cal.com/embed.js") {
|
||||||
|
|
||||||
return (window as CalWindow).Cal;
|
return (window as CalWindow).Cal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EmbedSnippetString = EmbedSnippet.toString();
|
||||||
|
|
|
@ -8,5 +8,9 @@ module.exports = defineConfig({
|
||||||
name: "snippet",
|
name: "snippet",
|
||||||
fileName: (format) => `snippet.${format}.js`,
|
fileName: (format) => `snippet.${format}.js`,
|
||||||
},
|
},
|
||||||
|
minify: "terser",
|
||||||
|
terserOptions: {
|
||||||
|
compress: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,16 +57,24 @@ export function Dialog(props: DialogProps) {
|
||||||
</DialogPrimitive.Root>
|
</DialogPrimitive.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]>;
|
type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]> & {
|
||||||
|
size?: "xl" | "lg";
|
||||||
|
};
|
||||||
|
|
||||||
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
|
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
|
||||||
({ children, ...props }, forwardedRef) => (
|
({ children, ...props }, forwardedRef) => (
|
||||||
<DialogPrimitive.Portal>
|
<DialogPrimitive.Portal>
|
||||||
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-black bg-opacity-50 transition-opacity" />
|
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
{/*zIndex one less than Toast */}
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"fadeIn fixed left-1/2 top-1/2 z-[9999999999] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white p-6 text-left shadow-xl focus-visible:outline-none sm:w-full sm:max-w-[35rem] sm:align-middle",
|
"fadeIn fixed left-1/2 top-1/2 z-[9998] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:w-full sm:align-middle",
|
||||||
|
props.size == "xl"
|
||||||
|
? "p-0.5 sm:max-w-[98vw]"
|
||||||
|
: props.size == "lg"
|
||||||
|
? "p-6 sm:max-w-[70rem]"
|
||||||
|
: "p-6 sm:max-w-[35rem]",
|
||||||
`${props.className}`
|
`${props.className}`
|
||||||
)}
|
)}
|
||||||
ref={forwardedRef}>
|
ref={forwardedRef}>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import React from "react";
|
||||||
|
|
||||||
const Switch = (
|
const Switch = (
|
||||||
props: React.ComponentProps<typeof PrimitiveSwitch.Root> & {
|
props: React.ComponentProps<typeof PrimitiveSwitch.Root> & {
|
||||||
label: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { label, ...primitiveProps } = props;
|
const { label, ...primitiveProps } = props;
|
||||||
|
|