import clsx from "clsx"; import differenceInMinutes from "date-fns/differenceInMinutes"; import { addMinutes, setHours } from "date-fns/esm"; import isSameDay from "date-fns/isSameDay"; import { usePlausible } from "next-plausible"; import * as React from "react"; import { expectTimeOption, getDateProps, removeAllOptionsForDay, } from "../../../../utils/date-time-utils"; import Button from "../../../button"; import CompactButton from "../../../compact-button"; import DateCard from "../../../date-card"; import Dropdown, { DropdownItem } from "../../../dropdown"; import { useHeadlessDatePicker } from "../../../headless-date-picker"; import Calendar from "../../../icons/calendar.svg"; import ChevronLeft from "../../../icons/chevron-left.svg"; import ChevronRight from "../../../icons/chevron-right.svg"; import DotsHorizontal from "../../../icons/dots-horizontal.svg"; import Magic from "../../../icons/magic.svg"; import PlusSm from "../../../icons/plus-sm.svg"; import Trash from "../../../icons/trash.svg"; import X from "../../../icons/x.svg"; import Switch from "../../../switch"; import { DateTimeOption } from ".."; import { DateTimePickerProps } from "../types"; import { formatDateWithoutTime, formatDateWithoutTz } from "../utils"; import TimePicker from "./time-picker"; const MonthCalendar: React.VoidFunctionComponent = ({ options, onNavigate, date, onChange, duration, onChangeDuration, }) => { const isTimedEvent = options.some((option) => option.type === "timeSlot"); const plausible = usePlausible(); const optionsByDay = React.useMemo(() => { const res: Record< string, [ { option: DateTimeOption; index: number; }, ] > = {}; options.forEach((option, index) => { const dateString = option.type === "date" ? option.date : option.start.substring(0, option.start.indexOf("T")); if (res[dateString]) { res[dateString].push({ option, index }); } else { res[dateString] = [{ option, index }]; } }); return res; }, [options]); const datepickerSelection = React.useMemo(() => { return Object.keys(optionsByDay).map( (dateString) => new Date(dateString + "T12:00:00"), ); }, [optionsByDay]); const datepicker = useHeadlessDatePicker({ selection: datepickerSelection, onNavigationChange: onNavigate, date, }); return (
{datepicker.daysOfWeek.map((dayOfWeek) => { return (
{dayOfWeek.substring(0, 2)}
); })}
{datepicker.days.map((day, i) => { return ( ); })}
Specify times
Include start and end times for each option
{ if (checked) { // convert dates to time slots onChange( options.map((option) => { if (option.type === "timeSlot") { throw new Error( "Expected option to be a date but received timeSlot", ); } const startDate = new Date(`${option.date}T12:00:00`); const endDate = addMinutes(startDate, duration); return { type: "timeSlot", start: formatDateWithoutTz(startDate), end: formatDateWithoutTz(endDate), }; }), ); } else { onChange( datepicker.selection.map((date) => ({ type: "date", date: formatDateWithoutTime(date), })), ); } }} />
{isTimedEvent ? (
{Object.keys(optionsByDay) .sort((a, b) => (a > b ? 1 : -1)) .map((dateString) => { const optionsForDay = optionsByDay[dateString]; return (
{optionsForDay.map(({ option, index }) => { if (option.type === "date") { throw new Error("Expected timeSlot but got date"); } const startDate = new Date(option.start); return (
{ const newEnd = addMinutes(newStart, duration); // replace enter with updated start time onChange([ ...options.slice(0, index), { ...option, start: formatDateWithoutTz(newStart), end: formatDateWithoutTz(newEnd), }, ...options.slice(index + 1), ]); onNavigate(newStart); onChangeDuration( differenceInMinutes(newEnd, newStart), ); }} /> { onChange([ ...options.slice(0, index), { ...option, end: formatDateWithoutTz(newEnd), }, ...options.slice(index + 1), ]); onNavigate(newEnd); onChangeDuration( differenceInMinutes(newEnd, startDate), ); }} /> { onChange([ ...options.slice(0, index), ...options.slice(index + 1), ]); }} />
); })}
} placement="bottom-start" > { plausible("Applied options to all dates"); const times = optionsForDay.map( ({ option }) => { if (option.type === "date") { throw new Error( "Expected timeSlot but got date", ); } return { startTime: option.start.substring( option.start.indexOf("T"), ), endTime: option.end.substring( option.end.indexOf("T"), ), }; }, ); const newOptions: DateTimeOption[] = []; Object.keys(optionsByDay).forEach( (dateString) => { times.forEach((time) => { newOptions.push({ type: "timeSlot", start: dateString + time.startTime, end: dateString + time.endTime, }); }); }, ); onChange(newOptions); }} /> { onChange( removeAllOptionsForDay( options, new Date(dateString), ), ); }} />
); })}
) : datepicker.selection.length ? (
{datepicker.selection .sort((a, b) => a.getTime() - b.getTime()) .map((selectedDate, i) => { return ( { // TODO (Luke Vella) [2022-03-19]: Find cleaner way to manage this state // Quite tedious right now to remove a single element onChange( removeAllOptionsForDay(options, selectedDate), ); }} /> } /> ); })}
) : (
No dates selected
)}
); }; export default MonthCalendar;