How to Build a Week View UI for Your Calendar App

How to Build a Week View UI for Your Calendar App

·

10 min read

A Calendar app like Google Calendar or Notion Calendar has become integral to our life. Be it scheduling meetings, events, planning etc. As a front-end engineer, this app is exciting. Be it the drag-and-drop feature to update the event in hand, the different views to display for the week, month and year to the stacking of events or tasks that elegantly overlap with each other.

Today, we are going to look into how to make the Weekly Events View UI for a calendar app and see how we can handle all the important features of the same.

You can check the demo here: https://calendar-pro-c3vm.vercel.app/

Let's talk about all the problems we have in our hand

  1. How do we define a task? (the properties of it etc)

  2. How do we make the date picker?

  3. How do we display the week grid?

  4. How do we display the task?

  5. How do we handle the dragging of tasks?

  6. How do we handle the stacking of tasks?

Lets talk about each and every problem step by step

How do we define and Task / Event?

The task should have all the important properties for its functioning. The task should contain the title, end time and start time, the date of it etc.

It should look something like this:

type Task = {
    title : string,
    description : string,
    date : Date,
    startTime : number,
    endTime : number,
}

These are the important properties the tasks should have. The start time and end time properties are in number in this example holding the times of the tasks in minutes. So for example, for a task starting at 1:30 am and ending at 2:00 am, the startTime property will be 90 minutes and endTime will be 120 minutes.

Full disclosure, you are free to determine how you are going to handle the types as you want. The method mentioned above allows easy calculations for the durations and styling and UI for the week view in the app for example for calculating the height of a task which is based on the duration of the task and during dragging-and-dropping or resizing the task/event to change the duration of the task.

How to make the date picker?

The date picker component is important to get the select the required date to see or to create tasks.

The date picker tab will require for us to show all the dates in the month of the selected date.

The flow of this will go as such:

  1. Get the month of the selected date.

  2. Get the first and last day of the month.

  3. From the first day of the month, fill the remaining spots in the calendar from the last few days of the previous year. here the dates in the first week with 28, 29, 30 in grey are from April.

  4. From the last day of the month, fill the remaining spots in the calendar from the first few days of the next year. here the date 1 in grey is from the month of June.

You can follow the following guiding code to get the dates in the form of an array, but you can always have your own implementation

 function getDatesInCurrentMonth(displayDate): {
    const year = displayDate.getFullYear(); // store the current year
    const month = displayDate.getMonth(); // store the current month
    let firstDay = new Date(year, month, 1); // get the first day of the month (yyyy , mm ,dd format)
    let firstWeekFirstDay = new Date(year, month, 1 - firstDay.getDay()); //  go behind the number of days till sunday
    let lastDay = new Date(year, month + 1, 0); //  get the last day
    let lastWeekLastDay = new Date(year, month + 1, 6 - lastDay.getDay()); // go ahead till saturday
    const datesArray = [];
    let curr = new Date(firstWeekFirstDay); // initialize curr day for first day of the week
    while (curr <= lastWeekLastDay) {
      datesArray.push(new Date(curr)); // push the dates in the array
      curr.setDate(curr.getDate() + 1); // increment the curr date by one day
    }
    return datesArray;
  }

Now you can display those dates in the grid format.

How do we display the Week View grid?

This is where the fun begins. We are now going to the important part of our application.

Let's see the component and the skeleton of it to know what we need to build.

The final component looks like this

The component has few important aspects. The columns denote each day in a week, the columns have cells each denoting a certain time frame (each of 1hr in this example)

Lets have a closer look

Now to make the DayView components possible, we need the week of the current date.

We can get the entire week in the following manner:

function getDisplayWeek(): Date[] {
    const dayOfWeek = dateState.displayDate.getDay(); // get the day of the week, 0 for Sun, 1 for Mon etc
    const daysInSameWeek = [];

    const startingDay = new Date(); // initialise the starting day of the week
    startingDay.setDate(dateState.displayDate.getDate() - dayOfWeek); // get the first day of the week

    for (let i = 0; i < 7; i++) {
      const newDay = new Date(); 
      newDay.setDate(startingDay.getDate() + i); // get the ith day
      newDay.setHours(6);
      daysInSameWeek.push(newDay); // push the day in the array
    }
    return daysInSameWeek;
  }

This approach takes access of the first day of the week and gets the subsequent seven days.

You can then loop over the days and display these weeks.

Now, we need to worry about the time interval cells. We are going to be calling them TimeBoxes

For this, we need to find the time intervals for the app. Let's say, we want the intervals to be of 15mins.

As we discussed we are going to be storing the startTime and endTime by the count in minutes, hence, we need the 15-minute time intervals from 0 to 1440 and display them in the the DayView columns.

How do we display the task?

The component skeleton of the task is as follows:

Remember we are storing the date and time of the component? Well, based on the date, we can display them in the particular DayView component.

// Inside the DayView component

{
displayTasks.map((task) => (
    <TaskDisplay
        dayNumber={dayNumber}
        key={task.id}
        task={task}
    />
))
}

Now for the vertical distance for the task, we need to simply calculate the height of the TimeBox component and the time interval.

For example, if the TimeBox component is 30px in height and represents 15 minutes, 2px represents 1 minute. Now, if the startTime of a task is 1:30 am, that is 90 mins from 00:00; hence, the task component should be 90 X 2px = 180px from the top.

Let this task component be positioned relatively and the distance be the top css property.

How do we handle the dragging of the task?

Now, we will discuss the drag-and-drop feature of this.

We need to maintain the top and left property of the task to be updated during the dragging and dropping feature.

This is my approach to the problem at hand.

We need to maintain the start clientX and ClientY values when the mouse is down on the task component. While the mouse is moving, we simply need to update the left and top values with simply the difference between the previous and current ClientX and ClientY values

Here, we will update the time just like how we set the initial top value. Based on the verticalDistance travelled and convert it to the equivalent time in minutes and add it to the existing startTime. Similarly, during the horizontal dragging, the distance travelled divided by the width of a column will give the number of days to be added or subtracted by the original date.

newDate = oldDate + (horizontalDistance/columnWidth)

Another peculiar feature of the task component is the resizing of the component. It is demonstrated as follows:

In this example as well, we need to calculate the previous and current clientY difference, convert it to the distance equivalent and update the task likewise.

How do we handle the task stacking?

This might be the reason why you are here. This is the part where I might need a bit more attention from you than before.

The algorithm we are using to stack the conflicting tasks is the interval partitioning algorithm.

In this algorithm, we will be creating columns of tasks/events and put the tasks/events in the columns checking if the given task fits a particular column or not.

The steps to carry out the algorithm are as follows:

  1. Sort the tasks/events based on the start time

  2. Initiate the arrays of columns to put these tasks/events in(initially empty)

  3. Iterate through each task/event

  4. for each task, check if the last task of any column ends before the start of the task/event, and put the task/event in that column

  5. If we don't find a column whose last task ends before the start of the task in hand, we create a new column and add this task inside the new column.

  6. Return these columns of tasks to display them.

We just have to check that a certain task starts after a particular column's last task ends and if there exists no column where the task fits without any conflict, then we need to create a new column altogether.

Let's understand it with an example.

Imagine we were to stack the tasks: Video Launch, Friends Meet and Dentist. We have to display them using our interval partitioning algorithm.

This is the final ideal result

Now, we need to sort the tasks based on the start time. The sorted list of tasks is as follows:

Name of TaskStart TimeEnd Time
Video Launch1:304:00
Friend's Meet2:304:15
Dentist4:307:00

Now, let's simplify the view by viewing them without the overlapping. As you can see, we will be displaying these tasks in columns. Which event goes in which column depends upon how the task conflicts with other columns and decides whether to display the task in a new column altogether, or whether it can fit in a pre-existing column.

Now, lets go step by step with this implementation, starting with the first task of Video Launch

Since it is the first task and we have not created any columns. We will simply assign column 1 to it and fit it inside of it.

Now, the second task in the sorted list is the Friend's Meet. This task starts at 2:30.

The algorithm checks all the present columns. It sees column 1 and check when is the final task going to end. It learns that the final task ends at 4:30. But the Friend's Meet task starts at 2:30 so it clearly cannot fit in this column. The algorithm then checks for the next column. It realises that there are no remaining columns. Hence, it now has to assign a new column to this task.

Now, we have the final task Dentist starting at 4:30. The algorithm checks for the first column and checks if this task can fit inside of it or not. It checks column 1 and sees that column 1's last task ends at 4:30 and hence the Dentist task can fit since its start time is equal to the end time of column 1.

Hence, we add this task in column 1.

Now, as we have the required columns, we simply need to display them using some CSS to give the overlapping view.

Let's take a look at the pseudocode for the interval partitioning algorithm

And that's it. This is the way you can display these tasks in a stacked fashion.

That's a wrap!

I hope you liked this blog and learned something new from this.

If you have any doubts or want to connect with me, you can connect with me at:

Twitter: https://twitter.com/om_gate

LinkedIn: https://www.linkedin.com/in/omgate/

Here are the following videos whose reference I took while making this app:

System Design: https://www.youtube.com/watch?v=leo1FZ6vu1I

Interval Partitioning Algorithm: https://www.youtube.com/watch?v=i_G8hZYcKnI

Finally, if you want to see the final results of it, you can visit the website here

That's all folks! See you next time!