October 2, 2022

4 min read

Dynamic Google Calendar Events using ReactJs, TailwindCSS and dayjs

Have you ever wondered how Google Calendar works? How can you build your custom Google Calendar-like component simply by using Reactjs, TailwindCSS, and dayjs?

Well, it's dead simple, and here's a quick tutorial on how to do it.

Creating a dynamic Google Calendar Events using ReactJs, TailwindCSS, and dayjs

Prerequisites

To get the best out of this tutorial, you should be familiar with:

  1. TailwindCSS: A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.
  2. dayjs: is a minimalist JavaScript library that parses, validates, manipulates, and displays dates and times for modern browsers with a largely Moment.js-compatible API.

Install the above dependencies:

npm install -D tailwindcss postcss autoprefixer
npm install dayjs --save
npx tailwindcss init -p

With all the required packages installed, let's get started!

  • Create a skeleton of the Google Calendar component
type Props = {};

export const CALENDAR_HOURS = [10, 11, 12, 13, 14, 15, 16, 17, 18];

const GoogleCalendar = (props: Props) => {
  const NUMBER_OF_DAYS = 7;
  const NUMBER_OF_HOURS = CALENDAR_HOURS.length;
  const NUMBER_OF_GRIDS = NUMBER_OF_DAYS * NUMBER_OF_HOURS;


  return (
    <div className="w-full my-3 flex space-x-3">
    // HOURS COLUMN 
    <div className="flex-shrink-0 flex-initial"></div>
    
    // EVENTS GRID
    <div
        className="relative isolate w-full bg-white rounded-lg flex-initial flex-shrink-0 overflow-x-auto scrollbar-none grid grid-cols-[repeat(7,minmax(9rem,1fr))] lg:grid-cols-7"
      >
    </div>
    </div>
  );
};

export default GoogleCalendar;
  • Map through the CALENDAR_HOURS array to display the right sidebar of the calendar that will contain the hours of the day the event may run on.
<div className="flex-shrink-0 flex-initial">
    {CALENDAR_HOURS.map((hour, index) => (
      <div
        key={`hour_${index}`}
        className="first:mt-24 mt-[5.8rem] flex-shrink-0 text-black/60 text-sm"
          >
       {hour}h
     </div>
  ))}
  </div>
  • Add a grid system layout that will display the header, the squares, and the event cards.
    Before that, we need to create 3 functions that we will use later on.

Function 1: getTimeIndex

This function will help us find the position of the time inside the CALENDAR_HOURS array, which will determine the row at which the event starts and ends.

This function returns an array containing the index (the position of the hour) and the minute. In case the user passes the date without minutes, we return an array containing only the index.

The minute will be used to stretch the event card.

const getTimeIndex = (time: string) => {
    const hour = time.split("h:")[0];
    const minute = time.split("h:")[1];

    const hourIndex = CALENDAR_HOURS.findIndex((hours) => hours === hour);

    if (minute) {
      return [hourIndex, +minute];
    }

    return [hourIndex];
};

Function 2: getEventColor

This function will help us display different colors of the event card based on the status.

const getCourseColor = (status: string) => {
    if (status === "ongoing") {
      return ["bg-yellow-300 text-metalic", "bg-metalic"];
    }
    if (status === "completed") {
      return ["bg-primary/50 text-white", "bg-primary"];
    }
    return ["bg-secondary/50 text-primary", "bg-secondary"];
};

Function 3: getDaysOfWeek

This function helps us build an array of dates based on the number of days we specified for the NUMBER_OF_DAYS variable.

const getDaysOfWeek = () =>
    Array.from({ length: NUMBER_OF_DAYS }).map((_, index) =>
      dayjs().isoWeekday(index + 1)
);

And that's it for the functions!

  • Next, build the header section that will display the day and date of the week.

We check for the current day by using the isSame function from the dayjs package to apply a different style to the current day.dayjs package to apply a different style to the current day.

{getDaysOfWeek().map((day, index) => {
  const isCurrentDay = dayjs().isSame(dayjs(day));
  return (
    <div
    key={`day_${index}`}
    className="text-primary py-3 border-b w-36 md:w-full h-28 shrink-0 first:border-l">
       <div className={`text-center rounded-lg py-2 ${isCurrentDay ? "bg-secondary mx-4" : ""}`} >
          <p className={`${isCurrentDay ? "font-black" : ""}`}>
            {dayjs().isoWeekday(index + 1).format("DD")}
          </p>
          <p className="text-sm">
            {dayjs().isoWeekday(index + 1).format("dddd")}
          </p>
         </div>
      </div>
    );
})}
  • Build the squares that will be displayed behind the event cards.

    We define the height of 7rem(h-28) and width of 9rem(w-36) for mobile and tablet screens and auto width for large screens that each square will occupy in the grid.
{Array.from({ length: NUMBER_OF_GRIDS }).map((_, index) => (
  <div
    key={`event_${index}`}
    className="border-b border-l w-36 lg:w-auto h-28 shrink-0"
  />
))}
  • Lastly, build the section that will display the event cards.
{EVENTS.map((event, index) => {
   const colStart = dayjs(event.startDate).isoWeekday();
   const [cardCls, indicatorCls] = getEventColor(event.status);

   const [rowStart] = getTimeIndex(event.startTime);
   const [rowEnd, minute] = getTimeIndex(event.endTime);
   const hasMinute = minute > 0;

   const lineClamp = rowEnd - rowStart > 2 ? 3 : rowEnd - rowStart || 1;

   return (
      <div
         key={`course_${index}`}
          className={`absolute inset-y-0 w-36 my-1 rounded-tr-lg rounded-br-lg flex ${cardCls}`}
         style={{
                gridColumnStart: colStart,
                // +2 because the grid starts counting at index 1, and the days occupy the index 1
                gridRowStart: rowStart + 2,
                gridRowEnd: rowEnd + (hasMinute ? 3 : 2),
                marginBottom: `calc(60px - ${minute || 54}px)`,
              }}
            >
       <div
         className={`w-1.5 rounded-tr-lg rounded-br-lg ${indicatorCls}`}
       />
       <div className={`flex flex-col justify-between p-3`}>
          <p className="text-xs">{event.module}</p>
          <p className="font-bold line-clamp-2"
             style={{  WebkitLineClamp: lineClamp, }}>
                  {event.title}
          </p>
          <p className="text-xs line-clamp-3"
            style={{WebkitLineClamp: lineClamp,}}>
             {course.description}
          </p>
          <p className="text-xs">
           {course.startTime} - {course.endTime}
          </p>
       </div>
     </div>
   );
})}

Full code on how to create a dynamic Google Calendar Events using ReactJs, TailwindCSS, and dayjs.

import React from "react";
import dayjs from "dayjs";

type Props = {};

export const CALENDAR_HOURS = [10, 11, 12, 13, 14, 15, 16, 17, 18];

export const EVENTS = [
  {
    title: "Introduction Orale",
    moduleTitle: "Module 1",
    description: "Describe this event here",
    startDate: new Date(2022, 8, 27),
    startTime: "11h:00",
    endTime: "14h:00",
    status: "started",
  },
  {
    title: "Parcours",
    moduleTitle: "Module 3",
    description: "Describe this event here",
    startDate: new Date(2022, 8, 29),
    startTime: "13h:00",
    endTime: "14h:00",
    status: "ongoing",
  },
  {
    title: "Parcours",
    moduleTitle: "Module 3",
    description: "Describe this event here",
    startDate: new Date(2022, 8, 29),
    startTime: "16h:00",
    endTime: "18h:30",
    status: "completed",
  },
  {
    title: "Bienvenue au cours",
    moduleTitle: "Module 4",
    description: "Describe this event here",
    startDate: new Date(2022, 8, 30),
    startTime: "14h:00",
    endTime: "15h:20",
    status: "started",
  },
];

const GoogleCalendar = (props: Props) => {
  const NUMBER_OF_DAYS = 7;
  const NUMBER_OF_HOURS = CALENDAR_HOURS.length;
  const NUMBER_OF_GRIDS = NUMBER_OF_DAYS * NUMBER_OF_HOURS;

  const getTimeIndex = (time: string) => {
    const hour = time.split("h:")[0];
    const minute = time.split("h:")[1];

    const hourIndex = CALENDAR_HOURS.findIndex((hours) => hours === hour);

    if (minute) {
      return [hourIndex, +minute];
    }

    return [hourIndex];
  };

  const getCourseColor = (status: string) => {
    if (status === "ongoing") {
      return ["bg-yellow-300 text-metalic", "bg-metalic"];
    }
    if (status === "completed") {
      return ["bg-primary/50 text-white", "bg-primary"];
    }
    return ["bg-secondary/50 text-primary", "bg-secondary"];
  };

  const getDaysOfWeek = () =>
    Array.from({ length: NUMBER_OF_DAYS }).map((_, index) =>
      dayjs().isoWeekday(index + 1)
    );

  return (
    <div className="w-full my-3 flex space-x-3">
      {/* HOURS */}
      <div className="flex-shrink-0 flex-initial">
        {CALENDAR_HOURS.map((hour, index) => (
          <div
            key={`hour_${index}`}
            className="first:mt-24 mt-[5.8rem] flex-shrink-0 text-black/60 text-sm"
          >
            {hour}h
          </div>
        ))}
      </div>
      <div
        className="relative isolate w-full bg-white rounded-lg flex-initial flex-shrink-0 overflow-x-auto scrollbar-none grid grid-cols-[repeat(7,minmax(9rem,1fr))] lg:grid-cols-7"
        style={{
          gridTemplateRows: "repeat(9, minmax(0, 1fr))",
        }}
      >
        {/* DAYS */}
        {getDaysOfWeek().map((day, index) => {
          const isCurrentDay = dayjs().isSame(dayjs(day));
          return (
            <div
              key={`day_${index}`}
              className="text-primary py-3 border-b w-36 md:w-full h-28 shrink-0 first:border-l"
            >
              <div
                className={`text-center rounded-lg py-2 ${
                  isCurrentDay ? "bg-secondary mx-4" : ""
                }`}
              >
                <p className={`${isCurrentDay ? "font-black" : ""}`}>
                  {dayjs()
                    .isoWeekday(index + 1)
                    .format("DD")}
                </p>
                <p className="text-sm">
                  {dayjs()
                    .isoWeekday(index + 1)
                    .format("dddd")}
                </p>
              </div>
            </div>
          );
        })}

        {/* GRID */}
        {Array.from({ length: NUMBER_OF_GRIDS }).map((_, index) => (
          <div
            key={`event_${index}`}
            className="border-b border-l w-36 lg:w-auto h-28 shrink-0"
          />
        ))}

        {/* COURSES */}
        {EVENTS.map((course, index) => {
          const colStart = dayjs(course.startDate).isoWeekday();
          const [cardCls, indicatorCls] = getCourseColor(course.status);

          // +2 because the grid starts counting at index 1, and the days occupy the index 1
          const [rowStart] = getTimeIndex(course.startTime);
          const [rowEnd, minute] = getTimeIndex(course.endTime);
          const hasMinute = minute > 0;

          const lineClamp = rowEnd - rowStart > 2 ? 3 : rowEnd - rowStart || 1;
          // const marginBottom = minute > 1 ? (112 / 60) * minute : 6;

          return (
            <div
              key={`course_${index}`}
              className={`absolute inset-y-0 w-36 my-1 rounded-tr-lg rounded-br-lg flex ${cardCls}`}
              style={{
                gridColumnStart: colStart,
                gridRowStart: rowStart + 2,
                gridRowEnd: rowEnd + (hasMinute ? 3 : 2),
                marginBottom: `calc(60px - ${minute || 54}px)`,
              }}
            >
              <div
                className={`w-1.5 rounded-tr-lg rounded-br-lg ${indicatorCls}`}
              />
              <div className={`flex flex-col justify-between p-3`}>
                <p className="text-xs">{course.moduleTitle}</p>
                <p
                  className="font-bold line-clamp-2"
                  style={{
                    WebkitLineClamp: lineClamp,
                  }}
                >
                  {course.title}
                </p>
                <p
                  className="text-xs line-clamp-3"
                  style={{
                    WebkitLineClamp: lineClamp,
                  }}
                >
                  {course.description}
                </p>
                <p className="text-xs">
                  {course.startTime} - {course.endTime}
                </p>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default GoogleCalendar;
Let's build something together.

©Eliezer W. Basubi. All Rights Reserved.