import type { User, UserPlan } from "@prisma/client"; import { SessionContextValue, 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 { DESKTOP_APP_LINK, JOIN_SLACK, ROADMAP, WEBAPP_URL } from "@calcom/lib/constants"; import useApp from "@calcom/lib/hooks/useApp"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import Badge from "@calcom/ui/Badge"; import Button from "@calcom/ui/Button"; import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@calcom/ui/Dropdown"; import { CollectionIcon, Icon } from "@calcom/ui/Icon"; import Loader from "@calcom/ui/Loader"; import { HeadSeo } from "@calcom/ui/v2/core/head-seo"; import { useViewerI18n } from "@calcom/web/components/I18nLanguageHandler"; /* TODO: Get this from endpoint */ import pkg from "../../apps/web/package.json"; import ErrorBoundary from "./ErrorBoundary"; import { KBarContent, KBarRoot, KBarTrigger } from "./Kbar"; import Logo from "./Logo"; 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 = ({ status, plan, ...props }: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => { const isEmbed = useIsEmbed(); const router = useRouter(); const { data: routingForms } = useApp("routing_forms"); const { t } = useLocale(); const navigation = [ { name: t("event_types_page_title"), href: "/event-types", icon: Icon.FiLink, current: router.asPath.startsWith("/event-types"), }, { name: t("bookings"), href: "/bookings/upcoming", icon: Icon.FiCalendar, current: router.asPath.startsWith("/bookings"), }, { name: t("availability"), href: "/availability", icon: Icon.FiClock, current: router.asPath.startsWith("/availability"), }, routingForms ? { name: "Routing Forms", href: "/apps/routing_forms/forms", icon: CollectionIcon, current: router.asPath.startsWith("/apps/routing_forms/"), } : null, { name: t("workflows"), href: "/workflows", icon: Icon.FiZap, current: router.asPath.startsWith("/workflows"), pro: true, }, { name: t("apps"), href: "/apps", icon: Icon.FiGrid, current: router.asPath.startsWith("/apps") && !router.asPath.startsWith("/apps/routing_forms/"), child: [ { name: t("app_store"), href: "/apps", current: router.asPath === "/apps", }, { name: t("installed_apps"), href: "/apps/installed", current: router.asPath === "/apps/installed", }, ], }, { name: t("settings"), href: "/settings/profile", icon: Icon.FiSettings, current: router.asPath.startsWith("/settings"), }, ]; const pageTitle = typeof props.heading === "string" ? props.heading : props.title; return ( <>
{status === "authenticated" && (
{/* logo icon for tablet */}
)}
{/* show top navigation for md and smaller (tablet and phones) */} {status === "authenticated" && ( )}
{!!props.backPath && (
)} {props.heading && (
{props.HeadingLeftIcon &&
{props.HeadingLeftIcon}
}
{props.isLoading ? ( <>
) : ( <>

{props.heading}

{props.subtitle}

)}
{props.CTA &&
{props.CTA}
}
)}
{!props.isLoading ? props.children : props.customLoader}
{/* show bottom navigation for md and smaller (tablet and phones) */} {status === "authenticated" && ( )} {/* add padding to content for mobile navigation*/}
); }; const MemoizedLayout = React.memo(Layout); type LayoutProps = { centered?: boolean; title?: string; heading?: ReactNode; subtitle?: ReactNode; children: ReactNode; CTA?: ReactNode; large?: boolean; HeadingLeftIcon?: ReactNode; backPath?: string; // renders back button to specified path // use when content needs to expand with flex flexChildrenContainer?: boolean; isPublic?: boolean; customLoader?: ReactNode; }; export default function Shell(props: LayoutProps) { const { loading, session } = useRedirectToLoginIfUnauthenticated(props.isPublic); const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded(); const query = useMeQuery(); const user = query.data; const i18n = useViewerI18n(); const { status } = useSession(); const isLoading = isRedirectingToOnboarding || loading; // Don't show any content till translations are loaded. // As they are cached infintely, this status would be loading just once for the app's lifetime until refresh if (i18n.status === "loading") { return (
); } if (!session && !props.isPublic) return null; 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")} {" "} {t("download_desktop_app")} signOut({ callbackUrl: "/auth/logout" })} className="flex cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"> )} ); } 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} ); }