From 9dd97a0c1fc0c247b7e4215c2e7f4b939964e326 Mon Sep 17 00:00:00 2001 From: sean-brydon Date: Tue, 1 Oct 2024 10:40:26 +0100 Subject: [PATCH 01/17] banners --- packages/features/shell/Shell.tsx | 186 +----------------- .../features/shell/banners/LayoutBanner.tsx | 69 +++++++ packages/features/shell/banners/useBanners.ts | 45 +++++ 3 files changed, 119 insertions(+), 181 deletions(-) create mode 100644 packages/features/shell/banners/LayoutBanner.tsx create mode 100644 packages/features/shell/banners/useBanners.ts diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 247f443f9805e2..1650a298b10ff0 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -7,36 +7,14 @@ import type { Dispatch, ReactElement, ReactNode, SetStateAction } from "react"; import React, { cloneElement, Fragment, useEffect, useMemo, useState } from "react"; import { Toaster } from "react-hot-toast"; -import dayjs from "@calcom/dayjs"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge"; -import ImpersonatingBanner, { - type ImpersonatingBannerProps, -} from "@calcom/features/ee/impersonation/components/ImpersonatingBanner"; -import { - OrgUpgradeBanner, - type OrgUpgradeBannerProps, -} from "@calcom/features/ee/organizations/components/OrgUpgradeBanner"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; import useIntercom, { isInterComEnabled } from "@calcom/features/ee/support/lib/intercom/useIntercom"; -import { TeamsUpgradeBanner, type TeamsUpgradeBannerProps } from "@calcom/features/ee/teams/components"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar"; import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog"; -import AdminPasswordBanner, { - type AdminPasswordBannerProps, -} from "@calcom/features/users/components/AdminPasswordBanner"; -import CalendarCredentialBanner, { - type CalendarCredentialBannerProps, -} from "@calcom/features/users/components/CalendarCredentialBanner"; -import { - InvalidAppCredentialBanners, - type InvalidAppCredentialBannersProps, -} from "@calcom/features/users/components/InvalidAppCredentialsBanner"; -import VerifyEmailBanner, { - type VerifyEmailBannerProps, -} from "@calcom/features/users/components/VerifyEmailBanner"; import classNames from "@calcom/lib/classNames"; import { APP_NAME, @@ -46,8 +24,6 @@ import { IS_VISUAL_REGRESSION_TESTING, JOIN_COMMUNITY, ROADMAP, - TOP_BANNER_HEIGHT, - WEBAPP_URL, } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useFormbricks } from "@calcom/lib/formbricks-client"; @@ -60,9 +36,7 @@ import { useRefreshData } from "@calcom/lib/hooks/useRefreshData"; import useTheme from "@calcom/lib/hooks/useTheme"; import { isKeyInObject } from "@calcom/lib/isKeyInObject"; import { localStorage } from "@calcom/lib/webstorage"; -import type { User } from "@calcom/prisma/client"; import { trpc } from "@calcom/trpc/react"; -import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { Avatar, @@ -91,133 +65,17 @@ import { useGetUserAttributes } from "@calcom/web/components/settings/platform/h import { useOrgBranding } from "../ee/organizations/context/provider"; import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider"; import { TeamInviteBadge } from "./TeamInviteBadge"; +import { BannerContainer } from "./banners/LayoutBanner"; +import { useBanners } from "./banners/useBanners"; // need to import without ssr to prevent hydration errors const Tips = dynamic(() => import("@calcom/features/tips").then((mod) => mod.Tips), { ssr: false, }); -/* TODO: Migate this */ - -export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString(); - -export const ONBOARDING_NEXT_REDIRECT = { - redirect: { - permanent: false, - destination: "/getting-started", - }, -} as const; - -export const shouldShowOnboarding = ( - user: Pick & { - organizationId: number | null; - } -) => { - return ( - !user.completedOnboarding && - !user.organizationId && - 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) { - const urlSearchParams = new URLSearchParams(); - urlSearchParams.set("callbackUrl", `${WEBAPP_URL}${location.pathname}${location.search}`); - router.replace(`/auth/login?${urlSearchParams.toString()}`); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, session, isPublic]); - - return { - loading: loading && !session, - session, - }; -} - -type BannerTypeProps = { - teamUpgradeBanner: TeamsUpgradeBannerProps; - orgUpgradeBanner: OrgUpgradeBannerProps; - verifyEmailBanner: VerifyEmailBannerProps; - adminPasswordBanner: AdminPasswordBannerProps; - impersonationBanner: ImpersonatingBannerProps; - calendarCredentialBanner: CalendarCredentialBannerProps; - invalidAppCredentialBanners: InvalidAppCredentialBannersProps; -}; - -type BannerType = keyof BannerTypeProps; - -type BannerComponent = { - [Key in BannerType]: (props: BannerTypeProps[Key]) => JSX.Element; -}; - -const BannerComponent: BannerComponent = { - teamUpgradeBanner: (props: TeamsUpgradeBannerProps) => , - orgUpgradeBanner: (props: OrgUpgradeBannerProps) => , - verifyEmailBanner: (props: VerifyEmailBannerProps) => , - adminPasswordBanner: (props: AdminPasswordBannerProps) => , - impersonationBanner: (props: ImpersonatingBannerProps) => , - calendarCredentialBanner: (props: CalendarCredentialBannerProps) => , - invalidAppCredentialBanners: (props: InvalidAppCredentialBannersProps) => ( - - ), -}; - -function useRedirectToOnboardingIfNeeded() { - const router = useRouter(); - const query = useMeQuery(); - const user = query.data; - const flags = useFlagMap(); - - const { data: email } = useEmailVerifyCheck(); - - const needsEmailVerification = !email?.isVerified && flags["email-verification"]; - - const isRedirectingToOnboarding = user && shouldShowOnboarding(user); - - useEffect(() => { - if (isRedirectingToOnboarding && !needsEmailVerification) { - router.replace("/getting-started"); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isRedirectingToOnboarding, needsEmailVerification]); - - return { - isRedirectingToOnboarding, - }; -} - -type allBannerProps = { [Key in BannerType]: BannerTypeProps[Key]["data"] }; - -const useBanners = () => { - const { data: getUserTopBanners, isPending } = trpc.viewer.getUserTopBanners.useQuery(); - const { data: userSession } = useSession(); - - if (isPending || !userSession) return null; - - const isUserInactiveAdmin = userSession?.user.role === "INACTIVE_ADMIN"; - const userImpersonatedByUID = userSession?.user.impersonatedBy?.id; - - const userSessionBanners = { - adminPasswordBanner: isUserInactiveAdmin ? userSession : null, - impersonationBanner: userImpersonatedByUID ? userSession : null, - }; - - const allBanners: allBannerProps = Object.assign({}, getUserTopBanners, userSessionBanners); - - return allBanners; -}; - const Layout = (props: LayoutProps) => { - const banners = useBanners(); + const { banners, bannersHeight } = useBanners(); + const pathname = usePathname(); const isFullPageWithoutSidebar = pathname?.startsWith("/apps/routing-forms/reporting/"); const { data: user } = trpc.viewer.me.useQuery(); @@ -232,15 +90,6 @@ const Layout = (props: LayoutProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [user]); - const bannersHeight = useMemo(() => { - const activeBanners = - banners && - Object.entries(banners).filter(([_, value]) => { - return value && (!Array.isArray(value) || value.length > 0); - }); - return (activeBanners?.length ?? 0) * TOP_BANNER_HEIGHT; - }, [banners]); - useFormbricks(); return ( @@ -259,32 +108,7 @@ const Layout = (props: LayoutProps) => {
{banners && !props.isPlatformUser && !isFullPageWithoutSidebar && ( -
- {Object.keys(banners).map((key) => { - if (key === "teamUpgradeBanner") { - const Banner = BannerComponent[key]; - return ; - } else if (key === "orgUpgradeBanner") { - const Banner = BannerComponent[key]; - return ; - } else if (key === "verifyEmailBanner") { - const Banner = BannerComponent[key]; - return ; - } else if (key === "adminPasswordBanner") { - const Banner = BannerComponent[key]; - return ; - } else if (key === "impersonationBanner") { - const Banner = BannerComponent[key]; - return ; - } else if (key === "calendarCredentialBanner") { - const Banner = BannerComponent[key]; - return ; - } else if (key === "invalidAppCredentialBanners") { - const Banner = BannerComponent[key]; - return ; - } - })} -
+ )}
diff --git a/packages/features/shell/banners/LayoutBanner.tsx b/packages/features/shell/banners/LayoutBanner.tsx new file mode 100644 index 00000000000000..5a894f427a07ec --- /dev/null +++ b/packages/features/shell/banners/LayoutBanner.tsx @@ -0,0 +1,69 @@ +import ImpersonatingBanner, { + type ImpersonatingBannerProps, +} from "@calcom/features/ee/impersonation/components/ImpersonatingBanner"; +import { + OrgUpgradeBanner, + type OrgUpgradeBannerProps, +} from "@calcom/features/ee/organizations/components/OrgUpgradeBanner"; +import { TeamsUpgradeBanner, type TeamsUpgradeBannerProps } from "@calcom/features/ee/teams/components"; +import AdminPasswordBanner, { + type AdminPasswordBannerProps, +} from "@calcom/features/users/components/AdminPasswordBanner"; +import CalendarCredentialBanner, { + type CalendarCredentialBannerProps, +} from "@calcom/features/users/components/CalendarCredentialBanner"; +import { + InvalidAppCredentialBanners, + type InvalidAppCredentialBannersProps, +} from "@calcom/features/users/components/InvalidAppCredentialsBanner"; +import VerifyEmailBanner, { + type VerifyEmailBannerProps, +} from "@calcom/features/users/components/VerifyEmailBanner"; + +type BannerTypeProps = { + teamUpgradeBanner: TeamsUpgradeBannerProps; + orgUpgradeBanner: OrgUpgradeBannerProps; + verifyEmailBanner: VerifyEmailBannerProps; + adminPasswordBanner: AdminPasswordBannerProps; + impersonationBanner: ImpersonatingBannerProps; + calendarCredentialBanner: CalendarCredentialBannerProps; + invalidAppCredentialBanners: InvalidAppCredentialBannersProps; +}; + +type BannerType = keyof BannerTypeProps; + +type BannerComponent = { + [Key in BannerType]: (props: BannerTypeProps[Key]) => JSX.Element; +}; + +export type AllBannerProps = { [Key in BannerType]: BannerTypeProps[Key]["data"] }; + +export const BannerComponent: BannerComponent = { + teamUpgradeBanner: (props: TeamsUpgradeBannerProps) => , + orgUpgradeBanner: (props: OrgUpgradeBannerProps) => , + verifyEmailBanner: (props: VerifyEmailBannerProps) => , + adminPasswordBanner: (props: AdminPasswordBannerProps) => , + impersonationBanner: (props: ImpersonatingBannerProps) => , + calendarCredentialBanner: (props: CalendarCredentialBannerProps) => , + invalidAppCredentialBanners: (props: InvalidAppCredentialBannersProps) => ( + + ), +}; + +interface BannerContainerProps { + banners: AllBannerProps; +} + +export const BannerContainer: React.FC = ({ banners }) => { + return ( +
+ {Object.entries(banners).map(([key, data]) => { + const BannerComponentToRender = BannerComponent[key as BannerType]; + if (BannerComponentToRender) { + return ; + } + return null; + })} +
+ ); +}; diff --git a/packages/features/shell/banners/useBanners.ts b/packages/features/shell/banners/useBanners.ts new file mode 100644 index 00000000000000..a1511b547a614b --- /dev/null +++ b/packages/features/shell/banners/useBanners.ts @@ -0,0 +1,45 @@ +import { useSession } from "next-auth/react"; + +import { trpc } from "@calcom/trpc/react"; + +import { type AllBannerProps } from "./LayoutBanner"; + +const _useBanners = () => { + const { data: getUserTopBanners, isPending } = trpc.viewer.getUserTopBanners.useQuery(); + const { data: userSession } = useSession(); + + if (isPending || !userSession) return null; + + const isUserInactiveAdmin = userSession?.user.role === "INACTIVE_ADMIN"; + const userImpersonatedByUID = userSession?.user.impersonatedBy?.id; + + const userSessionBanners = { + adminPasswordBanner: isUserInactiveAdmin ? userSession : null, + impersonationBanner: userImpersonatedByUID ? userSession : null, + }; + + const allBanners: AllBannerProps = Object.assign({}, getUserTopBanners, userSessionBanners); + + return allBanners; +}; + +const useBannersHeight = (banners: AllBannerProps) => { + const bannersHeight = useMemo(() => { + const activeBanners = + banners && + Object.entries(banners).filter(([_, value]) => { + return value && (!Array.isArray(value) || value.length > 0); + }); + return (activeBanners?.length ?? 0) * TOP_BANNER_HEIGHT; + }, [banners]); + + return bannersHeight; +}; + +const useBanners = () => { + const banners = _useBanners(); + const bannersHeight = useBannersHeight(banners); + return { banners, bannersHeight }; +}; + +export { useBanners, useBannersHeight }; From bf1174a09f5ea212c4b7c190bcfd8b9f500df4c9 Mon Sep 17 00:00:00 2001 From: sean-brydon Date: Tue, 1 Oct 2024 10:40:40 +0100 Subject: [PATCH 02/17] useAuthHooks --- .../useRedirectToLoginIfUnauthenticated.tsx | 26 ++++++++++ .../hooks/useRedirectToOnboardingIfNeeded.tsx | 50 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx create mode 100644 packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx diff --git a/packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx b/packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx new file mode 100644 index 00000000000000..cba5f53438c73b --- /dev/null +++ b/packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx @@ -0,0 +1,26 @@ +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +export function useRedirectToLoginIfUnauthenticated(isPublic = false) { + const { data: session, status } = useSession(); + const loading = status === "loading"; + const router = useRouter(); + useEffect(() => { + if (isPublic) { + return; + } + + if (!loading && !session) { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.set("callbackUrl", `${WEBAPP_URL}${location.pathname}${location.search}`); + router.replace(`/auth/login?${urlSearchParams.toString()}`); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loading, session, isPublic]); + + return { + loading: loading && !session, + session, + }; +} diff --git a/packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx b/packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx new file mode 100644 index 00000000000000..efaa5a4b5a270b --- /dev/null +++ b/packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx @@ -0,0 +1,50 @@ +import { useRouter } from "next/navigation"; + +import { useEmailVerifyCheck } from "@calcom/features/auth/lib/hooks/useEmailVerifyCheck"; +import { useFlagMap } from "@calcom/features/ee/flags/useFlags"; +import { useMeQuery } from "@calcom/trpc/react"; + +const shouldShowOnboarding = ( + user: Pick & { + organizationId: number | null; + } +) => { + return ( + !user.completedOnboarding && + !user.organizationId && + dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT) + ); +}; + +export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString(); + +export const ONBOARDING_NEXT_REDIRECT = { + redirect: { + permanent: false, + destination: "/getting-started", + }, +} as const; + +export function useRedirectToOnboardingIfNeeded() { + const router = useRouter(); + const query = useMeQuery(); + const user = query.data; + const flags = useFlagMap(); + + const { data: email } = useEmailVerifyCheck(); + + const needsEmailVerification = !email?.isVerified && flags["email-verification"]; + + const isRedirectingToOnboarding = user && shouldShowOnboarding(user); + + useEffect(() => { + if (isRedirectingToOnboarding && !needsEmailVerification) { + router.replace("/getting-started"); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isRedirectingToOnboarding, needsEmailVerification]); + + return { + isRedirectingToOnboarding, + }; +} From 3b7cf1753318cca8fd4f27b8f7cafc5ea568e484 Mon Sep 17 00:00:00 2001 From: sean-brydon Date: Tue, 1 Oct 2024 10:55:09 +0100 Subject: [PATCH 03/17] fixes to redirect and banner --- .../lib/hooks/useRedirectToLoginIfUnauthenticated.tsx | 2 ++ .../auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx | 8 +++++--- packages/features/shell/banners/useBanners.ts | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx b/packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx index cba5f53438c73b..84cb2afdd9b68c 100644 --- a/packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx +++ b/packages/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated.tsx @@ -2,6 +2,8 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; +import { WEBAPP_URL } from "@calcom/lib/constants"; + export function useRedirectToLoginIfUnauthenticated(isPublic = false) { const { data: session, status } = useSession(); const loading = status === "loading"; diff --git a/packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx b/packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx index efaa5a4b5a270b..41daddf8117343 100644 --- a/packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx +++ b/packages/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded.tsx @@ -1,8 +1,10 @@ import { useRouter } from "next/navigation"; +import { useEffect } from "react"; -import { useEmailVerifyCheck } from "@calcom/features/auth/lib/hooks/useEmailVerifyCheck"; -import { useFlagMap } from "@calcom/features/ee/flags/useFlags"; -import { useMeQuery } from "@calcom/trpc/react"; +import dayjs from "@calcom/dayjs"; +import { useFlagMap } from "@calcom/features/flags/context/provider"; +import { useEmailVerifyCheck } from "@calcom/trpc/react/hooks/useEmailVerifyCheck"; +import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; const shouldShowOnboarding = ( user: Pick & { diff --git a/packages/features/shell/banners/useBanners.ts b/packages/features/shell/banners/useBanners.ts index a1511b547a614b..0aab3d66672072 100644 --- a/packages/features/shell/banners/useBanners.ts +++ b/packages/features/shell/banners/useBanners.ts @@ -1,5 +1,7 @@ import { useSession } from "next-auth/react"; +import { useMemo } from "react"; +import { TOP_BANNER_HEIGHT } from "@calcom/lib/constants"; import { trpc } from "@calcom/trpc/react"; import { type AllBannerProps } from "./LayoutBanner"; From d76be7b868e2058df0cf56d7a8650967a95f6a4f Mon Sep 17 00:00:00 2001 From: sean-brydon Date: Tue, 1 Oct 2024 10:55:55 +0100 Subject: [PATCH 04/17] extract useIntercom to custom hook --- .../ee/support/lib/intercom/useIntercom.ts | 13 +++++++++++++ packages/features/shell/Shell.tsx | 16 ++++------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/features/ee/support/lib/intercom/useIntercom.ts b/packages/features/ee/support/lib/intercom/useIntercom.ts index b87367836f7c1b..a24d68c0ad47b5 100644 --- a/packages/features/ee/support/lib/intercom/useIntercom.ts +++ b/packages/features/ee/support/lib/intercom/useIntercom.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line no-restricted-imports import { noop } from "lodash"; +import { useEffect } from "react"; import { useIntercom as useIntercomLib } from "react-use-intercom"; import { z } from "zod"; @@ -117,4 +118,16 @@ export const useIntercom = () => { return { ...hookData, open, boot }; }; +export const useBootIntercom = () => { + const { boot } = useIntercom(); + const { data: user } = trpc.viewer.me.useQuery(); + useEffect(() => { + // not using useMediaQuery as it toggles between true and false + const showIntercom = localStorage.getItem("showIntercom"); + if (!isInterComEnabled || showIntercom === "false" || window.innerWidth <= 768 || !user) return; + boot(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user]); +}; + export default useIntercom; diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 1650a298b10ff0..b0c43dcd984331 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -8,10 +8,12 @@ import React, { cloneElement, Fragment, useEffect, useMemo, useState } from "rea import { Toaster } from "react-hot-toast"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; +import { useRedirectToLoginIfUnauthenticated } from "@calcom/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated"; +import { useRedirectToOnboardingIfNeeded } from "@calcom/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded"; import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; -import useIntercom, { isInterComEnabled } from "@calcom/features/ee/support/lib/intercom/useIntercom"; +import { useBootIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar"; import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog"; @@ -35,7 +37,6 @@ import { ButtonState, useNotifications } from "@calcom/lib/hooks/useNotification import { useRefreshData } from "@calcom/lib/hooks/useRefreshData"; import useTheme from "@calcom/lib/hooks/useTheme"; import { isKeyInObject } from "@calcom/lib/isKeyInObject"; -import { localStorage } from "@calcom/lib/webstorage"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { @@ -78,18 +79,9 @@ const Layout = (props: LayoutProps) => { const pathname = usePathname(); const isFullPageWithoutSidebar = pathname?.startsWith("/apps/routing-forms/reporting/"); - const { data: user } = trpc.viewer.me.useQuery(); - const { boot } = useIntercom(); const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title; - useEffect(() => { - // not using useMediaQuery as it toggles between true and false - const showIntercom = localStorage.getItem("showIntercom"); - if (!isInterComEnabled || showIntercom === "false" || window.innerWidth <= 768 || !user) return; - boot(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user]); - + useBootIntercom(); useFormbricks(); return ( From 71dd810a49fe9a62a8c47028f8c5533db3d9ac79 Mon Sep 17 00:00:00 2001 From: sean-brydon Date: Tue, 1 Oct 2024 10:59:16 +0100 Subject: [PATCH 05/17] use app theme --- .../ee/support/lib/intercom/useIntercom.ts | 1 + packages/features/shell/Shell.tsx | 14 +------------- packages/features/shell/useAppTheme.ts | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 packages/features/shell/useAppTheme.ts diff --git a/packages/features/ee/support/lib/intercom/useIntercom.ts b/packages/features/ee/support/lib/intercom/useIntercom.ts index a24d68c0ad47b5..d639137feb644a 100644 --- a/packages/features/ee/support/lib/intercom/useIntercom.ts +++ b/packages/features/ee/support/lib/intercom/useIntercom.ts @@ -7,6 +7,7 @@ import { z } from "zod"; import dayjs from "@calcom/dayjs"; import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants"; import { useHasTeamPlan, useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan"; +import { localStorage } from "@calcom/lib/webstorage"; import { trpc } from "@calcom/trpc/react"; // eslint-disable-next-line turbo/no-undeclared-env-vars diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index b0c43dcd984331..011e6406c0f54d 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -29,13 +29,11 @@ import { } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useFormbricks } from "@calcom/lib/formbricks-client"; -import getBrandColours from "@calcom/lib/getBrandColours"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { ButtonState, useNotifications } from "@calcom/lib/hooks/useNotifications"; import { useRefreshData } from "@calcom/lib/hooks/useRefreshData"; -import useTheme from "@calcom/lib/hooks/useTheme"; import { isKeyInObject } from "@calcom/lib/isKeyInObject"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; @@ -58,7 +56,6 @@ import { showToast, SkeletonText, Tooltip, - useCalcomTheme, type IconName, } from "@calcom/ui"; import { useGetUserAttributes } from "@calcom/web/components/settings/platform/hooks/useGetUserAttributes"; @@ -68,6 +65,7 @@ import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider"; import { TeamInviteBadge } from "./TeamInviteBadge"; import { BannerContainer } from "./banners/LayoutBanner"; import { useBanners } from "./banners/useBanners"; +import { useAppTheme } from "./useAppTheme"; // need to import without ssr to prevent hydration errors const Tips = dynamic(() => import("@calcom/features/tips").then((mod) => mod.Tips), { @@ -151,16 +149,6 @@ export type LayoutProps = { isPlatformUser?: boolean; }; -const useAppTheme = () => { - const { data: user } = useMeQuery(); - const brandTheme = getBrandColours({ - lightVal: user?.brandColor, - darkVal: user?.darkBrandColor, - }); - useCalcomTheme(brandTheme); - useTheme(user?.appTheme); -}; - const KBarWrapper = ({ children, withKBar = false }: { withKBar: boolean; children: React.ReactNode }) => withKBar ? ( diff --git a/packages/features/shell/useAppTheme.ts b/packages/features/shell/useAppTheme.ts new file mode 100644 index 00000000000000..c608ab8858e37e --- /dev/null +++ b/packages/features/shell/useAppTheme.ts @@ -0,0 +1,14 @@ +import getBrandColours from "@calcom/lib/getBrandColours"; +import useTheme from "@calcom/lib/hooks/useTheme"; +import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; +import { useCalcomTheme } from "@calcom/ui"; + +export const useAppTheme = () => { + const { data: user } = useMeQuery(); + const brandTheme = getBrandColours({ + lightVal: user?.brandColor, + darkVal: user?.darkBrandColor, + }); + useCalcomTheme(brandTheme); + useTheme(user?.appTheme); +}; From de537c3b52f8911ed6a6245d81b3a8c14ad012c3 Mon Sep 17 00:00:00 2001 From: sean-brydon Date: Tue, 1 Oct 2024 11:05:51 +0100 Subject: [PATCH 06/17] extract user-dropdown to new component --- packages/features/shell/Shell.tsx | 212 +---------------- .../shell/user-dropdown/UserDropdown.tsx | 217 ++++++++++++++++++ 2 files changed, 220 insertions(+), 209 deletions(-) create mode 100644 packages/features/shell/user-dropdown/UserDropdown.tsx diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 011e6406c0f54d..0f1ae24d718d9f 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -1,10 +1,10 @@ import type { User as UserAuth } from "next-auth"; -import { signOut, useSession } from "next-auth/react"; +import { useSession } from "next-auth/react"; import dynamic from "next/dynamic"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import type { Dispatch, ReactElement, ReactNode, SetStateAction } from "react"; -import React, { cloneElement, Fragment, useEffect, useMemo, useState } from "react"; +import React, { cloneElement, Fragment, useMemo, useState } from "react"; import { Toaster } from "react-hot-toast"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; @@ -12,7 +12,6 @@ import { useRedirectToLoginIfUnauthenticated } from "@calcom/features/auth/lib/h import { useRedirectToOnboardingIfNeeded } from "@calcom/features/auth/lib/hooks/useRedirectToOnboardingIfNeeded"; import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; import { useBootIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar"; @@ -20,23 +19,18 @@ import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog import classNames from "@calcom/lib/classNames"; import { APP_NAME, - DESKTOP_APP_LINK, ENABLE_PROFILE_SWITCHER, IS_CALCOM, IS_VISUAL_REGRESSION_TESTING, - JOIN_COMMUNITY, - ROADMAP, } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useFormbricks } from "@calcom/lib/formbricks-client"; -import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { ButtonState, useNotifications } from "@calcom/lib/hooks/useNotifications"; import { useRefreshData } from "@calcom/lib/hooks/useRefreshData"; import { isKeyInObject } from "@calcom/lib/isKeyInObject"; import { trpc } from "@calcom/trpc/react"; -import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { Avatar, Button, @@ -47,7 +41,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, - DropdownMenuSeparator, DropdownMenuTrigger, ErrorBoundary, HeadSeo, @@ -58,14 +51,13 @@ import { Tooltip, type IconName, } from "@calcom/ui"; -import { useGetUserAttributes } from "@calcom/web/components/settings/platform/hooks/useGetUserAttributes"; import { useOrgBranding } from "../ee/organizations/context/provider"; -import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider"; import { TeamInviteBadge } from "./TeamInviteBadge"; import { BannerContainer } from "./banners/LayoutBanner"; import { useBanners } from "./banners/useBanners"; import { useAppTheme } from "./useAppTheme"; +import { UserDropdown } from "./user-dropdown/UserDropdown"; // need to import without ssr to prevent hydration errors const Tips = dynamic(() => import("@calcom/features/tips").then((mod) => mod.Tips), { @@ -74,7 +66,6 @@ const Tips = dynamic(() => import("@calcom/features/tips").then((mod) => mod.Tip const Layout = (props: LayoutProps) => { const { banners, bannersHeight } = useBanners(); - const pathname = usePathname(); const isFullPageWithoutSidebar = pathname?.startsWith("/apps/routing-forms/reporting/"); const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title; @@ -183,203 +174,6 @@ export default function Shell(props: LayoutProps) { ); } -interface UserDropdownProps { - small?: boolean; -} - -function UserDropdown({ small }: UserDropdownProps) { - const { isPlatformUser } = useGetUserAttributes(); - const { t } = useLocale(); - const { data: user } = useMeQuery(); - const utils = trpc.useUtils(); - const bookerUrl = useBookerUrl(); - const pathname = usePathname(); - const isPlatformPages = pathname?.startsWith("/settings/platform"); - 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 [helpOpen, setHelpOpen] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); - - const onHelpItemSelect = () => { - setHelpOpen(false); - setMenuOpen(false); - }; - - // Prevent rendering dropdown if user isn't available. - // We don't want to show nameless user. - if (!user) { - return null; - } - - return ( - - setMenuOpen((menuOpen) => !menuOpen)}> - - - - - - { - setMenuOpen(false); - setHelpOpen(false); - }} - className="group overflow-hidden rounded-md"> - {helpOpen ? ( - onHelpItemSelect()} /> - ) : ( - <> - {!isPlatformPages && ( - <> - - - - - - - - - - - - )} - - - - {t("join_our_community")} - - - - - {t("visit_roadmap")} - - - - - - {!isPlatformPages && ( - - - {t("download_desktop_app")} - - - )} - - {!isPlatformPages && isPlatformUser && ( - - - Platform - - - )} - - - - - - - )} - - - - - ); -} - export type NavigationItemType = { name: string; href: string; diff --git a/packages/features/shell/user-dropdown/UserDropdown.tsx b/packages/features/shell/user-dropdown/UserDropdown.tsx new file mode 100644 index 00000000000000..696766dee4013f --- /dev/null +++ b/packages/features/shell/user-dropdown/UserDropdown.tsx @@ -0,0 +1,217 @@ +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; + +import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; +import { classNames } from "@calcom/lib"; +import { JOIN_COMMUNITY, ROADMAP, DESKTOP_APP_LINK } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; +import { + Avatar, + Dropdown, + DropdownItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuTrigger, + Icon, +} from "@calcom/ui"; +// TODO (Platform): we shouldnt be importing from web here +import { useGetUserAttributes } from "@calcom/web/components/settings/platform/hooks/useGetUserAttributes"; + +import FreshChatProvider from "../../ee/support/lib/freshchat/FreshChatProvider"; + +interface UserDropdownProps { + small?: boolean; +} +export function UserDropdown({ small }: UserDropdownProps) { + const { isPlatformUser } = useGetUserAttributes(); + const { t } = useLocale(); + const { data: user } = useMeQuery(); + const pathname = usePathname(); + const isPlatformPages = pathname?.startsWith("/settings/platform"); + 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 [helpOpen, setHelpOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + + const onHelpItemSelect = () => { + setHelpOpen(false); + setMenuOpen(false); + }; + + // Prevent rendering dropdown if user isn't available. + // We don't want to show nameless user. + if (!user) { + return null; + } + + return ( + + setMenuOpen((menuOpen) => !menuOpen)}> + + + + + + { + setMenuOpen(false); + setHelpOpen(false); + }} + className="group overflow-hidden rounded-md"> + {helpOpen ? ( + onHelpItemSelect()} /> + ) : ( + <> + {!isPlatformPages && ( + <> + + + + + + + + + + + + )} + + + + {t("join_our_community")} + + + + + {t("visit_roadmap")} + + + + + + {!isPlatformPages && ( + + + {t("download_desktop_app")} + + + )} + + {!isPlatformPages && isPlatformUser && ( + + + Platform + + + )} + + + + + + + )} + + + + + ); +} From f13c3f4f5feb1f2794d0cd899579e3b5d3d67825 Mon Sep 17 00:00:00 2001 From: sean-brydon Date: Tue, 1 Oct 2024 11:14:56 +0100 Subject: [PATCH 07/17] Navigation Item --- packages/features/shell/Shell.tsx | 136 +-------------- .../shell/navigation/NavigationItem.tsx | 160 ++++++++++++++++++ .../useShouldDisplayNavigationItem.ts | 10 ++ 3 files changed, 171 insertions(+), 135 deletions(-) create mode 100644 packages/features/shell/navigation/NavigationItem.tsx create mode 100644 packages/features/shell/navigation/useShouldDisplayNavigationItem.ts diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 0f1ae24d718d9f..02dd9efb15f279 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -13,7 +13,6 @@ import { useRedirectToOnboardingIfNeeded } from "@calcom/features/auth/lib/hooks import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { useBootIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom"; -import { useFlagMap } from "@calcom/features/flags/context/provider"; import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar"; import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog"; import classNames from "@calcom/lib/classNames"; @@ -29,7 +28,6 @@ import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { ButtonState, useNotifications } from "@calcom/lib/hooks/useNotifications"; import { useRefreshData } from "@calcom/lib/hooks/useRefreshData"; -import { isKeyInObject } from "@calcom/lib/isKeyInObject"; import { trpc } from "@calcom/trpc/react"; import { Avatar, @@ -56,6 +54,7 @@ import { useOrgBranding } from "../ee/organizations/context/provider"; import { TeamInviteBadge } from "./TeamInviteBadge"; import { BannerContainer } from "./banners/LayoutBanner"; import { useBanners } from "./banners/useBanners"; +import { NavigationItem, MobileNavigationItem, MobileNavigationMoreItem } from "./navigation/NavigationItem"; import { useAppTheme } from "./useAppTheme"; import { UserDropdown } from "./user-dropdown/UserDropdown"; @@ -350,81 +349,6 @@ const Navigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: b ); }; -function useShouldDisplayNavigationItem(item: NavigationItemType) { - const flags = useFlagMap(); - if (isKeyInObject(item.name, flags)) return flags[item.name]; - return true; -} - -const defaultIsCurrent: NavigationItemType["isCurrent"] = ({ isChild, item, pathname }) => { - return isChild ? item.href === pathname : item.href ? pathname?.startsWith(item.href) ?? false : false; -}; - -const NavigationItem: React.FC<{ - index?: number; - item: NavigationItemType; - isChild?: boolean; -}> = (props) => { - const { item, isChild } = props; - const { t, isLocaleReady } = useLocale(); - const pathname = usePathname(); - const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent; - const current = isCurrent({ isChild: !!isChild, item, pathname }); - const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item); - - if (!shouldDisplayNavigationItem) return null; - - return ( - - - - {item.icon && ( - - {item.child && - isCurrent({ pathname, isChild, item }) && - item.child.map((item, index) => )} - - ); -}; - function MobileNavigationContainer({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) { const { status } = useSession(); if (status !== "authenticated") return null; @@ -452,64 +376,6 @@ const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigati ); }; -const MobileNavigationItem: React.FC<{ - item: NavigationItemType; - isChild?: boolean; -}> = (props) => { - const { item, isChild } = props; - const pathname = usePathname(); - const { t, isLocaleReady } = useLocale(); - const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent; - const current = isCurrent({ isChild: !!isChild, item, pathname }); - const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item); - - if (!shouldDisplayNavigationItem) return null; - return ( - - {item.badge &&
{item.badge}
} - {item.icon && ( -