Created
January 3, 2025 15:29
-
-
Save justgeek/8ad575530de8cc513375cf7a39b0ae97 to your computer and use it in GitHub Desktop.
A simple react calendar component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useState } from 'react'; | |
import './Calendar.scss'; | |
import { CalendarMolecule } from 'molecules/Calendar'; | |
import { WeekBarMolecule } from 'molecules/WeekBar'; | |
import { VirtualScroller } from 'elements/VirtualScroller'; | |
import { instanceOf, func, bool } from 'prop-types'; | |
import classNames from 'classnames'; | |
const TOTAL_MONTHS = 200 * 12; // 100 years ahead + 100 years behind | |
const FRAME_HEIGHT = 270; // next month should be visible to make user aware of scrolling possibility | |
const SINGLE_CALENDAR_HEIGHT = 236; | |
const CURRENT_VISIBLE_ITEM_CLASS = 'initial-virtual-scroll'; | |
export function Calendar(props) { | |
const { | |
currentDate, | |
hasRange, | |
onSelect, | |
selectedDate: propSelectedDate, | |
selectedStartDate: propSelectedStartDate, | |
selectedEndDate: propSelectedEndDate, | |
} = props; | |
const [selectedStartDate, setSelectedStartDate] = useState(propSelectedStartDate); | |
const [selectedEndDate, setSelectedEndDate] = useState(propSelectedEndDate); | |
const [shouldUpdateDates, setShouldUpdateDates] = useState(null); | |
const [selectedDate, setSelectedDate] = useState(propSelectedDate); | |
const SETTINGS = { | |
viewportFrameHeight: FRAME_HEIGHT, | |
itemHeight: SINGLE_CALENDAR_HEIGHT, | |
amount: 1, | |
tolerance: 1, | |
minIndex: -TOTAL_MONTHS / 2, | |
maxIndex: TOTAL_MONTHS / 2, | |
initialScrollItemIndex: TOTAL_MONTHS / 2 + currentDate.getMonth(), // (TOTAL_MONTHS / 2) will always scroll to first month of current year | |
initialScrollItemClass: CURRENT_VISIBLE_ITEM_CLASS, // essential to have accurate scrolling (easier than padding, or margin calculation .. etc) | |
}; | |
useEffect(() => { | |
if (shouldUpdateDates) { | |
onSelect({ selectedStartDate, selectedEndDate }); | |
} | |
}, [shouldUpdateDates]); | |
useEffect(() => { | |
setSelectedStartDate(propSelectedStartDate); | |
}, [propSelectedStartDate]); | |
useEffect(() => { | |
setSelectedEndDate(propSelectedEndDate); | |
}, [propSelectedEndDate]); | |
const handleOnSelect = (date) => { | |
if (hasRange) { | |
if (selectedEndDate) { | |
setSelectedEndDate(null); | |
setSelectedStartDate(date); | |
} else if (selectedStartDate) { | |
setSelectedEndDate(date); | |
} else { | |
setSelectedStartDate(date); | |
} | |
setShouldUpdateDates(new Date()); | |
} else { | |
setSelectedDate(date); | |
onSelect(date); | |
} | |
}; | |
const generateMonths = (offset, limit) => { | |
const months = []; | |
const start = Math.max(SETTINGS.minIndex, offset); | |
const end = Math.min(offset + limit - 1, SETTINGS.maxIndex); | |
if (start <= end) { | |
for (let i = start; i <= end; i++) { | |
months.push({ index: i, month: i }); | |
} | |
} | |
return months; | |
}; | |
const year = currentDate.getFullYear(); | |
const rowTemplate = (item) => { | |
const className = classNames({ | |
'virtual-scroll-item': true, | |
[CURRENT_VISIBLE_ITEM_CLASS]: item.month === currentDate.getMonth(), | |
}); | |
return ( | |
<div className={className} key={item.index}> | |
<CalendarMolecule | |
year={year} | |
month={item.month} | |
onSelect={handleOnSelect} | |
selectedDate={selectedDate} | |
selectedStartDate={selectedStartDate} | |
selectedEndDate={selectedEndDate} | |
hasRange={hasRange} | |
/> | |
</div> | |
); | |
}; | |
return ( | |
<calendar-element> | |
<WeekBarMolecule /> | |
<VirtualScroller className="viewport" generateItems={generateMonths} settings={SETTINGS} row={rowTemplate} /> | |
</calendar-element> | |
); | |
} | |
Calendar.propTypes = { | |
/** | |
Current initial month to be shown extracted from the passed date Object | |
*/ | |
currentDate: instanceOf(Date), | |
hasRange: bool, | |
selectedDate: instanceOf(Date), | |
selectedEndDate: instanceOf(Date), | |
selectedStartDate: instanceOf(Date), | |
/** | |
Fires when any month day is clicked <br/> | |
returns: selected date as Date Object | |
*/ | |
onSelect: func, | |
}; | |
Calendar.defaultProps = { | |
currentDate: new Date(), | |
onSelect: () => {}, | |
hasRange: false, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment