intuita codemod: app-directory-boilerplate-calcom
This commit is contained in:
parent
0bdc45a1a5
commit
6860ee67f0
|
@ -2,16 +2,20 @@ import type { GetServerSideProps, GetServerSidePropsContext } from "next";
|
|||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
export const withAppDir =
|
||||
(getServerSideProps: GetServerSideProps) => async (context: GetServerSidePropsContext) => {
|
||||
<T extends Record<string, any>>(getServerSideProps: GetServerSideProps<T>) =>
|
||||
async (context: GetServerSidePropsContext): Promise<T> => {
|
||||
const ssrResponse = await getServerSideProps(context);
|
||||
|
||||
if ("redirect" in ssrResponse) {
|
||||
redirect(ssrResponse.redirect.destination);
|
||||
}
|
||||
|
||||
if ("notFound" in ssrResponse) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return ssrResponse.props;
|
||||
return {
|
||||
...ssrResponse.props,
|
||||
// includes dehydratedState required for future page trpcPropvider
|
||||
...("trpcState" in ssrResponse.props && { dehydratedState: ssrResponse.props.trpcState }),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import LegacyPage from "@pages/[user]/[type]";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getPageProps } from "../page";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getPageProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
|
@ -0,0 +1,208 @@
|
|||
import LegacyPage, { type PageProps } from "@pages/[user]/[type]";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const pageProps = await getPageProps(
|
||||
buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
const { eventData, booking, user, slug } = pageProps;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { trpc } = await import("@calcom/trpc/react");
|
||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||
{ username: user, eventSlug: slug, isTeamEvent: false, org: eventData.entity.orgSlug ?? null },
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
|
||||
const paramsSchema = z.object({
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => getUsernameList(s)),
|
||||
});
|
||||
|
||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernames,
|
||||
},
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
select: {
|
||||
allowDynamicBooking: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!users.length) {
|
||||
return notFound();
|
||||
}
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username: usernames.join("+"),
|
||||
eventSlug: slug,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: {
|
||||
...eventData.metadata,
|
||||
multipleDuration: [15, 30, 60],
|
||||
},
|
||||
},
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
away: false,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
themeBasis: null,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const username = usernames[0];
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
|
||||
if (!isOrgContext) {
|
||||
const redirectObj = await getTemporaryOrgRedirect({
|
||||
slug: usernames[0],
|
||||
redirectType: RedirectType.User,
|
||||
eventTypeSlug: slug,
|
||||
currentQuery: context.query,
|
||||
});
|
||||
|
||||
if (redirectObj) {
|
||||
return redirect(redirectObj.redirect.destination);
|
||||
}
|
||||
}
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
||||
},
|
||||
select: {
|
||||
away: true,
|
||||
hideBranding: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we can show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username,
|
||||
eventSlug: slug,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
booking,
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
away: user?.away,
|
||||
user: username,
|
||||
slug,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
themeBasis: username,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
export const getPageProps = async (context: GetServerSidePropsContext) => {
|
||||
const { user } = paramsSchema.parse(context.params);
|
||||
const isDynamicGroup = user.length > 1;
|
||||
|
||||
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
|
||||
};
|
||||
|
||||
// @ts-expect-error arg
|
||||
export const getData = withAppDir<PageProps>(getPageProps);
|
||||
|
||||
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;
|
|
@ -0,0 +1,10 @@
|
|||
import LegacyPage from "@pages/[user]/[type]";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getPageProps } from "../page";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getPageProps);
|
||||
|
||||
// @ts-expect-error arg
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
|
@ -0,0 +1,202 @@
|
|||
import LegacyPage, { type UserPageProps } from "@pages/[user]";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { encode } from "querystring";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-user";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { getEventTypesWithHiddenFromDB } from "@lib/[user]/getServerSideProps";
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("workflows"),
|
||||
(t) => t("workflows_to_automate_notifications")
|
||||
);
|
||||
|
||||
export const getPageProps = async (context: GetServerSidePropsContext) => {
|
||||
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();
|
||||
let outOfOffice = false;
|
||||
|
||||
if (usernameList.length === 1) {
|
||||
const result = await handleUserRedirection({ username: usernameList[0] });
|
||||
if (result && result.outOfOffice) {
|
||||
outOfOffice = true;
|
||||
}
|
||||
if (result && result.redirect?.destination) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
avatarUrl: 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();
|
||||
}
|
||||
|
||||
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 ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
avatarUrl: user.avatarUrl,
|
||||
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||
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),
|
||||
}));
|
||||
|
||||
// if profile only has one public event-type, redirect to it
|
||||
if (eventTypes.length === 1 && context.query.redirect !== "false" && !outOfOffice) {
|
||||
// Redirect but don't change the URL
|
||||
const urlDestination = `/${user.username}/${eventTypes[0].slug}`;
|
||||
const { query } = context;
|
||||
const urlQuery = new URLSearchParams(encode(query));
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `${urlDestination}?${urlQuery}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||
const org = usersWithoutAvatar[0].organization;
|
||||
|
||||
return {
|
||||
users: users.map((user) => ({
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
bio: user.bio,
|
||||
avatarUrl: user.avatarUrl,
|
||||
away: usernameList.length === 1 ? outOfOffice : 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,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
markdownStrippedBio,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-expect-error arg
|
||||
export const getData = withAppDir<UserPageProps>(getPageProps);
|
||||
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
|
|
@ -1,7 +1,7 @@
|
|||
import LegacyPage from "@pages/apps/categories/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import AppsPage from "@pages/apps";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import LegacyPage from "@pages/getting-started/[[...step]]";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
@ -10,10 +9,7 @@ import prisma from "@calcom/prisma";
|
|||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
//@ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
|
||||
const session = await getServerSession({ req });
|
||||
const session = await getServerSession({ req: ctx.req });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import OldPage from "@pages/teams/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
|
|
@ -2,7 +2,7 @@ import OldPage from "@pages/video/[uid]";
|
|||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
import { type GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { handleTypeRedirection } from "@calcom/features/booking-redirect/handle-type";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
|
||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernames,
|
||||
},
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
select: {
|
||||
allowDynamicBooking: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!users.length) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username: usernames.join("+"),
|
||||
eventSlug: slug,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: {
|
||||
...eventData.metadata,
|
||||
multipleDuration: [15, 30, 60],
|
||||
},
|
||||
},
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
themeBasis: null,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const username = usernames[0];
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
let outOfOffice = false;
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
|
||||
if (!isOrgContext) {
|
||||
const redirect = await getTemporaryOrgRedirect({
|
||||
slug: usernames[0],
|
||||
redirectType: RedirectType.User,
|
||||
eventTypeSlug: slug,
|
||||
currentQuery: context.query,
|
||||
});
|
||||
|
||||
if (redirect) {
|
||||
return redirect;
|
||||
}
|
||||
}
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hideBranding: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
// If user is found, quickly verify bookingRedirects
|
||||
const result = await handleTypeRedirection({
|
||||
userId: user.id,
|
||||
username,
|
||||
slug,
|
||||
});
|
||||
if (result && result.outOfOffice) {
|
||||
outOfOffice = true;
|
||||
}
|
||||
if (result && result.redirect?.destination) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we can show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username,
|
||||
eventSlug: slug,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
away: outOfOffice,
|
||||
user: username,
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
themeBasis: username,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Booker page fetches a tiny bit of data server side, to determine early
|
||||
// whether the page should show an away state or dynamic booking not allowed.
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { user } = paramsSchema.parse(context.params);
|
||||
const isDynamicGroup = user.length > 1;
|
||||
|
||||
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
|
||||
};
|
||||
|
||||
const paramsSchema = z.object({
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => getUsernameList(s)),
|
||||
});
|
|
@ -0,0 +1,291 @@
|
|||
import type { DehydratedState } from "@tanstack/react-query";
|
||||
import type { GetServerSideProps } from "next";
|
||||
import { encode } from "querystring";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-user";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import logger from "@calcom/lib/logger";
|
||||
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 { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
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<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl">[];
|
||||
themeBasis: string | null;
|
||||
markdownStrippedBio: string;
|
||||
safeBio: string;
|
||||
entity: {
|
||||
isUnpublished?: boolean;
|
||||
orgSlug?: string | null;
|
||||
name?: string | null;
|
||||
};
|
||||
eventTypes: ({
|
||||
descriptionAsSafeHTML: string;
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
} & Pick<
|
||||
EventType,
|
||||
| "id"
|
||||
| "title"
|
||||
| "slug"
|
||||
| "length"
|
||||
| "hidden"
|
||||
| "lockTimeZoneToggleOnBookingPage"
|
||||
| "requiresConfirmation"
|
||||
| "requiresBookerEmailVerification"
|
||||
| "price"
|
||||
| "currency"
|
||||
| "recurringEvent"
|
||||
>)[];
|
||||
} & EmbedProps;
|
||||
|
||||
export const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
const eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
users: {
|
||||
some: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
position: "desc",
|
||||
},
|
||||
{
|
||||
id: "asc",
|
||||
},
|
||||
],
|
||||
select: {
|
||||
...baseEventTypeSelect,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
// map and filter metadata, exclude eventType entirely when faulty metadata is found.
|
||||
// report error to exception so we don't lose the error.
|
||||
return eventTypes.reduce<typeof eventTypes>((eventTypes, eventType) => {
|
||||
const parsedMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata);
|
||||
if (!parsedMetadata.success) {
|
||||
logger.error(parsedMetadata.error);
|
||||
return eventTypes;
|
||||
}
|
||||
eventTypes.push({
|
||||
...eventType,
|
||||
metadata: parsedMetadata.data,
|
||||
});
|
||||
return eventTypes;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<UserPageProps> = 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();
|
||||
let outOfOffice = false;
|
||||
|
||||
if (usernameList.length === 1) {
|
||||
const result = await handleUserRedirection({ username: usernameList[0] });
|
||||
if (result && result.outOfOffice) {
|
||||
outOfOffice = true;
|
||||
}
|
||||
if (result && result.redirect?.destination) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
avatarUrl: 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 ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
avatarUrl: user.avatarUrl,
|
||||
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||
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),
|
||||
}));
|
||||
|
||||
// if profile only has one public event-type, redirect to it
|
||||
if (eventTypes.length === 1 && context.query.redirect !== "false" && !outOfOffice) {
|
||||
// Redirect but don't change the URL
|
||||
const urlDestination = `/${user.username}/${eventTypes[0].slug}`;
|
||||
const { query } = context;
|
||||
const urlQuery = new URLSearchParams(encode(query));
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `${urlDestination}?${urlQuery}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
avatarUrl: user.avatarUrl,
|
||||
away: usernameList.length === 1 ? outOfOffice : 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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,11 +1,10 @@
|
|||
import type { DehydratedState } from "@tanstack/react-query";
|
||||
"use client";
|
||||
|
||||
import classNames from "classnames";
|
||||
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { encode } from "querystring";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import type { z } from "zod";
|
||||
|
||||
import {
|
||||
sdkActionManager,
|
||||
|
@ -13,34 +12,20 @@ import {
|
|||
useEmbedStyles,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-user";
|
||||
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 { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
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 logger from "@calcom/lib/logger";
|
||||
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 { UserAvatar } from "@calcom/ui";
|
||||
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
import { getServerSideProps, type UserPageProps } from "@lib/[user]/getServerSideProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "../lib/getTemporaryOrgRedirect";
|
||||
export { getServerSideProps, UserPageProps };
|
||||
|
||||
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||
|
@ -215,273 +200,4 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||
UserPage.isBookingPage = true;
|
||||
UserPage.PageWrapper = PageWrapper;
|
||||
|
||||
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
const eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
users: {
|
||||
some: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
position: "desc",
|
||||
},
|
||||
{
|
||||
id: "asc",
|
||||
},
|
||||
],
|
||||
select: {
|
||||
...baseEventTypeSelect,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
// map and filter metadata, exclude eventType entirely when faulty metadata is found.
|
||||
// report error to exception so we don't lose the error.
|
||||
return eventTypes.reduce<typeof eventTypes>((eventTypes, eventType) => {
|
||||
const parsedMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata);
|
||||
if (!parsedMetadata.success) {
|
||||
logger.error(parsedMetadata.error);
|
||||
return eventTypes;
|
||||
}
|
||||
eventTypes.push({
|
||||
...eventType,
|
||||
metadata: parsedMetadata.data,
|
||||
});
|
||||
return eventTypes;
|
||||
}, []);
|
||||
};
|
||||
|
||||
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<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl">[];
|
||||
themeBasis: string | null;
|
||||
markdownStrippedBio: string;
|
||||
safeBio: string;
|
||||
entity: {
|
||||
isUnpublished?: boolean;
|
||||
orgSlug?: string | null;
|
||||
name?: string | null;
|
||||
};
|
||||
eventTypes: ({
|
||||
descriptionAsSafeHTML: string;
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
} & Pick<
|
||||
EventType,
|
||||
| "id"
|
||||
| "title"
|
||||
| "slug"
|
||||
| "length"
|
||||
| "hidden"
|
||||
| "lockTimeZoneToggleOnBookingPage"
|
||||
| "requiresConfirmation"
|
||||
| "requiresBookerEmailVerification"
|
||||
| "price"
|
||||
| "currency"
|
||||
| "recurringEvent"
|
||||
>)[];
|
||||
} & EmbedProps;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<UserPageProps> = 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();
|
||||
let outOfOffice = false;
|
||||
|
||||
if (usernameList.length === 1) {
|
||||
const result = await handleUserRedirection({ username: usernameList[0] });
|
||||
if (result && result.outOfOffice) {
|
||||
outOfOffice = true;
|
||||
}
|
||||
if (result && result.redirect?.destination) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
avatarUrl: 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 ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
avatarUrl: user.avatarUrl,
|
||||
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||
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),
|
||||
}));
|
||||
|
||||
// if profile only has one public event-type, redirect to it
|
||||
if (eventTypes.length === 1 && context.query.redirect !== "false" && !outOfOffice) {
|
||||
// Redirect but don't change the URL
|
||||
const urlDestination = `/${user.username}/${eventTypes[0].slug}`;
|
||||
const { query } = context;
|
||||
const urlQuery = new URLSearchParams(encode(query));
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `${urlDestination}?${urlQuery}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
avatarUrl: user.avatarUrl,
|
||||
away: usernameList.length === 1 ? outOfOffice : 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;
|
||||
|
|
|
@ -1,28 +1,19 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { handleTypeRedirection } from "@calcom/features/booking-redirect/handle-type";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
|
||||
import { getServerSideProps } from "@lib/[user]/[type]/getServerSideProps";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
export type PageProps = Omit<inferSSRProps<typeof getServerSideProps>, "trpcState"> & EmbedProps;
|
||||
export { getServerSideProps };
|
||||
|
||||
export const getMultipleDurationValue = (
|
||||
multipleDurationConfig: number[] | undefined,
|
||||
|
@ -81,191 +72,3 @@ export default function Type({
|
|||
|
||||
Type.isBookingPage = true;
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernames,
|
||||
},
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
select: {
|
||||
allowDynamicBooking: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!users.length) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username: usernames.join("+"),
|
||||
eventSlug: slug,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: {
|
||||
...eventData.metadata,
|
||||
multipleDuration: [15, 30, 60],
|
||||
},
|
||||
},
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
themeBasis: null,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const username = usernames[0];
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
let outOfOffice = false;
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
|
||||
if (!isOrgContext) {
|
||||
const redirect = await getTemporaryOrgRedirect({
|
||||
slug: usernames[0],
|
||||
redirectType: RedirectType.User,
|
||||
eventTypeSlug: slug,
|
||||
currentQuery: context.query,
|
||||
});
|
||||
|
||||
if (redirect) {
|
||||
return redirect;
|
||||
}
|
||||
}
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hideBranding: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
// If user is found, quickly verify bookingRedirects
|
||||
const result = await handleTypeRedirection({
|
||||
userId: user.id,
|
||||
username,
|
||||
slug,
|
||||
});
|
||||
if (result && result.outOfOffice) {
|
||||
outOfOffice = true;
|
||||
}
|
||||
if (result && result.redirect?.destination) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
} else if (bookingUid) {
|
||||
booking = await getBookingForSeatedEvent(`${bookingUid}`);
|
||||
}
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we can show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username,
|
||||
eventSlug: slug,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
away: outOfOffice,
|
||||
user: username,
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
themeBasis: username,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const paramsSchema = z.object({
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => getUsernameList(s)),
|
||||
});
|
||||
|
||||
// Booker page fetches a tiny bit of data server side, to determine early
|
||||
// whether the page should show an away state or dynamic booking not allowed.
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { user } = paramsSchema.parse(context.params);
|
||||
const isDynamicGroup = user.length > 1;
|
||||
|
||||
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
"use client";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/[user]/[type]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
export { default } from "../[type]";
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
"use client";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[user]";
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/[user]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
export { default } from "../[user]";
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user