2022-07-28 19:58:26 +00:00
import type { User , UserPlan } from "@prisma/client" ;
2022-04-14 21:49:51 +00:00
import { SessionContextValue , signOut , useSession } from "next-auth/react" ;
2021-09-22 19:52:38 +00:00
import Link from "next/link" ;
import { useRouter } from "next/router" ;
2022-05-24 13:29:39 +00:00
import React , { Fragment , ReactNode , useEffect , useState } from "react" ;
2022-05-02 20:39:35 +00:00
import { Toaster } from "react-hot-toast" ;
2021-09-22 19:52:38 +00:00
2022-07-28 19:58:26 +00:00
import dayjs from "@calcom/dayjs" ;
2022-05-27 15:37:02 +00:00
import { useIsEmbed } from "@calcom/embed-core/embed-iframe" ;
2022-07-28 19:58:26 +00:00
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" ;
2022-07-23 00:39:50 +00:00
import CustomBranding from "@calcom/lib/CustomBranding" ;
2022-07-28 19:58:26 +00:00
import classNames from "@calcom/lib/classNames" ;
import { JOIN_SLACK , ROADMAP , WEBAPP_URL } from "@calcom/lib/constants" ;
2022-08-13 11:04:57 +00:00
import useApp from "@calcom/lib/hooks/useApp" ;
2022-04-14 21:49:51 +00:00
import { useLocale } from "@calcom/lib/hooks/useLocale" ;
2022-07-22 17:27:06 +00:00
import { trpc } from "@calcom/trpc/react" ;
2022-07-28 19:58:26 +00:00
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery" ;
import Badge from "@calcom/ui/Badge" ;
2022-03-16 23:36:43 +00:00
import Button from "@calcom/ui/Button" ;
2022-03-16 19:55:18 +00:00
import Dropdown , {
DropdownMenuContent ,
DropdownMenuItem ,
DropdownMenuSeparator ,
DropdownMenuTrigger ,
} from "@calcom/ui/Dropdown" ;
2022-07-27 02:24:00 +00:00
import { CollectionIcon , Icon } from "@calcom/ui/Icon" ;
2022-07-28 19:58:26 +00:00
import Loader from "@calcom/ui/Loader" ;
import { useViewerI18n } from "@calcom/web/components/I18nLanguageHandler" ;
2021-09-24 20:02:03 +00:00
2021-08-27 12:35:20 +00:00
import { HeadSeo } from "@components/seo/head-seo" ;
2021-09-22 19:52:38 +00:00
2022-07-28 19:58:26 +00:00
/* TODO: Get this from endpoint */
import pkg from "../../apps/web/package.json" ;
import ErrorBoundary from "./ErrorBoundary" ;
import { KBarContent , KBarRoot , KBarTrigger } from "./Kbar" ;
2021-09-22 19:52:38 +00:00
import Logo from "./Logo" ;
2021-03-24 15:03:04 +00:00
2022-07-28 19:58:26 +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 ) ;
} ;
2022-04-12 04:38:10 +00:00
function useRedirectToLoginIfUnauthenticated ( isPublic = false ) {
2022-01-07 20:23:37 +00:00
const { data : session , status } = useSession ( ) ;
const loading = status === "loading" ;
2021-09-27 14:47:55 +00:00
const router = useRouter ( ) ;
useEffect ( ( ) = > {
2022-04-12 04:38:10 +00:00
if ( isPublic ) {
2022-03-23 22:00:30 +00:00
return ;
}
2021-09-27 14:47:55 +00:00
if ( ! loading && ! session ) {
router . replace ( {
pathname : "/auth/login" ,
query : {
2022-04-21 20:32:25 +00:00
callbackUrl : ` ${ WEBAPP_URL } ${ location . pathname } ${ location . search } ` ,
2021-09-27 14:47:55 +00:00
} ,
} ) ;
}
2021-11-08 14:10:02 +00:00
// eslint-disable-next-line react-hooks/exhaustive-deps
2022-04-12 04:38:10 +00:00
} , [ loading , session , isPublic ] ) ;
2021-10-14 10:57:49 +00:00
2021-11-08 14:10:02 +00:00
return {
loading : loading && ! session ,
2022-04-06 17:07:22 +00:00
session ,
2021-11-08 14:10:02 +00:00
} ;
2021-10-14 10:57:49 +00:00
}
function useRedirectToOnboardingIfNeeded() {
const router = useRouter ( ) ;
const query = useMeQuery ( ) ;
const user = query . data ;
2022-04-06 17:07:22 +00:00
const isRedirectingToOnboarding = user && shouldShowOnboarding ( user ) ;
2022-02-20 14:07:15 +00:00
2021-11-08 14:10:02 +00:00
useEffect ( ( ) = > {
if ( isRedirectingToOnboarding ) {
router . replace ( {
pathname : "/getting-started" ,
} ) ;
2021-10-14 10:57:49 +00:00
}
2021-11-08 14:10:02 +00:00
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ isRedirectingToOnboarding ] ) ;
return {
isRedirectingToOnboarding ,
} ;
2021-09-27 14:47:55 +00:00
}
2021-10-12 09:35:44 +00:00
export function ShellSubHeading ( props : {
title : ReactNode ;
subtitle? : ReactNode ;
actions? : ReactNode ;
className? : string ;
} ) {
return (
2022-02-09 00:05:13 +00:00
< div className = { classNames ( "mb-3 block justify-between sm:flex" , props . className ) } >
2021-10-12 09:35:44 +00:00
< div >
2022-08-03 16:01:29 +00:00
< h2 className = "text-brand-500 flex content-center items-center space-x-2 text-base font-bold leading-6 rtl:space-x-reverse" >
2021-10-12 09:35:44 +00:00
{ props . title }
< / h2 >
2022-02-09 00:05:13 +00:00
{ props . subtitle && < p className = "text-sm text-neutral-500 ltr:mr-4" > { props . subtitle } < / p > }
2021-10-12 09:35:44 +00:00
< / div >
2021-12-09 19:37:29 +00:00
{ props . actions && < div className = "flex-shrink-0" > { props . actions } < / div > }
2021-10-12 09:35:44 +00:00
< / div >
) ;
}
2022-04-14 21:49:51 +00:00
const Layout = ( {
status ,
plan ,
. . . props
2022-04-25 17:01:51 +00:00
} : LayoutProps & { status : SessionContextValue [ "status" ] ; plan? : UserPlan ; isLoading : boolean } ) = > {
2022-04-14 02:47:34 +00:00
const isEmbed = useIsEmbed ( ) ;
2021-06-23 15:49:10 +00:00
const router = useRouter ( ) ;
2022-08-13 11:04:57 +00:00
const { data : routingForms } = useApp ( "routing_forms" ) ;
2022-04-26 08:48:17 +00:00
2022-04-14 21:49:51 +00:00
const { t } = useLocale ( ) ;
2021-07-30 23:05:38 +00:00
const navigation = [
{
2021-10-14 13:58:17 +00:00
name : t ( "event_types_page_title" ) ,
2021-07-30 23:05:38 +00:00
href : "/event-types" ,
2022-08-03 16:01:29 +00:00
icon : Icon.FiLink ,
2021-09-29 21:33:18 +00:00
current : router.asPath.startsWith ( "/event-types" ) ,
2021-07-30 23:05:38 +00:00
} ,
{
2021-10-14 13:58:17 +00:00
name : t ( "bookings" ) ,
2021-09-29 21:33:18 +00:00
href : "/bookings/upcoming" ,
2022-08-03 16:01:29 +00:00
icon : Icon.FiCalendar ,
2021-09-29 21:33:18 +00:00
current : router.asPath.startsWith ( "/bookings" ) ,
2021-07-30 23:05:38 +00:00
} ,
{
2021-10-14 13:58:17 +00:00
name : t ( "availability" ) ,
2021-07-30 23:05:38 +00:00
href : "/availability" ,
2022-08-03 16:01:29 +00:00
icon : Icon.FiClock ,
2021-09-29 21:33:18 +00:00
current : router.asPath.startsWith ( "/availability" ) ,
2021-07-30 23:05:38 +00:00
} ,
2022-07-14 12:40:53 +00:00
routingForms
? {
name : "Routing Forms" ,
href : "/apps/routing_forms/forms" ,
icon : CollectionIcon ,
current : router.asPath.startsWith ( "/apps/routing_forms/" ) ,
}
: null ,
2022-07-14 00:10:45 +00:00
{
name : t ( "workflows" ) ,
href : "/workflows" ,
2022-08-03 16:01:29 +00:00
icon : Icon.FiZap ,
2022-07-14 00:10:45 +00:00
current : router.asPath.startsWith ( "/workflows" ) ,
pro : true ,
} ,
2021-07-30 23:05:38 +00:00
{
2022-03-23 22:00:30 +00:00
name : t ( "apps" ) ,
href : "/apps" ,
2022-08-03 16:01:29 +00:00
icon : Icon.FiGrid ,
2022-07-14 12:40:53 +00:00
current : router.asPath.startsWith ( "/apps" ) && ! router . asPath . startsWith ( "/apps/routing_forms/" ) ,
2022-03-23 22:00:30 +00:00
child : [
{
name : t ( "app_store" ) ,
href : "/apps" ,
current : router.asPath === "/apps" ,
} ,
{
name : t ( "installed_apps" ) ,
href : "/apps/installed" ,
current : router.asPath === "/apps/installed" ,
} ,
] ,
2021-07-30 23:05:38 +00:00
} ,
2021-08-02 14:10:24 +00:00
{
2021-10-14 13:58:17 +00:00
name : t ( "settings" ) ,
2021-08-02 14:10:24 +00:00
href : "/settings/profile" ,
2022-08-03 16:01:29 +00:00
icon : Icon.FiSettings ,
2021-09-29 21:33:18 +00:00
current : router.asPath.startsWith ( "/settings" ) ,
2021-08-02 14:10:24 +00:00
} ,
2021-07-30 23:05:38 +00:00
] ;
2021-08-27 12:35:20 +00:00
const pageTitle = typeof props . heading === "string" ? props.heading : props.title ;
2021-09-27 14:47:55 +00:00
return (
2021-08-03 11:13:48 +00:00
< >
2021-08-27 12:35:20 +00:00
< HeadSeo
2021-09-27 14:47:55 +00:00
title = { pageTitle ? ? "Cal.com" }
2021-10-11 09:34:43 +00:00
description = { props . subtitle ? props . subtitle ? . toString ( ) : "" }
2021-08-27 12:35:20 +00:00
nextSeoProps = { {
nofollow : true ,
noindex : true ,
} }
/ >
2021-08-18 08:18:18 +00:00
< div >
< Toaster position = "bottom-right" / >
< / div >
2022-03-23 22:00:30 +00:00
< div
className = { classNames ( "flex h-screen overflow-hidden" , props . large ? "bg-white" : "bg-gray-100" ) }
data - testid = "dashboard-shell" >
{ status === "authenticated" && (
2022-04-14 02:47:34 +00:00
< div style = { isEmbed ? { display : "none" } : { } } className = "hidden md:flex lg:flex-shrink-0" >
2022-03-23 22:00:30 +00:00
< div className = "flex w-14 flex-col lg:w-56" >
2022-08-08 19:39:51 +00:00
< header className = "flex h-0 flex-1 flex-col border-r border-gray-200 bg-white" >
2022-03-23 22:00:30 +00:00
< div className = "flex flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5" >
2022-07-20 09:17:33 +00:00
< div className = "items-center justify-between md:hidden lg:flex" >
2022-07-14 11:32:28 +00:00
< Link href = "/event-types" >
< a className = "px-4" >
< Logo small / >
< / a >
< / Link >
2022-08-08 19:39:51 +00:00
< div className = "flex space-x-2 px-4" >
< button
color = "minimal"
onClick = { ( ) = > window . history . forward ( ) }
className = "desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900" >
< Icon.FiArrowLeft className = "h-4 w-4 flex-shrink-0 text-neutral-400 group-hover:text-neutral-500" / >
< / button >
< button
color = "minimal"
onClick = { ( ) = > window . history . forward ( ) }
className = "desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900" >
< Icon.FiArrowRight className = "h-4 w-4 flex-shrink-0 text-neutral-400 group-hover:text-neutral-500" / >
< / button >
2022-07-14 11:32:28 +00:00
< KBarTrigger / >
< / div >
< / div >
2022-03-23 22:00:30 +00:00
{ /* logo icon for tablet */ }
< Link href = "/event-types" >
2022-05-14 13:49:39 +00:00
< a className = "text-center md:inline lg:hidden" >
2022-03-23 22:00:30 +00:00
< Logo small icon / >
< / a >
< / Link >
2022-08-08 19:39:51 +00:00
< hr className = "desktop-only mt-2.5" / >
2022-08-05 08:46:44 +00:00
< nav className = "mt-2 flex-1 space-y-0.5 bg-white px-2 lg:mt-5" >
2022-07-14 12:40:53 +00:00
{ navigation . map ( ( item ) = >
! item ? null : (
< Fragment key = { item . name } >
< Link href = { item . href } >
< a
aria - label = { item . name }
2022-03-23 22:00:30 +00:00
className = { classNames (
item . current
2022-07-14 12:40:53 +00:00
? "bg-neutral-100 text-neutral-900"
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900" ,
2022-08-05 08:46:44 +00:00
"group flex items-center justify-center rounded py-2.5 px-3 text-sm font-medium sm:justify-start"
2022-07-14 12:40:53 +00:00
) } >
< item.icon
className = { classNames (
item . current
2022-07-27 02:24:00 +00:00
? "text-neutral-900"
: "text-neutral-400 group-hover:text-neutral-900" ,
"h-4 w-4 flex-shrink-0 md:ltr:mr-2 md:rtl:ml-3"
2022-07-14 12:40:53 +00:00
) }
aria - hidden = "true"
/ >
2022-08-05 08:46:44 +00:00
< span className = "hidden leading-none lg:inline" > { item . name } < / span >
2022-07-14 12:40:53 +00:00
{ item . pro && (
< span className = "ml-1" >
{ plan === "FREE" && < Badge variant = "default" > PRO < / Badge > }
< / span >
2022-03-23 22:00:30 +00:00
) }
2022-07-14 12:40:53 +00:00
< / a >
< / Link >
{ item . child &&
router . asPath . startsWith ( item . href ) &&
item . child . map ( ( item ) = > {
return (
< Link key = { item . name } href = { item . href } >
< a
className = { classNames (
item . current
? "text-neutral-900"
: "text-neutral-500 hover:text-neutral-900" ,
"group hidden items-center rounded-sm px-2 py-2 pl-10 text-sm font-medium lg:flex"
) } >
2022-08-05 08:46:44 +00:00
< span className = "hidden leading-none lg:inline" > { item . name } < / span >
2022-07-14 12:40:53 +00:00
< / a >
< / Link >
) ;
} ) }
< / Fragment >
)
) }
2022-07-14 11:32:28 +00:00
< span className = "group flex items-center rounded-sm px-2 py-2 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden" >
< KBarTrigger / >
< / span >
2022-03-23 22:00:30 +00:00
< / nav >
< / div >
< TrialBanner / >
2022-07-27 02:24:00 +00:00
< div data - testid = "user-dropdown-trigger" >
2022-03-23 22:00:30 +00:00
< span className = "hidden lg:inline" >
< UserDropdown / >
< / span >
< span className = "hidden md:inline lg:hidden" >
< UserDropdown small / >
< / span >
< / div >
2022-07-28 19:58:26 +00:00
< DeploymentInfo / >
2022-08-08 19:39:51 +00:00
< / header >
2021-12-01 14:56:25 +00:00
< / div >
< / div >
2022-03-23 22:00:30 +00:00
) }
2021-08-03 11:13:48 +00:00
2022-02-09 00:05:13 +00:00
< div className = "flex w-0 flex-1 flex-col overflow-hidden" >
2022-07-18 18:08:48 +00:00
< ImpersonatingBanner / >
2021-12-09 23:51:30 +00:00
< main
className = { classNames (
2022-03-23 22:00:30 +00:00
"relative z-0 flex-1 overflow-y-auto focus:outline-none" ,
status === "authenticated" && "max-w-[1700px]" ,
2021-12-09 23:51:30 +00:00
props . flexChildrenContainer && "flex flex-col"
) } >
2021-08-03 11:13:48 +00:00
{ /* show top navigation for md and smaller (tablet and phones) */ }
2022-03-23 22:00:30 +00:00
{ status === "authenticated" && (
2022-04-14 02:47:34 +00:00
< nav
style = { isEmbed ? { display : "none" } : { } }
className = "flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden" >
2022-03-23 22:00:30 +00:00
< Link href = "/event-types" >
< a >
< Logo / >
< / a >
< / Link >
2022-07-14 11:32:28 +00:00
< div className = "flex items-center gap-2 self-center" >
< span className = "group flex items-center rounded-full p-2.5 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden" >
< KBarTrigger / >
< / span >
2022-03-23 22:00:30 +00:00
< button className = "rounded-full bg-white p-2 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-07-14 11:32:28 +00:00
< span className = "sr-only" > { t ( "settings" ) } < / span >
2022-03-23 22:00:30 +00:00
< Link href = "/settings/profile" >
< a >
2022-08-03 16:01:29 +00:00
< Icon.FiSettings className = "h-4 w-4" aria - hidden = "true" / >
2022-03-23 22:00:30 +00:00
< / a >
< / Link >
< / button >
< UserDropdown small / >
< / div >
< / nav >
) }
2021-12-09 23:51:30 +00:00
< div
className = { classNames (
2022-02-09 00:05:13 +00:00
props . centered && "mx-auto md:max-w-5xl" ,
2022-08-08 19:39:51 +00:00
props . flexChildrenContainer && "flex flex-1 flex-col"
2021-12-09 23:51:30 +00:00
) } >
2022-01-12 09:29:20 +00:00
{ ! ! props . backPath && (
2021-12-09 23:51:30 +00:00
< div className = "mx-3 mb-8 sm:mx-8" >
2022-01-12 09:29:20 +00:00
< Button
onClick = { ( ) = > router . push ( props . backPath as string ) }
2022-08-03 16:01:29 +00:00
StartIcon = { Icon . FiArrowLeft }
2022-01-12 09:29:20 +00:00
color = "secondary" >
2021-12-09 23:51:30 +00:00
Back
< / Button >
< / div >
) }
2022-04-03 11:15:31 +00:00
{ props . heading && (
2022-08-08 19:39:51 +00:00
< header
2022-03-23 22:00:30 +00:00
className = { classNames (
props . large && "bg-gray-100 py-8 lg:mb-8 lg:pt-16 lg:pb-7" ,
2022-08-08 19:39:51 +00:00
"block justify-between px-4 pt-8 sm:flex sm:px-6 md:px-8"
2022-03-23 22:00:30 +00:00
) } >
{ props . HeadingLeftIcon && < div className = "ltr:mr-4" > { props . HeadingLeftIcon } < / div > }
< div className = "mb-8 w-full" >
2022-04-26 02:38:41 +00:00
{ props . isLoading ? (
< >
2022-07-13 21:14:16 +00:00
< div className = "mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200" / >
< div className = "mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200" / >
2022-04-26 02:38:41 +00:00
< / >
) : (
< >
< h1 className = "font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900" >
{ props . heading }
< / h1 >
2022-07-14 12:40:53 +00:00
< p className = "text-sm text-neutral-500 ltr:mr-4 rtl:ml-4" > { props . subtitle } < / p >
2022-04-26 02:38:41 +00:00
< / >
) }
2022-03-23 22:00:30 +00:00
< / div >
{ props . CTA && < div className = "mb-4 flex-shrink-0" > { props . CTA } < / div > }
2022-08-08 19:39:51 +00:00
< / header >
2022-03-23 22:00:30 +00:00
) }
2021-12-09 23:51:30 +00:00
< div
className = { classNames (
"px-4 sm:px-6 md:px-8" ,
2022-02-09 00:05:13 +00:00
props . flexChildrenContainer && "flex flex-1 flex-col"
2021-12-09 23:51:30 +00:00
) } >
2022-05-26 17:07:14 +00:00
< ErrorBoundary > { ! props . isLoading ? props.children : props.customLoader } < / ErrorBoundary >
2021-12-09 23:51:30 +00:00
< / div >
2021-08-03 11:13:48 +00:00
{ /* show bottom navigation for md and smaller (tablet and phones) */ }
2022-03-23 22:00:30 +00:00
{ status === "authenticated" && (
2022-04-14 02:47:34 +00:00
< nav
style = { isEmbed ? { display : "none" } : { } }
className = "bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden" >
2022-03-23 22:00:30 +00:00
{ /* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */ }
2022-07-14 12:40:53 +00:00
{ navigation . flatMap ( ( item , itemIdx ) = > {
if ( ! item ) {
return null ;
}
return item . href === "/settings/profile" ? (
2022-03-23 22:00:30 +00:00
[ ]
) : (
< Link key = { item . name } href = { item . href } >
< a
2021-08-03 11:13:48 +00:00
className = { classNames (
2022-03-23 22:00:30 +00:00
item . current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700" ,
itemIdx === 0 ? "rounded-l-lg" : "" ,
itemIdx === navigation . length - 1 ? "rounded-r-lg" : "" ,
2022-07-27 02:24:00 +00:00
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs text-sm font-medium hover:bg-gray-50 focus:z-10"
2021-08-03 11:13:48 +00:00
) }
2022-03-23 22:00:30 +00:00
aria - current = { item . current ? "page" : undefined } >
< item.icon
className = { classNames (
item . current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500" ,
2022-07-27 02:24:00 +00:00
"mx-auto mb-1 block h-4 w-4 flex-shrink-0 text-center"
2022-03-23 22:00:30 +00:00
) }
aria - hidden = "true"
/ >
2022-07-14 12:40:53 +00:00
< span className = "block truncate" > { item . name } < / span >
2022-03-23 22:00:30 +00:00
< / a >
< / Link >
2022-07-14 12:40:53 +00:00
) ;
} ) }
2022-03-23 22:00:30 +00:00
< / nav >
) }
2021-08-03 11:13:48 +00:00
{ /* add padding to content for mobile navigation*/ }
2021-10-13 14:00:40 +00:00
< div className = "block pt-12 md:hidden" / >
2021-08-03 11:13:48 +00:00
< / div >
2021-09-30 23:42:08 +00:00
< LicenseBanner / >
2021-08-03 11:13:48 +00:00
< / main >
< / div >
2021-06-23 15:49:10 +00:00
< / div >
2021-08-03 11:13:48 +00:00
< / >
2021-09-27 14:47:55 +00:00
) ;
2022-04-14 21:49:51 +00:00
} ;
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 ;
2022-04-25 17:01:51 +00:00
customLoader? : ReactNode ;
2022-04-14 21:49:51 +00:00
} ;
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 ( ) ;
2022-07-15 05:47:37 +00:00
const isLoading = isRedirectingToOnboarding || loading ;
2022-04-25 17:01:51 +00:00
2022-07-11 14:37:20 +00:00
// 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" ) {
2022-04-14 21:49:51 +00:00
return (
< div className = "absolute z-50 flex h-screen w-full items-center bg-gray-50" >
< Loader / >
< / div >
) ;
}
if ( ! session && ! props . isPublic ) return null ;
return (
2022-07-14 06:45:07 +00:00
< KBarRoot >
2022-04-14 21:49:51 +00:00
< CustomBranding lightVal = { user ? . brandColor } darkVal = { user ? . darkBrandColor } / >
2022-04-25 17:01:51 +00:00
< MemoizedLayout plan = { user ? . plan } status = { status } { ...props } isLoading = { isLoading } / >
2022-07-14 06:45:07 +00:00
< KBarContent / >
< / KBarRoot >
2022-04-14 21:49:51 +00:00
) ;
2021-06-23 15:49:10 +00:00
}
2021-08-02 14:10:24 +00:00
2021-10-13 14:00:40 +00:00
function UserDropdown ( { small } : { small? : boolean } ) {
2021-10-15 10:53:42 +00:00
const { t } = useLocale ( ) ;
2021-09-27 14:47:55 +00:00
const query = useMeQuery ( ) ;
const user = query . data ;
2022-07-22 14:38:02 +00:00
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-01-12 13:46:24 +00:00
const mutation = trpc . useMutation ( "viewer.away" , {
onSettled() {
utils . invalidateQueries ( "viewer.me" ) ;
} ,
} ) ;
2022-01-11 10:32:40 +00:00
const utils = trpc . useContext ( ) ;
2022-05-24 13:29:39 +00:00
const [ helpOpen , setHelpOpen ] = useState ( false ) ;
2022-06-20 15:29:48 +00:00
const [ menuOpen , setMenuOpen ] = useState ( false ) ;
2022-07-14 12:40:53 +00:00
if ( ! user ) {
return null ;
}
2022-06-20 15:29:48 +00:00
const onHelpItemSelect = ( ) = > {
setHelpOpen ( false ) ;
setMenuOpen ( false ) ;
} ;
2022-05-24 13:29:39 +00:00
2022-07-11 14:37:20 +00:00
// Prevent rendering dropdown if user isn't available.
// We don't want to show nameless user.
if ( ! user ) {
return null ;
}
2021-11-16 01:08:04 +00:00
return (
2022-06-20 15:29:48 +00:00
< Dropdown open = { menuOpen } onOpenChange = { ( ) = > setHelpOpen ( false ) } >
< DropdownMenuTrigger asChild onClick = { ( ) = > setMenuOpen ( true ) } >
2022-07-27 02:24:00 +00:00
< 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" >
2021-12-09 11:53:34 +00:00
< span
2022-01-11 10:32:40 +00:00
className = { classNames (
2022-07-27 02:24:00 +00:00
small ? "h-8 w-8" : "h-9 w-9 ltr:mr-2 rtl:ml-3" ,
"relative flex-shrink-0 rounded-full bg-gray-300 "
2022-01-11 10:32:40 +00:00
) } >
2022-05-14 13:49:39 +00:00
{
// eslint-disable-next-line @next/next/no-img-element
< img
className = "rounded-full"
2022-07-14 12:40:53 +00:00
src = { WEBAPP_URL + "/" + user . username + "/avatar.png" }
alt = { user . username || "Nameless User" }
2022-05-14 13:49:39 +00:00
/ >
}
2022-07-14 12:40:53 +00:00
{ ! user . away && (
2022-07-13 21:14:16 +00:00
< div className = "absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-green-500" / >
2022-01-11 10:32:40 +00:00
) }
2022-07-14 12:40:53 +00:00
{ user . away && (
2022-07-13 21:14:16 +00:00
< div className = "absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-yellow-500" / >
2022-01-11 10:32:40 +00:00
) }
2021-12-09 11:53:34 +00:00
< / span >
2021-10-17 12:13:24 +00:00
{ ! small && (
2022-02-09 00:05:13 +00:00
< span className = "flex flex-grow items-center truncate" >
< span className = "flex-grow truncate text-sm" >
< span className = "block truncate font-medium text-gray-900" >
2022-07-14 12:40:53 +00:00
{ user . name || "Nameless User" }
2021-11-16 01:08:04 +00:00
< / span >
2022-02-09 00:05:13 +00:00
< span className = "block truncate font-normal text-neutral-500" >
2022-07-14 12:40:53 +00:00
{ user . username
2022-06-25 05:25:17 +00:00
? process . env . NEXT_PUBLIC_WEBSITE_URL === "https://cal.com"
? ` cal.com/ ${ user . username } `
: ` / ${ user . username } `
: "No public page" }
2021-11-16 01:08:04 +00:00
< / span >
2021-10-17 12:13:24 +00:00
< / span >
2022-08-03 16:01:29 +00:00
< Icon.FiMoreVertical
2022-07-27 02:24:00 +00:00
className = "h-4 w-4 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
2021-10-17 12:13:24 +00:00
aria - hidden = "true"
/ >
2021-12-09 11:53:34 +00:00
< / span >
2021-10-17 12:13:24 +00:00
) }
2022-03-18 18:22:56 +00:00
< / button >
2021-10-17 12:13:24 +00:00
< / DropdownMenuTrigger >
2022-06-20 15:29:48 +00:00
< DropdownMenuContent portalled = { true } onInteractOutside = { ( ) = > setMenuOpen ( false ) } >
2022-05-24 13:29:39 +00:00
{ helpOpen ? (
2022-06-20 15:29:48 +00:00
< HelpMenuItem onHelpItemSelect = { ( ) = > onHelpItemSelect ( ) } / >
2022-05-24 13:29:39 +00:00
) : (
< >
< DropdownMenuItem >
< a
onClick = { ( ) = > {
2022-07-21 00:32:46 +00:00
mutation . mutate ( { away : ! user ? . away } ) ;
2022-05-24 13:29:39 +00:00
utils . invalidateQueries ( "viewer.me" ) ;
} }
2022-07-27 02:24:00 +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" >
2022-08-03 16:01:29 +00:00
< Icon.FiMoon
2022-05-24 13:29:39 +00:00
className = { classNames (
2022-07-14 12:40:53 +00:00
user . away
2022-05-24 13:29:39 +00:00
? "text-purple-500 group-hover:text-purple-700"
: "text-gray-500 group-hover:text-gray-700" ,
2022-07-27 02:24:00 +00:00
"h-4 w-4 flex-shrink-0 ltr:mr-2 rtl:ml-3"
2022-05-24 13:29:39 +00:00
) }
aria - hidden = "true"
/ >
2022-07-14 12:40:53 +00:00
{ user . away ? t ( "set_as_free" ) : t ( "set_as_away" ) }
2022-05-24 13:29:39 +00:00
< / a >
< / DropdownMenuItem >
< DropdownMenuSeparator className = "h-px bg-gray-200" / >
2022-07-14 12:40:53 +00:00
{ user . username && (
2022-05-24 13:29:39 +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" >
2022-08-03 16:01:29 +00:00
< Icon.FiExternalLink className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { " " }
2022-05-24 13:29:39 +00:00
{ t ( "view_public_page" ) }
< / a >
< / DropdownMenuItem >
) }
< DropdownMenuSeparator className = "h-px bg-gray-200" / >
< DropdownMenuItem >
< a
2022-07-01 17:19:52 +00:00
href = { JOIN_SLACK }
2022-05-24 13:29:39 +00:00
target = "_blank"
rel = "noreferrer"
2022-07-27 02:24:00 +00:00
className = "flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900" >
2022-08-03 16:01:29 +00:00
< Icon.FiSlack strokeWidth = { 1.5 } className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { " " }
2022-05-24 13:29:39 +00:00
{ t ( "join_our_slack" ) }
< / a >
< / DropdownMenuItem >
< DropdownMenuItem >
< a
target = "_blank"
rel = "noopener noreferrer"
2022-07-01 17:19:52 +00:00
href = { ROADMAP }
2022-05-24 13:29:39 +00:00
className = "flex items-center px-4 py-2 text-sm text-gray-700" >
2022-08-03 16:01:29 +00:00
< Icon.FiMap className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { t ( "visit_roadmap" ) }
2022-05-24 13:29:39 +00:00
< / a >
< / DropdownMenuItem >
< button
2022-07-27 02:24:00 +00:00
className = "flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
2022-05-24 13:29:39 +00:00
onClick = { ( ) = > setHelpOpen ( true ) } >
2022-08-03 16:01:29 +00:00
< Icon.FiHelpCircle
2022-05-24 13:29:39 +00:00
className = { classNames (
"text-gray-500 group-hover:text-neutral-500" ,
2022-07-27 02:24:00 +00:00
"h-4 w-4 flex-shrink-0 ltr:mr-2"
2022-05-24 13:29:39 +00:00
) }
aria - hidden = "true"
/ >
{ t ( "help" ) }
< / button >
2022-08-08 19:39:51 +00:00
< DropdownMenuItem >
< a
target = "_blank"
rel = "noopener noreferrer"
href = { ROADMAP }
className = "desktop-hidden flex items-center px-4 py-2 text-sm text-gray-700" >
< Icon.FiDownload className = "h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-3" / > { " " }
{ t ( "download_desktop_app" ) }
< / a >
< / DropdownMenuItem >
2022-05-24 13:29:39 +00:00
< DropdownMenuSeparator className = "h-px bg-gray-200" / >
< DropdownMenuItem >
< a
onClick = { ( ) = > signOut ( { callbackUrl : "/auth/logout" } ) }
2022-07-27 02:24:00 +00:00
className = "flex cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900" >
2022-08-03 16:01:29 +00:00
< Icon.FiLogOut
2022-05-24 13:29:39 +00:00
className = { classNames (
"text-gray-500 group-hover:text-gray-700" ,
2022-07-27 02:24:00 +00:00
"h-4 w-4 flex-shrink-0 ltr:mr-2 rtl:ml-3"
2022-05-24 13:29:39 +00:00
) }
aria - hidden = "true"
/ >
{ t ( "sign_out" ) }
< / a >
< / DropdownMenuItem >
< / >
2021-11-16 01:08:04 +00:00
) }
2021-10-17 12:13:24 +00:00
< / DropdownMenuContent >
< / Dropdown >
2021-11-16 01:08:04 +00:00
) ;
2021-08-02 17:40:13 +00:00
}
2022-07-28 19:58:26 +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 >
) ;
}