Calendly & SavvyCal import (#1512)
* Calendly & SavvyCal import * added string keys to import * Update pages/api/import/savvycal.ts Co-authored-by: Omar López <zomars@me.com> * Update pages/api/import/savvycal.ts Co-authored-by: Omar López <zomars@me.com> * Update pages/getting-started.tsx Co-authored-by: Omar López <zomars@me.com> * fixed string * prettier Co-authored-by: Peer Richelsen <peeroke@richelsen.net> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
parent
b5569c6b1c
commit
33694196e1
4 changed files with 274 additions and 37 deletions
78
pages/api/import/calendly.ts
Normal file
78
pages/api/import/calendly.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const authenticatedUser = await prisma.user.findFirst({
|
||||
rejectOnNotFound: true,
|
||||
where: {
|
||||
id: session?.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (req.method == "POST") {
|
||||
const userResult = await fetch("https://api.calendly.com/users/me", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
});
|
||||
|
||||
if (userResult.status == 200) {
|
||||
const userData = await userResult.json();
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
name: userData.resource.name,
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesResult = await fetch(
|
||||
"https://api.calendly.com/event_types?user=" + userData.resource.uri,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const eventTypesData = await eventTypesResult.json();
|
||||
|
||||
eventTypesData.collection.forEach(async (eventType: any) => {
|
||||
await prisma.eventType.create({
|
||||
data: {
|
||||
title: eventType.name,
|
||||
slug: eventType.slug,
|
||||
length: eventType.duration,
|
||||
description: eventType.description_plain,
|
||||
hidden: eventType.secret,
|
||||
users: {
|
||||
connect: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
},
|
||||
userId: authenticatedUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
res.status(201).end();
|
||||
} else {
|
||||
res.status(500).end();
|
||||
}
|
||||
} else {
|
||||
res.status(405).end();
|
||||
}
|
||||
}
|
78
pages/api/import/savvycal.ts
Normal file
78
pages/api/import/savvycal.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const authenticatedUser = await prisma.user.findFirst({
|
||||
rejectOnNotFound: true,
|
||||
where: {
|
||||
id: session?.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (req.method === "POST") {
|
||||
const userResult = await fetch("https://api.savvycal.com/v1/me", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
});
|
||||
|
||||
if (userResult.status === 200) {
|
||||
const userData = await userResult.json();
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
name: userData.display_name,
|
||||
timeZone: userData.time_zone,
|
||||
weekStart: userData.first_day_of_week === 0 ? "Sunday" : "Monday",
|
||||
avatar: userData.avatar_url,
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesResult = await fetch("https://api.savvycal.com/v1/links?limit=100", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesData = await eventTypesResult.json();
|
||||
|
||||
eventTypesData.entries.forEach(async (eventType: any) => {
|
||||
await prisma.eventType.create({
|
||||
data: {
|
||||
title: eventType.name,
|
||||
slug: eventType.slug,
|
||||
length: eventType.durations[0],
|
||||
description: eventType.description.replace(/<[^>]*>?/gm, ""),
|
||||
hidden: eventType.state === "active" ? true : false,
|
||||
users: {
|
||||
connect: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
},
|
||||
userId: authenticatedUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
res.status(201).end();
|
||||
} else {
|
||||
res.status(500).end();
|
||||
}
|
||||
} else {
|
||||
res.status(405).end();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import classnames from "classnames";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -14,6 +15,7 @@ import { useRouter } from "next/router";
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import TimezoneSelect from "react-timezone-select";
|
||||
import * as z from "zod";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { DEFAULT_SCHEDULE } from "@lib/availability";
|
||||
|
@ -71,6 +73,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
const { status } = useSession();
|
||||
const loading = status === "loading";
|
||||
const [ready, setReady] = useState(false);
|
||||
const [selectedImport, setSelectedImport] = useState("");
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const updateUser = async (data: Prisma.UserUpdateInput) => {
|
||||
|
@ -229,6 +232,14 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
router.push("/event-types");
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
const formMethods = useForm<{
|
||||
token: string;
|
||||
}>({ resolver: zodResolver(schema), mode: "onSubmit" });
|
||||
|
||||
const availabilityForm = useForm({ defaultValues: { schedule: DEFAULT_SCHEDULE } });
|
||||
const steps = [
|
||||
{
|
||||
|
@ -236,6 +247,71 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
title: t("welcome_to_calcom"),
|
||||
description: t("welcome_instructions"),
|
||||
Component: (
|
||||
<>
|
||||
{selectedImport == "" && (
|
||||
<div className="grid grid-cols-2 mb-4 gap-x-4">
|
||||
<Button color="secondary" onClick={() => setSelectedImport("calendly")}>
|
||||
{t("import_from")} Calendly
|
||||
</Button>
|
||||
<Button color="secondary" onClick={() => setSelectedImport("savvycal")}>
|
||||
{t("import_from")} SavvyCal
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{selectedImport && (
|
||||
<div>
|
||||
<h2 className="text-2xl text-gray-900 font-cal">
|
||||
{t("import_from")} {selectedImport === "calendly" ? "Calendly" : "SavvyCal"}
|
||||
</h2>
|
||||
<p className="mb-2 text-sm text-gray-500">{t("you_will_need_to_generate")}</p>
|
||||
<form
|
||||
className="flex"
|
||||
onSubmit={formMethods.handleSubmit(async (values) => {
|
||||
setSubmitting(true);
|
||||
const response = await fetch(`/api/import/${selectedImport}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
token: values.token,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (response.status === 201) {
|
||||
setSubmitting(false);
|
||||
handleSkipStep();
|
||||
} else {
|
||||
await response.json().catch((e) => {
|
||||
console.log("Error: response.json invalid: " + e);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}
|
||||
})}>
|
||||
<input
|
||||
onChange={async (e) => {
|
||||
formMethods.setValue("token", e.target.value);
|
||||
}}
|
||||
type="text"
|
||||
name="token"
|
||||
id="token"
|
||||
placeholder={t("access_token")}
|
||||
required
|
||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
<Button type="submit" className="h-10 mt-1 ml-4">
|
||||
{t("import")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative my-4">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="px-2 text-sm text-gray-500 bg-white">or</span>
|
||||
</div>
|
||||
</div>
|
||||
<form className="sm:mx-auto sm:w-full">
|
||||
<section className="space-y-8">
|
||||
<fieldset>
|
||||
|
@ -274,6 +350,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
</fieldset>
|
||||
</section>
|
||||
</form>
|
||||
</>
|
||||
),
|
||||
hideConfirm: false,
|
||||
confirmText: t("continue"),
|
||||
|
|
|
@ -595,5 +595,9 @@
|
|||
"saml_configuration_update_failed": "SAML configuration update failed",
|
||||
"saml_configuration_delete_failed": "SAML configuration delete failed",
|
||||
"saml_email_required": "Please enter an email so we can find your SAML Identity Provider",
|
||||
"you_will_need_to_generate": "You will need to generate an access token from the integrations page.",
|
||||
"import": "Import",
|
||||
"import_from": "Import from",
|
||||
"access_token": "Access token",
|
||||
"visit_roadmap": "Roadmap"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue