Skip to content

Instantly share code, notes, and snippets.

@justgeek
Created January 3, 2025 15:29
Show Gist options
  • Save justgeek/8ad575530de8cc513375cf7a39b0ae97 to your computer and use it in GitHub Desktop.
Save justgeek/8ad575530de8cc513375cf7a39b0ae97 to your computer and use it in GitHub Desktop.
A simple react calendar component
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