Logo

dev-resources.site

for different kinds of informations.

How to build Google calendar clone with React (Week view)

Published at
7/24/2024
Categories
react
tailwindcss
typescript
guide
Author
cookiemonsterdev
Author
16 person written this
cookiemonsterdev
open
How to build Google calendar clone with React (Week view)

Welcome back, folks! In the previous section, I explored how to create a day view for our custom calendar application. Now, it's time to expand our calendar's functionality to include a week view.

In this section, I'll cover:


Week layout

The Week layout is somewhat similar to the Day layout, with the key difference being that it is scaled to display 7 days. There isn't much else to explain, so just follow the code below:

import { isToday, format } from "date-fns";

import { cn } from "../../utils";

export type WeekDayLabelProps = {
  day: Date;
};

export const WeekDayLabel: React.FC<WeekDayLabelProps> = ({ day }) => {
  const isDayToday = isToday(day);

  return (
    <div className="flex-1 min-w-36 flex flex-col items-center">
      <span aria-hidden className="text-md text-gray-400">
        {format(day, "EEEEEE")}
      </span>
      <div
        aria-label={day.toDateString()}
        className={cn(
          "w-11 h-11  rounded-full flex items-center justify-center text-2xl font-medium text-gray-400",
          isDayToday && "text-white bg-blue-400"
        )}
      >
        <p className="leading-[44px]">{format(day, "d")}</p>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
import { useState } from "react";

import { cn } from "../../utils";
import { endOfDay, startOfDay, eachHourOfInterval } from "date-fns";

import type { Event } from "../types";

type WeekDayViewProps = {
  day: Date;
  events?: Event[];
};

export const WeekDayView: React.FC<WeekDayViewProps> = ({
  day,
  events = [],
}) => {
  const [ref, setRef] = useState<HTMLDivElement | null>(null);

  const hours = eachHourOfInterval({
    start: startOfDay(day),
    end: endOfDay(day),
  });

  return (
    <div
      aria-label={"Events slot for " + day.toDateString()}
      className="min-w-36 h-full flex flex-1 relative"
    >
      <div className="w-[95%] h-full absolute">
        <div className="w-full h-full relative" ref={(ref) => setRef(ref)}>
        </div>
      </div>
      <div className="w-full flex flex-col">
        {hours.map((time, index) => (
          <div
            key={time.toISOString()}
            className={cn(
              "h-14 w-full border-l",
              index !== hours.length - 1 && "border-b"
            )}
          />
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
import { WeekDayView } from "./week-day-view";
import { WeekDayLabel } from "./week-day-label";

import {
  format,
  endOfDay,
  endOfWeek,
  startOfDay,
  startOfWeek,
  eachDayOfInterval,
  eachHourOfInterval,
} from "date-fns";

import { Event } from "../types";

type WeekViewProps = {
  date: Date;
  events?: Event[];
};

export const WeekView: React.FC<WeekViewProps> = ({ date, events = [] }) => {
  const hours = eachHourOfInterval({
    start: startOfDay(date),
    end: endOfDay(date),
  });

  const days = eachDayOfInterval({
    start: startOfWeek(date),
    end: endOfWeek(date),
  });

  return (
    <section id="calendar-day-view" className="flex-1 h-full">
       <div className="min-w-[calc(96px+(144px*7))] flex border-b scrollbar-gutter-stable">
        <div className="min-w-24 h-14 flex justify-center items-center">
          <span className="text-xs">{format(new Date(), "z")}</span>
        </div>
        <div className="flex flex-col flex-1">
          <div className="relative flex flex-1">
            {days.map((day) => (
              <WeekDayLabel
                day={day}
                key={"week-day-label-" + day.toISOString()}
              />
            ))}
          </div>
          <div className="relative min-h-6">
            <div className="absolute inset-0 h-full flex flex-1">
              <div className="flex-1 min-w-36 border-l" />
              <div className="flex-1 min-w-36 border-l" />
              <div className="flex-1 min-w-36 border-l" />
              <div className="flex-1 min-w-36 border-l" />
              <div className="flex-1 min-w-36 border-l" />
              <div className="flex-1 min-w-36 border-l" />
              <div className="flex-1 min-w-36 border-l" />
            </div>
          </div>
        </div>
      </div>
     <div className="min-w-[calc(96px+(144px*7))] flex overflow-y-auto">
        <div className="h-fit flex flex-col">
          {hours.map((time, index) => (
            <div
              key={time.toISOString() + index}
              aria-label={format(time, "h a")}
              className="min-h-14 w-24 flex items-start justify-center"
            >
              <time
                className="text-xs -m-3 select-none"
                dateTime={format(time, "yyyy-MM-dd")}
              >
                {index === 0 ? "" : format(time, "h a")}
              </time>
            </div>
          ))}
        </div>
        <div className="flex flex-1 h-fit">
          {days.map((day) => {
            const iso = day.toISOString();
            return <WeekDayView day={day} key={iso} events={[]} />;
          })}
        </div>
      </div>
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

Current time

The current time line is similar to the one used in the Day view. Considering this, I decided to implement it at the calendar level and add an option to customize its container styles.

import { useState, useEffect } from "react";

import { startOfDay, differenceInMinutes } from "date-fns";

import { cn } from "../utils";

const ONE_MINUTE = 60 * 1000;
const MINUTES_IN_DAY = 24 * 60;

type DayProgressProps = {
  className?: string;
  containerHeight: number;
};

export const DayProgress: React.FC<DayProgressProps> = ({
  className,
  containerHeight,
}) => {
  const [top, setTop] = useState(0);

  const today = new Date();
  const startOfToday = startOfDay(today);

  useEffect(() => {
    const updateTop = () => {
      const minutesPassed = differenceInMinutes(today, startOfToday);
      const percentage = minutesPassed / MINUTES_IN_DAY;
      const top = percentage * containerHeight;

      setTop(top);
    };

    updateTop();

    const interval = setInterval(() => updateTop(), ONE_MINUTE);

    return () => clearInterval(interval);
  }, [containerHeight]);

  return (
    <div
      aria-hidden
      style={{ top }}
      aria-label="day time progress"
      className={cn(
        "h-1 w-full absolute left-24 -translate-y-1/2 z-[1000000]",
        className
      )}
    >
      <div className="relative w-full h-full">
        <div
          aria-label="current time dot"
          className="w-4 aspect-square rounded-full absolute -left-2 top-1/2 -translate-y-1/2  bg-[rgb(234,67,53)]"
        />
        <div
          aria-label="current time line"
          className="h-[2px] w-full absolute top-1/2 -translate-y-1/2 bg-[rgb(234,67,53)]"
        />
      </div>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

After adding this to <WeekDayView /> component we will see layout with current time:

week-1


Day events grouping and displaying

For the Week view, we will have two major event groups: day groups, which contain events happening on a specific day, and week groups, which contain events lasting a day or more. Each day's group of events must also be grouped to display them as in the Day view, and the same goes for the week groups.

For this, I created simple functions. The first one handles events that have the same start and end date. I then reused and copied the grouping logic from the Day view.

import { add, isSameDay, startOfDay, isWithinInterval, differenceInMilliseconds} from "date-fns";

const MILLISECONDS_IN_DAY = 86399999;

export type GroupedEvents = {
  weekGroups: Event[];
  dayGroups: Record<string, Event[]>;
};

export const createGroups = (events: Event[]): GroupedEvents => {
  const weekGroups: Event[] = [];
  const dayGroups: Record<string, Event[]> = {};

  for (let event of events) {
    const { start_date, end_date } = event;

    const same = isSameDay(start_date, end_date);
    const difference = differenceInMilliseconds(end_date, start_date);

    if (same && difference < MILLISECONDS_IN_DAY) {
      const key = startOfDay(start_date).toISOString();

      if (!dayGroups[key]) dayGroups[key] = [];

      dayGroups[key].push(event);
    } else {
      weekGroups.push(event);
    }
  }

  return { dayGroups, weekGroups };
};

export const createDayGroups = (
  events: Event[],
  groupedEvents: Event[][] = []
): Event[][] => {
  if (events.length <= 0) return groupedEvents;

  const [first, ...rest] = events;

  const eventsInRage = rest.filter((event) =>
    isWithinInterval(event.start_date, {
      start: first.start_date,
      end: add(first.end_date, { minutes: -1 }),
    })
  );

  const group = [first, ...eventsInRage];
  const sliced = rest.slice(eventsInRage.length);
  groupedEvents.push(group);

  return createDayGroups(sliced, groupedEvents);
};
Enter fullscreen mode Exit fullscreen mode

Now after adding createGroups to <WeekView /> and createDayGroups to <WeekDayView /> events will be displayed like this:

week-2


Week events grouping and displaying

Compared to grouping day events, grouping week events involves slightly more complex logic. First, events must be filtered and modified to ensure proper display. Filtering is straightforward: if at least the start or end date is not within the week, the event can be ignored. However, for events that have only one date within the week, the other date must be modified to display the event correctly. For this, I use display_start_date and display_end_date, where I store the dates that will be used for display.

After that, events must be sorted by start date and only then grouped.

export type WeekEvent = Event & {
  display_start_date: Date;
  display_end_date: Date;
};

export const createWeekGroups = (
  events: Event[],
  date = new Date()
): WeekEvent[][] => {
  const filteredEvents: WeekEvent[] = [];

  const weekEnd = endOfWeek(date);
  const weekStart = startOfWeek(date);

  for (let event of events) {
    const { end_date, start_date } = event;

    const isEnd = isSameWeek(end_date, date);
    const isStart = isSameWeek(start_date, date);
    const isMonth =
      isBefore(start_date, weekStart) && isAfter(end_date, weekEnd);

    if (!(isStart || isEnd || isMonth)) continue;

    const display_start_date = isBefore(start_date, weekStart)
      ? weekStart
      : start_date;
    const display_end_date = isAfter(end_date, weekEnd) ? weekEnd : end_date;

    filteredEvents.push({
      ...event,
      display_end_date,
      display_start_date,
    });
  }

  const sortedEvents = filteredEvents.sort(
    (a, b) => a.start_date.getTime() - b.start_date.getTime()
  );

  const groups: WeekEvent[][] = [];

  for (const event of sortedEvents) {
    let placed = false;

    for (const group of groups) {
      const lastEventInGroup = group[group.length - 1];

      if (lastEventInGroup.end_date.getTime() <= event.start_date.getTime()) {
        group.push(event);
        placed = true;
        break;
      }
    }

    if (!placed) {
      groups.push([event]);
    }
  }

  return groups;
};
Enter fullscreen mode Exit fullscreen mode

Since events are displayed horizontally, the main value for an event is its width, which is calculated based on the event's duration and the container's width. The event duration is easily accessible, but determining the container width requires more than just getting the value from a reference. Because the calendar size can be adjusted, the width needs to be recalculated on resize event and for this I used ResizeObserver.

import { startOfWeek, differenceInMinutes, format } from "date-fns";

import { WeekEvent as Event } from "./group-events";

const MINUTES_IN_WEEK = 7 * 24 * 60;

type WeekEventProps = {
  date: Date;
  event: Event;
  containerWidth: number;
};

export const WeekEvent: React.FC<WeekEventProps> = ({
  date,
  event,
  containerWidth,
}) => {
  const generateBoxStyle = () => {
    const week = startOfWeek(date);
    const eventDuration = differenceInMinutes(
      event.display_end_date,
      event.display_start_date
    );
    const minutesPassed = differenceInMinutes(event.display_start_date, week);

    const left = (minutesPassed / MINUTES_IN_WEEK) * containerWidth;
    const width = (eventDuration / MINUTES_IN_WEEK) * containerWidth;

    return { left, width: `calc(${width}px - 1px)` };
  };

  return (
    <div
      style={generateBoxStyle()}
      className="h-full px-2 absolute z-10 bg-blue-400 rounded cursor-pointer"
    >
      <h1 className="text-white text-sm text-ellipsis overflow-hidden">
        {`${format(event.start_date, "h:mm a")}, ${event.title}`}
      </h1>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
import { useRef, useState, useEffect } from "react";

import { WeekEvent } from "./week-event";

import { createWeekGroups } from "./group-events";

import { Event } from "../types";

type WeekEventsViewProps = {
  date: Date;
  events?: Event[];
};

export const WeekEventsView: React.FC<WeekEventsViewProps> = ({
  date,
  events = [],
}) => {
  const ref = useRef<HTMLDivElement | null>(null);
  const [containerWidth, setContainerWidth] = useState(1);

  const groups = createWeekGroups(events, date);

  useEffect(() => {
    if (!ref.current) return;

    const resizeObserver = new ResizeObserver((entries) => {
      for (let entry of entries) {
        setContainerWidth(entry.contentRect.width);
      }
    });

    resizeObserver.observe(ref.current);

    return () => resizeObserver.disconnect();
  }, [ref]);

  return (
    <div className="mt-2 space-y-1 overflow-hidden" ref={ref}>
      {groups.map((events, groupIndex) => (
        <div className="h-6 relative" key={"group-" + groupIndex}>
          {events.map((event) => (
            <WeekEvent
              date={date}
              event={event}
              key={event.id}
              containerWidth={containerWidth}
            />
          ))}
        </div>
      ))}
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

After adding <WeekEventsView /> to <WeekView /> all week events will be displayed like this:

week-3


Conclusion

That's it for now. In this section, I covered the week view, delving into its layout, the current time display, and the grouping and displaying of both day and week events.

In the final part of this guide, I will explore the month view, completing our journey of creating a comprehensive calendar application. Stay tuned!

The complete week-view can be found here.

guide Article's
30 articles in total
Favicon
Understanding Big O Notation: A Comprehensive Guide
Favicon
Starting Your Public Speaking Journey
Favicon
Be Viral on Twitter/X - Full Guide
Favicon
Indian Law Simplified: A Toolkit for Every Citizen
Favicon
Startup Metrics
Favicon
AWS Networking Tutorial
Favicon
Network Security, CDN Technologies and Performance Optimization
Favicon
A Comprehensive Guide to Tor and Digital Freedom 😊
Favicon
Mastering `sed` Commands and Flags: A Guide to Stream Editing in Linux πŸ–₯️
Favicon
Getting Started with Blog Automation: A Test Post
Favicon
Embracing the Suckless Philosophy: A Minimalist Approach to Computing and Life
Favicon
Building a Minimum Viable Product (MVP)
Favicon
Customized Local Models: A Comprehensive Guide
Favicon
Comprehensive Guide to AWS AI/ML Services: The Ultimate Decision Maker’s Playbook
Favicon
The Ultimate Guide to Data Analytics.
Favicon
The Ultimate Guide to Data Analytics
Favicon
A Guide to Digital Experience Monitoring(DEM) for Businesses
Favicon
Junior Developer's Guide to Legacy Systems | IUG 2024
Favicon
Best practices to configure an uptime monitoring service
Favicon
Navigating the Complexities of β€˜this’ in JavaScript
Favicon
Building a Dedicated Offshore Team in India: A Step-by-Step Guide for Startups to Leverage Indian Tech Talent
Favicon
Mastodon: Comprehensive Guide to Installing and Using Mastodon
Favicon
CompTIA A+ Study Guide Core 1 and Core 2 Exam Info
Favicon
Rapid API Hub Made Easy: Your Comprehensive Guide to Subscribing and Starting with APIs
Favicon
The Book of Nodes: 3D
Favicon
STARTING OUT IN TECH: THE NEWBIE’S NAVIGATOR
Favicon
How to build Google calendar clone with React (Month view)
Favicon
How to build Google calendar clone with React (Week view)
Favicon
Troubleshooting Adabas Event Replicator on z/OS Command Code A1 Error 77 Subcode 11
Favicon
My New Book on IIoT

Featured ones: