Files
calendar/apps/web/modules/shell/Shell.tsx
T
Udit TakkarGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
267686704d fix: availability page UI (#27825)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 09:06:21 +00:00

284 lines
8.9 KiB
TypeScript

"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import type { Dispatch, ReactElement, ReactNode, SetStateAction } from "react";
import type React from "react";
import { cloneElement } from "react";
import { Toaster } from "sonner";
import { useFormbricks } from "@calcom/web/modules/formbricks/hooks/useFormbricks";
import { useRedirectToLoginIfUnauthenticated } from "@calcom/web/modules/auth/hooks/useRedirectToLoginIfUnauthenticated";
import { useRedirectToOnboardingIfNeeded } from "@calcom/web/modules/auth/hooks/useRedirectToOnboardingIfNeeded";
import TimezoneChangeDialog from "@calcom/web/modules/settings/components/TimezoneChangeDialog";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { DynamicModals } from "./DynamicModals";
import { KBarContent, KBarRoot } from "./Kbar";
import { SideBarContainer } from "./SideBar";
import { TopNavContainer } from "./TopNav";
import { BannerContainer } from "./banners/LayoutBanner";
import { useBanners } from "./banners/useBanners";
import { MobileNavigationContainer } from "./navigation/Navigation";
import { useAppTheme } from "./useAppTheme";
const Layout = (props: LayoutProps) => {
const { banners, bannersHeight } = useBanners();
useFormbricks();
return (
<>
<div>
<Toaster position="bottom-right" />
</div>
<TimezoneChangeDialog />
<DynamicModals />
<div className="flex min-h-screen flex-col">
{banners && !props.isPlatformUser && (
<BannerContainer banners={banners} />
)}
<div className="flex flex-1" data-testid="dashboard-shell">
{props.SidebarContainer ? (
cloneElement(props.SidebarContainer, { bannersHeight })
) : (
<SideBarContainer
isPlatformUser={props.isPlatformUser}
bannersHeight={bannersHeight}
/>
)}
<div className="flex w-0 flex-1 flex-col">
<MainContainer {...props} />
</div>
</div>
</div>
</>
);
};
type DrawerState = [
isOpen: boolean,
setDrawerOpen: Dispatch<SetStateAction<boolean>>
];
export type LayoutProps = {
centered?: boolean;
title?: string;
description?: string;
heading?: ReactNode;
subtitle?: ReactNode;
headerClassName?: string;
children: ReactNode;
CTA?: ReactNode;
large?: boolean;
MobileNavigationContainer?: ReactNode;
SidebarContainer?: ReactElement;
TopNavContainer?: ReactNode;
drawerState?: DrawerState;
HeadingLeftIcon?: ReactNode;
backPath?: string | boolean; // renders back button to specified path
// use when content needs to expand with flex
flexChildrenContainer?: boolean;
isPublic?: boolean;
withoutMain?: boolean;
// Gives the ability to include actions to the right of the heading
actions?: JSX.Element;
beforeCTAactions?: JSX.Element;
afterHeading?: ReactNode;
smallHeading?: boolean;
isPlatformUser?: boolean;
disableSticky?: boolean;
topAlignedHeading?: boolean;
};
const KBarWrapper = ({
children,
withKBar = false,
}: {
withKBar: boolean;
children: React.ReactNode;
}) =>
withKBar ? (
<KBarRoot>
{children}
<KBarContent />
</KBarRoot>
) : (
<>{children}</>
);
const PublicShell = (props: LayoutProps) => {
const { status } = useSession();
return (
<KBarWrapper withKBar={status === "authenticated"}>
<Layout {...props} />
</KBarWrapper>
);
};
export default function Shell(props: LayoutProps) {
// if a page is unauthed and isPublic is true, the redirect does not happen.
useRedirectToLoginIfUnauthenticated(props.isPublic);
useRedirectToOnboardingIfNeeded();
useAppTheme();
return !props.isPublic ? (
<KBarWrapper withKBar>
<Layout {...props} />
</KBarWrapper>
) : (
<PublicShell {...props} />
);
}
export function ShellMain(props: LayoutProps) {
const router = useRouter();
const { isLocaleReady } = useLocale();
const { bannersHeight } = useBanners();
const headerStyle =
!props.disableSticky && bannersHeight
? { top: `${bannersHeight}px` as const }
: undefined;
return (
<>
{(props.heading || !!props.backPath) && (
<div
style={headerStyle}
className={classNames(
"bg-default mb-0 flex md:mb-6 md:mt-0",
props.smallHeading ? "lg:mb-7" : "lg:mb-8",
props.topAlignedHeading ? "items-start" : "items-center",
!props.disableSticky &&
"sticky top-0 z-10 md:fixed md:w-[calc(100%-3.5rem)] md:pt-3 md:pb-2 md:px-5 md:left-14 lg:left-56 lg:w-[calc(100%-14rem)]"
)}
>
{!!props.backPath && (
<Button
variant="icon"
size="sm"
color="minimal"
onClick={() =>
typeof props.backPath === "string"
? router.push(props.backPath as string)
: router.back()
}
StartIcon="arrow-left"
aria-label="Go Back"
className="rounded-md ltr:mr-2 rtl:ml-2"
data-testid="go-back-button"
/>
)}
{props.heading && (
<header
className={classNames(
props.large && "py-8",
"flex w-full max-w-full truncate",
props.topAlignedHeading ? "items-start" : "items-center"
)}
>
{props.HeadingLeftIcon && (
<div className="ltr:mr-4">{props.HeadingLeftIcon}</div>
)}
<div
className={classNames(
"w-full truncate ltr:mr-4 rtl:ml-4 md:block",
props.headerClassName
)}
>
{props.heading && (
<h3
className={classNames(
"font-cal text-emphasis max-w-28 sm:max-w-72 md:max-w-80 hidden truncate text-lg font-semibold tracking-wide sm:text-xl md:block xl:max-w-full",
props.smallHeading ? "text-base" : "text-xl"
)}
>
{!isLocaleReady ? (
<SkeletonText invisible />
) : (
props.heading
)}
</h3>
)}
{props.subtitle && (
<p
className="text-default hidden text-sm md:block"
data-testid="subtitle"
>
{!isLocaleReady ? (
<SkeletonText invisible />
) : (
props.subtitle
)}
</p>
)}
</div>
{props.beforeCTAactions}
{props.CTA && (
<div
className={classNames(
props.backPath
? "relative"
: "pwa:bottom-[max(7rem,_calc(5rem_+_env(safe-area-inset-bottom)))] fixed bottom-20 z-40 ltr:right-4 rtl:left-4 md:z-auto md:ltr:right-0 md:rtl:left-0",
"shrink-0 [-webkit-app-region:no-drag] md:relative md:bottom-auto md:right-auto"
)}
>
{isLocaleReady && props.CTA}
</div>
)}
{props.actions && props.actions}
</header>
)}
</div>
)}
{(props.heading || !!props.backPath) && !props.disableSticky && (
<div className="hidden md:block md:h-14" aria-hidden="true" />
)}
{props.afterHeading && <>{props.afterHeading}</>}
<div
className={classNames(
props.flexChildrenContainer && "flex flex-1 flex-col"
)}
>
{props.children}
</div>
</>
);
}
function MainContainer({
isPlatformUser,
MobileNavigationContainer: MobileNavigationContainerProp = (
<MobileNavigationContainer isPlatformNavigation={isPlatformUser} />
),
TopNavContainer: TopNavContainerProp = <TopNavContainer />,
...props
}: LayoutProps) {
return (
<main className="bg-default relative z-0 flex-1 pb-8 focus:outline-none">
{/* show top navigation for md and smaller (tablet and phones) */}
{TopNavContainerProp}
<div className="max-w-full p-2 sm:py-4 lg:px-6">
<ErrorBoundary>
{!props.withoutMain ? (
<ShellMain {...props}>{props.children}</ShellMain>
) : (
props.children
)}
</ErrorBoundary>
{/* show bottom navigation for md and smaller (tablet and phones) on pages where back button doesn't exist */}
{!props.backPath ? MobileNavigationContainerProp : null}
</div>
</main>
);
}