From 0b03bcb90e474025058a74d459298fd0b306aa66 Mon Sep 17 00:00:00 2001 From: Rajiv Sahal Date: Tue, 13 Aug 2024 21:51:29 +0530 Subject: [PATCH] feat: `CalendarSettings` atom (#16120) * dumb components for calendar settings * shadcn switch * update exports * update packages * add calendar settings atom to examples app * init calendar settings atom * refactors * fix import path * export type for calendar switch props * invalidate queries on deleting calendar credentials * replace calendars list with calendar settings wrapper * cleanup * update styling * refactors * cleanup * fix: missing key prop CalendarSettingsPlatformWrapper * Label as client components * Address client component build errors * Move QueryCell out of packages/lib * PR feedback * more feedback --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay Co-authored-by: Joe Au-Yeung --- apps/web/components/AppListCard.tsx | 75 +----- .../apps/AdditionalCalendarSelector.tsx | 3 +- .../components/apps/CalendarListContainer.tsx | 120 +-------- packages/app-store/components.tsx | 2 + .../embeds/embed-core/src/embed-iframe.ts | 2 + .../apps/components/DisconnectIntegration.tsx | 44 +--- packages/features/bookings/Booker/store.ts | 2 + .../features/calendars/CalendarSwitch.tsx | 6 +- .../availability/AvailabilitySettings.tsx | 2 + .../atoms/booker/BookerWebWrapper.tsx | 2 + .../atoms/cal-provider/CalProvider.tsx | 2 + .../calendar-settings/CalendarSettings.tsx | 5 + .../platform/atoms/calendar-settings/index.ts | 1 + .../CalendarSettingsPlatformWrapper.tsx | 242 ++++++++++++++++++ .../wrappers/CalendarSettingsWebWrapper.tsx | 152 +++++++++++ .../calendars/useDeleteCalendarCredentials.ts | 7 +- .../platform/atoms/hooks/useAtomsContext.ts | 2 + packages/platform/atoms/index.ts | 1 + packages/platform/atoms/monorepo.ts | 1 + packages/platform/atoms/package.json | 1 + .../atoms/src/components/ui/switch.tsx | 25 ++ .../examples/base/src/pages/calendars.tsx | 43 +--- packages/trpc/components/QueryCell.tsx | 80 ++++++ .../components/app-list-card/AppListCard.tsx | 74 ++++++ packages/ui/components/app-list-card/index.ts | 1 + packages/ui/components/apps/AllApps.tsx | 2 + packages/ui/components/apps/AppCard.tsx | 2 + packages/ui/components/apps/Slider.tsx | 2 + .../ui/components/breadcrumb/Breadcrumb.tsx | 2 + .../calendar-switch/CalendarSwitch.tsx | 43 ++++ .../ui/components/calendar-switch/index.ts | 1 + packages/ui/components/credits/Credits.tsx | 2 + packages/ui/components/data-table/index.tsx | 2 + packages/ui/components/dialog/Dialog.tsx | 2 + .../DisconnectIntegration.tsx | 48 ++++ .../disconnect-calendar-integration/index.ts | 1 + .../editable-heading/EditableHeading.tsx | 2 + .../editor/plugins/AddVariablesPlugin.tsx | 2 + .../editor/plugins/ToolbarPlugin.tsx | 2 + .../form/color-picker/colorpicker.tsx | 2 + packages/ui/components/form/inputs/Input.tsx | 2 + .../ui/components/form/inputs/TextField.tsx | 2 + .../form/timezone-select/TimezoneSelect.tsx | 2 + .../form/toggleGroup/BooleanToggleGroup.tsx | 2 + .../ui/components/form/wizard/WizardForm.tsx | 2 + .../image-uploader/BannerUploader.tsx | 2 + .../image-uploader/ImageUploader.tsx | 2 + packages/ui/components/meta/Meta.tsx | 2 + packages/ui/components/mocks/trpc.tsx | 2 + .../components/scrollable/ScrollableArea.tsx | 2 + packages/ui/form/PhoneInput.tsx | 2 + packages/ui/index.tsx | 3 + packages/ui/layouts/WizardLayout.tsx | 2 + packages/ui/styles/useCalcomTheme.tsx | 2 + 54 files changed, 781 insertions(+), 258 deletions(-) create mode 100644 packages/platform/atoms/calendar-settings/CalendarSettings.tsx create mode 100644 packages/platform/atoms/calendar-settings/index.ts create mode 100644 packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx create mode 100644 packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsWebWrapper.tsx create mode 100644 packages/platform/atoms/src/components/ui/switch.tsx create mode 100644 packages/trpc/components/QueryCell.tsx create mode 100644 packages/ui/components/app-list-card/AppListCard.tsx create mode 100644 packages/ui/components/app-list-card/index.ts create mode 100644 packages/ui/components/calendar-switch/CalendarSwitch.tsx create mode 100644 packages/ui/components/calendar-switch/index.ts create mode 100644 packages/ui/components/disconnect-calendar-integration/DisconnectIntegration.tsx create mode 100644 packages/ui/components/disconnect-calendar-integration/index.ts diff --git a/apps/web/components/AppListCard.tsx b/apps/web/components/AppListCard.tsx index f884a9bb56d57b..7a7eccfa66ade1 100644 --- a/apps/web/components/AppListCard.tsx +++ b/apps/web/components/AppListCard.tsx @@ -1,15 +1,14 @@ +"use client"; + import { usePathname, useRouter } from "next/navigation"; import type { ReactNode } from "react"; import { useEffect, useRef, useState } from "react"; import { z } from "zod"; import type { CredentialOwner } from "@calcom/app-store/types"; -import classNames from "@calcom/lib/classNames"; -import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; -import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui"; +import { AppListCard as AppListCardComponent } from "@calcom/ui"; type ShouldHighlight = | { @@ -21,7 +20,7 @@ type ShouldHighlight = slug?: never; }; -type AppListCardProps = { +export type AppListCardProps = { logo?: string; title: string; description: string; @@ -37,21 +36,7 @@ type AppListCardProps = { const schema = z.object({ hl: z.string().optional() }); export default function AppListCard(props: AppListCardProps) { - const { t } = useLocale(); - const { - logo, - title, - description, - actions, - isDefault, - slug, - shouldHighlight, - isTemplate, - invalidCredential, - children, - credentialOwner, - className, - } = props; + const { slug, shouldHighlight } = props; const { data: { hl }, } = useTypedQuery(schema); @@ -83,53 +68,5 @@ export default function AppListCard(props: AppListCardProps) { }; }, [highlight, pathname, router, searchParams, shouldHighlight]); - return ( -
-
- {logo ? ( - {`${title} - ) : null} -
-
-

{title}

-
- {isDefault && {t("default")}} - {isTemplate && Template} -
-
- {description} - {invalidCredential && ( -
- - - {t("invalid_credential")} - -
- )} -
- {credentialOwner && ( -
- -
- - {credentialOwner.name} -
-
-
- )} - - {actions} -
- {children &&
{children}
} -
- ); + return ; } diff --git a/apps/web/components/apps/AdditionalCalendarSelector.tsx b/apps/web/components/apps/AdditionalCalendarSelector.tsx index 901bf8314664a0..a9150e7ddadf14 100644 --- a/apps/web/components/apps/AdditionalCalendarSelector.tsx +++ b/apps/web/components/apps/AdditionalCalendarSelector.tsx @@ -2,6 +2,7 @@ import type { FunctionComponent, SVGProps } from "react"; import { InstallAppButton } from "@calcom/app-store/components"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { QueryCell } from "@calcom/trpc/components/QueryCell"; import { trpc } from "@calcom/trpc/react"; import { Button, @@ -12,8 +13,6 @@ import { DropdownMenuTrigger, } from "@calcom/ui"; -import { QueryCell } from "@lib/QueryCell"; - interface AdditionalCalendarSelectorProps { isPending?: boolean; } diff --git a/apps/web/components/apps/CalendarListContainer.tsx b/apps/web/components/apps/CalendarListContainer.tsx index a98cd0c97404f0..d161cc75c7732b 100644 --- a/apps/web/components/apps/CalendarListContainer.tsx +++ b/apps/web/components/apps/CalendarListContainer.tsx @@ -1,14 +1,11 @@ -import Link from "next/link"; import { Fragment, useEffect } from "react"; import { InstallAppButton } from "@calcom/app-store/components"; -import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration"; -import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch"; +import { CalendarSettingsWebWrapper } from "@calcom/atoms/monorepo"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { - Alert, Button, EmptyScreen, Label, @@ -22,7 +19,6 @@ import { QueryCell } from "@lib/QueryCell"; import useRouterQuery from "@lib/hooks/useRouterQuery"; import AppListCard from "@components/AppListCard"; -import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector"; import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections"; type Props = { @@ -68,118 +64,6 @@ function CalendarList(props: Props) { ); } -// todo: @hariom extract this into packages/apps-store as "GeneralAppSettings" -function ConnectedCalendarsList(props: Props) { - const { t } = useLocale(); - const query = trpc.viewer.connectedCalendars.useQuery(undefined, { - suspense: true, - refetchOnWindowFocus: false, - }); - const { fromOnboarding, isPending } = props; - return ( - null} - success={({ data }) => { - if (!data.connectedCalendars.length) { - return null; - } - - return ( -
-
-
-
-

- {t("check_for_conflicts")} -

-

{t("select_calendars")}

-
-
- {!!data.connectedCalendars.length && ( -
- -
- )} -
-
-
- - {data.connectedCalendars.map((item) => ( - - {item.calendars ? ( - - -
- }> -
- {!fromOnboarding && ( - <> -

{t("toggle_calendars_conflict")}

-
    - {item.calendars.map((cal) => ( - - ))} -
- - )} -
- - ) : ( - - {item.integration.name}:{" "} - {t("calendar_error")} - - } - iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5" - actions={ -
- -
- } - /> - )} - - ))} - - - ); - }} - /> - ); -} - export function CalendarListContainer(props: { heading?: boolean; fromOnboarding?: boolean }) { const { t } = useLocale(); const { heading = true, fromOnboarding } = props; @@ -245,7 +129,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding - - - - - - { - mutation.mutate({ id: credentialId }); - }}> -

{t("are_you_sure_you_want_to_remove_this_app")}

-
-
- + mutation.mutate({ id: credentialId })} + isModalOpen={modalOpen} + onModalOpen={() => setModalOpen((prevValue) => !prevValue)} + {...props} + /> ); } diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 674fd6b5f65d95..836f61a3b362fb 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -1,3 +1,5 @@ +"use client"; + import { useEffect } from "react"; import { create } from "zustand"; diff --git a/packages/features/calendars/CalendarSwitch.tsx b/packages/features/calendars/CalendarSwitch.tsx index 6bafec960344a3..25c3c5a09c0437 100644 --- a/packages/features/calendars/CalendarSwitch.tsx +++ b/packages/features/calendars/CalendarSwitch.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; @@ -6,7 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Icon, showToast, Switch } from "@calcom/ui"; -interface ICalendarSwitchProps { +export type ICalendarSwitchProps = { title: string; externalId: string; type: string; @@ -15,7 +17,7 @@ interface ICalendarSwitchProps { isLastItemInList?: boolean; destination?: boolean; credentialId: number; -} +}; const CalendarSwitch = (props: ICalendarSwitchProps) => { const { title, externalId, type, isChecked, name, isLastItemInList = false, credentialId } = props; const [checkedInternal, setCheckedInternal] = useState(isChecked); diff --git a/packages/platform/atoms/availability/AvailabilitySettings.tsx b/packages/platform/atoms/availability/AvailabilitySettings.tsx index 90a3772c43859a..00dbb668aaa329 100644 --- a/packages/platform/atoms/availability/AvailabilitySettings.tsx +++ b/packages/platform/atoms/availability/AvailabilitySettings.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useMemo, useState, useEffect } from "react"; import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form"; diff --git a/packages/platform/atoms/booker/BookerWebWrapper.tsx b/packages/platform/atoms/booker/BookerWebWrapper.tsx index b32ff954cd4a60..bcdb05e41a063d 100644 --- a/packages/platform/atoms/booker/BookerWebWrapper.tsx +++ b/packages/platform/atoms/booker/BookerWebWrapper.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useSession } from "next-auth/react"; import { useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation"; diff --git a/packages/platform/atoms/cal-provider/CalProvider.tsx b/packages/platform/atoms/cal-provider/CalProvider.tsx index 0aa79c719fe4dd..2a206b0b7bedda 100644 --- a/packages/platform/atoms/cal-provider/CalProvider.tsx +++ b/packages/platform/atoms/cal-provider/CalProvider.tsx @@ -1,3 +1,5 @@ +"use client"; + import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useEffect, type ReactNode } from "react"; diff --git a/packages/platform/atoms/calendar-settings/CalendarSettings.tsx b/packages/platform/atoms/calendar-settings/CalendarSettings.tsx new file mode 100644 index 00000000000000..09557b2492afc1 --- /dev/null +++ b/packages/platform/atoms/calendar-settings/CalendarSettings.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export const CalendarSettings = (props: { children: ReactNode }) => { + return
{props.children}
; +}; diff --git a/packages/platform/atoms/calendar-settings/index.ts b/packages/platform/atoms/calendar-settings/index.ts new file mode 100644 index 00000000000000..e10911a3f29a18 --- /dev/null +++ b/packages/platform/atoms/calendar-settings/index.ts @@ -0,0 +1 @@ +export { CalendarSettingsPlatformWrapper } from "./wrappers/CalendarSettingsPlatformWrapper"; diff --git a/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx b/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx new file mode 100644 index 00000000000000..71105ef66108da --- /dev/null +++ b/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx @@ -0,0 +1,242 @@ +import { useState } from "react"; + +import type { ICalendarSwitchProps } from "@calcom/features/calendars/CalendarSwitch"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { CALENDARS } from "@calcom/platform-constants"; +import { QueryCell } from "@calcom/trpc/components/QueryCell"; +import type { ButtonProps } from "@calcom/ui"; +import { + CalendarSwitchComponent, + AppListCard, + List, + DisconnectIntegrationComponent, + Alert, +} from "@calcom/ui"; + +import { useAddSelectedCalendar } from "../../hooks/calendars/useAddSelectedCalendar"; +import { useDeleteCalendarCredentials } from "../../hooks/calendars/useDeleteCalendarCredentials"; +import { useRemoveSelectedCalendar } from "../../hooks/calendars/useRemoveSelectedCalendar"; +import { useConnectedCalendars } from "../../hooks/useConnectedCalendars"; +import { AtomsWrapper } from "../../src/components/atoms-wrapper"; +import { Switch } from "../../src/components/ui/switch"; +import { useToast } from "../../src/components/ui/use-toast"; +import { CalendarSettings } from "../CalendarSettings"; + +export const CalendarSettingsPlatformWrapper = () => { + const { t } = useLocale(); + const query = useConnectedCalendars({}); + + return ( + +
+ { + const destinationCalendarId = data.destinationCalendar.externalId; + + if (!data.connectedCalendars.length) { + return null; + } + + return ( + + + + {data.connectedCalendars.map((connectedCalendar) => { + if (!!connectedCalendar.calendars && connectedCalendar.calendars.length > 0) { + return ( + + +
+ }> +
+

{t("toggle_calendars_conflict")}

+
    + {connectedCalendar.calendars?.map((cal) => { + return ( + + ); + })} +
+
+ + ); + } + return ( + {t("calendar_error")}} + iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5" + actions={ +
+ +
+ } + /> + ); + })} + + + ); + }} + /> + +
+ ); +}; + +const CalendarSettingsHeading = () => { + const { t } = useLocale(); + + return ( +
+
+
+

{t("check_for_conflicts")}

+

{t("select_calendars")}

+
+
+
+ ); +}; + +const PlatformDisconnectIntegration = (props: { + credentialId: number; + label?: string; + slug?: string; + trashIcon?: boolean; + isGlobal?: boolean; + onSuccess?: () => void; + buttonProps?: ButtonProps; +}) => { + const { t } = useLocale(); + const { onSuccess, credentialId, slug } = props; + + const [modalOpen, setModalOpen] = useState(false); + const { toast } = useToast(); + const { mutate: deleteCalendarCredentials } = useDeleteCalendarCredentials({ + onSuccess: () => { + toast({ + description: t("app_removed_successfully"), + }); + setModalOpen(false); + onSuccess && onSuccess(); + }, + onError: () => { + toast({ + description: t("error_removing_app"), + }); + setModalOpen(false); + }, + }); + + return ( + { + slug && + (await deleteCalendarCredentials({ + calendar: slug.split("-")[0] as unknown as (typeof CALENDARS)[number], + id: credentialId, + })); + }} + {...props} + isModalOpen={modalOpen} + onModalOpen={() => setModalOpen((prevValue) => !prevValue)} + /> + ); +}; + +const PlatformCalendarSwitch = (props: ICalendarSwitchProps) => { + const { isChecked, title, credentialId, type, externalId } = props; + const [checkedInternal, setCheckedInternal] = useState(isChecked); + const { toast } = useToast(); + + const { mutate: addSelectedCalendar, isPending: isAddingSelectedCalendar } = useAddSelectedCalendar({ + onError: (err) => { + toast({ + description: `Something went wrong while adding calendar - ${title}. ${err}`, + }); + }, + }); + const { mutate: removeSelectedCalendar, isPending: isRemovingSelectedCalendar } = useRemoveSelectedCalendar( + { + onError: (err) => { + toast({ + description: `Something went wrong while removing calendar - ${title}. ${err}`, + }); + }, + } + ); + + const toggleSelectedCalendars = async ({ + isOn, + credentialId, + integration, + externalId, + }: { + isOn: boolean; + credentialId: number; + integration: string; + externalId: string; + }) => { + if (isOn) { + await addSelectedCalendar({ credentialId, integration, externalId }); + } else { + await removeSelectedCalendar({ credentialId, integration, externalId }); + } + }; + + return ( + + { + setCheckedInternal((prevValue) => !prevValue); + await toggleSelectedCalendars({ + isOn: !checkedInternal, + credentialId, + externalId, + integration: type, + }); + }} + /> + + ); +}; diff --git a/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsWebWrapper.tsx b/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsWebWrapper.tsx new file mode 100644 index 00000000000000..54a5eee5722810 --- /dev/null +++ b/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsWebWrapper.tsx @@ -0,0 +1,152 @@ +import Link from "next/link"; +import React from "react"; + +import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration"; +import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { QueryCell } from "@calcom/trpc/components/QueryCell"; +import { trpc } from "@calcom/trpc/react"; +import { Alert } from "@calcom/ui"; +import { List } from "@calcom/ui"; +import AppListCard from "@calcom/web/components/AppListCard"; +import AdditionalCalendarSelector from "@calcom/web/components/apps/AdditionalCalendarSelector"; + +import { CalendarSettings } from "../CalendarSettings"; + +type CalendarSettingsWebWrapperProps = { + onChanged: () => unknown | Promise; + fromOnboarding?: boolean; + destinationCalendarId?: string; + isPending?: boolean; +}; + +export const CalendarSettingsWebWrapper = (props: CalendarSettingsWebWrapperProps) => { + const { t } = useLocale(); + const query = trpc.viewer.connectedCalendars.useQuery(undefined, { + suspense: true, + refetchOnWindowFocus: false, + }); + const { fromOnboarding, isPending } = props; + + return ( +
+ { + if (!data.connectedCalendars.length) { + return null; + } + + return ( + + + + {data.connectedCalendars.map((connectedCalendar) => { + if (!!connectedCalendar.calendars && connectedCalendar.calendars.length > 0) { + return ( + + +
+ }> +
+ {!fromOnboarding && ( + <> +

+ {t("toggle_calendars_conflict")} +

+
    + {connectedCalendar.calendars?.map((cal) => ( + + ))} +
+ + )} +
+ + ); + } + return ( + + + {connectedCalendar.integration.name} + + : {t("calendar_error")} + + } + iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5" + actions={ +
+ +
+ } + /> + ); + })} + + + ); + }} + /> + + ); +}; + +const CalendarSettingsHeading = (props: { isConnectedCalendarsPresent: boolean; isPending?: boolean }) => { + const { t } = useLocale(); + + return ( +
+
+
+

{t("check_for_conflicts")}

+

{t("select_calendars")}

+
+
+ {props.isConnectedCalendarsPresent && ( +
+ +
+ )} +
+
+
+ ); +}; diff --git a/packages/platform/atoms/hooks/calendars/useDeleteCalendarCredentials.ts b/packages/platform/atoms/hooks/calendars/useDeleteCalendarCredentials.ts index 66223cd9459089..7f2ea93f4ce617 100644 --- a/packages/platform/atoms/hooks/calendars/useDeleteCalendarCredentials.ts +++ b/packages/platform/atoms/hooks/calendars/useDeleteCalendarCredentials.ts @@ -1,10 +1,11 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { CALENDARS } from "@calcom/platform-constants"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; import type { ApiErrorResponse, ApiResponse } from "@calcom/platform-types"; import http from "../../lib/http"; +import { QUERY_KEY } from "../useConnectedCalendars"; interface IUseDeleteCalendarCredentials { onSuccess?: (res: ApiResponse) => void; @@ -21,6 +22,7 @@ export const useDeleteCalendarCredentials = ( }, } ) => { + const queryClient = useQueryClient(); const deleteCalendarCredentials = useMutation< ApiResponse<{ status: string; @@ -56,6 +58,9 @@ export const useDeleteCalendarCredentials = ( onError: (err) => { onError?.(err as ApiErrorResponse); }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY] }); + }, }); return deleteCalendarCredentials; diff --git a/packages/platform/atoms/hooks/useAtomsContext.ts b/packages/platform/atoms/hooks/useAtomsContext.ts index bfd5d942b85bc0..7a379f63bed14b 100644 --- a/packages/platform/atoms/hooks/useAtomsContext.ts +++ b/packages/platform/atoms/hooks/useAtomsContext.ts @@ -1,3 +1,5 @@ +"use client"; + import { createContext, useContext } from "react"; import type { translationKeys, CalProviderLanguagesType } from "../cal-provider/CalProvider"; diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index 272ff8098ce8d8..b58af5df5a509b 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -20,3 +20,4 @@ export { useDeleteCalendarCredentials } from "./hooks/calendars/useDeleteCalenda export { useAddSelectedCalendar } from "./hooks/calendars/useAddSelectedCalendar"; export { useRemoveSelectedCalendar } from "./hooks/calendars/useRemoveSelectedCalendar"; export { useTeams } from "./hooks/teams/useTeams"; +export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/wrappers/CalendarSettingsPlatformWrapper"; diff --git a/packages/platform/atoms/monorepo.ts b/packages/platform/atoms/monorepo.ts index 3a285354ae56ce..36f6c9bc759d49 100644 --- a/packages/platform/atoms/monorepo.ts +++ b/packages/platform/atoms/monorepo.ts @@ -7,3 +7,4 @@ export { useEventTypeById } from "./hooks/event-types/private/useEventTypeById"; export { useHandleBookEvent } from "./hooks/useHandleBookEvent"; export * as Dialog from "./src/components/ui/dialog"; export { Timezone } from "./timezone"; +export { CalendarSettingsWebWrapper } from "./calendar-settings/wrappers/CalendarSettingsWebWrapper"; diff --git a/packages/platform/atoms/package.json b/packages/platform/atoms/package.json index 67163992b7538b..9958e91fc1c84c 100644 --- a/packages/platform/atoms/package.json +++ b/packages/platform/atoms/package.json @@ -57,6 +57,7 @@ "dependencies": { "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-toast": "^1.1.5", "@tanstack/react-query": "^5.17.15", "class-variance-authority": "^0.4.0", diff --git a/packages/platform/atoms/src/components/ui/switch.tsx b/packages/platform/atoms/src/components/ui/switch.tsx new file mode 100644 index 00000000000000..3e8b9d045254c5 --- /dev/null +++ b/packages/platform/atoms/src/components/ui/switch.tsx @@ -0,0 +1,25 @@ +import { cn } from "@/lib/utils"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; +import * as React from "react"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/packages/platform/examples/base/src/pages/calendars.tsx b/packages/platform/examples/base/src/pages/calendars.tsx index 9ae93bdb0ee933..0dd75dbf4202c4 100644 --- a/packages/platform/examples/base/src/pages/calendars.tsx +++ b/packages/platform/examples/base/src/pages/calendars.tsx @@ -1,47 +1,24 @@ import { Navbar } from "@/components/Navbar"; import { Inter } from "next/font/google"; -import { useConnectedCalendars } from "@calcom/atoms"; +import { useConnectedCalendars, CalendarSettings } from "@calcom/atoms"; const inter = Inter({ subsets: ["latin"] }); export default function Calendars(props: { calUsername: string; calEmail: string }) { - const { isLoading, data: calendars } = useConnectedCalendars(); + const { isLoading, data: calendars } = useConnectedCalendars({}); const connectedCalendars = calendars?.connectedCalendars ?? []; - const destinationCalendar = calendars?.destinationCalendar ?? {}; + return (
-
- {!!connectedCalendars?.length ? ( -

Your Connected Calendars

- ) : ( -

- You have not connected any calendars yet, please connect your Google calendar. -

- )} - {isLoading ? ( -
Loading...
- ) : ( - Boolean(connectedCalendars?.length) && - connectedCalendars.map((connectedCalendar) => ( -
-

{connectedCalendar.integration.name}

- {connectedCalendar.calendars?.map((calendar) => ( -
-

{calendar.name}

-
- ))} -
- )) - )} - {!!connectedCalendars?.length &&
} - {!isLoading && destinationCalendar.id && ( -
-

Destination Calendar: {destinationCalendar.name}

-

{destinationCalendar.integrationTitle}

-
- )} + {!isLoading && !connectedCalendars?.length && ( +

+ You have not connected any calendars yet, please connect your Google, Outlook or Apple calendar. +

+ )} +
+
); diff --git a/packages/trpc/components/QueryCell.tsx b/packages/trpc/components/QueryCell.tsx new file mode 100644 index 00000000000000..cfc2fbbb5f27e0 --- /dev/null +++ b/packages/trpc/components/QueryCell.tsx @@ -0,0 +1,80 @@ +import type { + QueryObserverPendingResult, + QueryObserverRefetchErrorResult, + QueryObserverSuccessResult, + QueryObserverLoadingErrorResult, + UseQueryResult, +} from "@tanstack/react-query"; +import type { ReactNode } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Alert, Loader } from "@calcom/ui"; + +type ErrorLike = { + message: string; +}; +type JSXElementOrNull = JSX.Element | null; + +interface QueryCellOptionsBase { + query: UseQueryResult; + customLoader?: ReactNode; + error?: ( + query: QueryObserverLoadingErrorResult | QueryObserverRefetchErrorResult + ) => JSXElementOrNull; + loading?: (query: QueryObserverPendingResult | null) => JSXElementOrNull; +} + +interface QueryCellOptionsNoEmpty + extends QueryCellOptionsBase { + success: (query: QueryObserverSuccessResult) => JSXElementOrNull; +} + +interface QueryCellOptionsWithEmpty + extends QueryCellOptionsBase { + success: (query: QueryObserverSuccessResult, TError>) => JSXElementOrNull; + /** + * If there's no data (`null`, `undefined`, or `[]`), render this component + */ + empty: (query: QueryObserverSuccessResult) => JSXElementOrNull; +} + +export function QueryCell( + opts: QueryCellOptionsWithEmpty +): JSXElementOrNull; +export function QueryCell( + opts: QueryCellOptionsNoEmpty +): JSXElementOrNull; +/** @deprecated Use `trpc.useQuery` instead. */ +export function QueryCell( + opts: QueryCellOptionsNoEmpty | QueryCellOptionsWithEmpty +) { + const { query } = opts; + const { isLocaleReady } = useLocale(); + const StatusLoader = opts.customLoader || ; // Fixes edge case where this can return null form query cell + + if (!isLocaleReady) { + return opts.loading?.(query.status === "pending" ? query : null) ?? StatusLoader; + } + if (query.status === "pending") { + return opts.loading?.(query) ?? StatusLoader; + } + + if (query.status === "success") { + if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) { + return opts.empty(query); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return opts.success(query as any); + } + + if (query.status === "error") { + return ( + opts.error?.(query) ?? ( + + ) + ); + } + + // impossible state + return null; +} diff --git a/packages/ui/components/app-list-card/AppListCard.tsx b/packages/ui/components/app-list-card/AppListCard.tsx new file mode 100644 index 00000000000000..81d6a144d9fe90 --- /dev/null +++ b/packages/ui/components/app-list-card/AppListCard.tsx @@ -0,0 +1,74 @@ +"use client"; + +import classNames from "@calcom/lib/classNames"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui"; + +import type { AppListCardProps } from "../../../../apps/web/components/AppListCard"; + +export const AppListCard = (props: AppListCardProps & { highlight?: boolean }) => { + const { t } = useLocale(); + const { + logo, + title, + description, + actions, + isDefault, + isTemplate, + invalidCredential, + children, + credentialOwner, + className, + highlight, + } = props; + + return ( +
+
+ {logo ? ( + {`${title} + ) : null} +
+
+

{title}

+
+ {isDefault && {t("default")}} + {isTemplate && Template} +
+
+ {description} + {invalidCredential && ( +
+ + + {t("invalid_credential")} + +
+ )} +
+ {credentialOwner && ( +
+ +
+ + {credentialOwner.name} +
+
+
+ )} + {actions} +
+
{children}
+
+ ); +}; diff --git a/packages/ui/components/app-list-card/index.ts b/packages/ui/components/app-list-card/index.ts new file mode 100644 index 00000000000000..03dc995e46278d --- /dev/null +++ b/packages/ui/components/app-list-card/index.ts @@ -0,0 +1 @@ +export { AppListCard } from "./AppListCard"; diff --git a/packages/ui/components/apps/AllApps.tsx b/packages/ui/components/apps/AllApps.tsx index 2f2e751a6ee37b..4e3a8befe5d81f 100644 --- a/packages/ui/components/apps/AllApps.tsx +++ b/packages/ui/components/apps/AllApps.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { AppCategories } from "@prisma/client"; import { usePathname, useRouter } from "next/navigation"; diff --git a/packages/ui/components/apps/AppCard.tsx b/packages/ui/components/apps/AppCard.tsx index d39445ebf1da1f..e9f074243953b8 100644 --- a/packages/ui/components/apps/AppCard.tsx +++ b/packages/ui/components/apps/AppCard.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; diff --git a/packages/ui/components/apps/Slider.tsx b/packages/ui/components/apps/Slider.tsx index a0d5cc63a3b3b7..07897f4e904e28 100644 --- a/packages/ui/components/apps/Slider.tsx +++ b/packages/ui/components/apps/Slider.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { Options } from "@glidejs/glide"; import Glide from "@glidejs/glide"; import "@glidejs/glide/dist/css/glide.core.min.css"; diff --git a/packages/ui/components/breadcrumb/Breadcrumb.tsx b/packages/ui/components/breadcrumb/Breadcrumb.tsx index d7c9f9d1d43716..04aa15316e0ac3 100644 --- a/packages/ui/components/breadcrumb/Breadcrumb.tsx +++ b/packages/ui/components/breadcrumb/Breadcrumb.tsx @@ -1,3 +1,5 @@ +"use client"; + import Link from "next/link"; import { usePathname } from "next/navigation"; import { Children, Fragment, useEffect, useState } from "react"; diff --git a/packages/ui/components/calendar-switch/CalendarSwitch.tsx b/packages/ui/components/calendar-switch/CalendarSwitch.tsx new file mode 100644 index 00000000000000..beb056a199ece7 --- /dev/null +++ b/packages/ui/components/calendar-switch/CalendarSwitch.tsx @@ -0,0 +1,43 @@ +"use client"; + +import type { ReactNode } from "react"; + +import { type ICalendarSwitchProps } from "@calcom/features/calendars/CalendarSwitch"; +import { classNames } from "@calcom/lib"; +import { Icon } from "@calcom/ui"; + +export function CalendarSwitchComponent( + props: ICalendarSwitchProps & { + isLoading: boolean; + children: ReactNode; + translations?: { + spanText?: string; + }; + } +) { + const { + externalId, + name, + isLoading, + translations = { + spanText: "Adding events to", + }, + children, + } = props; + + return ( +
+
{children}
+ + {!!props.destination && ( + + + {translations.spanText} + + )} + {isLoading && } +
+ ); +} diff --git a/packages/ui/components/calendar-switch/index.ts b/packages/ui/components/calendar-switch/index.ts new file mode 100644 index 00000000000000..942d6f9d35549d --- /dev/null +++ b/packages/ui/components/calendar-switch/index.ts @@ -0,0 +1 @@ +export { CalendarSwitchComponent } from "./CalendarSwitch"; diff --git a/packages/ui/components/credits/Credits.tsx b/packages/ui/components/credits/Credits.tsx index f51de9219daf63..fd27cbd371d3eb 100644 --- a/packages/ui/components/credits/Credits.tsx +++ b/packages/ui/components/credits/Credits.tsx @@ -1,3 +1,5 @@ +"use client"; + import Link from "next/link"; import { useEffect, useState } from "react"; diff --git a/packages/ui/components/data-table/index.tsx b/packages/ui/components/data-table/index.tsx index 4314e52f731632..ff0ac6e17c9d30 100644 --- a/packages/ui/components/data-table/index.tsx +++ b/packages/ui/components/data-table/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { ColumnDef, ColumnFiltersState, diff --git a/packages/ui/components/dialog/Dialog.tsx b/packages/ui/components/dialog/Dialog.tsx index 49b29659a943f1..2d88f9648aeb56 100644 --- a/packages/ui/components/dialog/Dialog.tsx +++ b/packages/ui/components/dialog/Dialog.tsx @@ -1,3 +1,5 @@ +"use client"; + import * as DialogPrimitive from "@radix-ui/react-dialog"; import { usePathname, useRouter } from "next/navigation"; import type { ForwardRefExoticComponent, ReactElement, ReactNode } from "react"; diff --git a/packages/ui/components/disconnect-calendar-integration/DisconnectIntegration.tsx b/packages/ui/components/disconnect-calendar-integration/DisconnectIntegration.tsx new file mode 100644 index 00000000000000..be3209877a0ee6 --- /dev/null +++ b/packages/ui/components/disconnect-calendar-integration/DisconnectIntegration.tsx @@ -0,0 +1,48 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { ButtonProps } from "@calcom/ui"; +import { Button, ConfirmationDialogContent, Dialog, DialogTrigger } from "@calcom/ui"; + +export const DisconnectIntegrationComponent = ({ + label, + trashIcon, + isGlobal, + isModalOpen = false, + onModalOpen, + onDeletionConfirmation, + buttonProps, +}: { + label?: string; + trashIcon?: boolean; + isGlobal?: boolean; + isModalOpen: boolean; + onModalOpen: () => void; + onDeletionConfirmation: () => void; + buttonProps?: ButtonProps; +}) => { + const { t } = useLocale(); + + return ( + <> + + + + + +

{t("are_you_sure_you_want_to_remove_this_app")}

+
+
+ + ); +}; diff --git a/packages/ui/components/disconnect-calendar-integration/index.ts b/packages/ui/components/disconnect-calendar-integration/index.ts new file mode 100644 index 00000000000000..9f7ce16c72086c --- /dev/null +++ b/packages/ui/components/disconnect-calendar-integration/index.ts @@ -0,0 +1 @@ +export { DisconnectIntegrationComponent } from "./DisconnectIntegration"; diff --git a/packages/ui/components/editable-heading/EditableHeading.tsx b/packages/ui/components/editable-heading/EditableHeading.tsx index b0dc1fd5c0c7ef..d221210532366e 100644 --- a/packages/ui/components/editable-heading/EditableHeading.tsx +++ b/packages/ui/components/editable-heading/EditableHeading.tsx @@ -1,3 +1,5 @@ +"use client"; + import classNames from "classnames"; import { useState } from "react"; import type { ControllerRenderProps } from "react-hook-form"; diff --git a/packages/ui/components/editor/plugins/AddVariablesPlugin.tsx b/packages/ui/components/editor/plugins/AddVariablesPlugin.tsx index 463fb289a8708e..83844720392240 100644 --- a/packages/ui/components/editor/plugins/AddVariablesPlugin.tsx +++ b/packages/ui/components/editor/plugins/AddVariablesPlugin.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { LexicalTypeaheadMenuPlugin, diff --git a/packages/ui/components/editor/plugins/ToolbarPlugin.tsx b/packages/ui/components/editor/plugins/ToolbarPlugin.tsx index 9e3a07036eb217..989d40bea8da27 100644 --- a/packages/ui/components/editor/plugins/ToolbarPlugin.tsx +++ b/packages/ui/components/editor/plugins/ToolbarPlugin.tsx @@ -1,3 +1,5 @@ +"use client"; + import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html"; import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; import { diff --git a/packages/ui/components/form/color-picker/colorpicker.tsx b/packages/ui/components/form/color-picker/colorpicker.tsx index b14528f0cc7b92..d30482c98e7fe8 100644 --- a/packages/ui/components/form/color-picker/colorpicker.tsx +++ b/packages/ui/components/form/color-picker/colorpicker.tsx @@ -1,3 +1,5 @@ +"use client"; + import * as Popover from "@radix-ui/react-popover"; import { useState } from "react"; import { HexColorInput, HexColorPicker } from "react-colorful"; diff --git a/packages/ui/components/form/inputs/Input.tsx b/packages/ui/components/form/inputs/Input.tsx index f31cbd122a1c76..4983ba4ea087cc 100644 --- a/packages/ui/components/form/inputs/Input.tsx +++ b/packages/ui/components/form/inputs/Input.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { ReactNode } from "react"; import React, { forwardRef, useCallback, useId, useState } from "react"; import { useFormContext } from "react-hook-form"; diff --git a/packages/ui/components/form/inputs/TextField.tsx b/packages/ui/components/form/inputs/TextField.tsx index 3bb93811544df6..fe665924b7b47e 100644 --- a/packages/ui/components/form/inputs/TextField.tsx +++ b/packages/ui/components/form/inputs/TextField.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { forwardRef, useId, useState } from "react"; import classNames from "@calcom/lib/classNames"; diff --git a/packages/ui/components/form/timezone-select/TimezoneSelect.tsx b/packages/ui/components/form/timezone-select/TimezoneSelect.tsx index 0cc6cb6818e938..438a0205b4d9fa 100644 --- a/packages/ui/components/form/timezone-select/TimezoneSelect.tsx +++ b/packages/ui/components/form/timezone-select/TimezoneSelect.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useMemo, useState } from "react"; import type { ITimezoneOption, ITimezone, Props as SelectProps } from "react-timezone-select"; import BaseSelect from "react-timezone-select"; diff --git a/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx b/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx index 3f3d05ef5c882b..b2182f2a08fb7c 100644 --- a/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx +++ b/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useId } from "@radix-ui/react-id"; import { Root as ToggleGroupPrimitive, Item as ToggleGroupItemPrimitive } from "@radix-ui/react-toggle-group"; import { useState } from "react"; diff --git a/packages/ui/components/form/wizard/WizardForm.tsx b/packages/ui/components/form/wizard/WizardForm.tsx index 5a67c52008cdc0..c51481daa0dbcb 100644 --- a/packages/ui/components/form/wizard/WizardForm.tsx +++ b/packages/ui/components/form/wizard/WizardForm.tsx @@ -1,3 +1,5 @@ +"use client"; + // eslint-disable-next-line no-restricted-imports import { noop } from "lodash"; import { useRouter } from "next/navigation"; diff --git a/packages/ui/components/image-uploader/BannerUploader.tsx b/packages/ui/components/image-uploader/BannerUploader.tsx index dea72ea30812a2..0dae88760ea495 100644 --- a/packages/ui/components/image-uploader/BannerUploader.tsx +++ b/packages/ui/components/image-uploader/BannerUploader.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useState, useEffect } from "react"; import Cropper from "react-easy-crop"; diff --git a/packages/ui/components/image-uploader/ImageUploader.tsx b/packages/ui/components/image-uploader/ImageUploader.tsx index 2c615e0f245999..f4a9020fbe3273 100644 --- a/packages/ui/components/image-uploader/ImageUploader.tsx +++ b/packages/ui/components/image-uploader/ImageUploader.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useState } from "react"; import Cropper from "react-easy-crop"; diff --git a/packages/ui/components/meta/Meta.tsx b/packages/ui/components/meta/Meta.tsx index b513ca6ae1d574..58ca372a16e86a 100644 --- a/packages/ui/components/meta/Meta.tsx +++ b/packages/ui/components/meta/Meta.tsx @@ -1,3 +1,5 @@ +"use client"; + import Head from "next/head"; import { createContext, useContext, useState, useEffect } from "react"; import type { ReactNode } from "react"; diff --git a/packages/ui/components/mocks/trpc.tsx b/packages/ui/components/mocks/trpc.tsx index b44c08b5afbe15..b81db79f09ca1b 100644 --- a/packages/ui/components/mocks/trpc.tsx +++ b/packages/ui/components/mocks/trpc.tsx @@ -1,3 +1,5 @@ +"use client"; + import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState, type PropsWithChildren } from "react"; diff --git a/packages/ui/components/scrollable/ScrollableArea.tsx b/packages/ui/components/scrollable/ScrollableArea.tsx index 23e3da94bdf811..0b52dd2d21a8c8 100644 --- a/packages/ui/components/scrollable/ScrollableArea.tsx +++ b/packages/ui/components/scrollable/ScrollableArea.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { PropsWithChildren } from "react"; import React, { useRef, useEffect, useState } from "react"; diff --git a/packages/ui/form/PhoneInput.tsx b/packages/ui/form/PhoneInput.tsx index 277ef9ea2826f8..aa031a2c743c1e 100644 --- a/packages/ui/form/PhoneInput.tsx +++ b/packages/ui/form/PhoneInput.tsx @@ -1,3 +1,5 @@ +"use client"; + import { isSupportedCountry } from "libphonenumber-js"; import { useState, useEffect } from "react"; import PhoneInput from "react-phone-input-2"; diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 4d1e9313c5c864..6a77c8a8e47495 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -140,6 +140,9 @@ export { ConfirmationContent, } from "./components/dialog"; export type { DialogProps, ConfirmationDialogContentProps } from "./components/dialog"; +export { AppListCard } from "./components/app-list-card"; +export { DisconnectIntegrationComponent } from "./components/disconnect-calendar-integration"; +export { CalendarSwitchComponent } from "./components/calendar-switch"; export { showToast } from "./components/toast"; // We don't export the toast components as they are only used in local storybook file export { Meta, MetaProvider, useMeta } from "./components/meta"; export { ShellSubHeading } from "./components/layout"; diff --git a/packages/ui/layouts/WizardLayout.tsx b/packages/ui/layouts/WizardLayout.tsx index 0ca7d4e43f8f29..f0178b0283b7ba 100644 --- a/packages/ui/layouts/WizardLayout.tsx +++ b/packages/ui/layouts/WizardLayout.tsx @@ -1,3 +1,5 @@ +"use client"; + // eslint-disable-next-line no-restricted-imports import { noop } from "lodash"; import { usePathname } from "next/navigation"; diff --git a/packages/ui/styles/useCalcomTheme.tsx b/packages/ui/styles/useCalcomTheme.tsx index 9dc215f0fad07e..60791dbda9e739 100644 --- a/packages/ui/styles/useCalcomTheme.tsx +++ b/packages/ui/styles/useCalcomTheme.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect } from "react"; type CssVariables = Record;