DEV Community

Cover image for Building a Date Range Picker with React and Day.js.
Oluwadahunsi A.
Oluwadahunsi A.

Posted on

Building a Date Range Picker with React and Day.js.

Welcome to the third and final part of this tutorial series on creating a custom calendar using React and Day.js. In this session, we will build upon the date picker we developed in part two. Our goal is to enhance it further by enabling users to select multiple dates and highlighting the selected range — essentially creating a range picker feature.

Range picker

If you have not checked the first two parts, I encourage you to do so.

The first part:

The second part:

If you do wish to start from here, I'm providing all the necessary files you need to catch up.

Starter files.

//style

.calendar__container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 25px;
  width: max-content;
  background: #ffffff;
  box-shadow: 5px 10px 10px #dedfe2;
}

.month-year__layout {
  display: flex;
  margin: 0 auto;
  width: 100%;
  flex-direction: row;
  align-items: center;
  justify-content: space-around;
}

.year__layout,
.month__layout {
  width: 150px;
  display: flex;
  padding: 10px;
  font-weight: 600;
  align-items: center;
  text-transform: capitalize;
  justify-content: space-between;
}

.back__arrow,
.forward__arrow {
  cursor: pointer;
  background: transparent;
  border: none;
}

.back__arrow:hover,
.forward__arrow:hover {
  scale: 1.1;
  transition: scale 0.3s;
}

.days {
  display: grid;
  grid-gap: 0;
  width: 100%;
  grid-template-columns: repeat(7, 1fr);
}

.day {
  flex: 1;
  font-size: 16px;
  padding: 5px 7px;
  text-align: center;
}

.calendar__content {
  position: relative;
  background-color: transparent;
}

.calendar__items-list {
  text-align: center;
  width: 100%;
  height: max-content;
  overflow: hidden;
  display: grid;
  grid-gap: 0;
  list-style-type: none;
  grid-template-columns: repeat(7, 1fr);
}

.calendar__items-list:focus {
  outline: none;
}

.calendar__day {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
}

.calendar__item {
  position: relative;
  width: 50px;
  height: 50px;
  cursor: pointer;
  background: transparent;
  border-collapse: collapse;
  background-color: white;

  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  border: 1px solid transparent;
  z-index: 200;
}

button {
  margin: 0;
  display: inline;
  box-sizing: border-box;
}

.calendar__item:focus {
  outline: none;
}

.calendar__item.selected {
  font-weight: 700;
  border-radius: 50%;
  background: #1a73e8;
  color: white;
  outline: none;
  border: none;
}

.calendar__item.selectDay {
  position: relative;
  background: #1a73e8;
  color: white;
  border-radius: 50%;
  border: none;
  z-index: 200;
}

.calendar__item.gray,
.calendar__item.gray:hover {
  color: #c4cee5;
  display: flex;
  justify-content: center;
  align-items: center;
}

.input__container {
  display: flex;
  justify-content: space-around;
}

.input {
  height: 30px;
  border-radius: 8px;
  text-align: center;
  align-self: center;
  border: 1px solid #1a73e8;
}

.shadow {
  position: absolute;
  display: inline-block;
  z-index: 10;
  top: 0;
  background-color: #f4f6fa;
  height: 50px;
  width: 50px;
}

.shadow.right {
  left: 50%;
}

.shadow.left {
  right: 50%;
}

Enter fullscreen mode Exit fullscreen mode

The calendar component


//Calendar.tsx

import dayjs, { Dayjs } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat'; // add line
import backArrow from '../assets/images/back.svg';
import forwardArrow from '../assets/images/forward.svg';
import { useState } from 'react';
import { calendarObjectGenerator } from '../helper/calendarObjectGenerator';
import './style.css';

dayjs.extend(customParseFormat); // add line

const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'];

export const Calendar = () => {

  const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs(Date.now()));
  const [inputValue, setInputValue] = useState<string>('');

  const daysListGenerator = calendarObjectGenerator(currentDate);

  const dateArrowHandler = (date: Dayjs) => {
    setCurrentDate(date);
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const date = event.target.value;

    setInputValue(date);

    // check if the entered date is valid.
    const isValidDate = dayjs(date, 'DD.MM.YYYY').isValid();

    if (!isValidDate || date.length < 10) return;

    //if you pass date without specifying the format ('DD.MM.YYYY'), 
    // you might get an error when you decide to edit a selected date in the input field.

    setCurrentDate(dayjs(date, 'DD.MM.YYYY'));
  };



  const handlePreviousMonthClick = (day: number) => {

    const dayInPreviousMonth = currentDate.subtract(1, 'month').date(day);

    setCurrentDate(dayInPreviousMonth);

    setInputValue(dayInPreviousMonth.format('DD.MM.YYYY')); // add line
  };


   const handleCurrentMonthClick = (day: number) => {

    const dayInCurrentMonth = currentDate.date(day);

    setCurrentDate(dayInCurrentMonth);

    setInputValue(dayInCurrentMonth.format('DD.MM.YYYY')); // add line
  };


  const handleNextMonthClick = (day: number) => {

    const dayInNextMonth = currentDate.add(1, 'month').date(day);

    setCurrentDate(dayInNextMonth);

    setInputValue(dayInNextMonth.format('DD.MM.YYYY'));  // add line
  };



  return (
    <div className='calendar__container'>
      <input
        className='input'
        value={inputValue}
        onChange={handleInputChange}
      />
      <div className='control__layer'>
        <div className='month-year__layout'>
          <div className='year__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'year'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='title'>{currentDate.year()}</div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'year'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
          <div className='month__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'month'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='new-title'>
              {daysListGenerator.months[currentDate.month()]}
            </div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'month'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
        </div>
        <div className='days'>
          {weekDays.map((el, index) => (
            <div key={`${el}-${index}`} className='day'>
              {el}
            </div>
          ))}
        </div>
        <div className='calendar__content'>
          <div className={'calendar__items-list'}>
            {daysListGenerator.prevMonthDays.map((el, index) => {
              return (
               <div 
                key={`${el}/${index}`}
                className='calendar__day'
                onClick={() => handlePreviousMonthClick(el)}
                > 
                <button
                  className='calendar__item gray'
                                    >
                  {el}
                </button>
               </div>
              );
            })}
            {daysListGenerator.days.map((el, index) => {
              return (
                <div 
                key={`${index}-/-${el}`} 
                className='calendar__day' 
                onClick={() => handleCurrentMonthClick(el)                           
               >
                  <button
                    className={`calendar__item 
                      ${+el === +daysListGenerator.day ? 'selected' : ''}`}
                  >
                    <div className='day__layout'>
                      <div className='text'>{el.toString()}</div>
                    </div>
                  </button>
                </div>
              );
            })}

            {daysListGenerator.remainingDays.map((el, idx) => {
              return (
                <div 
                 key={`${idx}----${el}`}
                className='calendar__day'
                onClick={() => handleNextMonthClick(el)}
                > 
                <button 
                className='calendar__item gray' 
                >
                  {el}
                </button>
               </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

//back.svg
<?xml version="1.0" encoding="utf-8"?>
<svg width="20px" height="20px" viewBox="0 0 1000 1000" class="icon"  version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M768 903.232l-50.432 56.768L256 512l461.568-448 50.432 56.768L364.928 512z" fill="#000000" /></svg>

Enter fullscreen mode Exit fullscreen mode

//forward.svg
<?xml version="1.0" encoding="utf-8"?>
<svg width="20px" height="20px" viewBox="0 0 1000 1000" class="icon"  version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M256 120.768L306.432 64 768 512l-461.568 448L256 903.232 659.072 512z" fill="#000000" /></svg>

Enter fullscreen mode Exit fullscreen mode

The helper function for generating necessary data.


//calendarObjectGenerator.tsx

import dayjs, { Dayjs } from 'dayjs';

import LocaleData from 'dayjs/plugin/localeData';

dayjs.extend(LocaleData);

type GeneratedObjectType = {
  prevMonthDays: number[];
  days: number[];
  remainingDays: number[];
  day: number;
  months: string[];
};

export const calendarObjectGenerator = (
  currentDate: Dayjs
): GeneratedObjectType => {
  const numOfDaysInPrevMonth = currentDate.subtract(1, 'month').daysInMonth();
  const firstDayOfCurrentMonth = currentDate.startOf('month').day();
  return {
    days: Array.from(
      { length: currentDate.daysInMonth() },
      (_, index) => index + 1
    ),
    day: Number(currentDate.format('DD')),
    months: currentDate.localeData().months(),

    prevMonthDays: Array.from(
      { length: firstDayOfCurrentMonth },
      (_, index) => numOfDaysInPrevMonth - index
    ).reverse(),

    remainingDays: Array.from(
      { length: 6 - currentDate.endOf('month').day() },
      (_, index) => index + 1
    ),
  };
};


Enter fullscreen mode Exit fullscreen mode

//App.tsx

import { Calendar } from './Calendar/Calendar';

function App() {
  return (
    <>
      <Calendar />
    </>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Ensure that at this point, your app works as expected.

a date picker

Most of what we will be doing will be in our Calendar component, so
the other files will stay the same.

Adding a Second Input field.

Let us add another input for the second day. We can also use a masked input instead of having two different inputs.

Now we are going to be changing things up a bit in the Calendar component.

Our inputValue state will take a new look, the functions where we use setInputValue namely handlePreviousMonthClick, handleCurrentMonthClick and handleNextMonthClick will change and we will make some changes to our input elements as well:

Make the necessary changes in the Calendar.tsx file as shown below.


//Calendar.tsx


  //changes in state
  const [inputValue, setInputValue] = useState({
    firstInput: '',
    secondInput: '',
  });

  // destructure inputValue
  const {firstInput, secondInput} = inputValue;


  //changes in function
    const handlePreviousMonthClick = (day: number) => {
    const dayInPreviousMonth = currentDate.subtract(1, 'month').date(day);
    setCurrentDate(dayInPreviousMonth);

    // setInputValue(dayInPreviousMonth.format('DD.MM.YYYY')); remove this line
  };

  const handleCurrentMonthClick = (day: number) => {
    const dayInCurrentMonth = currentDate.date(day);
    setCurrentDate(dayInCurrentMonth);
    // setInputValue(dayInCurrentMonth.format('DD.MM.YYYY')); remove this line
  };

  const handleNextMonthClick = (day: number) => {
    const dayInNextMonth = currentDate.add(1, 'month').date(day);
    setCurrentDate(dayInNextMonth);
    // setInputValue(dayInNextMonth.format('DD.MM.YYYY')); remove this line
  };



  // changes in input fields
       <div className='input__container'> //add this container
        <input
          name='firstInput'        //add this line
          className='input'
          value={firstInput}       //add this line
          onChange={handleInputChange}
        />
        <input                     //add the second input
          name='secondInput'
          className='input'
          value={secondInput} 
          onChange={handleInputChange}
        />
      </div>

Enter fullscreen mode Exit fullscreen mode

The RangePicker function.

Next, we want to allow users to select two dates and visually mark the selected dates as well as the days in between them. Let's focus on implementing this feature now.

We are about to create a little complex function in the Calendar.tsx file, so bear with me as I explain each line in detail. The primary goal of this function is to allow us to select our firstInput (or first day) and secondInput (or second day). We'll name this function rangePicker.


// Calendar.tsx


...

  //check explanation
  const rangePicker = (day: Dayjs) => {

     const isTheSameYear =
      currentDate.year() === dayjs(firstInput, 'DD.MM.YYYY').get('year');

     const isFirstInputAfterSecondInput = dayjs(
      firstInput,
      'DD.MM.YYYY'
    ).isAfter(day);


    const isCurrentMonthLessThanFirstInputMonth =
      currentDate.month() < dayjs(firstInput, 'DD.MM.YYYY').get('month');


    const isCurrentYearLessThanFirstInputYear =
      currentDate.year() < dayjs(firstInput, 'DD.MM.YYYY').get('year');


      //we do not want to be able to select the same day

     const isTheSameDay =
      dayjs(firstInput, 'DD.MM.YYYY').format('DD.MM.YYYY') ===
      day.format('DD.MM.YYYY');


    if (!firstInput && !secondInput) {

    // if there is no firstInput and no secondInput, 
    // then the first clicked value should be the firstInput. 

      setInputValue({
        ...inputValue,
        firstInput: day.format('DD.MM.YYYY'),
      });

    } else if (firstInput && !secondInput) {
    //we do not want to be able to select the same day
     if (isTheSameDay) return;

    // if there is a firstInput value, and no secondInput,
    // check to see if the newly selected date is not before the firstInput date
    // if the newly selected date is earlier than the firstInput date then
    // swap the dates
    // if not, set the secondInput to the selected date.

        if (
       isFirstInputAfterSecondInput ||
        (isTheSameYear && isCurrentMonthLessThanFirstInputMonth) ||
        isCurrentYearLessThanFirstInputYear
      ) {
        setInputValue({
          ...inputValue,
          secondInput: firstInput,
          firstInput: day.format('DD.MM.YYYY'),
        });

        return;
      }


      setInputValue({
        ...inputValue,
        secondInput: day.format('DD.MM.YYYY'),
      });

    } else if (firstInput && secondInput) {

    //if the user clicks again when there are both inputs,
    // clear the secondInput and set the firstInput to the selected value. 
      setInputValue({
        firstInput: day.format('DD.MM.YYYY'),
        secondInput: '',
      });
    }
  };


  ...



Enter fullscreen mode Exit fullscreen mode

Do not worry if your calendar does not look exactly like the images below yet, we are going to get there.

The rangePicker function checks if there is a firstInput and secondInput.

  • If there is no firstInput and no secondInput, then we can assume that the first clicked value should be the first input. Therefore, we set the first clicked value to firstInput.

  • If there is a firstInput value and no secondInput value then we can assume that the user wants to select a secondInput value. The selected value is then set to secondInput. But wait, there is a caveat.

    • We do not want the second day to be the same as the first day, hence the check for isTheSameDay. As you can see, we are not allowed to select the same day.

Range picker

  • We also need to check if the second selected date is after (or greater than) the first date. If the firstInput value is after ( or greater than) the second selected date, then we can just swap the dates out. As shown below.

    You can see that when 30th June was selected as the firstInput and 3rd June as the second input, the dates swapped automatically.

Range picker

  • Lastly if there are both firstInput and secondInput, then when the user clicks on another day we just need to clear the secondInput and set the firstInput to the clicked date.

For simplicity, we will only be using this function in the current month. We can always customize it to meet our specifications.

So call the range picker function inside the handleCurrentMonthClick function like so:

  const handleCurrentMonthClick = (day: number) => {
    const dayInCurrentMonth = currentDate.date(day);
    setCurrentDate(dayInCurrentMonth);

    rangePicker(dayInCurrentMonth); // add line 
  };
Enter fullscreen mode Exit fullscreen mode

Now we should be able to select two dates one for the firstInput and the other for the secondInput.

That is not all though, we need to be able to highlight both of the selected days and the days in-between them to get something like this:

Range picker

A quick reminder: If you ever get lost, do not worry, I will provide the entire files at the end.

Highlighting Days of the Current Month

Let’s start by highlighting the selected days. Make the necessary adjustments in your Calendar.tsx file.


//Calendar.tsx

//add function
  const highlightDays = (el: number) => {
    if (!secondInput) return;

    return (
      currentDate?.set('date', el).isAfter(dayjs(firstInput, 'DD.MM.YYYY')) &&
      currentDate?.set('date', el).isBefore(dayjs(secondInput, 'DD.MM.YYYY'))
    );
  };

...

return (

...

         {daysListGenerator.days.map((el, index) => {
           //add lines
           const formattedCurrentMonthDates = currentDate
                .set('date', el)
                ?.format('DD.MM.YYYY');

              const isDayEqualFirstInput =
                firstInput === formattedCurrentMonthDates;

              const isDayEqualSecondInput =
                secondInput === formattedCurrentMonthDates;

              const applyGrayBackground = !(
                isDayEqualFirstInput || isDayEqualSecondInput
              );

              return (
                <div
                  key={`${index}-/-${el}`}
                  className='calendar__day'
                  onClick={() => handleCurrentMonthClick(el)}
                >
                  <button

                    className={`calendar__item   //add lines
                    ${
                        +el === +daysListGenerator.day &&
                       !(firstInput || secondInput)
                          ? 'selected'
                          : isDayEqualFirstInput || 
                           isDayEqualSecondInput
                          ? 'selectDay'
                          : ''
                      }`}

                  style={{                       //add lines 
                      backgroundColor: `${
                        applyGrayBackground && highlightDays(el)
                          ? '#F4F6FA'
                          : ''
                      }`,
                    }}


...
Enter fullscreen mode Exit fullscreen mode

While mapping through the days array, we will create some variables to help us pick some important moments.

formattedCurrentMonthDates ensures that we convert the numbers in the array to dates of the format ‘DD.MM.YYYY’

isDayEqualFirstInput and isDayEqualSecondInput are checking the day that is equal to the firstInput or the secondInput value, since we need to know these days and style them accordingly. If the day is equal to either firstInput or secondInput, we can then add the selectDay class for styling.

applyGrayBackground checks if the value is not equal to the firstInput or secondInput. We are applying gray background to the days that are neither firstInput or secondInput but that are in-between them.

As you might have noticed, the highlightDays function takes in each of the integers we are mapping through, converts them to dates and checks if the date is between the firstInput and the secondInput.

Highlighting Days of the Previous months.

Next is to ensure that the days of the previous months are also properly highlighted when they fall between selected days. Something like this:

Range picker

To do this, let’s add some lines to the prevMonthDays map function.


//Calendar.tsx
...

//add this Day.js plugin

import isBetween from 'dayjs/plugin/isBetween';

...

// add line
dayjs.extend(isBetween);

...

return (

...
 //add lines
            {daysListGenerator.prevMonthDays.map((el, index) => {


                const formatPrevMonthsDates = currentDate
                .subtract(1, 'month')
                .set('date', el)
                ?.format('DD.MM.YYYY');

   //add lines              
                const isBetween = currentDate
                .subtract(1, 'month')
                .set('date', el)
                .isBetween(
                  dayjs(firstInput, 'DD.MM.YYYY'),
                  dayjs(secondInput, 'DD.MM.YYYY')
                );

   //add line

    const isFirstDay = firstInput === formatPrevMonthsDates;

              return (
                <div
                  className='calendar__day'
                  key={`${el}/${index}`}
                  onClick={() => handlePreviousMonthClick(el)}
                >
                  <button
                    className='calendar__item gray'
                    style={{                       //add lines
                      backgroundColor: `${
                        isBetween || (isFirstDay && secondInput)
                          ? '#F4F6FA'
                          : ''
                      }`,
                    }}
                  >
                    {el}
                  </button>
                </div>
              );
            })}
  ...

Enter fullscreen mode Exit fullscreen mode

Again, we will convert the numbers in the prevMonthDays array into the date of the previous month. This is why we are calling the subtract(1, 'month') method on the currentDate.

The next thing is to check if the date is between the firstInput and the secondInput. To do this, we need to include another DayJs isBetween plugin.

Lastly we use isFirstDay to check if the day of the prevMonth is equal to the selected firstInput, we might need this for styling as well.

If you have done everything, you should be able to see the prevMonthDays highlighted provided they fall between the firstInput and the lastInput.

Highlighting The Remaining Days (days of the next month).

Lastly we should do the same thing for the remainingDays. We will highlight them when they fall between the firstInput and the lastInput. See, they are currently not highlighted so let’s fix that.

range picker

We are going to create another super contrived function to handle the possible cases with the goal of achieving this.

Range picker

Let us call the function remainingDaysIsBetween. Create this function inside your Calendar.tsx file.


  ...
      const remainingDaysIsBetween = () => {
                const firstDay = dayjs(firstInput, 'DD.MM.YYYY');
                const secondDay = dayjs(secondInput, 'DD.MM.YYYY');

                const firstYear = firstDay?.year();
                const secondYear = secondDay?.year();

                if (
                  firstYear === secondYear &&
                  currentDate.year() === firstYear
                ) {
                  return (
                    firstDay &&
                    firstDay?.month() <= currentDate.month() &&
                    secondDay &&
                    secondDay?.month() > currentDate.month()
                  );
                } else if (
                  secondYear &&
                  firstYear &&
                  secondYear > firstYear &&
                  currentDate.year() <= secondYear
                ) {
                  if (
                    currentDate.year() === firstYear &&
                    currentDate.month() < firstDay?.month()
                  ) {
                    return;
                  }
                  if (currentDate.year() < secondYear) {
                    return (
                      (firstDay &&
                        firstDay?.year() === currentDate.year() &&
                        firstDay?.month() >= currentDate.month()) ||
                      (secondDay &&
                        currentDate.year() <= secondDay?.year() &&
                        currentDate.year() >= firstDay?.year())
                    );
                  } else {
                    return (
                      (firstDay &&
                        firstDay?.year() === currentDate.year() &&
                        firstDay?.month() <= currentDate.month()) ||
                      (secondDay &&
                        currentDate.year() <= secondDay?.year() &&
                        secondDay?.month() > currentDate.month())
                    );
                  }
                }
              };
     ...
Enter fullscreen mode Exit fullscreen mode

Now there is a lot going on here because I was trying to catch the obvious edge cases. You can play around with it and optimize the function.

The function remainingDaysIsBetween checks whether the current date falls within a specific range defined by two input dates (firstInput and secondInput). These dates are expected to be in the format DD.MM.YYYY. Let's break down the conditions step by step:

const firstDay = dayjs(firstInput, 'DD.MM.YYYY');
const secondDay = dayjs(secondInput, 'DD.MM.YYYY');

const firstYear = firstDay?.year();
const secondYear = secondDay?.year();
Enter fullscreen mode Exit fullscreen mode

firstInput and secondInput are parsed into Day.js objects (firstDay and secondDay).

We then extract the years of the parsed objects into firstYear and secondYear .

It is very important to remeber that currentDate is a Day.js object representing the last selected date and its value changes as you move back and forth between months and years with the control arrows. Let’s look further into the conditions in the remainingDaysIsBetween function.

Condition 1

We check that both the firstInput and secondInput dates are in the Same Year and that the current year is the same as the first year:

if (firstYear === secondYear && currentDate.year() === firstYear) {
  return (
    firstDay &&
    firstDay?.month() <= currentDate.month() &&
    secondDay &&
    secondDay?.month() > currentDate.month()
  );
}
Enter fullscreen mode Exit fullscreen mode

If the condition is true, then we ensure that we are only highlighting the remaining days for that particular month where the firstInput’s month is less than or equal to the currentDate’s month and the secondInput’s month is greater than the currentDate’s month.

For instance when the firstInput is 18.06.2024 and the secondInput is 16.07.2024. When move between months with our month control arrows, when we are in June,

firstDay?.month() <= currentDate.month() returns true because the firstDay.month is June and the currentDate.month() is also June (remember that currentDate changes as you move around) also, the second check secondDay?.month() > currentDate.month() returns true because secondDay.month() is July and remember that while we are viewing June, currentDate.month() is still June. Because both inequalities return true when we are viewing June, the remaining days in June are highlighted with a gray color.

However when we move forward to July, the second check secondDay?.month() > currentDate.month() returns false since secondDay?.month() is July and currentDate.month()** is also July. Hence the remaining days in July are not highlighted

Range picker

Range picker

Condition 2

We check for when both dates (firstInput and secondInput) are in different years and the current date is less than or equal to the secondInput year.

else if (secondYear && firstYear && secondYear > firstYear && currentDate.year() <= secondYear) {
  // Sub-condition 2.1: Current Year Matches First Year but Before First Month
  if (currentDate.year() === firstYear && currentDate.month() < firstDay?.month()) {
    return;
  }
  // Sub-condition 2.2: Current Year is Before Second Year
  if (currentDate.year() < secondYear) {
    return (
      (firstDay &&
        firstDay?.year() === currentDate.year() &&
        firstDay?.month() >= currentDate.month()) ||
      (secondDay &&
        currentDate.year() <= secondDay?.year() &&
        currentDate.year() >= firstDay?.year())
    );
  } else {
    // Sub-condition 2.3: Current Year Matches Second Year
    return (
      (firstDay &&
        firstDay?.year() === currentDate.year() &&
        firstDay?.month() <= currentDate.month()) ||
      (secondDay &&
        currentDate.year() <= secondDay?.year() &&
        secondDay?.month() > currentDate.month())
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

Following the first condition, we can play around with other conditions to meet our specifications. If you have done everything as explained in this article, you should now have a fully functional Calendar with a range picker. There are still multiple ways to improve on this code.

If you have followed along till this point, you can as well have the whole changes we have made to the Calendar.tsx file, so here you have it:

//Calendar.tsx

import dayjs, { Dayjs } from 'dayjs';
import backArrow from '../assets/images/back.svg';
import forwardArrow from '../assets/images/forward.svg';
import './style.css';
import { useState } from 'react';
import { calendarObjectGenerator } from '../helper/calendarObjectGenerator';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isBetween from 'dayjs/plugin/isBetween';

dayjs.extend(customParseFormat);
dayjs.extend(isBetween);

const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'];

export const Calendar = () => {
  const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs(Date.now()));

  const [inputValue, setInputValue] = useState({
    firstInput: '',
    secondInput: '',
  });

  const { firstInput, secondInput } = inputValue;

  const daysListGenerator = calendarObjectGenerator(currentDate);

  const dateArrowHandler = (date: Dayjs) => {
    setCurrentDate(date);
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const date = event.target.value;

    setInputValue({ ...inputValue, [event.target.name]: event.target.value });

    // check if the entered date is valid.
    const isValidDate = dayjs(date, 'DD.MM.YYYY').isValid();

    if (!isValidDate || date.length < 10) return;

    setCurrentDate(dayjs(date, 'DD.MM.YYYY'));
  };

  const rangePicker = (day: Dayjs) => {
    const isTheSameYear =
      currentDate.year() === dayjs(firstInput, 'DD.MM.YYYY').get('year');
    const isFirstInputAfterSecondInput = dayjs(
      firstInput,
      'DD.MM.YYYY'
    ).isAfter(day);

    const isCurrentMonthLessThanFirstInputMonth =
      currentDate.month() < dayjs(firstInput, 'DD.MM.YYYY').get('month');
    const isCurrentYearLessThanFirstInputYear =
      currentDate.year() < dayjs(firstInput, 'DD.MM.YYYY').get('year');

    const isTheSameDay =
      dayjs(firstInput, 'DD.MM.YYYY').format('DD.MM.YYYY') ===
      day.format('DD.MM.YYYY');

    if (!firstInput && !secondInput) {
      setInputValue({
        ...inputValue,
        firstInput: day.format('DD.MM.YYYY'),
      });
    } else if (firstInput && !secondInput) {
      if (isTheSameDay) return;

      if (
        isFirstInputAfterSecondInput ||
        (isTheSameYear && isCurrentMonthLessThanFirstInputMonth) ||
        isCurrentYearLessThanFirstInputYear
      ) {
        setInputValue({
          ...inputValue,
          secondInput: firstInput,
          firstInput: day.format('DD.MM.YYYY'),
        });

        return;
      }

      setInputValue({
        ...inputValue,
        secondInput: day.format('DD.MM.YYYY'),
      });
    } else if (firstInput && secondInput) {
      setInputValue({
        firstInput: day.format('DD.MM.YYYY'),
        secondInput: '',
      });
    }
  };

  const handlePreviousMonthClick = (day: number) => {
    const dayInPreviousMonth = currentDate.subtract(1, 'month').date(day);
    setCurrentDate(dayInPreviousMonth);
  };

  const handleCurrentMonthClick = (day: number) => {
    const dayInCurrentMonth = currentDate.date(day);
    setCurrentDate(dayInCurrentMonth);

    rangePicker(dayInCurrentMonth);
  };

  const handleNextMonthClick = (day: number) => {
    const dayInNextMonth = currentDate.add(1, 'month').date(day);

    setCurrentDate(dayInNextMonth);
  };

  const highlightDays = (el: number) => {
    if (!secondInput) return;

    return (
      currentDate?.set('date', el).isAfter(dayjs(firstInput, 'DD.MM.YYYY')) &&
      currentDate?.set('date', el).isBefore(dayjs(secondInput, 'DD.MM.YYYY'))
    );
  };

 const remainingDaysIsBetween = () => {
     const firstDay = dayjs(firstInput, 'DD.MM.YYYY');
      const secondDay = dayjs(secondInput, 'DD.MM.YYYY');

      const firstYear = firstDay?.year();
      const secondYear = secondDay?.year();

       if (
          firstYear === secondYear &&
           currentDate.year() === firstYear
          ) {

           return (
            firstDay &&
               firstDay?.month() <= currentDate.month() &&
                secondDay &&
                 secondDay?.month() > currentDate.month()
                  );
                } else if (
                  secondYear &&
                  firstYear &&
                  secondYear > firstYear &&
                  currentDate.year() <= secondYear
                ) {
                  if (
                    currentDate.year() === firstYear &&
                    currentDate.month() < firstDay?.month()
                  ) {
                    return;
                  }
                  if (currentDate.year() < secondYear) {
                    return (
                      (firstDay &&
                        firstDay?.year() === currentDate.year() &&
                        firstDay?.month() >= currentDate.month()) ||
                      (secondDay &&
                        currentDate.year() <= secondDay?.year() &&
                        currentDate.year() >= firstDay?.year())
                    );
                  } else {
                    return (
                      (firstDay &&
                        firstDay?.year() === currentDate.year() &&
                        firstDay?.month() <= currentDate.month()) ||
                      (secondDay &&
                  currentDate.year() <= secondDay?.year() &&
                    secondDay?.month() > currentDate.month())
                  );
          }
       }
   };


  return (
    <div className='calendar__container'>
      <div className='input__container'>
        <input
          name='firstInput'
          className='input'
          value={firstInput}
          onChange={handleInputChange}
        />
        <input
          name='secondInput'
          className='input'
          value={secondInput}
          onChange={handleInputChange}
        />
      </div>

      <div className='control__layer'>
        <div className='month-year__layout'>
          <div className='year__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'year'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='title'>{currentDate.year()}</div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'year'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
          <div className='month__layout'>
            <button
              className='back__arrow'
              onClick={() => dateArrowHandler(currentDate.subtract(1, 'month'))}
            >
              <img src={backArrow} alt='back arrow' />
            </button>
            <div className='new-title'>
              {daysListGenerator.months[currentDate.month()]}
            </div>
            <button
              className='forward__arrow'
              onClick={() => dateArrowHandler(currentDate.add(1, 'month'))}
            >
              <img src={forwardArrow} alt='forward arrow' />
            </button>
          </div>
        </div>
        <div className='days'>
          {weekDays.map((el, index) => (
            <div key={`${el}-${index}`} className='day'>
              {el}
            </div>
          ))}
        </div>
        <div className='calendar__content'>
          <div className={'calendar__items-list'}>
            {daysListGenerator.prevMonthDays.map((el, index) => {
              const formatPrevMonthsDates = currentDate
                .subtract(1, 'month')
                .set('date', el)
                ?.format('DD.MM.YYYY');

              const isBetween = currentDate
                .subtract(1, 'month')
                .set('date', el)
                .isBetween(
                  dayjs(firstInput, 'DD.MM.YYYY'),
                  dayjs(secondInput, 'DD.MM.YYYY')
                );

              const isFirstDay = firstInput === formatPrevMonthsDates;

              return (
                <div
                  className='calendar__day'
                  key={`${el}/${index}`}
                  onClick={() => handlePreviousMonthClick(el)}
                >
                  <button
                    className='calendar__item gray'
                    style={{
                      backgroundColor: `${
                        isBetween || (isFirstDay && secondInput)
                          ? '#F4F6FA'
                          : ''
                      }`,
                    }}
                  >
                    {el}
                  </button>
                </div>
              );
            })}
            {daysListGenerator.days.map((el, index) => {
              const formattedCurrentMonthDates = currentDate
                .set('date', el)
                ?.format('DD.MM.YYYY');

              const isDayEqualFirstInput =
                firstInput === formattedCurrentMonthDates;

              const isDayEqualSecondInput =
                secondInput === formattedCurrentMonthDates;

              const applyGrayBackground = !(
                isDayEqualFirstInput || isDayEqualSecondInput
              );

              return (
                <div
                  key={`${index}-/-${el}`}
                  className='calendar__day'
                  onClick={() => handleCurrentMonthClick(el)}
                >
                  <button
                    className={`calendar__item 
                      ${
                        +el === +daysListGenerator.day &&
                        !(firstInput || secondInput)
                          ? 'selected'
                          : isDayEqualFirstInput || isDayEqualSecondInput
                          ? 'selectDay'
                          : ''
                      }`}
                    style={{
                      backgroundColor: `${
                        applyGrayBackground && highlightDays(el)
                          ? '#F4F6FA'
                          : ''
                      }`,
                    }}
                  >
                    <div className='day__layout'>
                      <div className='text'>{el.toString()}</div>
                    </div>
                  </button>
                  {firstInput && secondInput && isDayEqualFirstInput && (
                    <span className='shadow right'></span>
                  )}
                  {firstInput && secondInput && isDayEqualSecondInput && (
                    <span className='shadow left'></span>
                  )}
                </div>
              );
            })}

            {daysListGenerator.remainingDays.map((el, idx) => {
              return (
                <div
                  key={`${idx}----${el}`}
                  className='calendar__day'
                  onClick={() => handleNextMonthClick(el)}
                >
                  <button
                    className='calendar__item gray'
                    style={{
                      background: `${
                        remainingDaysIsBetween() ? '#F4F6FA' : ''
                      }`,
                    }}
                  >
                    {el}
                  </button>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

Thank you for following this till the end of this tutorial.

If you so desire, you can play around with what we have done so far, and find several areas to improve.

Top comments (0)