Fix/duplicate events on onboarding (#716)
This commit is contained in:
parent
d4f29464f2
commit
a047177e72
6 changed files with 115 additions and 16 deletions
10
lib/queries/event-types/get-event-types.ts
Normal file
10
lib/queries/event-types/get-event-types.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
|
import { EventType } from "@prisma/client";
|
||||||
|
|
||||||
|
type GetEventsResponse = { message: string; data: EventType[] };
|
||||||
|
const getEventTypes = async () => {
|
||||||
|
const response = await fetch.get<GetEventsResponse>("/api/event-type");
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getEventTypes;
|
|
@ -72,6 +72,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/node": "^16.6.1",
|
"@types/node": "^16.6.1",
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode": "^1.4.1",
|
"@types/qrcode": "^1.4.1",
|
||||||
|
|
44
pages/api/event-type/index.ts
Normal file
44
pages/api/event-type/index.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { getSession } from "@lib/auth";
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const session = await getSession({ req: req });
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.user?.id) {
|
||||||
|
console.error("Session is missing a user id");
|
||||||
|
return res.status(500).json({ message: "Something went wrong" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
eventTypes: {
|
||||||
|
where: {
|
||||||
|
team: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
length: true,
|
||||||
|
schedulingType: true,
|
||||||
|
slug: true,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Events.", data: user.eventTypes });
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
"theme",
|
"theme",
|
||||||
"completedOnboarding",
|
"completedOnboarding",
|
||||||
]),
|
]),
|
||||||
bio: req.body.description,
|
bio: req.body.description ?? req.body.data?.bio,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
|
@ -30,6 +30,9 @@ import classnames from "classnames";
|
||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
import Loader from "@components/Loader";
|
||||||
|
import getEventTypes from "../lib/queries/event-types/get-event-types";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
@ -63,6 +66,7 @@ type OnboardingProps = {
|
||||||
export default function Onboarding(props: OnboardingProps) {
|
export default function Onboarding(props: OnboardingProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isSubmitting, setSubmitting] = React.useState(false);
|
||||||
const [enteredName, setEnteredName] = React.useState();
|
const [enteredName, setEnteredName] = React.useState();
|
||||||
const Sess = useSession();
|
const Sess = useSession();
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
@ -116,7 +120,7 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
return responseData.data;
|
return responseData.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const integrationHandler = (type: string) => {
|
const handleAddIntegration = (type: string) => {
|
||||||
if (type === "caldav_calendar") {
|
if (type === "caldav_calendar") {
|
||||||
setAddCalDavError(null);
|
setAddCalDavError(null);
|
||||||
setIsAddCalDavIntegrationDialogOpen(true);
|
setIsAddCalDavIntegrationDialogOpen(true);
|
||||||
|
@ -137,7 +141,7 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li onClick={() => integrationHandler(integration.type)} key={integration.type} className="flex py-4">
|
<li onClick={() => handleAddIntegration(integration.type)} key={integration.type} className="flex py-4">
|
||||||
<div className="w-1/12 mr-4 pt-2">
|
<div className="w-1/12 mr-4 pt-2">
|
||||||
<img className="h-8 w-8 mr-2" src={integration.imageSrc} alt={integration.title} />
|
<img className="h-8 w-8 mr-2" src={integration.imageSrc} alt={integration.title} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -148,7 +152,7 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/12 text-right pt-2">
|
<div className="w-2/12 text-right pt-2">
|
||||||
<Button color="secondary" onClick={() => integrationHandler(integration.type)}>
|
<Button color="secondary" onClick={() => handleAddIntegration(integration.type)}>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -280,6 +284,7 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
|
|
||||||
const handleConfirmStep = async () => {
|
const handleConfirmStep = async () => {
|
||||||
try {
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
if (
|
if (
|
||||||
steps[currentStep] &&
|
steps[currentStep] &&
|
||||||
steps[currentStep]?.onComplete &&
|
steps[currentStep]?.onComplete &&
|
||||||
|
@ -288,12 +293,16 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
await steps[currentStep].onComplete();
|
await steps[currentStep].onComplete();
|
||||||
}
|
}
|
||||||
incrementStep();
|
incrementStep();
|
||||||
|
setSubmitting(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("handleConfirmStep", error);
|
console.log("handleConfirmStep", error);
|
||||||
|
setSubmitting(false);
|
||||||
setError(error);
|
setError(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const debouncedHandleConfirmStep = debounce(handleConfirmStep, 850);
|
||||||
|
|
||||||
const handleSkipStep = () => {
|
const handleSkipStep = () => {
|
||||||
incrementStep();
|
incrementStep();
|
||||||
};
|
};
|
||||||
|
@ -331,17 +340,22 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
* then the default availability is applied.
|
* then the default availability is applied.
|
||||||
*/
|
*/
|
||||||
const completeOnboarding = async () => {
|
const completeOnboarding = async () => {
|
||||||
|
setSubmitting(true);
|
||||||
if (!props.eventTypes || props.eventTypes.length === 0) {
|
if (!props.eventTypes || props.eventTypes.length === 0) {
|
||||||
Promise.all(
|
const eventTypes = await getEventTypes();
|
||||||
DEFAULT_EVENT_TYPES.map(async (event) => {
|
if (eventTypes.length === 0) {
|
||||||
return await createEventType(event);
|
Promise.all(
|
||||||
})
|
DEFAULT_EVENT_TYPES.map(async (event) => {
|
||||||
);
|
return await createEventType(event);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await updateUser({
|
await updateUser({
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
router.push("/event-types");
|
router.push("/event-types");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -365,7 +379,7 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
id="name"
|
id="name"
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
placeholder="Your name"
|
placeholder="Your name"
|
||||||
defaultValue={props.user.name}
|
defaultValue={props.user.name ?? enteredName}
|
||||||
required
|
required
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
|
@ -397,13 +411,16 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
cancelText: "Set up later",
|
cancelText: "Set up later",
|
||||||
onComplete: async () => {
|
onComplete: async () => {
|
||||||
try {
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
await updateUser({
|
await updateUser({
|
||||||
name: nameRef.current.value,
|
name: nameRef.current.value,
|
||||||
timeZone: selectedTimeZone.value,
|
timeZone: selectedTimeZone.value,
|
||||||
});
|
});
|
||||||
setEnteredName(nameRef.current.value);
|
setEnteredName(nameRef.current.value);
|
||||||
|
setSubmitting(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -435,10 +452,12 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
<SchedulerForm
|
<SchedulerForm
|
||||||
onSubmit={async (data) => {
|
onSubmit={async (data) => {
|
||||||
try {
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
await createSchedule({
|
await createSchedule({
|
||||||
freeBusyTimes: data,
|
freeBusyTimes: data,
|
||||||
});
|
});
|
||||||
handleConfirmStep();
|
debouncedHandleConfirmStep();
|
||||||
|
setSubmitting(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
}
|
}
|
||||||
|
@ -505,11 +524,15 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
cancelText: "Set up later",
|
cancelText: "Set up later",
|
||||||
onComplete: async () => {
|
onComplete: async () => {
|
||||||
try {
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
console.log("updating");
|
||||||
await updateUser({
|
await updateUser({
|
||||||
bio: bioRef.current.value,
|
description: bioRef.current.value,
|
||||||
});
|
});
|
||||||
|
setSubmitting(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -532,8 +555,13 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
{isSubmitting && (
|
||||||
|
<div className="fixed w-full h-full bg-white bg-opacity-25 flex flex-col justify-center items-center content-center z-10">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mx-auto py-24 px-4">
|
<div className="mx-auto py-24 px-4">
|
||||||
<article>
|
<article className="relative">
|
||||||
<section className="sm:mx-auto sm:w-full sm:max-w-md space-y-4">
|
<section className="sm:mx-auto sm:w-full sm:max-w-md space-y-4">
|
||||||
<header className="">
|
<header className="">
|
||||||
<Text className="text-white" variant="largetitle">
|
<Text className="text-white" variant="largetitle">
|
||||||
|
@ -572,7 +600,11 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
|
|
||||||
{!steps[currentStep].hideConfirm && (
|
{!steps[currentStep].hideConfirm && (
|
||||||
<footer className="py-6 sm:mx-auto sm:w-full sm:max-w-md flex flex-col space-y-6 mt-8">
|
<footer className="py-6 sm:mx-auto sm:w-full sm:max-w-md flex flex-col space-y-6 mt-8">
|
||||||
<Button className="justify-center" onClick={handleConfirmStep} EndIcon={ArrowRightIcon}>
|
<Button
|
||||||
|
className="justify-center"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={debouncedHandleConfirmStep}
|
||||||
|
EndIcon={ArrowRightIcon}>
|
||||||
{steps[currentStep].confirmText}
|
{steps[currentStep].confirmText}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -580,11 +612,11 @@ export default function Onboarding(props: OnboardingProps) {
|
||||||
</section>
|
</section>
|
||||||
<section className="py-6 mt-8 mx-auto max-w-xl">
|
<section className="py-6 mt-8 mx-auto max-w-xl">
|
||||||
<div className="flex justify-between flex-row-reverse">
|
<div className="flex justify-between flex-row-reverse">
|
||||||
<button onClick={handleSkipStep}>
|
<button disabled={isSubmitting} onClick={handleSkipStep}>
|
||||||
<Text variant="caption">Skip Step</Text>
|
<Text variant="caption">Skip Step</Text>
|
||||||
</button>
|
</button>
|
||||||
{currentStep !== 0 && (
|
{currentStep !== 0 && (
|
||||||
<button onClick={decrementStep}>
|
<button disabled={isSubmitting} onClick={decrementStep}>
|
||||||
<Text variant="caption">Prev Step</Text>
|
<Text variant="caption">Prev Step</Text>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -1371,6 +1371,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
||||||
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
|
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
|
||||||
|
|
||||||
|
"@types/lodash.debounce@^4.0.6":
|
||||||
|
version "4.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
|
||||||
|
integrity sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*":
|
||||||
|
version "4.14.173"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.173.tgz#9d3b674c67a26cf673756f6aca7b429f237f91ed"
|
||||||
|
integrity sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==
|
||||||
|
|
||||||
"@types/lodash@^4.14.165":
|
"@types/lodash@^4.14.165":
|
||||||
version "4.14.172"
|
version "4.14.172"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a"
|
||||||
|
|
Loading…
Reference in a new issue