Fixed #1015 - Teams user registration is broken (#1090)

* Fixed #1015 - Teams user registration is broken

* Type fixes for avilability form in onboarding

* Re adds missing strings

* Updates user availability in one query

Tested and working correctly

* Fixes seeder and tests

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Alex van Andel 2021-11-11 05:44:53 +00:00 committed by GitHub
parent 16fba702fb
commit bf659c0b16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 267 additions and 217 deletions

View File

@ -1,11 +1,14 @@
import { useId } from "@radix-ui/react-id"; import { useId } from "@radix-ui/react-id";
import { forwardRef, ReactNode } from "react"; import { forwardRef, ReactElement, ReactNode, Ref } from "react";
import { FormProvider, SubmitHandler, UseFormReturn } from "react-hook-form"; import { FieldValues, FormProvider, SubmitHandler, useFormContext, UseFormReturn } from "react-hook-form";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { getErrorFromUnknown } from "@lib/errors"; import { getErrorFromUnknown } from "@lib/errors";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification"; import showToast from "@lib/notification";
import { Alert } from "@components/ui/Alert";
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string }; type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) { export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return ( return (
@ -28,78 +31,97 @@ export function Label(props: JSX.IntrinsicElements["label"]) {
); );
} }
export const TextField = forwardRef< type InputFieldProps = {
HTMLInputElement, label?: ReactNode;
{ addOnLeading?: ReactNode;
label: ReactNode; } & React.ComponentProps<typeof Input> & {
} & React.ComponentProps<typeof Input> & { labelProps?: React.ComponentProps<typeof Label>;
labelProps?: React.ComponentProps<typeof Label>; };
}
>(function TextField(props, ref) {
const id = useId();
const { label, ...passThroughToInput } = props;
// TODO: use `useForm()` from RHF and get error state here too! const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(props, ref) {
const id = useId();
const { t } = useLocale();
const methods = useFormContext();
const {
label = t(props.name),
labelProps,
placeholder = t(props.name + "_placeholder") !== props.name + "_placeholder"
? t(props.name + "_placeholder")
: "",
className,
addOnLeading,
...passThroughToInput
} = props;
return ( return (
<div> <div>
<Label htmlFor={id} {...props.labelProps}> <Label htmlFor={id} {...labelProps}>
{label} {label}
</Label> </Label>
<Input id={id} {...passThroughToInput} ref={ref} /> {addOnLeading ? (
<div className="flex mt-1 rounded-md shadow-sm">
{addOnLeading}
<Input
id={id}
placeholder={placeholder}
className={classNames(className, "mt-0")}
{...passThroughToInput}
ref={ref}
/>
</div>
) : (
<Input id={id} placeholder={placeholder} className={className} {...passThroughToInput} ref={ref} />
)}
{methods?.formState?.errors[props.name] && (
<Alert className="mt-1" severity="error" message={methods.formState.errors[props.name].message} />
)}
</div> </div>
); );
}); });
/** export const TextField = forwardRef<HTMLInputElement, InputFieldProps>(function TextField(props, ref) {
* Form helper that creates a rect-hook-form Provider and helps with submission handling & default error handling return <InputField ref={ref} {...props} />;
*/ });
export function Form<TFieldValues>(
props: { export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
/** props,
* Pass in the return from `react-hook-form`s `useForm()` ref
*/
form: UseFormReturn<TFieldValues>;
/**
* Submit handler - you'll get the typed form values back
*/
handleSubmit?: SubmitHandler<TFieldValues>;
/**
* Optional - Override the default error handling
* By default it shows a toast with the error
*/
handleError?: (err: ReturnType<typeof getErrorFromUnknown>) => void;
} & Omit<JSX.IntrinsicElements["form"], "ref">
) { ) {
const { return <InputField type="password" placeholder="•••••••••••••" ref={ref} {...props} />;
form, });
handleSubmit,
handleError = (err) => { export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
showToast(err.message, "error"); return <InputField type="email" inputMode="email" ref={ref} {...props} />;
}, });
...passThrough
} = props; type FormProps<T> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
JSX.IntrinsicElements["form"],
"onSubmit"
>;
const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLFormElement>) => {
const { form, handleSubmit, ...passThrough } = props;
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<form <form
onSubmit={ ref={ref}
handleSubmit onSubmit={(event) => {
? form.handleSubmit(async (...args) => { form
try { .handleSubmit(handleSubmit)(event)
await handleSubmit(...args); .catch((err) => {
} catch (_err) { showToast(`${getErrorFromUnknown(err).message}`, "error");
const err = getErrorFromUnknown(_err); });
handleError(err); }}
}
})
: undefined
}
{...passThrough}> {...passThrough}>
{props.children} {props.children}
</form> </form>
</FormProvider> </FormProvider>
); );
} };
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
) => ReactElement;
export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) { export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) {
return ( return (

View File

@ -4,6 +4,9 @@ interface UsernameInputProps extends React.ComponentPropsWithRef<"input"> {
label?: string; label?: string;
} }
/**
* @deprecated Use <TextField addOnLeading={}> to achieve the same effect.
*/
const UsernameInput = React.forwardRef<HTMLInputElement, UsernameInputProps>((props, ref) => ( const UsernameInput = React.forwardRef<HTMLInputElement, UsernameInputProps>((props, ref) => (
// todo, check if username is already taken here? // todo, check if username is already taken here?
<div> <div>

View File

@ -3,9 +3,10 @@ import dayjs, { Dayjs } from "dayjs";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { Controller, useFieldArray } from "react-hook-form"; import { Controller, useFieldArray } from "react-hook-form";
import { defaultDayRange } from "@lib/availability";
import { weekdayNames } from "@lib/core/i18n/weekday"; import { weekdayNames } from "@lib/core/i18n/weekday";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { TimeRange, Schedule as ScheduleType } from "@lib/types/schedule"; import { TimeRange } from "@lib/types/schedule";
import Button from "@components/ui/Button"; import Button from "@components/ui/Button";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@ -30,22 +31,6 @@ const TIMES = (() => {
})(); })();
/** End Time Increments For Select */ /** End Time Increments For Select */
// sets the desired time in current date, needs to be current date for proper DST translation
const defaultDayRange: TimeRange = {
start: new Date(new Date().setHours(9, 0, 0, 0)),
end: new Date(new Date().setHours(17, 0, 0, 0)),
};
export const DEFAULT_SCHEDULE: ScheduleType = [
[],
[defaultDayRange],
[defaultDayRange],
[defaultDayRange],
[defaultDayRange],
[defaultDayRange],
[],
];
type Option = { type Option = {
readonly label: string; readonly label: string;
readonly value: number; readonly value: number;
@ -139,7 +124,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
onChange={(e) => (e.target.checked ? replace([defaultDayRange]) : replace([]))} onChange={(e) => (e.target.checked ? replace([defaultDayRange]) : replace([]))}
className="inline-block border-gray-300 rounded-sm focus:ring-neutral-500 text-neutral-900" className="inline-block border-gray-300 rounded-sm focus:ring-neutral-500 text-neutral-900"
/> />
<span className="text-sm inline-block capitalize">{weekday}</span> <span className="inline-block text-sm capitalize">{weekday}</span>
</label> </label>
</div> </div>
<div className="flex-grow"> <div className="flex-grow">
@ -157,7 +142,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
/> />
</div> </div>
))} ))}
<span className="text-sm block text-gray-500">{!fields.length && t("no_availability")}</span> <span className="block text-sm text-gray-500">{!fields.length && t("no_availability")}</span>
</div> </div>
<div> <div>
<Button <Button

47
lib/availability.ts Normal file
View File

@ -0,0 +1,47 @@
import { Availability } from "@prisma/client";
import { Schedule, TimeRange } from "./types/schedule";
// sets the desired time in current date, needs to be current date for proper DST translation
export const defaultDayRange: TimeRange = {
start: new Date(new Date().setHours(9, 0, 0, 0)),
end: new Date(new Date().setHours(17, 0, 0, 0)),
};
export const DEFAULT_SCHEDULE: Schedule = [
[],
[defaultDayRange],
[defaultDayRange],
[defaultDayRange],
[defaultDayRange],
[defaultDayRange],
[],
];
export function getAvailabilityFromSchedule(schedule: Schedule): Availability[] {
return schedule.reduce((availability: Availability[], times: TimeRange[], day: number) => {
const addNewTime = (time: TimeRange) =>
({
days: [day],
startTime: time.start,
endTime: time.end,
} as Availability);
const filteredTimes = times.filter((time) => {
let idx;
if (
(idx = availability.findIndex(
(schedule) => schedule.startTime === time.start && schedule.endTime === time.end
)) !== -1
) {
availability[idx].days.push(day);
return false;
}
return true;
});
filteredTimes.forEach((time) => {
availability.push(addNewTime(time));
});
return availability;
}, [] as Availability[]);
}

View File

@ -39,6 +39,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: userEmail, email: userEmail,
}, },
], ],
AND: [
{
emailVerified: {
not: null,
},
},
],
}, },
}); });

View File

@ -1,12 +1,11 @@
import { Availability } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { getAvailabilityFromSchedule } from "@lib/availability";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { TimeRange } from "@lib/types/schedule";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req }); const session = await getSession({ req });
const userId = session?.user?.id; const userId = session?.user?.id;
if (!userId) { if (!userId) {
res.status(401).json({ message: "Not authenticated" }); res.status(401).json({ message: "Not authenticated" });
@ -17,58 +16,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Bad Request." }); return res.status(400).json({ message: "Bad Request." });
} }
const availability = req.body.schedule.reduce( const availability = getAvailabilityFromSchedule(req.body.schedule);
(availability: Availability[], times: TimeRange[], day: number) => {
const addNewTime = (time: TimeRange) =>
({
days: [day],
startTime: time.start,
endTime: time.end,
} as Availability);
const filteredTimes = times.filter((time) => {
let idx;
if (
(idx = availability.findIndex(
(schedule) => schedule.startTime === time.start && schedule.endTime === time.end
)) !== -1
) {
availability[idx].days.push(day);
return false;
}
return true;
});
filteredTimes.forEach((time) => {
availability.push(addNewTime(time));
});
return availability;
},
[] as Availability[]
);
if (req.method === "POST") { if (req.method === "POST") {
try { try {
await prisma.availability.deleteMany({ await prisma.user.update({
where: { where: {
userId, id: userId,
}, },
}); data: {
await Promise.all( availability: {
availability.map((schedule: Availability) => /* We delete user availabilty */
prisma.availability.create({ deleteMany: {
data: { userId: {
days: schedule.days, equals: userId,
startTime: schedule.startTime,
endTime: schedule.endTime,
user: {
connect: {
id: userId,
},
}, },
}, },
}) /* So we can replace it */
) createMany: {
); data: availability.map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
},
},
});
return res.status(200).json({ return res.status(200).json({
message: "created", message: "created",
}); });

View File

@ -1,43 +1,50 @@
import { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/client"; import { signIn } from "next-auth/client";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useForm, SubmitHandler, FormProvider } from "react-hook-form";
import { asStringOrNull } from "@lib/asStringOrNull";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmailField, PasswordField, TextField } from "@components/form/fields";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
import { UsernameInput } from "@components/ui/UsernameInput"; import { Alert } from "@components/ui/Alert";
import ErrorAlert from "@components/ui/alerts/Error"; import Button from "@components/ui/Button";
export default function Signup(props) { type Props = inferSSRProps<typeof getServerSideProps>;
type FormValues = {
username: string;
email: string;
password: string;
passwordcheck: string;
apiError: string;
};
export default function Signup({ email }: Props) {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const methods = useForm<FormValues>();
const {
register,
formState: { errors, isSubmitting },
} = methods;
const [hasErrors, setHasErrors] = useState(false); methods.setValue("email", email);
const [errorMessage, setErrorMessage] = useState("");
const handleErrors = async (resp) => { const handleErrors = async (resp: Response) => {
if (!resp.ok) { if (!resp.ok) {
const err = await resp.json(); const err = await resp.json();
throw new Error(err.message); throw new Error(err.message);
} }
}; };
const signUp = (e) => { const signUp: SubmitHandler<FormValues> = async (data) => {
e.preventDefault(); await fetch("/api/auth/signup", {
if (e.target.password.value !== e.target.passwordcheck.value) {
throw new Error("Password mismatch");
}
const email: string = e.target.email.value;
const password: string = e.target.password.value;
fetch("/api/auth/signup", {
body: JSON.stringify({ body: JSON.stringify({
username: e.target.username.value, ...data,
password,
email,
}), }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -45,104 +52,96 @@ export default function Signup(props) {
method: "POST", method: "POST",
}) })
.then(handleErrors) .then(handleErrors)
.then(() => signIn("Cal.com", { callbackUrl: (router.query.callbackUrl || "") as string })) .then(async () => await signIn("Cal.com", { callbackUrl: (router.query.callbackUrl || "") as string }))
.catch((err) => { .catch((err) => {
setHasErrors(true); methods.setError("apiError", { message: err.message });
setErrorMessage(err.message);
}); });
}; };
return ( return (
<div <div
className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8" className="flex flex-col justify-center min-h-screen py-12 bg-gray-50 sm:px-6 lg:px-8"
aria-labelledby="modal-title" aria-labelledby="modal-title"
role="dialog" role="dialog"
aria-modal="true"> aria-modal="true">
<HeadSeo title={t("sign_up")} description={t("sign_up")} /> <HeadSeo title={t("sign_up")} description={t("sign_up")} />
<div className="sm:mx-auto sm:w-full sm:max-w-md"> <div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="font-cal text-center text-3xl font-extrabold text-gray-900"> <h2 className="text-3xl font-extrabold text-center text-gray-900 font-cal">
{t("create_your_account")} {t("create_your_account")}
</h2> </h2>
</div> </div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow mx-2 sm:rounded-lg sm:px-10"> <div className="px-4 py-8 mx-2 bg-white shadow sm:rounded-lg sm:px-10">
<form method="POST" onSubmit={signUp} className="bg-white space-y-6"> {/* TODO: Refactor as soon as /availability is live */}
{hasErrors && <ErrorAlert message={errorMessage} />} <FormProvider {...methods}>
<div> <form onSubmit={methods.handleSubmit(signUp)} className="space-y-6 bg-white">
<div className="mb-2"> {errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
<UsernameInput required /> <div className="space-y-2">
</div> <TextField
<div className="mb-2"> addOnLeading={
<label htmlFor="email" className="block text-sm font-medium text-gray-700"> <span className="inline-flex items-center px-3 text-gray-500 border border-r-0 border-gray-300 rounded-l-sm bg-gray-50 sm:text-sm">
{t("email")} {process.env.NEXT_PUBLIC_APP_URL}/
</label> </span>
<input }
type="email" labelProps={{ className: "block text-sm font-medium text-gray-700" }}
name="email" className="flex-grow block w-full min-w-0 lowercase border-gray-300 rounded-none rounded-r-sm focus:ring-black focus:border-black sm:text-sm"
inputMode="email" {...register("username")}
id="email"
placeholder="jdoe@example.com"
disabled={!!props.email}
readOnly={!!props.email}
value={props.email}
className="bg-gray-100 mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-black focus:border-brand sm:text-sm"
/>
</div>
<div className="mb-2">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
{t("password")}
</label>
<input
type="password"
name="password"
id="password"
required required
placeholder="•••••••••••••" />
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-black focus:border-brand sm:text-sm" <EmailField
{...register("email")}
className="block w-full px-3 py-2 mt-1 bg-gray-100 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-black focus:border-black sm:text-sm"
/>
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
}}
{...register("password")}
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-black focus:border-black sm:text-sm"
/>
<PasswordField
label={t("confirm_password")}
labelProps={{
className: "block text-sm font-medium text-gray-700",
}}
{...register("passwordcheck", {
validate: (value) =>
value === methods.watch("password") || (t("error_password_mismatch") as string),
})}
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-black focus:border-black sm:text-sm"
/> />
</div> </div>
<div> <div className="flex space-x-2">
<label htmlFor="passwordcheck" className="block text-sm font-medium text-gray-700"> <Button loading={isSubmitting} className="justify-center w-7/12">
{t("confirm_password")} {t("create_account")}
</label> </Button>
<input <Button
type="password" color="secondary"
name="passwordcheck" className="justify-center w-5/12"
id="passwordcheck" onClick={() =>
required signIn("Cal.com", { callbackUrl: (router.query.callbackUrl || "") as string })
placeholder="•••••••••••••" }>
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-black focus:border-brand sm:text-sm" {t("login_instead")}
/> </Button>
</div> </div>
</div> </form>
<div className="mt-3 sm:mt-4 flex"> </FormProvider>
<input
type="submit"
value={t("create_account")}
className="btn btn-primary w-7/12 mr-2 inline-flex justify-center rounded-md border border-transparent cursor-pointer shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black sm:text-sm"
/>
<a
onClick={() => signIn("Cal.com", { callbackUrl: (router.query.callbackUrl || "") as string })}
className="w-5/12 inline-flex justify-center text-sm text-gray-500 font-medium border px-4 py-2 rounded btn cursor-pointer">
{t("login_instead")}
</a>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
); );
} }
export async function getServerSideProps(ctx) { export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
if (!ctx.query.token) { const token = asStringOrNull(ctx.query.token);
if (!token) {
return { return {
notFound: true, notFound: true,
}; };
} }
const verificationRequest = await prisma.verificationRequest.findUnique({ const verificationRequest = await prisma.verificationRequest.findUnique({
where: { where: {
token: ctx.query.token, token,
}, },
}); });
@ -175,4 +174,4 @@ export async function getServerSideProps(ctx) {
} }
return { props: { email: verificationRequest.identifier } }; return { props: { email: verificationRequest.identifier } };
} };

View File

@ -2,6 +2,7 @@ import Link from "next/link";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
import { DEFAULT_SCHEDULE } from "@lib/availability";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification"; import showToast from "@lib/notification";
import { inferQueryOutput, trpc } from "@lib/trpc"; import { inferQueryOutput, trpc } from "@lib/trpc";
@ -10,7 +11,7 @@ import { Schedule as ScheduleType } from "@lib/types/schedule";
import Shell from "@components/Shell"; import Shell from "@components/Shell";
import { Form } from "@components/form/fields"; import { Form } from "@components/form/fields";
import Button from "@components/ui/Button"; import Button from "@components/ui/Button";
import Schedule, { DEFAULT_SCHEDULE } from "@components/ui/form/Schedule"; import Schedule from "@components/ui/form/Schedule";
type FormValues = { type FormValues = {
schedule: ScheduleType; schedule: ScheduleType;

View File

@ -16,6 +16,7 @@ import { useForm } from "react-hook-form";
import TimezoneSelect from "react-timezone-select"; import TimezoneSelect from "react-timezone-select";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { DEFAULT_SCHEDULE } from "@lib/availability";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import getIntegrations from "@lib/integrations/getIntegrations"; import getIntegrations from "@lib/integrations/getIntegrations";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
@ -29,7 +30,7 @@ import { CalendarListContainer } from "@components/integrations/CalendarListCont
import { Alert } from "@components/ui/Alert"; import { Alert } from "@components/ui/Alert";
import Button from "@components/ui/Button"; import Button from "@components/ui/Button";
import Text from "@components/ui/Text"; import Text from "@components/ui/Text";
import Schedule, { DEFAULT_SCHEDULE } from "@components/ui/form/Schedule"; import Schedule from "@components/ui/form/Schedule";
import getCalendarCredentials from "@server/integrations/getCalendarCredentials"; import getCalendarCredentials from "@server/integrations/getCalendarCredentials";
import getConnectedCalendars from "@server/integrations/getConnectedCalendars"; import getConnectedCalendars from "@server/integrations/getConnectedCalendars";
@ -313,7 +314,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
title: t("set_availability"), title: t("set_availability"),
description: t("set_availability_instructions"), description: t("set_availability_instructions"),
Component: ( Component: (
<Form <Form<ScheduleFormValues>
className="max-w-lg mx-auto text-black bg-white dark:bg-opacity-5 dark:text-white" className="max-w-lg mx-auto text-black bg-white dark:bg-opacity-5 dark:text-white"
form={availabilityForm} form={availabilityForm}
handleSubmit={async (values) => { handleSubmit={async (values) => {

View File

@ -245,7 +245,7 @@
"troubleshoot_availability": "Troubleshoot your availability to explore why your times are showing as they are.", "troubleshoot_availability": "Troubleshoot your availability to explore why your times are showing as they are.",
"change_available_times": "Change available times", "change_available_times": "Change available times",
"change_your_available_times": "Change your available times", "change_your_available_times": "Change your available times",
"change_start_end": "Set your weekly hours", "change_start_end": "Change the start and end times of your day",
"change_start_end_buffer": "Set the start and end time of your day and a minimum buffer between your meetings.", "change_start_end_buffer": "Set the start and end time of your day and a minimum buffer between your meetings.",
"current_start_date": "Currently, your day is set to start at", "current_start_date": "Currently, your day is set to start at",
"start_end_changed_successfully": "The start and end times for your day have been changed successfully.", "start_end_changed_successfully": "The start and end times for your day have been changed successfully.",
@ -254,7 +254,10 @@
"dark": "Dark", "dark": "Dark",
"automatically_adjust_theme": "Automatically adjust theme based on invitee preferences", "automatically_adjust_theme": "Automatically adjust theme based on invitee preferences",
"email": "Email", "email": "Email",
"email_placeholder": "jdoe@example.com",
"full_name": "Full name", "full_name": "Full name",
"browse_api_documentation": "Browse our API documentation",
"leverage_our_api": "Leverage our API for full control and customizability.",
"create_webhook": "Create Webhook", "create_webhook": "Create Webhook",
"booking_cancelled": "Booking Cancelled", "booking_cancelled": "Booking Cancelled",
"booking_rescheduled": "Booking Rescheduled", "booking_rescheduled": "Booking Rescheduled",
@ -463,7 +466,7 @@
"manage_your_billing_info": "Manage your billing information and cancel your subscription.", "manage_your_billing_info": "Manage your billing information and cancel your subscription.",
"availability": "Availability", "availability": "Availability",
"availability_updated_successfully": "Availability updated successfully", "availability_updated_successfully": "Availability updated successfully",
"configure_availability": "Set times when you are available for bookings.", "configure_availability": "Configure times when you are available for bookings.",
"change_weekly_schedule": "Change your weekly schedule", "change_weekly_schedule": "Change your weekly schedule",
"logo": "Logo", "logo": "Logo",
"error": "Error", "error": "Error",
@ -525,5 +528,7 @@
"connect_an_additional_calendar": "Connect an additional calendar", "connect_an_additional_calendar": "Connect an additional calendar",
"conferencing": "Conferencing", "conferencing": "Conferencing",
"calendar": "Calendar", "calendar": "Calendar",
"not_installed": "Not installed" "not_installed": "Not installed",
"error_password_mismatch": "Passwords don't match.",
"error_required_field": "This field is required."
} }

View File

@ -3,6 +3,7 @@ import dayjs from "dayjs";
import { uuid } from "short-uuid"; import { uuid } from "short-uuid";
import { hashPassword } from "../lib/auth"; import { hashPassword } from "../lib/auth";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "../lib/availability";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@ -26,6 +27,11 @@ async function createUserAndEventType(opts: {
password: await hashPassword(opts.user.password), password: await hashPassword(opts.user.password),
emailVerified: new Date(), emailVerified: new Date(),
completedOnboarding: opts.user.completedOnboarding ?? true, completedOnboarding: opts.user.completedOnboarding ?? true,
availability: {
createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
},
},
}; };
const user = await prisma.user.upsert({ const user = await prisma.user.upsert({
where: { email: opts.user.email }, where: { email: opts.user.email },