diff --git a/components/Schedule.model.tsx b/components/Schedule.model.tsx deleted file mode 100644 index d100f13d..00000000 --- a/components/Schedule.model.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import {Dayjs} from "dayjs"; - -interface Schedule { - startDate: Dayjs; - endDate: Dayjs; -} \ No newline at end of file diff --git a/components/booking/Slots.tsx b/components/booking/Slots.tsx index e3091e96..3c689d5c 100644 --- a/components/booking/Slots.tsx +++ b/components/booking/Slots.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/router"; import getSlots from "../../lib/slots"; -import dayjs, {Dayjs} from "dayjs"; +import dayjs, { Dayjs } from "dayjs"; import isBetween from "dayjs/plugin/isBetween"; import utc from "dayjs/plugin/utc"; dayjs.extend(isBetween); @@ -11,44 +11,43 @@ type Props = { eventLength: number; minimumBookingNotice?: number; date: Dayjs; -} - -const Slots = ({ eventLength, minimumBookingNotice, date, workingHours }: Props) => { +}; +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); + 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 + `/api/availability/${user}?dateFrom=${date.startOf("day").utc().startOf("day").format()}&dateTo=${date .endOf("day") .utc() - .endOf('day') + .endOf("day") .format()}` ) .then((res) => res.json()) .then(handleAvailableSlots) - .catch( e => { + .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; @@ -56,7 +55,6 @@ const Slots = ({ eventLength, minimumBookingNotice, date, workingHours }: Props) // Check for conflicts for (let i = times.length - 1; i >= 0; i -= 1) { busyTimes.forEach((busyTime) => { - const startTime = dayjs(busyTime.start).utc(); const endTime = dayjs(busyTime.end).utc(); diff --git a/components/ui/Scheduler.tsx b/components/ui/Scheduler.tsx index 4dea789e..bbe3ad28 100644 --- a/components/ui/Scheduler.tsx +++ b/components/ui/Scheduler.tsx @@ -3,56 +3,87 @@ import TimezoneSelect from "react-timezone-select"; import { TrashIcon } from "@heroicons/react/outline"; import { WeekdaySelect } from "./WeekdaySelect"; import SetTimesModal from "./modal/SetTimesModal"; -import Schedule from "../../lib/schedule.model"; 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); -export const Scheduler = (props) => { - const [schedules, setSchedules]: Schedule[] = useState( - props.schedules.map((schedule) => { - const startDate = schedule.isOverride - ? dayjs(schedule.startDate) - : dayjs.utc().startOf("day").add(schedule.startTime, "minutes").tz(props.timeZone); - return { - days: schedule.days, - startDate, - endDate: startDate.add(schedule.length, "minutes"), - }; - }) - ); +type Props = { + timeZone: string; + availability: Availability[]; + setTimeZone: unknown; +}; - const [timeZone, setTimeZone] = useState(props.timeZone); +export const Scheduler = ({ + availability, + setAvailability, + timeZone: selectedTimeZone, + setTimeZone, +}: Props) => { const [editSchedule, setEditSchedule] = useState(-1); + const [dateOverrides, setDateOverrides] = useState([]); + const [openingHours, setOpeningHours] = useState([]); useEffect(() => { - props.onChange(schedules); - }, [schedules]); + 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)); + }, []); - const addNewSchedule = () => setEditSchedule(schedules.length); + // updates availability to how it should be formatted outside this component. + useEffect(() => { + setAvailability({ + dateOverrides: dateOverrides, + openingHours: openingHours, + }); + }, [dateOverrides, openingHours]); - const applyEditSchedule = (changed: Schedule) => { - const replaceWith = { - ...schedules[editSchedule], - ...changed, - }; + const addNewSchedule = () => setEditSchedule(openingHours.length); - schedules.splice(editSchedule, 1, replaceWith); + const applyEditSchedule = (changed) => { + if (!changed.days) { + changed.days = [1, 2, 3, 4, 5]; // Mon - Fri + } - setSchedules([].concat(schedules)); + const replaceWith = { ...openingHours[editSchedule], ...changed }; + openingHours.splice(editSchedule, 1, replaceWith); + setOpeningHours([].concat(openingHours)); }; const removeScheduleAt = (toRemove: number) => { - schedules.splice(toRemove, 1); - setSchedules([].concat(schedules)); + openingHours.splice(toRemove, 1); + setOpeningHours([].concat(openingHours)); }; - const setWeekdays = (idx: number, days: number[]) => { - schedules[idx].days = days; - setSchedules([].concat(schedules)); - }; + const OpeningHours = ({ idx, item }) => ( +
  • +
    + (item.days = selected)} /> + +
    + +
  • + ); + + console.log(selectedTimeZone); return (
    @@ -65,32 +96,15 @@ export const Scheduler = (props) => {
    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" />

    @@ -108,7 +122,7 @@ export const Scheduler = (props) => { {editSchedule >= 0 && ( setEditSchedule(-1)} /> diff --git a/components/ui/modal/SetTimesModal.tsx b/components/ui/modal/SetTimesModal.tsx index 2a9b03a9..a286780e 100644 --- a/components/ui/modal/SetTimesModal.tsx +++ b/components/ui/modal/SetTimesModal.tsx @@ -7,9 +7,15 @@ dayjs.extend(utc); dayjs.extend(timezone); export default function SetTimesModal(props) { - const { startDate, endDate } = props.schedule || { - startDate: dayjs.utc().startOf("day").add(540, "minutes"), - endDate: dayjs.utc().startOf("day").add(1020, "minutes"), + const { startDate, endDate } = { + startDate: dayjs + .utc() + .startOf("day") + .add(props.schedule.startTime || 540, "minutes"), + endDate: dayjs + .utc() + .startOf("day") + .add(props.schedule.endTime || 1020, "minutes"), }; startDate.tz(props.timeZone); diff --git a/lib/jsonUtils.ts b/lib/jsonUtils.ts new file mode 100644 index 00000000..3f617cb0 --- /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/schedule.model.tsx b/lib/schedule.model.tsx deleted file mode 100644 index 7a5f6354..00000000 --- a/lib/schedule.model.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import {Dayjs} from "dayjs"; - -export default interface Schedule { - id: number | null; - startDate: Dayjs; - endDate: Dayjs; -} \ No newline at end of file diff --git a/lib/slots.ts b/lib/slots.ts index c3dad4b5..7d3a7773 100644 --- a/lib/slots.ts +++ b/lib/slots.ts @@ -1,12 +1,15 @@ -import dayjs, {Dayjs} from "dayjs"; +import dayjs, { Dayjs } from "dayjs"; import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; dayjs.extend(utc); +dayjs.extend(timezone); interface GetSlotsType { inviteeDate: Dayjs; frequency: number; workingHours: []; minimumBookingNotice?: number; + organizerUtcOffset: number; } interface Boundary { @@ -17,32 +20,41 @@ interface Boundary { const freqApply: number = (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 - ) { + 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) + upperBound: Math.min(b.upperBound, a.upperBound), }; -} +}; // 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); +const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) => + boundaries.map((boundary) => intersectBoundary(inviteeBoundary, boundary)).filter(Boolean); -const organizerBoundaries = (workingHours: [], inviteeDate: Dayjs, inviteeBounds: Boundary): Boundary[] => { +const organizerBoundaries = ( + workingHours: [], + inviteeDate: Dayjs, + inviteeBounds: Boundary, + organizerTimeZone +): Boundary[] => { const boundaries: Boundary[] = []; - const startDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.lowerBound, 'minutes').format('d'); - const endDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.upperBound, 'minutes').format('d'); + const startDay: number = +inviteeDate + .utc() + .startOf("day") + .add(inviteeBounds.lowerBound, "minutes") + .format("d"); + const endDay: number = +inviteeDate + .utc() + .startOf("day") + .add(inviteeBounds.upperBound, "minutes") + .format("d"); - workingHours.forEach( (item) => { - const lowerBound: number = item.startTime; - const upperBound: number = lowerBound + item.length; + 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 @@ -62,7 +74,7 @@ const organizerBoundaries = (workingHours: [], inviteeDate: Dayjs, inviteeBounds } } } else { - boundaries.push({lowerBound, upperBound}); + boundaries.push({ lowerBound, upperBound }); } }); return boundaries; @@ -72,38 +84,42 @@ const inviteeBoundary = (startTime: number, utcOffset: number, frequency: number const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency); const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency); return { - lowerBound, upperBound, + lowerBound, + upperBound, }; }; -const getSlotsBetweenBoundary = (frequency: number, {lowerBound,upperBound}: Boundary) => { +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')); + for (let minutes = 0; lowerBound + minutes <= upperBound - frequency; minutes += frequency) { + slots.push( + dayjs + .utc() + .startOf("day") + .add(lowerBound + minutes, "minutes") + ); } return slots; }; -const getSlots = ( - { inviteeDate, frequency, minimumBookingNotice, workingHours, }: GetSlotsType -): Dayjs[] => { - - const startTime = ( - dayjs.utc().isSame(dayjs(inviteeDate), 'day') ? inviteeDate.hour() * 60 + inviteeDate.minute() + minimumBookingNotice : 0 - ); +const getSlots = ({ + inviteeDate, + frequency, + minimumBookingNotice, + workingHours, + organizerTimeZone, +}: GetSlotsType): Dayjs[] => { + const startTime = dayjs.utc().isSame(dayjs(inviteeDate), "day") + ? inviteeDate.hour() * 60 + inviteeDate.minute() + minimumBookingNotice + : 0; const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency); return getOverlaps( - inviteeBounds, organizerBoundaries(workingHours, inviteeDate, inviteeBounds) - ).reduce( - (slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary) ], [] - ).map( - (slot) => slot.utcOffset(dayjs(inviteeDate).utcOffset()) + inviteeBounds, + organizerBoundaries(workingHours, inviteeDate, inviteeBounds, organizerTimeZone) ) -} + .reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], []) + .map((slot) => slot.utcOffset(dayjs(inviteeDate).utcOffset())); +}; export default getSlots; diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 328a2884..42fba5ec 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -32,6 +32,7 @@ export default function Type(props): Type { frequency: props.eventType.length, inviteeDate: dayjs.utc(today) as Dayjs, workingHours: props.workingHours, + organizerTimeZone: props.eventType.timeZone, minimumBookingNotice: 0, }).length === 0, [today, props.eventType.length, props.workingHours] @@ -63,21 +64,46 @@ export default function Type(props): Type { {rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username} | Calendso - + - - - " + props.eventType.description).replace(/'/g, "%27") + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + encodeURIComponent(props.user.avatar)} /> + + + " + props.eventType.description + ).replace(/'/g, "%27") + + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + + encodeURIComponent(props.user.avatar) + } + /> - + - " + props.eventType.description).replace(/'/g, "%27") + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + encodeURIComponent(props.user.avatar)} /> - + " + props.eventType.description + ).replace(/'/g, "%27") + + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + + encodeURIComponent(props.user.avatar) + } + />
    { description: true, length: true, availability: true, + timeZone: true, }, }); diff --git a/pages/api/availability/eventtype.ts b/pages/api/availability/eventtype.ts index e50fc4ab..dc5709e6 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/schedule/[eventtype].ts b/pages/api/availability/schedule/[eventtype].ts deleted file mode 100644 index 9d3a9eb6..00000000 --- a/pages/api/availability/schedule/[eventtype].ts +++ /dev/null @@ -1,69 +0,0 @@ -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}); - if (!session) { - res.status(401).json({message: "Not authenticated"}); - return; - } - - if (req.method == "PUT") { - - const openingHours = req.body.openingHours || []; - const overrides = req.body.overrides || []; - - const removeSchedule = await prisma.schedule.deleteMany({ - where: { - eventTypeId: +req.query.eventtype, - } - }) - - const updateSchedule = Promise.all(openingHours.map( (schedule) => prisma.schedule.create({ - data: { - eventTypeId: +req.query.eventtype, - days: schedule.days, - startTime: schedule.startTime, - length: schedule.endTime - schedule.startTime, - }, - }))) - .catch( (error) => { - console.log(error); - }) - } - - res.status(200).json({message: 'Created schedule'}); - - /*if (req.method == "PATCH") { - const openingHours = req.body.openingHours || []; - const overrides = req.body.overrides || []; - - openingHours.forEach( (schedule) => { - const updateSchedule = await prisma.schedule.update({ - where: { - id: req.body.id, - }, - data: { - eventTypeId: req.query.eventtype, - days: req.body.days, - startTime: 333, - endTime: 540 - req.body.startTime, - }, - }); - }); - - overrides.forEach( (schedule) => { - const updateSchedule = await prisma.schedule.update({ - where: { - id: req.body.id, - }, - data: { - eventTypeId: req.query.eventtype, - startDate: req.body.startDate, - length: 540, - }, - }); - });*/ -} diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index 4a48b7b1..2237d065 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -1,26 +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 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 { Scheduler } from "@components/ui/Scheduler"; import { LocationMarkerIcon, PlusCircleIcon, XIcon, PhoneIcon } from "@heroicons/react/outline"; -import { EventTypeCustomInput, EventTypeCustomInputType } from "../../../lib/eventTypeInput"; +import { EventTypeCustomInput, EventTypeCustomInputType } from "@lib/eventTypeInput"; import { PlusIcon } from "@heroicons/react/solid"; 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); -export default function EventType(props: any): JSX.Element { +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[] = [ @@ -30,17 +70,17 @@ export default function EventType(props: any): JSX.Element { { value: EventTypeCustomInputType.Bool, label: "Checkbox" }, ]; + 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(props.eventType.locations || []); - const [schedule, setSchedule] = useState(undefined); + const [locations, setLocations] = useState(eventType.locations || []); const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); 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(); @@ -49,60 +89,55 @@ export default function EventType(props: any): JSX.Element { const isHiddenRef = useRef(); const eventNameRef = useRef(); + 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 = { + dateOverrides: [], + openingHours: enteredAvailability.openingHours.map((item): OpeningHours => { + item.startTime = item.startDate.hour() * 60 + item.startDate.minute(); + delete item.startDate; + item.endTime = item.endDate.hour() * 60 + item.endDate.minute(); + delete item.endDate; + return item; + }), + }; + } + 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", }, }); - if (schedule) { - const schedulePayload = { overrides: [], timeZone: props.user.timeZone, openingHours: [] }; - schedule.forEach((item) => { - if (item.isOverride) { - delete item.isOverride; - schedulePayload.overrides.push(item); - } else { - const endTime = item.endDate.hour() * 60 + item.endDate.minute() || 1440; // also handles 00:00 - schedulePayload.openingHours.push({ - days: item.days, - startTime: item.startDate.hour() * 60 + item.startDate.minute() - item.startDate.utcOffset(), - endTime: endTime - item.endDate.utcOffset(), - }); - } - }); - - await fetch("/api/availability/schedule/" + props.eventType.id, { - method: "PUT", - body: JSON.stringify(schedulePayload), - headers: { - "Content-Type": "application/json", - }, - }); - } - router.push("/availability"); } @@ -111,7 +146,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", }, @@ -237,10 +272,10 @@ export default function EventType(props: any): JSX.Element { return (
    - {props.eventType.title} | Event Type | Calendso + {eventType.title} | Event Type | Calendso - +
    @@ -259,7 +294,7 @@ export default function EventType(props: any): JSX.Element { required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Quick Chat" - defaultValue={props.eventType.title} + defaultValue={eventType.title} />
    @@ -270,7 +305,7 @@ export default function EventType(props: any): JSX.Element {
    - {typeof location !== "undefined" ? location.hostname : ""}/{props.user.username}/ + {typeof location !== "undefined" ? location.hostname : ""}/{user.username}/
    @@ -420,7 +455,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}>
    @@ -436,7 +471,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 @@ -455,7 +490,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} />
    @@ -514,7 +549,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} />
    @@ -531,9 +566,10 @@ export default function EventType(props: any): JSX.Element {

    How do you want to offer your availability for this event type?

    @@ -709,24 +745,18 @@ export default function EventType(props: any): JSX.Element { ); } -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; -}; - -export async function getServerSideProps(context) { - const session = await getSession(context); +export const getServerSideProps: GetServerSideProps = async ({ req, query }) => { + const session = await getSession({ req }); if (!session) { - return { redirect: { permanent: false, destination: "/auth/login" } }; + return { + redirect: { + permanent: false, + destination: "/auth/login", + }, + }; } - const user = await prisma.user.findFirst({ + + const user: User = await prisma.user.findFirst({ where: { email: session.user.email, }, @@ -739,9 +769,9 @@ export async function getServerSideProps(context) { }, }); - const eventType = await prisma.eventType.findUnique({ + const eventType: EventType | null = await prisma.eventType.findUnique({ where: { - id: parseInt(context.query.type), + id: parseInt(query.type as string), }, select: { id: true, @@ -754,9 +784,16 @@ export async function getServerSideProps(context) { eventName: true, availability: true, customInputs: true, + timeZone: true, }, }); + if (!eventType) { + return { + notFound: true, + }; + } + const credentials = await prisma.credential.findMany({ where: { userId: user.id, @@ -808,18 +845,12 @@ export async function getServerSideProps(context) { // Assuming it's Microsoft Teams. } - if (!eventType) { - return { - notFound: true, - }; - } - const getAvailability = (providesAvailability) => providesAvailability.availability && providesAvailability.availability.length ? providesAvailability.availability : null; - const schedules = getAvailability(eventType) || + const availability: Availability[] = getAvailability(eventType) || getAvailability(user) || [ { days: [0, 1, 2, 3, 4, 5, 6], @@ -832,8 +863,8 @@ export async function getServerSideProps(context) { props: { user, eventType, - schedules, locationOptions, + availability, }, }; -} +}; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 21291251..1d4fe82a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,7 @@ model EventType { availability Availability[] eventName String? customInputs EventTypeCustomInput[] + timeZone String? } model Credential {