chore: [app-router-migration 26] Migrate "/[user]" page (#13057)
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
1 parent
9422e28598
commit
c839e40a3e
17 files changed
+428
-279
No files matched your search
@@ -0,0 +1,12 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import LegacyPage from "~/users/views/users-type-public-view";
|
||||
import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import LegacyPage from "~/users/views/users-type-public-view";
|
||||
import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Record<string, string | string[]>;
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
|
||||
const { eventData, booking, user, slug } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { trpc } = await import("@calcom/trpc");
|
||||
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 getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
getData,
|
||||
Page: LegacyPage,
|
||||
getLayout: null,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<UserPageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Record<string, string | string[]>;
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
|
||||
const { profile, markdownStrippedBio } = props;
|
||||
return await _generateMetadata(
|
||||
() => profile.name,
|
||||
() => markdownStrippedBio
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<UserPageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
|
||||
@@ -5,9 +5,10 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getServerSideProps as GSSUserTypePage } from "@lib/[user]/[type]/getServerSideProps";
|
||||
import { getServerSideProps as GSSTeamTypePage } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import { getServerSideProps as GSSUserTypePage } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
orgSlug: z.string().transform((s) => slugify(s)),
|
||||
user: z.string(),
|
||||
|
||||
+46
-242
@@ -1,28 +1,13 @@
|
||||
import type { DehydratedState } from "@tanstack/react-query";
|
||||
import classNames from "classnames";
|
||||
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import type { GetServerSideProps } from "next";
|
||||
import { encode } from "querystring";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import type { z } from "zod";
|
||||
|
||||
import {
|
||||
sdkActionManager,
|
||||
useEmbedNonStylesConfig,
|
||||
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";
|
||||
@@ -30,192 +15,57 @@ 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 { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "../lib/getTemporaryOrgRedirect";
|
||||
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 function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
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("<p><br></p>", "").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,
|
||||
redirect: _redirect,
|
||||
...query
|
||||
} = useRouterQuery();
|
||||
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("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]); */
|
||||
|
||||
if (entity?.isUnpublished) {
|
||||
return (
|
||||
<div className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<UnpublishedEntity {...entity} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isEventListEmpty = eventTypes.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
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: !profile.allowSEOIndexing,
|
||||
nofollow: !profile.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 border" : "",
|
||||
"max-w-3xl px-4 py-24"
|
||||
)}>
|
||||
{isRedirect && (
|
||||
<div className="mb-8 rounded-md bg-blue-100 p-4 dark:border dark:bg-transparent dark:bg-transparent">
|
||||
<h2 className="text-default mb-2 text-sm font-semibold dark:text-white">
|
||||
{t("user_redirect_title", {
|
||||
username: fromUserNameRedirected,
|
||||
})}{" "}
|
||||
🏝️
|
||||
</h2>
|
||||
<p className="text-default text-sm">
|
||||
{t("user_redirect_description", {
|
||||
profile: {
|
||||
username: user.username,
|
||||
},
|
||||
username: fromUserNameRedirected,
|
||||
})}{" "}
|
||||
😄
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-8 text-center">
|
||||
<UserAvatar
|
||||
size="xl"
|
||||
user={{
|
||||
organizationId: profile.organization?.id,
|
||||
name: profile.name,
|
||||
username: profile.username,
|
||||
}}
|
||||
organization={
|
||||
profile.organization?.id
|
||||
? {
|
||||
id: profile.organization.id,
|
||||
slug: profile.organization.slug,
|
||||
requestedSlug: null,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
|
||||
{profile.name}
|
||||
{user.verified && (
|
||||
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-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"
|
||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
|
||||
data-testid="event-types">
|
||||
{user.away ? (
|
||||
<div className="overflow-hidden rounded-sm border ">
|
||||
<div className="text-muted p-8 text-center">
|
||||
<h2 className="font-cal text-default mb-2 text-3xl">😴{` ${t("user_away")}`}</h2>
|
||||
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||
<ArrowRight 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">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<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 />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
UserPage.isBookingPage = true;
|
||||
UserPage.PageWrapper = PageWrapper;
|
||||
|
||||
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
export const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
const eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
@@ -267,50 +117,6 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
}, []);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -484,5 +290,3 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default UserPage;
|
||||
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import classNames from "classnames";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import {
|
||||
sdkActionManager,
|
||||
useEmbedNonStylesConfig,
|
||||
useEmbedStyles,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { HeadSeo, UnpublishedEntity, UserAvatar } from "@calcom/ui";
|
||||
import { ArrowRight, Verified } from "@calcom/ui/components/icon";
|
||||
|
||||
import { type getServerSideProps } from "./users-public-view.getServerSideProps";
|
||||
|
||||
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
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("<p><br></p>", "").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,
|
||||
redirect: _redirect,
|
||||
...query
|
||||
} = useRouterQuery();
|
||||
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("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]); */
|
||||
|
||||
if (entity?.isUnpublished) {
|
||||
return (
|
||||
<div className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<UnpublishedEntity {...entity} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isEventListEmpty = eventTypes.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
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: !profile.allowSEOIndexing,
|
||||
nofollow: !profile.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 border" : "",
|
||||
"max-w-3xl px-4 py-24"
|
||||
)}>
|
||||
{isRedirect && (
|
||||
<div className="mb-8 rounded-md bg-blue-100 p-4 dark:border dark:bg-transparent dark:bg-transparent">
|
||||
<h2 className="text-default mb-2 text-sm font-semibold dark:text-white">
|
||||
{t("user_redirect_title", {
|
||||
username: fromUserNameRedirected,
|
||||
})}{" "}
|
||||
🏝️
|
||||
</h2>
|
||||
<p className="text-default text-sm">
|
||||
{t("user_redirect_description", {
|
||||
profile: {
|
||||
username: user.username,
|
||||
},
|
||||
username: fromUserNameRedirected,
|
||||
})}{" "}
|
||||
😄
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-8 text-center">
|
||||
<UserAvatar
|
||||
size="xl"
|
||||
user={{
|
||||
organizationId: profile.organization?.id,
|
||||
name: profile.name,
|
||||
username: profile.username,
|
||||
}}
|
||||
organization={
|
||||
profile.organization?.id
|
||||
? {
|
||||
id: profile.organization.id,
|
||||
slug: profile.organization.slug,
|
||||
requestedSlug: null,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
|
||||
{profile.name}
|
||||
{user.verified && (
|
||||
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-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"
|
||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
|
||||
data-testid="event-types">
|
||||
{user.away ? (
|
||||
<div className="overflow-hidden rounded-sm border ">
|
||||
<div className="text-muted p-8 text-center">
|
||||
<h2 className="font-cal text-default mb-2 text-3xl">😴{` ${t("user_away")}`}</h2>
|
||||
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||
<ArrowRight 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">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<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 />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserPage;
|
||||
+4
@@ -12,6 +12,10 @@ import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import { type EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
+1
-11
@@ -6,14 +6,7 @@ import { Booker } from "@calcom/atoms";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
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";
|
||||
|
||||
export type PageProps = Omit<inferSSRProps<typeof getServerSideProps>, "trpcState"> & EmbedProps;
|
||||
export { getServerSideProps };
|
||||
import { type PageProps } from "./users-type-public-view.getServerSideProps";
|
||||
|
||||
export const getMultipleDurationValue = (
|
||||
multipleDurationConfig: number[] | undefined,
|
||||
@@ -69,6 +62,3 @@ export default function Type({
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Type.isBookingPage = true;
|
||||
Type.PageWrapper = PageWrapper;
|
||||
@@ -1,11 +1,18 @@
|
||||
import { type AppProps } from "@lib/app-providers";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||
import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
|
||||
|
||||
export { default } from "../[type]";
|
||||
import TypePage from "~/users/views/users-type-public-view";
|
||||
import { getServerSideProps as _getServerSideProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
// Somehow these types don't accept the {notFound: true} return type.
|
||||
// Probably still need to fix this. I don't know why this isn't allowed yet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
const Type = TypePage as unknown as CalPageWrapper & {
|
||||
isBookingPage: AppProps["Component"]["isBookingPage"];
|
||||
};
|
||||
|
||||
Type.isBookingPage = true;
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
export default Type;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type AppProps } from "@lib/app-providers";
|
||||
|
||||
import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
|
||||
|
||||
import TypePage from "~/users/views/users-type-public-view";
|
||||
|
||||
export { getServerSideProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
const Type = TypePage as unknown as CalPageWrapper & {
|
||||
isBookingPage: AppProps["Component"]["isBookingPage"];
|
||||
};
|
||||
|
||||
Type.isBookingPage = true;
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
export default Type;
|
||||
@@ -1,7 +1,18 @@
|
||||
import { type AppProps } from "@lib/app-providers";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[user]";
|
||||
import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
|
||||
|
||||
export { default } from "../[user]";
|
||||
import User from "~/users/views/users-public-view";
|
||||
import { getServerSideProps as _getServerSideProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||
|
||||
const UserPage = User as unknown as CalPageWrapper & {
|
||||
isBookingPage: AppProps["Component"]["isBookingPage"];
|
||||
};
|
||||
|
||||
UserPage.isBookingPage = true;
|
||||
UserPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default UserPage;
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type AppProps } from "@lib/app-providers";
|
||||
|
||||
import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
|
||||
|
||||
import User from "~/users/views/users-public-view";
|
||||
|
||||
export { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||
|
||||
const UserPage = User as unknown as CalPageWrapper & {
|
||||
isBookingPage: AppProps["Component"]["isBookingPage"];
|
||||
};
|
||||
|
||||
UserPage.isBookingPage = true;
|
||||
UserPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default UserPage;
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||
import { getServerSideProps as _getServerSideProps } from ".";
|
||||
|
||||
export { default, type PageProps } from "../[type]";
|
||||
export { default, type PageProps } from ".";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
+5
-4
@@ -2,10 +2,11 @@
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps as UserTypePageProps } from "../../../[user]/[type]";
|
||||
import UserTypePage from "../../../[user]/[type]";
|
||||
import type { PageProps as TeamTypePageProps } from "../../../team/[slug]/[type]";
|
||||
import TeamTypePage from "../../../team/[slug]/[type]";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "../../../../team/[slug]/[type]";
|
||||
import TeamTypePage from "../../../../team/[slug]/[type]";
|
||||
|
||||
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { UserPageProps } from "../../../[user]";
|
||||
import UserPage from "../../../[user]";
|
||||
import UserPage from "~/users/views/users-public-view";
|
||||
import { type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamPageProps } from "../../../team/[slug]";
|
||||
import TeamPage from "../../../team/[slug]";
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SchedulingType } from "@calcom/prisma/client";
|
||||
import type { Schedule, TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import {
|
||||
bookFirstEvent,
|
||||
bookOptinEvent,
|
||||
@@ -21,7 +22,7 @@ test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("free user", () => {
|
||||
testBothFutureAndLegacyRoutes.describe("free user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const free = await users.create(freeUserObj);
|
||||
await page.goto(`/${free.username}`);
|
||||
@@ -62,7 +63,7 @@ test.describe("free user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("pro user", () => {
|
||||
testBothFutureAndLegacyRoutes.describe("pro user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await page.goto(`/${pro.username}`);
|
||||
@@ -262,7 +263,7 @@ test.describe("pro user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("prefill", () => {
|
||||
testBothFutureAndLegacyRoutes.describe("prefill", () => {
|
||||
test("logged in", async ({ page, users }) => {
|
||||
const prefill = await users.create({ name: "Prefill User" });
|
||||
await prefill.apiLogin();
|
||||
@@ -319,7 +320,7 @@ test.describe("prefill", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking on different layouts", () => {
|
||||
testBothFutureAndLegacyRoutes.describe("Booking on different layouts", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await page.goto(`/${user.username}`);
|
||||
@@ -376,7 +377,7 @@ test.describe("Booking on different layouts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking round robin event", () => {
|
||||
testBothFutureAndLegacyRoutes.describe("Booking round robin event", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const teamMatesObj = [{ name: "teammate-1" }];
|
||||
|
||||
@@ -388,7 +389,7 @@ test.describe("Booking round robin event", () => {
|
||||
const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []];
|
||||
|
||||
const testUser = await users.create(
|
||||
{ username: "test-user", name: "Test User", email: "testuser@example.com", schedule },
|
||||
{ schedule },
|
||||
{
|
||||
hasTeam: true,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
@@ -402,7 +403,7 @@ test.describe("Booking round robin event", () => {
|
||||
|
||||
test("Does not book round robin host outside availability with date override", async ({ page, users }) => {
|
||||
const [testUser] = users.get();
|
||||
testUser.apiLogin();
|
||||
await testUser.apiLogin();
|
||||
|
||||
const team = await testUser.getFirstTeamMembership();
|
||||
|
||||
@@ -429,7 +430,7 @@ test.describe("Booking round robin event", () => {
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const host = await page.locator('[data-testid="booking-host-name"]');
|
||||
const host = page.locator('[data-testid="booking-host-name"]');
|
||||
const hostName = await host.innerText();
|
||||
|
||||
//expect teammate-1 to be booked, test-user is not available at this time
|
||||
@@ -459,7 +460,7 @@ test.describe("Booking round robin event", () => {
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const hostSecondBooking = await page.locator('[data-testid="booking-host-name"]');
|
||||
const hostSecondBooking = page.locator('[data-testid="booking-host-name"]');
|
||||
const hostNameSecondBooking = await hostSecondBooking.innerText();
|
||||
expect(hostNameSecondBooking).toBe("teammate-1"); // teammate-1 should be booked again
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user