Final thing to check is timezones, currently if I am in Kuala Lumpur the time is correct, but it jumps 8 hours due to being out of bound on Seoul.
This commit is contained in:
parent
b4272ad7aa
commit
575747bcd3
12 changed files with 413 additions and 361 deletions
|
@ -1,6 +0,0 @@
|
|||
import {Dayjs} from "dayjs";
|
||||
|
||||
interface Schedule {
|
||||
startDate: Dayjs;
|
||||
endDate: Dayjs;
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 }) => (
|
||||
<li className="py-2 flex justify-between border-t">
|
||||
<div className="inline-flex ml-2">
|
||||
<WeekdaySelect defaultValue={item.days} onSelect={(selected: number[]) => (item.days = selected)} />
|
||||
<button className="ml-2 text-sm px-2" type="button" onClick={() => setEditSchedule(idx)}>
|
||||
{dayjs(item.startDate).format(item.startDate.minute() === 0 ? "ha" : "h:mma")}
|
||||
until
|
||||
{dayjs(item.endDate).format(item.endDate.minute() === 0 ? "ha" : "h:mma")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeScheduleAt(idx)}
|
||||
className="btn-sm bg-transparent px-2 py-1 ml-1">
|
||||
<TrashIcon className="h-6 w-6 inline text-gray-400 -mt-1" />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
|
||||
console.log(selectedTimeZone);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -65,32 +96,15 @@ export const Scheduler = (props) => {
|
|||
<div className="mt-1">
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={timeZone}
|
||||
onChange={setTimeZone}
|
||||
value={selectedTimeZone}
|
||||
onChange={(tz) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{schedules.map((schedule, idx) => (
|
||||
<li key={idx} className="py-2 flex justify-between border-t">
|
||||
<div className="inline-flex ml-2">
|
||||
<WeekdaySelect
|
||||
defaultValue={schedules[idx].days}
|
||||
onSelect={(days: number[]) => setWeekdays(idx, days)}
|
||||
/>
|
||||
<button className="ml-2 text-sm px-2" type="button" onClick={() => setEditSchedule(idx)}>
|
||||
{dayjs(schedule.startDate).format(schedule.startDate.minute() === 0 ? "ha" : "h:mma")}{" "}
|
||||
until {dayjs(schedule.endDate).format(schedule.endDate.minute() === 0 ? "ha" : "h:mma")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeScheduleAt(idx)}
|
||||
className="btn-sm bg-transparent px-2 py-1 ml-1">
|
||||
<TrashIcon className="h-6 w-6 inline text-gray-400 -mt-1" />
|
||||
</button>
|
||||
</li>
|
||||
{openingHours.map((item, idx) => (
|
||||
<OpeningHours key={idx} idx={idx} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
<hr />
|
||||
|
@ -108,7 +122,7 @@ export const Scheduler = (props) => {
|
|||
</div>
|
||||
{editSchedule >= 0 && (
|
||||
<SetTimesModal
|
||||
schedule={schedules[editSchedule]}
|
||||
schedule={{ ...openingHours[editSchedule], timeZone: selectedTimeZone }}
|
||||
onChange={applyEditSchedule}
|
||||
onExit={() => setEditSchedule(-1)}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
|
|
11
lib/jsonUtils.ts
Normal file
11
lib/jsonUtils.ts
Normal file
|
@ -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;
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
import {Dayjs} from "dayjs";
|
||||
|
||||
export default interface Schedule {
|
||||
id: number | null;
|
||||
startDate: Dayjs;
|
||||
endDate: Dayjs;
|
||||
}
|
92
lib/slots.ts
92
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>dayjs.utc().startOf('day').add(lowerBound + minutes, 'minutes'));
|
||||
for (let minutes = 0; lowerBound + minutes <= upperBound - frequency; minutes += frequency) {
|
||||
slots.push(
|
||||
<Dayjs>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;
|
||||
|
|
|
@ -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]
|
||||
|
@ -68,16 +69,41 @@ export default function Type(props): Type {
|
|||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://calendso/" />
|
||||
<meta property="og:title" content={"Meet " + (props.user.name || props.user.username) + " via Calendso"}/>
|
||||
<meta property="og:description" content={props.eventType.description}/>
|
||||
<meta property="og:image" content={"https://og-image-one-pi.vercel.app/" + encodeURIComponent("Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description).replace(/'/g, "%27") + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + encodeURIComponent(props.user.avatar)} />
|
||||
<meta
|
||||
property="og:title"
|
||||
content={"Meet " + (props.user.name || props.user.username) + " via Calendso"}
|
||||
/>
|
||||
<meta property="og:description" content={props.eventType.description} />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={
|
||||
"https://og-image-one-pi.vercel.app/" +
|
||||
encodeURIComponent(
|
||||
"Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description
|
||||
).replace(/'/g, "%27") +
|
||||
".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" +
|
||||
encodeURIComponent(props.user.avatar)
|
||||
}
|
||||
/>
|
||||
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://calendso/" />
|
||||
<meta property="twitter:title" content={"Meet " + (props.user.name || props.user.username) + " via Calendso"} />
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content={"Meet " + (props.user.name || props.user.username) + " via Calendso"}
|
||||
/>
|
||||
<meta property="twitter:description" content={props.eventType.description} />
|
||||
<meta property="twitter:image" content={"https://og-image-one-pi.vercel.app/" + encodeURIComponent("Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description).replace(/'/g, "%27") + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + encodeURIComponent(props.user.avatar)} />
|
||||
|
||||
<meta
|
||||
property="twitter:image"
|
||||
content={
|
||||
"https://og-image-one-pi.vercel.app/" +
|
||||
encodeURIComponent(
|
||||
"Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description
|
||||
).replace(/'/g, "%27") +
|
||||
".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" +
|
||||
encodeURIComponent(props.user.avatar)
|
||||
}
|
||||
/>
|
||||
</Head>
|
||||
<main
|
||||
className={
|
||||
|
@ -184,6 +210,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
description: true,
|
||||
length: true,
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
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});
|
||||
const session = await getSession({ req: req });
|
||||
if (!session) {
|
||||
res.status(401).json({message: "Not authenticated"});
|
||||
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,
|
||||
|
@ -25,57 +24,88 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
deleteMany: {
|
||||
eventTypeId: req.body.id,
|
||||
NOT: {
|
||||
id: {in: req.body.customInputs.filter(input => !!input.id).map(e => e.id)}
|
||||
}
|
||||
id: { in: req.body.customInputs.filter((input) => !!input.id).map((e) => e.id) },
|
||||
},
|
||||
},
|
||||
createMany: {
|
||||
data: req.body.customInputs.filter(input => !input.id).map(input => ({
|
||||
data: req.body.customInputs
|
||||
.filter((input) => !input.id)
|
||||
.map((input) => ({
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required
|
||||
}))
|
||||
required: input.required,
|
||||
})),
|
||||
},
|
||||
update: req.body.customInputs.filter(input => !!input.id).map(input => ({
|
||||
update: req.body.customInputs
|
||||
.filter((input) => !!input.id)
|
||||
.map((input) => ({
|
||||
data: {
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required
|
||||
required: input.required,
|
||||
},
|
||||
where: {
|
||||
id: input.id
|
||||
}
|
||||
}))
|
||||
id: input.id,
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
if (req.method == "POST") {
|
||||
const createEventType = await prisma.eventType.create({
|
||||
await prisma.eventType.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
res.status(200).json({message: 'Event created successfully'});
|
||||
res.status(200).json({ message: "Event created successfully" });
|
||||
} else if (req.method == "PATCH") {
|
||||
if (req.body.timeZone) {
|
||||
data.timeZone = req.body.timeZone;
|
||||
}
|
||||
else if (req.method == "PATCH") {
|
||||
const updateEventType = await prisma.eventType.update({
|
||||
|
||||
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'});
|
||||
res.status(200).json({ message: "Event updated successfully" });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method == "DELETE") {
|
||||
|
||||
const deleteEventType = await prisma.eventType.delete({
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: req.body.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({message: 'Event deleted successfully'});
|
||||
res.status(200).json({ message: "Event deleted successfully" });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});*/
|
||||
}
|
|
@ -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<OptionBase | undefined>(undefined);
|
||||
const [selectedInputOption, setSelectedInputOption] = useState<OptionBase>(inputOptions[0]);
|
||||
const [locations, setLocations] = useState(props.eventType.locations || []);
|
||||
const [schedule, setSchedule] = useState(undefined);
|
||||
const [locations, setLocations] = useState(eventType.locations || []);
|
||||
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
||||
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<HTMLInputElement>();
|
||||
const slugRef = useRef<HTMLInputElement>();
|
||||
|
@ -49,21 +89,23 @@ export default function EventType(props: any): JSX.Element {
|
|||
const isHiddenRef = useRef<HTMLInputElement>();
|
||||
const eventNameRef = useRef<HTMLInputElement>();
|
||||
|
||||
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
|
||||
|
||||
await fetch("/api/availability/eventtype", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
id: props.eventType.id,
|
||||
const payload: EventTypeInput = {
|
||||
id: eventType.id,
|
||||
title: enteredTitle,
|
||||
slug: enteredSlug,
|
||||
description: enteredDescription,
|
||||
|
@ -72,37 +114,30 @@ export default function EventType(props: any): JSX.Element {
|
|||
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(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 (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{props.eventType.title} | Event Type | Calendso</title>
|
||||
<title>{eventType.title} | Event Type | Calendso</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Shell heading={"Event Type - " + props.eventType.title}>
|
||||
<Shell heading={"Event Type - " + eventType.title}>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3 sm:col-span-2">
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg mb-4">
|
||||
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -270,7 +305,7 @@ export default function EventType(props: any): JSX.Element {
|
|||
<div className="mt-1">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
|
||||
{typeof location !== "undefined" ? location.hostname : ""}/{props.user.username}/
|
||||
{typeof location !== "undefined" ? location.hostname : ""}/{user.username}/
|
||||
</span>
|
||||
<input
|
||||
ref={slugRef}
|
||||
|
@ -279,7 +314,7 @@ export default function EventType(props: any): JSX.Element {
|
|||
id="slug"
|
||||
required
|
||||
className="flex-1 block w-full focus:ring-blue-500 focus:border-blue-500 min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"
|
||||
defaultValue={props.eventType.slug}
|
||||
defaultValue={eventType.slug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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}></textarea>
|
||||
defaultValue={eventType.description}></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
|
@ -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}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
|
@ -531,9 +566,10 @@ export default function EventType(props: any): JSX.Element {
|
|||
<div>
|
||||
<h3 className="mb-2">How do you want to offer your availability for this event type?</h3>
|
||||
<Scheduler
|
||||
onChange={setSchedule}
|
||||
timeZone={props.user.timeZone}
|
||||
schedules={props.schedules}
|
||||
setAvailability={setEnteredAvailability}
|
||||
setTimeZone={setSelectedTimeZone}
|
||||
timeZone={selectedTimeZone}
|
||||
availability={availability}
|
||||
/>
|
||||
<div className="py-4 flex justify-end">
|
||||
<Link href="/availability">
|
||||
|
@ -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<Props> = 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ model EventType {
|
|||
availability Availability[]
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
timeZone String?
|
||||
}
|
||||
|
||||
model Credential {
|
||||
|
|
Loading…
Reference in a new issue