feat: adding posthog libs for node + js (#16862)
* adding posthog libs for node + js * updating lock * move to init posthog client - pageview component * adding identify in shell * removing test event captures * add signout tracking to dropdown as it was missed in merge --------- Co-authored-by: sean <sean@brydon.io> Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
This commit is contained in:
co-authored by
sean
Bailey Pumfleet
parent
dd944bace1
commit
ddfbe25903
@@ -102,6 +102,11 @@ NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||
# Secret to enable Intercom Identity Verification
|
||||
INTERCOM_SECRET=
|
||||
|
||||
# Posthog Config
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
NEXT_PUBLIC_POSTHOG_HOST=
|
||||
|
||||
# Zendesk Config
|
||||
NEXT_PUBLIC_ZENDESK_KEY=
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
@@ -299,7 +300,9 @@ const AppProviders = (props: PageWrapperProps) => {
|
||||
|
||||
return (
|
||||
<DynamicHelpscoutProvider>
|
||||
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
|
||||
<DynamicIntercomProvider>
|
||||
<DynamicPostHogProvider>{RemainingProviders}</DynamicPostHogProvider>
|
||||
</DynamicIntercomProvider>
|
||||
</DynamicHelpscoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,11 +7,13 @@ import type { SSRConfig } from "next-i18next";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
@@ -56,6 +58,13 @@ export type AppProps = Omit<
|
||||
err?: Error;
|
||||
};
|
||||
|
||||
const PostHogPageView = dynamic(
|
||||
() => import("@calcom/features/ee/event-tracking/lib/posthog/web/PostHogPageView"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
type AppPropsWithChildren = AppProps & {
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -319,7 +328,12 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
|
||||
return (
|
||||
<DynamicHelpscoutProvider>
|
||||
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
|
||||
<DynamicIntercomProvider>
|
||||
<DynamicPostHogProvider>
|
||||
<PostHogPageView />
|
||||
{RemainingProviders}
|
||||
</DynamicPostHogProvider>
|
||||
</DynamicIntercomProvider>
|
||||
</DynamicHelpscoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -624,7 +624,7 @@ export default function Signup({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 dark:bg-none lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12">
|
||||
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12 dark:bg-none">
|
||||
{IS_CALCOM && (
|
||||
<>
|
||||
<div className="-mt-4 mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
||||
@@ -675,7 +675,7 @@ export default function Signup({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] dark:bg-white/5 lg:block lg:py-[6px] lg:pl-[6px]">
|
||||
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] lg:block lg:py-[6px] lg:pl-[6px] dark:bg-white/5">
|
||||
<img className="block dark:hidden" src="/mock-event-type-list.svg" alt="Cal.com Booking Page" />
|
||||
<img
|
||||
className="hidden dark:block"
|
||||
@@ -686,7 +686,7 @@ export default function Signup({
|
||||
<div className="mr-12 mt-8 hidden h-full w-full grid-cols-3 gap-4 overflow-hidden lg:grid">
|
||||
{FEATURES.map((feature) => (
|
||||
<>
|
||||
<div className="max-w-52 mb-8 flex flex-col leading-none sm:mb-0">
|
||||
<div className="mb-8 flex max-w-52 flex-col leading-none sm:mb-0">
|
||||
<div className="text-emphasis items-center">
|
||||
<Icon name={feature.icon} className="mb-1 h-4 w-4" />
|
||||
<span className="text-sm font-medium">{t(feature.title)}</span>
|
||||
|
||||
@@ -109,6 +109,8 @@
|
||||
"nodemailer": "^6.7.8",
|
||||
"nuqs": "^1.20.0",
|
||||
"otplib": "^12.0.1",
|
||||
"posthog-js": "^1.164.1",
|
||||
"posthog-node": "^4.2.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"raw-body": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -10,6 +10,7 @@ import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg";
|
||||
import postHogClient from "@calcom/features/ee/event-tracking/lib/posthog/postHogClient";
|
||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||
import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
@@ -947,24 +948,32 @@ export const getOptions = ({
|
||||
// this is a workaround – in the future once we move to use the Account model in the DB
|
||||
// we should use NextAuth's isNewUser flag instead: https://next-auth.js.org/configuration/events#signin
|
||||
const isNewUser = new Date(user.createdDate) > new Date(Date.now() - 10 * 60 * 1000);
|
||||
if ((isENVDev || IS_CALCOM) && process.env.DUB_API_KEY && isNewUser) {
|
||||
const clickId = getDubId();
|
||||
// check if there's a clickId (dub_id) cookie set by @dub/analytics
|
||||
if (clickId) {
|
||||
// here we use waitUntil – meaning this code will run async to not block the main thread
|
||||
waitUntil(
|
||||
// if so, send a lead event to Dub
|
||||
// @see https://d.to/conversions/next-auth
|
||||
dub.track.lead({
|
||||
clickId,
|
||||
eventName: "Sign Up",
|
||||
customerId: user.id.toString(),
|
||||
customerName: user.name,
|
||||
customerEmail: user.email,
|
||||
customerAvatar: user.image,
|
||||
})
|
||||
);
|
||||
if ((isENVDev || IS_CALCOM) && isNewUser) {
|
||||
if (process.env.DUB_API_KEY) {
|
||||
const clickId = getDubId();
|
||||
// check if there's a clickId (dub_id) cookie set by @dub/analytics
|
||||
if (clickId) {
|
||||
// here we use waitUntil – meaning this code will run async to not block the main thread
|
||||
waitUntil(
|
||||
// if so, send a lead event to Dub
|
||||
// @see https://d.to/conversions/next-auth
|
||||
dub.track.lead({
|
||||
clickId,
|
||||
eventName: "Sign Up",
|
||||
customerId: user.id.toString(),
|
||||
customerName: user.name,
|
||||
customerEmail: user.email,
|
||||
customerAvatar: user.image,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
postHogClient().capture(user.id.toString(), "Sign Up", {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -275,7 +275,7 @@ const BookerComponent = ({
|
||||
data-testid="booker-container"
|
||||
className={classNames(
|
||||
...getBookerSizeClassNames(layout, bookerState, hideEventTypeDetails),
|
||||
`bg-default dark:bg-muted grid max-w-full items-start dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row`,
|
||||
`bg-default dark:bg-muted grid max-w-full items-start sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row dark:[color-scheme:dark]`,
|
||||
// We remove border only when the content covers entire viewport. Because in embed, it can almost never be the case that it covers entire viewport, we show the border there
|
||||
(layout === BookerLayouts.MONTH_VIEW || isEmbed) && "border-subtle rounded-md",
|
||||
!isEmbed && "sm:transition-[width] sm:duration-300",
|
||||
@@ -411,7 +411,7 @@ const BookerComponent = ({
|
||||
layout === BookerLayouts.COLUMN_VIEW
|
||||
}
|
||||
className={classNames(
|
||||
"border-subtle rtl:border-default flex h-full w-full flex-col overflow-x-auto px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
|
||||
"border-subtle rtl:border-default flex h-full w-full flex-col overflow-x-auto px-5 py-3 pb-0 ltr:md:border-l rtl:border-r",
|
||||
layout === BookerLayouts.MONTH_VIEW &&
|
||||
"h-full overflow-hidden md:w-[var(--booker-timeslots-width)]",
|
||||
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import { PostHog, type PostHog as PostHogType } from "posthog-node";
|
||||
|
||||
let postHog: {
|
||||
capture: PostHogType["capture"];
|
||||
identify: PostHogType["identify"];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
postHog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
flushAt: 1,
|
||||
});
|
||||
} else {
|
||||
postHog = {
|
||||
capture: noop,
|
||||
identify: noop,
|
||||
};
|
||||
}
|
||||
|
||||
export function postHogClient() {
|
||||
function capture(distinctId: string, event: string, properties?: Record<string, any>) {
|
||||
postHog?.capture({ distinctId, event, properties });
|
||||
}
|
||||
|
||||
function identify(distinctId: string, properties?: Record<string, any>) {
|
||||
postHog?.identify({ distinctId, properties });
|
||||
}
|
||||
|
||||
return {
|
||||
capture,
|
||||
identify,
|
||||
};
|
||||
}
|
||||
|
||||
export default postHogClient;
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import posthog from "posthog-js";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function Provider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return;
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
|
||||
autocapture: false,
|
||||
persistence: "memory",
|
||||
request_batching: false,
|
||||
// Enable debug mode in development
|
||||
loaded: (posthog) => {
|
||||
if (process.env.NODE_ENV === "development") posthog.debug();
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
|
||||
}
|
||||
|
||||
export default Provider;
|
||||
@@ -0,0 +1,14 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { Fragment } from "react";
|
||||
|
||||
const initPostProvider = () => {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
return Fragment;
|
||||
}
|
||||
|
||||
return dynamic(() => import("./provider"));
|
||||
};
|
||||
|
||||
const DynamicPostHogProvider = initPostProvider();
|
||||
export default DynamicPostHogProvider;
|
||||
@@ -0,0 +1,66 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import { usePostHog as usePostHogLib } from "posthog-js/react";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useHasTeamPlan, useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
export const isPostHogEnabled = z.string().min(1).safeParse(process.env.NEXT_PUBLIC_POSTHOG_KEY).success;
|
||||
|
||||
type PostHogHook = {
|
||||
capture: (event: string, properties?: Record<string, any>) => void;
|
||||
identify: (distinctId: string, properties?: Record<string, any>) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
const usePostHogHook: () => PostHogHook = isPostHogEnabled
|
||||
? usePostHogLib
|
||||
: () => ({
|
||||
identify: noop,
|
||||
capture: noop,
|
||||
reset: noop,
|
||||
});
|
||||
|
||||
const usePostHog = () => {
|
||||
const posthog = usePostHogHook();
|
||||
const { data } = trpc.viewer.me.useQuery();
|
||||
const { hasPaidPlan } = useHasPaidPlan();
|
||||
const { hasTeamPlan } = useHasTeamPlan();
|
||||
|
||||
const identify = async () => {
|
||||
if (!data) return;
|
||||
|
||||
posthog.identify(String(data.id), {
|
||||
distinctId: String(data.id),
|
||||
...(data && data?.name && { name: data.name }),
|
||||
...(data && data?.email && { email: data.email }),
|
||||
created_at: String(dayjs(data?.createdDate).unix()),
|
||||
//keys should be snake cased
|
||||
user_name: data?.username,
|
||||
link: `${WEBSITE_URL}/${data?.username}`,
|
||||
identity_provider: data?.identityProvider,
|
||||
timezone: data?.timeZone,
|
||||
locale: data?.locale,
|
||||
has_paid_plan: hasPaidPlan,
|
||||
has_team_plan: hasTeamPlan,
|
||||
metadata: data?.metadata,
|
||||
completed_onboarding: data?.completedOnboarding,
|
||||
sum_of_bookings: data?.sumOfBookings,
|
||||
sum_of_calendars: data?.sumOfCalendars,
|
||||
sum_of_teams: data?.sumOfTeams,
|
||||
has_orgs_plan: !!data?.organizationId,
|
||||
organization: data?.organization?.slug,
|
||||
sum_of_event_types: data?.sumOfEventTypes,
|
||||
sum_of_team_event_types: data?.sumOfTeamEventTypes,
|
||||
is_premium: data?.isPremium,
|
||||
});
|
||||
};
|
||||
|
||||
return { identify, capture: posthog.capture, reset: posthog.reset };
|
||||
};
|
||||
|
||||
export default usePostHog;
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function PostHogPageView() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const posthog = usePostHog();
|
||||
useEffect(() => {
|
||||
// Track pageviews
|
||||
if (pathname && posthog) {
|
||||
let url = window.origin + pathname;
|
||||
if (searchParams.toString()) {
|
||||
url = `${url}?${searchParams.toString()}`;
|
||||
}
|
||||
posthog.capture("$pageview", {
|
||||
$current_url: url,
|
||||
});
|
||||
}
|
||||
}, [pathname, searchParams, posthog]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import { Button, Dialog, DialogClose, DialogContent, DialogFooter, showToast } f
|
||||
|
||||
import CreateEventTypeForm from "./CreateEventTypeForm";
|
||||
|
||||
import usePostHog from "../../ee/event-tracking/lib/posthog/userPostHog";
|
||||
|
||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
||||
export interface EventTypeParent {
|
||||
teamId: number | null | undefined; // if undefined, then it's a profile
|
||||
@@ -60,6 +62,7 @@ export default function CreateEventTypeDialog({
|
||||
membershipRole: MembershipRole | null | undefined;
|
||||
}[];
|
||||
}) {
|
||||
const postHog = usePostHog();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const orgBranding = useOrgBranding();
|
||||
@@ -137,6 +140,7 @@ export default function CreateEventTypeDialog({
|
||||
form={form}
|
||||
isManagedEventType={isManagedEventType}
|
||||
handleSubmit={(values) => {
|
||||
postHog.capture("Event Created Frontend");
|
||||
createMutation.mutate(values);
|
||||
}}
|
||||
SubmitButton={SubmitButton}
|
||||
|
||||
@@ -23,7 +23,7 @@ const ClearFilters = () => {
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="min-w-24 h-[38px] border-0"
|
||||
className="h-[38px] min-w-24 border-0"
|
||||
onClick={() => {
|
||||
clearFilters();
|
||||
}}>
|
||||
|
||||
@@ -228,9 +228,9 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
|
||||
data-testid={`vertical-tab-${name}`}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0"
|
||||
className="h-4 w-4 stroke-[2px] md:mt-0 ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180"
|
||||
/>
|
||||
<Skeleton title={name} as="p" className="max-w-36 min-h-4 truncate" loadingClassName="ms-3">
|
||||
<Skeleton title={name} as="p" className="min-h-4 max-w-36 truncate" loadingClassName="ms-3">
|
||||
{name}
|
||||
</Skeleton>
|
||||
</Link>
|
||||
@@ -313,7 +313,7 @@ const TeamListCollapsible = () => {
|
||||
{!team.parentId && (
|
||||
<img
|
||||
src={getPlaceholderAvatar(team.logoUrl, team.name)}
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] md:mt-0 ltr:mr-2 rtl:ml-2"
|
||||
alt={team.name || "Team logo"}
|
||||
/>
|
||||
)}
|
||||
@@ -463,7 +463,7 @@ const SettingsSidebarContainer = ({
|
||||
{tab && tab.icon && (
|
||||
<Icon
|
||||
name={tab.icon}
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0"
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] md:mt-0 ltr:mr-3 rtl:ml-3"
|
||||
/>
|
||||
)}
|
||||
{!tab.icon && tab?.avatar && (
|
||||
@@ -508,7 +508,7 @@ const SettingsSidebarContainer = ({
|
||||
{tab && tab.icon && (
|
||||
<Icon
|
||||
name={tab.icon}
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0"
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] md:mt-0 ltr:mr-3 rtl:ml-3"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
@@ -542,7 +542,7 @@ const SettingsSidebarContainer = ({
|
||||
{tab && tab.icon && (
|
||||
<Icon
|
||||
name={tab.icon}
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0"
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] md:mt-0 ltr:mr-3 rtl:ml-3"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
@@ -597,7 +597,7 @@ const SettingsSidebarContainer = ({
|
||||
{!otherTeam.parentId && (
|
||||
<img
|
||||
src={getPlaceholderAvatar(otherTeam.logoUrl, otherTeam.name)}
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] md:mt-0 ltr:mr-2 rtl:ml-2"
|
||||
alt={otherTeam.name || "Team logo"}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -227,9 +227,9 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
|
||||
data-testid={`vertical-tab-${name}`}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0"
|
||||
className="h-4 w-4 stroke-[2px] md:mt-0 ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180"
|
||||
/>
|
||||
<Skeleton title={name} as="p" className="max-w-36 min-h-4 truncate" loadingClassName="ms-3">
|
||||
<Skeleton title={name} as="p" className="min-h-4 max-w-36 truncate" loadingClassName="ms-3">
|
||||
{name}
|
||||
</Skeleton>
|
||||
</Link>
|
||||
@@ -310,7 +310,7 @@ const TeamListCollapsible = () => {
|
||||
{!team.parentId && (
|
||||
<img
|
||||
src={getPlaceholderAvatar(team.logoUrl, team.name)}
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] md:mt-0 ltr:mr-2 rtl:ml-2"
|
||||
alt={team.name || "Team logo"}
|
||||
/>
|
||||
)}
|
||||
@@ -457,7 +457,7 @@ const SettingsSidebarContainer = ({
|
||||
{tab && tab.icon && (
|
||||
<Icon
|
||||
name={tab.icon}
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0"
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] md:mt-0 ltr:mr-3 rtl:ml-3"
|
||||
/>
|
||||
)}
|
||||
{!tab.icon && tab?.avatar && (
|
||||
@@ -502,7 +502,7 @@ const SettingsSidebarContainer = ({
|
||||
{tab && tab.icon && (
|
||||
<Icon
|
||||
name={tab.icon}
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0"
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] md:mt-0 ltr:mr-3 rtl:ml-3"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
@@ -536,7 +536,7 @@ const SettingsSidebarContainer = ({
|
||||
{tab && tab.icon && (
|
||||
<Icon
|
||||
name={tab.icon}
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0"
|
||||
className="text-subtle h-[16px] w-[16px] stroke-[2px] md:mt-0 ltr:mr-3 rtl:ml-3"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
@@ -591,7 +591,7 @@ const SettingsSidebarContainer = ({
|
||||
{!otherTeam.parentId && (
|
||||
<img
|
||||
src={getPlaceholderAvatar(otherTeam.logoUrl, otherTeam.name)}
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
|
||||
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] md:mt-0 ltr:mr-2 rtl:ml-2"
|
||||
alt={otherTeam.name || "Team logo"}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { Dispatch, ReactElement, ReactNode, SetStateAction } from "react";
|
||||
import React, { cloneElement, Fragment } from "react";
|
||||
import React, { cloneElement, useEffect } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useRedirectToLoginIfUnauthenticated } from "@calcom/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated";
|
||||
@@ -16,6 +16,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { ButtonState, useNotifications } from "@calcom/lib/hooks/useNotifications";
|
||||
import { Button, ErrorBoundary, HeadSeo, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import usePostHog from "../ee/event-tracking/lib/posthog/userPostHog";
|
||||
import { SideBarContainer } from "./SideBar";
|
||||
import { TopNavContainer } from "./TopNav";
|
||||
import { BannerContainer } from "./banners/LayoutBanner";
|
||||
@@ -26,13 +27,19 @@ import { useAppTheme } from "./useAppTheme";
|
||||
const Layout = (props: LayoutProps) => {
|
||||
const { banners, bannersHeight } = useBanners();
|
||||
const pathname = usePathname();
|
||||
|
||||
const postHog = usePostHog();
|
||||
const isFullPageWithoutSidebar = pathname?.startsWith("/apps/routing-forms/reporting/");
|
||||
const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title;
|
||||
|
||||
useBootIntercom();
|
||||
useFormbricks();
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.isPublic) {
|
||||
postHog.identify();
|
||||
}
|
||||
}, [props.isPublic, postHog]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!props.withoutSeo && (
|
||||
@@ -47,7 +54,7 @@ const Layout = (props: LayoutProps) => {
|
||||
|
||||
<TimezoneChangeDialog />
|
||||
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{banners && !props.isPlatformUser && !isFullPageWithoutSidebar && (
|
||||
<BannerContainer banners={banners} />
|
||||
)}
|
||||
@@ -58,7 +65,7 @@ const Layout = (props: LayoutProps) => {
|
||||
) : (
|
||||
<SideBarContainer isPlatformUser={props.isPlatformUser} bannersHeight={bannersHeight} />
|
||||
)}
|
||||
<div className="flex w-0 flex-1 flex-col">
|
||||
<div className="flex flex-col flex-1 w-0">
|
||||
<MainContainer {...props} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +179,7 @@ export function ShellMain(props: LayoutProps) {
|
||||
{props.heading && (
|
||||
<h3
|
||||
className={classNames(
|
||||
"font-cal max-w-28 sm:max-w-72 md:max-w-80 text-emphasis inline truncate text-lg font-semibold tracking-wide sm:text-xl md:block xl:max-w-full",
|
||||
"font-cal text-emphasis max-w-28 sm:max-w-72 md:max-w-80 inline truncate text-lg font-semibold tracking-wide sm:text-xl md:block xl:max-w-full",
|
||||
props.smallHeading ? "text-base" : "text-xl",
|
||||
props.hideHeadingOnMobile && "hidden"
|
||||
)}>
|
||||
@@ -180,7 +187,7 @@ export function ShellMain(props: LayoutProps) {
|
||||
</h3>
|
||||
)}
|
||||
{props.subtitle && (
|
||||
<p className="text-default hidden text-sm md:block" data-testid="subtitle">
|
||||
<p className="hidden text-sm text-default md:block" data-testid="subtitle">
|
||||
{!isLocaleReady ? <SkeletonText invisible /> : props.subtitle}
|
||||
</p>
|
||||
)}
|
||||
@@ -236,7 +243,7 @@ function MainContainer({
|
||||
...props
|
||||
}: LayoutProps) {
|
||||
return (
|
||||
<main className="bg-default relative z-0 flex-1 focus:outline-none">
|
||||
<main className="relative z-0 flex-1 bg-default focus:outline-none">
|
||||
{/* show top navigation for md and smaller (tablet and phones) */}
|
||||
{TopNavContainerProp}
|
||||
<div className="max-w-full px-2 py-4 lg:px-6">
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
// TODO (Platform): we shouldnt be importing from web here
|
||||
import { useGetUserAttributes } from "@calcom/web/components/settings/platform/hooks/useGetUserAttributes";
|
||||
|
||||
import usePostHog from "../../ee/event-tracking/lib/posthog/userPostHog";
|
||||
import FreshChatProvider from "../../ee/support/lib/freshchat/FreshChatProvider";
|
||||
|
||||
interface UserDropdownProps {
|
||||
@@ -31,6 +32,7 @@ export function UserDropdown({ small }: UserDropdownProps) {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = useMeQuery();
|
||||
const pathname = usePathname();
|
||||
const posthog = usePostHog();
|
||||
const isPlatformPages = pathname?.startsWith("/settings/platform");
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -204,7 +206,11 @@ export function UserDropdown({ small }: UserDropdownProps) {
|
||||
type="button"
|
||||
StartIcon="log-out"
|
||||
aria-hidden="true"
|
||||
onClick={() => signOut({ callbackUrl: "/auth/logout" })}>
|
||||
onClick={() => {
|
||||
posthog.capture("sign_out");
|
||||
posthog.reset();
|
||||
signOut({ callbackUrl: "/auth/logout" });
|
||||
}}>
|
||||
{t("sign_out")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
Vendored
+2
@@ -33,6 +33,8 @@ declare namespace NodeJS {
|
||||
readonly PAYMENT_FEE_PERCENTAGE: number | undefined;
|
||||
readonly PAYMENT_FEE_FIXED: number | undefined;
|
||||
readonly NEXT_PUBLIC_INTERCOM_APP_ID: string | undefined;
|
||||
readonly NEXT_PUBLIC_POSTHOG_KEY: string | undefined;
|
||||
readonly NEXT_PUBLIC_POSTHOG_HOST: string | undefined;
|
||||
readonly TANDEM_CLIENT_ID: string | undefined;
|
||||
readonly TANDEM_CLIENT_SECRET: string | undefined;
|
||||
readonly TANDEM_BASE_URL: string | undefined;
|
||||
|
||||
@@ -456,6 +456,8 @@
|
||||
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
|
||||
"NEXT_PUBLIC_API_V2_ROOT_URL",
|
||||
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
|
||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||
"VAPID_PRIVATE_KEY",
|
||||
"HUDDLE01_API_TOKEN"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user