DEV Community

Mark Woodson
Mark Woodson

Posted on

React Scheduler using Google Calendar API

Hey team! I was recently working on a project that involved getting someone's personal calendar integrated into a React project. After looking into it a bit, I couldn't find anything that had a full documentation on how to do it, which inspired me to make this. So today, let's make a fully functioning calendar in React, and populate it with events from your own Google Calendar!

Now this tutorial is broken up into a few parts:

  • Firstly, we'll focus entirely on the front end, using sample data from a local json file.
  • Next, we'll create the Firebase functions that use the Google Calendar API to retrieve events
  • Lastly, we'll connect the two and talk about some future ideas you can add.

This tutorial will assume you have some experience or familiarity with React.

Project Setup

To begin, we'll start with a basic React project by running npx create-react-app reservation-demo in the terminal and then opening the directory in your editor of choice.

Next, we will take advantage of a few npm packages, so run the following script to install all the packages we'll need for this tutorial:

npm install devextreme@24.1 devextreme-react@24.1 --save --save-exact
npm install dayjs@1.11.11 --save
Enter fullscreen mode Exit fullscreen mode

While we can just use Javascript's native Date object, I found using dayjs for the Firebase functions to be a lot easier to use.

Create Calendar Component

For the frontend, we will make use of the DevExtreme React Scheduler. This package gives us everything we need to get a working frontend to display our calendar events, and handle any additions, edits, and deletions for the events. Inside our App.js file, replace the contents with the following:

import 'devextreme/dist/css/dx.light.css';
import { Scheduler, View, Editing } from 'devextreme-react/scheduler';

const App = () => {
  return (
    <Scheduler defaultCurrentView='week'>
      <Editing />
      <View 
        type="week"
        startDayHour={9}
        endDayHour={19}
      />
      <View type="month" />
    </Scheduler>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

This code uses the DevExtreme package to get each component that we need. If you run this, you should see a scheduler with a view of the week from 9am-7pm, and the option to see it by month. If you're curious on how each component works and any additional props or components that you can use with this, the documentation for each component can be found in the DevExtreme Docs within the API Reference section.

Create Calendar Data

For now, we will use some dummy data to populate our calendar, but by the end, we will use the data that we get from our API call. We'll add this data to a json file and then incorporate that into our Calendar logic.
Now create a new file called appointments.js and paste the following into the file:

import dayjs from 'dayjs';

export const appointments = [
    {
      title: 'Website Re-Design Plan',
      startDate: dayjs('2023-07-16 9:35').toDate(),
      endDate: dayjs('2023-07-16 11:30').toDate(),
      id: 0,
      location: 'Room 1',
    }, {
      title: 'Book Flights to San Fran for Sales Trip',
      startDate: dayjs('2023-07-18 12:35').toDate(),
      endDate: dayjs('2023-07-18 13:30').toDate(),
      id: 1,
      location: 'Room 1',
    }
];
Enter fullscreen mode Exit fullscreen mode

This array contains each event that will display on the calendar.
When we import this object to our App.jsx file, we can populate our calendar by adding the object to the scheduler in our App.jsx like this:

// previous imports above
import { appointments } from './appointments';
const App = () => {
    const currentDate = dayjs('2023-07-16').toDate()
    return (
        <Scheduler
          dataSource={appointments}
          textExpr='title'
          defaultCurrentView='week'
          currentDate={currentDate}
          adaptivityEnabled={true}
        >
        // other components below
    );
}; 
export default App;
Enter fullscreen mode Exit fullscreen mode

A couple notes on this block of code:

  1. For the props, we set textExpr to use 'title' as the key from the appointments to represent the 'subject' for each appointment (This is useful for part two).
  2. We add the 'currentDate' value to set the schedule to a date where we can see the dummy events in the calendar.

By adding our appointments to our scheduler and creating a default date near those event dates, we should see them in the calendar with the code running with npm start.

Running sample of calendar

Great! Everything should be up and running. You should be able to see the events that you created with the dummy data, and also add, edit, and remove events. One thing to note is that the data does not persist if you refresh the page, so be mindful of that.

This concludes the bulk of the frontend portion (it wasn't much but it's honest work). But now we can dive into creating those backend functions to populate the calendar.

Create Google Project

We will now go create a Google Cloud Project so we can set up our Calendar API calls and Firebase Functions. Go to the
Firebase Console to create an account or sign in, and create a new project. You can give the project whatever name, but keep note of the PROJECT_ID that gets generated, since this will be our unique identifier.
Firebase Project Creation Screen
You can leave the Enable Google Analytics option on or turn it off, but I enabled mine during this demo. Select 'Create Project' and then you're project should be ready within a few minutes. This should also initialize the project in the Google Cloud Console.

Once that's ready, we will set up Firebase Function, which will be what our component calls to read our Google calendar's events and add new ones. Inside the root of our project, we will set up our Firebase structure with:

npm install -g firebase-tools
firebase login
firebase init
Enter fullscreen mode Exit fullscreen mode

In the options presented, select Functions and hit Enter to continue. Afterwards, make sure to select your PROJECT_ID and hit Enter. Now, select Javascript for the language we are using, and hit Enter to finish the set up for our Functions. Once it's all finished, our Functions should be set-up! There should be a hello-world example in our index.js file that you can test out if you want.

Create Google API Key

This section will focus on setting up the credentials that are needed to get the backend working.
Let's go to Google Cloud Console and select Go to APIs overview and click Credentials. Create a new project with whatever name (my-calendar-demo for consistency). In the top bar, go to Create Credentials -> OAuth Client ID. If you get a warning to create an OAuth Client ID, click the Configure consent screen button and provide a name and support email, and hit save. You should now be on the Credentials screen. Go to Application Type -> Web Application and give your credentials a name. In the Restrictions section, add the URI https://developers.google.com/oauthplayground to Authorized redirect URIs. Now we can hit Create. If successful, you should now see your clientId and client secret. Copy these since we'll need them in a second. Select OK and then in the table that appears, download the json file so that we can add it to our project. If you lose the client ID and secret, you can retrieve them in this json file.
Now we must configure our OAuth2 by going to the redirect added: OAuth Playground.
Click the Settings icon in the top right and select Use your own OAuth credentials, so we can add our client ID and secret. Now in the left side column, we are going to search for and select Calendar API v3 and give it the scope of https://www.googleapis.com/auth/calendar so that we can read and write to our calendar. Now click Authorize APIs to move to Step 2. You should be taken to the Google sign-in page, so log in (thankfully just this once) and now it should return back to a screen like the following.
OAuth Playground Step Two

Click the Exchange authorization code for tokens button to get a refresh token, which will allow us access to the API calls we'll make. We'll add the refresh_token to the json file that we downloaded, so now it should look like:

{
  "web": {
    "client_id": "706881267217-852pu9orud6araf6k.apps.googleusercontent.com",
    "project_id": "my-calendar-demo-444701",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "<REDACTED>",
    "redirect_uris": [
      "https://developers.google.com/oauthplayground"
    ]
  },
  "refresh_token": "<REDACTED>"
}
Enter fullscreen mode Exit fullscreen mode

Get Calendar ID

Lastly let's retrieve the ID for the calendar that we'll get and add events. Go to your Google Calendar and go to the settings of the calendar you want to use. In the settings, click the Integrate Calendar button and you should see the Calendar ID.

Google Calendar settings with calendar ID

Copy this and add it to the credentials.json file.

Once we've done that, we are done with the tedious setup! Now let's do the finishing touches.

Set up the functions

Now for our calendar to work how we want, we need to implement a read and write function to get the events on the calendar and add new ones. So back in our functions/index.js file, we are going to add the following code.

const { google } = require('googleapis');
const OAuth2 = google.auth.OAuth2; 
const calendar = google.calendar('v3');
const { onRequest } = require("firebase-functions/v2/https");
const { defineSecret } = require("firebase-functions/params");
const cors = require('cors')({ origin: true });
const credentials = require('./credentials.json');

const clientSecret = defineSecret("CLIENT_SECRET");
const refreshToken = defineSecret("REFRESH_TOKEN");

const READ_ERROR_RESPONSE = {
  status: "500",
  message: "There was an error reading your events on your Google calendar"
};

const ADD_ERROR_RESPONSE = {
  status: "500",
  message: "There was an error adding an event to your Google calendar"
};
Enter fullscreen mode Exit fullscreen mode

Here we are instantiating the variables and constants that will be used for each of our functions.
Next, we'll add the code for reading the calendar events:

const readEvents = (params, auth) => {
  return calendar.events.list({
    auth: auth,
    timeMin: params.timeMin,
    timeMax: params.timeMax,
    calendarId: credentials.web.calendar_id,
    singleEvents: true,
    orderBy: 'startTime',
  }).then(res => {
    console.log('Request successful');
    return res.data;
  }).catch(err => {
    console.log('Rejecting because of error');
    throw new Error(err);
  })
};

exports.readEventsFromCalendar = onRequest(
  { secrets: [clientSecret, refreshToken] },
  async (request, response) => {
    cors(request, response, () => {
      const oAuth2Client = new OAuth2(
        credentials.web.client_id,
        process.env.CLIENT_SECRET,
        credentials.web.redirect_uris[0]
      );
      oAuth2Client.setCredentials({
        refresh_token: process.env.REFRESH_TOKEN,
      });
      const eventData = {
        timeMin: request.body.timeMin,
        timeMax: request.body.timeMax,
      };
      readEvents(eventData, oAuth2Client).then(data => {
        response.status(200).send(data);
        return;
      }).catch(err => {
        console.error('Error reading events: ' + err.message); 
        response.status(500).send(READ_ERROR_RESPONSE); 
        return;
      });
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

Our function uses the Events List Google Calendar call to get the details of the events in our calendar, with an optional timeMin and timeMax parameter to narrow the scope of our returned events.

And finally, we'll add the code to publish new events in our calendar:

const addEvent = (event, auth) => {
  return calendar.events.insert({
    auth: auth,
    calendarId: credentials.web.calendar_id,
    resource: {
      'summary': event.eventName,
      'description': event.description,
      'start': {
        'dateTime': event.startTime,
        'timeZone': event.timeZone,
      },
      'end': {
        'dateTime': event.endTime,
        'timeZone': event.timeZone,
      },
    },
  }).then(res => {
    console.log('Request successful');
    return res.data;
  }).catch(err => {
    console.log('Rejecting because of error');
    throw new Error(err);
  })
};

exports.addEventToCalendar = onRequest(
  { secrets: [clientSecret, refreshToken] },
  async (request, response) => {
    cors(request, response, () => {
      const oAuth2Client = new OAuth2(
        credentials.web.client_id,
        process.env.CLIENT_SECRET,
        credentials.web.redirect_uris[0]
      );

      oAuth2Client.setCredentials({
        refresh_token: process.env.REFRESH_TOKEN,
      });
      const eventData = {
        eventName: request.body.eventName,
        description: request.body.description,
        startTime: request.body.startTime,
        endTime: request.body.endTime,
        timeZone: request.body.timeZone,
      };
      addEvent(eventData, oAuth2Client).then(data => {
        response.status(200).send(data);
        return;
      }).catch(err => {
        console.error('Error adding event: ' + err.message); 
        response.status(500).send(ADD_ERROR_RESPONSE); 
        return;
      });
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

Now we are almost ready to publish our functions, but first we want to make sure we publish this as safely as possible. Since our client secret and refresh key are things we don't really want to expose, we are going to take those out of the credentials file, before publishing. So copy those and put them somewhere for a second, because we'll come back to them in the next step. Now that your credentials should look like:

{
  "web": {
    "calendar_id": "a4f940ab772d782b18824b5cb114ce4b91412fb8b7631d0@group.calendar.google.com",
    "client_id": "706881267217-852cmqf4vrud6araf6k.apps.googleusercontent.com",
    "project_id": "my-calendar-demo-444701",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "redirect_uris": [
      "https://developers.google.com/oauthplayground"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

We can run firebase deploy --only functions to get our functions deployed. Once that's complete, we are almost ready to start testing.

Create function secrets

Let's go to [Google Cloud Console (https://console.cloud.google.com/) and select the Firebase project. In the navigation menu, go to Cloud Functions in the Serverless section and from here you should be able to see our two functions. Let's start with addEventToCalendar by clicking on it, and in this view you should be able to see the details about our function. Click the edit button and go to Runtime, build, connections and security settings. Now we'll go to the
SECURITY AND IMAGE REPO tab and click Add a secret reference. In the first input, click the dropdown and Create New Secret, and add CLIENT_SECRET as the name and paste the value in the Secret value section, and then click Create Secret. Now select this secret, change the Reference method to Exposed as environment variable, and for the path, add CLIENT_SECRET and then select Done. Now we will repeat these steps for REFRESH_TOKEN. After that, we have to do the same for our second function readEventsFromCalendar. And once we complete all that, we can finally try and test it out.

Testing the functions

So within our Function details for addEventToCalendar, there is a Testing tab that we'll select and add the following payload:

Enter fullscreen mode Exit fullscreen mode

Once we add this and hit Test the Function, we should see a successful response, and if we check out calendar, we should see this new event!
And just to check our other function, we'll go to the testing tab there and add this payload:

Enter fullscreen mode Exit fullscreen mode

And this success should show the information on the event we just added! If these are successful, now all that's left to do is just tie it all together!

Use with our app

Now that we’ve published and tested our functions, we are going to make those function calls to populate the calendar in our app. We’re going to create a new file called util.js and will add these lines of code:

import dayjs from 'dayjs';

const parseCalendarEvents = (data) => {
  const eventsList = data.items?.map(event => ({
    startDate: dayjs(event.start.dateTime).toDate(),
    endDate: dayjs(event.end.dateTime).toDate(),
    title: event.summary,
    description: event.description,
    id: event.id,
  }))
  return {calendarTimeZone: data.timeZone, eventsList};
};

export const readCalendarEvents = async (timeMin, timeMax) => {
  const requestBody = {
    timeMin,
    timeMax
  }
  return fetch('https://us-central1-my-calendar-demo-fa016.cloudfunctions.net/readEventsFromCalendar', {
    method: 'POST',
    body: JSON.stringify(requestBody),
    headers: { 'Content-Type': 'application/json' }
  })
  .then(res => res.json())
  .then(json => parseCalendarEvents(json))
  .catch(error => console.log(error));
};

export const addToCalendar = async ({title, startDate, endDate, description, allDay}, timeZone) => {
  const startTime = dayjs(startDate);
  const endTime = dayjs(endDate);
  const requestBody = {
    eventName: title,
    description,
    startTime: startTime.format('YYYY-MM-DDTHH:mm:ssZ'),
    endTime: endTime.format('YYYY-MM-DDTHH:mm:ssZ'),
    timeZone
  };
  return fetch('https://us-central1-my-calendar-demo-fa016.cloudfunctions.net/addEventToCalendar', {
    method: 'POST',
    body: JSON.stringify(requestBody),
    headers: { 'Content-Type': 'application/json' }
  }).catch(error => console.log(error));
}
Enter fullscreen mode Exit fullscreen mode

The addToCalendar function calls our Firebase function addEventToCalendar and readCalendarEvents calls readEventsFromCalendar while using a helper function called parseCalendarEvents to clean up the response and format the data to how we need it for our scheduler.

Now we will call these functions in App.jsx. We will add useEffect to fetch the events once the module loads by adding:

useEffect(() => {
    const getAppointments = async () => {
      const startTime = currentDate.subtract(2, 'month').format('YYYY-MM-DDTHH:mm:ss') + '.000Z';
      const endTime = currentDate.add(2, 'month').format('YYYY-MM-DDTHH:mm:ss') + '.000Z';
      const {calendarTimeZone, eventsList} = await readCalendarEvents(startTime, endTime);
      setApps(eventsList);
      setTimeZone(calendarTimeZone);
    };
    getAppointments();
 }, []);
Enter fullscreen mode Exit fullscreen mode

So now your App.jsx file should look like:

import 'devextreme/dist/css/dx.light.css';
import { useCallback, useState, useEffect } from 'react';
import { Scheduler, View, Editing } from 'devextreme-react/scheduler';
import { addToCalendar, readCalendarEvents } from './util';
import dayjs from 'dayjs';

const App = () => {
  const currentDate = dayjs();
  const [apps, setApps] = useState([]);
  const [timeZone, setTimeZone] = useState('');

  useEffect(() => {
    const getAppointments = async () => {
      const startTime = currentDate.subtract(2, 'month').format('YYYY-MM-DDTHH:mm:ss') + '.000Z';
      const endTime = currentDate.add(2, 'month').format('YYYY-MM-DDTHH:mm:ss') + '.000Z';
      const {calendarTimeZone, eventsList} = await readCalendarEvents(startTime, endTime);
      setApps(eventsList);
      setTimeZone(calendarTimeZone);
    };
    getAppointments();
 }, []);

  const handleAppointment = useCallback(async (e) => {
    await addToCalendar(e.appointmentData, timeZone);
  }, []);

  return (
    <Scheduler
      dataSource={apps}
      timeZone={timeZone}
      textExpr='title'
      defaultCurrentView='week'
      currentDate={currentDate.toDate()}
      adaptivityEnabled={true}
      onAppointmentAdding={handleAppointment}
    >
      <Editing />
      <View 
        type="week"
        startDayHour={9}
        endDayHour={19}
      />
      <View type="month" />
    </Scheduler>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

And with this we should be able to run the app and see how your calendar looks like (with or without events). But to fully test this out, lets double click a spot on the scheduler, add some event details, and click create. If no console log shows up for an error, we should have a successful API call, and we should see the newly created event in the calendar!

Conclusion

And with that, you should have a Calendar up and running, populated with your Google Calendar events! Now there's still a lot you can do to add onto this, like playing around with some more of the props that you can use with the scheduler, creating functions for updating and deleting events, adding a loading state for the read call, etc. Please let me know in replies if you have any questions but I hope this was helpful!

Top comments (0)