diff --git a/apps/web/app/_utils.tsx b/apps/web/app/_utils.tsx index 21ddec0994..ae9ef7d181 100644 --- a/apps/web/app/_utils.tsx +++ b/apps/web/app/_utils.tsx @@ -5,11 +5,10 @@ import { headers } from "next/headers"; import { constructGenericImage } from "@calcom/lib/OgImages"; import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants"; +import { truncateOnWord } from "@calcom/lib/text"; //@ts-expect-error no type definitions import config from "@calcom/web/next-i18next.config"; -import { preparePageMetadata } from "@lib/metadata"; - const create = async (locale: string, ns: string) => { const { _nextI18Next } = await serverSideTranslations(locale, [ns], config); @@ -30,7 +29,8 @@ export const getFixedT = async (locale: string, ns = "common") => { export const _generateMetadata = async ( getTitle: (t: TFunction) => string, - getDescription: (t: TFunction) => string + getDescription: (t: TFunction) => string, + excludeAppNameFromTitle?: boolean ) => { const h = headers(); const canonical = h.get("x-pathname") ?? ""; @@ -50,12 +50,24 @@ export const _generateMetadata = async ( description, }); - return preparePageMetadata({ - title, - canonical, - image, + const titleSuffix = `| ${APP_NAME}`; + const displayedTitle = + title.includes(titleSuffix) || excludeAppNameFromTitle ? title : `${title} ${titleSuffix}`; + + return { + title: title.length === 0 ? APP_NAME : displayedTitle, description, - siteName: APP_NAME, + alternates: { + canonical, + }, + openGraph: { + description: truncateOnWord(description, 158), + url: canonical, + type: "website", + siteName: APP_NAME, + title, + images: [image], + }, metadataBase, - }); + }; }; diff --git a/apps/web/app/future/getting-started/[[...step]]/page.tsx b/apps/web/app/future/getting-started/[[...step]]/page.tsx index 28c8d891e4..7a1a5298e6 100644 --- a/apps/web/app/future/getting-started/[[...step]]/page.tsx +++ b/apps/web/app/future/getting-started/[[...step]]/page.tsx @@ -1,7 +1,24 @@ -import Page from "@pages/getting-started/[[...step]]"; import { withAppDirSsr } from "app/WithAppDirSsr"; +import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { APP_NAME } from "@calcom/lib/constants"; + import { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps"; -export default WithLayout({ getLayout: null, getData: withAppDirSsr(getServerSideProps), Page }); +import type { PageProps } from "~/getting-started/[[...step]]/onboarding-view"; +import Page from "~/getting-started/[[...step]]/onboarding-view"; + +export const generateMetadata = async () => { + return await _generateMetadata( + (t) => `${APP_NAME} - ${t("getting_started")}`, + () => "", + true + ); +}; + +export default WithLayout({ + getLayout: null, + getData: withAppDirSsr(getServerSideProps), + Page, +}); diff --git a/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx b/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx index f5f6ebea59..bd948fcd5a 100644 --- a/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx +++ b/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx @@ -3,7 +3,7 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { getLocale } from "@calcom/features/auth/lib/getLocale"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import prisma from "@calcom/prisma"; +import { UserRepository } from "@calcom/lib/server/repository/user"; import { ssrInit } from "@server/lib/ssr"; @@ -20,38 +20,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => await ssr.viewer.me.prefetch(); - const user = await prisma.user.findUnique({ - where: { - id: session.user.id, - }, - select: { - completedOnboarding: true, - teams: { - select: { - accepted: true, - team: { - select: { - id: true, - name: true, - logoUrl: true, - }, - }, - }, - }, - }, + const user = await UserRepository.findUserTeams({ + id: session.user.id, }); if (!user) { throw new Error("User from session not found"); } - if (user.completedOnboarding) { - return { redirect: { permanent: false, destination: "/event-types" } }; - } const locale = await getLocale(context.req); return { props: { - ...(await serverSideTranslations(locale, ["common"])), + ...(await serverSideTranslations(locale || "en", ["common"])), trpcState: ssr.dehydrate(), hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false, }, diff --git a/apps/web/lib/metadata.ts b/apps/web/lib/metadata.ts index bf98f6560c..05c3699bf8 100644 --- a/apps/web/lib/metadata.ts +++ b/apps/web/lib/metadata.ts @@ -1,8 +1,5 @@ import type { Metadata } from "next"; -import { APP_NAME } from "@calcom/lib/constants"; -import { truncateOnWord } from "@calcom/lib/text"; - type RootMetadataRecipe = Readonly<{ twitterCreator: string; twitterSite: string; @@ -65,25 +62,3 @@ export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({ card: "summary_large_image", }, }); - -export const preparePageMetadata = (recipe: PageMetadataRecipe): Metadata => { - const { title, description } = recipe; - const titleSuffix = `| ${APP_NAME}`; - - return { - title: title.length === 0 ? APP_NAME : title.includes(titleSuffix) ? title : `${title} ${titleSuffix}`, - description, - alternates: { - canonical: recipe.canonical, - }, - openGraph: { - description: truncateOnWord(description, 158), - url: recipe.canonical, - type: "website", - siteName: recipe.siteName, - title, - images: [recipe.image], - }, - metadataBase: recipe.metadataBase, - }; -}; diff --git a/apps/web/modules/getting-started/[[...step]]/onboarding-view.tsx b/apps/web/modules/getting-started/[[...step]]/onboarding-view.tsx new file mode 100644 index 0000000000..3bb63630a2 --- /dev/null +++ b/apps/web/modules/getting-started/[[...step]]/onboarding-view.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { signOut } from "next-auth/react"; +import Head from "next/head"; +import { usePathname, useRouter } from "next/navigation"; +import { Suspense } from "react"; +import { z } from "zod"; + +import { classNames } from "@calcom/lib"; +import { APP_NAME } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; +import { trpc } from "@calcom/trpc"; +import type { inferSSRProps } from "@calcom/types/inferSSRProps"; +import { Button, StepCard, Steps } from "@calcom/ui"; +import { Icon } from "@calcom/ui"; + +import type { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps"; + +import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars"; +import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep"; +import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability"; +import UserProfile from "@components/getting-started/steps-views/UserProfile"; +import { UserSettings } from "@components/getting-started/steps-views/UserSettings"; + +const INITIAL_STEP = "user-settings"; +const steps = [ + "user-settings", + "connected-calendar", + "connected-video", + "setup-availability", + "user-profile", +] as const; + +const stepTransform = (step: (typeof steps)[number]) => { + const stepIndex = steps.indexOf(step); + if (stepIndex > -1) { + return steps[stepIndex]; + } + return INITIAL_STEP; +}; + +const stepRouteSchema = z.object({ + step: z.array(z.enum(steps)).default([INITIAL_STEP]), + from: z.string().optional(), +}); + +export type PageProps = inferSSRProps; +// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place +const OnboardingPage = (props: PageProps) => { + const pathname = usePathname(); + const params = useParamsWithFallback(); + + const router = useRouter(); + const [user] = trpc.viewer.me.useSuspenseQuery(); + const { t } = useLocale(); + + const result = stepRouteSchema.safeParse({ + ...params, + step: Array.isArray(params.step) ? params.step : [params.step], + }); + + const currentStep = result.success ? result.data.step[0] : INITIAL_STEP; + const from = result.success ? result.data.from : ""; + const headers = [ + { + title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`, + subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`], + }, + { + title: `${t("connect_your_calendar")}`, + subtitle: [`${t("connect_your_calendar_instructions")}`], + skipText: `${t("connect_calendar_later")}`, + }, + { + title: `${t("connect_your_video_app")}`, + subtitle: [`${t("connect_your_video_app_instructions")}`], + skipText: `${t("set_up_later")}`, + }, + { + title: `${t("set_availability")}`, + subtitle: [ + `${t("set_availability_getting_started_subtitle_1")}`, + `${t("set_availability_getting_started_subtitle_2")}`, + ], + }, + { + title: `${t("nearly_there")}`, + subtitle: [`${t("nearly_there_instructions")}`], + }, + ]; + + // TODO: Add this in when we have solved the ability to move to tokens accept invite and note invitedto + // Ability to accept other pending invites if any (low priority) + // if (props.hasPendingInvites) { + // headers.unshift( + // props.hasPendingInvites && { + // title: `${t("email_no_user_invite_heading", { appName: APP_NAME })}`, + // subtitle: [], // TODO: come up with some subtitle text here + // } + // ); + // } + + const goToIndex = (index: number) => { + const newStep = steps[index]; + router.push(`/getting-started/${stepTransform(newStep)}`); + }; + + const currentStepIndex = steps.indexOf(currentStep); + + return ( +
+ + {`${APP_NAME} - ${t("getting_started")}`} + + + +
+
+
+
+
+

+ {headers[currentStepIndex]?.title || "Undefined title"} +

+ + {headers[currentStepIndex]?.subtitle.map((subtitle, index) => ( +

+ {subtitle} +

+ ))} +
+ +
+ + }> + {currentStep === "user-settings" && ( + goToIndex(1)} hideUsername={from === "signup"} /> + )} + {currentStep === "connected-calendar" && goToIndex(2)} />} + + {currentStep === "connected-video" && goToIndex(3)} />} + + {currentStep === "setup-availability" && ( + goToIndex(4)} + defaultScheduleId={user.defaultScheduleId} + /> + )} + {currentStep === "user-profile" && } + + + + {headers[currentStepIndex]?.skipText && ( +
+ +
+ )} +
+
+ +
+
+
+
+ ); +}; + +export default OnboardingPage; diff --git a/apps/web/pages/getting-started/[[...step]].tsx b/apps/web/pages/getting-started/[[...step]].tsx index 46dbe1e413..37a605a374 100644 --- a/apps/web/pages/getting-started/[[...step]].tsx +++ b/apps/web/pages/getting-started/[[...step]].tsx @@ -1,193 +1,10 @@ -"use client"; - -import { signOut } from "next-auth/react"; -import Head from "next/head"; -import { usePathname, useRouter } from "next/navigation"; -import { Suspense } from "react"; -import { z } from "zod"; - -import { classNames } from "@calcom/lib"; -import { APP_NAME } from "@calcom/lib/constants"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; -import { trpc } from "@calcom/trpc"; -import { Button, StepCard, Steps } from "@calcom/ui"; -import { Icon } from "@calcom/ui"; - import PageWrapper from "@components/PageWrapper"; -import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars"; -import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep"; -import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability"; -import UserProfile from "@components/getting-started/steps-views/UserProfile"; -import { UserSettings } from "@components/getting-started/steps-views/UserSettings"; + +import type { PageProps } from "~/getting-started/[[...step]]/onboarding-view"; +import OnboardingPage from "~/getting-started/[[...step]]/onboarding-view"; + +const Page = (props: PageProps) => ; export { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps"; - -const INITIAL_STEP = "user-settings"; -const steps = [ - "user-settings", - "connected-calendar", - "connected-video", - "setup-availability", - "user-profile", -] as const; - -const stepTransform = (step: (typeof steps)[number]) => { - const stepIndex = steps.indexOf(step); - if (stepIndex > -1) { - return steps[stepIndex]; - } - return INITIAL_STEP; -}; - -const stepRouteSchema = z.object({ - step: z.array(z.enum(steps)).default([INITIAL_STEP]), - from: z.string().optional(), -}); - -// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place -const OnboardingPage = () => { - const pathname = usePathname(); - const params = useParamsWithFallback(); - - const router = useRouter(); - const [user] = trpc.viewer.me.useSuspenseQuery(); - const { t } = useLocale(); - - const result = stepRouteSchema.safeParse({ - ...params, - step: Array.isArray(params.step) ? params.step : [params.step], - }); - - const currentStep = result.success ? result.data.step[0] : INITIAL_STEP; - const from = result.success ? result.data.from : ""; - const headers = [ - { - title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`, - subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`], - }, - { - title: `${t("connect_your_calendar")}`, - subtitle: [`${t("connect_your_calendar_instructions")}`], - skipText: `${t("connect_calendar_later")}`, - }, - { - title: `${t("connect_your_video_app")}`, - subtitle: [`${t("connect_your_video_app_instructions")}`], - skipText: `${t("set_up_later")}`, - }, - { - title: `${t("set_availability")}`, - subtitle: [ - `${t("set_availability_getting_started_subtitle_1")}`, - `${t("set_availability_getting_started_subtitle_2")}`, - ], - }, - { - title: `${t("nearly_there")}`, - subtitle: [`${t("nearly_there_instructions")}`], - }, - ]; - - // TODO: Add this in when we have solved the ability to move to tokens accept invite and note invitedto - // Ability to accept other pending invites if any (low priority) - // if (props.hasPendingInvites) { - // headers.unshift( - // props.hasPendingInvites && { - // title: `${t("email_no_user_invite_heading", { appName: APP_NAME })}`, - // subtitle: [], // TODO: come up with some subtitle text here - // } - // ); - // } - - const goToIndex = (index: number) => { - const newStep = steps[index]; - router.push(`/getting-started/${stepTransform(newStep)}`); - }; - - const currentStepIndex = steps.indexOf(currentStep); - - return ( -
- - {`${APP_NAME} - ${t("getting_started")}`} - - - -
-
-
-
-
-

- {headers[currentStepIndex]?.title || "Undefined title"} -

- - {headers[currentStepIndex]?.subtitle.map((subtitle, index) => ( -

- {subtitle} -

- ))} -
- -
- - }> - {currentStep === "user-settings" && ( - goToIndex(1)} hideUsername={from === "signup"} /> - )} - {currentStep === "connected-calendar" && goToIndex(2)} />} - - {currentStep === "connected-video" && goToIndex(3)} />} - - {currentStep === "setup-availability" && ( - goToIndex(4)} - defaultScheduleId={user.defaultScheduleId} - /> - )} - {currentStep === "user-profile" && } - - - - {headers[currentStepIndex]?.skipText && ( -
- -
- )} -
-
- -
-
-
-
- ); -}; - -OnboardingPage.PageWrapper = PageWrapper; - -export default OnboardingPage; +Page.PageWrapper = PageWrapper; +export default Page; diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index 68bbb8bef6..832f32c7c2 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -585,4 +585,32 @@ export class UserRepository { }, }); } + + static async findUserTeams({ id }: { id: number }) { + const user = await prisma.user.findUnique({ + where: { + id, + }, + select: { + completedOnboarding: true, + teams: { + select: { + accepted: true, + team: { + select: { + id: true, + name: true, + logoUrl: true, + }, + }, + }, + }, + }, + }); + + if (!user) { + return null; + } + return user; + } }