2022-08-09 09:21:15 +00:00
import type { User } from "@prisma/client" ;
2022-09-07 01:33:50 +00:00
import noop from "lodash/noop" ;
2022-08-09 09:21:15 +00:00
import { signOut , useSession } from "next-auth/react" ;
import Link from "next/link" ;
2022-09-02 19:00:41 +00:00
import { NextRouter , useRouter } from "next/router" ;
2022-09-07 01:33:50 +00:00
import React , { Dispatch , Fragment , ReactNode , SetStateAction , useEffect , useState } from "react" ;
2022-08-09 09:21:15 +00:00
import { Toaster } from "react-hot-toast" ;
import dayjs from "@calcom/dayjs" ;
import { useIsEmbed } from "@calcom/embed-core/embed-iframe" ;
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" ;
2022-09-05 20:25:21 +00:00
import UserV2OptInBanner from "@calcom/features/users/components/UserV2OptInBanner" ;
2022-08-09 09:21:15 +00:00
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" ;
2022-09-02 19:00:41 +00:00
import { Icon } from "@calcom/ui/Icon" ;
2022-08-09 09:21:15 +00:00
/* TODO: Get this from endpoint */
2022-08-24 20:18:42 +00:00
import pkg from "../../../../apps/web/package.json" ;
import ErrorBoundary from "../../ErrorBoundary" ;
2022-09-05 19:06:34 +00:00
import { KBarContent , KBarRoot , KBarTrigger } from "../../Kbar" ;
2022-08-24 20:18:42 +00:00
import Logo from "../../Logo" ;
2022-08-29 12:04:39 +00:00
// TODO: re-introduce in 2.1 import Tips from "../modules/tips/Tips";
2022-08-24 20:18:42 +00:00
import HeadSeo from "./head-seo" ;
2022-09-05 19:06:34 +00:00
import { SkeletonText } from "./skeleton" ;
2022-08-24 20:18:42 +00:00
/* TODO: Migate this */
2022-08-09 09:21:15 +00:00
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 < User , " createdDate " | " completedOnboarding " > ) = > {
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 (
< div className = { classNames ( "mb-3 block justify-between sm:flex" , props . className ) } >
< div >
< h2 className = "flex content-center items-center space-x-2 text-base font-bold leading-6 text-gray-900 rtl:space-x-reverse" >
{ props . title }
< / h2 >
{ props . subtitle && < p className = "text-sm text-neutral-500 ltr:mr-4" > { props . subtitle } < / p > }
< / div >
{ props . actions && < div className = "flex-shrink-0" > { props . actions } < / div > }
< / div >
) ;
}
const Layout = ( props : LayoutProps ) = > {
const pageTitle = typeof props . heading === "string" ? props.heading : props.title ;
return (
< >
< HeadSeo
title = { pageTitle ? ? "Cal.com" }
description = { props . subtitle ? props . subtitle ? . toString ( ) : "" }
nextSeoProps = { {
nofollow : true ,
noindex : true ,
} }
/ >
< div >
< Toaster position = "bottom-right" / >
< / div >
2022-09-06 19:34:58 +00:00
< div className = "flex h-screen overflow-hidden" data - testid = "dashboard-shell" >
{ props . SidebarContainer || < SideBarContainer / > }
2022-08-09 09:21:15 +00:00
< div className = "flex w-0 flex-1 flex-col overflow-hidden" >
2022-09-05 20:25:21 +00:00
< UserV2OptInBanner / >
2022-08-09 09:21:15 +00:00
< ImpersonatingBanner / >
< MainContainer { ...props } / >
< / div >
< / div >
< / >
) ;
} ;
2022-09-07 01:33:50 +00:00
type DrawerState = [ isOpen : boolean , setDrawerOpen : Dispatch < SetStateAction < boolean > > ] ;
2022-08-09 09:21:15 +00:00
type LayoutProps = {
centered? : boolean ;
title? : string ;
heading? : ReactNode ;
subtitle? : ReactNode ;
children : ReactNode ;
CTA? : ReactNode ;
large? : boolean ;
2022-09-07 01:33:50 +00:00
SettingsSidebarContainer? : ReactNode ;
MobileNavigationContainer? : ReactNode ;
2022-08-26 00:11:41 +00:00
SidebarContainer? : ReactNode ;
2022-09-07 01:33:50 +00:00
TopNavContainer? : ReactNode ;
drawerState? : DrawerState ;
2022-08-09 09:21:15 +00:00
HeadingLeftIcon? : ReactNode ;
backPath? : string ; // renders back button to specified path
// use when content needs to expand with flex
flexChildrenContainer? : boolean ;
isPublic? : boolean ;
2022-09-02 19:00:41 +00:00
withoutMain? : boolean ;
2022-08-09 09:21:15 +00:00
} ;
const CustomBrandingContainer = ( ) = > {
const { data : user } = useMeQuery ( ) ;
return < CustomBranding lightVal = { user ? . brandColor } darkVal = { user ? . darkBrandColor } / > ;
} ;
export default function Shell ( props : LayoutProps ) {
useRedirectToLoginIfUnauthenticated ( props . isPublic ) ;
useRedirectToOnboardingIfNeeded ( ) ;
useTheme ( "light" ) ;
2022-09-02 19:00:41 +00:00
const { session } = useRedirectToLoginIfUnauthenticated ( props . isPublic ) ;
if ( ! session && ! props . isPublic ) return null ;
2022-08-09 09:21:15 +00:00
return (
< KBarRoot >
< CustomBrandingContainer / >
< Layout { ...props } / >
< KBarContent / >
< / KBarRoot >
) ;
}
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 (
< Dropdown open = { menuOpen } onOpenChange = { ( ) = > setHelpOpen ( false ) } >
< DropdownMenuTrigger asChild onClick = { ( ) = > setMenuOpen ( true ) } >
< button className = "group flex w-full cursor-pointer appearance-none items-center rounded-full p-2 text-left hover:bg-gray-100 sm:pl-3 md:rounded-none lg:pl-2" >
< span
className = { classNames (
small ? "h-8 w-8" : "h-9 w-9 ltr:mr-2 rtl:ml-3" ,
"relative flex-shrink-0 rounded-full bg-gray-300 "
) } >
{
// eslint-disable-next-line @next/next/no-img-element
< img
className = "rounded-full"
src = { WEBAPP_URL + "/" + user . username + "/avatar.png" }
alt = { user . username || "Nameless User" }
/ >
}
{ ! user . away && (
< div className = "absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-green-500" / >
) }
{ user . away && (
< div className = "absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-yellow-500" / >
) }
< / span >
{ ! small && (
< span className = "flex flex-grow items-center truncate" >
< span className = "flex-grow truncate text-sm" >
< span className = "block truncate font-medium text-gray-900" >
{ user . name || "Nameless User" }
< / span >
< span className = "block truncate font-normal text-neutral-500" >
{ user . username
? process . env . NEXT_PUBLIC_WEBSITE_URL === "https://cal.com"
? ` cal.com/ ${ user . username } `
: ` / ${ user . username } `
: "No public page" }
< / span >
< / span >
< Icon.FiMoreVertical
className = "h-4 w-4 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria - hidden = "true"
/ >
< / span >
) }
< / button >
< / DropdownMenuTrigger >
< DropdownMenuContent portalled = { true } onInteractOutside = { ( ) = > setMenuOpen ( false ) } >
{ helpOpen ? (
< HelpMenuItem onHelpItemSelect = { ( ) = > onHelpItemSelect ( ) } / >
) : (
< >
< DropdownMenuItem >
< a
onClick = { ( ) = > {
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" >
< Icon.FiMoon
className = { classNames (
user . away
? "text-purple-500 group-hover:text-purple-700"
: "text-gray-500 group-hover:text-gray-700" ,
"h-4 w-4 flex-shrink-0 ltr:mr-2 rtl:ml-3"
) }
aria - hidden = "true"
/ >
{ user . away ? t ( "set_as_free" ) : t ( "set_as_away" ) }
< / a >
< / DropdownMenuItem >
< DropdownMenuSeparator className = "h-px bg-gray-200" / >
{ user . username && (
< DropdownMenuItem >
< a
target = "_blank"
rel = "noopener noreferrer"
href = { ` ${ process . env . NEXT_PUBLIC_WEBSITE_URL } / ${ user . username } ` }
className = "flex items-center px-4 py-2 text-sm text-gray-700" >
< Icon.FiExternalLink className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { " " }
{ t ( "view_public_page" ) }
< / a >
< / DropdownMenuItem >
) }
< DropdownMenuSeparator className = "h-px bg-gray-200" / >
< DropdownMenuItem >
< a
href = { JOIN_SLACK }
target = "_blank"
rel = "noreferrer"
className = "flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900" >
< Icon.FiSlack strokeWidth = { 1.5 } className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { " " }
{ t ( "join_our_slack" ) }
< / a >
< / DropdownMenuItem >
< DropdownMenuItem >
< a
target = "_blank"
rel = "noopener noreferrer"
href = { ROADMAP }
className = "flex items-center px-4 py-2 text-sm text-gray-700" >
< Icon.FiMap className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { t ( "visit_roadmap" ) }
< / a >
< / DropdownMenuItem >
< button
className = "flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
onClick = { ( ) = > setHelpOpen ( true ) } >
< Icon.FiHelpCircle
className = { classNames (
"text-gray-500 group-hover:text-neutral-500" ,
"h-4 w-4 flex-shrink-0 ltr:mr-2"
) }
aria - hidden = "true"
/ >
{ t ( "help" ) }
< / button >
< DropdownMenuSeparator className = "h-px bg-gray-200" / >
< DropdownMenuItem >
< a
onClick = { ( ) = > signOut ( { callbackUrl : "/auth/logout" } ) }
className = "flex cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900" >
< Icon.FiLogOut
className = { classNames (
"text-gray-500 group-hover:text-gray-700" ,
"h-4 w-4 flex-shrink-0 ltr:mr-2 rtl:ml-3"
) }
aria - hidden = "true"
/ >
{ t ( "sign_out" ) }
< / a >
< / DropdownMenuItem >
< / >
) }
< / DropdownMenuContent >
< / Dropdown >
) ;
}
type NavigationItemType = {
name : string ;
href : string ;
icon? : SVGComponent ;
child? : NavigationItemType [ ] ;
pro? : true ;
2022-09-02 19:00:41 +00:00
isCurrent ? : ( {
item ,
isChild ,
router ,
} : {
item : NavigationItemType ;
isChild? : boolean ;
router : NextRouter ;
} ) = > boolean ;
2022-08-09 09:21:15 +00:00
} ;
2022-09-02 19:00:41 +00:00
const requiredCredentialNavigationItems = [ "Routing Forms" ] ;
2022-09-07 04:13:31 +00:00
const MORE_SEPARATOR_NAME = "more" ;
2022-08-09 09:21:15 +00:00
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 : "apps" ,
href : "/apps" ,
icon : Icon.FiGrid ,
2022-09-02 19:00:41 +00:00
isCurrent : ( { router , item } ) = > {
const path = router . asPath . split ( "?" ) [ 0 ] ;
return ! ! item . child ? . some ( ( child ) = > path === child . href ) ;
} ,
2022-08-09 09:21:15 +00:00
child : [
{
name : "app_store" ,
href : "/apps" ,
} ,
{
name : "installed_apps" ,
href : "/apps/installed" ,
} ,
] ,
} ,
2022-09-07 04:13:31 +00:00
{
name : MORE_SEPARATOR_NAME ,
href : "/more" ,
icon : Icon.FiMoreHorizontal ,
} ,
{
name : "Routing Forms" ,
href : "/apps/routing_forms/forms" ,
icon : Icon.FiFileText ,
isCurrent : ( { router } ) = > {
return router . asPath . startsWith ( "/apps/routing_forms/" ) ;
} ,
} ,
{
name : "workflows" ,
href : "/workflows" ,
icon : Icon.FiZap ,
} ,
2022-08-09 09:21:15 +00:00
{
name : "settings" ,
2022-08-24 20:18:42 +00:00
href : "/settings" ,
2022-08-09 09:21:15 +00:00
icon : Icon.FiSettings ,
} ,
] ;
2022-09-07 04:13:31 +00:00
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 < string , NavigationItemType [ ] >
> (
( 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 ) items . mobileNavigationBottomItems . push ( item ) ;
// Items for the "more" menu in mobile navigation
else items . mobileNavigationMoreItems . push ( item ) ;
return items ;
} ,
{ desktopNavigationItems : [ ] , mobileNavigationBottomItems : [ ] , mobileNavigationMoreItems : [ ] }
) ;
2022-08-09 09:21:15 +00:00
const Navigation = ( ) = > {
return (
2022-09-02 19:00:41 +00:00
< nav className = "mt-2 flex-1 space-y-1 md:px-2 lg:mt-5 lg:px-0" >
2022-09-07 04:13:31 +00:00
{ desktopNavigationItems . map ( ( item ) = > (
2022-08-09 09:21:15 +00:00
< NavigationItem key = { item . name } item = { item } / >
) ) }
2022-09-05 10:02:21 +00:00
< div className = "text-gray-500 lg:hidden" >
2022-08-09 09:21:15 +00:00
< KBarTrigger / >
2022-09-05 10:02:21 +00:00
< / div >
2022-08-09 09:21:15 +00:00
< / nav >
) ;
} ;
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 ;
}
2022-09-02 19:00:41 +00:00
const defaultIsCurrent : NavigationItemType [ "isCurrent" ] = ( { isChild , item , router } ) = > {
return isChild ? item . href === router.asPath : router.asPath.startsWith ( item . href ) ;
} ;
2022-08-09 09:21:15 +00:00
const NavigationItem : React.FC < {
item : NavigationItemType ;
isChild? : boolean ;
} > = ( props ) = > {
const { item , isChild } = props ;
2022-09-05 19:06:34 +00:00
const { t , isLocaleReady } = useLocale ( ) ;
2022-08-09 09:21:15 +00:00
const router = useRouter ( ) ;
2022-09-02 19:00:41 +00:00
const isCurrent : NavigationItemType [ "isCurrent" ] = item . isCurrent || defaultIsCurrent ;
const current = isCurrent ( { isChild : ! ! isChild , item , router } ) ;
2022-08-09 09:21:15 +00:00
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem ( props . item ) ;
2022-08-24 20:18:42 +00:00
2022-08-09 09:21:15 +00:00
if ( ! shouldDisplayNavigationItem ) return null ;
2022-08-24 20:18:42 +00:00
2022-08-09 09:21:15 +00:00
return (
< Fragment >
< Link href = { item . href } >
< a
aria - label = { t ( item . name ) }
className = { classNames (
2022-09-05 10:02:21 +00:00
"group flex items-center rounded-md py-2 px-3 text-sm font-medium text-gray-600 hover:bg-gray-100 lg:px-[14px] [&[aria-current='page']]:bg-gray-200 [&[aria-current='page']]:hover:text-neutral-900" ,
2022-08-09 09:21:15 +00:00
isChild
2022-09-05 10:02:21 +00:00
? "[&[aria-current='page']]:text-brand-900 hidden pl-16 lg:flex lg:pl-11 [&[aria-current='page']]:bg-transparent"
2022-08-09 09:21:15 +00:00
: "[&[aria-current='page']]:text-brand-900 "
) }
aria - current = { current ? "page" : undefined } >
{ item . icon && (
< item.icon
2022-09-05 10:02:21 +00:00
className = "h-4 w-4 flex-shrink-0 text-gray-500 ltr:mr-3 rtl:ml-3 [&[aria-current='page']]:text-inherit"
2022-08-09 09:21:15 +00:00
aria - hidden = "true"
aria - current = { current ? "page" : undefined }
/ >
) }
2022-09-06 00:40:27 +00:00
{ isLocaleReady ? (
2022-09-05 19:06:34 +00:00
< span className = "hidden lg:inline" > { t ( item . name ) } < / span >
2022-09-06 00:40:27 +00:00
) : (
< SkeletonText className = "h-3 w-32" / >
2022-09-05 19:06:34 +00:00
) }
2022-08-09 09:21:15 +00:00
< / a >
< / Link >
{ item . child &&
2022-09-02 19:00:41 +00:00
isCurrent ( { router , isChild , item } ) &&
2022-08-09 09:21:15 +00:00
router . asPath . startsWith ( item . href ) &&
item . child . map ( ( item ) = > < NavigationItem key = { item . name } item = { item } isChild / > ) }
< / Fragment >
) ;
} ;
function MobileNavigationContainer() {
const { status } = useSession ( ) ;
if ( status !== "authenticated" ) return null ;
return < MobileNavigation / > ;
}
const MobileNavigation = ( ) = > {
const isEmbed = useIsEmbed ( ) ;
return (
< >
< nav
className = { classNames (
2022-09-07 04:13:31 +00:00
"bottom-nav fixed bottom-0 z-30 -mx-4 flex w-full border border-t border-gray-200 bg-gray-50 px-1 shadow md:hidden" ,
2022-08-09 09:21:15 +00:00
isEmbed && "hidden"
) } >
2022-09-07 04:13:31 +00:00
{ mobileNavigationBottomItems . map ( ( item ) = > (
< MobileNavigationItem key = { item . name } item = { item } / >
) ) }
2022-08-09 09:21:15 +00:00
< / nav >
{ /* add padding to content for mobile navigation*/ }
< div className = "block pt-12 md:hidden" / >
< / >
) ;
} ;
const MobileNavigationItem : React.FC < {
item : NavigationItemType ;
isChild? : boolean ;
} > = ( props ) = > {
2022-09-07 04:13:31 +00:00
const { item , isChild } = props ;
2022-08-09 09:21:15 +00:00
const router = useRouter ( ) ;
2022-09-05 19:06:34 +00:00
const { t , isLocaleReady } = useLocale ( ) ;
2022-09-02 19:00:41 +00:00
const isCurrent : NavigationItemType [ "isCurrent" ] = item . isCurrent || defaultIsCurrent ;
const current = isCurrent ( { isChild : ! ! isChild , item , router } ) ;
2022-08-09 09:21:15 +00:00
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem ( props . item ) ;
2022-09-05 19:06:34 +00:00
2022-08-09 09:21:15 +00:00
if ( ! shouldDisplayNavigationItem ) return null ;
return (
< Link key = { item . name } href = { item . href } >
< a
2022-09-07 04:13:31 +00:00
className = "relative my-2 min-w-0 flex-1 overflow-hidden rounded-md py-2 px-1 text-center text-xs font-medium text-neutral-400 hover:bg-gray-200 hover:text-gray-700 focus:z-10 sm:text-sm [&[aria-current='page']]:text-gray-900"
2022-08-09 09:21:15 +00:00
aria - current = { current ? "page" : undefined } >
{ item . icon && (
< item.icon
2022-09-07 04:13:31 +00:00
className = "mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center text-inherit [&[aria-current='page']]:text-gray-900"
2022-08-09 09:21:15 +00:00
aria - hidden = "true"
aria - current = { current ? "page" : undefined }
/ >
) }
2022-09-06 00:40:27 +00:00
{ isLocaleReady ? (
2022-09-05 19:06:34 +00:00
< span className = "block truncate" > { t ( item . name ) } < / span >
) : (
< SkeletonText className = "" / >
) }
2022-08-09 09:21:15 +00:00
< / a >
< / Link >
) ;
} ;
2022-09-07 04:13:31 +00:00
const MobileNavigationMoreItem : React.FC < {
item : NavigationItemType ;
isChild? : boolean ;
} > = ( props ) = > {
const { item } = props ;
const { t , isLocaleReady } = useLocale ( ) ;
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem ( props . item ) ;
if ( ! shouldDisplayNavigationItem ) return null ;
return (
< li className = "border-b last:border-b-0" key = { item . name } >
< Link href = { item . href } >
< a className = "flex items-center justify-between p-5 hover:bg-gray-100" >
< span className = "flex items-center font-semibold text-gray-700 " >
{ item . icon && (
< item.icon className = "h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3" aria - hidden = "true" / >
) }
{ isLocaleReady ? t ( item . name ) : < SkeletonText className = "" / > }
< / span >
< Icon.FiArrowRight className = "h-5 w-5 text-gray-500" / >
< / a >
< / Link >
< / li >
) ;
} ;
2022-08-09 09:21:15 +00:00
function DeploymentInfo() {
const query = useMeQuery ( ) ;
const user = query . data ;
return (
< small
style = { {
fontSize : "0.5rem" ,
} }
className = "mx-3 mt-1 mb-2 hidden opacity-50 lg:block" >
& copy ; { new Date ( ) . getFullYear ( ) } Cal . com , Inc . v . { pkg . version + "-" }
{ process . env . NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh" }
< span className = "lowercase" data - testid = { ` plan- ${ user ? . plan . toLowerCase ( ) } ` } >
- { user ? . plan }
< / span >
< / small >
) ;
}
function SideBarContainer() {
const { status } = useSession ( ) ;
const router = useRouter ( ) ;
if ( status !== "authenticated" ) return null ;
if ( router . route . startsWith ( "/v2/settings/" ) ) return null ;
return < SideBar / > ;
}
function SideBar() {
2022-09-05 19:06:34 +00:00
const { isLocaleReady } = useLocale ( ) ;
2022-08-09 09:21:15 +00:00
return (
2022-09-02 19:00:41 +00:00
< aside className = "hidden w-14 flex-col border-r border-gray-100 bg-gray-50 md:flex lg:w-56 lg:flex-shrink-0 lg:px-4" >
2022-08-09 09:21:15 +00:00
< div className = "flex h-0 flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5" >
< div className = "items-center justify-between md:hidden lg:flex" >
< Link href = "/event-types" >
< a className = "px-4" >
< Logo small / >
< / a >
< / Link >
2022-09-05 10:02:21 +00:00
< KBarTrigger / >
2022-08-09 09:21:15 +00:00
< / div >
{ /* logo icon for tablet */ }
< Link href = "/event-types" >
< a className = "text-center md:inline lg:hidden" >
< Logo small icon / >
< / a >
< / Link >
< Navigation / >
< / div >
2022-08-24 20:18:42 +00:00
2022-09-05 21:22:28 +00:00
{ / * T O D O @ P e e r _ R i c h : r e i n t r o d u c e i n 2 . 1
2022-08-24 20:18:42 +00:00
< Tips / >
2022-08-29 12:04:39 +00:00
* / }
2022-08-24 20:18:42 +00:00
2022-09-05 19:06:34 +00:00
{ ! isLocaleReady ? null : < TrialBanner / > }
2022-08-09 09:21:15 +00:00
< div data - testid = "user-dropdown-trigger" >
< span className = "hidden lg:inline" >
< UserDropdown / >
< / span >
< span className = "hidden md:inline lg:hidden" >
< UserDropdown small / >
< / span >
< / div >
< DeploymentInfo / >
< / aside >
) ;
}
2022-09-02 19:00:41 +00:00
export function ShellMain ( props : LayoutProps ) {
const router = useRouter ( ) ;
2022-09-05 19:06:34 +00:00
const { isLocaleReady } = useLocale ( ) ;
2022-08-09 09:21:15 +00:00
return (
2022-09-02 19:00:41 +00:00
< >
< div className = "flex items-baseline" >
{ ! ! props . backPath && (
< Icon.FiArrowLeft
className = "mr-3 hover:cursor-pointer"
onClick = { ( ) = > router . push ( props . backPath as string ) }
/ >
) }
2022-08-24 20:18:42 +00:00
{ props . heading && (
2022-09-06 17:05:16 +00:00
< div className = { classNames ( props . large && "py-8" , "flex w-full items-center pt-4 md:p-0" ) } >
2022-08-24 20:18:42 +00:00
{ props . HeadingLeftIcon && < div className = "ltr:mr-4" > { props . HeadingLeftIcon } < / div > }
2022-09-05 19:06:34 +00:00
< div className = "mb-4 w-full ltr:mr-4 rtl:ml-4" >
{ props . heading && (
< h1 className = "font-cal mb-1 text-xl font-bold capitalize tracking-wide text-black" >
{ ! isLocaleReady ? null : props . heading }
< / h1 >
) }
{ props . subtitle && (
2022-09-06 17:05:16 +00:00
< p className = "hidden text-sm text-neutral-500 sm:block" >
{ ! isLocaleReady ? null : props . subtitle }
< / p >
2022-09-05 19:06:34 +00:00
) }
2022-08-24 20:18:42 +00:00
< / div >
{ props . CTA && < div className = "mb-4 flex-shrink-0" > { props . CTA } < / div > }
< / div >
) }
2022-09-02 19:00:41 +00:00
< / div >
< div className = { classNames ( "" , props . flexChildrenContainer && "flex flex-1 flex-col" ) } >
{ props . children }
< / div >
< / >
) ;
}
2022-09-07 01:33:50 +00:00
const SettingsSidebarContainerDefault = ( ) = > null ;
function MainContainer ( {
SettingsSidebarContainer : SettingsSidebarContainerProp = < SettingsSidebarContainerDefault / > ,
MobileNavigationContainer : MobileNavigationContainerProp = < MobileNavigationContainer / > ,
TopNavContainer : TopNavContainerProp = < TopNavContainer / > ,
. . . props
} : LayoutProps ) {
const [ sideContainerOpen , setSideContainerOpen ] = props . drawerState || [ false , noop ] ;
2022-09-02 19:00:41 +00:00
return (
2022-09-06 19:34:58 +00:00
< main className = "relative z-0 flex flex-1 flex-col overflow-y-auto bg-white focus:outline-none " >
2022-09-02 19:00:41 +00:00
{ /* show top navigation for md and smaller (tablet and phones) */ }
2022-09-07 01:33:50 +00:00
{ TopNavContainerProp }
{ /* The following is used for settings navigation on medium and smaller screens */ }
< div
className = { classNames (
"absolute z-40 m-0 h-screen w-screen bg-black opacity-50" ,
sideContainerOpen ? "" : "hidden"
) }
onClick = { ( ) = > {
setSideContainerOpen ( false ) ;
} }
/ >
{ SettingsSidebarContainerProp }
2022-09-06 17:05:16 +00:00
< div className = "px-4 py-2 lg:py-8 lg:px-12" >
< ErrorBoundary >
{ ! props . withoutMain ? < ShellMain { ...props } > { props . children } < / ShellMain > : props . children }
< / ErrorBoundary >
{ /* show bottom navigation for md and smaller (tablet and phones) */ }
2022-09-07 01:33:50 +00:00
{ MobileNavigationContainerProp }
2022-09-06 17:05:16 +00:00
{ /* <LicenseBanner /> */ }
< / div >
2022-08-09 09:21:15 +00:00
< / main >
) ;
}
function TopNavContainer() {
const { status } = useSession ( ) ;
if ( status !== "authenticated" ) return null ;
return < TopNav / > ;
}
function TopNav() {
const isEmbed = useIsEmbed ( ) ;
const { t } = useLocale ( ) ;
return (
< nav
style = { isEmbed ? { display : "none" } : { } }
2022-09-06 19:34:58 +00:00
className = "flex items-center justify-between border-b border-gray-200 bg-gray-50 py-1.5 px-4 sm:p-4 md:hidden" >
2022-08-09 09:21:15 +00:00
< Link href = "/event-types" >
< a >
< Logo / >
< / a >
< / Link >
< div className = "flex items-center gap-2 self-center" >
2022-09-06 17:05:16 +00:00
< span className = "group flex items-center rounded-full text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-neutral-900 lg:hidden" >
2022-08-09 09:21:15 +00:00
< KBarTrigger / >
< / span >
2022-09-06 17:05:16 +00:00
< button className = "rounded-full p-1 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2" >
2022-08-09 09:21:15 +00:00
< span className = "sr-only" > { t ( "settings" ) } < / span >
< Link href = "/settings/profile" >
< a >
2022-09-06 17:05:16 +00:00
< Icon.FiSettings className = "h-4 w-4 text-gray-700" aria - hidden = "true" / >
2022-08-09 09:21:15 +00:00
< / a >
< / Link >
< / button >
< UserDropdown small / >
< / div >
< / nav >
) ;
}
2022-09-07 04:13:31 +00:00
export const MobileNavigationMoreItems = ( ) = > (
< ul className = "mt-2 rounded-md border" >
{ mobileNavigationMoreItems . map ( ( item ) = > (
< MobileNavigationMoreItem key = { item . name } item = { item } / >
) ) }
< / ul >
) ;