import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible"; import classNames from "classnames"; import { useSession } from "next-auth/react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import type { RefObject } from "react"; import { createRef, useRef, useState } from "react"; import type { ControlProps } from "react-select"; import { components } from "react-select"; import { shallow } from "zustand/shallow"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { AvailableTimesHeader } from "@calcom/features/bookings"; import { AvailableTimes } from "@calcom/features/bookings"; import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event"; import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences"; import DatePicker from "@calcom/features/calendars/DatePicker"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate"; import { APP_NAME, CAL_URL } from "@calcom/lib/constants"; import { weekdayToWeekIndex } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { Button, ColorPicker, Dialog, DialogClose, DialogContent, DialogFooter, HorizontalTabs, Label, Select, showToast, Switch, TextField, TimezoneSelect, } from "@calcom/ui"; import { ArrowLeft, Sun } from "@calcom/ui/components/icon"; import { getDimension } from "./lib/getDimension"; import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types"; type EventType = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"] | undefined; const enum Theme { auto = "auto", light = "light", dark = "dark", } const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl", "eventId"]; function useRouterHelpers() { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); const goto = (newSearchParams: Record) => { const newQuery = new URLSearchParams(searchParams ?? undefined); Object.keys(newSearchParams).forEach((key) => { newQuery.set(key, newSearchParams[key]); }); router.push(`${pathname}?${newQuery.toString()}`); }; const removeQueryParams = (queryParams: string[]) => { const params = new URLSearchParams(searchParams ?? undefined); queryParams.forEach((param) => { params.delete(param); }); router.push(`${pathname}?${params.toString()}`); }; return { goto, removeQueryParams }; } const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => { return ( {children} ); }; const ChooseEmbedTypesDialogContent = ({ types }: { types: EmbedTypes }) => { const { t } = useLocale(); const { goto } = useRouterHelpers(); return (

{t("choose_ways_put_cal_site", { appName: APP_NAME })}

{types.map((embed, index) => ( ))}
); }; const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username: string }) => { const { t, i18n } = useLocale(); const [timezone] = useTimePreferences((state) => [state.timezone]); useInitializeBookerStore({ username, eventSlug: eventType?.slug ?? "", eventId: eventType?.id, layout: BookerLayouts.MONTH_VIEW, }); const [month, selectedDate, selectedDatesAndTimes] = useBookerStore( (state) => [state.month, state.selectedDate, state.selectedDatesAndTimes], shallow ); const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot] = useBookerStore( (state) => [ state.setSelectedDate, state.setMonth, state.setSelectedDatesAndTimes, state.setSelectedTimeslot, ], shallow ); const event = useEvent(); const schedule = useScheduleForEvent(); const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots); const onTimeSelect = (time: string) => { if (!eventType) { return null; } if (selectedDatesAndTimes && selectedDatesAndTimes[eventType.slug]) { const selectedDatesAndTimesForEvent = selectedDatesAndTimes[eventType.slug]; const selectedSlots = selectedDatesAndTimesForEvent[selectedDate as string] ?? []; if (selectedSlots?.includes(time)) { // Checks whether a user has removed all their timeSlots and thus removes it from the selectedDatesAndTimesForEvent state if (selectedSlots?.length > 1) { const updatedDatesAndTimes = { ...selectedDatesAndTimes, [eventType.slug]: { ...selectedDatesAndTimesForEvent, [selectedDate as string]: selectedSlots?.filter((slot: string) => slot !== time), }, }; setSelectedDatesAndTimes(updatedDatesAndTimes); } else { const updatedDatesAndTimesForEvent = { ...selectedDatesAndTimesForEvent }; delete updatedDatesAndTimesForEvent[selectedDate as string]; setSelectedTimeslot(null); setSelectedDatesAndTimes({ ...selectedDatesAndTimes, [eventType.slug]: updatedDatesAndTimesForEvent, }); } return; } const updatedDatesAndTimes = { ...selectedDatesAndTimes, [eventType.slug]: { ...selectedDatesAndTimesForEvent, [selectedDate as string]: [...selectedSlots, time], }, }; setSelectedDatesAndTimes(updatedDatesAndTimes); } else if (!selectedDatesAndTimes) { setSelectedDatesAndTimes({ [eventType.slug]: { [selectedDate as string]: [time] } }); } else { setSelectedDatesAndTimes({ ...selectedDatesAndTimes, [eventType.slug]: { [selectedDate as string]: [time] }, }); } setSelectedTimeslot(time); }; const slots = useSlotsForDate(selectedDate, schedule?.data?.slots); if (!eventType) { return null; } return (
{t("select_date")}
{ setSelectedDate(date === null ? date : date.format("YYYY-MM-DD")); }} onMonthChange={(date: Dayjs) => { setMonth(date.format("YYYY-MM")); setSelectedDate(date.format("YYYY-MM-DD")); }} includedDates={nonEmptyScheduleDays} locale={i18n.language} browsingDate={month ? dayjs(month) : undefined} selected={dayjs(selectedDate)} weekStart={weekdayToWeekIndex(event?.data?.users?.[0]?.weekStart)} eventSlug={eventType?.slug} />
{selectedDate ? (
{selectedDate ? (
) : null}
) : null}
{t("duration")}
{t("minutes")}} />
{t("timezone")}
); }; const EmailEmbedPreview = ({ eventType, emailContentRef, username, month, selectedDateAndTime, }: { eventType: EventType; timezone?: string; emailContentRef: RefObject; username?: string; month?: string; selectedDateAndTime: { [key: string]: string[] }; }) => { const { t } = useLocale(); const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]); if (!eventType) { return null; } return (
{eventType.title}
{t("duration")}: {eventType.length} mins
{t("timezone")}: {timezone}
<> {selectedDateAndTime && Object.keys(selectedDateAndTime) .sort() .map((key) => { const date = new Date(key); return (
{date.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric", })}  
{selectedDateAndTime[key]?.length > 0 && selectedDateAndTime[key].map((time) => { const bookingURL = `${CAL_URL}/${username}/${eventType.slug}?duration=${eventType.length}&date=${key}&month=${month}&slot=${time}`; return ( ); })}
{dayjs.utc(time).tz(timezone).format(timeFormat)}  
); })}
{t("powered_by")}{" "} Cal.com
); }; const EmbedTypeCodeAndPreviewDialogContent = ({ embedType, embedUrl, tabs, eventTypeHideOptionDisabled, types, }: { embedType: EmbedType; embedUrl: string; tabs: EmbedTabs; eventTypeHideOptionDisabled: boolean; types: EmbedTypes; }) => { const { t } = useLocale(); const searchParams = useSearchParams(); const pathname = usePathname(); const { goto, removeQueryParams } = useRouterHelpers(); const iframeRef = useRef(null); const dialogContentRef = useRef(null); const flags = useFlagMap(); const isBookerLayoutsEnabled = flags["booker-layouts"] === true; const emailContentRef = useRef(null); const { data } = useSession(); const [month, selectedDatesAndTimes] = useBookerStore( (state) => [state.month, state.selectedDatesAndTimes], shallow ); const eventId = searchParams?.get("eventId"); const parsedEventId = parseInt(eventId ?? "", 10); const calLink = decodeURIComponent(embedUrl); const { data: eventTypeData } = trpc.viewer.eventTypes.get.useQuery( { id: parsedEventId }, { enabled: !Number.isNaN(parsedEventId) && embedType === "email", refetchOnWindowFocus: false } ); const s = (href: string) => { const _searchParams = new URLSearchParams(searchParams ?? undefined); const [a, b] = href.split("="); _searchParams.set(a, b); return `${pathname?.split("?")[0] ?? ""}?${_searchParams.toString()}`; }; const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) })); const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject> = {}; tabs .filter((tab) => tab.type === "code") .forEach((codeTab) => { embedCodeRefs[codeTab.name] = createRef(); }); const refOfEmbedCodesRefs = useRef(embedCodeRefs); const embed = types.find((embed) => embed.type === embedType); const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true); const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true); const [previewState, setPreviewState] = useState({ inline: { width: "100%", height: "100%", }, theme: Theme.auto, layout: BookerLayouts.MONTH_VIEW, floatingPopup: {}, elementClick: {}, hideEventTypeDetails: false, palette: { brandColor: "#000000", }, }); const close = () => { removeQueryParams(["dialog", ...queryParamsForDialog]); }; // Use embed-code as default tab if (!searchParams?.get("embedTabName")) { goto({ embedTabName: "embed-code", }); } if (!embed || !embedUrl) { close(); return null; } const addToPalette = (update: (typeof previewState)["palette"]) => { setPreviewState((previewState) => { return { ...previewState, palette: { ...previewState.palette, ...update, }, }; }); }; const previewInstruction = (instruction: { name: string; arg: unknown }) => { 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, layout: previewState.layout, hideEventTypeDetails: previewState.hideEventTypeDetails, styles: { branding: { ...previewState.palette, }, }, }, }); const handleCopyEmailText = () => { const contentElement = emailContentRef.current; if (contentElement !== null) { const range = document.createRange(); range.selectNode(contentElement); const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(range); document.execCommand("copy"); selection.removeAllRanges(); } showToast(t("code_copied"), "success"); } }; 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: Theme.auto, label: "Auto" }, { value: Theme.dark, label: "Dark Theme" }, { value: Theme.light, label: "Light Theme" }, ]; const layoutOptions = [ { value: BookerLayouts.MONTH_VIEW, label: t("bookerlayout_month_view") }, { value: BookerLayouts.WEEK_VIEW, label: t("bookerlayout_week_view") }, { value: BookerLayouts.COLUMN_VIEW, label: t("bookerlayout_column_view") }, ]; const FloatingPopupPositionOptions = [ { value: "bottom-right", label: "Bottom right", }, { value: "bottom-left", label: "Bottom left", }, ]; return (

{embed.subtitle}

{eventTypeData?.eventType && embedType === "email" ? ( ) : (
setIsEmbedCustomizationOpen((val) => !val)}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
Window sizing
{ setPreviewState((previewState) => { const width = e.target.value || "100%"; return { ...previewState, inline: { ...previewState.inline, width, }, }; }); }} addOnLeading={<>W} />
{ const height = e.target.value || "100%"; setPreviewState((previewState) => { return { ...previewState, inline: { ...previewState.inline, height, }, }; }); }} addOnLeading={<>H} />
Button text
{/* Default Values should come from preview iframe */} { setPreviewState((previewState) => { return { ...previewState, floatingPopup: { ...previewState.floatingPopup, buttonText: e.target.value, }, }; }); }} defaultValue={t("book_my_cal")} required />
{ setPreviewState((previewState) => { return { ...previewState, floatingPopup: { ...previewState.floatingPopup, hideButtonIcon: !checked, }, }; }); }} />
Display calendar icon
Position of button
null, }} onChange={(option) => { if (!option) { return; } setPreviewState((previewState) => { return { ...previewState, theme: option.value, }; }); }} options={ThemeOptions} /> {!eventTypeHideOptionDisabled ? (
{ setPreviewState((previewState) => { return { ...previewState, hideEventTypeDetails: checked, }; }); }} />
{t("hide_eventtype_details")}
) : null} {[ { 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) => ( ))} {isBookerLayoutsEnabled && (