diff --git a/apps/web/app/future/apps/categories/[category]/page.tsx b/apps/web/app/future/apps/categories/[category]/page.tsx index 6f3d54e434..bdf85125ff 100644 --- a/apps/web/app/future/apps/categories/[category]/page.tsx +++ b/apps/web/app/future/apps/categories/[category]/page.tsx @@ -2,6 +2,7 @@ import CategoryPage from "@pages/apps/categories/[category]"; import { Prisma } from "@prisma/client"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { type GetServerSidePropsContext } from "next"; import { notFound } from "next/navigation"; import z from "zod"; @@ -38,8 +39,8 @@ const querySchema = z.object({ category: z.nativeEnum(AppCategories), }); -const getPageProps = async ({ params }: { params: Record }) => { - const p = querySchema.safeParse(params); +const getPageProps = async (context: GetServerSidePropsContext) => { + const p = querySchema.safeParse(context.params); if (!p.success) { return notFound(); @@ -66,6 +67,5 @@ const getPageProps = async ({ params }: { params: Record; +export default WithLayout({ getData: getPageProps, Page: CategoryPage, getLayout: null })<"P">; export const dynamic = "force-static"; diff --git a/apps/web/app/future/team/[slug]/[type]/embed/page.tsx b/apps/web/app/future/team/[slug]/[type]/embed/page.tsx new file mode 100644 index 0000000000..7577b56681 --- /dev/null +++ b/apps/web/app/future/team/[slug]/[type]/embed/page.tsx @@ -0,0 +1,9 @@ +import LegacyPage from "@pages/team/[slug]/[type]/embed"; +import withEmbedSsrAppDir from "app/WithEmbedSSR"; +import { WithLayout } from "app/layoutHOC"; + +import { getData } from "../page"; + +const getEmbedData = withEmbedSsrAppDir(getData); + +export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">; diff --git a/apps/web/app/future/team/[slug]/[type]/page.tsx b/apps/web/app/future/team/[slug]/[type]/page.tsx new file mode 100644 index 0000000000..f6b971de73 --- /dev/null +++ b/apps/web/app/future/team/[slug]/[type]/page.tsx @@ -0,0 +1,35 @@ +import LegacyPage, { type PageProps, getServerSideProps } from "@pages/team/[slug]/[type]"; +import { withAppDir } from "app/AppDirSSRHOC"; +import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; +import { type GetServerSidePropsContext } from "next"; +import { cookies, headers } from "next/headers"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +export const generateMetadata = async ({ params }: { params: Record }) => { + const legacyCtx = buildLegacyCtx(headers(), cookies(), params); + const props = await getData(legacyCtx as unknown as GetServerSidePropsContext); + const { entity, user, slug, booking } = props; + const { trpc } = await import("@calcom/trpc"); + const { data: event } = trpc.viewer.public.event.useQuery( + { username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null }, + { refetchOnWindowFocus: false } + ); + + const profileName = event?.profile?.name ?? ""; + const title = event?.title ?? ""; + + return await _generateMetadata( + (t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, + (t) => `${booking?.uid ? t("reschedule") : ""} ${title}` + ); +}; +export const getData = withAppDir(getServerSideProps); + +export default WithLayout({ + Page: LegacyPage, + getData, + getLayout: null, + isBookingPage: true, +})<"P">; diff --git a/apps/web/app/future/team/[slug]/embed/page.tsx b/apps/web/app/future/team/[slug]/embed/page.tsx new file mode 100644 index 0000000000..df0d97365e --- /dev/null +++ b/apps/web/app/future/team/[slug]/embed/page.tsx @@ -0,0 +1,9 @@ +import LegacyPage from "@pages/team/[slug]/embed"; +import withEmbedSsrAppDir from "app/WithEmbedSSR"; +import { WithLayout } from "app/layoutHOC"; + +import { getData } from "../page"; + +const getEmbedData = withEmbedSsrAppDir(getData); + +export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">; diff --git a/apps/web/app/future/team/[slug]/page.tsx b/apps/web/app/future/team/[slug]/page.tsx new file mode 100644 index 0000000000..df6569a226 --- /dev/null +++ b/apps/web/app/future/team/[slug]/page.tsx @@ -0,0 +1,29 @@ +import LegacyPage, { type PageProps, getServerSideProps } from "@pages/team/[slug]"; +import { withAppDir } from "app/AppDirSSRHOC"; +import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; +import { type GetServerSidePropsContext } from "next"; +import { cookies, headers } from "next/headers"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +export const generateMetadata = async ({ params }: { params: Record }) => { + const props = await getData( + buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext + ); + const teamName = props.team.name || "Nameless Team"; + + return await _generateMetadata( + () => teamName, + () => teamName + ); +}; + +export const getData = withAppDir(getServerSideProps); + +export default WithLayout({ + Page: LegacyPage, + getData, + getLayout: null, + isBookingPage: true, +})<"P">; diff --git a/apps/web/lib/team/[slug]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/getServerSideProps.tsx new file mode 100644 index 0000000000..ccd782122c --- /dev/null +++ b/apps/web/lib/team/[slug]/getServerSideProps.tsx @@ -0,0 +1,137 @@ +import type { GetServerSidePropsContext } from "next"; + +import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; +import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; +import logger from "@calcom/lib/logger"; +import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; +import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; +import slugify from "@calcom/lib/slugify"; +import { stripMarkdown } from "@calcom/lib/stripMarkdown"; +import prisma from "@calcom/prisma"; +import { RedirectType } from "@calcom/prisma/client"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; + +import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; + +import { ssrInit } from "@server/lib/ssr"; + +const log = logger.getSubLogger({ prefix: ["team/[slug]"] }); + +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug; + const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); + const isOrgContext = isValidOrgDomain && currentOrgDomain; + + // Provided by Rewrite from next.config.js + const isOrgProfile = context.query?.isOrgProfile === "1"; + const flags = await getFeatureFlagMap(prisma); + const isOrganizationFeatureEnabled = flags["organizations"]; + + log.debug("getServerSideProps", { + isOrgProfile, + isOrganizationFeatureEnabled, + isValidOrgDomain, + currentOrgDomain, + }); + + const team = await getTeamWithMembers({ + slug: slugify(slug ?? ""), + orgSlug: currentOrgDomain, + isTeamView: true, + isOrgView: isValidOrgDomain && isOrgProfile, + }); + + if (!isOrgContext && slug) { + const redirect = await getTemporaryOrgRedirect({ + slugs: slug, + redirectType: RedirectType.Team, + eventTypeSlug: null, + currentQuery: context.query, + }); + + if (redirect) { + return redirect; + } + } + + const ssr = await ssrInit(context); + const metadata = teamMetadataSchema.parse(team?.metadata ?? {}); + + // Taking care of sub-teams and orgs + if ( + (!isValidOrgDomain && team?.parent) || + (!isValidOrgDomain && !!metadata?.isOrganization) || + !isOrganizationFeatureEnabled + ) { + return { notFound: true } as const; + } + + if (!team || (team.parent && !team.parent.slug)) { + const unpublishedTeam = await prisma.team.findFirst({ + where: { + ...(team?.parent + ? { id: team.parent.id } + : { + metadata: { + path: ["requestedSlug"], + equals: slug, + }, + }), + }, + }); + + if (!unpublishedTeam) return { notFound: true } as const; + + return { + props: { + isUnpublished: true, + team: { ...unpublishedTeam, createdAt: null }, + trpcState: ssr.dehydrate(), + }, + } as const; + } + + team.eventTypes = + team.eventTypes?.map((type) => ({ + ...type, + users: type.users.map((user) => ({ + ...user, + avatar: `/${user.username}/avatar.png`, + })), + descriptionAsSafeHTML: markdownToSafeHTML(type.description), + })) ?? null; + + const safeBio = markdownToSafeHTML(team.bio) || ""; + + const members = !team.isPrivate + ? team.members.map((member) => { + return { + name: member.name, + id: member.id, + bio: member.bio, + subteams: member.subteams, + username: member.username, + accepted: member.accepted, + organizationId: member.organizationId, + safeBio: markdownToSafeHTML(member.bio || ""), + bookerUrl: getBookerBaseUrlSync(member.organization?.slug || ""), + }; + }) + : []; + + const markdownStrippedBio = stripMarkdown(team?.bio || ""); + + const { inviteToken: _inviteToken, ...serializableTeam } = team; + + return { + props: { + team: { ...serializableTeam, safeBio, members, metadata }, + themeBasis: serializableTeam.slug, + trpcState: ssr.dehydrate(), + markdownStrippedBio, + isValidOrgDomain, + currentOrgDomain, + }, + } as const; +}; diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index 6b80836974..678303d029 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -1,3 +1,5 @@ +"use client"; + // This route is reachable by // 1. /team/[slug] // 2. / (when on org domain e.g. http://calcom.cal.com/. This is through a rewrite from next.config.js) @@ -5,45 +7,32 @@ // 1. org/[orgSlug]/team/[slug] // 2. org/[orgSlug]/[user]/[type] import classNames from "classnames"; -import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect } from "react"; import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe"; -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription"; -import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; import { WEBAPP_URL } from "@calcom/lib/constants"; -import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; 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 { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; -import slugify from "@calcom/lib/slugify"; -import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; -import prisma from "@calcom/prisma"; -import { RedirectType } from "@calcom/prisma/client"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { Avatar, Button, HeadSeo, UnpublishedEntity } from "@calcom/ui"; import { ArrowRight } from "@calcom/ui/components/icon"; import { useToggleQuery } from "@lib/hooks/useToggleQuery"; +import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import PageWrapper from "@components/PageWrapper"; import Team from "@components/team/screens/Team"; import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup"; -import { ssrInit } from "@server/lib/ssr"; - -import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect"; +export { getServerSideProps }; export type PageProps = inferSSRProps; -const log = logger.getSubLogger({ prefix: ["team/[slug]"] }); function TeamPage({ team, isUnpublished, @@ -268,124 +257,6 @@ function TeamPage({ ); } -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug; - const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); - const isOrgContext = isValidOrgDomain && currentOrgDomain; - - // Provided by Rewrite from next.config.js - const isOrgProfile = context.query?.isOrgProfile === "1"; - const flags = await getFeatureFlagMap(prisma); - const isOrganizationFeatureEnabled = flags["organizations"]; - - log.debug("getServerSideProps", { - isOrgProfile, - isOrganizationFeatureEnabled, - isValidOrgDomain, - currentOrgDomain, - }); - - const team = await getTeamWithMembers({ - slug: slugify(slug ?? ""), - orgSlug: currentOrgDomain, - isTeamView: true, - isOrgView: isValidOrgDomain && isOrgProfile, - }); - - if (!isOrgContext && slug) { - const redirect = await getTemporaryOrgRedirect({ - slugs: slug, - redirectType: RedirectType.Team, - eventTypeSlug: null, - currentQuery: context.query, - }); - - if (redirect) { - return redirect; - } - } - - const ssr = await ssrInit(context); - const metadata = teamMetadataSchema.parse(team?.metadata ?? {}); - - // Taking care of sub-teams and orgs - if ( - (!isValidOrgDomain && team?.parent) || - (!isValidOrgDomain && !!metadata?.isOrganization) || - !isOrganizationFeatureEnabled - ) { - return { notFound: true } as const; - } - - if (!team || (team.parent && !team.parent.slug)) { - const unpublishedTeam = await prisma.team.findFirst({ - where: { - ...(team?.parent - ? { id: team.parent.id } - : { - metadata: { - path: ["requestedSlug"], - equals: slug, - }, - }), - }, - }); - - if (!unpublishedTeam) return { notFound: true } as const; - - return { - props: { - isUnpublished: true, - team: { ...unpublishedTeam, createdAt: null }, - trpcState: ssr.dehydrate(), - }, - } as const; - } - - team.eventTypes = - team.eventTypes?.map((type) => ({ - ...type, - users: type.users.map((user) => ({ - ...user, - avatar: `/${user.username}/avatar.png`, - })), - descriptionAsSafeHTML: markdownToSafeHTML(type.description), - })) ?? null; - - const safeBio = markdownToSafeHTML(team.bio) || ""; - - const members = !team.isPrivate - ? team.members.map((member) => { - return { - name: member.name, - id: member.id, - bio: member.bio, - subteams: member.subteams, - username: member.username, - accepted: member.accepted, - organizationId: member.organizationId, - safeBio: markdownToSafeHTML(member.bio || ""), - bookerUrl: getBookerBaseUrlSync(member.organization?.slug || ""), - }; - }) - : []; - - const markdownStrippedBio = stripMarkdown(team?.bio || ""); - - const { inviteToken: _inviteToken, ...serializableTeam } = team; - - return { - props: { - team: { ...serializableTeam, safeBio, members, metadata }, - themeBasis: serializableTeam.slug, - trpcState: ssr.dehydrate(), - markdownStrippedBio, - isValidOrgDomain, - currentOrgDomain, - }, - } as const; -}; - TeamPage.isBookingPage = true; TeamPage.PageWrapper = PageWrapper; diff --git a/apps/web/pages/team/[slug]/[type]/embed.tsx b/apps/web/pages/team/[slug]/[type]/embed.tsx index 5cfd1726ca..9b0fb7a7a8 100644 --- a/apps/web/pages/team/[slug]/[type]/embed.tsx +++ b/apps/web/pages/team/[slug]/[type]/embed.tsx @@ -1,3 +1,5 @@ +"use client"; + import withEmbedSsr from "@lib/withEmbedSsr"; import { getServerSideProps as _getServerSideProps } from "../[type]"; diff --git a/apps/web/pages/team/[slug]/embed.tsx b/apps/web/pages/team/[slug]/embed.tsx index d220355cc9..fa255bc512 100644 --- a/apps/web/pages/team/[slug]/embed.tsx +++ b/apps/web/pages/team/[slug]/embed.tsx @@ -1,3 +1,5 @@ +"use client"; + import withEmbedSsr from "@lib/withEmbedSsr"; import { getServerSideProps as _getServerSideProps } from "../../team/[slug]";