* fix: hide insights submenu for free users * fix: allow navigation access while plan pending * refactor: update upgrade link to insights in navigation * fix: addressed all the issues * fix: prevent flash of premium features for free users during loading Change from optimistic to pessimistic loading so free users don't briefly see the Insights submenu while plan status is being fetched. --------- Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
278 lines
7.9 KiB
TypeScript
278 lines
7.9 KiB
TypeScript
import { useSession } from "next-auth/react";
|
|
import { useMemo } from "react";
|
|
|
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
|
import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge";
|
|
import { useHasPaidPlan } from "@calcom/features/billing/hooks/useHasPaidPlan";
|
|
import {
|
|
useOrgBranding,
|
|
type OrganizationBranding,
|
|
} from "@calcom/features/ee/organizations/context/provider";
|
|
import { KBarTrigger } from "@calcom/features/kbar/Kbar";
|
|
import classNames from "@calcom/ui/classNames";
|
|
|
|
import { TeamInviteBadge } from "../TeamInviteBadge";
|
|
import type { NavigationItemType } from "./NavigationItem";
|
|
import { NavigationItem, MobileNavigationItem, MobileNavigationMoreItem } from "./NavigationItem";
|
|
|
|
export const MORE_SEPARATOR_NAME = "more";
|
|
|
|
const preserveBookingsQueryParams = ({
|
|
prevPathname,
|
|
nextPathname,
|
|
}: {
|
|
prevPathname: string | null;
|
|
nextPathname: string;
|
|
}) => Boolean(prevPathname?.startsWith("/bookings/")) && nextPathname.startsWith("/bookings/");
|
|
|
|
const getNavigationItems = (
|
|
orgBranding: OrganizationBranding,
|
|
hasInsightsAccess: boolean
|
|
): NavigationItemType[] => [
|
|
|
|
{
|
|
name: "event_types_page_title",
|
|
href: "/event-types",
|
|
icon: "link",
|
|
},
|
|
{
|
|
name: "bookings",
|
|
href: "/bookings/upcoming",
|
|
icon: "calendar",
|
|
badge: <UnconfirmedBookingBadge />,
|
|
isCurrent: ({ pathname }) => pathname?.startsWith("/bookings") ?? false,
|
|
},
|
|
{
|
|
name: "availability",
|
|
href: "/availability",
|
|
icon: "clock",
|
|
},
|
|
...(orgBranding
|
|
? [
|
|
{
|
|
name: "members",
|
|
href: `/settings/organizations/${orgBranding.slug}/members`,
|
|
icon: "building",
|
|
moreOnMobile: true,
|
|
} satisfies NavigationItemType,
|
|
]
|
|
: []),
|
|
{
|
|
name: "teams",
|
|
href: "/teams",
|
|
icon: "users",
|
|
badge: <TeamInviteBadge />,
|
|
moreOnMobile: true,
|
|
},
|
|
{
|
|
name: "apps",
|
|
href: "/apps",
|
|
icon: "grid-3x3",
|
|
moreOnMobile: true,
|
|
isCurrent: ({ pathname: path, item }) => {
|
|
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
|
|
return (path?.startsWith(item.href) ?? false) && !(path?.includes("routing-forms/") ?? false);
|
|
},
|
|
child: [
|
|
{
|
|
name: "app_store",
|
|
href: "/apps",
|
|
isCurrent: ({ pathname: path, item }) => {
|
|
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
|
|
return (
|
|
(path?.startsWith(item.href) ?? false) &&
|
|
!(path?.includes("routing-forms/") ?? false) &&
|
|
!(path?.includes("/installed") ?? false)
|
|
);
|
|
},
|
|
},
|
|
{
|
|
name: "installed_apps",
|
|
href: "/apps/installed/calendar",
|
|
isCurrent: ({ pathname: path }) =>
|
|
(path?.startsWith("/apps/installed/") ?? false) ||
|
|
(path?.startsWith("/v2/apps/installed/") ?? false),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: MORE_SEPARATOR_NAME,
|
|
href: "/more",
|
|
icon: "ellipsis",
|
|
},
|
|
{
|
|
name: "routing",
|
|
href: "/routing",
|
|
icon: "split",
|
|
isCurrent: ({ pathname }) => pathname?.startsWith("/routing") ?? false,
|
|
moreOnMobile: true,
|
|
},
|
|
{
|
|
name: "workflows",
|
|
href: "/workflows",
|
|
icon: "zap",
|
|
moreOnMobile: true,
|
|
},
|
|
{
|
|
name: "insights",
|
|
href: "/insights",
|
|
icon: "chart-bar",
|
|
isCurrent: ({ pathname: path, item }) => path?.startsWith(item.href) ?? false,
|
|
moreOnMobile: true,
|
|
child: hasInsightsAccess
|
|
? [
|
|
{
|
|
name: "bookings",
|
|
href: "/insights",
|
|
isCurrent: ({ pathname: path }) => path === "/insights",
|
|
},
|
|
{
|
|
name: "routing",
|
|
href: "/insights/routing",
|
|
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/routing") ?? false,
|
|
},
|
|
{
|
|
name: "router_position",
|
|
href: "/insights/router-position",
|
|
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/router-position") ?? false,
|
|
},
|
|
{
|
|
name: "call_history",
|
|
href: "/insights/call-history",
|
|
// icon: "phone",
|
|
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/call-history") ?? false,
|
|
},
|
|
]
|
|
: undefined,
|
|
},
|
|
];
|
|
|
|
const platformNavigationItems: NavigationItemType[] = [
|
|
{
|
|
name: "Dashboard",
|
|
href: "/settings/platform/",
|
|
icon: "layout-dashboard",
|
|
},
|
|
{
|
|
name: "Documentation",
|
|
href: "https://docs.cal.com/docs/platform",
|
|
icon: "chart-bar",
|
|
target: "_blank",
|
|
},
|
|
{
|
|
name: "API reference",
|
|
href: "https://api.cal.com/v2/docs#/",
|
|
icon: "terminal",
|
|
target: "_blank",
|
|
},
|
|
{
|
|
name: "Atoms",
|
|
href: "https://docs.cal.com/docs/platform#atoms",
|
|
icon: "atom",
|
|
target: "_blank",
|
|
},
|
|
{
|
|
name: MORE_SEPARATOR_NAME,
|
|
href: "https://docs.cal.com/docs/platform/faq",
|
|
icon: "ellipsis",
|
|
target: "_blank",
|
|
},
|
|
{
|
|
name: "Billing",
|
|
href: "/settings/platform/billing",
|
|
icon: "credit-card",
|
|
moreOnMobile: true,
|
|
},
|
|
{
|
|
name: "Members",
|
|
href: "/settings/platform/members",
|
|
icon: "users",
|
|
moreOnMobile: true,
|
|
},
|
|
{
|
|
name: "Managed Users",
|
|
href: "/settings/platform/managed-users",
|
|
icon: "users",
|
|
moreOnMobile: true,
|
|
},
|
|
];
|
|
|
|
const useNavigationItems = (isPlatformNavigation = false) => {
|
|
const orgBranding = useOrgBranding();
|
|
const { hasPaidPlan, isPending } = useHasPaidPlan();
|
|
return useMemo(() => {
|
|
const hasInsightsAccess = !isPending && !!hasPaidPlan;
|
|
const items = !isPlatformNavigation
|
|
? getNavigationItems(orgBranding, hasInsightsAccess)
|
|
: platformNavigationItems;
|
|
|
|
const desktopNavigationItems = items.filter((item) => item.name !== MORE_SEPARATOR_NAME);
|
|
const mobileNavigationBottomItems = items.filter(
|
|
(item) => (!item.moreOnMobile && !item.onlyDesktop) || item.name === MORE_SEPARATOR_NAME
|
|
);
|
|
const mobileNavigationMoreItems = items.filter(
|
|
(item) => item.moreOnMobile && !item.onlyDesktop && item.name !== MORE_SEPARATOR_NAME
|
|
);
|
|
|
|
return { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems };
|
|
}, [hasPaidPlan, isPending, isPlatformNavigation, orgBranding]);
|
|
};
|
|
|
|
export const Navigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) => {
|
|
const { desktopNavigationItems } = useNavigationItems(isPlatformNavigation);
|
|
|
|
return (
|
|
<nav className="mt-2 flex-1 md:px-2 lg:mt-4 lg:px-0">
|
|
{desktopNavigationItems.map((item) => (
|
|
<NavigationItem key={item.name} item={item} />
|
|
))}
|
|
<div className="text-subtle mt-0.5 lg:hidden">
|
|
<KBarTrigger />
|
|
</div>
|
|
</nav>
|
|
);
|
|
};
|
|
|
|
export function MobileNavigationContainer({
|
|
isPlatformNavigation = false,
|
|
}: {
|
|
isPlatformNavigation?: boolean;
|
|
}) {
|
|
const { status } = useSession();
|
|
if (status !== "authenticated") return null;
|
|
return <MobileNavigation isPlatformNavigation={isPlatformNavigation} />;
|
|
}
|
|
|
|
const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) => {
|
|
const isEmbed = useIsEmbed();
|
|
const { mobileNavigationBottomItems } = useNavigationItems(isPlatformNavigation);
|
|
|
|
return (
|
|
<>
|
|
<nav
|
|
className={classNames(
|
|
"pwa:pb-[max(0.25rem,env(safe-area-inset-bottom))] pwa:-mx-2 bg-cal-muted/40 border-subtle fixed bottom-0 left-0 z-30 flex w-full border-t px-1 shadow backdrop-blur-md md:hidden",
|
|
isEmbed && "hidden"
|
|
)}>
|
|
{mobileNavigationBottomItems.map((item) => (
|
|
<MobileNavigationItem key={item.name} item={item} />
|
|
))}
|
|
</nav>
|
|
{/* add padding to content for mobile navigation*/}
|
|
<div className="block pt-12 md:hidden" />
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const MobileNavigationMoreItems = () => {
|
|
const { mobileNavigationMoreItems } = useNavigationItems();
|
|
|
|
return (
|
|
<ul className="border-subtle mt-2 rounded-md border">
|
|
{mobileNavigationMoreItems.map((item) => (
|
|
<MobileNavigationMoreItem key={item.name} item={item} />
|
|
))}
|
|
</ul>
|
|
);
|
|
};
|