import { Maybe } from "@trpc/server"; import Image from "next/image"; import { ReactNode, useEffect, useState } from "react"; import { useMutation } from "react-query"; import { QueryCell } from "@lib/QueryCell"; import classNames from "@lib/classNames"; import { AddAppleIntegrationModal } from "@lib/integrations/Apple/components/AddAppleIntegration"; import { AddCalDavIntegrationModal } from "@lib/integrations/CalDav/components/AddCalDavIntegration"; import showToast from "@lib/notification"; import { inferQueryOutput, trpc } from "@lib/trpc"; import { Dialog } from "@components/Dialog"; import { List, ListItem, ListItemText, ListItemTitle } from "@components/List"; import Shell, { ShellSubHeading } from "@components/Shell"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import { Alert } from "@components/ui/Alert"; import Badge from "@components/ui/Badge"; import Button, { ButtonBaseProps } from "@components/ui/Button"; import Switch from "@components/ui/Switch"; type IntegrationCalendar = inferQueryOutput<"viewer.integrations">["calendar"]["items"][number]; function pluralize(opts: { num: number; plural: string; singular: string }) { if (opts.num === 0) { return opts.singular; } return opts.singular; } function SubHeadingTitleWithConnections(props: { title: ReactNode; numConnections?: number }) { const num = props.numConnections; return ( <> {props.title} {num ? ( {num}{" "} {pluralize({ num, singular: "connection", plural: "connections", })} ) : null} > ); } function ConnectIntegration(props: { type: IntegrationCalendar["type"]; render: (renderProps: ButtonBaseProps) => JSX.Element; }) { const { type } = props; const [isLoading, setIsLoading] = useState(false); const mutation = useMutation(async () => { const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add"); if (!res.ok) { throw new Error("Something went wrong"); } const json = await res.json(); window.location.href = json.url; setIsLoading(true); }); const [isModalOpen, setIsModalOpen] = useState(false); // refetch intergrations when modal closes const utils = trpc.useContext(); useEffect(() => { utils.invalidateQueries(["viewer.integrations"]); }, [isModalOpen, utils]); return ( <> {props.render({ onClick() { if (["caldav_calendar", "apple_calendar"].includes(type)) { // special handlers setIsModalOpen(true); return; } mutation.mutate(); }, loading: mutation.isLoading || isLoading, disabled: isModalOpen, })} {type === "caldav_calendar" && ( )} {type === "apple_calendar" && ( )} > ); } function DisconnectIntegration(props: { /** * Integration credential id */ id: number; render: (renderProps: ButtonBaseProps) => JSX.Element; }) { const utils = trpc.useContext(); const [modalOpen, setModalOpen] = useState(false); const mutation = useMutation( async () => { const res = await fetch("/api/integrations", { method: "DELETE", body: JSON.stringify({ id: props.id }), headers: { "Content-Type": "application/json", }, }); if (!res.ok) { throw new Error("Something went wrong"); } }, { async onSettled() { await utils.invalidateQueries(["viewer.integrations"]); }, onSuccess() { setModalOpen(false); }, } ); return ( <> { mutation.mutate(); }}> Are you sure you want to disconnect this integration? {props.render({ onClick() { setModalOpen(true); }, disabled: modalOpen, loading: mutation.isLoading, })} > ); } function ConnectOrDisconnectIntegrationButton(props: { // credential: Maybe<{ id: number }>; type: IntegrationCalendar["type"]; installed: boolean; }) { if (props.credential) { return ( ( Disconnect )} /> ); } if (!props.installed) { return ; } return ( Connect} /> ); } function IntegrationListItem(props: { imageSrc: string; title: string; description: string; actions?: ReactNode; children?: ReactNode; }) { return ( {props.title} {props.description} {props.actions} {props.children && {props.children}} ); } export function CalendarSwitch(props: { type: IntegrationCalendar["type"]; externalId: string; title: string; defaultSelected: boolean; }) { const utils = trpc.useContext(); const mutation = useMutation< unknown, unknown, { isOn: boolean; } >( async ({ isOn }) => { const body = { integration: props.type, externalId: props.externalId, }; if (isOn) { const res = await fetch("/api/availability/calendar", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) { throw new Error("Something went wrong"); } } else { const res = await fetch("/api/availability/calendar", { method: "DELETE", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) { throw new Error("Something went wrong"); } } }, { async onSettled() { await utils.invalidateQueries(["viewer.integrations"]); }, onError() { showToast(`Something went wrong when toggling "${props.title}""`, "error"); }, } ); return ( { mutation.mutate({ isOn }); }} /> ); } export default function IntegrationsPage() { const query = trpc.useQuery(["viewer.integrations"]); return ( { return ( <> } /> {data.conferencing.items.map((item) => ( } /> ))} } /> {data.payment.items.map((item) => ( } /> ))} } subtitle={ <> Configure how your links integrate with your calendars. You can override these settings on a per event basis. > } /> {data.connectedCalendars.length > 0 && ( <> {data.connectedCalendars.map((item, index) => ( {item.calendars ? ( ( Disconnect )} /> }> {item.calendars.map((cal) => ( ))} ) : ( ( Disconnect )} /> } /> )} ))} Connect an additional calendar > )} {data.calendar.items.map((item) => ( Connect} /> } /> ))} > ); }} /> ); }