Skip to content

Commit

Permalink
Calendly & SavvyCal import (calcom#1512)
Browse files Browse the repository at this point in the history
* Calendly & SavvyCal import

* added string keys to import

* Update pages/api/import/savvycal.ts

Co-authored-by: Omar López <[email protected]>

* Update pages/api/import/savvycal.ts

Co-authored-by: Omar López <[email protected]>

* Update pages/getting-started.tsx

Co-authored-by: Omar López <[email protected]>

* fixed string

* prettier

Co-authored-by: Peer Richelsen <[email protected]>
Co-authored-by: Omar López <[email protected]>
Co-authored-by: Peer Richelsen <[email protected]>
  • Loading branch information
4 people authored Jan 15, 2022
1 parent b5569c6 commit 3369419
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 37 deletions.
78 changes: 78 additions & 0 deletions pages/api/import/calendly.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 78 additions & 0 deletions pages/api/import/savvycal.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
151 changes: 114 additions & 37 deletions pages/getting-started.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -229,51 +232,125 @@ 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 = [
{
id: t("welcome"),
title: t("welcome_to_calcom"),
description: t("welcome_instructions"),
Component: (
<form className="sm:mx-auto sm:w-full">
<section className="space-y-8">
<fieldset>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
{t("full_name")}
</label>
<input
ref={nameRef}
type="text"
name="name"
id="name"
autoComplete="given-name"
placeholder={t("your_name")}
defaultValue={props.user.name ?? enteredName}
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"
/>
</fieldset>

<fieldset>
<section className="flex justify-between">
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
<>
{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>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
{t("full_name")}
</label>
<Text variant="caption">
{t("current_time")}:&nbsp;
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
</Text>
</section>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</fieldset>
</section>
</form>
<input
ref={nameRef}
type="text"
name="name"
id="name"
autoComplete="given-name"
placeholder={t("your_name")}
defaultValue={props.user.name ?? enteredName}
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"
/>
</fieldset>

<fieldset>
<section className="flex justify-between">
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Text variant="caption">
{t("current_time")}:&nbsp;
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
</Text>
</section>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</fieldset>
</section>
</form>
</>
),
hideConfirm: false,
confirmText: t("continue"),
Expand Down
4 changes: 4 additions & 0 deletions public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

0 comments on commit 3369419

Please sign in to comment.