diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000000..e49a7e6569
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["next/babel"]
+}
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
index 182300ba52..6b0d79e8ac 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -24,7 +24,8 @@
"env": {
"browser": true,
"node": true,
- "es6": true
+ "es6": true,
+ "jest": true
},
"settings": {
"react": {
diff --git a/components/booking/AvailableTimes.tsx b/components/booking/AvailableTimes.tsx
index 6f4e413033..bfa4a0cfd3 100644
--- a/components/booking/AvailableTimes.tsx
+++ b/components/booking/AvailableTimes.tsx
@@ -1,112 +1,40 @@
-import dayjs from "dayjs";
-import isBetween from "dayjs/plugin/isBetween";
-dayjs.extend(isBetween);
-import { useEffect, useState, useMemo } from "react";
-import getSlots from "../../lib/slots";
import Link from "next/link";
-import { timeZone } from "../../lib/clock";
import { useRouter } from "next/router";
+import Slots from "./Slots";
import { ExclamationIcon } from "@heroicons/react/solid";
-const AvailableTimes = (props) => {
+const AvailableTimes = ({ date, eventLength, eventTypeId, workingHours, timeFormat, user }) => {
const router = useRouter();
- const { user, rescheduleUid } = router.query;
- const [loaded, setLoaded] = useState(false);
- const [error, setError] = useState(false);
-
- const times = useMemo(() => {
- const slots = getSlots({
- calendarTimeZone: props.user.timeZone,
- selectedTimeZone: timeZone(),
- eventLength: props.eventType.length,
- selectedDate: props.date,
- dayStartTime: props.user.startTime,
- dayEndTime: props.user.endTime,
- });
-
- return slots;
- }, [props.date]);
-
- const handleAvailableSlots = (busyTimes: []) => {
- // Check for conflicts
- for (let i = times.length - 1; i >= 0; i -= 1) {
- busyTimes.forEach((busyTime) => {
- const startTime = dayjs(busyTime.start);
- const endTime = dayjs(busyTime.end);
-
- // Check if start times are the same
- if (dayjs(times[i]).format("HH:mm") == startTime.format("HH:mm")) {
- times.splice(i, 1);
- }
-
- // Check if time is between start and end times
- if (dayjs(times[i]).isBetween(startTime, endTime)) {
- times.splice(i, 1);
- }
-
- // Check if slot end time is between start and end time
- if (dayjs(times[i]).add(props.eventType.length, "minutes").isBetween(startTime, endTime)) {
- times.splice(i, 1);
- }
-
- // Check if startTime is between slot
- if (startTime.isBetween(dayjs(times[i]), dayjs(times[i]).add(props.eventType.length, "minutes"))) {
- times.splice(i, 1);
- }
- });
- }
- // Display available times
- setLoaded(true);
- };
-
- // Re-render only when invitee changes date
- useEffect(() => {
- setLoaded(false);
- setError(false);
- fetch(
- `/api/availability/${user}?dateFrom=${props.date.startOf("day").utc().format()}&dateTo=${props.date
- .endOf("day")
- .utc()
- .format()}`
- )
- .then((res) => res.json())
- .then(handleAvailableSlots)
- .catch((e) => {
- console.error(e);
- setError(true);
- });
- }, [props.date]);
-
+ const { rescheduleUid } = router.query;
+ const { slots, isFullyBooked, hasErrors } = Slots({ date, eventLength, workingHours });
return (
- {props.date.format("dddd DD MMMM YYYY")}
+ {date.format("dddd DD MMMM YYYY")}
- {!error &&
- loaded &&
- times.length > 0 &&
- times.map((time) => (
-
+ {slots.length > 0 &&
+ slots.map((slot) => (
+
))}
- {!error && loaded && times.length == 0 && (
+ {isFullyBooked && (
-
{props.user.name} is all booked today.
+ {user.name} is all booked today.
)}
- {!error && !loaded &&
}
- {error && (
+
+ {!isFullyBooked && slots.length === 0 && !hasErrors &&
}
+
+ {hasErrors && (
diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx
new file mode 100644
index 0000000000..1bea6caf65
--- /dev/null
+++ b/components/booking/DatePicker.tsx
@@ -0,0 +1,134 @@
+import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
+import { useEffect, useState } from "react";
+import dayjs, { Dayjs } from "dayjs";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
+import getSlots from "@lib/slots";
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+const DatePicker = ({
+ weekStart,
+ onDatePicked,
+ workingHours,
+ organizerTimeZone,
+ inviteeTimeZone,
+ eventLength,
+}) => {
+ const [calendar, setCalendar] = useState([]);
+ const [selectedMonth, setSelectedMonth]: number = useState();
+ const [selectedDate, setSelectedDate]: Dayjs = useState();
+
+ useEffect(() => {
+ setSelectedMonth(dayjs().tz(inviteeTimeZone).month());
+ }, []);
+
+ useEffect(() => {
+ if (selectedDate) onDatePicked(selectedDate);
+ }, [selectedDate]);
+
+ // Handle month changes
+ const incrementMonth = () => {
+ setSelectedMonth(selectedMonth + 1);
+ };
+
+ const decrementMonth = () => {
+ setSelectedMonth(selectedMonth - 1);
+ };
+
+ useEffect(() => {
+ if (!selectedMonth) {
+ // wish next had a way of dealing with this magically;
+ return;
+ }
+
+ const inviteeDate = dayjs().tz(inviteeTimeZone).month(selectedMonth);
+
+ const isDisabled = (day: number) => {
+ const date: Dayjs = inviteeDate.date(day);
+ return (
+ date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
+ !getSlots({
+ inviteeDate: date,
+ frequency: eventLength,
+ workingHours,
+ organizerTimeZone,
+ }).length
+ );
+ };
+
+ // 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) => (
+
+ {null}
+
+ ));
+
+ // Combine placeholder days with actual days
+ setCalendar([
+ ...emptyDays,
+ ...days.map((day) => (
+
+ )),
+ ]);
+ }, [selectedMonth, inviteeTimeZone, selectedDate]);
+
+ return selectedMonth ? (
+
+
+
{dayjs().month(selectedMonth).format("MMMM YYYY")}
+
+
+
+
+
+
+ {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+ .sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
+ .map((weekDay) => (
+
+ {weekDay}
+
+ ))}
+ {calendar}
+
+
+ ) : null;
+};
+
+export default DatePicker;
diff --git a/components/booking/Slots.tsx b/components/booking/Slots.tsx
new file mode 100644
index 0000000000..8f92aaeb14
--- /dev/null
+++ b/components/booking/Slots.tsx
@@ -0,0 +1,96 @@
+import { useState, useEffect } from "react";
+import { useRouter } from "next/router";
+import getSlots from "../../lib/slots";
+import dayjs, { Dayjs } from "dayjs";
+import isBetween from "dayjs/plugin/isBetween";
+import utc from "dayjs/plugin/utc";
+dayjs.extend(isBetween);
+dayjs.extend(utc);
+
+type Props = {
+ eventLength: number;
+ minimumBookingNotice?: number;
+ date: Dayjs;
+};
+
+const Slots = ({ eventLength, minimumBookingNotice, date, workingHours, organizerUtcOffset }: Props) => {
+ minimumBookingNotice = minimumBookingNotice || 0;
+
+ const router = useRouter();
+ const { user } = router.query;
+ const [slots, setSlots] = useState([]);
+ const [isFullyBooked, setIsFullyBooked] = useState(false);
+ const [hasErrors, setHasErrors] = useState(false);
+
+ useEffect(() => {
+ setSlots([]);
+ setIsFullyBooked(false);
+ setHasErrors(false);
+ fetch(
+ `/api/availability/${user}?dateFrom=${date.startOf("day").utc().startOf("day").format()}&dateTo=${date
+ .endOf("day")
+ .utc()
+ .endOf("day")
+ .format()}`
+ )
+ .then((res) => res.json())
+ .then(handleAvailableSlots)
+ .catch((e) => {
+ console.error(e);
+ setHasErrors(true);
+ });
+ }, [date]);
+
+ const handleAvailableSlots = (busyTimes: []) => {
+ const times = getSlots({
+ frequency: eventLength,
+ inviteeDate: date,
+ workingHours,
+ minimumBookingNotice,
+ organizerUtcOffset,
+ });
+
+ const timesLengthBeforeConflicts: number = times.length;
+
+ // Check for conflicts
+ for (let i = times.length - 1; i >= 0; i -= 1) {
+ busyTimes.every((busyTime): boolean => {
+ const startTime = dayjs(busyTime.start).utc();
+ const endTime = dayjs(busyTime.end).utc();
+ // Check if start times are the same
+ if (times[i].utc().isSame(startTime)) {
+ times.splice(i, 1);
+ }
+ // Check if time is between start and end times
+ else if (times[i].utc().isBetween(startTime, endTime)) {
+ times.splice(i, 1);
+ }
+ // Check if slot end time is between start and end time
+ else if (times[i].utc().add(eventLength, "minutes").isBetween(startTime, endTime)) {
+ times.splice(i, 1);
+ }
+ // Check if startTime is between slot
+ else if (startTime.isBetween(times[i].utc(), times[i].utc().add(eventLength, "minutes"))) {
+ times.splice(i, 1);
+ } else {
+ return true;
+ }
+ return false;
+ });
+ }
+
+ if (times.length === 0 && timesLengthBeforeConflicts !== 0) {
+ setIsFullyBooked(true);
+ }
+ // Display available times
+ setSlots(times);
+ };
+
+ return {
+ slots,
+ isFullyBooked,
+ hasErrors,
+ };
+};
+
+export default Slots;
diff --git a/components/booking/TimeOptions.tsx b/components/booking/TimeOptions.tsx
index 38aafd8bf5..580e174e4b 100644
--- a/components/booking/TimeOptions.tsx
+++ b/components/booking/TimeOptions.tsx
@@ -1,73 +1,72 @@
-import {Switch} from "@headlessui/react";
+import { Switch } from "@headlessui/react";
import TimezoneSelect from "react-timezone-select";
-import {useEffect, useState} from "react";
-import {timeZone, is24h} from '../../lib/clock';
+import { useEffect, useState } from "react";
+import { timeZone, is24h } from "../../lib/clock";
function classNames(...classes) {
- return classes.filter(Boolean).join(' ')
+ return classes.filter(Boolean).join(" ");
}
const TimeOptions = (props) => {
-
- const [selectedTimeZone, setSelectedTimeZone] = useState('');
+ const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [is24hClock, setIs24hClock] = useState(false);
- useEffect( () => {
+ useEffect(() => {
setIs24hClock(is24h());
setSelectedTimeZone(timeZone());
}, []);
- useEffect( () => {
- props.onSelectTimeZone(timeZone(selectedTimeZone));
+ useEffect(() => {
+ if (selectedTimeZone && timeZone() && selectedTimeZone !== timeZone()) {
+ props.onSelectTimeZone(timeZone(selectedTimeZone));
+ }
}, [selectedTimeZone]);
- useEffect( () => {
+ useEffect(() => {
props.onToggle24hClock(is24h(is24hClock));
}, [is24hClock]);
- return selectedTimeZone !== "" && (
-
-
-
Time Options
-
-
-
- am/pm
-
-
- Use setting
-
+
+
Time Options
+
+
+
+ am/pm
+
+
-
-
- 24h
-
-
+ is24hClock ? "bg-blue-600" : "bg-gray-200",
+ "relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+ )}>
+ Use setting
+
+
+
+ 24h
+
+
+
+ setSelectedTimeZone(tz.value)}
+ className="mb-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
+ />
-
setSelectedTimeZone(tz.value)}
- className="mb-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
- />
-
+ )
);
-}
+};
-export default TimeOptions;
\ No newline at end of file
+export default TimeOptions;
diff --git a/components/ui/PoweredByCalendso.tsx b/components/ui/PoweredByCalendso.tsx
new file mode 100644
index 0000000000..2022fa931e
--- /dev/null
+++ b/components/ui/PoweredByCalendso.tsx
@@ -0,0 +1,22 @@
+import Link from "next/link";
+
+const PoweredByCalendso = (props) => (
+
+);
+
+export default PoweredByCalendso;
\ No newline at end of file
diff --git a/components/ui/Scheduler.tsx b/components/ui/Scheduler.tsx
new file mode 100644
index 0000000000..045c726d20
--- /dev/null
+++ b/components/ui/Scheduler.tsx
@@ -0,0 +1,143 @@
+import React, { useEffect, useState } from "react";
+import TimezoneSelect from "react-timezone-select";
+import { TrashIcon } from "@heroicons/react/outline";
+import { WeekdaySelect } from "./WeekdaySelect";
+import SetTimesModal from "./modal/SetTimesModal";
+import dayjs from "dayjs";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
+import { Availability } from "@prisma/client";
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+type Props = {
+ timeZone: string;
+ availability: Availability[];
+ setTimeZone: unknown;
+};
+
+export const Scheduler = ({
+ availability,
+ setAvailability,
+ timeZone: selectedTimeZone,
+ setTimeZone,
+}: Props) => {
+ const [editSchedule, setEditSchedule] = useState(-1);
+ const [dateOverrides, setDateOverrides] = useState([]);
+ const [openingHours, setOpeningHours] = useState([]);
+
+ useEffect(() => {
+ setOpeningHours(
+ availability
+ .filter((item: Availability) => item.days.length !== 0)
+ .map((item) => {
+ item.startDate = dayjs().utc().startOf("day").add(item.startTime, "minutes");
+ item.endDate = dayjs().utc().startOf("day").add(item.endTime, "minutes");
+ return item;
+ })
+ );
+ setDateOverrides(availability.filter((item: Availability) => item.date));
+ }, []);
+
+ // updates availability to how it should be formatted outside this component.
+ useEffect(() => {
+ setAvailability({
+ dateOverrides: dateOverrides,
+ openingHours: openingHours,
+ });
+ }, [dateOverrides, openingHours]);
+
+ const addNewSchedule = () => setEditSchedule(openingHours.length);
+
+ const applyEditSchedule = (changed) => {
+ // new entry
+ if (!changed.days) {
+ changed.days = [1, 2, 3, 4, 5]; // Mon - Fri
+ setOpeningHours(openingHours.concat(changed));
+ } else {
+ // update
+ const replaceWith = { ...openingHours[editSchedule], ...changed };
+ openingHours.splice(editSchedule, 1, replaceWith);
+ setOpeningHours([].concat(openingHours));
+ }
+ };
+
+ const removeScheduleAt = (toRemove: number) => {
+ openingHours.splice(toRemove, 1);
+ setOpeningHours([].concat(openingHours));
+ };
+
+ const OpeningHours = ({ idx, item }) => (
+
+
+ (item.days = selected)} />
+
+
+
+
+ );
+
+ return (
+
+
+
+
+
+
+ setTimeZone(tz.value)}
+ className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
+ />
+
+
+
+ {openingHours.map((item, idx) => (
+
+ ))}
+
+
+
+
+
+ {/*
Add date overrides
+
+ Add dates when your availability changes from your weekly hours
+
+
*/}
+
+
+ {editSchedule >= 0 && (
+
applyEditSchedule({ ...(openingHours[editSchedule] || {}), ...times })}
+ onExit={() => setEditSchedule(-1)}
+ />
+ )}
+ {/*{showDateOverrideModal &&
+
+ }*/}
+
+ );
+};
diff --git a/components/ui/WeekdaySelect.tsx b/components/ui/WeekdaySelect.tsx
new file mode 100644
index 0000000000..a9f371d827
--- /dev/null
+++ b/components/ui/WeekdaySelect.tsx
@@ -0,0 +1,53 @@
+import React, { useEffect, useState } from "react";
+
+export const WeekdaySelect = (props) => {
+ const [activeDays, setActiveDays] = useState(
+ [...Array(7).keys()].map((v, i) => (props.defaultValue || []).includes(i))
+ );
+
+ const days = ["S", "M", "T", "W", "T", "F", "S"];
+
+ useEffect(() => {
+ props.onSelect(activeDays.map((v, idx) => (v ? idx : -1)).filter((v) => v !== -1));
+ }, [activeDays]);
+
+ const toggleDay = (e, idx: number) => {
+ e.preventDefault();
+ activeDays[idx] = !activeDays[idx];
+ setActiveDays([].concat(activeDays));
+ };
+
+ return (
+
+
+ {days.map((day, idx) =>
+ activeDays[idx] ? (
+
+ ) : (
+
+ )
+ )}
+
+
+ );
+};
diff --git a/components/ui/modal/SetTimesModal.tsx b/components/ui/modal/SetTimesModal.tsx
new file mode 100644
index 0000000000..2334802e11
--- /dev/null
+++ b/components/ui/modal/SetTimesModal.tsx
@@ -0,0 +1,146 @@
+import { ClockIcon } from "@heroicons/react/outline";
+import { useRef } from "react";
+
+export default function SetTimesModal(props) {
+ const [startHours, startMinutes] = [Math.floor(props.startTime / 60), props.startTime % 60];
+ const [endHours, endMinutes] = [Math.floor(props.endTime / 60), props.endTime % 60];
+
+ const startHoursRef = useRef
();
+ const startMinsRef = useRef();
+ const endHoursRef = useRef();
+ const endMinsRef = useRef();
+
+ function updateStartEndTimesHandler(event) {
+ event.preventDefault();
+
+ const enteredStartHours = parseInt(startHoursRef.current.value);
+ const enteredStartMins = parseInt(startMinsRef.current.value);
+ const enteredEndHours = parseInt(endHoursRef.current.value);
+ const enteredEndMins = parseInt(endMinsRef.current.value);
+
+ props.onChange({
+ startTime: enteredStartHours * 60 + enteredStartMins,
+ endTime: enteredEndHours * 60 + enteredEndMins,
+ });
+
+ props.onExit(0);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Change when you are available for bookings
+
+
+
Set your work schedule
+
+
+
+
+
+
+
+
+
+
:
+
+
+
+
+
+
+
+
+
+
+
+
:
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/lib/jsonUtils.ts b/lib/jsonUtils.ts
new file mode 100644
index 0000000000..3f617cb0ce
--- /dev/null
+++ b/lib/jsonUtils.ts
@@ -0,0 +1,11 @@
+export const validJson = (jsonString: string) => {
+ try {
+ const o = JSON.parse(jsonString);
+ if (o && typeof o === "object") {
+ return o;
+ }
+ } catch (e) {
+ console.log("Invalid JSON:", e);
+ }
+ return false;
+};
diff --git a/lib/slots.ts b/lib/slots.ts
index 157b56a692..5beeb9f8af 100644
--- a/lib/slots.ts
+++ b/lib/slots.ts
@@ -1,94 +1,134 @@
-const dayjs = require("dayjs");
-
-const isToday = require("dayjs/plugin/isToday");
-const utc = require("dayjs/plugin/utc");
-const timezone = require("dayjs/plugin/timezone");
-
-dayjs.extend(isToday);
+import dayjs, { Dayjs } from "dayjs";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);
-const getMinutesFromMidnight = (date) => {
- return date.hour() * 60 + date.minute();
+type WorkingHour = {
+ days: number[];
+ startTime: number;
+ endTime: number;
};
-const getSlots = ({
- calendarTimeZone,
- eventLength,
- selectedTimeZone,
- selectedDate,
- dayStartTime,
- dayEndTime
-}) => {
+type GetSlots = {
+ inviteeDate: Dayjs;
+ frequency: number;
+ workingHours: WorkingHour[];
+ minimumBookingNotice?: number;
+ organizerTimeZone: string;
+};
- if(!selectedDate) return []
-
- const lowerBound = selectedDate.tz(selectedTimeZone).startOf("day");
+type Boundary = {
+ lowerBound: number;
+ upperBound: number;
+};
- // Simple case, same timezone
- if (calendarTimeZone === selectedTimeZone) {
- const slots = [];
- const now = dayjs();
- for (
- let minutes = dayStartTime;
- minutes <= dayEndTime - eventLength;
- minutes += parseInt(eventLength, 10)
- ) {
- const slot = lowerBound.add(minutes, "minutes");
- if (slot > now) {
- slots.push(slot);
- }
- }
- return slots;
+const freqApply = (cb, value: number, frequency: number): number => cb(value / frequency) * frequency;
+
+const intersectBoundary = (a: Boundary, b: Boundary) => {
+ if (a.upperBound < b.lowerBound || a.lowerBound > b.upperBound) {
+ return;
}
+ return {
+ lowerBound: Math.max(b.lowerBound, a.lowerBound),
+ upperBound: Math.min(b.upperBound, a.upperBound),
+ };
+};
- const upperBound = selectedDate.tz(selectedTimeZone).endOf("day");
+// say invitee is -60,1380, and boundary is -120,240 - the overlap is -60,240
+const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) =>
+ boundaries.map((boundary) => intersectBoundary(inviteeBoundary, boundary)).filter(Boolean);
- // We need to start generating slots from the start of the calendarTimeZone day
- const startDateTime = lowerBound
- .tz(calendarTimeZone)
+const organizerBoundaries = (
+ workingHours: [],
+ inviteeDate: Dayjs,
+ inviteeBounds: Boundary,
+ organizerTimeZone
+): Boundary[] => {
+ const boundaries: Boundary[] = [];
+
+ const startDay: number = +inviteeDate
+ .utc()
.startOf("day")
- .add(dayStartTime, "minutes");
+ .add(inviteeBounds.lowerBound, "minutes")
+ .format("d");
+ const endDay: number = +inviteeDate
+ .utc()
+ .startOf("day")
+ .add(inviteeBounds.upperBound, "minutes")
+ .format("d");
- let phase = 0;
- if (startDateTime < lowerBound) {
- // Getting minutes of the first event in the day of the chooser
- const diff = lowerBound.diff(startDateTime, "minutes");
-
- // finding first event
- phase = diff + eventLength - (diff % eventLength);
- }
-
- // We can stop as soon as the selectedTimeZone day ends
- const endDateTime = upperBound
- .tz(calendarTimeZone)
- .subtract(eventLength, "minutes");
-
- const maxMinutes = endDateTime.diff(startDateTime, "minutes");
-
- const slots = [];
- const now = dayjs();
- for (
- let minutes = phase;
- minutes <= maxMinutes;
- minutes += parseInt(eventLength, 10)
- ) {
- const slot = startDateTime.add(minutes, "minutes");
-
- const minutesFromMidnight = getMinutesFromMidnight(slot);
-
- if (
- minutesFromMidnight < dayStartTime ||
- minutesFromMidnight > dayEndTime - eventLength ||
- slot < now
- ) {
- continue;
+ workingHours.forEach((item) => {
+ const lowerBound: number = item.startTime - dayjs().tz(organizerTimeZone).utcOffset();
+ const upperBound: number = item.endTime - dayjs().tz(organizerTimeZone).utcOffset();
+ if (startDay !== endDay) {
+ if (inviteeBounds.lowerBound < 0) {
+ // lowerBound edges into the previous day
+ if (item.days.includes(startDay)) {
+ boundaries.push({ lowerBound: lowerBound - 1440, upperBound: upperBound - 1440 });
+ }
+ if (item.days.includes(endDay)) {
+ boundaries.push({ lowerBound, upperBound });
+ }
+ } else {
+ // upperBound edges into the next day
+ if (item.days.includes(endDay)) {
+ boundaries.push({ lowerBound: lowerBound + 1440, upperBound: upperBound + 1440 });
+ }
+ if (item.days.includes(startDay)) {
+ boundaries.push({ lowerBound, upperBound });
+ }
+ }
+ } else {
+ boundaries.push({ lowerBound, upperBound });
}
+ });
+ return boundaries;
+};
- slots.push(slot.tz(selectedTimeZone));
+const inviteeBoundary = (startTime: number, utcOffset: number, frequency: number): Boundary => {
+ const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency);
+ const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency);
+ return {
+ lowerBound,
+ upperBound,
+ };
+};
+
+const getSlotsBetweenBoundary = (frequency: number, { lowerBound, upperBound }: Boundary) => {
+ const slots: Dayjs[] = [];
+ for (let minutes = 0; lowerBound + minutes <= upperBound - frequency; minutes += frequency) {
+ slots.push(
+ dayjs
+ .utc()
+ .startOf("day")
+ .add(lowerBound + minutes, "minutes")
+ );
}
-
return slots;
};
-export default getSlots
+const getSlots = ({
+ inviteeDate,
+ frequency,
+ minimumBookingNotice,
+ workingHours,
+ organizerTimeZone,
+}: GetSlots): Dayjs[] => {
+ const startTime = dayjs.utc().isSame(dayjs(inviteeDate), "day")
+ ? inviteeDate.hour() * 60 + inviteeDate.minute() + (minimumBookingNotice || 0)
+ : 0;
+
+ const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency);
+
+ return getOverlaps(
+ inviteeBounds,
+ organizerBoundaries(workingHours, inviteeDate, inviteeBounds, organizerTimeZone)
+ )
+ .reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], [])
+ .map((slot) =>
+ slot.month(inviteeDate.month()).date(inviteeDate.date()).utcOffset(inviteeDate.utcOffset())
+ );
+};
+
+export default getSlots;
diff --git a/package.json b/package.json
index c59a83a0e4..b0ac1af372 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "test": "node --experimental-vm-modules node_modules/.bin/jest",
"build": "next build",
"start": "next start",
"postinstall": "prisma generate",
@@ -38,6 +39,7 @@
"uuid": "^8.3.2"
},
"devDependencies": {
+ "@types/jest": "^26.0.23",
"@types/node": "^14.14.33",
"@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.3",
@@ -50,7 +52,9 @@
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^6.0.0",
+ "jest": "^27.0.5",
"lint-staged": "^11.0.0",
+ "mockdate": "^3.0.5",
"postcss": "^8.2.8",
"prettier": "^2.3.1",
"prisma": "^2.23.0",
@@ -62,5 +66,19 @@
"prettier --write",
"eslint"
]
+ },
+ "jest": {
+ "verbose": true,
+ "extensionsToTreatAsEsm": [
+ ".ts"
+ ],
+ "moduleFileExtensions": [
+ "js",
+ "ts"
+ ],
+ "moduleNameMapper": {
+ "^@components(.*)$": "/components$1",
+ "^@lib(.*)$": "/lib$1"
+ }
}
}
diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx
index bf09b98776..fd81a9c516 100644
--- a/pages/[user]/[type].tsx
+++ b/pages/[user]/[type].tsx
@@ -1,115 +1,47 @@
import { useEffect, useState } from "react";
import { GetServerSideProps } from "next";
import Head from "next/head";
-import Link from "next/link";
+import { ClockIcon, GlobeIcon, ChevronDownIcon } from "@heroicons/react/solid";
import prisma from "../../lib/prisma";
import { useRouter } from "next/router";
-import dayjs, { Dayjs } from "dayjs";
-import {
- ClockIcon,
- GlobeIcon,
- ChevronDownIcon,
- ChevronLeftIcon,
- ChevronRightIcon,
-} from "@heroicons/react/solid";
-import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
-import utc from "dayjs/plugin/utc";
-import timezone from "dayjs/plugin/timezone";
-dayjs.extend(isSameOrBefore);
-dayjs.extend(utc);
-dayjs.extend(timezone);
+import { Dayjs } from "dayjs";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
import AvailableTimes from "../../components/booking/AvailableTimes";
import TimeOptions from "../../components/booking/TimeOptions";
import Avatar from "../../components/Avatar";
import { timeZone } from "../../lib/clock";
+import DatePicker from "../../components/booking/DatePicker";
+import PoweredByCalendso from "../../components/ui/PoweredByCalendso";
export default function Type(props): Type {
// Get router variables
const router = useRouter();
const { rescheduleUid } = router.query;
- // Initialise state
const [selectedDate, setSelectedDate] = useState();
- const [selectedMonth, setSelectedMonth] = useState(dayjs().month());
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
const [timeFormat, setTimeFormat] = useState("h:mma");
const telemetry = useTelemetry();
- useEffect((): void => {
+ useEffect(() => {
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters()));
}, [telemetry]);
- // Handle month changes
- const incrementMonth = () => {
- setSelectedMonth(selectedMonth + 1);
- };
-
- const decrementMonth = () => {
- setSelectedMonth(selectedMonth - 1);
- };
-
- // Set up calendar
- const daysInMonth = dayjs().month(selectedMonth).daysInMonth();
- const days = [];
- for (let i = 1; i <= daysInMonth; i++) {
- days.push(i);
- }
-
- // Create placeholder elements for empty days in first week
- let weekdayOfFirst = dayjs().month(selectedMonth).date(1).day();
- if (props.user.weekStart === "Monday") {
- weekdayOfFirst -= 1;
- if (weekdayOfFirst < 0) weekdayOfFirst = 6;
- }
- const emptyDays = Array(weekdayOfFirst)
- .fill(null)
- .map((day, i) => (
-
- {null}
-
- ));
-
- const changeDate = (day): void => {
+ const changeDate = (date: Dayjs) => {
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.dateSelected, collectPageParameters()));
- setSelectedDate(dayjs().month(selectedMonth).date(day));
+ setSelectedDate(date);
};
- // Combine placeholder days with actual days
- const calendar = [
- ...emptyDays,
- ...days.map((day) => (
-
- )),
- ];
-
const handleSelectTimeZone = (selectedTimeZone: string): void => {
if (selectedDate) {
setSelectedDate(selectedDate.tz(selectedTimeZone));
}
+ setIsTimeOptionsOpen(false);
};
- const handleToggle24hClock = (is24hClock: boolean): void => {
- if (selectedDate) {
- setTimeFormat(is24hClock ? "HH:mm" : "h:mma");
- }
+ const handleToggle24hClock = (is24hClock: boolean) => {
+ setTimeFormat(is24hClock ? "HH:mm" : "h:mma");
};
return (
@@ -190,63 +122,27 @@ export default function Type(props): Type {
)}
{props.eventType.description}
-
-
-
{dayjs().month(selectedMonth).format("MMMM YYYY")}
-
-
-
-
-
-
- {props.user.weekStart !== "Monday" ? (
-
Sun
- ) : null}
-
Mon
-
Tue
-
Wed
-
Thu
-
Fri
-
Sat
- {props.user.weekStart === "Monday" ? (
-
Sun
- ) : null}
- {calendar}
-
-
+
{selectedDate && (
)}
- {!props.user.hideBranding && (
-
- )}
+ {!props.user.hideBranding &&
}
);
@@ -269,6 +165,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
timeZone: true,
endTime: true,
weekStart: true,
+ availability: true,
hideBranding: true,
},
});
@@ -291,6 +188,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
title: true,
description: true,
length: true,
+ availability: true,
+ timeZone: true,
},
});
@@ -300,10 +199,27 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
};
}
+ const getWorkingHours = (providesAvailability) =>
+ providesAvailability.availability && providesAvailability.availability.length
+ ? providesAvailability.availability
+ : null;
+
+ const workingHours: [] =
+ getWorkingHours(eventType) ||
+ getWorkingHours(user) ||
+ [
+ {
+ days: [0, 1, 2, 3, 4, 5, 6],
+ startTime: user.startTime,
+ endTime: user.endTime,
+ },
+ ].filter((availability): boolean => typeof availability["days"] !== "undefined");
+
return {
props: {
user,
eventType,
+ workingHours,
},
};
};
diff --git a/pages/api/availability/eventtype.ts b/pages/api/availability/eventtype.ts
index e50fc4abee..dc5709e668 100644
--- a/pages/api/availability/eventtype.ts
+++ b/pages/api/availability/eventtype.ts
@@ -1,81 +1,111 @@
-import type {NextApiRequest, NextApiResponse} from 'next';
-import {getSession} from 'next-auth/client';
-import prisma from '../../../lib/prisma';
+import type { NextApiRequest, NextApiResponse } from "next";
+import { getSession } from "next-auth/client";
+import prisma from "../../../lib/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- const session = await getSession({req: req});
- if (!session) {
- res.status(401).json({message: "Not authenticated"});
- return;
- }
+ const session = await getSession({ req: req });
+ if (!session) {
+ res.status(401).json({ message: "Not authenticated" });
+ return;
+ }
- if (req.method == "PATCH" || req.method == "POST") {
-
- const data = {
- title: req.body.title,
- slug: req.body.slug,
- description: req.body.description,
- length: parseInt(req.body.length),
- hidden: req.body.hidden,
- locations: req.body.locations,
- eventName: req.body.eventName,
- customInputs: !req.body.customInputs
- ? undefined
- : {
- deleteMany: {
- eventTypeId: req.body.id,
- NOT: {
- id: {in: req.body.customInputs.filter(input => !!input.id).map(e => e.id)}
- }
- },
- createMany: {
- data: req.body.customInputs.filter(input => !input.id).map(input => ({
- type: input.type,
- label: input.label,
- required: input.required
- }))
- },
- update: req.body.customInputs.filter(input => !!input.id).map(input => ({
- data: {
- type: input.type,
- label: input.label,
- required: input.required
- },
- where: {
- id: input.id
- }
- }))
+ if (req.method == "PATCH" || req.method == "POST") {
+ const data = {
+ title: req.body.title,
+ slug: req.body.slug,
+ description: req.body.description,
+ length: parseInt(req.body.length),
+ hidden: req.body.hidden,
+ locations: req.body.locations,
+ eventName: req.body.eventName,
+ customInputs: !req.body.customInputs
+ ? undefined
+ : {
+ deleteMany: {
+ eventTypeId: req.body.id,
+ NOT: {
+ id: { in: req.body.customInputs.filter((input) => !!input.id).map((e) => e.id) },
},
- };
-
- if (req.method == "POST") {
- const createEventType = await prisma.eventType.create({
- data: {
- userId: session.user.id,
- ...data,
- },
- });
- res.status(200).json({message: 'Event created successfully'});
- }
- else if (req.method == "PATCH") {
- const updateEventType = await prisma.eventType.update({
- where: {
- id: req.body.id,
- },
- data,
- });
- res.status(200).json({message: 'Event updated successfully'});
- }
- }
-
- if (req.method == "DELETE") {
-
- const deleteEventType = await prisma.eventType.delete({
- where: {
- id: req.body.id,
},
- });
+ createMany: {
+ data: req.body.customInputs
+ .filter((input) => !input.id)
+ .map((input) => ({
+ type: input.type,
+ label: input.label,
+ required: input.required,
+ })),
+ },
+ update: req.body.customInputs
+ .filter((input) => !!input.id)
+ .map((input) => ({
+ data: {
+ type: input.type,
+ label: input.label,
+ required: input.required,
+ },
+ where: {
+ id: input.id,
+ },
+ })),
+ },
+ };
- res.status(200).json({message: 'Event deleted successfully'});
+ if (req.method == "POST") {
+ await prisma.eventType.create({
+ data: {
+ userId: session.user.id,
+ ...data,
+ },
+ });
+ res.status(200).json({ message: "Event created successfully" });
+ } else if (req.method == "PATCH") {
+ if (req.body.timeZone) {
+ data.timeZone = req.body.timeZone;
+ }
+
+ if (req.body.availability) {
+ const openingHours = req.body.availability.openingHours || [];
+ // const overrides = req.body.availability.dateOverrides || [];
+
+ await prisma.availability.deleteMany({
+ where: {
+ eventTypeId: +req.body.id,
+ },
+ });
+ Promise.all(
+ openingHours.map((schedule) =>
+ prisma.availability.create({
+ data: {
+ eventTypeId: +req.body.id,
+ days: schedule.days,
+ startTime: schedule.startTime,
+ endTime: schedule.endTime,
+ },
+ })
+ )
+ ).catch((error) => {
+ console.log(error);
+ });
+ }
+
+ await prisma.eventType.update({
+ where: {
+ id: req.body.id,
+ },
+ data,
+ });
+ res.status(200).json({ message: "Event updated successfully" });
}
+ }
+
+ if (req.method == "DELETE") {
+ await prisma.eventType.delete({
+ where: {
+ id: req.body.id,
+ },
+ });
+
+ res.status(200).json({ message: "Event deleted successfully" });
+ }
}
diff --git a/pages/api/availability/week.ts b/pages/api/availability/week.ts
new file mode 100644
index 0000000000..d52c55d7b7
--- /dev/null
+++ b/pages/api/availability/week.ts
@@ -0,0 +1,30 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { getSession } from 'next-auth/client';
+import prisma from '../../../lib/prisma';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const session = await getSession({req: req});
+
+ if (!session) {
+ res.status(401).json({message: "Not authenticated"});
+ return;
+ }
+
+ if (req.method == "PATCH") {
+
+ const startMins = req.body.start;
+ const endMins = req.body.end;
+
+ const updateWeek = await prisma.schedule.update({
+ where: {
+ id: session.user.id,
+ },
+ data: {
+ startTime: startMins,
+ endTime: endMins
+ },
+ });
+
+ res.status(200).json({message: 'Start and end times updated successfully'});
+ }
+}
\ No newline at end of file
diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx
index ff41c7614c..de3e435339 100644
--- a/pages/availability/event/[type].tsx
+++ b/pages/availability/event/[type].tsx
@@ -1,17 +1,66 @@
+import { GetServerSideProps } from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
-import { useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import Select, { OptionBase } from "react-select";
-import prisma from "../../../lib/prisma";
-import { LocationType } from "../../../lib/location";
-import Shell from "../../../components/Shell";
-import { getSession, useSession } from "next-auth/client";
-import { LocationMarkerIcon, PhoneIcon, PlusCircleIcon, XIcon } from "@heroicons/react/outline";
-import { EventTypeCustomInput, EventTypeCustomInputType } from "../../../lib/eventTypeInput";
+import prisma from "@lib/prisma";
+import { LocationType } from "@lib/location";
+import Shell from "@components/Shell";
+import { getSession } from "next-auth/client";
+import { Scheduler } from "@components/ui/Scheduler";
+
+import { LocationMarkerIcon, PlusCircleIcon, XIcon, PhoneIcon } from "@heroicons/react/outline";
+import { EventTypeCustomInput, EventTypeCustomInputType } from "@lib/eventTypeInput";
import { PlusIcon } from "@heroicons/react/solid";
-export default function EventType(props: any): JSX.Element {
+import dayjs from "dayjs";
+import utc from "dayjs/plugin/utc";
+dayjs.extend(utc);
+import timezone from "dayjs/plugin/timezone";
+import { EventType, User, Availability } from "@prisma/client";
+import { validJson } from "@lib/jsonUtils";
+dayjs.extend(timezone);
+
+type Props = {
+ user: User;
+ eventType: EventType;
+ locationOptions: OptionBase[];
+ availability: Availability[];
+};
+
+type OpeningHours = {
+ days: number[];
+ startTime: number;
+ endTime: number;
+};
+
+type DateOverride = {
+ date: string;
+ startTime: number;
+ endTime: number;
+};
+
+type EventTypeInput = {
+ id: number;
+ title: string;
+ slug: string;
+ description: string;
+ length: number;
+ hidden: boolean;
+ locations: unknown;
+ eventName: string;
+ customInputs: EventTypeCustomInput[];
+ timeZone: string;
+ availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] };
+};
+
+export default function EventTypePage({
+ user,
+ eventType,
+ locationOptions,
+ availability,
+}: Props): JSX.Element {
const router = useRouter();
const inputOptions: OptionBase[] = [
@@ -21,17 +70,17 @@ export default function EventType(props: any): JSX.Element {
{ value: EventTypeCustomInputType.Bool, label: "Checkbox" },
];
- const [, loading] = useSession();
+ const [enteredAvailability, setEnteredAvailability] = useState();
const [showLocationModal, setShowLocationModal] = useState(false);
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
+ const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [selectedLocation, setSelectedLocation] = useState
(undefined);
const [selectedInputOption, setSelectedInputOption] = useState(inputOptions[0]);
+ const [locations, setLocations] = useState(eventType.locations || []);
const [selectedCustomInput, setSelectedCustomInput] = useState(undefined);
- const [locations, setLocations] = useState(props.eventType.locations || []);
const [customInputs, setCustomInputs] = useState(
- props.eventType.customInputs.sort((a, b) => a.id - b.id) || []
+ eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
- const locationOptions = props.locationOptions;
const titleRef = useRef();
const slugRef = useRef();
@@ -40,34 +89,41 @@ export default function EventType(props: any): JSX.Element {
const isHiddenRef = useRef();
const eventNameRef = useRef();
- if (loading) {
- return Loading...
;
- }
+ useEffect(() => {
+ setSelectedTimeZone(eventType.timeZone || user.timeZone);
+ }, []);
async function updateEventTypeHandler(event) {
event.preventDefault();
- const enteredTitle = titleRef.current.value;
- const enteredSlug = slugRef.current.value;
- const enteredDescription = descriptionRef.current.value;
- const enteredLength = lengthRef.current.value;
- const enteredIsHidden = isHiddenRef.current.checked;
- const enteredEventName = eventNameRef.current.value;
+ const enteredTitle: string = titleRef.current.value;
+ const enteredSlug: string = slugRef.current.value;
+ const enteredDescription: string = descriptionRef.current.value;
+ const enteredLength: number = parseInt(lengthRef.current.value);
+ const enteredIsHidden: boolean = isHiddenRef.current.checked;
+ const enteredEventName: string = eventNameRef.current.value;
// TODO: Add validation
+ const payload: EventTypeInput = {
+ id: eventType.id,
+ title: enteredTitle,
+ slug: enteredSlug,
+ description: enteredDescription,
+ length: enteredLength,
+ hidden: enteredIsHidden,
+ locations,
+ eventName: enteredEventName,
+ customInputs,
+ timeZone: selectedTimeZone,
+ };
+
+ if (enteredAvailability) {
+ payload.availability = enteredAvailability;
+ }
+
await fetch("/api/availability/eventtype", {
method: "PATCH",
- body: JSON.stringify({
- id: props.eventType.id,
- title: enteredTitle,
- slug: enteredSlug,
- description: enteredDescription,
- length: enteredLength,
- hidden: enteredIsHidden,
- locations,
- eventName: enteredEventName,
- customInputs,
- }),
+ body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
@@ -81,7 +137,7 @@ export default function EventType(props: any): JSX.Element {
await fetch("/api/availability/eventtype", {
method: "DELETE",
- body: JSON.stringify({ id: props.eventType.id }),
+ body: JSON.stringify({ id: eventType.id }),
headers: {
"Content-Type": "application/json",
},
@@ -106,6 +162,30 @@ export default function EventType(props: any): JSX.Element {
setSelectedCustomInput(undefined);
};
+ const updateLocations = (e) => {
+ e.preventDefault();
+
+ let details = {};
+ if (e.target.location.value === LocationType.InPerson) {
+ details = { address: e.target.address.value };
+ }
+
+ const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type);
+ if (existingIdx !== -1) {
+ const copy = locations;
+ copy[existingIdx] = { ...locations[existingIdx], ...details };
+ setLocations(copy);
+ } else {
+ setLocations(locations.concat({ type: e.target.location.value, ...details }));
+ }
+
+ setShowLocationModal(false);
+ };
+
+ const removeLocation = (selectedLocation) => {
+ setLocations(locations.filter((location) => location.type !== selectedLocation.type));
+ };
+
const openEditCustomModel = (customInput: EventTypeCustomInput) => {
setSelectedCustomInput(customInput);
setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type));
@@ -147,30 +227,6 @@ export default function EventType(props: any): JSX.Element {
return null;
};
- const updateLocations = (e) => {
- e.preventDefault();
-
- let details = {};
- if (e.target.location.value === LocationType.InPerson) {
- details = { address: e.target.address.value };
- }
-
- const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type);
- if (existingIdx !== -1) {
- const copy = locations;
- copy[existingIdx] = { ...locations[existingIdx], ...details };
- setLocations(copy);
- } else {
- setLocations(locations.concat({ type: e.target.location.value, ...details }));
- }
-
- setShowLocationModal(false);
- };
-
- const removeLocation = (selectedLocation) => {
- setLocations(locations.filter((location) => location.type !== selectedLocation.type));
- };
-
const updateCustom = (e) => {
e.preventDefault();
@@ -207,13 +263,13 @@ export default function EventType(props: any): JSX.Element {
return (
-
{props.eventType.title} | Event Type | Calendso
+
{eventType.title} | Event Type | Calendso
-
-
-
-
+
+
+
+
@@ -240,7 +296,7 @@ export default function EventType(props: any): JSX.Element {
@@ -390,7 +446,7 @@ export default function EventType(props: any): JSX.Element {
id="description"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="A quick video meeting."
- defaultValue={props.eventType.description}>
+ defaultValue={eventType.description}>
@@ -406,7 +462,7 @@ export default function EventType(props: any): JSX.Element {
required
className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md"
placeholder="15"
- defaultValue={props.eventType.length}
+ defaultValue={eventType.length}
/>
minutes
@@ -425,7 +481,7 @@ export default function EventType(props: any): JSX.Element {
id="title"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Meeting with {USER}"
- defaultValue={props.eventType.eventName}
+ defaultValue={eventType.eventName}
/>
@@ -484,7 +540,7 @@ export default function EventType(props: any): JSX.Element {
name="ishidden"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
- defaultChecked={props.eventType.hidden}
+ defaultChecked={eventType.hidden}
/>
@@ -497,12 +553,24 @@ export default function EventType(props: any): JSX.Element {
-
-
-
Cancel
-
+
+
+
How do you want to offer your availability for this event type?
+
+
+
@@ -649,9 +717,7 @@ export default function EventType(props: any): JSX.Element {
Is required
-