397 lines
14 KiB
TypeScript
Executable File
397 lines
14 KiB
TypeScript
Executable File
import dayjs from "dayjs";
|
|
import isBetween from "dayjs/plugin/isBetween";
|
|
import React, { useCallback, useContext } from "react";
|
|
|
|
import { BG_COLOR, TEXT_COLOR } from "../../constants";
|
|
import DatepickerContext from "../../contexts/DatepickerContext";
|
|
import { formatDate, nextMonth, previousMonth, classNames as cn } from "../../helpers";
|
|
import { Period } from "../../types";
|
|
|
|
dayjs.extend(isBetween);
|
|
|
|
interface Props {
|
|
calendarData: {
|
|
date: dayjs.Dayjs;
|
|
days: {
|
|
previous: number[];
|
|
current: number[];
|
|
next: number[];
|
|
};
|
|
};
|
|
onClickPreviousDays: (day: number) => void;
|
|
onClickDay: (day: number) => void;
|
|
onClickNextDays: (day: number) => void;
|
|
}
|
|
|
|
const Days: React.FC<Props> = ({
|
|
calendarData,
|
|
onClickPreviousDays,
|
|
onClickDay,
|
|
onClickNextDays
|
|
}) => {
|
|
// Contexts
|
|
const {
|
|
primaryColor,
|
|
period,
|
|
changePeriod,
|
|
dayHover,
|
|
changeDayHover,
|
|
minDate,
|
|
maxDate,
|
|
disabledDates
|
|
} = useContext(DatepickerContext);
|
|
|
|
// Functions
|
|
const currentDateClass = useCallback(
|
|
(item: number) => {
|
|
const itemDate = `${calendarData.date.year()}-${calendarData.date.month() + 1}-${
|
|
item >= 10 ? item : "0" + item
|
|
}`;
|
|
if (formatDate(dayjs()) === formatDate(dayjs(itemDate)))
|
|
return TEXT_COLOR["500"][primaryColor as keyof (typeof TEXT_COLOR)["500"]];
|
|
return "";
|
|
},
|
|
[calendarData.date, primaryColor]
|
|
);
|
|
|
|
const activeDateData = useCallback(
|
|
(day: number) => {
|
|
const fullDay = `${calendarData.date.year()}-${calendarData.date.month() + 1}-${day}`;
|
|
let className = "";
|
|
|
|
if (dayjs(fullDay).isSame(period.start) && dayjs(fullDay).isSame(period.end)) {
|
|
className = ` ${BG_COLOR["500"][primaryColor]} c-text-white c-font-medium rounded-full`;
|
|
} else if (dayjs(fullDay).isSame(period.start)) {
|
|
className = ` ${BG_COLOR["500"][primaryColor]} c-text-white c-font-medium ${
|
|
dayjs(fullDay).isSame(dayHover) && !period.end
|
|
? "c-rounded-full"
|
|
: "c-rounded-l-full"
|
|
}`;
|
|
} else if (dayjs(fullDay).isSame(period.end)) {
|
|
className = ` ${BG_COLOR["500"][primaryColor]} c-text-white c-font-medium ${
|
|
dayjs(fullDay).isSame(dayHover) && !period.start
|
|
? "c-rounded-full"
|
|
: "c-rounded-r-full"
|
|
}`;
|
|
}
|
|
|
|
return {
|
|
active: dayjs(fullDay).isSame(period.start) || dayjs(fullDay).isSame(period.end),
|
|
className: className
|
|
};
|
|
},
|
|
[calendarData.date, dayHover, period.end, period.start, primaryColor]
|
|
);
|
|
|
|
const hoverClassByDay = useCallback(
|
|
(day: number) => {
|
|
let className = currentDateClass(day);
|
|
const fullDay = `${calendarData.date.year()}-${calendarData.date.month() + 1}-${
|
|
day >= 10 ? day : "0" + day
|
|
}`;
|
|
|
|
if (period.start && period.end) {
|
|
if (dayjs(fullDay).isBetween(period.start, period.end, "day", "[)")) {
|
|
return ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass(
|
|
day
|
|
)} dark:bg-white/10`;
|
|
}
|
|
}
|
|
|
|
if (!dayHover) {
|
|
return className;
|
|
}
|
|
|
|
if (period.start && dayjs(fullDay).isBetween(period.start, dayHover, "day", "[)")) {
|
|
className = ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass(
|
|
day
|
|
)} dark:bg-white/10`;
|
|
}
|
|
|
|
if (period.end && dayjs(fullDay).isBetween(dayHover, period.end, "day", "[)")) {
|
|
className = ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass(
|
|
day
|
|
)} dark:bg-white/10`;
|
|
}
|
|
|
|
if (dayHover === fullDay) {
|
|
const bgColor = BG_COLOR["500"][primaryColor];
|
|
className = ` transition-all duration-500 text-white font-medium ${bgColor} ${
|
|
period.start ? "rounded-r-full" : "rounded-l-full"
|
|
}`;
|
|
}
|
|
|
|
return className;
|
|
},
|
|
[calendarData.date, currentDateClass, dayHover, period.end, period.start, primaryColor]
|
|
);
|
|
|
|
const isDateTooEarly = useCallback(
|
|
(day: number, type: "current" | "previous" | "next") => {
|
|
if (!minDate) {
|
|
return false;
|
|
}
|
|
const object = {
|
|
previous: previousMonth(calendarData.date),
|
|
current: calendarData.date,
|
|
next: nextMonth(calendarData.date)
|
|
};
|
|
const newDate = object[type as keyof typeof object];
|
|
const formattedDate = newDate.set("date", day);
|
|
return dayjs(formattedDate).isSame(dayjs(minDate), "day")
|
|
? false
|
|
: dayjs(formattedDate).isBefore(dayjs(minDate));
|
|
},
|
|
[calendarData.date, minDate]
|
|
);
|
|
|
|
const isDateTooLate = useCallback(
|
|
(day: number, type: "current" | "previous" | "next") => {
|
|
if (!maxDate) {
|
|
return false;
|
|
}
|
|
const object = {
|
|
previous: previousMonth(calendarData.date),
|
|
current: calendarData.date,
|
|
next: nextMonth(calendarData.date)
|
|
};
|
|
const newDate = object[type as keyof typeof object];
|
|
const formattedDate = newDate.set("date", day);
|
|
return dayjs(formattedDate).isSame(dayjs(maxDate), "day")
|
|
? false
|
|
: dayjs(formattedDate).isAfter(dayjs(maxDate));
|
|
},
|
|
[calendarData.date, maxDate]
|
|
);
|
|
|
|
const isDateDisabled = useCallback(
|
|
(day: number, type: "current" | "previous" | "next") => {
|
|
if (isDateTooEarly(day, type) || isDateTooLate(day, type)) {
|
|
return true;
|
|
}
|
|
const object = {
|
|
previous: previousMonth(calendarData.date),
|
|
current: calendarData.date,
|
|
next: nextMonth(calendarData.date)
|
|
};
|
|
const newDate = object[type as keyof typeof object];
|
|
const formattedDate = `${newDate.year()}-${newDate.month() + 1}-${
|
|
day >= 10 ? day : "0" + day
|
|
}`;
|
|
|
|
if (!disabledDates || (Array.isArray(disabledDates) && !disabledDates.length)) {
|
|
return false;
|
|
}
|
|
|
|
let matchingCount = 0;
|
|
disabledDates?.forEach(dateRange => {
|
|
if (
|
|
dayjs(formattedDate).isAfter(dateRange.startDate) &&
|
|
dayjs(formattedDate).isBefore(dateRange.endDate)
|
|
) {
|
|
matchingCount++;
|
|
}
|
|
if (
|
|
dayjs(formattedDate).isSame(dateRange.startDate) ||
|
|
dayjs(formattedDate).isSame(dateRange.endDate)
|
|
) {
|
|
matchingCount++;
|
|
}
|
|
});
|
|
return matchingCount > 0;
|
|
},
|
|
[calendarData.date, isDateTooEarly, isDateTooLate, disabledDates]
|
|
);
|
|
|
|
const buttonClass = useCallback(
|
|
(day: number, type: "current" | "next" | "previous") => {
|
|
const baseClass = "c-flex c-items-center c-justify-center c-w-12 c-h-12 lg:c-w-10 lg:c-h-10";
|
|
if (type === "current") {
|
|
return cn(
|
|
baseClass,
|
|
!activeDateData(day).active
|
|
? hoverClassByDay(day)
|
|
: activeDateData(day).className,
|
|
isDateDisabled(day, type) && "c-text-gray-400 c-cursor-not-allowed"
|
|
);
|
|
}
|
|
return cn(baseClass, isDateDisabled(day, type) && "c-cursor-not-allowed", "c-text-gray-400");
|
|
},
|
|
[activeDateData, hoverClassByDay, isDateDisabled]
|
|
);
|
|
|
|
const checkIfHoverPeriodContainsDisabledPeriod = useCallback(
|
|
(hoverPeriod: Period) => {
|
|
if (!Array.isArray(disabledDates)) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < disabledDates.length; i++) {
|
|
if (
|
|
dayjs(hoverPeriod.start).isBefore(disabledDates[i].startDate) &&
|
|
dayjs(hoverPeriod.end).isAfter(disabledDates[i].endDate)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
[disabledDates]
|
|
);
|
|
|
|
const getMetaData = useCallback(() => {
|
|
return {
|
|
previous: previousMonth(calendarData.date),
|
|
current: calendarData.date,
|
|
next: nextMonth(calendarData.date)
|
|
};
|
|
}, [calendarData.date]);
|
|
|
|
const hoverDay = useCallback(
|
|
(day: number, type: string) => {
|
|
const object = getMetaData();
|
|
const newDate = object[type as keyof typeof object];
|
|
const newHover = `${newDate.year()}-${newDate.month() + 1}-${
|
|
day >= 10 ? day : "0" + day
|
|
}`;
|
|
|
|
if (period.start && !period.end) {
|
|
const hoverPeriod = { ...period, end: newHover };
|
|
if (dayjs(newHover).isBefore(dayjs(period.start))) {
|
|
hoverPeriod.start = newHover;
|
|
hoverPeriod.end = period.start;
|
|
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
|
|
changePeriod({
|
|
start: null,
|
|
end: period.start
|
|
});
|
|
}
|
|
}
|
|
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
|
|
changeDayHover(newHover);
|
|
}
|
|
}
|
|
|
|
if (!period.start && period.end) {
|
|
const hoverPeriod = { ...period, start: newHover };
|
|
if (dayjs(newHover).isAfter(dayjs(period.end))) {
|
|
hoverPeriod.start = period.end;
|
|
hoverPeriod.end = newHover;
|
|
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
|
|
changePeriod({
|
|
start: period.end,
|
|
end: null
|
|
});
|
|
}
|
|
}
|
|
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
|
|
changeDayHover(newHover);
|
|
}
|
|
}
|
|
},
|
|
[
|
|
changeDayHover,
|
|
changePeriod,
|
|
checkIfHoverPeriodContainsDisabledPeriod,
|
|
getMetaData,
|
|
period
|
|
]
|
|
);
|
|
|
|
const handleClickDay = useCallback(
|
|
(day: number, type: "previous" | "current" | "next") => {
|
|
function continueClick() {
|
|
if (type === "previous") {
|
|
onClickPreviousDays(day);
|
|
}
|
|
|
|
if (type === "current") {
|
|
onClickDay(day);
|
|
}
|
|
|
|
if (type === "next") {
|
|
onClickNextDays(day);
|
|
}
|
|
}
|
|
|
|
if (disabledDates?.length) {
|
|
const object = getMetaData();
|
|
const newDate = object[type as keyof typeof object];
|
|
const clickDay = `${newDate.year()}-${newDate.month() + 1}-${
|
|
day >= 10 ? day : "0" + day
|
|
}`;
|
|
|
|
if (period.start && !period.end) {
|
|
dayjs(clickDay).isSame(dayHover) && continueClick();
|
|
} else if (!period.start && period.end) {
|
|
dayjs(clickDay).isSame(dayHover) && continueClick();
|
|
} else {
|
|
continueClick();
|
|
}
|
|
} else {
|
|
continueClick();
|
|
}
|
|
},
|
|
[
|
|
dayHover,
|
|
disabledDates?.length,
|
|
getMetaData,
|
|
onClickDay,
|
|
onClickNextDays,
|
|
onClickPreviousDays,
|
|
period.end,
|
|
period.start
|
|
]
|
|
);
|
|
|
|
return (
|
|
<div className="c-grid c-grid-cols-7 c-gap-y-0.5 c-my-1">
|
|
{calendarData.days.previous.map((item, index) => (
|
|
<button
|
|
type="button"
|
|
key={index}
|
|
disabled={isDateDisabled(item, "previous")}
|
|
className={`${buttonClass(item, "previous")}`}
|
|
onClick={() => handleClickDay(item, "previous")}
|
|
onMouseOver={() => {
|
|
hoverDay(item, "previous");
|
|
}}
|
|
>
|
|
{item}
|
|
</button>
|
|
))}
|
|
|
|
{calendarData.days.current.map((item, index) => (
|
|
<button
|
|
type="button"
|
|
key={index}
|
|
disabled={isDateDisabled(item, "current")}
|
|
className={`${buttonClass(item, "current")}`}
|
|
onClick={() => handleClickDay(item, "current")}
|
|
onMouseOver={() => {
|
|
hoverDay(item, "current");
|
|
}}
|
|
>
|
|
{item}
|
|
</button>
|
|
))}
|
|
|
|
{calendarData.days.next.map((item, index) => (
|
|
<button
|
|
type="button"
|
|
key={index}
|
|
disabled={isDateDisabled(item, "next")}
|
|
className={`${buttonClass(item, "next")}`}
|
|
onClick={() => handleClickDay(item, "next")}
|
|
onMouseOver={() => {
|
|
hoverDay(item, "next");
|
|
}}
|
|
>
|
|
{item}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Days;
|