2021-08-08 15:13:31 +00:00
|
|
|
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
2021-11-18 01:03:19 +00:00
|
|
|
import { PeriodType } from "@prisma/client";
|
2021-06-24 22:15:18 +00:00
|
|
|
import dayjs, { Dayjs } from "dayjs";
|
2021-10-18 21:07:06 +00:00
|
|
|
// Then, include dayjs-business-time
|
|
|
|
import dayjsBusinessTime from "dayjs-business-time";
|
2021-09-22 19:52:38 +00:00
|
|
|
import utc from "dayjs/plugin/utc";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
import classNames from "@lib/classNames";
|
2021-10-08 11:43:48 +00:00
|
|
|
import { useLocale } from "@lib/hooks/useLocale";
|
2021-09-22 19:52:38 +00:00
|
|
|
import getSlots from "@lib/slots";
|
2021-11-18 01:03:19 +00:00
|
|
|
import { WorkingHours } from "@lib/types/schedule";
|
2021-07-07 10:43:13 +00:00
|
|
|
|
2021-10-18 21:07:06 +00:00
|
|
|
dayjs.extend(dayjsBusinessTime);
|
2021-06-30 01:35:08 +00:00
|
|
|
dayjs.extend(utc);
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-11-18 01:03:19 +00:00
|
|
|
type DatePickerProps = {
|
|
|
|
weekStart: string;
|
|
|
|
onDatePicked: (pickedDate: Dayjs) => void;
|
|
|
|
workingHours: WorkingHours[];
|
|
|
|
eventLength: number;
|
|
|
|
date: Dayjs | null;
|
|
|
|
periodType: string;
|
|
|
|
periodStartDate: Date | null;
|
|
|
|
periodEndDate: Date | null;
|
|
|
|
periodDays: number | null;
|
|
|
|
periodCountCalendarDays: boolean | null;
|
|
|
|
minimumBookingNotice: number;
|
|
|
|
};
|
|
|
|
|
2021-10-18 21:07:06 +00:00
|
|
|
function DatePicker({
|
2021-06-30 15:41:38 +00:00
|
|
|
weekStart,
|
|
|
|
onDatePicked,
|
|
|
|
workingHours,
|
|
|
|
eventLength,
|
2021-07-13 16:10:22 +00:00
|
|
|
date,
|
2021-11-18 01:03:19 +00:00
|
|
|
periodType = PeriodType.UNLIMITED,
|
2021-07-15 14:10:26 +00:00
|
|
|
periodStartDate,
|
|
|
|
periodEndDate,
|
|
|
|
periodDays,
|
|
|
|
periodCountCalendarDays,
|
2021-07-22 22:52:27 +00:00
|
|
|
minimumBookingNotice,
|
2021-11-18 01:03:19 +00:00
|
|
|
}: DatePickerProps): JSX.Element {
|
2021-10-12 13:11:33 +00:00
|
|
|
const { t } = useLocale();
|
2021-09-14 08:45:28 +00:00
|
|
|
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);
|
2021-06-30 01:35:08 +00:00
|
|
|
|
2021-11-18 01:03:19 +00:00
|
|
|
const [selectedMonth, setSelectedMonth] = useState<number>(
|
2021-09-14 08:45:28 +00:00
|
|
|
date
|
2021-11-18 01:03:19 +00:00
|
|
|
? periodType === PeriodType.RANGE
|
2021-09-14 08:45:28 +00:00
|
|
|
? dayjs(periodStartDate).utcOffset(date.utcOffset()).month()
|
|
|
|
: date.month()
|
|
|
|
: dayjs().month() /* High chance server is going to have the same month */
|
|
|
|
);
|
2021-07-13 16:10:22 +00:00
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (dayjs().month() !== selectedMonth) {
|
|
|
|
setSelectedMonth(dayjs().month());
|
2021-07-15 14:10:26 +00:00
|
|
|
}
|
2021-09-14 08:45:28 +00:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2021-06-30 01:35:08 +00:00
|
|
|
}, []);
|
2021-06-21 20:26:04 +00:00
|
|
|
|
|
|
|
// Handle month changes
|
|
|
|
const incrementMonth = () => {
|
2021-10-18 21:07:06 +00:00
|
|
|
setSelectedMonth((selectedMonth ?? 0) + 1);
|
2021-06-24 22:15:18 +00:00
|
|
|
};
|
2021-06-21 20:26:04 +00:00
|
|
|
|
|
|
|
const decrementMonth = () => {
|
2021-10-18 21:07:06 +00:00
|
|
|
setSelectedMonth((selectedMonth ?? 0) - 1);
|
2021-06-24 22:15:18 +00:00
|
|
|
};
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
const inviteeDate = (): Dayjs => (date || dayjs()).month(selectedMonth);
|
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
useEffect(() => {
|
2021-09-14 08:45:28 +00:00
|
|
|
// 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;
|
2021-06-30 01:35:08 +00:00
|
|
|
}
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
const days = Array(weekdayOfFirst).fill(null);
|
2021-06-24 22:15:18 +00:00
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
const isDisabled = (day: number) => {
|
2021-09-14 08:45:28 +00:00
|
|
|
const date: Dayjs = inviteeDate().date(day);
|
2021-07-15 14:10:26 +00:00
|
|
|
switch (periodType) {
|
2021-11-18 01:03:19 +00:00
|
|
|
case PeriodType.ROLLING: {
|
|
|
|
if (!periodDays) {
|
|
|
|
throw new Error("PeriodType rolling requires periodDays");
|
|
|
|
}
|
2021-07-15 14:10:26 +00:00
|
|
|
const periodRollingEndDay = periodCountCalendarDays
|
2021-11-18 01:03:19 +00:00
|
|
|
? dayjs.utc().add(periodDays, "days").endOf("day")
|
|
|
|
: (dayjs.utc() as Dayjs).addBusinessTime(periodDays, "days").endOf("day");
|
2021-07-15 14:10:26 +00:00
|
|
|
return (
|
2021-09-23 14:08:44 +00:00
|
|
|
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
|
2021-07-15 14:10:26 +00:00
|
|
|
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,
|
|
|
|
}).length
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-11-18 01:03:19 +00:00
|
|
|
case PeriodType.RANGE: {
|
|
|
|
const periodRangeStartDay = dayjs(periodStartDate).utc().endOf("day");
|
|
|
|
const periodRangeEndDay = dayjs(periodEndDate).utc().endOf("day");
|
2021-07-15 14:10:26 +00:00
|
|
|
return (
|
2021-09-14 08:45:28 +00:00
|
|
|
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
|
2021-07-15 14:10:26 +00:00
|
|
|
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,
|
|
|
|
}).length
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-11-18 01:03:19 +00:00
|
|
|
case PeriodType.UNLIMITED:
|
2021-07-15 14:10:26 +00:00
|
|
|
default:
|
|
|
|
return (
|
2021-09-14 08:45:28 +00:00
|
|
|
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
|
2021-07-15 14:10:26 +00:00
|
|
|
!getSlots({
|
|
|
|
inviteeDate: date,
|
|
|
|
frequency: eventLength,
|
2021-07-22 22:52:27 +00:00
|
|
|
minimumBookingNotice,
|
2021-07-15 14:10:26 +00:00
|
|
|
workingHours,
|
|
|
|
}).length
|
|
|
|
);
|
|
|
|
}
|
2021-06-30 01:35:08 +00:00
|
|
|
};
|
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
const daysInMonth = inviteeDate().daysInMonth();
|
2021-06-30 01:35:08 +00:00
|
|
|
for (let i = 1; i <= daysInMonth; i++) {
|
2021-09-14 08:45:28 +00:00
|
|
|
days.push({ disabled: isDisabled(i), date: i });
|
2021-06-30 01:35:08 +00:00
|
|
|
}
|
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
setDays(days);
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [selectedMonth]);
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-09-14 08:45:28 +00:00
|
|
|
return (
|
2021-08-08 15:13:31 +00:00
|
|
|
<div
|
|
|
|
className={
|
2021-08-12 17:05:46 +00:00
|
|
|
"mt-8 sm:mt-0 sm:min-w-[455px] " +
|
2021-09-14 08:45:28 +00:00
|
|
|
(date
|
2021-08-12 17:05:46 +00:00
|
|
|
? "w-full sm:w-1/2 md:w-1/3 sm:border-r sm:dark:border-gray-800 sm:pl-4 sm:pr-6 "
|
2021-08-14 12:26:33 +00:00
|
|
|
: "w-full sm:pl-4")
|
2021-08-08 15:13:31 +00:00
|
|
|
}>
|
2021-11-18 01:03:19 +00:00
|
|
|
<div className="flex mb-4 text-xl font-light text-gray-600">
|
2021-08-08 15:13:31 +00:00
|
|
|
<span className="w-1/2 text-gray-600 dark:text-white">
|
2021-10-08 11:43:48 +00:00
|
|
|
<strong className="text-gray-900 dark:text-white">
|
|
|
|
{t(inviteeDate().format("MMMM").toLowerCase())}
|
|
|
|
</strong>{" "}
|
|
|
|
<span className="text-gray-500">{inviteeDate().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-10-18 21:07:06 +00:00
|
|
|
className={classNames(
|
|
|
|
"group mr-2 p-1",
|
|
|
|
typeof selectedMonth === "number" &&
|
|
|
|
selectedMonth <= dayjs().month() &&
|
|
|
|
"text-gray-400 dark:text-gray-600"
|
|
|
|
)}
|
2021-11-15 15:03:04 +00:00
|
|
|
disabled={typeof selectedMonth === "number" && selectedMonth <= dayjs().month()}
|
|
|
|
data-testid="decrementMonth">
|
2021-11-18 01:03:19 +00:00
|
|
|
<ChevronLeftIcon className="w-5 h-5 group-hover:text-black dark:group-hover:text-white" />
|
2021-06-21 20:26:04 +00:00
|
|
|
</button>
|
2021-11-18 01:03:19 +00:00
|
|
|
<button className="p-1 group" onClick={incrementMonth} data-testid="incrementMonth">
|
|
|
|
<ChevronRightIcon className="w-5 h-5 group-hover:text-black dark:group-hover:text-white" />
|
2021-06-21 20:26:04 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-11-18 01:03:19 +00:00
|
|
|
<div className="grid grid-cols-7 gap-4 text-center border-t border-b dark:border-gray-800 sm:border-0">
|
2021-10-08 11:43:48 +00:00
|
|
|
{["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
2021-06-24 22:15:18 +00:00
|
|
|
.sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
|
|
|
|
.map((weekDay) => (
|
2021-11-18 01:03:19 +00:00
|
|
|
<div key={weekDay} className="my-4 text-xs tracking-widest text-gray-500 uppercase">
|
2021-10-08 11:43:48 +00:00
|
|
|
{t(weekDay.toLowerCase()).substring(0, 3)}
|
2021-06-24 22:15:18 +00:00
|
|
|
</div>
|
|
|
|
))}
|
2021-06-21 20:26:04 +00:00
|
|
|
</div>
|
2021-09-14 08:45:28 +00:00
|
|
|
<div className="grid grid-cols-7 gap-2 text-center">
|
|
|
|
{days.map((day, idx) => (
|
|
|
|
<div
|
|
|
|
key={day === null ? `e-${idx}` : `day-${day.date}`}
|
|
|
|
style={{
|
|
|
|
paddingTop: "100%",
|
|
|
|
}}
|
2021-11-18 01:03:19 +00:00
|
|
|
className="relative w-full">
|
2021-09-14 08:45:28 +00:00
|
|
|
{day === null ? (
|
|
|
|
<div key={`e-${idx}`} />
|
|
|
|
) : (
|
|
|
|
<button
|
|
|
|
onClick={() => onDatePicked(inviteeDate().date(day.date))}
|
|
|
|
disabled={day.disabled}
|
|
|
|
className={classNames(
|
|
|
|
"absolute w-full top-0 left-0 right-0 bottom-0 rounded-sm text-center mx-auto",
|
2021-11-04 14:30:37 +00:00
|
|
|
"hover:border hover:border-brand dark:hover:border-white",
|
2021-12-14 10:39:32 +00:00
|
|
|
day.disabled ? "text-gray-400 font-light hover:border-0 cursor-default" : "font-medium",
|
2021-09-14 08:45:28 +00:00
|
|
|
date && date.isSame(inviteeDate().date(day.date), "day")
|
2021-12-14 10:39:32 +00:00
|
|
|
? "bg-brand text-brandcontrast"
|
2021-09-14 08:45:28 +00:00
|
|
|
: !day.disabled
|
2021-12-14 10:39:32 +00:00
|
|
|
? " bg-gray-100 dark:bg-gray-600 dark:text-white"
|
2021-09-14 08:45:28 +00:00
|
|
|
: ""
|
2021-10-18 21:07:06 +00:00
|
|
|
)}
|
|
|
|
data-testid="day"
|
|
|
|
data-disabled={day.disabled}>
|
2021-09-14 08:45:28 +00:00
|
|
|
{day.date}
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
2021-06-21 20:26:04 +00:00
|
|
|
</div>
|
2021-09-14 08:45:28 +00:00
|
|
|
);
|
2021-10-18 21:07:06 +00:00
|
|
|
}
|
2021-06-21 20:26:04 +00:00
|
|
|
|
2021-06-24 22:15:18 +00:00
|
|
|
export default DatePicker;
|