Welcome!
This is the second part of our three-part tutorial on creating a simple calendar using React and Day.js. In the first part, we built a custom calendar with React and Day.js.
You can check the first and the last parts here:
The first part:
Building a Custom Calendar with React and Day.js: A Step-by-Step Guide.
Oluwadahunsi A. ・ Jun 20
The last part:
Building a Date Range Picker with React and Day.js.
Oluwadahunsi A. ・ Jun 20
In this part, we'll enhance our calendar from the first part by adding a date picker functionality. Our goal is to build the date picker below.
Starter files.
If you have not gone through the first part where we built a basic calendar, don't worry. I'm providing all the necessary files so we can start together from the same point.
//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%;
}
//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';
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'];
export const Calendar = () => {
const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs(Date.now()));
const daysListGenerator = calendarObjectGenerator(currentDate);
const dateArrowHandler = (date: Dayjs) => {
setCurrentDate(date);
};
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);
};
const handleNextMonthClick = (day: number) => {
const dayInNextMonth = currentDate.add(1, 'month').date(day);
setCurrentDate(dayInNextMonth);
};
return (
<div className='calendar__container'>
<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 (
<button
key={`${el}/${index}`}
className='calendar__item gray'
onClick={() => handlePreviousMonthClick(el)}
>
{el}
</button>
);
})}
{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 (
<button
className='calendar__item gray'
key={`${idx}----${el}`}
onClick={() => handleNextMonthClick(el)}
>
{el}
</button>
);
})}
</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 the days in our calendar.
//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;
Adding an input field
To create a single date picker, we need to add an input field to our calender.
Ideally, we should use a masked input to ensure that only a valid date format is entered. Although we're currently checking if the entry is at least 10 characters to satisfy the DD.MM.YYYY
format, this alone isn't enough to ensure validity. Implementing a masked input will enforce the exact format we require.
Also, because we will be changing date formats often, let us add the customParseFormat plugin from dayjs. Here is the updated Calendar.tsx file.
//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'
//add this line
onClick={() => handlePreviousMonthClick(el)}
>
<button
className='calendar__item gray'
>
{el}
</button>
</div>
);
})}
{daysListGenerator.days.map((el, index) => {
return (
<div
key={`${index}-/-${el}`}
className='calendar__day'
//add this line
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'
//add this line
onClick={() => handleNextMonthClick(el)}
>
<button
className='calendar__item gray'
>
{el}
</button>
</div>
);
})}
</div>
</div>
</div>
</div>
);
};
At this point, you should have something similar to this:
There you have it, a simple date picker built upon our Calender. In the concluding part, we are going to be building a date range picker on this date picker.
See you in the next one.
Top comments (0)