* 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:
parent
16fba702fb
commit
bf659c0b16
11 changed files with 267 additions and 217 deletions
|
@ -1,11 +1,14 @@
|
|||
import { useId } from "@radix-ui/react-id";
|
||||
import { forwardRef, ReactNode } from "react";
|
||||
import { FormProvider, SubmitHandler, UseFormReturn } from "react-hook-form";
|
||||
import { forwardRef, ReactElement, ReactNode, Ref } from "react";
|
||||
import { FieldValues, FormProvider, SubmitHandler, useFormContext, UseFormReturn } from "react-hook-form";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import showToast from "@lib/notification";
|
||||
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
|
||||
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
|
||||
return (
|
||||
|
@ -28,78 +31,97 @@ export function Label(props: JSX.IntrinsicElements["label"]) {
|
|||
);
|
||||
}
|
||||
|
||||
export const TextField = forwardRef<
|
||||
HTMLInputElement,
|
||||
{
|
||||
label: ReactNode;
|
||||
} & React.ComponentProps<typeof Input> & {
|
||||
labelProps?: React.ComponentProps<typeof Label>;
|
||||
}
|
||||
>(function TextField(props, ref) {
|
||||
const id = useId();
|
||||
const { label, ...passThroughToInput } = props;
|
||||
type InputFieldProps = {
|
||||
label?: ReactNode;
|
||||
addOnLeading?: ReactNode;
|
||||
} & React.ComponentProps<typeof Input> & {
|
||||
labelProps?: React.ComponentProps<typeof Label>;
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div>
|
||||
<Label htmlFor={id} {...props.labelProps}>
|
||||
<Label htmlFor={id} {...labelProps}>
|
||||
{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>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Form helper that creates a rect-hook-form Provider and helps with submission handling & default error handling
|
||||
*/
|
||||
export function Form<TFieldValues>(
|
||||
props: {
|
||||
/**
|
||||
* Pass in the return from `react-hook-form`s `useForm()`
|
||||
*/
|
||||
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">
|
||||
export const TextField = forwardRef<HTMLInputElement, InputFieldProps>(function TextField(props, ref) {
|
||||
return <InputField ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const {
|
||||
form,
|
||||
handleSubmit,
|
||||
handleError = (err) => {
|
||||
showToast(err.message, "error");
|
||||
},
|
||||
...passThrough
|
||||
} = props;
|
||||
return <InputField type="password" placeholder="•••••••••••••" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
|
||||
return <InputField type="email" inputMode="email" ref={ref} {...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 (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={
|
||||
handleSubmit
|
||||
? form.handleSubmit(async (...args) => {
|
||||
try {
|
||||
await handleSubmit(...args);
|
||||
} catch (_err) {
|
||||
const err = getErrorFromUnknown(_err);
|
||||
handleError(err);
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
ref={ref}
|
||||
onSubmit={(event) => {
|
||||
form
|
||||
.handleSubmit(handleSubmit)(event)
|
||||
.catch((err) => {
|
||||
showToast(`${getErrorFromUnknown(err).message}`, "error");
|
||||
});
|
||||
}}
|
||||
{...passThrough}>
|
||||
{props.children}
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
|
||||
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
|
||||
) => ReactElement;
|
||||
|
||||
export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) {
|
||||
return (
|
||||
|
|
|
@ -4,6 +4,9 @@ interface UsernameInputProps extends React.ComponentPropsWithRef<"input"> {
|
|||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use <TextField addOnLeading={}> to achieve the same effect.
|
||||
*/
|
||||
const UsernameInput = React.forwardRef<HTMLInputElement, UsernameInputProps>((props, ref) => (
|
||||
// todo, check if username is already taken here?
|
||||
<div>
|
||||
|
|
|
@ -3,9 +3,10 @@ import dayjs, { Dayjs } from "dayjs";
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { Controller, useFieldArray } from "react-hook-form";
|
||||
|
||||
import { defaultDayRange } from "@lib/availability";
|
||||
import { weekdayNames } from "@lib/core/i18n/weekday";
|
||||
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 Select from "@components/ui/form/Select";
|
||||
|
@ -30,22 +31,6 @@ const TIMES = (() => {
|
|||
})();
|
||||
/** 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 = {
|
||||
readonly label: string;
|
||||
readonly value: number;
|
||||
|
@ -139,7 +124,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
|
|||
onChange={(e) => (e.target.checked ? replace([defaultDayRange]) : replace([]))}
|
||||
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>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
|
@ -157,7 +142,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
|
|||
/>
|
||||
</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>
|
||||
<Button
|
||||
|
|
47
lib/availability.ts
Normal file
47
lib/availability.ts
Normal 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[]);
|
||||
}
|
|
@ -39,6 +39,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
email: userEmail,
|
||||
},
|
||||
],
|
||||
AND: [
|
||||
{
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Availability } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getAvailabilityFromSchedule } from "@lib/availability";
|
||||
import prisma from "@lib/prisma";
|
||||
import { TimeRange } from "@lib/types/schedule";
|
||||
|
||||
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;
|
||||
if (!userId) {
|
||||
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." });
|
||||
}
|
||||
|
||||
const availability = req.body.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[]
|
||||
);
|
||||
const availability = getAvailabilityFromSchedule(req.body.schedule);
|
||||
|
||||
if (req.method === "POST") {
|
||||
try {
|
||||
await prisma.availability.deleteMany({
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
userId,
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
await Promise.all(
|
||||
availability.map((schedule: Availability) =>
|
||||
prisma.availability.create({
|
||||
data: {
|
||||
days: schedule.days,
|
||||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
availability: {
|
||||
/* We delete user availabilty */
|
||||
deleteMany: {
|
||||
userId: {
|
||||
equals: 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({
|
||||
message: "created",
|
||||
});
|
||||
|
|
|
@ -1,43 +1,50 @@
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { signIn } from "next-auth/client";
|
||||
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 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 { UsernameInput } from "@components/ui/UsernameInput";
|
||||
import ErrorAlert from "@components/ui/alerts/Error";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
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 router = useRouter();
|
||||
const methods = useForm<FormValues>();
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const [hasErrors, setHasErrors] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
methods.setValue("email", email);
|
||||
|
||||
const handleErrors = async (resp) => {
|
||||
const handleErrors = async (resp: Response) => {
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
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", {
|
||||
const signUp: SubmitHandler<FormValues> = async (data) => {
|
||||
await fetch("/api/auth/signup", {
|
||||
body: JSON.stringify({
|
||||
username: e.target.username.value,
|
||||
password,
|
||||
email,
|
||||
...data,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -45,104 +52,96 @@ export default function Signup(props) {
|
|||
method: "POST",
|
||||
})
|
||||
.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) => {
|
||||
setHasErrors(true);
|
||||
setErrorMessage(err.message);
|
||||
methods.setError("apiError", { message: err.message });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<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"
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||
<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")}
|
||||
</h2>
|
||||
</div>
|
||||
<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">
|
||||
<form method="POST" onSubmit={signUp} className="bg-white space-y-6">
|
||||
{hasErrors && <ErrorAlert message={errorMessage} />}
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<UsernameInput required />
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
inputMode="email"
|
||||
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"
|
||||
<div className="px-4 py-8 mx-2 bg-white shadow sm:rounded-lg sm:px-10">
|
||||
{/* TODO: Refactor as soon as /availability is live */}
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(signUp)} className="space-y-6 bg-white">
|
||||
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
|
||||
<div className="space-y-2">
|
||||
<TextField
|
||||
addOnLeading={
|
||||
<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">
|
||||
{process.env.NEXT_PUBLIC_APP_URL}/
|
||||
</span>
|
||||
}
|
||||
labelProps={{ className: "block text-sm font-medium text-gray-700" }}
|
||||
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"
|
||||
{...register("username")}
|
||||
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>
|
||||
<label htmlFor="passwordcheck" className="block text-sm font-medium text-gray-700">
|
||||
{t("confirm_password")}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="passwordcheck"
|
||||
id="passwordcheck"
|
||||
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"
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<Button loading={isSubmitting} className="justify-center w-7/12">
|
||||
{t("create_account")}
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="justify-center w-5/12"
|
||||
onClick={() =>
|
||||
signIn("Cal.com", { callbackUrl: (router.query.callbackUrl || "") as string })
|
||||
}>
|
||||
{t("login_instead")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-4 flex">
|
||||
<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>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(ctx) {
|
||||
if (!ctx.query.token) {
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const token = asStringOrNull(ctx.query.token);
|
||||
if (!token) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const verificationRequest = await prisma.verificationRequest.findUnique({
|
||||
where: {
|
||||
token: ctx.query.token,
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -175,4 +174,4 @@ export async function getServerSideProps(ctx) {
|
|||
}
|
||||
|
||||
return { props: { email: verificationRequest.identifier } };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { DEFAULT_SCHEDULE } from "@lib/availability";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import showToast from "@lib/notification";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
@ -10,7 +11,7 @@ import { Schedule as ScheduleType } from "@lib/types/schedule";
|
|||
import Shell from "@components/Shell";
|
||||
import { Form } from "@components/form/fields";
|
||||
import Button from "@components/ui/Button";
|
||||
import Schedule, { DEFAULT_SCHEDULE } from "@components/ui/form/Schedule";
|
||||
import Schedule from "@components/ui/form/Schedule";
|
||||
|
||||
type FormValues = {
|
||||
schedule: ScheduleType;
|
||||
|
|
|
@ -16,6 +16,7 @@ import { useForm } from "react-hook-form";
|
|||
import TimezoneSelect from "react-timezone-select";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { DEFAULT_SCHEDULE } from "@lib/availability";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import getIntegrations from "@lib/integrations/getIntegrations";
|
||||
import prisma from "@lib/prisma";
|
||||
|
@ -29,7 +30,7 @@ import { CalendarListContainer } from "@components/integrations/CalendarListCont
|
|||
import { Alert } from "@components/ui/Alert";
|
||||
import Button from "@components/ui/Button";
|
||||
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 getConnectedCalendars from "@server/integrations/getConnectedCalendars";
|
||||
|
@ -313,7 +314,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
title: t("set_availability"),
|
||||
description: t("set_availability_instructions"),
|
||||
Component: (
|
||||
<Form
|
||||
<Form<ScheduleFormValues>
|
||||
className="max-w-lg mx-auto text-black bg-white dark:bg-opacity-5 dark:text-white"
|
||||
form={availabilityForm}
|
||||
handleSubmit={async (values) => {
|
||||
|
|
|
@ -245,7 +245,7 @@
|
|||
"troubleshoot_availability": "Troubleshoot your availability to explore why your times are showing as they are.",
|
||||
"change_available_times": "Change 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.",
|
||||
"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.",
|
||||
|
@ -254,7 +254,10 @@
|
|||
"dark": "Dark",
|
||||
"automatically_adjust_theme": "Automatically adjust theme based on invitee preferences",
|
||||
"email": "Email",
|
||||
"email_placeholder": "jdoe@example.com",
|
||||
"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",
|
||||
"booking_cancelled": "Booking Cancelled",
|
||||
"booking_rescheduled": "Booking Rescheduled",
|
||||
|
@ -463,7 +466,7 @@
|
|||
"manage_your_billing_info": "Manage your billing information and cancel your subscription.",
|
||||
"availability": "Availability",
|
||||
"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",
|
||||
"logo": "Logo",
|
||||
"error": "Error",
|
||||
|
@ -525,5 +528,7 @@
|
|||
"connect_an_additional_calendar": "Connect an additional calendar",
|
||||
"conferencing": "Conferencing",
|
||||
"calendar": "Calendar",
|
||||
"not_installed": "Not installed"
|
||||
"not_installed": "Not installed",
|
||||
"error_password_mismatch": "Passwords don't match.",
|
||||
"error_required_field": "This field is required."
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import dayjs from "dayjs";
|
|||
import { uuid } from "short-uuid";
|
||||
|
||||
import { hashPassword } from "../lib/auth";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "../lib/availability";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
@ -26,6 +27,11 @@ async function createUserAndEventType(opts: {
|
|||
password: await hashPassword(opts.user.password),
|
||||
emailVerified: new Date(),
|
||||
completedOnboarding: opts.user.completedOnboarding ?? true,
|
||||
availability: {
|
||||
createMany: {
|
||||
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
|
||||
},
|
||||
},
|
||||
};
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: opts.user.email },
|
||||
|
|
Loading…
Reference in a new issue