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 {
@@ -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 {