2021-08-08 15:13:31 +00:00
|
|
|
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
2021-06-24 22:15:18 +00:00
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import dayjs, { Dayjs } from "dayjs";
|
2021-06-30 01:35:08 +00:00
|
|
|
import utc from "dayjs/plugin/utc";
|
|
|
|
import timezone from "dayjs/plugin/timezone";
|
|
|
|
import getSlots from "@lib/slots";
|
2021-07-15 14:10:26 +00:00
|
|
|
import dayjsBusinessDays from "dayjs-business-days";
|
2021-07-07 10:43:13 +00:00
|
|
|
|
2021-07-15 14:10:26 +00:00
|
|
|
dayjs.extend(dayjsBusinessDays);
|
2021-06-30 01:35:08 +00:00
|
|
|
dayjs.extend(utc);
|
|
|
|
dayjs.extend(timezone);
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-06-30 15:41:38 +00:00
|
|
|
const DatePicker = ({
|
|
|
|
weekStart,
|
|
|
|
onDatePicked,
|
|
|
|
workingHours,
|
|
|
|
organizerTimeZone,
|
|
|
|
inviteeTimeZone,
|
|
|
|
eventLength,
|
2021-07-13 16:10:22 +00:00
|
|
|
date,
|
2021-07-15 14:10:26 +00:00
|
|
|
periodType = "unlimited",
|
|
|
|
periodStartDate,
|
|
|
|
periodEndDate,
|
|
|
|
periodDays,
|
|
|
|
periodCountCalendarDays,
|
2021-07-22 22:52:27 +00:00
|
|
|
minimumBookingNotice,
|
2021-06-30 15:41:38 +00:00
|
|
|
}) => {
|
2021-06-30 01:35:08 +00:00
|
|
|
const [calendar, setCalendar] = useState([]);
|
2021-07-13 16:10:22 +00:00
|
|
|
const [selectedMonth, setSelectedMonth] = useState<number>();
|
|
|
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
2021-06-30 01:35:08 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2021-07-13 16:10:22 +00:00
|
|
|
if (date) {
|
|
|
|
setSelectedDate(dayjs(date).tz(inviteeTimeZone));
|
|
|
|
setSelectedMonth(dayjs(date).tz(inviteeTimeZone).month());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-07-15 14:10:26 +00:00
|
|
|
if (periodType === "range") {
|
|
|
|
setSelectedMonth(dayjs(periodStartDate).tz(inviteeTimeZone).month());
|
|
|
|
} else {
|
|
|
|
setSelectedMonth(dayjs().tz(inviteeTimeZone).month());
|
|
|
|
}
|
2021-06-30 01:35:08 +00:00
|
|
|
}, []);
|
2021-06-21 20:26:04 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2021-06-24 22:15:18 +00:00
|
|
|
if (selectedDate) onDatePicked(selectedDate);
|
2021-06-27 21:28:35 +00:00
|
|
|
}, [selectedDate]);
|
2021-06-21 20:26:04 +00:00
|
|
|
|
|
|
|
// Handle month changes
|
|
|
|
const incrementMonth = () => {
|
|
|
|
setSelectedMonth(selectedMonth + 1);
|
2021-06-24 22:15:18 +00:00
|
|
|
};
|
2021-06-21 20:26:04 +00:00
|
|
|
|
|
|
|
const decrementMonth = () => {
|
|
|
|
setSelectedMonth(selectedMonth - 1);
|
2021-06-24 22:15:18 +00:00
|
|
|
};
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (!selectedMonth) {
|
|
|
|
// wish next had a way of dealing with this magically;
|
|
|
|
return;
|
|
|
|
}
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
const inviteeDate = dayjs().tz(inviteeTimeZone).month(selectedMonth);
|
2021-06-24 22:15:18 +00:00
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
const isDisabled = (day: number) => {
|
|
|
|
const date: Dayjs = inviteeDate.date(day);
|
2021-07-15 14:10:26 +00:00
|
|
|
|
|
|
|
switch (periodType) {
|
|
|
|
case "rolling": {
|
|
|
|
const periodRollingEndDay = periodCountCalendarDays
|
|
|
|
? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day")
|
|
|
|
: dayjs().tz(organizerTimeZone).businessDaysAdd(periodDays, "days").endOf("day");
|
|
|
|
return (
|
|
|
|
date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
|
|
|
|
date.endOf("day").isAfter(periodRollingEndDay) ||
|
|
|
|
!getSlots({
|
|
|
|
inviteeDate: date,
|
|
|
|
frequency: eventLength,
|
2021-07-22 22:52:27 +00:00
|
|
|
minimumBookingNotice,
|
2021-07-15 14:10:26 +00:00
|
|
|
workingHours,
|
|
|
|
organizerTimeZone,
|
|
|
|
}).length
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
case "range": {
|
|
|
|
const periodRangeStartDay = dayjs(periodStartDate).tz(organizerTimeZone).endOf("day");
|
|
|
|
const periodRangeEndDay = dayjs(periodEndDate).tz(organizerTimeZone).endOf("day");
|
|
|
|
return (
|
|
|
|
date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
|
|
|
|
date.endOf("day").isBefore(periodRangeStartDay) ||
|
|
|
|
date.endOf("day").isAfter(periodRangeEndDay) ||
|
|
|
|
!getSlots({
|
|
|
|
inviteeDate: date,
|
|
|
|
frequency: eventLength,
|
2021-07-22 22:52:27 +00:00
|
|
|
minimumBookingNotice,
|
2021-07-15 14:10:26 +00:00
|
|
|
workingHours,
|
|
|
|
organizerTimeZone,
|
|
|
|
}).length
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
case "unlimited":
|
|
|
|
default:
|
|
|
|
return (
|
|
|
|
date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
|
|
|
|
!getSlots({
|
|
|
|
inviteeDate: date,
|
|
|
|
frequency: eventLength,
|
2021-07-22 22:52:27 +00:00
|
|
|
minimumBookingNotice,
|
2021-07-15 14:10:26 +00:00
|
|
|
workingHours,
|
|
|
|
organizerTimeZone,
|
|
|
|
}).length
|
|
|
|
);
|
|
|
|
}
|
2021-06-30 01:35:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Set up calendar
|
|
|
|
const daysInMonth = inviteeDate.daysInMonth();
|
|
|
|
const days = [];
|
|
|
|
for (let i = 1; i <= daysInMonth; i++) {
|
|
|
|
days.push(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create placeholder elements for empty days in first week
|
|
|
|
let weekdayOfFirst = inviteeDate.date(1).day();
|
|
|
|
if (weekStart === "Monday") {
|
|
|
|
weekdayOfFirst -= 1;
|
|
|
|
if (weekdayOfFirst < 0) weekdayOfFirst = 6;
|
|
|
|
}
|
|
|
|
const emptyDays = Array(weekdayOfFirst)
|
|
|
|
.fill(null)
|
|
|
|
.map((day, i) => (
|
|
|
|
<div key={`e-${i}`} className={"text-center w-10 h-10 rounded-full mx-auto"}>
|
|
|
|
{null}
|
|
|
|
</div>
|
|
|
|
));
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
// Combine placeholder days with actual days
|
|
|
|
setCalendar([
|
|
|
|
...emptyDays,
|
|
|
|
...days.map((day) => (
|
|
|
|
<button
|
|
|
|
key={day}
|
|
|
|
onClick={() => setSelectedDate(inviteeDate.date(day))}
|
|
|
|
disabled={isDisabled(day)}
|
|
|
|
className={
|
2021-08-12 13:51:40 +00:00
|
|
|
"text-center w-14 h-14 mx-auto hover:border hover:border-black dark:hover:border-white" +
|
2021-08-06 15:29:09 +00:00
|
|
|
(isDisabled(day)
|
2021-08-08 15:56:34 +00:00
|
|
|
? " text-gray-400 font-light hover:border-0 cursor-default"
|
2021-08-08 15:13:31 +00:00
|
|
|
: " dark:text-white text-primary-500 font-medium") +
|
2021-06-30 01:35:08 +00:00
|
|
|
(selectedDate && selectedDate.isSame(inviteeDate.date(day), "day")
|
2021-08-08 15:13:31 +00:00
|
|
|
? " bg-black text-white-important"
|
2021-06-30 01:35:08 +00:00
|
|
|
: !isDisabled(day)
|
2021-08-12 13:51:40 +00:00
|
|
|
? " bg-gray-100 dark:bg-gray-600"
|
2021-06-30 01:35:08 +00:00
|
|
|
: "")
|
|
|
|
}>
|
|
|
|
{day}
|
|
|
|
</button>
|
|
|
|
)),
|
|
|
|
]);
|
2021-06-30 02:41:22 +00:00
|
|
|
}, [selectedMonth, inviteeTimeZone, selectedDate]);
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
return selectedMonth ? (
|
2021-08-08 15:13:31 +00:00
|
|
|
<div
|
|
|
|
className={
|
2021-08-12 13:51:40 +00:00
|
|
|
"mt-8 sm:mt-0 sm:min-w-[430px]" +
|
2021-08-08 15:46:21 +00:00
|
|
|
(selectedDate
|
2021-08-12 13:51:40 +00:00
|
|
|
? "w-full sm:min-w-[455px] sm:w-1/2 md:w-1/3 sm:border-r sm:dark:border-gray-800 sm:pl-4 sm:pr-4"
|
2021-08-08 15:46:21 +00:00
|
|
|
: "sm:w-1/2 sm:pl-4")
|
2021-08-08 15:13:31 +00:00
|
|
|
}>
|
2021-08-12 13:51:40 +00:00
|
|
|
<div className="flex text-gray-600 font-light text-xl mb-4">
|
2021-08-08 15:13:31 +00:00
|
|
|
<span className="w-1/2 text-gray-600 dark:text-white">
|
2021-08-08 15:29:45 +00:00
|
|
|
<strong className="text-gray-900 dark:text-white">
|
|
|
|
{dayjs().month(selectedMonth).format("MMMM")}
|
|
|
|
</strong>
|
|
|
|
<span className="text-gray-500"> {dayjs().month(selectedMonth).format("YYYY")}</span>
|
2021-08-08 15:13:31 +00:00
|
|
|
</span>
|
2021-07-30 12:48:51 +00:00
|
|
|
<div className="w-1/2 text-right text-gray-600 dark:text-gray-400">
|
2021-06-21 20:26:04 +00:00
|
|
|
<button
|
|
|
|
onClick={decrementMonth}
|
2021-07-30 12:48:51 +00:00
|
|
|
className={
|
2021-08-08 15:29:45 +00:00
|
|
|
"group mr-2 p-1" +
|
2021-08-08 15:13:31 +00:00
|
|
|
(selectedMonth <= dayjs().tz(inviteeTimeZone).month() && "text-gray-400 dark:text-gray-600")
|
2021-07-30 12:48:51 +00:00
|
|
|
}
|
2021-06-30 01:35:08 +00:00
|
|
|
disabled={selectedMonth <= dayjs().tz(inviteeTimeZone).month()}>
|
2021-08-08 15:29:45 +00:00
|
|
|
<ChevronLeftIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
|
2021-06-21 20:26:04 +00:00
|
|
|
</button>
|
2021-08-08 15:29:45 +00:00
|
|
|
<button className="group p-1" onClick={incrementMonth}>
|
|
|
|
<ChevronRightIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
|
2021-06-21 20:26:04 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-08-12 13:51:40 +00:00
|
|
|
<div className="grid grid-cols-7 gap-2 sm:gap-1 text-center">
|
2021-06-24 22:15:18 +00:00
|
|
|
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
|
|
.sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
|
|
|
|
.map((weekDay) => (
|
2021-08-12 13:51:40 +00:00
|
|
|
<div key={weekDay} className="uppercase text-gray-500 text-xs tracking-widest my-2">
|
2021-06-24 22:15:18 +00:00
|
|
|
{weekDay}
|
|
|
|
</div>
|
|
|
|
))}
|
2021-06-21 20:26:04 +00:00
|
|
|
{calendar}
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-06-30 01:35:08 +00:00
|
|
|
) : null;
|
2021-06-24 22:15:18 +00:00
|
|
|
};
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-06-24 22:15:18 +00:00
|
|
|
export default DatePicker;
|