chore: Revert "chore: app router - /team, /org, /[user] booking pages (excl. embeds) (#18186) (#18526)

This commit is contained in:
Benny Joo
2025-01-08 11:19:45 -03:00
committed by GitHub
parent b941b716d5
commit e6d76b630c
59 changed files with 800 additions and 654 deletions
+1
View File
@@ -363,6 +363,7 @@ APP_ROUTER_APPS_CATEGORIES_ENABLED=0
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
APP_ROUTER_AUTH_LOGIN_ENABLED=0
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
+1
View File
@@ -18,6 +18,7 @@ const ROUTES: [URLPattern, boolean][] = [
["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const,
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const,
].map(([pathname, enabled]) => [
new URLPattern({
pathname,
+1 -20
View File
@@ -3,8 +3,7 @@ import i18next from "i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers } from "next/headers";
import type { MeetingImageProps } from "@calcom/lib/OgImages";
import { constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
import { constructGenericImage } from "@calcom/lib/OgImages";
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
import { buildCanonical } from "@calcom/lib/next-seo.config";
import { truncateOnWord } from "@calcom/lib/text";
@@ -104,21 +103,3 @@ export const _generateMetadata = async (
},
};
};
export const generateMeetingMetadata = async (
meeting: MeetingImageProps,
getTitle: (t: TFunction<string, undefined>) => string,
getDescription: (t: TFunction<string, undefined>) => string,
hideBranding?: boolean,
origin?: string
) => {
const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin);
const image = SEO_IMG_OGIMG + constructMeetingImage(meeting);
return {
...metadata,
openGraph: {
...metadata.openGraph,
images: [image],
},
};
};
+2 -3
View File
@@ -17,7 +17,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const pageProps = await getData(legacyCtx);
const { booking, user: username, slug: eventSlug, isTeamEvent, isBrandingHidden } = pageProps;
const { booking, user: username, slug: eventSlug, isTeamEvent } = pageProps;
const rescheduleUid = booking?.uid;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
const org = isValidOrgDomain ? currentOrgDomain : null;
@@ -34,8 +34,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
isBrandingHidden
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
);
};
@@ -1,6 +1,6 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
@@ -18,7 +18,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { booking, user: username, slug: eventSlug, isSEOIndexable, eventData, isBrandingHidden } = props;
const { booking, user: username, slug: eventSlug } = props;
const rescheduleUid = booking?.uid;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
@@ -31,21 +31,12 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
});
const profileName = event?.profile?.name ?? "";
const profileImage = event?.profile.image;
const title = event?.title ?? "";
const metadata = await generateEventBookingPageMetadata({
event: {
hidden: event?.hidden ?? false,
title,
users: event?.users ?? [],
},
hideBranding: isBrandingHidden,
orgSlug: eventData?.entity.orgSlug ?? null,
isSEOIndexable: !!isSEOIndexable,
profile: { name: profileName, image: profileImage ?? "" },
isReschedule: !!rescheduleUid,
});
return metadata;
return await _generateMetadata(
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
);
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
@@ -1,6 +1,6 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateUserProfilePageMetadata } from "app/generateBookingPageMetadata";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
@@ -14,25 +14,11 @@ import LegacyPage from "~/users/views/users-public-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props;
const isOrg = !!profile?.organization;
const allowSEOIndexing = !!(
(!isOrg && profile.allowSEOIndexing) ||
(isOrg && isOrgSEOIndexable && profile.allowSEOIndexing)
const { profile, markdownStrippedBio } = props;
return await _generateMetadata(
() => profile.name,
() => markdownStrippedBio
);
return await generateUserProfilePageMetadata({
profile: {
name: profile.name,
image: profile.image ?? "",
username: profile.username ?? "",
markdownStrippedBio: markdownStrippedBio,
},
event: null,
hideBranding: false,
orgSlug: entity.orgSlug ?? null,
isSEOIndexable: allowSEOIndexing,
});
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
@@ -1,21 +1,10 @@
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { type OrgTypePageProps } from "app/org/[orgSlug]/[user]/[type]/page";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir<OrgTypePageProps>(getServerSideProps);
const Page = async (props: OrgTypePageProps) => {
if ((props as TeamTypePageProps)?.teamId) {
return <TeamTypePage {...(props as TeamTypePageProps)} />;
}
return <UserTypePage {...(props as UserTypePageProps)} />;
};
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
@@ -0,0 +1,9 @@
import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page });
@@ -1,21 +1,8 @@
import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
import Page from "@pages/org/[orgSlug]/[user]/embed";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { type OrgPageProps } from "app/org/[orgSlug]/[user]/page";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
const getEmbedData = withEmbedSsrAppDir<OrgPageProps>(getServerSideProps);
const Page = async (props: OrgPageProps) => {
if ((props as TeamPageProps)?.team) {
return <TeamPage {...(props as TeamPageProps)} />;
}
return <UserPage {...(props as UserPageProps)} />;
};
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
@@ -0,0 +1,7 @@
import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page });
@@ -15,7 +15,7 @@ import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-mee
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const { slug: eventSlug, user: username, isBrandingHidden } = await getData(context);
const { slug: eventSlug, user: username } = await getData(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
const org = isValidOrgDomain ? currentOrgDomain : null;
@@ -33,10 +33,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
return await _generateMetadata(
() => `${title} | ${profileName}`,
() => `${title}`,
isBrandingHidden
() => `${title}`
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getData, Page, isBookingPage: true })<"P">;
export default WithLayout({ getData, Page })<"P">;
@@ -0,0 +1,29 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return await _generateMetadata(
(t) => props.team.name || t("nameless_team"),
(t) => props.team.name || t("nameless_team")
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TeamPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -0,0 +1,45 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { PageProps } from "~/team/type-view";
import TypePage from "~/team/type-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { user: username, slug: eventSlug, booking } = props;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent: true,
org: isValidOrgDomain ? currentOrgDomain : null,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TypePage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -0,0 +1,29 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return await _generateMetadata(
(t) => props.team.name || t("nameless_team"),
(t) => props.team.name || t("nameless_team")
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TeamPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -1,6 +1,6 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -15,7 +15,7 @@ import LegacyPage, { type PageProps as LegacyPageProps } from "~/team/type-view"
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { user: username, slug: eventSlug, booking, isSEOIndexable, eventData, isBrandingHidden } = props;
const { user: username, slug: eventSlug, booking } = props;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
@@ -26,26 +26,13 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
return await generateEventBookingPageMetadata({
profile: {
name: event?.profile?.name ?? "",
image: event?.profile.image ?? "",
},
event: {
title: event?.title ?? "",
hidden: event?.hidden ?? false,
users: [
...(event?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
},
hideBranding: isBrandingHidden,
orgSlug: eventData?.entity.orgSlug ?? null,
isSEOIndexable,
isReschedule: !!booking,
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
);
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
@@ -1,11 +1,9 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { generateTeamProfilePageMetadata } from "app/generateBookingPageMetadata";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
@@ -13,21 +11,12 @@ import type { PageProps } from "~/team/team-view";
import LegacyPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams)
);
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return await generateTeamProfilePageMetadata({
profile: {
teamName: team.name,
image: getOrgOrTeamAvatar(team),
markdownStrippedBio: markdownStrippedBio,
},
event: null,
hideBranding: false,
orgSlug: currentOrgDomain ?? null,
isSEOIndexable: !!isSEOIndexable,
});
return await _generateMetadata(
(t) => props.team.name || t("nameless_team"),
(t) => props.team.name || t("nameless_team")
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
@@ -36,5 +25,4 @@ export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
-151
View File
@@ -1,151 +0,0 @@
import type { TFunction } from "next-i18next";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { generateMeetingMetadata } from "./_utils";
type MetadataPayload = {
title: (t: TFunction) => string;
description: (t: TFunction) => string;
meeting: {
title: string;
profile: { name: string; image: string };
users?: { name: string; username: string }[];
};
robots: {
index: boolean;
follow: boolean;
};
};
type UserProfilePageProfile = {
name: string | null;
username: string | null;
image: string | null;
markdownStrippedBio: string | null | undefined;
};
type TeamProfilePageProfile = {
teamName: string | null;
image: string | null;
markdownStrippedBio: string | null | undefined;
};
type EventBookingPageProfile = { name: string; image: string };
type BaseMetadataArgs = {
hideBranding: boolean;
orgSlug: string | null;
isSEOIndexable: boolean;
isReschedule?: boolean;
};
type TeamProfilePageArgs = BaseMetadataArgs & {
profile: TeamProfilePageProfile;
event: null;
};
type UserProfilePageArgs = BaseMetadataArgs & {
profile: UserProfilePageProfile;
event: null;
};
type EventBookingPageArgs = BaseMetadataArgs & {
profile: EventBookingPageProfile;
event: {
title: string;
hidden: boolean;
users: { name: string | null; username: string | null }[];
};
};
export const generateEventBookingPageMetadata = async (props: EventBookingPageArgs) => {
const { profile, isReschedule, hideBranding, orgSlug, isSEOIndexable, event } = props;
const fullOrigin = getOrgFullOrigin(orgSlug);
const payload: MetadataPayload = {
title: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title} | ${profile.name}`,
description: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title}`,
meeting: {
title: event.title,
profile: { name: profile.name, image: profile.image },
users: event.users.map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
},
robots: {
index: !(event.hidden || !isSEOIndexable),
follow: !(event.hidden || !isSEOIndexable),
},
};
return {
...(await generateMeetingMetadata(
payload.meeting,
payload.title,
payload.description,
hideBranding,
fullOrigin
)),
robots: payload.robots,
};
};
export const generateUserProfilePageMetadata = async (props: UserProfilePageArgs) => {
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
const fullOrigin = getOrgFullOrigin(orgSlug);
const payload: MetadataPayload = {
title: () => profile.name ?? "",
description: () => profile.markdownStrippedBio ?? "",
meeting: {
title: profile.markdownStrippedBio ?? "",
profile: { name: profile.name ?? "", image: profile.image ?? "" },
users: [{ name: profile.name ?? "", username: profile.username ?? "" }],
},
robots: {
index: isSEOIndexable,
follow: isSEOIndexable,
},
};
return {
...(await generateMeetingMetadata(
payload.meeting,
payload.title,
payload.description,
hideBranding,
fullOrigin
)),
robots: payload.robots,
};
};
export const generateTeamProfilePageMetadata = async (props: TeamProfilePageArgs) => {
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
const fullOrigin = getOrgFullOrigin(orgSlug);
const payload: MetadataPayload = {
title: (t) => profile.teamName || t("nameless_team"),
description: () => profile.markdownStrippedBio ?? "",
meeting: {
title: profile.markdownStrippedBio ?? "",
profile: { name: `${profile.teamName}`, image: profile.image ?? "" },
},
robots: {
index: isSEOIndexable,
follow: isSEOIndexable,
},
};
return {
...(await generateMeetingMetadata(
payload.meeting,
payload.title,
payload.description,
hideBranding,
fullOrigin
)),
robots: payload.robots,
};
};
+1 -4
View File
@@ -26,10 +26,7 @@ export const generateMetadata = () => prepareRootMetadata();
const getInitialProps = async (url: string) => {
const { pathname, searchParams } = new URL(url);
const isEmbedSnippetGeneratorPath = pathname.startsWith("/event-types");
const isEmbed =
(pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null) &&
!isEmbedSnippetGeneratorPath;
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
const embedColorScheme = searchParams?.get("ui.color-scheme");
const req = { headers: headers(), cookies: cookies() };
@@ -1,54 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps;
const getData = withAppDirSsr<OrgTypePageProps>(getServerSideProps);
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
const rescheduleUid = booking?.uid;
const profileName = eventData?.profile?.name ?? "";
const profileImage = eventData?.profile.image ?? "";
return await generateEventBookingPageMetadata({
profile: { name: profileName, image: profileImage },
event: {
title: eventData?.title ?? "",
hidden: eventData?.hidden ?? false,
users: [
...(eventData?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
},
hideBranding: isBrandingHidden,
orgSlug: eventData?.entity.orgSlug ?? null,
isSEOIndexable: !!isSEOIndexable,
isReschedule: !!rescheduleUid,
});
};
const Page = async (props: OrgTypePageProps) => {
if ((props as TeamTypePageProps)?.teamId) {
return <TeamTypePage {...(props as TeamTypePageProps)} />;
}
return <UserTypePage {...(props as UserTypePageProps)} />;
};
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
@@ -1,64 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import {
generateTeamProfilePageMetadata,
generateUserProfilePageMetadata,
} from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
export type OrgPageProps = UserPageProps | TeamPageProps;
const getData = withAppDirSsr<OrgPageProps>(getServerSideProps);
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
if ((props as TeamPageProps)?.team) {
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = props as TeamPageProps;
return await generateTeamProfilePageMetadata({
profile: {
teamName: team.name,
image: getOrgOrTeamAvatar(team),
markdownStrippedBio: markdownStrippedBio,
},
event: null,
hideBranding: false,
orgSlug: currentOrgDomain ?? null,
isSEOIndexable: !!isSEOIndexable,
});
} else {
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props as UserPageProps;
const isOrg = !!profile?.organization;
const allowSEOIndexing =
(!isOrg && profile.allowSEOIndexing) || (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing);
return await generateUserProfilePageMetadata({
profile: { name: profile.name, username: profile.username, image: profile.image, markdownStrippedBio },
hideBranding: false,
orgSlug: entity.orgSlug ?? null,
isSEOIndexable: !!allowSEOIndexing,
event: null,
});
}
};
const Page = async (props: OrgPageProps) => {
if ((props as TeamPageProps)?.team) {
return <TeamPage {...(props as TeamPageProps)} />;
}
return <UserPage {...(props as UserPageProps)} />;
};
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
-1
View File
@@ -1 +0,0 @@
export { default, generateMetadata } from "app/team/[slug]/page";
@@ -1 +0,0 @@
export { default, generateMetadata } from "app/team/[slug]/[type]/page";
@@ -1 +0,0 @@
export { default, generateMetadata } from "app/team/[slug]/page";
@@ -1,3 +1,4 @@
import { getServerSideProps as GSSUserPage } from "@pages/[user]";
import type { GetServerSidePropsContext } from "next";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
@@ -5,8 +6,6 @@ import prisma from "@calcom/prisma";
import { getServerSideProps as GSSTeamPage } from "@lib/team/[slug]/getServerSideProps";
import { getServerSideProps as GSSUserPage } from "@server/lib/[user]/getServerSideProps";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const team = await prisma.team.findFirst({
where: {
@@ -55,6 +55,7 @@ const getTheLastArrayElement = (value: ReadonlyArray<string> | string | undefine
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const slug = getTheLastArrayElement(context.query.slug) ?? getTheLastArrayElement(context.query.orgSlug);
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
context.req,
context.params?.orgSlug ?? context.query?.orgSlug
+1 -4
View File
@@ -194,10 +194,7 @@ export const config = {
"/settings/:path*",
"/reschedule/:path*",
"/availability/:path*",
"/org/:path*",
"/team/:path*",
"/:user/:type/",
"/:user/",
"/booking/:path*",
],
};
@@ -2,6 +2,7 @@
import { Booker } from "@calcom/atoms/monorepo";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
@@ -19,6 +20,13 @@ export default function Type({
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
entity={entity}
/>
<Booker
username={user}
eventSlug={slug}
@@ -1,17 +1,28 @@
"use client";
import type { EmbedProps } from "app/WithEmbedSSR";
import { Booker } from "@calcom/atoms/monorepo";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import type { getServerSideProps } from "@lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "app/WithEmbedSSR";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
function Type({ slug, user, booking, isEmbed, isBrandingHidden, entity, eventTypeId, duration }: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={undefined}
hideBranding={isBrandingHidden}
isTeamEvent
entity={entity}
bookingData={booking}
/>
<Booker
username={user}
eventSlug={slug}
+97 -78
View File
@@ -12,6 +12,7 @@ import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -19,7 +20,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, Button, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import type { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
@@ -163,87 +164,105 @@ function TeamPage({
const profileImageSrc = getOrgOrTeamAvatar(team);
return (
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
<div className="relative">
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
<>
<HeadSeo
origin={getOrgFullOrigin(currentOrgDomain)}
title={teamName}
description={teamName}
meeting={{
title: markdownStrippedBio,
profile: {
name: `${team.name}`,
image: profileImageSrc,
},
}}
nextSeoProps={{
nofollow: !isSEOIndexable,
noindex: !isSEOIndexable,
}}
/>
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
<div className="relative">
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
</div>
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
{teamName}
</p>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
)}
</div>
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
{teamName}
</p>
{!isBioEmpty && (
{team.isOrganization ? (
!teamOrOrgIsPrivate ? (
<SubTeams />
) : (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
</div>
)
) : (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
{(showMembers.isOn || !team.eventTypes?.length) &&
(teamOrOrgIsPrivate ? (
<div className="w-full text-center">
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
{t("you_cannot_see_team_members")}
</h2>
</div>
) : (
<Team members={team.members} teamName={team.name} />
))}
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes eventTypes={team.eventTypes} />
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
</div>
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
<Button
color="minimal"
EndIcon="arrow-right"
data-testid="book-a-team-member-btn"
className="dark:hover:bg-darkgray-200"
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
query: {
...queryParamsToForward,
members: "1",
},
}}
shallow={true}>
{t("book_a_team_member")}
</Button>
</aside>
</div>
)}
</div>
)}
</>
)}
</div>
{team.isOrganization ? (
!teamOrOrgIsPrivate ? (
<SubTeams />
) : (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
</div>
)
) : (
<>
{(showMembers.isOn || !team.eventTypes?.length) &&
(teamOrOrgIsPrivate ? (
<div className="w-full text-center">
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
{t("you_cannot_see_team_members")}
</h2>
</div>
) : (
<Team members={team.members} teamName={team.name} />
))}
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes eventTypes={team.eventTypes} />
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
</div>
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
<Button
color="minimal"
EndIcon="arrow-right"
data-testid="book-a-team-member-btn"
className="dark:hover:bg-darkgray-200"
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
query: {
...queryParamsToForward,
members: "1",
},
}}
shallow={true}>
{t("book_a_team_member")}
</Button>
</aside>
</div>
)}
</div>
)}
</>
)}
</main>
</main>
</>
);
}
+23
View File
@@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation";
import { Booker } from "@calcom/atoms/monorepo";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import type { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -33,11 +34,33 @@ function Type({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isSEOIndexable,
}: PageProps) {
const searchParams = useSearchParams();
const { profile, users, hidden, title } = eventData;
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
eventData={
profile && users && title && hidden !== undefined
? {
profile,
users,
title,
hidden,
}
: undefined
}
entity={eventData.entity}
bookingData={booking}
isSEOIndexable={isSEOIndexable}
/>
<Booker
username={user}
eventSlug={slug}
@@ -1,8 +1,9 @@
import { render } from "@testing-library/react";
import { describe, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { HeadSeo } from "@calcom/ui";
import UserPage from "./users-public-view";
@@ -70,5 +71,33 @@ describe("UserPage Component", () => {
});
render(<UserPage {...mockData.props} />);
const expectedDescription = mockData.props.markdownStrippedBio;
const expectedTitle = expectedDescription;
expect(HeadSeo).toHaveBeenCalledWith(
{
origin: `${mockData.props.entity.orgSlug}.cal.local`,
title: `${mockData.props.profile.name}`,
description: expectedDescription,
meeting: {
profile: {
name: mockData.props.profile.name,
image: mockData.props.users[0].avatarUrl,
},
title: expectedTitle,
users: [
{
name: mockData.props.users[0].name,
username: mockData.props.users[0].username,
},
],
},
nextSeoProps: {
nofollow: !mockData.props.profile.allowSEOIndexing,
noindex: !mockData.props.profile.allowSEOIndexing,
},
},
{}
);
});
});
@@ -11,11 +11,12 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
import { HeadSeo, Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
import type { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
@@ -59,89 +60,112 @@ export function UserPage(props: PageProps) {
const isEventListEmpty = eventTypes.length === 0;
const isOrg = !!user?.profile?.organization;
const allowSEOIndexing = isOrg
? isOrgSEOIndexable
? profile.allowSEOIndexing
: false
: profile.allowSEOIndexing;
return (
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<UserAvatar
size="xl"
user={{
avatarUrl: user.avatarUrl,
profile: user.profile,
name: profile.name,
username: profile.username,
}}
/>
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
{profile.name}
{!isOrg && user.verified && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
/>
)}
{isOrg && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
/>
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
)}
</div>
<>
<HeadSeo
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
title={profile.name}
description={markdownStrippedBio}
meeting={{
title: markdownStrippedBio,
profile: { name: `${profile.name}`, image: user.avatarUrl || null },
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
nextSeoProps={{
noindex: !allowSEOIndexing,
nofollow: !allowSEOIndexing,
}}
/>
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{eventTypes.map((type) => (
<Link
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<UserAvatar
size="xl"
user={{
avatarUrl: user.avatarUrl,
profile: user.profile,
name: profile.name,
username: profile.username,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
data-testid="event-type-link">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<div className="flex flex-wrap items-center">
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
/>
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
{profile.name}
{!isOrg && user.verified && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
/>
)}
{isOrg && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
/>
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
)}
</div>
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{eventTypes.map((type) => (
<Link
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
data-testid="event-type-link">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<div className="flex flex-wrap items-center">
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
</div>
</Link>
))}
</div>
</Link>
))}
</div>
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
</>
);
}
UserPage.isBookingPage = true;
@@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation";
import { Booker } from "@calcom/atoms/monorepo";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -22,11 +23,40 @@ export const getMultipleDurationValue = (
return defaultValue;
};
function Type({ slug, user, isEmbed, booking, isBrandingHidden, eventData, orgBannerUrl }: PageProps) {
function Type({
slug,
user,
isEmbed,
booking,
isBrandingHidden,
isSEOIndexable,
rescheduleUid,
eventData,
orgBannerUrl,
}: PageProps) {
const searchParams = useSearchParams();
const { profile, users, hidden, title } = eventData;
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
isSEOIndexable={isSEOIndexable ?? true}
eventData={
profile && users && title && hidden !== undefined
? {
profile,
users,
title,
hidden,
}
: undefined
}
entity={eventData.entity}
bookingData={booking}
/>
<Booker
username={user}
eventSlug={slug}
+1 -10
View File
@@ -1,16 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import { getServerSideProps as _getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
import type { PageProps } from "~/users/views/users-type-public-view";
import TypePage from "~/users/views/users-type-public-view";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
const Type = (props: PageProps) => <TypePage {...props} />;
Type.PageWrapper = PageWrapper;
export default Type;
export { default } from "./index";
+12
View File
@@ -0,0 +1,12 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/users/views/users-type-public-view";
import TypePage from "~/users/views/users-type-public-view";
export { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
const Type = (props: PageProps) => <TypePage {...props} />;
Type.PageWrapper = PageWrapper;
export default Type;
+2 -17
View File
@@ -1,21 +1,6 @@
import { getServerSideProps as _getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
export type PageProps = UserPageProps | TeamPageProps;
import { getServerSideProps as _getServerSideProps } from "@server/lib/[user]/getServerSideProps";
export { default } from "./index";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
function Page(props: PageProps) {
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
return <UserPage {...(props as UserPageProps)} />;
}
Page.PageWrapper = PageWrapper;
export default Page;
+11
View File
@@ -0,0 +1,11 @@
import PageWrapper from "@components/PageWrapper";
import User, { type PageProps } from "~/users/views/users-public-view";
export { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
const UserPage = (props: PageProps) => <User {...props} />;
UserPage.PageWrapper = PageWrapper;
export default UserPage;
@@ -1,20 +1,7 @@
import { getServerSideProps as _getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import { getServerSideProps as _getServerSideProps } from ".";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
export type PageProps = UserTypePageProps | TeamTypePageProps;
export default function Page(props: PageProps) {
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
return <UserTypePage {...(props as UserTypePageProps)} />;
}
Page.PageWrapper = PageWrapper;
export { default, type PageProps } from ".";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -0,0 +1,17 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
export type PageProps = UserTypePageProps | TeamTypePageProps;
export default function Page(props: PageProps) {
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
return <UserTypePage {...(props as UserTypePageProps)} />;
}
Page.PageWrapper = PageWrapper;
@@ -1 +1,9 @@
export { default, getServerSideProps } from "@pages/[user]/embed";
"use client";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[user]";
export { default } from "../[user]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -0,0 +1,21 @@
"use client";
import PageWrapper from "@components/PageWrapper";
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
export type PageProps = UserPageProps | TeamPageProps;
function Page(props: PageProps) {
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
return <UserPage {...(props as UserPageProps)} />;
}
Page.PageWrapper = PageWrapper;
export default Page;
+2 -9
View File
@@ -1,14 +1,7 @@
import { getServerSideProps as _getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import { getServerSideProps as _getServerSideProps } from "./index";
import TeamPage, { type PageProps } from "~/team/team-view";
const Page = (props: PageProps) => <TeamPage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
export { default } from "./index";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+1
View File
@@ -0,0 +1 @@
export { getServerSideProps, default } from "@pages/team/[slug]";
@@ -0,0 +1,10 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
import Type from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
const Page = (props: PageProps) => <Type {...props} />;
Page.PageWrapper = PageWrapper;
export { getServerSideProps } from "@lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps";
export default Page;
@@ -0,0 +1 @@
export { getServerSideProps, default } from "@pages/team/[slug]/[type]";
@@ -0,0 +1 @@
export { getServerSideProps, default } from "@pages/team/[slug]";
+10
View File
@@ -0,0 +1,10 @@
import PageWrapper from "@components/PageWrapper";
import TeamPage, { type PageProps } from "~/team/team-view";
const Page = (props: PageProps) => <TeamPage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
export { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
+10
View File
@@ -0,0 +1,10 @@
import PageWrapper from "@components/PageWrapper";
import TypePage, { type PageProps } from "~/team/type-view";
const Page = (props: PageProps) => <TypePage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
export { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
@@ -11,8 +11,6 @@ import {
selectSecondAvailableTimeSlotNextMonth,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test("dynamic booking", async ({ page, users }) => {
@@ -66,7 +66,7 @@ async function verifyRobotsMetaTag({ page, orgSlug, urls, expectedContent }: Ver
await doOnOrgDomain({ orgSlug, page }, async ({ page, goToUrlWithErrorHandling }) => {
for (const relativeUrl of urls) {
const { url } = await goToUrlWithErrorHandling(relativeUrl);
const metaTag = await page.locator('head > meta[name="robots"]').first();
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual(expectedContent);
}
@@ -103,7 +103,7 @@ test.describe("Organization Settings", () => {
`/${orgMember.username}`,
`/${orgMember.username}/${userEvent.slug}`,
],
expectedContent: "noindex, nofollow",
expectedContent: "noindex,nofollow",
});
});
});
@@ -130,7 +130,7 @@ test.describe("Organization Settings", () => {
`/${orgMember.username}`,
`/${orgMember.username}/${userEvent.slug}`,
],
expectedContent: "index, follow",
expectedContent: "index,follow",
});
});
});
@@ -148,7 +148,7 @@ test.describe("Organization Settings", () => {
page,
orgSlug: org.slug,
urls: [`/${orgMember.username}/${userEvent.slug}`],
expectedContent: "noindex, nofollow",
expectedContent: "noindex,nofollow",
});
});
});
@@ -19,12 +19,14 @@ checkRoute "$APP_ROUTER_AUTH_SAML_ENABLED" app/future/auth/saml-idp
checkRoute "$APP_ROUTER_AUTH_ERROR_ENABLED" app/future/auth/error
checkRoute "$APP_ROUTER_AUTH_PLATFORM_ENABLED" app/future/auth/platform
checkRoute "$APP_ROUTER_AUTH_OAUTH2_ENABLED" app/future/auth/oauth2
checkRoute "$APP_ROUTER_TEAM_ENABLED" app/future/team
# These are routes that don't have and environment variable to enable or disable them
# Will stop removing gradually as we test and confirm that they are working
rm -rf \
app/future/d\
app/future/enterprise\
app/future/org\
app/future/payment\
app/future/reschedule\
app/future/routing-forms\
@@ -25,7 +25,7 @@ type Props = {
"profile" | "users"
> & {
profile: {
image: string | null;
image: string | undefined;
name: string | null;
username: string | null;
};
@@ -158,7 +158,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
multipleDuration: [15, 30, 45, 60, 90],
},
profile: {
image: eventData.profile.image ?? null,
image: eventData.profile.image,
name: eventData.profile.name ?? null,
username: eventData.profile.username ?? null,
},
@@ -254,7 +254,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
length: eventData.length,
metadata: eventData.metadata,
profile: {
image: eventData.profile.image ?? null,
image: eventData.profile.image,
name: eventData.profile.name ?? null,
username: eventData.profile.username ?? null,
},
@@ -0,0 +1,94 @@
import { render } from "@testing-library/react";
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { trpc } from "@calcom/trpc/react";
import { HeadSeo } from "@calcom/ui";
import { BookerSeo } from "./BookerSeo";
// Mocking necessary modules and hooks
vi.mock("@calcom/trpc/react", () => ({
trpc: {
viewer: {
public: {
event: {
useQuery: vi.fn(),
},
},
},
},
}));
vi.mock("@calcom/lib/hooks/useLocale", () => ({
useLocale: () => ({ t: (key: string) => key }),
}));
vi.mock("@calcom/ee/organizations/lib/orgDomains", () => ({
getOrgFullOrigin: vi.fn(),
}));
vi.mock("@calcom/ui", () => ({
HeadSeo: vi.fn(),
}));
describe("BookerSeo Component", () => {
it("renders HeadSeo with correct props", () => {
const mockData = {
event: {
slug: "event-slug",
profile: { name: "John Doe", image: "image-url", username: "john" },
title: "30min",
hidden: false,
users: [{ name: "Jane Doe", username: "jane" }],
},
entity: { fromRedirectOfNonOrgLink: false, orgSlug: "org1" },
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
trpc.viewer.public.event.useQuery.mockReturnValueOnce({
data: mockData.event,
});
vi.mocked(getOrgFullOrigin).mockImplementation((text: string | null) => `${text}.cal.local`);
render(
<BookerSeo
username={mockData.event.profile.username}
eventSlug={mockData.event.slug}
rescheduleUid={undefined}
entity={mockData.entity}
/>
);
expect(HeadSeo).toHaveBeenCalledWith(
{
origin: `${mockData.entity.orgSlug}.cal.local`,
isBrandingHidden: undefined,
// Don't know why we are adding space in the beginning
title: ` ${mockData.event.title} | ${mockData.event.profile.name}`,
description: ` ${mockData.event.title}`,
meeting: {
profile: {
name: mockData.event.profile.name,
image: mockData.event.profile.image,
},
title: mockData.event.title,
users: [
{
name: mockData.event.users[0].name,
username: mockData.event.users[0].username,
},
],
},
nextSeoProps: {
nofollow: true,
noindex: true,
},
},
{}
);
});
});
@@ -0,0 +1,88 @@
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { HeadSeo } from "@calcom/ui";
interface BookerSeoProps {
username: string;
eventSlug: string;
rescheduleUid: string | undefined;
hideBranding?: boolean;
isSEOIndexable?: boolean;
isTeamEvent?: boolean;
eventData?: Omit<
Pick<NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>, "profile" | "title" | "users" | "hidden">,
"profile" | "users"
> & {
profile: {
image: string | undefined;
name: string | null;
username: string | null;
};
users: {
username: string;
name: string;
}[];
};
entity: {
fromRedirectOfNonOrgLink: boolean;
orgSlug?: string | null;
teamSlug?: string | null;
name?: string | null;
};
bookingData?: GetBookingType | null;
}
export const BookerSeo = (props: BookerSeoProps) => {
const {
eventSlug,
username,
rescheduleUid,
hideBranding,
isTeamEvent,
entity,
isSEOIndexable,
bookingData,
eventData,
} = props;
const { t } = useLocale();
const { data: _event } = trpc.viewer.public.event.useQuery(
{
username,
eventSlug,
isTeamEvent,
org: entity.orgSlug ?? null,
fromRedirectOfNonOrgLink: entity.fromRedirectOfNonOrgLink,
},
{ refetchOnWindowFocus: false, enabled: !eventData }
);
const event = eventData ?? _event;
const profileName = event?.profile.name ?? "";
const profileImage = event?.profile.image;
const title = event?.title ?? "";
return (
<HeadSeo
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
title={`${rescheduleUid && !!bookingData ? t("reschedule") : ""} ${title} | ${profileName}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${title}`}
meeting={{
title: title,
profile: { name: profileName, image: profileImage },
users: [
...(event?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
}}
nextSeoProps={{
nofollow: event?.hidden || !isSEOIndexable,
noindex: event?.hidden || !isSEOIndexable,
}}
isBrandingHidden={hideBranding}
/>
);
};
+1 -1
View File
@@ -178,7 +178,7 @@ export const getUsernameList = (users: string | string[] | undefined): string[]
// So, even though this code handles even if individual user is dynamic link, that isn't a possibility right now.
users = arrayCast(users);
const allUsers = users.map((user) => user.replace(/( |%20|%2b)/gi, "+").split("+")).flat();
const allUsers = users.map((user) => user.replace(/( |%20|%2b)/g, "+").split("+")).flat();
return Array.prototype.concat(...allUsers.map((userSlug) => slugify(userSlug)));
};
-13
View File
@@ -800,19 +800,6 @@ export class UserRepository {
return withSelectedCalendars(user);
}
static async getAvatarUrl(id: number) {
const user = await prisma.user.findUnique({
where: { id },
select: { avatarUrl: true },
});
if (!user?.avatarUrl) {
return null;
}
return user.avatarUrl;
}
static async findForAvailabilityCheck({ where }: { where: Prisma.UserWhereInput }) {
const user = await prisma.user.findFirst({
where,
+15 -2
View File
@@ -2,8 +2,8 @@ import type { NextSeoProps } from "next-seo";
import { NextSeo } from "next-seo";
import { usePathname } from "next/navigation";
import type { AppImageProps } from "@calcom/lib/OgImages";
import { constructAppImage, constructGenericImage } from "@calcom/lib/OgImages";
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { buildCanonical, getSeoImage, seoConfig } from "@calcom/lib/next-seo.config";
import { truncateOnWord } from "@calcom/lib/text";
@@ -16,6 +16,7 @@ export type HeadSeoProps = {
canonical?: string;
nextSeoProps?: NextSeoProps;
app?: AppImageProps;
meeting?: MeetingImageProps;
isBrandingHidden?: boolean;
origin?: string;
};
@@ -82,6 +83,7 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
canonical = defaultUrl,
nextSeoProps = {},
app,
meeting,
isBrandingHidden,
} = props;
@@ -96,6 +98,17 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
siteName,
});
if (meeting) {
const pageImage = getSeoImage("ogImage") + constructMeetingImage(meeting);
seoObject = buildSeoMeta({
title: pageTitle,
description: truncatedDescription,
image: pageImage,
canonical,
siteName,
});
}
if (app) {
const pageImage =
getSeoImage("ogImage") + constructAppImage({ ...app, description: truncatedDescription });
@@ -45,6 +45,9 @@ vi.mock("@calcom/lib/OgImages", async () => {
constructGenericImage() {
return "constructGenericImage";
},
constructMeetingImage() {
return "constructMeetingImage";
},
};
});
@@ -146,4 +149,12 @@ describe("Tests for HeadSeo component", () => {
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructAppImage");
});
});
test("Should render with meeting props", async () => {
const { container } = render(<HeadSeo {...basicProps} meeting={{} as HeadSeoProps["meeting"]} />);
await waitFor(async () => {
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructMeetingImage");
});
});
});
+1
View File
@@ -251,6 +251,7 @@
"APP_ROUTER_AUTH_ERROR_ENABLED",
"APP_ROUTER_AUTH_PLATFORM_ENABLED",
"APP_ROUTER_AUTH_OAUTH2_ENABLED",
"APP_ROUTER_TEAM_ENABLED",
"APP_USER_NAME",
"BASECAMP3_CLIENT_ID",
"BASECAMP3_CLIENT_SECRET",