Onboarding Flow (#503)
* wip * wip * db: schedule schema * fix adding time goes into new day fix adding new time not keeping updating ranges fix updating ranges not maintaining changed values * remove photo upload * remove unused code * remove more unused code * undo time lib * didnt actually change this * dont show onboardi ng flow for users created before sept 1 2021 * use more consistent max-widths * align all inputs in scheduler component * allow overriding of default styles * match figma designs implement goto previous step * add more types, match figma Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
1 parent
f63c9311e5
commit
2d23a8b7db
24 files changed
+1358
-158
No files matched your search
@@ -0,0 +1,724 @@
|
||||
import Head from "next/head";
|
||||
import prisma from "@lib/prisma";
|
||||
import { getSession, useSession } from "next-auth/client";
|
||||
import {
|
||||
EventTypeCreateInput,
|
||||
ScheduleCreateInput,
|
||||
User,
|
||||
UserUpdateInput,
|
||||
EventType,
|
||||
Schedule,
|
||||
} from "@prisma/client";
|
||||
import { NextPageContext } from "next";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { validJson } from "@lib/jsonUtils";
|
||||
import TimezoneSelect from "react-timezone-select";
|
||||
import Text from "@components/ui/Text";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
import AddCalDavIntegration, {
|
||||
ADD_CALDAV_INTEGRATION_FORM_TITLE,
|
||||
} from "@lib/integrations/CalDav/components/AddCalDavIntegration";
|
||||
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@components/Dialog";
|
||||
import SchedulerForm, { SCHEDULE_FORM_ID } from "@components/ui/Schedule/Schedule";
|
||||
import { useRouter } from "next/router";
|
||||
import { Integration } from "pages/integrations";
|
||||
import { AddCalDavIntegrationRequest } from "../../lib/integrations/CalDav/components/AddCalDavIntegration";
|
||||
import classnames from "classnames";
|
||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
|
||||
const DEFAULT_EVENT_TYPES = [
|
||||
{
|
||||
title: "15 Min Meeting",
|
||||
slug: "15min",
|
||||
length: 15,
|
||||
},
|
||||
{
|
||||
title: "30 Min Meeting",
|
||||
slug: "30min",
|
||||
length: 30,
|
||||
},
|
||||
{
|
||||
title: "Secret Meeting",
|
||||
slug: "secret",
|
||||
length: 15,
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString();
|
||||
|
||||
type OnboardingProps = {
|
||||
user: User;
|
||||
integrations?: Record<string, string>[];
|
||||
eventTypes?: EventType[];
|
||||
schedules?: Schedule[];
|
||||
};
|
||||
|
||||
export default function Onboarding(props: OnboardingProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [enteredName, setEnteredName] = React.useState();
|
||||
const Sess = useSession();
|
||||
const [ready, setReady] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const updateUser = async (data: UserUpdateInput) => {
|
||||
const res = await fetch(`/api/user/${props.user.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ data: { ...data } }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error((await res.json()).message);
|
||||
}
|
||||
const responseData = await res.json();
|
||||
return responseData.data;
|
||||
};
|
||||
|
||||
const createEventType = async (data: EventTypeCreateInput) => {
|
||||
const res = await fetch(`/api/availability/eventtype`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error((await res.json()).message);
|
||||
}
|
||||
const responseData = await res.json();
|
||||
return responseData.data;
|
||||
};
|
||||
|
||||
const createSchedule = async (data: ScheduleCreateInput) => {
|
||||
const res = await fetch(`/api/schedule`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ data: { ...data } }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error((await res.json()).message);
|
||||
}
|
||||
const responseData = await res.json();
|
||||
return responseData.data;
|
||||
};
|
||||
|
||||
const integrationHandler = (type: string) => {
|
||||
if (type === "caldav_calendar") {
|
||||
setAddCalDavError(null);
|
||||
setIsAddCalDavIntegrationDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/api/integrations/" + type.replace("_", "") + "/add")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
window.location.href = data.url;
|
||||
});
|
||||
};
|
||||
|
||||
/** Internal Components */
|
||||
const IntegrationGridListItem = ({ integration }: { integration: Integration }) => {
|
||||
if (!integration || !integration.installed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li onClick={() => integrationHandler(integration.type)} key={integration.type} className="flex py-4">
|
||||
<div className="w-1/12 mr-4 pt-2">
|
||||
<img className="h-8 w-8 mr-2" src={integration.imageSrc} alt={integration.title} />
|
||||
</div>
|
||||
<div className="w-10/12">
|
||||
<Text className="text-gray-800 font-medium">{integration.title}</Text>
|
||||
<Text className="text-gray-400" variant="subtitle">
|
||||
{integration.description}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="w-2/12 text-right pt-2">
|
||||
<button
|
||||
onClick={() => integrationHandler(integration.type)}
|
||||
className="font-medium text-neutral-900 hover:text-neutral-500 border px-4 py-2 border-gray-200 rounded-sm">
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
/** End Internal Components */
|
||||
|
||||
/** Name */
|
||||
const nameRef = useRef(null);
|
||||
const bioRef = useRef(null);
|
||||
/** End Name */
|
||||
/** TimeZone */
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState({
|
||||
value: props.user.timeZone ?? dayjs.tz.guess(),
|
||||
label: null,
|
||||
});
|
||||
const currentTime = React.useMemo(() => {
|
||||
return dayjs().tz(selectedTimeZone.value).format("H:mm A");
|
||||
}, [selectedTimeZone]);
|
||||
/** End TimeZone */
|
||||
|
||||
/** CalDav Form */
|
||||
const addCalDavIntegrationRef = useRef<HTMLFormElement>(null);
|
||||
const [isAddCalDavIntegrationDialogOpen, setIsAddCalDavIntegrationDialogOpen] = useState(false);
|
||||
const [addCalDavError, setAddCalDavError] = useState<{ message: string } | null>(null);
|
||||
|
||||
const handleAddCalDavIntegration = async ({ url, username, password }: AddCalDavIntegrationRequest) => {
|
||||
const requestBody = JSON.stringify({
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
return await fetch("/api/integrations/caldav/add", {
|
||||
method: "POST",
|
||||
body: requestBody,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddCalDavIntegrationSaveButtonPress = async () => {
|
||||
const form = addCalDavIntegrationRef.current.elements;
|
||||
const url = form.url.value;
|
||||
const password = form.password.value;
|
||||
const username = form.username.value;
|
||||
|
||||
try {
|
||||
setAddCalDavError(null);
|
||||
const addCalDavIntegrationResponse = await handleAddCalDavIntegration({ username, password, url });
|
||||
if (addCalDavIntegrationResponse.ok) {
|
||||
setIsAddCalDavIntegrationDialogOpen(false);
|
||||
incrementStep();
|
||||
} else {
|
||||
const j = await addCalDavIntegrationResponse.json();
|
||||
setAddCalDavError({ message: j.message });
|
||||
}
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
}
|
||||
};
|
||||
|
||||
const ConnectCalDavServerDialog = () => {
|
||||
return (
|
||||
<Dialog
|
||||
open={isAddCalDavIntegrationDialogOpen}
|
||||
onOpenChange={(isOpen) => setIsAddCalDavIntegrationDialogOpen(isOpen)}>
|
||||
<DialogContent>
|
||||
<DialogHeader
|
||||
title="Connect to CalDav Server"
|
||||
subtitle="Your credentials will be stored and encrypted."
|
||||
/>
|
||||
<div className="my-4">
|
||||
{addCalDavError && (
|
||||
<p className="text-red-700 text-sm">
|
||||
<span className="font-bold">Error: </span>
|
||||
{addCalDavError.message}
|
||||
</p>
|
||||
)}
|
||||
<AddCalDavIntegration
|
||||
ref={addCalDavIntegrationRef}
|
||||
onSubmit={handleAddCalDavIntegrationSaveButtonPress}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="submit"
|
||||
form={ADD_CALDAV_INTEGRATION_FORM_TITLE}
|
||||
className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
|
||||
Save
|
||||
</button>
|
||||
<DialogClose
|
||||
onClick={() => {
|
||||
setIsAddCalDavIntegrationDialogOpen(false);
|
||||
}}
|
||||
as="button"
|
||||
className="btn btn-white mx-2">
|
||||
Cancel
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
/**End CalDav Form */
|
||||
|
||||
/** Onboarding Steps */
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const detectStep = () => {
|
||||
let step = 0;
|
||||
const hasSetUserNameOrTimeZone = props.user.name && props.user.timeZone;
|
||||
if (hasSetUserNameOrTimeZone) {
|
||||
step = 1;
|
||||
}
|
||||
|
||||
const hasConfigureCalendar = props.integrations.some((integration) => integration.credential != null);
|
||||
if (hasConfigureCalendar) {
|
||||
step = 2;
|
||||
}
|
||||
|
||||
const hasSchedules = props.schedules && props.schedules.length > 0;
|
||||
if (hasSchedules) {
|
||||
step = 3;
|
||||
}
|
||||
|
||||
setCurrentStep(step);
|
||||
};
|
||||
|
||||
const handleConfirmStep = async () => {
|
||||
try {
|
||||
if (
|
||||
steps[currentStep] &&
|
||||
steps[currentStep]?.onComplete &&
|
||||
typeof steps[currentStep]?.onComplete === "function"
|
||||
) {
|
||||
await steps[currentStep].onComplete();
|
||||
}
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
console.log("handleConfirmStep", error);
|
||||
setError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipStep = () => {
|
||||
incrementStep();
|
||||
};
|
||||
|
||||
const incrementStep = () => {
|
||||
const nextStep = currentStep + 1;
|
||||
|
||||
if (nextStep >= steps.length) {
|
||||
completeOnboarding();
|
||||
return;
|
||||
}
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
const decrementStep = () => {
|
||||
const previous = currentStep - 1;
|
||||
|
||||
if (previous < 0) {
|
||||
return;
|
||||
}
|
||||
setCurrentStep(previous);
|
||||
};
|
||||
|
||||
const goToStep = (step: number) => {
|
||||
setCurrentStep(step);
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete Onboarding finalizes the onboarding flow for a new user.
|
||||
*
|
||||
* Here, 3 event types are pre-created for the user as well.
|
||||
* Set to the availability the user enter during the onboarding.
|
||||
*
|
||||
* If a user skips through the Onboarding flow,
|
||||
* then the default availability is applied.
|
||||
*/
|
||||
const completeOnboarding = async () => {
|
||||
if (!props.eventTypes || props.eventTypes.length === 0) {
|
||||
Promise.all(
|
||||
DEFAULT_EVENT_TYPES.map(async (event) => {
|
||||
return await createEventType(event);
|
||||
})
|
||||
);
|
||||
}
|
||||
await updateUser({
|
||||
completedOnboarding: true,
|
||||
});
|
||||
|
||||
router.push("/event-types");
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: "welcome",
|
||||
title: "Welcome to Calendso",
|
||||
description:
|
||||
"Tell us what to call you and let us know what timezone you’re in. You’ll be able to edit this later.",
|
||||
Component: (
|
||||
<form className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<section className="space-y-4">
|
||||
<fieldset>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Full name
|
||||
</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
autoComplete="given-name"
|
||||
placeholder="Your name"
|
||||
defaultValue={props.user.name}
|
||||
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"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<section className="flex justify-between">
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
Timezone
|
||||
</label>
|
||||
<Text variant="caption">
|
||||
Current time:
|
||||
<span className="text-black">{currentTime}</span>
|
||||
</Text>
|
||||
</section>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={setSelectedTimeZone}
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
/>
|
||||
</fieldset>
|
||||
</section>
|
||||
</form>
|
||||
),
|
||||
hideConfirm: false,
|
||||
confirmText: "Continue",
|
||||
showCancel: true,
|
||||
cancelText: "Set up later",
|
||||
onComplete: async () => {
|
||||
try {
|
||||
await updateUser({
|
||||
name: nameRef.current.value,
|
||||
timeZone: selectedTimeZone.value,
|
||||
});
|
||||
setEnteredName(nameRef.current.value);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "connect-calendar",
|
||||
title: "Connect your calendar",
|
||||
description:
|
||||
"Connect your calendar to automatically check for busy times and new events as they’re scheduled.",
|
||||
Component: (
|
||||
<ul className="divide-y divide-gray-200 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
{props.integrations.map((integration) => {
|
||||
return <IntegrationGridListItem key={integration.type} integration={integration} />;
|
||||
})}
|
||||
</ul>
|
||||
),
|
||||
hideConfirm: true,
|
||||
confirmText: "Continue",
|
||||
showCancel: true,
|
||||
cancelText: "Continue without calendar",
|
||||
},
|
||||
{
|
||||
id: "set-availability",
|
||||
title: "Set your availability",
|
||||
description:
|
||||
"Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.",
|
||||
Component: (
|
||||
<>
|
||||
<section className="bg-white dark:bg-opacity-5 text-black dark:text-white mx-auto max-w-lg">
|
||||
<SchedulerForm
|
||||
onSubmit={async (data) => {
|
||||
try {
|
||||
await createSchedule({
|
||||
freeBusyTimes: data,
|
||||
});
|
||||
handleConfirmStep();
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
<footer className="py-6 sm:mx-auto sm:w-full sm:max-w-md flex flex-col space-y-6">
|
||||
<button
|
||||
type="submit"
|
||||
form={SCHEDULE_FORM_ID}
|
||||
className="w-full btn btn-primary text-center justify-center space-x-2">
|
||||
<Text variant="subtitle" className="text-white">
|
||||
Continue
|
||||
</Text>
|
||||
<ArrowRightIcon className="text-white h-4 w-4" />
|
||||
</button>
|
||||
</footer>
|
||||
</>
|
||||
),
|
||||
hideConfirm: true,
|
||||
showCancel: false,
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
title: "Nearly there",
|
||||
description:
|
||||
"Last thing, a brief description about you and a photo really help you get bookings and let people know who they’re booking with.",
|
||||
Component: (
|
||||
<form className="sm:mx-auto sm:w-full sm:max-w-md" id="ONBOARDING_STEP_4">
|
||||
<section className="space-y-4">
|
||||
<fieldset>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Full name
|
||||
</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
autoComplete="given-name"
|
||||
placeholder="Your name"
|
||||
defaultValue={props.user.name || enteredName}
|
||||
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"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="bio" className="block text-sm font-medium text-gray-700">
|
||||
About
|
||||
</label>
|
||||
<input
|
||||
ref={bioRef}
|
||||
type="text"
|
||||
name="bio"
|
||||
id="bio"
|
||||
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"
|
||||
defaultValue={props.user.bio}
|
||||
/>
|
||||
<Text variant="caption">
|
||||
A few sentences about yourself. This will appear on your personal url page.
|
||||
</Text>
|
||||
</fieldset>
|
||||
</section>
|
||||
</form>
|
||||
),
|
||||
hideConfirm: false,
|
||||
confirmText: "Finish",
|
||||
showCancel: true,
|
||||
cancelText: "Set up later",
|
||||
onComplete: async () => {
|
||||
try {
|
||||
await updateUser({
|
||||
bio: bioRef.current.value,
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
/** End Onboarding Steps */
|
||||
|
||||
useEffect(() => {
|
||||
detectStep();
|
||||
setReady(true);
|
||||
}, []);
|
||||
|
||||
if (Sess[1] || !ready) {
|
||||
return <div className="loader"></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-black min-h-screen">
|
||||
<Head>
|
||||
<title>Calendso - Getting Started</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<div className="mx-auto py-24 px-4">
|
||||
<article>
|
||||
<section className="sm:mx-auto sm:w-full sm:max-w-md space-y-4">
|
||||
<header className="">
|
||||
<Text className="text-white" variant="largetitle">
|
||||
{steps[currentStep].title}
|
||||
</Text>
|
||||
<Text className="text-white" variant="subtitle">
|
||||
{steps[currentStep].description}
|
||||
</Text>
|
||||
</header>
|
||||
<section className="space-y-2">
|
||||
<Text variant="footnote">
|
||||
Step {currentStep + 1} of {steps.length}
|
||||
</Text>
|
||||
|
||||
<section className="w-full space-x-2 flex">
|
||||
{steps.map((s, index) => {
|
||||
return index <= currentStep ? (
|
||||
<div
|
||||
key={`step-${index}`}
|
||||
onClick={() => goToStep(index)}
|
||||
className={classnames(
|
||||
"h-1 bg-white w-1/4",
|
||||
index < currentStep ? "cursor-pointer" : ""
|
||||
)}></div>
|
||||
) : (
|
||||
<div key={`step-${index}`} className="h-1 bg-white bg-opacity-25 w-1/4"></div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section className="py-6 mt-10 mx-auto max-w-xl bg-white p-10 rounded-sm">
|
||||
{steps[currentStep].Component}
|
||||
|
||||
{!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">
|
||||
<button
|
||||
onClick={handleConfirmStep}
|
||||
type="button"
|
||||
className="w-full btn btn-primary text-center justify-center space-x-2">
|
||||
<Text variant="subtitle" className="text-white">
|
||||
{steps[currentStep].confirmText}
|
||||
</Text>
|
||||
<ArrowRightIcon className="text-white h-4 w-4" />
|
||||
</button>
|
||||
</footer>
|
||||
)}
|
||||
</section>
|
||||
<section className="py-6 mt-8 mx-auto max-w-xl">
|
||||
<div className="flex justify-between">
|
||||
<button onClick={decrementStep}>
|
||||
<Text variant="caption">Prev Step</Text>
|
||||
</button>
|
||||
<button onClick={handleSkipStep}>
|
||||
<Text variant="caption">Skip Step</Text>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<ConnectCalDavServerDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const session = await getSession(context);
|
||||
|
||||
let user: User = null;
|
||||
let integrations = [];
|
||||
let credentials = [];
|
||||
let eventTypes = [];
|
||||
let schedules = [];
|
||||
|
||||
if (session) {
|
||||
user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: session.user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
timeZone: true,
|
||||
completedOnboarding: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user.completedOnboarding) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/event-types",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
},
|
||||
});
|
||||
|
||||
integrations = [
|
||||
{
|
||||
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
|
||||
credential: credentials.find((integration) => integration.type === "google_calendar") || null,
|
||||
type: "google_calendar",
|
||||
title: "Google Calendar",
|
||||
imageSrc: "integrations/google-calendar.svg",
|
||||
description: "Gmail, G Suite",
|
||||
},
|
||||
{
|
||||
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
|
||||
credential: credentials.find((integration) => integration.type === "office365_calendar") || null,
|
||||
type: "office365_calendar",
|
||||
title: "Office 365 Calendar",
|
||||
imageSrc: "integrations/outlook.svg",
|
||||
description: "Office 365, Outlook.com, live.com, or hotmail calendar",
|
||||
},
|
||||
{
|
||||
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
|
||||
credential: credentials.find((integration) => integration.type === "zoom_video") || null,
|
||||
type: "zoom_video",
|
||||
title: "Zoom",
|
||||
imageSrc: "integrations/zoom.svg",
|
||||
description: "Video Conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
credential: credentials.find((integration) => integration.type === "caldav_calendar") || null,
|
||||
type: "caldav_calendar",
|
||||
title: "Caldav",
|
||||
imageSrc: "integrations/caldav.svg",
|
||||
description: "CalDav Server",
|
||||
},
|
||||
];
|
||||
|
||||
eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
},
|
||||
});
|
||||
|
||||
schedules = await prisma.schedule.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
user,
|
||||
integrations,
|
||||
eventTypes,
|
||||
schedules,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user