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 ? (
-
- ) : 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 })}
+ 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 ? (
+
+ ) : 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 (
+ <>
+
+ >
+ );
+};
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;