import classNames from "classnames"; import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; import { Toaster } from "react-hot-toast"; import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; 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 { WEBAPP_URL } from "@calcom/lib/constants"; import defaultEvents, { getDynamicEventDescription, getGroupName, getUsernameList, getUsernameSlugLink, } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; 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 { baseEventTypeSelect } from "@calcom/prisma/selects"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui"; import { Verified, ArrowRight } from "@calcom/ui/components/icon"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { EmbedProps } from "@lib/withEmbedSsr"; import PageWrapper from "@components/PageWrapper"; import { ssrInit } from "@server/lib/ssr"; export type UserPageProps = inferSSRProps & EmbedProps; export function UserPage(props: UserPageProps) { const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser, markdownStrippedBio, } = props; const [user] = users; //To be used when we only have a single user, not dynamic group useTheme(user.theme); const { t } = useLocale(); const router = useRouter(); const isBioEmpty = !user.bio || !user.bio.replace("


", "").length; const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (

{" " + t("unavailable")}

{t("user_dynamic_booking_disabled") as string}

) : ( ); const isEmbed = useIsEmbed(props.isEmbed); const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem"); const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const query = { ...router.query }; delete query.user; // So it doesn't display in the Link (and make tests fail) delete query.orgSlug; const nameOrUsername = user.name || user.username || ""; /* 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]); */ const isEventListEmpty = eventTypes.length === 0; return ( <> ({ username, name: dynamicNames[index] })) : [{ username: `${user.username}`, name: `${user.name}` }], }} />
{isSingleUser && ( // When we deal with a single user, not dynamic group

{nameOrUsername} {user.verified && ( )}

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

😴{" " + t("user_away")}

{t("user_away_description") as string}

) : isDynamicGroup ? ( //When we deal with dynamic group (users > 1) groupEventTypes ) : ( 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 const getServerSideProps = async (context: GetServerSidePropsContext) => { const ssr = await ssrInit(context); const crypto = await import("crypto"); const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? ""); const usernameList = getUsernameList(context.query.user as string); const dataFetchStart = Date.now(); const usersWithoutAvatar = await prisma.user.findMany({ where: { username: { in: usernameList, }, organization: isValidOrgDomain ? { slug: currentOrgDomain, } : null, }, select: { id: true, username: true, email: true, name: true, bio: true, brandColor: true, darkBrandColor: true, organizationId: true, theme: true, away: true, verified: true, allowDynamicBooking: true, }, }); const users = usersWithoutAvatar.map((user) => ({ ...user, avatar: `${WEBAPP_URL}/${user.username}/avatar.png`, })); if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) { return { notFound: true, } as { notFound: true; }; } const isDynamicGroup = users.length > 1; if (isDynamicGroup) { // sort and be in the same order as usernameList so first user is the first user in the list users.sort((a, b) => { const aIndex = (a.username && usernameList.indexOf(a.username)) || 0; const bIndex = (b.username && usernameList.indexOf(b.username)) || 0; return aIndex - bIndex; }); } const dynamicNames = isDynamicGroup ? users.map((user) => { return user.name || ""; }) : []; const [user] = users; //to be used when dealing with single user, not dynamic group const profile = isDynamicGroup ? { name: getGroupName(dynamicNames), image: null, theme: null, weekStart: "Sunday", brandColor: "", darkBrandColor: "", allowDynamicBooking: !users.some((user) => { return !user.allowDynamicBooking; }), } : { name: user.name || user.username, image: user.avatar, theme: user.theme, brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, }; const eventTypesWithHidden = isDynamicGroup ? [] : 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 isSingleUser = users.length === 1; const dynamicUsernames = isDynamicGroup ? users.map((user) => { return user.username || ""; }) : []; const safeBio = markdownToSafeHTML(user.bio) || ""; const markdownStrippedBio = stripMarkdown(user?.bio || ""); return { props: { users, safeBio, profile, // Dynamic group has no theme preference right now. It uses system theme. themeBasis: isDynamicGroup ? null : user.username, user: { emailMd5: crypto.createHash("md5").update(user.email).digest("hex"), }, eventTypes: isDynamicGroup ? defaultEvents.map((event) => { event.description = getDynamicEventDescription(dynamicUsernames, event.slug); return event; }) : eventTypes, trpcState: ssr.dehydrate(), isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser, markdownStrippedBio, }, }; }; export default UserPage;