import type { User } from "@prisma/client"; import { signOut, useSession } from "next-auth/react"; import dynamic from "next/dynamic"; import Link from "next/link"; import type { NextRouter } from "next/router"; import { useRouter } from "next/router"; import type { Dispatch, ReactNode, SetStateAction } from "react"; import React, { Fragment, useEffect, useState } from "react"; import { Toaster } from "react-hot-toast"; import dayjs from "@calcom/dayjs"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge"; import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner"; import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; import { TeamsUpgradeBanner } from "@calcom/features/ee/teams/components"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar"; import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog"; import AdminPasswordBanner from "@calcom/features/users/components/AdminPasswordBanner"; import classNames from "@calcom/lib/classNames"; import { APP_NAME, DESKTOP_APP_LINK, JOIN_SLACK, ROADMAP, WEBAPP_URL } from "@calcom/lib/constants"; import getBrandColours from "@calcom/lib/getBrandColours"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { isKeyInObject } from "@calcom/lib/isKeyInObject"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import type { SVGComponent } from "@calcom/types/SVGComponent"; import { Button, Credits, Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuTrigger, ErrorBoundary, HeadSeo, Logo, SkeletonText, showToast, useCalcomTheme, } from "@calcom/ui"; import { ArrowLeft, ArrowRight, BarChart, Calendar, Clock, Download, ExternalLink, FileText, Grid, HelpCircle, Link as LinkIcon, LogOut, Map, Moon, MoreHorizontal, MoreVertical, Settings, Slack, Users, Zap, } from "@calcom/ui/components/icon"; import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider"; import { TeamInviteBadge } from "./TeamInviteBadge"; // need to import without ssr to prevent hydration errors const Tips = dynamic(() => import("@calcom/features/tips").then((mod) => mod.Tips), { ssr: false, }); /* 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, }; } const Layout = (props: LayoutProps) => { const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title; return ( <> {!props.withoutSeo && ( )}
{/* todo: only run this if timezone is different */}
{props.SidebarContainer || }
); }; type DrawerState = [isOpen: boolean, setDrawerOpen: Dispatch>]; type LayoutProps = { centered?: boolean; title?: string; heading?: ReactNode; subtitle?: ReactNode; headerClassName?: string; children: ReactNode; CTA?: ReactNode; large?: boolean; MobileNavigationContainer?: ReactNode; SidebarContainer?: ReactNode; 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 you the option to skip HeadSEO and render your own. withoutSeo?: boolean; // Gives the ability to include actions to the right of the heading actions?: JSX.Element; smallHeading?: boolean; hideHeadingOnMobile?: boolean; }; const useBrandColors = () => { const { data: user } = useMeQuery(); const brandTheme = getBrandColours({ lightVal: user?.brandColor, darkVal: user?.darkBrandColor, }); useCalcomTheme(brandTheme); }; const KBarWrapper = ({ children, withKBar = false }: { withKBar: boolean; children: React.ReactNode }) => withKBar ? ( {children} ) : ( <>{children} ); const PublicShell = (props: LayoutProps) => { const { status } = useSession(); return ( ); }; export default function Shell(props: LayoutProps) { // if a page is unauthed and isPublic is true, the redirect does not happen. useRedirectToLoginIfUnauthenticated(props.isPublic); useRedirectToOnboardingIfNeeded(); // System Theme is automatically supported using ThemeProvider. If we intend to use user theme throughout the app we need to uncomment this. // useTheme(profile.theme); useBrandColors(); return !props.isPublic ? ( ) : ( ); } 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.viewer.away.useMutation({ onSettled() { utils.viewer.me.invalidate(); }, }); 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 (
setMenuOpen((menuOpen) => !menuOpen)}>
{ setMenuOpen(false); setHelpOpen(false); }} className="group overflow-hidden rounded-md"> {helpOpen ? ( onHelpItemSelect()} /> ) : ( <> ( {user.username && ( <> {t("view_public_page")} { e.preventDefault(); navigator.clipboard.writeText( `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}` ); showToast(t("link_copied"), "success"); }}> {t("copy_public_page_link")} )} } target="_blank" rel="noreferrer" href={JOIN_SLACK}> {t("join_our_slack")} {t("visit_roadmap")} {t("download_desktop_app")} {t("settings")} )}
); } export type NavigationItemType = { name: string; href: string; badge?: React.ReactNode; icon?: SVGComponent; child?: NavigationItemType[]; pro?: true; onlyMobile?: boolean; onlyDesktop?: boolean; isCurrent?: ({ item, isChild, router, }: { item: NavigationItemType; isChild?: boolean; router: NextRouter; }) => boolean; }; const requiredCredentialNavigationItems = ["Routing Forms"]; const MORE_SEPARATOR_NAME = "more"; const navigation: NavigationItemType[] = [ { name: "event_types_page_title", href: "/event-types", icon: LinkIcon, }, { name: "bookings", href: "/bookings/upcoming", icon: Calendar, badge: , isCurrent: ({ router }) => { const path = router.asPath.split("?")[0]; return path.startsWith("/bookings"); }, }, { name: "availability", href: "/availability", icon: Clock, }, { name: "teams", href: "/teams", icon: Users, onlyDesktop: true, badge: , }, { name: "apps", href: "/apps", icon: Grid, isCurrent: ({ router, item }) => { const path = router.asPath.split("?")[0]; // During Server rendering path is /v2/apps but on client it becomes /apps(weird..) return ( (path.startsWith(item.href) || path.startsWith("/v2" + item.href)) && !path.includes("routing-forms/") ); }, child: [ { name: "app_store", href: "/apps", isCurrent: ({ router, item }) => { const path = router.asPath.split("?")[0]; // During Server rendering path is /v2/apps but on client it becomes /apps(weird..) return ( (path.startsWith(item.href) || path.startsWith("/v2" + item.href)) && !path.includes("routing-forms/") && !path.includes("/installed") ); }, }, { name: "installed_apps", href: "/apps/installed/calendar", isCurrent: ({ router }) => { const path = router.asPath; return path.startsWith("/apps/installed/") || path.startsWith("/v2/apps/installed/"); }, }, ], }, { name: MORE_SEPARATOR_NAME, href: "/more", icon: MoreHorizontal, }, { name: "Routing Forms", href: "/apps/routing-forms/forms", icon: FileText, isCurrent: ({ router }) => { return router.asPath.startsWith("/apps/routing-forms/"); }, }, { name: "workflows", href: "/workflows", icon: Zap, }, { name: "insights", href: "/insights", icon: BarChart, }, { name: "settings", href: "/settings/my-account/profile", icon: Settings, }, ]; const moreSeparatorIndex = navigation.findIndex((item) => item.name === MORE_SEPARATOR_NAME); // We create all needed navigation items for the different use cases const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems } = navigation.reduce< Record >( (items, item, index) => { // We filter out the "more" separator in` desktop navigation if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item); // Items for mobile bottom navigation if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) items.mobileNavigationBottomItems.push(item); // Items for the "more" menu in mobile navigation else items.mobileNavigationMoreItems.push(item); return items; }, { desktopNavigationItems: [], mobileNavigationBottomItems: [], mobileNavigationMoreItems: [] } ); const Navigation = () => { return ( ); }; function useShouldDisplayNavigationItem(item: NavigationItemType) { const { status } = useSession(); const { data: routingForms } = trpc.viewer.appById.useQuery( { appId: "routing-forms" }, { enabled: status === "authenticated" && requiredCredentialNavigationItems.includes(item.name), trpc: {}, } ); const flags = useFlagMap(); if (isKeyInObject(item.name, flags)) return flags[item.name]; return !requiredCredentialNavigationItems.includes(item.name) || routingForms?.isInstalled; } const defaultIsCurrent: NavigationItemType["isCurrent"] = ({ isChild, item, router }) => { return isChild ? item.href === router.asPath : router.asPath.startsWith(item.href); }; const NavigationItem: React.FC<{ index?: number; item: NavigationItemType; isChild?: boolean; }> = (props) => { const { item, isChild } = props; const { t, isLocaleReady } = useLocale(); const router = useRouter(); const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent; const current = isCurrent({ isChild: !!isChild, item, router }); const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item); if (!shouldDisplayNavigationItem) return null; return ( {item.icon && ( ); }; 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; isChild?: boolean; }> = (props) => { const { item, isChild } = props; const router = useRouter(); const { t, isLocaleReady } = useLocale(); const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent; const current = isCurrent({ isChild: !!isChild, item, router }); const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item); if (!shouldDisplayNavigationItem) return null; return ( {item.badge &&
{item.badge}
} {item.icon && (