chore: App Router - remove references to /pages for remaining pages in /auth, /insights, /d, signup and add missing default /future page (#16589)
* remove references to /pages for sso, setup, signin pages in /auth * remove references to pages for insights * remove references to pages for d * remove references to pages for signup * add page for /future index page * fix routing-forms * fix * add missing defaults * use getServerSessionForAppDir instead * fix apps/[slug]/[...pages] * fix metadata in apps/slug/pages * refactor * refactor * remove duplicate code for PageProps * remove references to pages for /reschedule * fix * fix routing forms * type fix * fix routing forms again * revert changes for app/slug/pages * revert * revert changes in yarn lock --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
1 parent
a60aa1bf6c
commit
f8dc7a115a
40 files changed
+1388
-1358
No files matched your search
@@ -1,9 +1,8 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { SearchParams } from "app/_types";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsResult } from "next";
|
||||
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound } from "next/navigation";
|
||||
import z from "zod";
|
||||
@@ -18,13 +17,7 @@ const paramsSchema = z.object({
|
||||
pages: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const p = paramsSchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
@@ -34,7 +27,7 @@ export const generateMetadata = async ({
|
||||
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const data = await getData(legacyContext);
|
||||
const form = "form" in data ? (data.form as { name?: string; description?: string }) : null;
|
||||
const formName = form?.name ?? "Forms";
|
||||
const formName = form?.name ?? "Routing Forms";
|
||||
const formDescription = form?.description ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Setup from "@pages/auth/setup";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
|
||||
|
||||
import Setup from "~/auth/setup-view";
|
||||
import type { PageProps } from "~/auth/setup-view";
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: Setup,
|
||||
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
|
||||
getData: withAppDirSsr<PageProps>(getServerSideProps),
|
||||
})<"P">;
|
||||
@@ -1,13 +1,14 @@
|
||||
import signin from "@pages/auth/signin";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
|
||||
|
||||
import SignIn from "~/auth/signin-view";
|
||||
import type { PageProps } from "~/auth/signin-view";
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: signin,
|
||||
Page: SignIn,
|
||||
// @ts-expect-error TODO: fix this
|
||||
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
|
||||
getData: withAppDirSsr<PageProps>(getServerSideProps),
|
||||
})<"P">;
|
||||
@@ -1,12 +1,13 @@
|
||||
import Provider from "@pages/auth/sso/[provider]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
|
||||
|
||||
import type { SSOProviderPageProps } from "~/auth/sso/provider-view";
|
||||
import SSOProviderView from "~/auth/sso/provider-view";
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: Provider,
|
||||
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
|
||||
Page: SSOProviderView,
|
||||
getData: withAppDirSsr<SSOProviderPageProps>(getServerSideProps),
|
||||
})<"P">;
|
||||
@@ -1,11 +1,13 @@
|
||||
import DirectSSOLogin from "@pages/auth/sso/direct";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
|
||||
|
||||
import type { SSODirectPageProps } from "~/auth/sso/direct-view";
|
||||
import SSODirectView from "~/auth/sso/direct-view";
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: DirectSSOLogin,
|
||||
getData: withAppDirSsr(getServerSideProps),
|
||||
Page: SSODirectView,
|
||||
getData: withAppDirSsr<SSODirectPageProps>(getServerSideProps),
|
||||
})<"P">;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -11,13 +11,7 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import OldPage from "~/bookings/views/bookings-single-view";
|
||||
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const { bookingInfo, eventType, recurringBookings } = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import LegacyPage from "@pages/d/[link]/[slug]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -10,14 +9,11 @@ import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
import Type from "~/d/[link]/d-type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const pageProps = await getData(legacyCtx);
|
||||
|
||||
@@ -42,5 +38,5 @@ export const generateMetadata = async ({
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
export default WithLayout({ getLayout: null, Page: LegacyPage, getData })<"P">;
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout: null, Page: Type, getData })<"P">;
|
||||
@@ -1,10 +1,11 @@
|
||||
import LegacyPage from "@pages/insights/index";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/insights/getServerSideProps";
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import type { PageProps } from "~/insights/insights-view";
|
||||
import InsightsPage from "~/insights/insights-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -12,6 +13,6 @@ export const generateMetadata = async () =>
|
||||
(t) => t("insights_subtitle")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });
|
||||
export default WithLayout({ getLayout: null, getData, Page: InsightsPage });
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -13,13 +13,7 @@ import TeamPage from "~/team/team-view";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const teamName = props.team.name || "Nameless Team";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -10,13 +10,7 @@ import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const teamName = props.team.name || "Nameless Team";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -13,13 +13,7 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
import type { PageProps } from "~/team/type-view";
|
||||
import TypePage from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { user: username, slug: eventSlug, booking } = props;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -10,13 +10,7 @@ import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const teamName = props.team.name || "Nameless Team";
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
|
||||
|
||||
const RedirectPage = async () => {
|
||||
const session = await getServerSessionForAppDir();
|
||||
if (!session?.user?.id) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
redirect("/event-types");
|
||||
};
|
||||
|
||||
export default RedirectPage;
|
||||
@@ -1,14 +1,15 @@
|
||||
import { getServerSideProps } from "@pages/reschedule/[uid]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { getServerSideProps as _getServerSideProps } from "@pages/reschedule/[uid]";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
const getData = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
await withAppDirSsr(withEmbedSsr(getServerSideProps))(legacyCtx);
|
||||
await getData(legacyCtx);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getServerSideProps } from "@pages/reschedule/[uid]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/reschedule/[uid]/getServerSideProps";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import type { PageProps } from "app/_types";
|
||||
import { redirect } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
const getPageProps = () => {
|
||||
return redirect(`/apps/routing-forms/forms`);
|
||||
};
|
||||
const Page = () => {
|
||||
getPageProps();
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
pages: z.array(z.string()),
|
||||
})
|
||||
.catch({
|
||||
pages: [],
|
||||
});
|
||||
|
||||
return null;
|
||||
const Page = ({ params, searchParams }: PageProps) => {
|
||||
const { pages } = paramsSchema.parse({ ...params, ...searchParams });
|
||||
|
||||
redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,30 +1,7 @@
|
||||
import type { PageProps } from "app/_types";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
pages: z.array(z.string()),
|
||||
})
|
||||
.catch({
|
||||
pages: [],
|
||||
});
|
||||
|
||||
const getPageProps = async (context: GetServerSidePropsContext) => {
|
||||
const { pages } = paramsSchema.parse(context.params);
|
||||
|
||||
return redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`);
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
await getPageProps(legacyCtx);
|
||||
|
||||
return null;
|
||||
const Page = () => {
|
||||
redirect("/apps/routing-forms/forms");
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,10 +1,12 @@
|
||||
import LegacyPage, { type SignupProps } from "@pages/signup";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/signup/getServerSideProps";
|
||||
|
||||
import type { SignupProps } from "~/signup-view";
|
||||
import Signup from "~/signup-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("sign_up"),
|
||||
@@ -14,7 +16,7 @@ export const generateMetadata = async () =>
|
||||
const getData = withAppDirSsr<SignupProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
Page: Signup,
|
||||
getLayout: null,
|
||||
getData,
|
||||
})<"P">;
|
||||
@@ -51,6 +51,7 @@ export function WithLayout<T extends Record<string, any>>({
|
||||
requiresLicense={requiresLicense || !!(Page && "requiresLicense" in Page && Page.requiresLicense)}
|
||||
nonce={nonce}
|
||||
themeBasis={null}
|
||||
isThemeSupported={!!(Page && "isThemeSupported" in Page && Page.isThemeSupported)}
|
||||
isBookingPage={isBookingPage || !!(Page && "isBookingPage" in Page && Page.isBookingPage)}
|
||||
{...props}>
|
||||
{pageWithServerLayout}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { AppProps } from "@lib/app-providers";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
type AppPageType = {
|
||||
getServerSideProps: AppGetServerSideProps;
|
||||
getServerSideProps?: AppGetServerSideProps;
|
||||
// A component than can accept any properties
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
default: ((props: any) => JSX.Element) &
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// page can be a server component
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { URLSearchParams } from "url";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
rescheduledBy: z.string().optional(),
|
||||
allowRescheduleForCancelledBooking: z
|
||||
.string()
|
||||
.transform((value) => value === "true")
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
|
||||
const {
|
||||
uid: bookingUid,
|
||||
seatReferenceUid,
|
||||
rescheduledBy,
|
||||
/**
|
||||
* This is for the case of request-reschedule where the booking is cancelled
|
||||
*/
|
||||
allowRescheduleForCancelledBooking,
|
||||
} = querySchema.parse(context.query);
|
||||
|
||||
const coepFlag = context.query["flag.coep"];
|
||||
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
|
||||
prisma,
|
||||
bookingUid
|
||||
);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
eventType: {
|
||||
select: {
|
||||
users: {
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
team: {
|
||||
select: {
|
||||
parentId: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
seatsPerTimeSlot: true,
|
||||
userId: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dynamicEventSlugRef: true,
|
||||
dynamicGroupSlugRef: true,
|
||||
user: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
const dynamicEventSlugRef = booking?.dynamicEventSlugRef || "";
|
||||
|
||||
if (!booking) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
// If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
|
||||
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
|
||||
if (
|
||||
!allowRescheduleForCancelledBooking &&
|
||||
(booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED)
|
||||
) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/booking/${uid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
|
||||
// TODO: Show something in UI to let user know that this booking is not rescheduleable
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
}
|
||||
|
||||
// if booking event type is for a seated event and no seat reference uid is provided, throw not found
|
||||
if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) {
|
||||
const userId = session?.user?.id;
|
||||
|
||||
if (!userId && !seatReferenceUid) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/login?callbackUrl=/reschedule/${bookingUid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
const userIsHost = booking?.eventType.hosts.find((host) => {
|
||||
if (host.user.id === userId) return true;
|
||||
});
|
||||
|
||||
const userIsOwnerOfEventType = booking?.eventType.owner?.id === userId;
|
||||
|
||||
if (!userIsHost && !userIsOwnerOfEventType) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||
|
||||
const enrichedBookingUser = booking.user
|
||||
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
|
||||
: null;
|
||||
|
||||
const eventUrl = await buildEventUrlFromBooking({
|
||||
eventType,
|
||||
dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null,
|
||||
profileEnrichedBookingUser: enrichedBookingUser,
|
||||
});
|
||||
|
||||
const destinationUrlSearchParams = new URLSearchParams();
|
||||
|
||||
destinationUrlSearchParams.set("rescheduleUid", seatReferenceUid || bookingUid);
|
||||
|
||||
// TODO: I think we should just forward all the query params here including coep flag
|
||||
if (coepFlag) {
|
||||
destinationUrlSearchParams.set("flag.coep", coepFlag as string);
|
||||
}
|
||||
|
||||
const currentUserEmail = rescheduledBy ?? session?.user?.email;
|
||||
|
||||
if (currentUserEmail) {
|
||||
destinationUrlSearchParams.set("rescheduledBy", currentUserEmail);
|
||||
}
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
destination: `${eventUrl}?${destinationUrlSearchParams.toString()}${
|
||||
eventType.seatsPerTimeSlot ? "&bookingUid=null" : ""
|
||||
}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -34,7 +34,10 @@ const AppsRouting = {
|
||||
};
|
||||
|
||||
function getRoute(appName: string, pages: string[]) {
|
||||
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record<string, AppPageType>;
|
||||
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as unknown as Record<
|
||||
string,
|
||||
AppPageType
|
||||
>;
|
||||
|
||||
if (!routingConfig) {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { WizardForm } from "@calcom/ui";
|
||||
|
||||
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
|
||||
import ChooseLicense from "@components/setup/ChooseLicense";
|
||||
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/setup/getServerSideProps";
|
||||
|
||||
function useSetStep() {
|
||||
const router = useRouter();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const setStep = (newStep = 1) => {
|
||||
const _searchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
_searchParams.set("step", newStep.toString());
|
||||
router.replace(`${pathname}?${_searchParams.toString()}`);
|
||||
};
|
||||
return setStep;
|
||||
}
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export function Setup(props: PageProps) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
|
||||
const isFreeLicense = value === "FREE";
|
||||
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
|
||||
const setStep = useSetStep();
|
||||
|
||||
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
|
||||
{
|
||||
title: t("administrator_user"),
|
||||
description: t("lets_create_first_administrator_user"),
|
||||
content: (setIsPending) => (
|
||||
<AdminUser
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(2);
|
||||
}}
|
||||
onError={() => {
|
||||
setIsPending(false);
|
||||
}}
|
||||
userCount={props.userCount}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("choose_a_license"),
|
||||
description: t("choose_license_description"),
|
||||
content: (setIsPending) => {
|
||||
return (
|
||||
<ChooseLicense
|
||||
id="wizard-step-2"
|
||||
name="wizard-step-2"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!isFreeLicense) {
|
||||
steps.push({
|
||||
title: t("step_enterprise_license"),
|
||||
description: t("step_enterprise_license_description"),
|
||||
content: (setIsPending) => {
|
||||
const currentStep = 3;
|
||||
return (
|
||||
<EnterpriseLicense
|
||||
id={`wizard-step-${currentStep}`}
|
||||
name={`wizard-step-${currentStep}`}
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(currentStep + 1);
|
||||
}}
|
||||
onSuccessValidate={() => {
|
||||
setIsEnabledEE(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
isEnabled: isEnabledEE,
|
||||
});
|
||||
}
|
||||
|
||||
steps.push({
|
||||
title: t("enable_apps"),
|
||||
description: t("enable_apps_description", { appName: APP_NAME }),
|
||||
contentClassname: "!pb-0 mb-[-1px]",
|
||||
content: (setIsPending) => {
|
||||
const currentStep = isFreeLicense ? 3 : 4;
|
||||
return (
|
||||
<AdminAppsList
|
||||
id={`wizard-step-${currentStep}`}
|
||||
name={`wizard-step-${currentStep}`}
|
||||
classNames={{
|
||||
form: "mb-4 rounded-md bg-default px-0 pt-0 md:max-w-full",
|
||||
appCategoryNavigationContainer: "max-h-[400px] overflow-y-auto md:p-4",
|
||||
verticalTabsItem: "!w-48 md:p-4",
|
||||
}}
|
||||
baseURL={`/auth/setup?step=${currentStep}`}
|
||||
useQueryParam={true}
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
router.replace("/");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="bg-subtle flex items-center print:h-full md:h-screen">
|
||||
<WizardForm
|
||||
href="/auth/setup"
|
||||
steps={steps}
|
||||
nextLabel={t("next_step_text")}
|
||||
finishLabel={t("finish")}
|
||||
prevLabel={t("prev_step")}
|
||||
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Setup.isThemeSupported = false;
|
||||
|
||||
export default Setup;
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
function Signin({ providers }: PageProps) {
|
||||
if (!providers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="center mt-10 justify-between space-y-5 text-center align-baseline">
|
||||
{Object.values(providers).map((provider) => {
|
||||
return (
|
||||
<div key={provider.name}>
|
||||
<Button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Signin;
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
|
||||
|
||||
// This page is used to initiate the SAML authentication flow by redirecting to the SAML provider.
|
||||
// Accessible only on self-hosted Cal.com instances.
|
||||
|
||||
export type SSODirectPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export default function Page({ samlTenantID, samlProductID }: SSODirectPageProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (HOSTED_CAL_FEATURES) {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Initiate SAML authentication flow
|
||||
signIn(
|
||||
"saml",
|
||||
{
|
||||
callbackUrl: "/",
|
||||
},
|
||||
{ tenant: samlTenantID, product: samlProductID }
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
|
||||
|
||||
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function Provider(props: SSOProviderPageProps) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const email = searchParams?.get("email");
|
||||
if (props.provider === "saml") {
|
||||
if (!email) {
|
||||
router.push(`/auth/error?error=Email not provided`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.isSAMLLoginEnabled) {
|
||||
router.push(`/auth/error?error=SAML login not enabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
signIn("saml", {}, { tenant: props.tenant, product: props.product });
|
||||
} else if (props.provider === "google" && email) {
|
||||
signIn("google", {}, { login_hint: email });
|
||||
} else {
|
||||
signIn(props.provider);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
|
||||
export default function Type({
|
||||
slug,
|
||||
isEmbed,
|
||||
user,
|
||||
booking,
|
||||
isBrandingHidden,
|
||||
isTeamEvent,
|
||||
entity,
|
||||
duration,
|
||||
hashedLink,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={entity}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent={isTeamEvent}
|
||||
entity={entity}
|
||||
duration={duration}
|
||||
hashedLink={hashedLink}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Type.isBookingPage = true;
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AverageEventDurationChart,
|
||||
BookingKPICards,
|
||||
BookingStatusLineChart,
|
||||
LeastBookedTeamMembersTable,
|
||||
MostBookedTeamMembersTable,
|
||||
PopularEventsTable,
|
||||
HighestNoShowHostTable,
|
||||
RecentFeedbackTable,
|
||||
HighestRatedMembersTable,
|
||||
LowestRatedMembersTable,
|
||||
} from "@calcom/features/insights/components";
|
||||
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||
import { Filters } from "@calcom/features/insights/filters";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import type { getServerSideProps } from "@lib/insights/getServerSideProps";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function InsightsPage() {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="users" className="h-5 w-5" />,
|
||||
title: t("view_bookings_across"),
|
||||
description: t("view_bookings_across_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="refresh-ccw" className="h-5 w-5" />,
|
||||
title: t("identify_booking_trends"),
|
||||
description: t("identify_booking_trends_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="user-plus" className="h-5 w-5" />,
|
||||
title: t("spot_popular_event_types"),
|
||||
description: t("spot_popular_event_types_description"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
heading="Insights"
|
||||
subtitle={t("insights_subtitle")}
|
||||
title="Insights"
|
||||
description="View booking insights across your events.">
|
||||
<UpgradeTip
|
||||
plan="team"
|
||||
title={t("make_informed_decisions")}
|
||||
description={t("make_informed_decisions_description")}
|
||||
features={features}
|
||||
background="/tips/insights"
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("create_team")}
|
||||
</Button>
|
||||
<Button color="minimal" href="https://go.cal.com/insights" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
}>
|
||||
{!user ? (
|
||||
<></>
|
||||
) : (
|
||||
<FiltersProvider>
|
||||
<Filters />
|
||||
|
||||
<div className="mb-4 space-y-4">
|
||||
<BookingKPICards />
|
||||
|
||||
<BookingStatusLineChart />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<PopularEventsTable />
|
||||
|
||||
<AverageEventDurationChart />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<MostBookedTeamMembersTable />
|
||||
<LeastBookedTeamMembersTable />
|
||||
</div>
|
||||
<RecentFeedbackTable />
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<HighestNoShowHostTable />
|
||||
<HighestRatedMembersTable />
|
||||
<LowestRatedMembersTable />
|
||||
</div>
|
||||
<small className="text-default block text-center">
|
||||
{t("looking_for_more_insights")}{" "}
|
||||
<a
|
||||
className="text-blue-500 hover:underline"
|
||||
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
|
||||
{" "}
|
||||
{t("contact_support")}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</FiltersProvider>
|
||||
)}
|
||||
</UpgradeTip>
|
||||
</Shell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
"use client";
|
||||
|
||||
import { Analytics as DubAnalytics } from "@dub/analytics/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { Trans } from "next-i18next";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Script from "next/script";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { SubmitHandler } from "react-hook-form";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import {
|
||||
APP_NAME,
|
||||
URL_PROTOCOL_REGEX,
|
||||
IS_CALCOM,
|
||||
IS_EUROPE,
|
||||
WEBAPP_URL,
|
||||
CLOUDFLARE_SITE_ID,
|
||||
WEBSITE_PRIVACY_POLICY_URL,
|
||||
WEBSITE_TERMS_URL,
|
||||
WEBSITE_URL,
|
||||
} from "@calcom/lib/constants";
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import { pushGTMEvent } from "@calcom/lib/gtm";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import {
|
||||
Button,
|
||||
HeadSeo,
|
||||
PasswordField,
|
||||
TextField,
|
||||
Form,
|
||||
Alert,
|
||||
showToast,
|
||||
CheckboxField,
|
||||
Icon,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { getServerSideProps } from "@lib/signup/getServerSideProps";
|
||||
|
||||
const signupSchema = apiSignupSchema.extend({
|
||||
apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
|
||||
cfToken: z.string().optional(),
|
||||
});
|
||||
|
||||
const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false });
|
||||
|
||||
type FormValues = z.infer<typeof signupSchema>;
|
||||
|
||||
export type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
title: "connect_all_calendars",
|
||||
description: "connect_all_calendars_description",
|
||||
i18nOptions: {
|
||||
appName: APP_NAME,
|
||||
},
|
||||
icon: "calendar-heart" as const,
|
||||
},
|
||||
{
|
||||
title: "set_availability",
|
||||
description: "set_availbility_description",
|
||||
icon: "users" as const,
|
||||
},
|
||||
{
|
||||
title: "share_a_link_or_embed",
|
||||
description: "share_a_link_or_embed_description",
|
||||
icon: "link-2" as const,
|
||||
i18nOptions: {
|
||||
appName: APP_NAME,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function UsernameField({
|
||||
username,
|
||||
setPremium,
|
||||
premium,
|
||||
setUsernameTaken,
|
||||
orgSlug,
|
||||
usernameTaken,
|
||||
disabled,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TextField> & {
|
||||
username: string;
|
||||
setPremium: (value: boolean) => void;
|
||||
premium: boolean;
|
||||
usernameTaken: boolean;
|
||||
orgSlug?: string;
|
||||
setUsernameTaken: (value: boolean) => void;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const { register, formState } = useFormContext<FormValues>();
|
||||
const debouncedUsername = useDebounce(username, 600);
|
||||
|
||||
useEffect(() => {
|
||||
if (formState.isSubmitting || formState.isSubmitSuccessful) return;
|
||||
|
||||
async function checkUsername() {
|
||||
// If the username can't be changed, there is no point in doing the username availability check
|
||||
if (disabled) return;
|
||||
if (!debouncedUsername) {
|
||||
setPremium(false);
|
||||
setUsernameTaken(false);
|
||||
return;
|
||||
}
|
||||
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
|
||||
setPremium(data.premium);
|
||||
setUsernameTaken(!data.available);
|
||||
});
|
||||
}
|
||||
checkUsername();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedUsername, disabled, orgSlug, formState.isSubmitting, formState.isSubmitSuccessful]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
{...register("username")}
|
||||
data-testid="signup-usernamefield"
|
||||
addOnFilled={false}
|
||||
/>
|
||||
{(!formState.isSubmitting || !formState.isSubmitted) && (
|
||||
<div className="text-gray text-default flex items-center text-sm">
|
||||
<div className="text-sm ">
|
||||
{usernameTaken ? (
|
||||
<div className="text-error flex items-center">
|
||||
<Icon name="info" className="mr-1 inline-block h-4 w-4" />
|
||||
<p>{t("already_in_use_error")}</p>
|
||||
</div>
|
||||
) : premium ? (
|
||||
<div data-testid="premium-username-warning" className="flex items-center">
|
||||
<Icon name="star" className="mr-1 inline-block h-4 w-4" />
|
||||
<p>
|
||||
{t("premium_username", {
|
||||
price: getPremiumPlanPriceValue(),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function addOrUpdateQueryParam(url: string, key: string, value: string) {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
const param = `${key}=${encodeURIComponent(value)}`;
|
||||
return `${url}${separator}${param}`;
|
||||
}
|
||||
|
||||
export default function Signup({
|
||||
prepopulateFormValues,
|
||||
token,
|
||||
orgSlug,
|
||||
isGoogleLoginEnabled,
|
||||
isSAMLLoginEnabled,
|
||||
orgAutoAcceptEmail,
|
||||
redirectUrl,
|
||||
emailVerificationEnabled,
|
||||
}: SignupProps) {
|
||||
const [premiumUsername, setPremiumUsername] = useState(false);
|
||||
const [usernameTaken, setUsernameTaken] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const searchParams = useCompatSearchParams();
|
||||
const telemetry = useTelemetry();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const formMethods = useForm<FormValues>({
|
||||
resolver: zodResolver(signupSchema),
|
||||
defaultValues: prepopulateFormValues satisfies FormValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
formState: { isSubmitting, errors, isSubmitSuccessful },
|
||||
} = formMethods;
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectUrl) {
|
||||
localStorage.setItem("onBoardingRedirect", redirectUrl);
|
||||
}
|
||||
}, [redirectUrl]);
|
||||
|
||||
const [COOKIE_CONSENT, setCOOKIE_CONSENT] = useState(false);
|
||||
|
||||
function handleConsentChange(consent: boolean) {
|
||||
setCOOKIE_CONSENT(!consent);
|
||||
}
|
||||
|
||||
const loadingSubmitState = isSubmitSuccessful || isSubmitting;
|
||||
|
||||
const handleErrorsAndStripe = async (resp: Response) => {
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
if (err.checkoutSessionId) {
|
||||
const stripe = await getStripe();
|
||||
if (stripe) {
|
||||
console.log("Redirecting to stripe checkout");
|
||||
const { error } = await stripe.redirectToCheckout({
|
||||
sessionId: err.checkoutSessionId,
|
||||
});
|
||||
console.warn(error.message);
|
||||
}
|
||||
} else {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
|
||||
const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new");
|
||||
|
||||
const signUp: SubmitHandler<FormValues> = async (_data) => {
|
||||
const { cfToken, ...data } = _data;
|
||||
await fetch("/api/auth/signup", {
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
language: i18n.language,
|
||||
token,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"cf-access-token": cfToken ?? "invalid-token",
|
||||
},
|
||||
method: "POST",
|
||||
})
|
||||
.then(handleErrorsAndStripe)
|
||||
.then(async () => {
|
||||
if (process.env.NEXT_PUBLIC_GTM_ID)
|
||||
pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language });
|
||||
|
||||
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
|
||||
|
||||
const verifyOrGettingStarted = emailVerificationEnabled ? "auth/verify-email" : "getting-started";
|
||||
const gettingStartedWithPlatform = "settings/platform/new";
|
||||
|
||||
const constructCallBackIfUrlPresent = () => {
|
||||
if (isOrgInviteByLink) {
|
||||
return `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`;
|
||||
}
|
||||
|
||||
return addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup");
|
||||
};
|
||||
|
||||
const constructCallBackIfUrlNotPresent = () => {
|
||||
if (!!isPlatformUser) {
|
||||
return `${WEBAPP_URL}/${gettingStartedWithPlatform}?from=signup`;
|
||||
}
|
||||
|
||||
return `${WEBAPP_URL}/${verifyOrGettingStarted}?from=signup`;
|
||||
};
|
||||
|
||||
const constructCallBackUrl = () => {
|
||||
const callbackUrlSearchParams = searchParams?.get("callbackUrl");
|
||||
|
||||
return !!callbackUrlSearchParams
|
||||
? constructCallBackIfUrlPresent()
|
||||
: constructCallBackIfUrlNotPresent();
|
||||
};
|
||||
|
||||
const callBackUrl = constructCallBackUrl();
|
||||
|
||||
await signIn<"credentials">("credentials", {
|
||||
...data,
|
||||
callbackUrl: callBackUrl,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
formMethods.setError("apiError", { message: err.message });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{IS_CALCOM && (!IS_EUROPE || COOKIE_CONSENT) ? (
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_GTM_ID && (
|
||||
<>
|
||||
<Script
|
||||
id="gtm-init-script"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||
var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||
j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', '${process.env.NEXT_PUBLIC_GTM_ID}');`,
|
||||
}}
|
||||
/>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<DubAnalytics
|
||||
cookieOptions={{
|
||||
domain: isENVDev ? undefined : `.${new URL(WEBSITE_URL).hostname}`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
"light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||
"[--cal-brand-subtle:#9CA3AF]",
|
||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
|
||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1] "
|
||||
)}>
|
||||
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||
{/* Left side */}
|
||||
<div className="ml-auto mr-auto mt-0 flex w-full max-w-xl flex-col px-4 pt-6 sm:px-16 md:px-20 lg:mt-12 2xl:px-28">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="font-cal text-[28px] leading-none ">
|
||||
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
|
||||
</h1>
|
||||
{IS_CALCOM ? (
|
||||
<p className="text-subtle text-base font-medium leading-5">{t("cal_signup_description")}</p>
|
||||
) : (
|
||||
<p className="text-subtle text-base font-medium leading-5">
|
||||
{t("calcom_explained", {
|
||||
appName: APP_NAME,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Form Container */}
|
||||
<div className="mt-12">
|
||||
<Form
|
||||
className="flex flex-col gap-4"
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
let updatedValues = values;
|
||||
if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
updatedValues = {
|
||||
...values,
|
||||
username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail),
|
||||
};
|
||||
}
|
||||
await signUp(updatedValues);
|
||||
}}>
|
||||
{/* Username */}
|
||||
{!isOrgInviteByLink ? (
|
||||
<UsernameField
|
||||
orgSlug={orgSlug}
|
||||
label={t("username")}
|
||||
username={watch("username") || ""}
|
||||
premium={premiumUsername}
|
||||
usernameTaken={usernameTaken}
|
||||
disabled={!!orgSlug}
|
||||
setUsernameTaken={(value) => setUsernameTaken(value)}
|
||||
data-testid="signup-usernamefield"
|
||||
setPremium={(value) => setPremiumUsername(value)}
|
||||
addOnLeading={
|
||||
orgSlug
|
||||
? `${getOrgFullOrigin(orgSlug, { protocol: true }).replace(URL_PROTOCOL_REGEX, "")}/`
|
||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/`
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{/* Email */}
|
||||
<TextField
|
||||
{...register("email")}
|
||||
label={t("email")}
|
||||
type="email"
|
||||
disabled={prepopulateFormValues?.email}
|
||||
data-testid="signup-emailfield"
|
||||
/>
|
||||
|
||||
{/* Password */}
|
||||
<PasswordField
|
||||
data-testid="signup-passwordfield"
|
||||
label={t("password")}
|
||||
{...register("password")}
|
||||
hintErrors={["caplow", "min", "num"]}
|
||||
/>
|
||||
{/* Cloudflare Turnstile Captcha */}
|
||||
{CLOUDFLARE_SITE_ID ? (
|
||||
<TurnstileCaptcha
|
||||
appearance="interaction-only"
|
||||
onVerify={(token) => {
|
||||
formMethods.setValue("cfToken", token);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<CheckboxField
|
||||
onChange={() => handleConsentChange(COOKIE_CONSENT)}
|
||||
description={t("cookie_consent_checkbox")}
|
||||
/>
|
||||
{errors.apiError && (
|
||||
<Alert
|
||||
className="mb-3"
|
||||
severity="error"
|
||||
message={errors.apiError?.message}
|
||||
data-testid="signup-error-message"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className="my-2 w-full justify-center"
|
||||
loading={loadingSubmitState}
|
||||
disabled={
|
||||
!!formMethods.formState.errors.username ||
|
||||
!!formMethods.formState.errors.email ||
|
||||
!formMethods.getValues("email") ||
|
||||
!formMethods.getValues("password") ||
|
||||
(CLOUDFLARE_SITE_ID &&
|
||||
!process.env.NEXT_PUBLIC_IS_E2E &&
|
||||
!formMethods.getValues("cfToken")) ||
|
||||
isSubmitting ||
|
||||
usernameTaken
|
||||
}>
|
||||
{premiumUsername && !usernameTaken
|
||||
? `Create Account for ${getPremiumPlanPriceValue()}`
|
||||
: t("create_account")}
|
||||
</Button>
|
||||
</Form>
|
||||
{!isGoogleLoginEnabled && !isSAMLLoginEnabled ? null : (
|
||||
<div className="mt-6">
|
||||
<div className="relative flex items-center">
|
||||
<div className="border-subtle flex-grow border-t" />
|
||||
<span className="text-subtle mx-2 flex-shrink text-sm font-normal leading-none">
|
||||
{t("or_continue_with")}
|
||||
</span>
|
||||
<div className="border-subtle flex-grow border-t" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 flex flex-col gap-2 md:flex-row">
|
||||
{isGoogleLoginEnabled ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={!!formMethods.formState.errors.username || premiumUsername}
|
||||
loading={isGoogleLoading}
|
||||
CustomStartIcon={
|
||||
<img
|
||||
className={classNames(
|
||||
"text-subtle mr-2 h-4 w-4 dark:invert",
|
||||
premiumUsername && "opacity-50"
|
||||
)}
|
||||
src="/google-icon.svg"
|
||||
alt=""
|
||||
/>
|
||||
}
|
||||
className={classNames(
|
||||
"w-full justify-center rounded-md text-center",
|
||||
formMethods.formState.errors.username ? "opacity-50" : ""
|
||||
)}
|
||||
onClick={async () => {
|
||||
setIsGoogleLoading(true);
|
||||
const username = formMethods.getValues("username");
|
||||
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
|
||||
const searchQueryParams = new URLSearchParams();
|
||||
if (username) {
|
||||
// If username is present we save it in query params to check for premium
|
||||
searchQueryParams.set("username", username);
|
||||
localStorage.setItem("username", username);
|
||||
}
|
||||
if (token) {
|
||||
searchQueryParams.set("email", prepopulateFormValues?.email);
|
||||
}
|
||||
const url = searchQueryParams.toString()
|
||||
? `${GOOGLE_AUTH_URL}?${searchQueryParams.toString()}`
|
||||
: GOOGLE_AUTH_URL;
|
||||
|
||||
router.push(url);
|
||||
}}>
|
||||
Google
|
||||
</Button>
|
||||
) : null}
|
||||
{isSAMLLoginEnabled ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={
|
||||
!!formMethods.formState.errors.username ||
|
||||
!!formMethods.formState.errors.email ||
|
||||
premiumUsername ||
|
||||
isSubmitting ||
|
||||
isGoogleLoading
|
||||
}
|
||||
className={classNames(
|
||||
"w-full justify-center rounded-md text-center",
|
||||
formMethods.formState.errors.username && formMethods.formState.errors.email
|
||||
? "opacity-50"
|
||||
: ""
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!formMethods.getValues("username")) {
|
||||
formMethods.trigger("username");
|
||||
}
|
||||
if (!formMethods.getValues("email")) {
|
||||
formMethods.trigger("email");
|
||||
|
||||
return;
|
||||
}
|
||||
const username = formMethods.getValues("username");
|
||||
if (!username) {
|
||||
showToast("error", t("username_required"));
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("username", username);
|
||||
const sp = new URLSearchParams();
|
||||
// @NOTE: don't remove username query param as it's required right now for stripe payment page
|
||||
sp.set("username", username);
|
||||
sp.set("email", formMethods.getValues("email"));
|
||||
router.push(
|
||||
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/sso/saml` + `?${sp.toString()}`
|
||||
);
|
||||
}}>
|
||||
<Icon name="shield-check" className="mr-2 h-5 w-5" />
|
||||
{t("saml_sso")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* Already have an account & T&C */}
|
||||
<div className="mt-10 flex h-full flex-col justify-end pb-6 text-xs">
|
||||
<div className="flex flex-col text-sm">
|
||||
<div className="flex gap-1">
|
||||
<p className="text-subtle">{t("already_have_account")}</p>
|
||||
<Link href="/auth/login" className="text-emphasis hover:underline">
|
||||
{t("sign_in")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-subtle ">
|
||||
<Trans
|
||||
i18nKey="signing_up_terms"
|
||||
components={[
|
||||
<Link
|
||||
className="text-emphasis hover:underline"
|
||||
key="terms"
|
||||
href={`${WEBSITE_TERMS_URL}`}
|
||||
target="_blank">
|
||||
Terms
|
||||
</Link>,
|
||||
<Link
|
||||
className="text-emphasis hover:underline"
|
||||
key="privacy"
|
||||
href={`${WEBSITE_PRIVACY_POLICY_URL}`}
|
||||
target="_blank">
|
||||
Privacy Policy.
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 dark:bg-none lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12">
|
||||
{IS_CALCOM && (
|
||||
<>
|
||||
<div className="-mt-4 mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/product-of-the-day.svg"
|
||||
className="h-[34px] w-full dark:invert"
|
||||
alt="Cal.com was Product of the Day at ProductHunt"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/product-of-the-week.svg"
|
||||
className="h-[34px] w-full dark:invert"
|
||||
alt="Cal.com was Product of the Week at ProductHunt"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/product-of-the-month.svg"
|
||||
className="h-[34px] w-full dark:invert"
|
||||
alt="Cal.com was Product of the Month at ProductHunt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/producthunt.svg"
|
||||
className="h-[54px] w-full"
|
||||
alt="ProductHunt Rating of 5 Stars"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/google-reviews.svg"
|
||||
className="h-[54px] w-full"
|
||||
alt="Google Reviews Rating of 4.7 Stars"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/g2.svg"
|
||||
className="h-[54px] w-full"
|
||||
alt="G2 Rating of 4.7 Stars"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] dark:bg-white/5 lg:block lg:py-[6px] lg:pl-[6px]">
|
||||
<img className="block dark:hidden" src="/mock-event-type-list.svg" alt="Cal.com Booking Page" />
|
||||
<img
|
||||
className="hidden dark:block"
|
||||
src="/mock-event-type-list-dark.svg"
|
||||
alt="Cal.com Booking Page"
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-12 mt-8 hidden h-full w-full grid-cols-3 gap-4 overflow-hidden lg:grid">
|
||||
{FEATURES.map((feature) => (
|
||||
<>
|
||||
<div className="max-w-52 mb-8 flex flex-col leading-none sm:mb-0">
|
||||
<div className="text-emphasis items-center">
|
||||
<Icon name={feature.icon} className="mb-1 h-4 w-4" />
|
||||
<span className="text-sm font-medium">{t(feature.title)}</span>
|
||||
</div>
|
||||
<div className="text-subtle text-sm">
|
||||
<p>
|
||||
{t(
|
||||
feature.description,
|
||||
feature.i18nOptions && {
|
||||
...feature.i18nOptions,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,152 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Meta, WizardForm } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
|
||||
import ChooseLicense from "@components/setup/ChooseLicense";
|
||||
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
|
||||
|
||||
function useSetStep() {
|
||||
const router = useRouter();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const setStep = (newStep = 1) => {
|
||||
const _searchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
_searchParams.set("step", newStep.toString());
|
||||
router.replace(`${pathname}?${_searchParams.toString()}`);
|
||||
};
|
||||
return setStep;
|
||||
}
|
||||
import type { PageProps } from "~/auth/setup-view";
|
||||
import Setup from "~/auth/setup-view";
|
||||
|
||||
export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const Page = (props: PageProps) => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
|
||||
const isFreeLicense = value === "FREE";
|
||||
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
|
||||
const setStep = useSetStep();
|
||||
|
||||
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
|
||||
{
|
||||
title: t("administrator_user"),
|
||||
description: t("lets_create_first_administrator_user"),
|
||||
content: (setIsPending) => (
|
||||
<AdminUser
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(2);
|
||||
}}
|
||||
onError={() => {
|
||||
setIsPending(false);
|
||||
}}
|
||||
userCount={props.userCount}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("choose_a_license"),
|
||||
description: t("choose_license_description"),
|
||||
content: (setIsPending) => {
|
||||
return (
|
||||
<ChooseLicense
|
||||
id="wizard-step-2"
|
||||
name="wizard-step-2"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!isFreeLicense) {
|
||||
steps.push({
|
||||
title: t("step_enterprise_license"),
|
||||
description: t("step_enterprise_license_description"),
|
||||
content: (setIsPending) => {
|
||||
const currentStep = 3;
|
||||
return (
|
||||
<EnterpriseLicense
|
||||
id={`wizard-step-${currentStep}`}
|
||||
name={`wizard-step-${currentStep}`}
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(currentStep + 1);
|
||||
}}
|
||||
onSuccessValidate={() => {
|
||||
setIsEnabledEE(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
isEnabled: isEnabledEE,
|
||||
});
|
||||
}
|
||||
|
||||
steps.push({
|
||||
title: t("enable_apps"),
|
||||
description: t("enable_apps_description", { appName: APP_NAME }),
|
||||
contentClassname: "!pb-0 mb-[-1px]",
|
||||
content: (setIsPending) => {
|
||||
const currentStep = isFreeLicense ? 3 : 4;
|
||||
return (
|
||||
<AdminAppsList
|
||||
id={`wizard-step-${currentStep}`}
|
||||
name={`wizard-step-${currentStep}`}
|
||||
classNames={{
|
||||
form: "mb-4 rounded-md bg-default px-0 pt-0 md:max-w-full",
|
||||
appCategoryNavigationContainer: "max-h-[400px] overflow-y-auto md:p-4",
|
||||
verticalTabsItem: "!w-48 md:p-4",
|
||||
}}
|
||||
baseURL={`/auth/setup?step=${currentStep}`}
|
||||
useQueryParam={true}
|
||||
onSubmit={() => {
|
||||
setIsPending(true);
|
||||
router.replace("/");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("setup")} description={t("setup_description")} />
|
||||
<main className="bg-subtle flex items-center print:h-full md:h-screen">
|
||||
<WizardForm
|
||||
href="/auth/setup"
|
||||
steps={steps}
|
||||
nextLabel={t("next_step_text")}
|
||||
finishLabel={t("finish")}
|
||||
prevLabel={t("prev_step")}
|
||||
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
|
||||
/>
|
||||
</main>
|
||||
<Setup {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Setup.isThemeSupported = false;
|
||||
Setup.PageWrapper = PageWrapper;
|
||||
export default Setup;
|
||||
};
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
@@ -1,34 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import type { getProviders } from "next-auth/react";
|
||||
import { signIn } from "next-auth/react";
|
||||
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
|
||||
|
||||
function signin({ providers }: { providers: Awaited<ReturnType<typeof getProviders>> }) {
|
||||
if (!providers) {
|
||||
return null;
|
||||
}
|
||||
import type { PageProps } from "~/auth/signin-view";
|
||||
import SignIn from "~/auth/signin-view";
|
||||
|
||||
return (
|
||||
<div className="center mt-10 justify-between space-y-5 text-center align-baseline">
|
||||
{Object.values(providers).map((provider) => {
|
||||
return (
|
||||
<div key={provider.name}>
|
||||
<Button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const Page = (props: PageProps) => <SignIn {...props} />;
|
||||
|
||||
signin.PageWrapper = PageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default signin;
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
@@ -1,47 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
|
||||
|
||||
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
import type { SSOProviderPageProps } from "~/auth/sso/provider-view";
|
||||
import SSOProviderView from "~/auth/sso/provider-view";
|
||||
|
||||
export default function Provider(props: SSOProviderPageProps) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const email = searchParams?.get("email");
|
||||
if (props.provider === "saml") {
|
||||
if (!email) {
|
||||
router.push(`/auth/error?error=Email not provided`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.isSAMLLoginEnabled) {
|
||||
router.push(`/auth/error?error=SAML login not enabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
signIn("saml", {}, { tenant: props.tenant, product: props.product });
|
||||
} else if (props.provider === "google" && email) {
|
||||
signIn("google", {}, { login_hint: email });
|
||||
} else {
|
||||
signIn(props.provider);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
Provider.PageWrapper = PageWrapper;
|
||||
const Page = (props: SSOProviderPageProps) => <SSOProviderView {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
export { getServerSideProps };
|
||||
@@ -1,44 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
|
||||
import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
|
||||
|
||||
// This page is used to initiate the SAML authentication flow by redirecting to the SAML provider.
|
||||
// Accessible only on self-hosted Cal.com instances.
|
||||
export default function Page({ samlTenantID, samlProductID }: inferSSRProps<typeof getServerSideProps>) {
|
||||
const router = useRouter();
|
||||
import type { SSODirectPageProps } from "~/auth/sso/direct-view";
|
||||
import SSODirectView from "~/auth/sso/direct-view";
|
||||
|
||||
useEffect(() => {
|
||||
if (HOSTED_CAL_FEATURES) {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Initiate SAML authentication flow
|
||||
signIn(
|
||||
"saml",
|
||||
{
|
||||
callbackUrl: "/",
|
||||
},
|
||||
{ tenant: samlTenantID, product: samlProductID }
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export { getServerSideProps };
|
||||
const Page = (props: SSODirectPageProps) => <SSODirectView {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
export { getServerSideProps };
|
||||
@@ -1,48 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import { getServerSideProps, type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function Type({
|
||||
slug,
|
||||
isEmbed,
|
||||
user,
|
||||
booking,
|
||||
isBrandingHidden,
|
||||
isTeamEvent,
|
||||
entity,
|
||||
duration,
|
||||
hashedLink,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={entity}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent={isTeamEvent}
|
||||
entity={entity}
|
||||
duration={duration}
|
||||
hashedLink={hashedLink}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
import Type from "~/d/[link]/d-type-view";
|
||||
|
||||
const Page = (props: PageProps) => <Type {...props} />;
|
||||
export { getServerSideProps };
|
||||
|
||||
Type.PageWrapper = PageWrapper;
|
||||
Type.isBookingPage = true;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
@@ -1,123 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AverageEventDurationChart,
|
||||
BookingKPICards,
|
||||
BookingStatusLineChart,
|
||||
LeastBookedTeamMembersTable,
|
||||
MostBookedTeamMembersTable,
|
||||
PopularEventsTable,
|
||||
HighestNoShowHostTable,
|
||||
RecentFeedbackTable,
|
||||
HighestRatedMembersTable,
|
||||
LowestRatedMembersTable,
|
||||
} from "@calcom/features/insights/components";
|
||||
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||
import { Filters } from "@calcom/features/insights/filters";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import { getServerSideProps } from "@lib/insights/getServerSideProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function InsightsPage() {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="users" className="h-5 w-5" />,
|
||||
title: t("view_bookings_across"),
|
||||
description: t("view_bookings_across_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="refresh-ccw" className="h-5 w-5" />,
|
||||
title: t("identify_booking_trends"),
|
||||
description: t("identify_booking_trends_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="user-plus" className="h-5 w-5" />,
|
||||
title: t("spot_popular_event_types"),
|
||||
description: t("spot_popular_event_types_description"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
heading="Insights"
|
||||
subtitle={t("insights_subtitle")}
|
||||
title="Insights"
|
||||
description="View booking insights across your events.">
|
||||
<UpgradeTip
|
||||
plan="team"
|
||||
title={t("make_informed_decisions")}
|
||||
description={t("make_informed_decisions_description")}
|
||||
features={features}
|
||||
background="/tips/insights"
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("create_team")}
|
||||
</Button>
|
||||
<Button color="minimal" href="https://go.cal.com/insights" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
}>
|
||||
{!user ? (
|
||||
<></>
|
||||
) : (
|
||||
<FiltersProvider>
|
||||
<Filters />
|
||||
|
||||
<div className="mb-4 space-y-4">
|
||||
<BookingKPICards />
|
||||
|
||||
<BookingStatusLineChart />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<PopularEventsTable />
|
||||
|
||||
<AverageEventDurationChart />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<MostBookedTeamMembersTable />
|
||||
<LeastBookedTeamMembersTable />
|
||||
</div>
|
||||
<RecentFeedbackTable />
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<HighestNoShowHostTable />
|
||||
<HighestRatedMembersTable />
|
||||
<LowestRatedMembersTable />
|
||||
</div>
|
||||
<small className="text-default block text-center">
|
||||
{t("looking_for_more_insights")}{" "}
|
||||
<a
|
||||
className="text-blue-500 hover:underline"
|
||||
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
|
||||
{" "}
|
||||
{t("contact_support")}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</FiltersProvider>
|
||||
)}
|
||||
</UpgradeTip>
|
||||
</Shell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InsightsPage.PageWrapper = PageWrapper;
|
||||
import type { PageProps } from "~/insights/insights-view";
|
||||
import InsightsPage from "~/insights/insights-view";
|
||||
|
||||
const Page = (props: PageProps) => <InsightsPage {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
export { getServerSideProps };
|
||||
@@ -1,185 +1,6 @@
|
||||
// page can be a server component
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { URLSearchParams } from "url";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
|
||||
export default function Type() {
|
||||
// Just redirect to the schedule page to reschedule it.
|
||||
return null;
|
||||
}
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
rescheduledBy: z.string().optional(),
|
||||
allowRescheduleForCancelledBooking: z
|
||||
.string()
|
||||
.transform((value) => value === "true")
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
|
||||
const {
|
||||
uid: bookingUid,
|
||||
seatReferenceUid,
|
||||
rescheduledBy,
|
||||
/**
|
||||
* This is for the case of request-reschedule where the booking is cancelled
|
||||
*/
|
||||
allowRescheduleForCancelledBooking,
|
||||
} = querySchema.parse(context.query);
|
||||
|
||||
const coepFlag = context.query["flag.coep"];
|
||||
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
|
||||
prisma,
|
||||
bookingUid
|
||||
);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
eventType: {
|
||||
select: {
|
||||
users: {
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
team: {
|
||||
select: {
|
||||
parentId: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
seatsPerTimeSlot: true,
|
||||
userId: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dynamicEventSlugRef: true,
|
||||
dynamicGroupSlugRef: true,
|
||||
user: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
const dynamicEventSlugRef = booking?.dynamicEventSlugRef || "";
|
||||
|
||||
if (!booking) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
// If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
|
||||
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
|
||||
if (
|
||||
!allowRescheduleForCancelledBooking &&
|
||||
(booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED)
|
||||
) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/booking/${uid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
|
||||
// TODO: Show something in UI to let user know that this booking is not rescheduleable
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
}
|
||||
|
||||
// if booking event type is for a seated event and no seat reference uid is provided, throw not found
|
||||
if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) {
|
||||
const userId = session?.user?.id;
|
||||
|
||||
if (!userId && !seatReferenceUid) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/login?callbackUrl=/reschedule/${bookingUid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
const userIsHost = booking?.eventType.hosts.find((host) => {
|
||||
if (host.user.id === userId) return true;
|
||||
});
|
||||
|
||||
const userIsOwnerOfEventType = booking?.eventType.owner?.id === userId;
|
||||
|
||||
if (!userIsHost && !userIsOwnerOfEventType) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||
|
||||
const enrichedBookingUser = booking.user
|
||||
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
|
||||
: null;
|
||||
|
||||
const eventUrl = await buildEventUrlFromBooking({
|
||||
eventType,
|
||||
dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null,
|
||||
profileEnrichedBookingUser: enrichedBookingUser,
|
||||
});
|
||||
|
||||
const destinationUrlSearchParams = new URLSearchParams();
|
||||
|
||||
destinationUrlSearchParams.set("rescheduleUid", seatReferenceUid || bookingUid);
|
||||
|
||||
// TODO: I think we should just forward all the query params here including coep flag
|
||||
if (coepFlag) {
|
||||
destinationUrlSearchParams.set("flag.coep", coepFlag as string);
|
||||
}
|
||||
|
||||
const currentUserEmail = rescheduledBy ?? session?.user?.email;
|
||||
|
||||
if (currentUserEmail) {
|
||||
destinationUrlSearchParams.set("rescheduledBy", currentUserEmail);
|
||||
}
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
destination: `${eventUrl}?${destinationUrlSearchParams.toString()}${
|
||||
eventType.seatsPerTimeSlot ? "&bookingUid=null" : ""
|
||||
}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
export { getServerSideProps } from "@lib/reschedule/[uid]/getServerSideProps";
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[uid]";
|
||||
|
||||
+5
-660
@@ -1,667 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Analytics as DubAnalytics } from "@dub/analytics/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { Trans } from "next-i18next";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Script from "next/script";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { SubmitHandler } from "react-hook-form";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import {
|
||||
APP_NAME,
|
||||
URL_PROTOCOL_REGEX,
|
||||
IS_CALCOM,
|
||||
IS_EUROPE,
|
||||
WEBAPP_URL,
|
||||
CLOUDFLARE_SITE_ID,
|
||||
WEBSITE_PRIVACY_POLICY_URL,
|
||||
WEBSITE_TERMS_URL,
|
||||
WEBSITE_URL,
|
||||
} from "@calcom/lib/constants";
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import { pushGTMEvent } from "@calcom/lib/gtm";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import {
|
||||
Button,
|
||||
HeadSeo,
|
||||
PasswordField,
|
||||
TextField,
|
||||
Form,
|
||||
Alert,
|
||||
showToast,
|
||||
CheckboxField,
|
||||
Icon,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { getServerSideProps } from "@lib/signup/getServerSideProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const signupSchema = apiSignupSchema.extend({
|
||||
apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
|
||||
cfToken: z.string().optional(),
|
||||
});
|
||||
import type { SignupProps } from "~/signup-view";
|
||||
import Signup from "~/signup-view";
|
||||
|
||||
const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false });
|
||||
|
||||
type FormValues = z.infer<typeof signupSchema>;
|
||||
|
||||
export type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
title: "connect_all_calendars",
|
||||
description: "connect_all_calendars_description",
|
||||
i18nOptions: {
|
||||
appName: APP_NAME,
|
||||
},
|
||||
icon: "calendar-heart" as const,
|
||||
},
|
||||
{
|
||||
title: "set_availability",
|
||||
description: "set_availbility_description",
|
||||
icon: "users" as const,
|
||||
},
|
||||
{
|
||||
title: "share_a_link_or_embed",
|
||||
description: "share_a_link_or_embed_description",
|
||||
icon: "link-2" as const,
|
||||
i18nOptions: {
|
||||
appName: APP_NAME,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function UsernameField({
|
||||
username,
|
||||
setPremium,
|
||||
premium,
|
||||
setUsernameTaken,
|
||||
orgSlug,
|
||||
usernameTaken,
|
||||
disabled,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TextField> & {
|
||||
username: string;
|
||||
setPremium: (value: boolean) => void;
|
||||
premium: boolean;
|
||||
usernameTaken: boolean;
|
||||
orgSlug?: string;
|
||||
setUsernameTaken: (value: boolean) => void;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const { register, formState } = useFormContext<FormValues>();
|
||||
const debouncedUsername = useDebounce(username, 600);
|
||||
|
||||
useEffect(() => {
|
||||
if (formState.isSubmitting || formState.isSubmitSuccessful) return;
|
||||
|
||||
async function checkUsername() {
|
||||
// If the username can't be changed, there is no point in doing the username availability check
|
||||
if (disabled) return;
|
||||
if (!debouncedUsername) {
|
||||
setPremium(false);
|
||||
setUsernameTaken(false);
|
||||
return;
|
||||
}
|
||||
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
|
||||
setPremium(data.premium);
|
||||
setUsernameTaken(!data.available);
|
||||
});
|
||||
}
|
||||
checkUsername();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedUsername, disabled, orgSlug, formState.isSubmitting, formState.isSubmitSuccessful]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
{...register("username")}
|
||||
data-testid="signup-usernamefield"
|
||||
addOnFilled={false}
|
||||
/>
|
||||
{(!formState.isSubmitting || !formState.isSubmitted) && (
|
||||
<div className="text-gray text-default flex items-center text-sm">
|
||||
<div className="text-sm ">
|
||||
{usernameTaken ? (
|
||||
<div className="text-error flex items-center">
|
||||
<Icon name="info" className="mr-1 inline-block h-4 w-4" />
|
||||
<p>{t("already_in_use_error")}</p>
|
||||
</div>
|
||||
) : premium ? (
|
||||
<div data-testid="premium-username-warning" className="flex items-center">
|
||||
<Icon name="star" className="mr-1 inline-block h-4 w-4" />
|
||||
<p>
|
||||
{t("premium_username", {
|
||||
price: getPremiumPlanPriceValue(),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function addOrUpdateQueryParam(url: string, key: string, value: string) {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
const param = `${key}=${encodeURIComponent(value)}`;
|
||||
return `${url}${separator}${param}`;
|
||||
}
|
||||
|
||||
export default function Signup({
|
||||
prepopulateFormValues,
|
||||
token,
|
||||
orgSlug,
|
||||
isGoogleLoginEnabled,
|
||||
isSAMLLoginEnabled,
|
||||
orgAutoAcceptEmail,
|
||||
redirectUrl,
|
||||
emailVerificationEnabled,
|
||||
}: SignupProps) {
|
||||
const [premiumUsername, setPremiumUsername] = useState(false);
|
||||
const [usernameTaken, setUsernameTaken] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const searchParams = useCompatSearchParams();
|
||||
const telemetry = useTelemetry();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const formMethods = useForm<FormValues>({
|
||||
resolver: zodResolver(signupSchema),
|
||||
defaultValues: prepopulateFormValues satisfies FormValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
formState: { isSubmitting, errors, isSubmitSuccessful },
|
||||
} = formMethods;
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectUrl) {
|
||||
localStorage.setItem("onBoardingRedirect", redirectUrl);
|
||||
}
|
||||
}, [redirectUrl]);
|
||||
|
||||
const [COOKIE_CONSENT, setCOOKIE_CONSENT] = useState(false);
|
||||
|
||||
function handleConsentChange(consent: boolean) {
|
||||
setCOOKIE_CONSENT(!consent);
|
||||
}
|
||||
|
||||
const loadingSubmitState = isSubmitSuccessful || isSubmitting;
|
||||
|
||||
const handleErrorsAndStripe = async (resp: Response) => {
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
if (err.checkoutSessionId) {
|
||||
const stripe = await getStripe();
|
||||
if (stripe) {
|
||||
console.log("Redirecting to stripe checkout");
|
||||
const { error } = await stripe.redirectToCheckout({
|
||||
sessionId: err.checkoutSessionId,
|
||||
});
|
||||
console.warn(error.message);
|
||||
}
|
||||
} else {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
|
||||
const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new");
|
||||
|
||||
const signUp: SubmitHandler<FormValues> = async (_data) => {
|
||||
const { cfToken, ...data } = _data;
|
||||
await fetch("/api/auth/signup", {
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
language: i18n.language,
|
||||
token,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"cf-access-token": cfToken ?? "invalid-token",
|
||||
},
|
||||
method: "POST",
|
||||
})
|
||||
.then(handleErrorsAndStripe)
|
||||
.then(async () => {
|
||||
if (process.env.NEXT_PUBLIC_GTM_ID)
|
||||
pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language });
|
||||
|
||||
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
|
||||
|
||||
const verifyOrGettingStarted = emailVerificationEnabled ? "auth/verify-email" : "getting-started";
|
||||
const gettingStartedWithPlatform = "settings/platform/new";
|
||||
|
||||
const constructCallBackIfUrlPresent = () => {
|
||||
if (isOrgInviteByLink) {
|
||||
return `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`;
|
||||
}
|
||||
|
||||
return addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup");
|
||||
};
|
||||
|
||||
const constructCallBackIfUrlNotPresent = () => {
|
||||
if (!!isPlatformUser) {
|
||||
return `${WEBAPP_URL}/${gettingStartedWithPlatform}?from=signup`;
|
||||
}
|
||||
|
||||
return `${WEBAPP_URL}/${verifyOrGettingStarted}?from=signup`;
|
||||
};
|
||||
|
||||
const constructCallBackUrl = () => {
|
||||
const callbackUrlSearchParams = searchParams?.get("callbackUrl");
|
||||
|
||||
return !!callbackUrlSearchParams
|
||||
? constructCallBackIfUrlPresent()
|
||||
: constructCallBackIfUrlNotPresent();
|
||||
};
|
||||
|
||||
const callBackUrl = constructCallBackUrl();
|
||||
|
||||
await signIn<"credentials">("credentials", {
|
||||
...data,
|
||||
callbackUrl: callBackUrl,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
formMethods.setError("apiError", { message: err.message });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{IS_CALCOM && (!IS_EUROPE || COOKIE_CONSENT) ? (
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_GTM_ID && (
|
||||
<>
|
||||
<Script
|
||||
id="gtm-init-script"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||
var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||
j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', '${process.env.NEXT_PUBLIC_GTM_ID}');`,
|
||||
}}
|
||||
/>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<DubAnalytics
|
||||
cookieOptions={{
|
||||
domain: isENVDev ? undefined : `.${new URL(WEBSITE_URL).hostname}`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
"light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||
"[--cal-brand-subtle:#9CA3AF]",
|
||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
|
||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1] "
|
||||
)}>
|
||||
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||
{/* Left side */}
|
||||
<div className="ml-auto mr-auto mt-0 flex w-full max-w-xl flex-col px-4 pt-6 sm:px-16 md:px-20 lg:mt-12 2xl:px-28">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="font-cal text-[28px] leading-none ">
|
||||
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
|
||||
</h1>
|
||||
{IS_CALCOM ? (
|
||||
<p className="text-subtle text-base font-medium leading-5">{t("cal_signup_description")}</p>
|
||||
) : (
|
||||
<p className="text-subtle text-base font-medium leading-5">
|
||||
{t("calcom_explained", {
|
||||
appName: APP_NAME,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Form Container */}
|
||||
<div className="mt-12">
|
||||
<Form
|
||||
className="flex flex-col gap-4"
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
let updatedValues = values;
|
||||
if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
updatedValues = {
|
||||
...values,
|
||||
username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail),
|
||||
};
|
||||
}
|
||||
await signUp(updatedValues);
|
||||
}}>
|
||||
{/* Username */}
|
||||
{!isOrgInviteByLink ? (
|
||||
<UsernameField
|
||||
orgSlug={orgSlug}
|
||||
label={t("username")}
|
||||
username={watch("username") || ""}
|
||||
premium={premiumUsername}
|
||||
usernameTaken={usernameTaken}
|
||||
disabled={!!orgSlug}
|
||||
setUsernameTaken={(value) => setUsernameTaken(value)}
|
||||
data-testid="signup-usernamefield"
|
||||
setPremium={(value) => setPremiumUsername(value)}
|
||||
addOnLeading={
|
||||
orgSlug
|
||||
? `${getOrgFullOrigin(orgSlug, { protocol: true }).replace(URL_PROTOCOL_REGEX, "")}/`
|
||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/`
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{/* Email */}
|
||||
<TextField
|
||||
{...register("email")}
|
||||
label={t("email")}
|
||||
type="email"
|
||||
disabled={prepopulateFormValues?.email}
|
||||
data-testid="signup-emailfield"
|
||||
/>
|
||||
|
||||
{/* Password */}
|
||||
<PasswordField
|
||||
data-testid="signup-passwordfield"
|
||||
label={t("password")}
|
||||
{...register("password")}
|
||||
hintErrors={["caplow", "min", "num"]}
|
||||
/>
|
||||
{/* Cloudflare Turnstile Captcha */}
|
||||
{CLOUDFLARE_SITE_ID ? (
|
||||
<TurnstileCaptcha
|
||||
appearance="interaction-only"
|
||||
onVerify={(token) => {
|
||||
formMethods.setValue("cfToken", token);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<CheckboxField
|
||||
onChange={() => handleConsentChange(COOKIE_CONSENT)}
|
||||
description={t("cookie_consent_checkbox")}
|
||||
/>
|
||||
{errors.apiError && (
|
||||
<Alert
|
||||
className="mb-3"
|
||||
severity="error"
|
||||
message={errors.apiError?.message}
|
||||
data-testid="signup-error-message"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className="my-2 w-full justify-center"
|
||||
loading={loadingSubmitState}
|
||||
disabled={
|
||||
!!formMethods.formState.errors.username ||
|
||||
!!formMethods.formState.errors.email ||
|
||||
!formMethods.getValues("email") ||
|
||||
!formMethods.getValues("password") ||
|
||||
(CLOUDFLARE_SITE_ID &&
|
||||
!process.env.NEXT_PUBLIC_IS_E2E &&
|
||||
!formMethods.getValues("cfToken")) ||
|
||||
isSubmitting ||
|
||||
usernameTaken
|
||||
}>
|
||||
{premiumUsername && !usernameTaken
|
||||
? `Create Account for ${getPremiumPlanPriceValue()}`
|
||||
: t("create_account")}
|
||||
</Button>
|
||||
</Form>
|
||||
{!isGoogleLoginEnabled && !isSAMLLoginEnabled ? null : (
|
||||
<div className="mt-6">
|
||||
<div className="relative flex items-center">
|
||||
<div className="border-subtle flex-grow border-t" />
|
||||
<span className="text-subtle mx-2 flex-shrink text-sm font-normal leading-none">
|
||||
{t("or_continue_with")}
|
||||
</span>
|
||||
<div className="border-subtle flex-grow border-t" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 flex flex-col gap-2 md:flex-row">
|
||||
{isGoogleLoginEnabled ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={!!formMethods.formState.errors.username || premiumUsername}
|
||||
loading={isGoogleLoading}
|
||||
CustomStartIcon={
|
||||
<img
|
||||
className={classNames(
|
||||
"text-subtle mr-2 h-4 w-4 dark:invert",
|
||||
premiumUsername && "opacity-50"
|
||||
)}
|
||||
src="/google-icon.svg"
|
||||
alt=""
|
||||
/>
|
||||
}
|
||||
className={classNames(
|
||||
"w-full justify-center rounded-md text-center",
|
||||
formMethods.formState.errors.username ? "opacity-50" : ""
|
||||
)}
|
||||
onClick={async () => {
|
||||
setIsGoogleLoading(true);
|
||||
const username = formMethods.getValues("username");
|
||||
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
|
||||
const searchQueryParams = new URLSearchParams();
|
||||
if (username) {
|
||||
// If username is present we save it in query params to check for premium
|
||||
searchQueryParams.set("username", username);
|
||||
localStorage.setItem("username", username);
|
||||
}
|
||||
if (token) {
|
||||
searchQueryParams.set("email", prepopulateFormValues?.email);
|
||||
}
|
||||
const url = searchQueryParams.toString()
|
||||
? `${GOOGLE_AUTH_URL}?${searchQueryParams.toString()}`
|
||||
: GOOGLE_AUTH_URL;
|
||||
|
||||
router.push(url);
|
||||
}}>
|
||||
Google
|
||||
</Button>
|
||||
) : null}
|
||||
{isSAMLLoginEnabled ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={
|
||||
!!formMethods.formState.errors.username ||
|
||||
!!formMethods.formState.errors.email ||
|
||||
premiumUsername ||
|
||||
isSubmitting ||
|
||||
isGoogleLoading
|
||||
}
|
||||
className={classNames(
|
||||
"w-full justify-center rounded-md text-center",
|
||||
formMethods.formState.errors.username && formMethods.formState.errors.email
|
||||
? "opacity-50"
|
||||
: ""
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!formMethods.getValues("username")) {
|
||||
formMethods.trigger("username");
|
||||
}
|
||||
if (!formMethods.getValues("email")) {
|
||||
formMethods.trigger("email");
|
||||
|
||||
return;
|
||||
}
|
||||
const username = formMethods.getValues("username");
|
||||
if (!username) {
|
||||
showToast("error", t("username_required"));
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("username", username);
|
||||
const sp = new URLSearchParams();
|
||||
// @NOTE: don't remove username query param as it's required right now for stripe payment page
|
||||
sp.set("username", username);
|
||||
sp.set("email", formMethods.getValues("email"));
|
||||
router.push(
|
||||
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/sso/saml` + `?${sp.toString()}`
|
||||
);
|
||||
}}>
|
||||
<Icon name="shield-check" className="mr-2 h-5 w-5" />
|
||||
{t("saml_sso")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* Already have an account & T&C */}
|
||||
<div className="mt-10 flex h-full flex-col justify-end pb-6 text-xs">
|
||||
<div className="flex flex-col text-sm">
|
||||
<div className="flex gap-1">
|
||||
<p className="text-subtle">{t("already_have_account")}</p>
|
||||
<Link href="/auth/login" className="text-emphasis hover:underline">
|
||||
{t("sign_in")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-subtle ">
|
||||
<Trans
|
||||
i18nKey="signing_up_terms"
|
||||
components={[
|
||||
<Link
|
||||
className="text-emphasis hover:underline"
|
||||
key="terms"
|
||||
href={`${WEBSITE_TERMS_URL}`}
|
||||
target="_blank">
|
||||
Terms
|
||||
</Link>,
|
||||
<Link
|
||||
className="text-emphasis hover:underline"
|
||||
key="privacy"
|
||||
href={`${WEBSITE_PRIVACY_POLICY_URL}`}
|
||||
target="_blank">
|
||||
Privacy Policy.
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 dark:bg-none lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12">
|
||||
{IS_CALCOM && (
|
||||
<>
|
||||
<div className="-mt-4 mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/product-of-the-day.svg"
|
||||
className="h-[34px] w-full dark:invert"
|
||||
alt="Cal.com was Product of the Day at ProductHunt"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/product-of-the-week.svg"
|
||||
className="h-[34px] w-full dark:invert"
|
||||
alt="Cal.com was Product of the Week at ProductHunt"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/product-of-the-month.svg"
|
||||
className="h-[34px] w-full dark:invert"
|
||||
alt="Cal.com was Product of the Month at ProductHunt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/producthunt.svg"
|
||||
className="h-[54px] w-full"
|
||||
alt="ProductHunt Rating of 5 Stars"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/google-reviews.svg"
|
||||
className="h-[54px] w-full"
|
||||
alt="Google Reviews Rating of 4.7 Stars"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/product-cards/g2.svg"
|
||||
className="h-[54px] w-full"
|
||||
alt="G2 Rating of 4.7 Stars"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] dark:bg-white/5 lg:block lg:py-[6px] lg:pl-[6px]">
|
||||
<img className="block dark:hidden" src="/mock-event-type-list.svg" alt="Cal.com Booking Page" />
|
||||
<img
|
||||
className="hidden dark:block"
|
||||
src="/mock-event-type-list-dark.svg"
|
||||
alt="Cal.com Booking Page"
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-12 mt-8 hidden h-full w-full grid-cols-3 gap-4 overflow-hidden lg:grid">
|
||||
{FEATURES.map((feature) => (
|
||||
<>
|
||||
<div className="max-w-52 mb-8 flex flex-col leading-none sm:mb-0">
|
||||
<div className="text-emphasis items-center">
|
||||
<Icon name={feature.icon} className="mb-1 h-4 w-4" />
|
||||
<span className="text-sm font-medium">{t(feature.title)}</span>
|
||||
</div>
|
||||
<div className="text-subtle text-sm">
|
||||
<p>
|
||||
{t(
|
||||
feature.description,
|
||||
feature.i18nOptions && {
|
||||
...feature.i18nOptions,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const Page = (props: SignupProps) => <Signup {...props} />;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
Signup.PageWrapper = PageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
@@ -57,4 +57,4 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const component = getComponent(context.params?.pages?.[0] || "");
|
||||
return component.getServerSideProps?.(context, ...rest) || { props: {} };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user