chore: app router - /team, /org, /[user] booking pages (excl. embeds) (#18186)
* update env vars * update middleware * remove pages router and move pages to /app * move to /team * update imports * fix * remove pages router and move org pages to /app * wip * fix orgSlug/user pages * fix orgSlug/user/type pages * remove generateMetadata from embed pages * fix * remove pages router for /user pages * generateMetadata is not needed in embed pages * remove future/org * no layout in [user] page * simplify * fix * fix OG image for [user] * fix OG images for org/user and team/slug * fix OG images for booking page * fix all metadata * use isBrandingHidden * remove BookerSeo and its usages * rename excludeAppNameFromTitle -> hideBranding * remove logic for meeting type in HeadSeo * remove BookerSeo instances from team-view and users-public-view * create generateMeetingMetadata util and use it to reduce duplicate code * remove BookerSeo imports * fix spacing * remove constructMeetingImage mock from head-seo.test * fetch avatarUrl using user id for user page metadata * fix test * remove unused test cases * index and follow must be true by default * invert noindex/nofollow flags * remove HeadSeo for already migrated pages and refactor prepareMetadata * fix organization-settings.e2e.ts * fix order * enable parallel test execution for dynamic-booking e2e test * fix * + could be %2B in app router * refactor handling logic for embeds in app router * fix isEmbed * fix embed-core * remove dead code * move embed pages back to /future * add back embed pages in pages router * revert some changes * fix import type checks * simplify * fix * feat: Implement generateBookingPageMetadata function for improved SEO and metadata handling across user and event pages (#18440) - Added a new utility function `generateBookingPageMetadata` to streamline the generation of metadata for booking and user profile pages. - Updated multiple page components to utilize the new function, enhancing SEO indexing and metadata consistency. - Removed redundant code related to previous metadata generation methods, improving code clarity and maintainability. * fix dirs * Pr-review-fixes-app-router-team-pages (#18450) * Remove unnecessary getPublicEvent call * Remove unused variable * Fix 404 for team page (#18451) * Remove console log --------- Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
co-authored by
Hariom Balhara
parent
39173900b1
commit
2f9c8cb441
@@ -365,7 +365,6 @@ 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
|
||||
|
||||
@@ -20,7 +20,6 @@ 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,6 +1,6 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
|
||||
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 } = props;
|
||||
const { booking, user: username, slug: eventSlug, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
@@ -31,12 +31,21 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const profileImage = event?.profile.image;
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${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;
|
||||
};
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateUserProfilePageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view";
|
||||
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)
|
||||
);
|
||||
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);
|
||||
export default WithLayout({ getData, Page: LegacyPage })<"P">;
|
||||
+20
-1
@@ -3,7 +3,8 @@ import i18next from "i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import type { MeetingImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructGenericImage, constructMeetingImage } 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";
|
||||
@@ -103,3 +104,21 @@ 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],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 } = pageProps;
|
||||
const { booking, user: username, slug: eventSlug, isTeamEvent, isBrandingHidden } = pageProps;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
@@ -34,7 +34,8 @@ 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}`
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
|
||||
isBrandingHidden
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view";
|
||||
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 } = props;
|
||||
return await _generateMetadata(
|
||||
() => profile.name,
|
||||
() => markdownStrippedBio
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
|
||||
@@ -1,10 +1,9 @@
|
||||
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 { Page, type OrgTypePageProps } from "app/org/[orgSlug]/[user]/[type]/page";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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,8 +1,9 @@
|
||||
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 { Page, type OrgPageProps } from "app/org/[orgSlug]/[user]/page";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgPageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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 });
|
||||
@@ -1,29 +0,0 @@
|
||||
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,45 +0,0 @@
|
||||
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">;
|
||||
@@ -1,29 +0,0 @@
|
||||
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,151 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -26,7 +26,10 @@ export const generateMetadata = () => prepareRootMetadata();
|
||||
const getInitialProps = async (url: string) => {
|
||||
const { pathname, searchParams } = new URL(url);
|
||||
|
||||
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
|
||||
const isEmbedSnippetGeneratorPath = pathname.startsWith("/event-types");
|
||||
const isEmbed =
|
||||
(pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null) &&
|
||||
!isEmbedSnippetGeneratorPath;
|
||||
const embedColorScheme = searchParams?.get("ui.color-scheme");
|
||||
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
export 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 });
|
||||
@@ -0,0 +1,64 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export 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 });
|
||||
+4
-4
@@ -4,7 +4,6 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
@@ -16,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 } = await getData(context);
|
||||
const { slug: eventSlug, user: username, isBrandingHidden } = await getData(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
@@ -34,9 +33,10 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
|
||||
return await _generateMetadata(
|
||||
() => `${title} | ${profileName}`,
|
||||
() => `${title}`
|
||||
() => `${title}`,
|
||||
isBrandingHidden
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page })<"P">;
|
||||
export default WithLayout({ getData, Page, isBookingPage: true })<"P">;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, generateMetadata } from "app/team/[slug]/page";
|
||||
@@ -0,0 +1 @@
|
||||
export { default, generateMetadata } from "app/team/[slug]/[type]/page";
|
||||
@@ -0,0 +1 @@
|
||||
export { default, generateMetadata } from "app/team/[slug]/page";
|
||||
+22
-9
@@ -1,6 +1,6 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
|
||||
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 } = props;
|
||||
const { user: username, slug: eventSlug, booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
@@ -26,13 +26,26 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
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}`
|
||||
);
|
||||
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 getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { generateTeamProfilePageMetadata } 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/team/[slug]/getServerSideProps";
|
||||
|
||||
@@ -11,12 +13,21 @@ import type { PageProps } from "~/team/team-view";
|
||||
import LegacyPage 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 { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = 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,
|
||||
});
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
@@ -25,4 +36,5 @@ export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getServerSideProps as GSSUserPage } from "@pages/[user]";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
@@ -6,6 +5,8 @@ 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,7 +55,6 @@ 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
|
||||
|
||||
@@ -195,7 +195,10 @@ export const config = {
|
||||
"/settings/:path*",
|
||||
"/reschedule/:path*",
|
||||
"/availability/:path*",
|
||||
"/booking/:path*",
|
||||
"/org/:path*",
|
||||
"/team/:path*",
|
||||
"/:user/:type/",
|
||||
"/:user/",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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";
|
||||
|
||||
@@ -20,13 +19,6 @@ 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}
|
||||
|
||||
-10
@@ -2,7 +2,6 @@
|
||||
|
||||
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";
|
||||
@@ -13,15 +12,6 @@ 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}
|
||||
|
||||
@@ -12,7 +12,6 @@ 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";
|
||||
@@ -20,7 +19,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, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
|
||||
import { Avatar, Button, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
|
||||
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import type { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
@@ -164,105 +163,87 @@ function TeamPage({
|
||||
const profileImageSrc = getOrgOrTeamAvatar(team);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
{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>
|
||||
)
|
||||
) : (
|
||||
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
|
||||
{team.parent && `${team.parent.name} `}
|
||||
{teamName}
|
||||
</p>
|
||||
{!isBioEmpty && (
|
||||
<>
|
||||
{(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
|
||||
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 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import type { EmbedProps } from "app/WithEmbedSSR";
|
||||
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";
|
||||
import type { EmbedProps } from "app/WithEmbedSSR";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
@@ -34,33 +33,11 @@ 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,9 +1,8 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, 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";
|
||||
|
||||
@@ -71,33 +70,5 @@ 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,12 +11,11 @@ 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 { HeadSeo, Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
|
||||
import { Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
@@ -60,112 +59,89 @@ 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 (
|
||||
<>
|
||||
<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(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 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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{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
|
||||
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>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
|
||||
</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;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { EmbedProps } from "app/WithEmbedSSR";
|
||||
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";
|
||||
import type { EmbedProps } from "app/WithEmbedSSR";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
|
||||
|
||||
@@ -23,40 +22,11 @@ export const getMultipleDurationValue = (
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
function Type({
|
||||
slug,
|
||||
user,
|
||||
isEmbed,
|
||||
booking,
|
||||
isBrandingHidden,
|
||||
isSEOIndexable,
|
||||
rescheduleUid,
|
||||
eventData,
|
||||
orgBannerUrl,
|
||||
}: PageProps) {
|
||||
function Type({ slug, user, isEmbed, booking, isBrandingHidden, 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,7 +1,16 @@
|
||||
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);
|
||||
|
||||
export { default } from "./index";
|
||||
const Type = (props: PageProps) => <TypePage {...props} />;
|
||||
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
export default Type;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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;
|
||||
@@ -1,6 +1,19 @@
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "@server/lib/[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 { default } from "./index";
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
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;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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,7 +1,20 @@
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from ".";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export { default, type PageProps } 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 const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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,9 +1 @@
|
||||
"use client";
|
||||
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[user]";
|
||||
|
||||
export { default } from "../[user]";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
export { default, getServerSideProps } from "@pages/[user]/embed";
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"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;
|
||||
@@ -1,7 +1,14 @@
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "./index";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export { default } from "./index";
|
||||
import TeamPage, { type PageProps } from "~/team/team-view";
|
||||
|
||||
const Page = (props: PageProps) => <TeamPage {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { getServerSideProps, default } from "@pages/team/[slug]";
|
||||
@@ -1,10 +0,0 @@
|
||||
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;
|
||||
@@ -1 +0,0 @@
|
||||
export { getServerSideProps, default } from "@pages/team/[slug]/[type]";
|
||||
@@ -1 +0,0 @@
|
||||
export { getServerSideProps, default } from "@pages/team/[slug]";
|
||||
@@ -1,10 +0,0 @@
|
||||
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";
|
||||
@@ -1,10 +0,0 @@
|
||||
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,6 +11,8 @@ 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"]');
|
||||
const metaTag = await page.locator('head > meta[name="robots"]').first();
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,14 +20,12 @@ 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 | undefined;
|
||||
image: string | null;
|
||||
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,
|
||||
image: eventData.profile.image ?? null,
|
||||
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,
|
||||
image: eventData.profile.image ?? null,
|
||||
name: eventData.profile.name ?? null,
|
||||
username: eventData.profile.username ?? null,
|
||||
},
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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)/g, "+").split("+")).flat();
|
||||
const allUsers = users.map((user) => user.replace(/( |%20|%2b)/gi, "+").split("+")).flat();
|
||||
return Array.prototype.concat(...allUsers.map((userSlug) => slugify(userSlug)));
|
||||
};
|
||||
|
||||
|
||||
@@ -800,6 +800,19 @@ 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,
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { NextSeoProps } from "next-seo";
|
||||
import { NextSeo } from "next-seo";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||
import type { AppImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructAppImage, constructGenericImage } 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,7 +16,6 @@ export type HeadSeoProps = {
|
||||
canonical?: string;
|
||||
nextSeoProps?: NextSeoProps;
|
||||
app?: AppImageProps;
|
||||
meeting?: MeetingImageProps;
|
||||
isBrandingHidden?: boolean;
|
||||
origin?: string;
|
||||
};
|
||||
@@ -83,7 +82,6 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
||||
canonical = defaultUrl,
|
||||
nextSeoProps = {},
|
||||
app,
|
||||
meeting,
|
||||
isBrandingHidden,
|
||||
} = props;
|
||||
|
||||
@@ -98,17 +96,6 @@ 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,9 +45,6 @@ vi.mock("@calcom/lib/OgImages", async () => {
|
||||
constructGenericImage() {
|
||||
return "constructGenericImage";
|
||||
},
|
||||
constructMeetingImage() {
|
||||
return "constructMeetingImage";
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -149,12 +146,4 @@ 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,7 +252,6 @@
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user