284 lines
8.9 KiB
TypeScript
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>
|
|
);
|
|
}
|