import type { User } from "@prisma/client"; import { signOut, useSession } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/router"; import React, { Fragment, ReactNode, useEffect, useState } from "react"; import { Toaster } from "react-hot-toast"; import dayjs from "@calcom/dayjs"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import LicenseBanner from "@calcom/features/ee/common/components/LicenseBanner"; import TrialBanner from "@calcom/features/ee/common/components/TrialBanner"; import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner"; import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; import CustomBranding from "@calcom/lib/CustomBranding"; import classNames from "@calcom/lib/classNames"; import { JOIN_SLACK, ROADMAP, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { SVGComponent } from "@calcom/types/SVGComponent"; import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@calcom/ui/Dropdown"; import { CollectionIcon, Icon } from "@calcom/ui/Icon"; /* TODO: Get this from endpoint */ import pkg from "../../../../apps/web/package.json"; import ErrorBoundary from "../../ErrorBoundary"; import { KBarRoot, KBarContent, KBarTrigger } from "../../Kbar"; import Logo from "../../Logo"; import Tips from "../modules/tips/Tips"; import Card from "./Card"; import HeadSeo from "./head-seo"; /* TODO: Migate this */ export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString(); export const ONBOARDING_NEXT_REDIRECT = { redirect: { permanent: false, destination: "/getting-started", }, } as const; export const shouldShowOnboarding = (user: Pick) => { return !user.completedOnboarding && dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT); }; function useRedirectToLoginIfUnauthenticated(isPublic = false) { const { data: session, status } = useSession(); const loading = status === "loading"; const router = useRouter(); useEffect(() => { if (isPublic) { return; } if (!loading && !session) { router.replace({ pathname: "/auth/login", query: { callbackUrl: `${WEBAPP_URL}${location.pathname}${location.search}`, }, }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [loading, session, isPublic]); return { loading: loading && !session, session, }; } function useRedirectToOnboardingIfNeeded() { const router = useRouter(); const query = useMeQuery(); const user = query.data; const isRedirectingToOnboarding = user && shouldShowOnboarding(user); useEffect(() => { if (isRedirectingToOnboarding) { router.replace({ pathname: "/getting-started", }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRedirectingToOnboarding]); return { isRedirectingToOnboarding, }; } export function ShellSubHeading(props: { title: ReactNode; subtitle?: ReactNode; actions?: ReactNode; className?: string; }) { return (

{props.title}

{props.subtitle &&

{props.subtitle}

}
{props.actions &&
{props.actions}
}
); } const Layout = (props: LayoutProps) => { const pageTitle = typeof props.heading === "string" ? props.heading : props.title; return ( <>
{props.SidebarContainer || }
); }; type LayoutProps = { centered?: boolean; title?: string; heading?: ReactNode; subtitle?: ReactNode; children: ReactNode; CTA?: ReactNode; large?: boolean; SidebarContainer?: ReactNode; HeadingLeftIcon?: ReactNode; backPath?: string; // renders back button to specified path // use when content needs to expand with flex flexChildrenContainer?: boolean; isPublic?: boolean; customLoader?: ReactNode; }; const CustomBrandingContainer = () => { const { data: user } = useMeQuery(); return ; }; export default function Shell(props: LayoutProps) { useRedirectToLoginIfUnauthenticated(props.isPublic); useRedirectToOnboardingIfNeeded(); useTheme("light"); return ( ); } function UserDropdown({ small }: { small?: boolean }) { const { t } = useLocale(); const query = useMeQuery(); const user = query.data; useEffect(() => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore const Beacon = window.Beacon; // window.Beacon is defined when user actually opens up HelpScout and username is available here. On every re-render update session info, so that it is always latest. Beacon && Beacon("session-data", { username: user?.username || "Unknown", screenResolution: `${screen.width}x${screen.height}`, }); }); const mutation = trpc.useMutation("viewer.away", { onSettled() { utils.invalidateQueries("viewer.me"); }, }); const utils = trpc.useContext(); const [helpOpen, setHelpOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false); if (!user) { return null; } const onHelpItemSelect = () => { setHelpOpen(false); setMenuOpen(false); }; // Prevent rendering dropdown if user isn't available. // We don't want to show nameless user. if (!user) { return null; } return ( setHelpOpen(false)}> setMenuOpen(true)}> setMenuOpen(false)}> {helpOpen ? ( onHelpItemSelect()} /> ) : ( <> { mutation.mutate({ away: !user?.away }); utils.invalidateQueries("viewer.me"); }} className="flex min-w-max cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"> {user.username && ( {" "} {t("view_public_page")} )} {" "} {t("join_our_slack")} {t("visit_roadmap")} signOut({ callbackUrl: "/auth/logout" })} className="flex cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"> )} ); } type NavigationItemType = { name: string; href: string; icon?: SVGComponent; child?: NavigationItemType[]; pro?: true; }; const navigation: NavigationItemType[] = [ { name: "event_types_page_title", href: "/event-types", icon: Icon.FiLink, }, { name: "bookings", href: "/bookings/upcoming", icon: Icon.FiCalendar, }, { name: "availability", href: "/availability", icon: Icon.FiClock, }, { name: "Routing Forms", href: "/apps/routing_forms/forms", icon: CollectionIcon, }, { name: "workflows", href: "/workflows", icon: Icon.FiZap, pro: true, }, { name: "apps", href: "/apps", icon: Icon.FiGrid, child: [ { name: "app_store", href: "/apps", }, { name: "installed_apps", href: "/apps/installed", }, ], }, { name: "settings", href: "/settings", icon: Icon.FiSettings, }, ]; const requiredCredentialNavigationItems = ["Routing Forms"]; const Navigation = () => { return ( ); }; function useShouldDisplayNavigationItem(item: NavigationItemType) { const { status } = useSession(); const { data: routingForms } = trpc.useQuery(["viewer.appById", { appId: "routing_forms" }], { enabled: status === "authenticated" && requiredCredentialNavigationItems.includes(item.name), }); return !requiredCredentialNavigationItems.includes(item.name) || !!routingForms; } const NavigationItem: React.FC<{ item: NavigationItemType; isChild?: boolean; }> = (props) => { const { item, isChild } = props; const { t } = useLocale(); const router = useRouter(); const current = isChild ? item.href === router.asPath : router.asPath.startsWith(item.href); const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item); if (!shouldDisplayNavigationItem) return null; return ( {item.child && router.asPath.startsWith(item.href) && item.child.map((item) => )} ); }; function MobileNavigationContainer() { const { status } = useSession(); if (status !== "authenticated") return null; return ; } const MobileNavigation = () => { const isEmbed = useIsEmbed(); return ( <> {/* add padding to content for mobile navigation*/}
); }; const MobileNavigationItem: React.FC<{ item: NavigationItemType; itemIdx: number; isChild?: boolean; }> = (props) => { const { item, itemIdx, isChild } = props; const router = useRouter(); const { t } = useLocale(); const current = isChild ? item.href === router.asPath : router.asPath.startsWith(item.href); const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item); if (!shouldDisplayNavigationItem) return null; return ( {item.icon && ( ); }; function DeploymentInfo() { const query = useMeQuery(); const user = query.data; return ( © {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"} {process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh"} -{user?.plan} ); } function SideBarContainer() { const { status } = useSession(); const router = useRouter(); if (status !== "authenticated") return null; if (router.route.startsWith("/v2/settings/")) return null; return ; } function SideBar() { const [visible, setVisible] = useState(true); const { t } = useLocale(); return ( ); } function MainContainer(props: LayoutProps) { return (
{/* show top navigation for md and smaller (tablet and phones) */} {props.heading && (
{props.HeadingLeftIcon &&
{props.HeadingLeftIcon}
}
<> {props.heading && (

{props.heading}

)} {props.subtitle && (

{props.subtitle}

)}
{props.CTA &&
{props.CTA}
}
)}
{props.children}
{/* show bottom navigation for md and smaller (tablet and phones) */}
); } function TopNavContainer() { const { status } = useSession(); if (status !== "authenticated") return null; return ; } function TopNav() { const isEmbed = useIsEmbed(); const { t } = useLocale(); return ( ); }