import type { DehydratedState } from "@tanstack/react-query"; import classNames from "classnames"; import type { GetServerSideProps, InferGetServerSidePropsType } from "next"; import Link from "next/link"; import { Toaster } from "react-hot-toast"; import type { z } from "zod"; import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar"; import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; import { orgDomainConfig } 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 { getUsernameList } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import useTheme from "@calcom/lib/hooks/useTheme"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import prisma from "@calcom/prisma"; import { RedirectType, type EventType, type User } from "@calcom/prisma/client"; import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { HeadSeo, UnpublishedEntity } from "@calcom/ui"; import { Verified, ArrowRight } from "@calcom/ui/components/icon"; import type { EmbedProps } from "@lib/withEmbedSsr"; import PageWrapper from "@components/PageWrapper"; import { ssrInit } from "@server/lib/ssr"; import { getTemporaryOrgRedirect } from "../lib/getTemporaryOrgRedirect"; export function UserPage(props: InferGetServerSidePropsType) { const { users, profile, eventTypes, markdownStrippedBio, entity } = props; const [user] = users; //To be used when we only have a single user, not dynamic group useTheme(profile.theme); const { t } = useLocale(); const isBioEmpty = !user.bio || !user.bio.replace("


", "").length; const isEmbed = useIsEmbed(props.isEmbed); const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem"); const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const { // So it doesn't display in the Link (and make tests fail) user: _user, orgSlug: _orgSlug, ...query } = useRouterQuery(); /* const telemetry = useTelemetry(); useEffect(() => { if (top !== window) { //page_view will be collected automatically by _middleware.ts telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]")); } }, [telemetry, router.asPath]); */ if (entity?.isUnpublished) { return (
); } const isEventListEmpty = eventTypes.length === 0; return ( <>

{profile.name} {user.verified && ( )}

{!isBioEmpty && ( <>
)}
{user.away ? (

😴{` ${t("user_away")}`}

{t("user_away_description") as string}

) : ( eventTypes.map((type) => (
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
{ sdkActionManager?.fire("eventTypeSelected", { eventType: type, }); }} data-testid="event-type-link">

{type.title}

)) )}
{isEventListEmpty && }
); } UserPage.isBookingPage = true; UserPage.PageWrapper = PageWrapper; const getEventTypesWithHiddenFromDB = async (userId: number) => { return ( await prisma.eventType.findMany({ where: { AND: [ { teamId: null, }, { OR: [ { userId, }, { users: { some: { id: userId, }, }, }, ], }, ], }, orderBy: [ { position: "desc", }, { id: "asc", }, ], select: { ...baseEventTypeSelect, metadata: true, }, }) ).map((eventType) => ({ ...eventType, metadata: EventTypeMetaDataSchema.parse(eventType.metadata), })); }; export type UserPageProps = { trpcState: DehydratedState; profile: { name: string; image: string; theme: string | null; brandColor: string; darkBrandColor: string; organization: { requestedSlug: string | null; slug: string | null; id: number | null; }; allowSEOIndexing: boolean; username: string | null; }; users: Pick[]; themeBasis: string | null; markdownStrippedBio: string; safeBio: string; entity: { isUnpublished?: boolean; orgSlug?: string | null; name?: string | null; }; eventTypes: ({ descriptionAsSafeHTML: string; metadata: z.infer; } & Pick< EventType, | "id" | "title" | "slug" | "length" | "hidden" | "lockTimeZoneToggleOnBookingPage" | "requiresConfirmation" | "requiresBookerEmailVerification" | "price" | "currency" | "recurringEvent" >)[]; } & EmbedProps; export const getServerSideProps: GetServerSideProps = async (context) => { const ssr = await ssrInit(context); const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); const usernameList = getUsernameList(context.query.user as string); const isOrgContext = isValidOrgDomain && currentOrgDomain; const dataFetchStart = Date.now(); const usersWithoutAvatar = await prisma.user.findMany({ where: { username: { in: usernameList, }, organization: isOrgContext ? getSlugOrRequestedSlug(currentOrgDomain) : null, }, select: { id: true, username: true, email: true, name: true, bio: true, metadata: true, brandColor: true, darkBrandColor: true, organizationId: true, organization: { select: { slug: true, name: true, metadata: true, }, }, theme: true, away: true, verified: true, allowDynamicBooking: true, allowSEOIndexing: true, }, }); const isDynamicGroup = usersWithoutAvatar.length > 1; if (isDynamicGroup) { return { redirect: { permanent: false, destination: `/${usernameList.join("+")}/dynamic`, }, } as { redirect: { permanent: false; destination: string; }; }; } const users = usersWithoutAvatar.map((user) => ({ ...user, organization: { ...user.organization, metadata: user.organization?.metadata ? teamMetadataSchema.parse(user.organization.metadata) : null, }, avatar: `/${user.username}/avatar.png`, })); if (!isOrgContext) { const redirect = await getTemporaryOrgRedirect({ slug: usernameList[0], redirectType: RedirectType.User, eventTypeSlug: null, currentQuery: context.query, }); if (redirect) { return redirect; } } if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) { return { notFound: true, } as { notFound: true; }; } const [user] = users; //to be used when dealing with single user, not dynamic group const profile = { name: user.name || user.username || "", image: user.avatar, theme: user.theme, brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, allowSEOIndexing: user.allowSEOIndexing ?? true, username: user.username, organization: { id: user.organizationId, slug: user.organization?.slug ?? null, requestedSlug: user.organization?.metadata?.requestedSlug ?? null, }, }; const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id); const dataFetchEnd = Date.now(); if (context.query.log === "1") { context.res.setHeader("X-Data-Fetch-Time", `${dataFetchEnd - dataFetchStart}ms`); } const eventTypesRaw = eventTypesWithHidden.filter((evt) => !evt.hidden); const eventTypes = eventTypesRaw.map((eventType) => ({ ...eventType, metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), descriptionAsSafeHTML: markdownToSafeHTML(eventType.description), })); const safeBio = markdownToSafeHTML(user.bio) || ""; const markdownStrippedBio = stripMarkdown(user?.bio || ""); const org = usersWithoutAvatar[0].organization; return { props: { users: users.map((user) => ({ name: user.name, username: user.username, bio: user.bio, away: user.away, verified: user.verified, })), entity: { isUnpublished: org?.slug === null, orgSlug: currentOrgDomain, name: org?.name ?? null, }, eventTypes, safeBio, profile, // Dynamic group has no theme preference right now. It uses system theme. themeBasis: user.username, trpcState: ssr.dehydrate(), markdownStrippedBio, }, }; }; export default UserPage;