4 min read
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:
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!
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;
<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>
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!
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>
);
})}
{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"
/>
))}
{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;