How to Build a Meeting Scheduler (Calendly Clone) in 30 Minutes
24
February
2021
Tutorial

How to Build a Meeting Scheduler (Calendly Clone) in 30 Minutes

How to Build a Meeting Scheduler (Calendly Clone) in 30 Minutes
Nikhil Nandagopal
0
 minutes ↗
#
open-source
#
reactjs
#
showhashnode
#
howtos
Tutorial

> The post you are reading is based on an old version of Appsmith. Please use our template for the updated configuration.

We've been a long-time Calendly user, an app that lets others block our time and schedules meetings. And we love how much time it saves me. But we've needed a few additional features that were only available on their premium plans. Instead of upgrading it, we've decided to build our own that can be easy to use and fully customisable!

Calendly Clone on Appsmith

Unfortunately, there's one problem here. We'll have to pick technologies, write lots of code, build UI, deploy them from scratch. Which is again time-consuming. To skip all of these, we used Appsmith for the UI, APIs by Google Calendar and Zoom for video-call services.

"✨ Appsmith is a cloud or self-hosted open-source platform to build admin panels, CRUD apps and workflows. With Appsmith, you can build everything you need 10x faster. ✨ "

You can build this, too, in just under 30 minutes! Click here to try. Block the time for a live demo. Here's a quick sketch of what we'll be going through in this guide:

  • Connecting Your Google Calendar
  • Listing our Events from Google Calendar
  • Displaying Free Slots
  • Creating Calendar Events
  • Setting up Zoom for Video Conferencing
  • Final step

Let’s get started!

Connecting Your Google Calendar

Our first task is to connect our Google Calendar and list down all our calendar events. For this, we’ll have to use Google APIs and OAuth authorization to authenticate the user from Appsmith.

If you're an existing user, you can sign in to Appsmith or sign up for a new one (it's free!). We’ll walk through different steps to list our events!

  1. First, you’ll have to create a new application on Appsmith.
  2. A new application opens up an application titled Untitled Application 1; you can rename it by double-clicking on the existing one.
  3. Next, you’ll have to create a new data-source to interact with Google Calendar: To do this create a new API by clicking on the + icon on the left navigation.
  4. Add a new API and save it as a data source with the following URL: https://www.googleapis.com/calendar
  5. You can also set the name of the data source; in this case, we’ll call it GCalender.
  6. Now, navigate to the GCalender data source and set the following configuration: Authentication Type: Oauth 2.0 Grant Type: Authorization Code
  7. Add Access Token URL: https://oauth2.googleapis.com/token this token allows users to verify their identity, and in return, receive a unique access token in return.
  8. Add the Client ID and Client Secret from Google Cloud Platform
  9. Lastly, set the following config:
  10. Scope: https://www.googleapis.com/auth/calendar
  11. Authorization URL https://accounts.google.com/o/oauth2/v2/auth
  12. Add Auth params prompt: consent, access_type: offline
  13. Save and Authorize the first time around!

Awesome! We've now had the authorization to access our google calendar.

Listing our Events from Google Calendar

Our next step is to create an Appsmith App and fetch all the calendar events.

Let’s create an API to fetch events from our calendar and call it fetch_calendar_events. Use the following CURL command:

curl --location --request GET 'https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=%3CMIN_TIME%3E&timeMax=%3CMAX_TIME%3E&singleEvents=true&orderBy=startTime'

Let's make these parameters dynamic by adding some javascript. To do this, we'll use Javascript and moment.js, both of which can be used in Appsmith by writing {{ }} in any field.

// Replace MIN_TIME 
{{moment().format("YYYY-MM-DDT00:mm:ss+05:30")}} 

// Replace MAX_TIME
{{moment().add(1,"days").format("YYYY-MM-DDT00:mm:ss+05:30")}}

Now replace the placeholders with the above js snippets and hit run to fetch your calendar events! Below is a screenshot explaining the same.

Updating Params


Awesome! This will now display all your calendar events.

Displaying Free Slots

Now that we have all the events from our calendars, we need to build a UI to display these events. Appsmith has a simply DnD interface to build UI. We can use the Table to display our events, a date picker to select which day we should fetch events for, and a dropdown to select the meeting duration we'd like our users to schedule. My UI looks like this, but you can get creative and built something that feels intuitive to you. Be sure to name your widgets well. The naming I'm following in this post is

  • durationDropdown for the dropdown widget
  • slotDatePicker for the date picker widget
  • eventsTable for the table widget

With an idea of the layout and inputs we need, we can configure our Table to display the events our API is returning using JS. Now, set the table data to : {{fetch_calendar_events.data.items}}

Now, this is how our app should look like:

Alt Text

No surprise our table doesn't look very readable because APIs are rarely built to work with views out of the box! Now with a little Javascript, we can transform the ugly API response into something human-readable and add some logic to show us the free slots instead of the calendar event. The following code goes into the table data property of the eventsTable.

{{
function() {
let bookings = fetch_calendar_events.data.items;
let bookingIndex = 0;
const startHour = 10;
const endHour = 20;
let startingTime = moment(slotDatePicker.selectedDate, "DD/MM/YYYY");
startingTime.hour(startHour);
startingTime.minutes(0);
const hr = startingTime.hour();
let slots = [];
const slotDuration = Number(durationDropdown.selectedOptionValue);
while (startingTime.hour() < endHour) {
  if (startingTime.isBefore(moment())) {
    startingTime = moment();
    if (startingTime.minutes() > 30) {
      startingTime.minutes(0);
      startingTime.hour(startingTime.hour() + 1);
    } else startingTime.minutes(30);
  }
  const booking = bookings[bookingIndex];
  let bookingStart = undefined;
  if (booking) {
    bookingStart = moment(booking.start.dateTime);
  } else {
    bookingStart = moment(DatePicker1.selectedDate, "DD/MM/YYYY");
    bookingStart.hour(endHour);
    bookingStart.minutes(0);
  }
  const slotNum = Math.floor(Math.round(moment.duration(bookingStart.diff(startingTime)).asMinutes()) / slotDuration);
  for (let i = 0; i < slotNum; i++) {
    const slotStartTime = startingTime.format("HH:mm");
    startingTime.add(slotDuration, "minutes");
    const slotEndTime = startingTime.format("HH:mm");
    slots.push({
      startTime: slotStartTime,
      endTime: slotEndTime
    });
  }
  startingTime = booking ? moment(booking.end.dateTime) : startingTime.hour(endHour);
  bookingIndex += 1;
}
return slots.map((slot) => {
  return {
    slot: slot.startTime + " - " + slot.endTime
  }
})
}()
}}

The above logic was really the hardest part of building this application and took the longest to get right (but also the most fun part). The code declares a self-invoking function that iterates over the response of the fetch_calendar_events API and adds free time slots to array slots that are returned by the function. It begins iterating from 10 am to 8 pm (again hardcoding my workday for convenience) and uses the value in the durationDropdown to determine the number of free slots of selected duration that can fit between the current available time and the next calendar event. It then skips to the end of the next busy calendar event and continues this till it reaches the end of my workday!

Creating Calendar Events

To schedule a meeting, we can create a button on the table and configure it to open a modal once it is clicked. The modal UI can also be custom-built using DnD to capture a user's name, email, and purpose of the meeting.

Name the inputs as nameInput, emailInput and purposeInput so we're able to use them in our API. Let's import the following CURL and name the API create_event:

{{nameInput.text}}

In the above API, we need to accept dynamic values from our app, so we need to replace some of the params and headers.

_USERNAME:

{{nameInput.text}}

_STARTTIME:

{{moment(slotDatePicker.selectedDate+" "+eventsTavble.selectedRow.slot.split(" - ")[0], "DD/MM/YYYY HH:mm").toISOString()}}

_ENDTIME:

{{moment(slotDatePicker.selectedDate+" "+eventsTavble.selectedRow.slot.split(" - ")[1], "DD/MM/YYYY HH:mm").toISOString()}}

DESCRIPTION:

{{purposeInput.text}}

TITLE:

{{nameInput.text + " Meeting with Nikhil"}}

EMAIL:

{{emailInput.text + " Meeting with Nikhil"}}

With this, our API is reading values from the eventsTable and the slotDatePicker to create the right event for the selected slot (eventsTable.selectedRow). We can now bind the onClick of the confirm button to call the create_event API and close the modal onSuccess.

Setting up Zoom for Video Conferencing

Finally, there's one more thing. We need to send out a Zoom link for every event to integrate Zoom's APIs and create a zoom link for the calendar events. (This is one of the things I love about Calendly) To integrate with zoom, we have to fetch a JWT to authenticate our application requests with zoom. We'll follow this guide. With a JWT in hand, it became as easy as importing another CURL and naming it create_zoom.

curl --location --request POST 'https://api.zoom.us/v2/users/5IrMmK-8Rv-dFkX1kbk22w/meetings' \
--header 'Content-Type: text/plain' \
--data-raw '{
  "topic": "",
  "type": 2,
  "start_time": "",
  "duration": ,
"timezone": "Asia/Kolkata",
  "settings": {
    "registrants_email_notification": true
  }
}'

Replace the following variables as shown:

JWT with your JWT token

TOPIC:

{{nameInput.text + "<> Name"}}

STARTTIME:

{moment(slotDatePicker.selectedDate+" "+eventsTable.selectedRow.slot.split(" - ")[0], "DD/MM/YYYY HH:mm").format("yyyy-MM-DDTHH:mm:ss")}}

-DURATION:_

{{durationDropdown.selectedOptionValue}}

We also have to update our create_event API to save the zoom link returned by this API in the calendar event. Update the description field to

{{purposeInput.text + "\n Zoom Link: " + create_zoom.data.join_url }}

Now let's call our create_zoom API before we call create_event and close the modal. To do this, we can easily convert the onClick configuration to javascript and write a neat little workflow using callbacks.

{{ create_zoom.run(() => 
    create_event_event.run(() => 
      fetch_calendar_events.run(() => 
        closeModal("Modal1")
      )
    )
  )
}}

Now hit deploy, make your application URL public by clicking the share button, and share it with anyone you need to meet. You can try my meeting scheduler here. Soon we’ll be having a fork app feature on Appsmith so that we can directly fork this one and customise accordingly within no time. All we’ll have to do is update the forked app with the new API keys.

If you like what we've built, star our ⭐️ Github Repo. Also, we’re an open-source company.

Also, do let us know your thoughts on this article in the comments section.

Evaluating JS in the Browser for a Low Code Product
26
January
2021
Engineering

Evaluating JS in the Browser for a Low Code Product

Evaluating JS in the Browser for a Low Code Product
Hetu Nandu
0
 minutes ↗
#
engineering
#
performance
#
reactjs
#
js
Engineering

Appsmith is an open-source low code platform for developers to build internal apps and workflows.

In Appsmith, our developer users define business logic by writing any Javascript code in between {{ }} dynamic bindings almost anywhere in the app. They can use this while creating SQL queries, APIs, or triggering actions. This functionality lets you control how your app behaves with the least amount of configuration. Underneath the hood, the platform will evaluate all this code in an optimized manner to make sure the app remains performant yet responsive.

Let us take an example of binding a query response to a table widget.

It all starts with the binding brackets {{ }} . When the platform sees these brackets and some code in it, in a widget or action configuration, it will flag the field as a dynamic field so that our evaluator can pick it up later. In our example let us bind usersQuery to usersTable

Screenshot_2020-12-05_at_2.46.08_PM.png

Since we have added this binding in our tableData field, we will flag this field and store it in our widget config

// usersTable config
{
  "usersTable": {
        ...
        "tableData": "{{
            usersQuery.data
                .map(row => ({
                    name: row.name,
                    email: row.email
                }))
            }}",
        "dynaminBindingPathList": [
            {"key": "tableData"}
            ...
        ]
    }
}

In the background, our evaluation listener, always keeps a lookout for such events that would need an evaluation. For our example, this is a scenario that definitely needs an evaluation, so it kicks off our evaluator.

We pass on our current list of app data constructed in what we call as DataTree to the evaluator thread and patiently wait to hear back from it ⏱

// DataTree
{
    "usersQuery": {
        "config": {...},
        "data": [...]
    },
    "usersTable": {
        "tableData": "{{
            usersQuery.data
                .map(row => ({
                    name: row.name,
                    email: row.email
                }))
            }}",
        "dynaminBindingPathList": [{"key": "tableData"}]
    }
}

For performance reasons, we run our evaluation process in a separate background thread with the help of web workers. This ensures that evaluation cycles running longer than 16ms do not hang up the main thread giving the app bandwidth to always respond to user events.

Inside the thread, the event listener gets a wake-up call and gets to work.

  • Get differences: First it will calculate differences in the DataTree from the last time. This will ensure we only process changes and not the whole tree. In our example, we would see the usersTable.tableData has changed and usersTable.dynamicBindingPathList has a new entry. It takes each difference, filters any un-important changes, and processes the rest.
  • Get evaluation order with dependency map: It also maintains a DependencyMap between various entity properties. The evaluator will notice if any bindings have changed and recreate the sort order accordingly.For our example, we will infer that usersTable.tableData now depends on usersQuery.data. This means that the query response should always be evaluated before we can evaluate the table data and that whenever we see a change in the query response, we need to re-evaluate the table data as well
// DependencyMap
  {
      ...
      "usersTable.tableData": ["usersQuery.data"]
  }
  // Evaluation order
  [
      "usersQuery.data",
      "usersTable.tableData"
  ]
  • Evaluate: After creating an optimized evaluation order, we will evaluate the update the tree, in that said order. Evaluation happens via a closed eval function with the whole DataTree acting as its global scope. This is why we can directly reference any object in our DataTree in our code.
// Evaluator

  const code = `
    usersQuery.data.map(row => ({
      name: row.name,
      email: row.email
    }))
  `;
	const scriptToEvaluate = `
    function closedFunction () {
      const result = ${code};
      return result
    }
    closedFunction()
  `;
	const result = eval(scriptToEvaluate);
  • Validate and parse: We always want to make sure the values returned after evaluation to be in the right data type that the widget expects. The ensures the widget always gets predictable data even if your code has returned some errors. This is also needed for any function down the line in the evaluation order, if it refers to this field, will always get a reasonable data type to work with.

And that completes it. At the end of this, we will have a fully evaluated DataTree that we can then send back to the main thread and start listening for any new event to do this whole process again.

// Evaluated DataTree
{
    "usersQuery": {
        "data": [...] 
    }
    "usersTable": {
        "tableData": [...]
    }
}

Our main thread gets an event saying the evaluation is complete, with the new evaluated DataTree which it stores in the app redux state. From here, the widgets pick up their data and render it.

Screenshot_2020-12-05_at_2.46.22_PM.png

Summarizing our philosophy

  • Pull vs Push: While building a low code app builder for varied developers, we thought hard about how the written code works with the rest of the platform. We wanted configuration to be easy to start yet powerful when it needed to be. For this reason, we went with a Pull based architecture rather than Push. What this means is that in most places, you won't have to think about how the data will get to a field. You write code that pulls everything from the global DataTree and sets it to the field where you write it. This way the moment the underlying data changes, it get propagated to all the fields dependant on it and you as a developer do not have to orchestrate ui changes.
  • One-way data flow: Since we are built on top React.js and Redux, we strongly embrace the one-way data flow model. What this means is that you cannot set a table's data directly to that field from some other part of the app. If you do need to update the table, you will have to trigger the query to run, which will then cause the table to re-render with the new data. This helps the code you write easy to reason about and bugs easy to find. It also encapsulates each widget's and action's logic in itself for good separation of concern.