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.
If you have not checked the first two parts, I encourage you to do so.
The first part:
Building a Custom Calendar with React and Day.js: A Step-by-Step Guide.
Oluwadahunsi A. ・ Jun 20
The second part:
Building a simple Date Picker with React and Day.js
Oluwadahunsi A. ・ Jun 20
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%;
}
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>
);
};
//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>
//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>
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
),
};
};
//App.tsx
import { Calendar } from './Calendar/Calendar';
function App() {
return (
<>
<Calendar />
</>
);
}
export default App;
Ensure that at this point, your app works as expected.
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>
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: '',
});
}
};
...
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 nosecondInput
, then we can assume that the first clicked value should be the first input. Therefore, we set the first clicked value tofirstInput
.-
If there is a
firstInput
value and nosecondInput
value then we can assume that the user wants to select asecondInput
value. The selected value is then set tosecondInput
. 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.
-
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.
- Lastly if there are both
firstInput
andsecondInput
, then when the user clicks on another day we just need to clear thesecondInput
and set thefirstInput
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
};
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:
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'
: ''
}`,
}}
...
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:
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>
);
})}
...
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.
We are going to create another super contrived function to handle the possible cases with the goal of achieving this.
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())
);
}
}
};
...
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();
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()
);
}
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
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())
);
}
}
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>
);
};
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)