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" ;
2022-10-10 17:00:09 +00:00
import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge" ;
2022-08-09 09:21:15 +00:00
import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner" ;
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem" ;
2022-11-16 21:07:20 +00:00
import { TeamsUpgradeBanner } from "@calcom/features/ee/teams/components" ;
2022-08-09 09:21:15 +00:00
import CustomBranding from "@calcom/lib/CustomBranding" ;
import classNames from "@calcom/lib/classNames" ;
2022-11-16 21:07:20 +00:00
import { DESKTOP_APP_LINK , JOIN_SLACK , ROADMAP , WEBAPP_URL } from "@calcom/lib/constants" ;
2022-08-09 09:21:15 +00:00
import { useLocale } from "@calcom/lib/hooks/useLocale" ;
import useTheme from "@calcom/lib/hooks/useTheme" ;
2022-10-14 19:38:05 +00:00
import isCalcom from "@calcom/lib/isCalcom" ;
2022-08-09 09:21:15 +00:00
import { trpc } from "@calcom/trpc/react" ;
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery" ;
import { SVGComponent } from "@calcom/types/SVGComponent" ;
2022-11-23 02:55:25 +00:00
import {
Button ,
Dropdown ,
2022-08-09 09:21:15 +00:00
DropdownMenuContent ,
DropdownMenuItem ,
2022-11-16 21:07:20 +00:00
DropdownMenuPortal ,
2022-08-09 09:21:15 +00:00
DropdownMenuSeparator ,
DropdownMenuTrigger ,
2022-11-23 02:55:25 +00:00
Icon ,
showToast ,
TimezoneChangeDialog ,
Tips ,
} from "../.." ;
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" ;
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 (
2022-09-07 15:01:33 +00:00
< header className = { classNames ( "mb-3 block justify-between sm:flex" , props . className ) } >
2022-08-09 09:21:15 +00:00
< 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 >
2022-10-15 08:05:37 +00:00
{ props . actions && < div className = "mt-2 flex-shrink-0 sm:mt-0" > { props . actions } < / div > }
2022-09-07 15:01:33 +00:00
< / header >
2022-08-09 09:21:15 +00:00
) ;
}
const Layout = ( props : LayoutProps ) = > {
2022-09-29 12:41:40 +00:00
const pageTitle = typeof props . heading === "string" && ! props . title ? props.heading : props.title ;
2022-08-09 09:21:15 +00:00
return (
< >
2022-10-18 17:46:22 +00:00
{ ! props . withoutSeo && (
< HeadSeo
title = { pageTitle ? ? "Cal.com" }
description = { props . subtitle ? props . subtitle ? . toString ( ) : "" }
nextSeoProps = { {
nofollow : true ,
noindex : true ,
} }
/ >
) }
2022-08-09 09:21:15 +00:00
< div >
< Toaster position = "bottom-right" / >
< / div >
2022-10-05 14:33:29 +00:00
{ /* todo: only run this if timezone is different */ }
< TimezoneChangeDialog / >
2022-11-15 19:33:59 +00:00
< div className = "h-screen overflow-hidden" >
< div className = "flex h-screen overflow-hidden" data - testid = "dashboard-shell" >
{ props . SidebarContainer || < SideBarContainer / > }
< div className = "flex w-0 flex-1 flex-col overflow-hidden" >
2022-11-16 21:07:20 +00:00
< TeamsUpgradeBanner / >
2022-11-15 19:33:59 +00:00
< ImpersonatingBanner / >
< MainContainer { ...props } / >
< / div >
2022-08-09 09:21:15 +00:00
< / 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 ;
2022-10-25 00:29:49 +00:00
backPath? : string | boolean ; // renders back button to specified path
2022-08-09 09:21:15 +00:00
// use when content needs to expand with flex
flexChildrenContainer? : boolean ;
isPublic? : boolean ;
2022-09-02 19:00:41 +00:00
withoutMain? : boolean ;
2022-10-18 17:46:22 +00:00
// Gives you the option to skip HeadSEO and render your own.
withoutSeo? : boolean ;
2022-11-30 20:51:44 +00:00
// Gives the ability to include actions to the right of the heading
actions? : JSX.Element ;
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 } ` ,
} ) ;
} ) ;
2022-11-10 23:40:01 +00:00
const mutation = trpc . viewer . away . useMutation ( {
2022-08-09 09:21:15 +00:00
onSettled() {
2022-11-10 23:40:01 +00:00
utils . viewer . me . invalidate ( ) ;
2022-08-09 09:21:15 +00:00
} ,
} ) ;
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 (
2022-10-17 19:05:00 +00:00
< Dropdown open = { menuOpen } >
2022-10-21 19:59:14 +00:00
< DropdownMenuTrigger asChild onClick = { ( ) = > setMenuOpen ( ( menuOpen ) = > ! menuOpen ) } >
2022-11-11 19:21:56 +00:00
< button className = "group flex w-full cursor-pointer appearance-none items-center rounded-full p-2 text-left outline-none hover:bg-gray-200 sm:pl-3 md:rounded lg:pl-2" >
2022-08-09 09:21:15 +00:00
< span
className = { classNames (
2022-11-11 19:21:56 +00:00
small ? "h-6 w-6" : "h-8 w-8 ltr:mr-2 rtl:ml-3" ,
2022-08-09 09:21:15 +00:00
"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 >
2022-11-11 19:21:56 +00:00
< span className = "block truncate font-normal text-gray-900" >
2022-08-09 09:21:15 +00:00
{ 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 >
2022-09-30 10:29:22 +00:00
< DropdownMenuPortal >
2022-10-07 12:21:49 +00:00
< DropdownMenuContent
2022-10-17 19:05:00 +00:00
onInteractOutside = { ( ) = > {
setMenuOpen ( false ) ;
setHelpOpen ( false ) ;
} }
2022-10-07 12:21:49 +00:00
className = "overflow-hidden rounded-md" >
2022-09-30 10:29:22 +00:00
{ helpOpen ? (
< HelpMenuItem onHelpItemSelect = { ( ) = > onHelpItemSelect ( ) } / >
) : (
< >
< DropdownMenuItem >
< button
onClick = { ( ) = > {
mutation . mutate ( { away : ! user ? . away } ) ;
2022-11-10 23:40:01 +00:00
utils . viewer . me . invalidate ( ) ;
2022-09-30 10:29:22 +00:00
} }
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" ) }
< / button >
< / DropdownMenuItem >
< DropdownMenuSeparator className = "h-px bg-gray-200" / >
{ user . username && (
2022-10-26 21:12:25 +00:00
< >
< 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 >
< DropdownMenuItem >
< a
href = "#"
onClick = { ( e ) = > {
e . preventDefault ( ) ;
navigator . clipboard . writeText (
` ${ process . env . NEXT_PUBLIC_WEBSITE_URL } / ${ user . username } `
) ;
showToast ( t ( "link_copied" ) , "success" ) ;
} }
className = "flex items-center px-4 py-2 text-sm text-gray-700" >
< Icon.FiLink className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { " " }
{ t ( "copy_public_page_link" ) }
< / a >
< / DropdownMenuItem >
< / >
2022-09-30 10:29:22 +00:00
) }
< 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 >
2022-10-17 19:05:00 +00:00
< DropdownMenuItem >
< button
onClick = { ( ) = > setHelpOpen ( true ) }
className = "flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900" >
< 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"
/ >
2022-09-30 10:29:22 +00:00
2022-10-17 19:05:00 +00:00
{ t ( "help" ) }
< / button >
< / DropdownMenuItem >
2022-08-09 09:21:15 +00:00
< DropdownMenuItem >
< a
target = "_blank"
rel = "noopener noreferrer"
2022-09-30 10:29:22 +00:00
href = { DESKTOP_APP_LINK }
className = "desktop-hidden hidden items-center px-4 py-2 text-sm text-gray-700 lg:flex" >
< Icon.FiDownload className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { " " }
{ t ( "download_desktop_app" ) }
2022-08-09 09:21:15 +00:00
< / a >
< / DropdownMenuItem >
2022-09-30 10:29:22 +00:00
< 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 >
< / DropdownMenuPortal >
2022-08-09 09:21:15 +00:00
< / Dropdown >
) ;
}
2022-09-15 19:53:09 +00:00
export type NavigationItemType = {
2022-08-09 09:21:15 +00:00
name : string ;
href : string ;
2022-10-10 17:00:09 +00:00
badge? : React.ReactNode ;
2022-08-09 09:21:15 +00:00
icon? : SVGComponent ;
child? : NavigationItemType [ ] ;
pro? : true ;
2022-09-17 17:53:31 +00:00
onlyMobile? : boolean ;
onlyDesktop? : boolean ;
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-10-10 17:00:09 +00:00
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 ,
2022-10-10 17:00:09 +00:00
badge : < UnconfirmedBookingBadge / > ,
2022-10-27 09:45:01 +00:00
isCurrent : ( { router } ) = > {
const path = router . asPath . split ( "?" ) [ 0 ] ;
return path . startsWith ( "/bookings" ) ;
} ,
2022-08-09 09:21:15 +00:00
} ,
{
name : "availability" ,
href : "/availability" ,
icon : Icon.FiClock ,
} ,
2022-09-17 17:53:31 +00:00
{
name : "teams" ,
href : "/teams" ,
icon : Icon.FiUsers ,
onlyDesktop : true ,
} ,
2022-08-09 09:21:15 +00:00
{
name : "apps" ,
href : "/apps" ,
icon : Icon.FiGrid ,
2022-09-02 19:00:41 +00:00
isCurrent : ( { router , item } ) = > {
const path = router . asPath . split ( "?" ) [ 0 ] ;
2022-09-15 19:53:09 +00:00
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
return (
2022-09-22 17:23:43 +00:00
( path . startsWith ( item . href ) || path . startsWith ( "/v2" + item . href ) ) && ! path . includes ( "routing-forms/" )
2022-09-15 19:53:09 +00:00
) ;
2022-09-02 19:00:41 +00:00
} ,
2022-08-09 09:21:15 +00:00
child : [
{
name : "app_store" ,
href : "/apps" ,
2022-09-15 19:53:09 +00:00
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 ) ) &&
2022-09-22 17:23:43 +00:00
! path . includes ( "routing-forms/" ) &&
2022-09-15 19:53:09 +00:00
! path . includes ( "/installed" )
) ;
} ,
2022-08-09 09:21:15 +00:00
} ,
{
name : "installed_apps" ,
2022-09-15 19:53:09 +00:00
href : "/apps/installed/calendar" ,
isCurrent : ( { router } ) = > {
const path = router . asPath ;
return path . startsWith ( "/apps/installed/" ) || path . startsWith ( "/v2/apps/installed/" ) ;
} ,
2022-08-09 09:21:15 +00:00
} ,
] ,
} ,
2022-09-07 04:13:31 +00:00
{
name : MORE_SEPARATOR_NAME ,
href : "/more" ,
icon : Icon.FiMoreHorizontal ,
} ,
{
name : "Routing Forms" ,
2022-09-22 17:23:43 +00:00
href : "/apps/routing-forms/forms" ,
2022-09-07 04:13:31 +00:00
icon : Icon.FiFileText ,
isCurrent : ( { router } ) = > {
2022-09-22 17:23:43 +00:00
return router . asPath . startsWith ( "/apps/routing-forms/" ) ;
2022-09-07 04:13:31 +00:00
} ,
} ,
{
name : "workflows" ,
href : "/workflows" ,
icon : Icon.FiZap ,
} ,
2022-08-09 09:21:15 +00:00
{
name : "settings" ,
2022-11-04 20:30:12 +00:00
href : "/settings/my-account/profile" ,
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
2022-09-17 17:53:31 +00:00
if ( index < moreSeparatorIndex + 1 && ! item . onlyDesktop ) items . mobileNavigationBottomItems . push ( item ) ;
2022-09-07 04:13:31 +00:00
// 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 ( ) ;
2022-11-10 23:40:01 +00:00
const { data : routingForms } = trpc . viewer . appById . useQuery (
{ appId : "routing-forms" } ,
{
enabled : status === "authenticated" && requiredCredentialNavigationItems . includes ( item . name ) ,
trpc : { } ,
}
) ;
2022-10-14 16:24:43 +00:00
return ! requiredCredentialNavigationItems . includes ( item . name ) || routingForms ? . isInstalled ;
2022-08-09 09:21:15 +00:00
}
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-10-10 17:00:09 +00:00
< span className = "hidden w-full justify-between lg:flex" >
< div className = "flex" > { t ( item . name ) } < / div >
{ item . badge && item . badge }
< / 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
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 ( ) ;
2022-09-21 15:39:54 +00:00
2022-08-09 09:21:15 +00:00
return (
< >
< nav
className = { classNames (
2022-09-24 11:55:50 +00:00
"bottom-nav fixed bottom-0 z-30 -mx-4 flex w-full border border-t border-gray-200 bg-gray-50 bg-opacity-40 px-1 shadow backdrop-blur-md 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 } >
2022-10-10 17:00:09 +00:00
{ item . badge && < div className = "absolute right-1 top-1" > { item . badge } < / div > }
2022-08-09 09:21:15 +00:00
{ 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-09 08:22:39 +00:00
{ isLocaleReady ? < span className = "block truncate" > { t ( item . name ) } < / span > : < SkeletonText / > }
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 && (
2022-10-15 08:05:37 +00:00
< item.icon className = "h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3" aria - hidden = "true" / >
2022-09-07 04:13:31 +00:00
) }
2022-09-09 08:22:39 +00:00
{ isLocaleReady ? t ( item . name ) : < SkeletonText / > }
2022-09-07 04:13:31 +00:00
< / 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 ( ) ;
2022-09-15 19:53:09 +00:00
// Make sure that Sidebar is rendered optimistically so that a refresh of pages when logged in have SideBar from the beginning.
// This improves the experience of refresh on app store pages(when logged in) which are SSG.
// Though when logged out, app store pages would temporarily show SideBar until session status is confirmed.
if ( status !== "loading" && status !== "authenticated" ) return null ;
2022-08-09 09:21:15 +00:00
if ( router . route . startsWith ( "/v2/settings/" ) ) return null ;
return < SideBar / > ;
}
function SideBar() {
return (
2022-09-07 15:01:33 +00:00
< aside className = "desktop-transparent 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" >
2022-09-07 15:01:33 +00:00
< header className = "items-center justify-between md:hidden lg:flex" >
2022-08-09 09:21:15 +00:00
< Link href = "/event-types" >
< a className = "px-4" >
< Logo small / >
< / a >
< / Link >
2022-09-07 15:01:33 +00:00
< div className = "flex space-x-2" >
< button
color = "minimal"
onClick = { ( ) = > window . history . back ( ) }
2022-10-15 08:05:37 +00:00
className = "desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900" >
2022-09-07 15:01:33 +00:00
< Icon.FiArrowLeft className = "h-4 w-4 flex-shrink-0 text-neutral-500 group-hover:text-neutral-900" / >
< / button >
< button
color = "minimal"
onClick = { ( ) = > window . history . forward ( ) }
2022-10-15 08:05:37 +00:00
className = "desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900" >
2022-09-07 15:01:33 +00:00
< Icon.FiArrowRight className = "h-4 w-4 flex-shrink-0 text-neutral-500 group-hover:text-neutral-900" / >
< / button >
< KBarTrigger / >
< / div >
< / header >
< hr className = "desktop-only absolute -left-3 -right-3 mt-4 block w-full border-gray-200" / >
2022-08-09 09:21:15 +00:00
{ /* logo icon for tablet */ }
< Link href = "/event-types" >
< a className = "text-center md:inline lg:hidden" >
< Logo small icon / >
< / a >
< / Link >
2022-09-07 15:01:33 +00:00
2022-08-09 09:21:15 +00:00
< Navigation / >
< / div >
2022-08-24 20:18:42 +00:00
2022-10-14 19:38:05 +00:00
{ isCalcom && < Tips / > }
2022-09-15 16:59:48 +00:00
{ / * S a v e i t f o r n e x t p r e v i e w v e r s i o n
2022-10-15 08:05:37 +00:00
< div className = "hidden mb-4 lg:block" >
2022-09-07 15:01:33 +00:00
< UserV2OptInBanner / >
2022-09-15 16:59:48 +00:00
< / div > * / }
2022-08-24 20:18:42 +00:00
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
< >
2022-09-09 15:02:31 +00:00
< div className = "flex items-baseline sm:mt-0" >
2022-09-02 19:00:41 +00:00
{ ! ! props . backPath && (
2022-09-28 17:46:14 +00:00
< Button
size = "icon"
color = "minimal"
2022-10-25 00:29:49 +00:00
onClick = { ( ) = >
typeof props . backPath === "string" ? router . push ( props . backPath as string ) : router . back ( )
}
2022-09-28 17:46:14 +00:00
StartIcon = { Icon . FiArrowLeft }
aria - label = "Go Back"
className = "ltr:mr-2 rtl:ml-2"
2022-09-02 19:00:41 +00:00
/ >
) }
2022-08-24 20:18:42 +00:00
{ props . heading && (
2022-09-07 16:28:58 +00:00
< header
className = { classNames (
props . large && "py-8" ,
2022-10-26 15:29:01 +00:00
"mb-4 flex w-full max-w-full items-center pt-4 md:p-0 lg:mb-10"
2022-09-07 16:28:58 +00:00
) } >
2022-08-24 20:18:42 +00:00
{ props . HeadingLeftIcon && < div className = "ltr:mr-4" > { props . HeadingLeftIcon } < / div > }
2022-09-24 11:55:50 +00:00
< div className = "w-full ltr:mr-4 rtl:ml-4 sm:block" >
2022-09-05 19:06:34 +00:00
{ props . heading && (
2022-11-08 16:50:13 +00:00
< h1 className = "font-cal max-w-28 sm:max-w-72 md:max-w-80 mb-1 hidden truncate text-xl font-bold tracking-wide text-black sm:block xl:max-w-full" >
2022-09-15 19:53:09 +00:00
{ ! isLocaleReady ? < SkeletonText invisible / > : props . heading }
2022-09-05 19:06:34 +00:00
< / h1 >
) }
{ props . subtitle && (
2022-09-06 17:05:16 +00:00
< p className = "hidden text-sm text-neutral-500 sm:block" >
2022-09-15 19:53:09 +00:00
{ ! isLocaleReady ? < SkeletonText invisible / > : props . subtitle }
2022-09-06 17:05:16 +00:00
< / p >
2022-09-05 19:06:34 +00:00
) }
2022-08-24 20:18:42 +00:00
< / div >
2022-09-09 15:02:31 +00:00
{ props . CTA && (
2022-09-28 17:56:59 +00:00
< div
className = { classNames (
props . backPath ? "relative" : "fixed right-4 bottom-[75px] z-40 " ,
2022-10-25 08:51:43 +00:00
"flex-shrink-0 sm:relative sm:bottom-auto sm:right-auto"
2022-09-28 17:56:59 +00:00
) } >
2022-09-09 15:02:31 +00:00
{ props . CTA }
< / div >
) }
2022-11-30 20:51:44 +00:00
{ props . actions && props . actions }
2022-09-07 15:01:33 +00:00
< / header >
2022-08-24 20:18:42 +00:00
) }
2022-09-02 19:00:41 +00:00
< / div >
2022-09-07 16:28:58 +00:00
< div className = { classNames ( props . flexChildrenContainer && "flex flex-1 flex-col" ) } >
2022-09-02 19:00:41 +00:00
{ 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-10-26 15:29:01 +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 (
2022-10-13 15:50:08 +00:00
"overflow-none fixed z-40 m-0 h-screen w-screen overscroll-none bg-black opacity-50" ,
2022-09-07 01:33:50 +00:00
sideContainerOpen ? "" : "hidden"
) }
onClick = { ( ) = > {
setSideContainerOpen ( false ) ;
} }
/ >
{ SettingsSidebarContainerProp }
2022-10-26 15:29:01 +00:00
< div className = "max-w-full px-4 py-2 lg:py-8 lg:px-12" >
2022-09-06 17:05:16 +00:00
< ErrorBoundary >
2022-09-24 11:55:50 +00:00
{ /* add padding to top for mobile when App Bar is fixed */ }
< div className = "pt-14 sm:hidden" / >
2022-09-06 17:05:16 +00:00
{ ! props . withoutMain ? < ShellMain { ...props } > { props . children } < / ShellMain > : props . children }
< / ErrorBoundary >
2022-09-24 11:55:50 +00:00
{ /* show bottom navigation for md and smaller (tablet and phones) on pages where back button doesn't exist */ }
{ ! props . backPath ? MobileNavigationContainerProp : null }
2022-09-06 17:05:16 +00:00
< / 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 (
2022-09-09 15:02:31 +00:00
< >
< nav
style = { isEmbed ? { display : "none" } : { } }
className = "fixed z-40 flex w-full items-center justify-between border-b border-gray-200 bg-gray-50 bg-opacity-50 py-1.5 px-4 backdrop-blur-lg sm:relative sm:p-4 md:hidden" >
< Link href = "/event-types" >
< a >
< Logo / >
< / a >
< / Link >
< div className = "flex items-center gap-2 self-center" >
< 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" >
< KBarTrigger / >
< / span >
< 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" >
< span className = "sr-only" > { t ( "settings" ) } < / span >
< Link href = "/settings/profile" >
< a >
< Icon.FiSettings className = "h-4 w-4 text-gray-700" aria - hidden = "true" / >
< / a >
< / Link >
< / button >
< UserDropdown small / >
< / div >
< / nav >
< / >
2022-08-09 09:21:15 +00:00
) ;
}
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 >
) ;