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 { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import getSlots from "../../lib/slots";
|
import getSlots from "../../lib/slots";
|
||||||
import dayjs, {Dayjs} from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import isBetween from "dayjs/plugin/isBetween";
|
import isBetween from "dayjs/plugin/isBetween";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
dayjs.extend(isBetween);
|
dayjs.extend(isBetween);
|
||||||
|
@ -11,44 +11,43 @@ type Props = {
|
||||||
eventLength: number;
|
eventLength: number;
|
||||||
minimumBookingNotice?: number;
|
minimumBookingNotice?: number;
|
||||||
date: Dayjs;
|
date: Dayjs;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Slots = ({ eventLength, minimumBookingNotice, date, workingHours }: Props) => {
|
|
||||||
|
|
||||||
|
const Slots = ({ eventLength, minimumBookingNotice, date, workingHours, organizerUtcOffset }: Props) => {
|
||||||
minimumBookingNotice = minimumBookingNotice || 0;
|
minimumBookingNotice = minimumBookingNotice || 0;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user } = router.query;
|
const { user } = router.query;
|
||||||
const [slots, setSlots] = useState([]);
|
const [slots, setSlots] = useState([]);
|
||||||
const [isFullyBooked, setIsFullyBooked ] = useState(false);
|
const [isFullyBooked, setIsFullyBooked] = useState(false);
|
||||||
const [hasErrors, setHasErrors ] = useState(false);
|
const [hasErrors, setHasErrors] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSlots([]);
|
setSlots([]);
|
||||||
setIsFullyBooked(false);
|
setIsFullyBooked(false);
|
||||||
setHasErrors(false);
|
setHasErrors(false);
|
||||||
fetch(
|
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")
|
.endOf("day")
|
||||||
.utc()
|
.utc()
|
||||||
.endOf('day')
|
.endOf("day")
|
||||||
.format()}`
|
.format()}`
|
||||||
)
|
)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(handleAvailableSlots)
|
.then(handleAvailableSlots)
|
||||||
.catch( e => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setHasErrors(true);
|
setHasErrors(true);
|
||||||
})
|
});
|
||||||
}, [date]);
|
}, [date]);
|
||||||
|
|
||||||
const handleAvailableSlots = (busyTimes: []) => {
|
const handleAvailableSlots = (busyTimes: []) => {
|
||||||
|
|
||||||
const times = getSlots({
|
const times = getSlots({
|
||||||
frequency: eventLength,
|
frequency: eventLength,
|
||||||
inviteeDate: date,
|
inviteeDate: date,
|
||||||
workingHours,
|
workingHours,
|
||||||
minimumBookingNotice,
|
minimumBookingNotice,
|
||||||
|
organizerUtcOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timesLengthBeforeConflicts: number = times.length;
|
const timesLengthBeforeConflicts: number = times.length;
|
||||||
|
@ -56,7 +55,6 @@ const Slots = ({ eventLength, minimumBookingNotice, date, workingHours }: Props)
|
||||||
// Check for conflicts
|
// Check for conflicts
|
||||||
for (let i = times.length - 1; i >= 0; i -= 1) {
|
for (let i = times.length - 1; i >= 0; i -= 1) {
|
||||||
busyTimes.forEach((busyTime) => {
|
busyTimes.forEach((busyTime) => {
|
||||||
|
|
||||||
const startTime = dayjs(busyTime.start).utc();
|
const startTime = dayjs(busyTime.start).utc();
|
||||||
const endTime = dayjs(busyTime.end).utc();
|
const endTime = dayjs(busyTime.end).utc();
|
||||||
|
|
||||||
|
|
|
@ -3,56 +3,87 @@ import TimezoneSelect from "react-timezone-select";
|
||||||
import { TrashIcon } from "@heroicons/react/outline";
|
import { TrashIcon } from "@heroicons/react/outline";
|
||||||
import { WeekdaySelect } from "./WeekdaySelect";
|
import { WeekdaySelect } from "./WeekdaySelect";
|
||||||
import SetTimesModal from "./modal/SetTimesModal";
|
import SetTimesModal from "./modal/SetTimesModal";
|
||||||
import Schedule from "../../lib/schedule.model";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import { Availability } from "@prisma/client";
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
export const Scheduler = (props) => {
|
type Props = {
|
||||||
const [schedules, setSchedules]: Schedule[] = useState(
|
timeZone: string;
|
||||||
props.schedules.map((schedule) => {
|
availability: Availability[];
|
||||||
const startDate = schedule.isOverride
|
setTimeZone: unknown;
|
||||||
? 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"),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const [timeZone, setTimeZone] = useState(props.timeZone);
|
export const Scheduler = ({
|
||||||
|
availability,
|
||||||
|
setAvailability,
|
||||||
|
timeZone: selectedTimeZone,
|
||||||
|
setTimeZone,
|
||||||
|
}: Props) => {
|
||||||
const [editSchedule, setEditSchedule] = useState(-1);
|
const [editSchedule, setEditSchedule] = useState(-1);
|
||||||
|
const [dateOverrides, setDateOverrides] = useState([]);
|
||||||
|
const [openingHours, setOpeningHours] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.onChange(schedules);
|
setOpeningHours(
|
||||||
}, [schedules]);
|
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 addNewSchedule = () => setEditSchedule(openingHours.length);
|
||||||
const replaceWith = {
|
|
||||||
...schedules[editSchedule],
|
|
||||||
...changed,
|
|
||||||
};
|
|
||||||
|
|
||||||
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) => {
|
const removeScheduleAt = (toRemove: number) => {
|
||||||
schedules.splice(toRemove, 1);
|
openingHours.splice(toRemove, 1);
|
||||||
setSchedules([].concat(schedules));
|
setOpeningHours([].concat(openingHours));
|
||||||
};
|
};
|
||||||
|
|
||||||
const setWeekdays = (idx: number, days: number[]) => {
|
const OpeningHours = ({ idx, item }) => (
|
||||||
schedules[idx].days = days;
|
<li className="py-2 flex justify-between border-t">
|
||||||
setSchedules([].concat(schedules));
|
<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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -65,32 +96,15 @@ export const Scheduler = (props) => {
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
value={timeZone}
|
value={selectedTimeZone}
|
||||||
onChange={setTimeZone}
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{schedules.map((schedule, idx) => (
|
{openingHours.map((item, idx) => (
|
||||||
<li key={idx} className="py-2 flex justify-between border-t">
|
<OpeningHours key={idx} idx={idx} item={item} />
|
||||||
<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>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -108,7 +122,7 @@ export const Scheduler = (props) => {
|
||||||
</div>
|
</div>
|
||||||
{editSchedule >= 0 && (
|
{editSchedule >= 0 && (
|
||||||
<SetTimesModal
|
<SetTimesModal
|
||||||
schedule={schedules[editSchedule]}
|
schedule={{ ...openingHours[editSchedule], timeZone: selectedTimeZone }}
|
||||||
onChange={applyEditSchedule}
|
onChange={applyEditSchedule}
|
||||||
onExit={() => setEditSchedule(-1)}
|
onExit={() => setEditSchedule(-1)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,9 +7,15 @@ dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
export default function SetTimesModal(props) {
|
export default function SetTimesModal(props) {
|
||||||
const { startDate, endDate } = props.schedule || {
|
const { startDate, endDate } = {
|
||||||
startDate: dayjs.utc().startOf("day").add(540, "minutes"),
|
startDate: dayjs
|
||||||
endDate: dayjs.utc().startOf("day").add(1020, "minutes"),
|
.utc()
|
||||||
|
.startOf("day")
|
||||||
|
.add(props.schedule.startTime || 540, "minutes"),
|
||||||
|
endDate: dayjs
|
||||||
|
.utc()
|
||||||
|
.startOf("day")
|
||||||
|
.add(props.schedule.endTime || 1020, "minutes"),
|
||||||
};
|
};
|
||||||
|
|
||||||
startDate.tz(props.timeZone);
|
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 utc from "dayjs/plugin/utc";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
interface GetSlotsType {
|
interface GetSlotsType {
|
||||||
inviteeDate: Dayjs;
|
inviteeDate: Dayjs;
|
||||||
frequency: number;
|
frequency: number;
|
||||||
workingHours: [];
|
workingHours: [];
|
||||||
minimumBookingNotice?: number;
|
minimumBookingNotice?: number;
|
||||||
|
organizerUtcOffset: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Boundary {
|
interface Boundary {
|
||||||
|
@ -17,32 +20,41 @@ interface Boundary {
|
||||||
const freqApply: number = (cb, value: number, frequency: number): number => cb(value / frequency) * frequency;
|
const freqApply: number = (cb, value: number, frequency: number): number => cb(value / frequency) * frequency;
|
||||||
|
|
||||||
const intersectBoundary = (a: Boundary, b: Boundary) => {
|
const intersectBoundary = (a: Boundary, b: Boundary) => {
|
||||||
if (
|
if (a.upperBound < b.lowerBound || a.lowerBound > b.upperBound) {
|
||||||
a.upperBound < b.lowerBound || a.lowerBound > b.upperBound
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
lowerBound: Math.max(b.lowerBound, a.lowerBound),
|
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
|
// say invitee is -60,1380, and boundary is -120,240 - the overlap is -60,240
|
||||||
const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) => boundaries
|
const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) =>
|
||||||
.map(
|
boundaries.map((boundary) => intersectBoundary(inviteeBoundary, boundary)).filter(Boolean);
|
||||||
(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 boundaries: Boundary[] = [];
|
||||||
|
|
||||||
const startDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.lowerBound, 'minutes').format('d');
|
const startDay: number = +inviteeDate
|
||||||
const endDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.upperBound, 'minutes').format('d');
|
.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) => {
|
workingHours.forEach((item) => {
|
||||||
const lowerBound: number = item.startTime;
|
const lowerBound: number = item.startTime - dayjs().tz(organizerTimeZone).utcOffset();
|
||||||
const upperBound: number = lowerBound + item.length;
|
const upperBound: number = item.endTime - dayjs().tz(organizerTimeZone).utcOffset();
|
||||||
if (startDay !== endDay) {
|
if (startDay !== endDay) {
|
||||||
if (inviteeBounds.lowerBound < 0) {
|
if (inviteeBounds.lowerBound < 0) {
|
||||||
// lowerBound edges into the previous day
|
// lowerBound edges into the previous day
|
||||||
|
@ -62,7 +74,7 @@ const organizerBoundaries = (workingHours: [], inviteeDate: Dayjs, inviteeBounds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
boundaries.push({lowerBound, upperBound});
|
boundaries.push({ lowerBound, upperBound });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return boundaries;
|
return boundaries;
|
||||||
|
@ -72,38 +84,42 @@ const inviteeBoundary = (startTime: number, utcOffset: number, frequency: number
|
||||||
const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency);
|
const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency);
|
||||||
const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency);
|
const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency);
|
||||||
return {
|
return {
|
||||||
lowerBound, upperBound,
|
lowerBound,
|
||||||
|
upperBound,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSlotsBetweenBoundary = (frequency: number, {lowerBound,upperBound}: Boundary) => {
|
const getSlotsBetweenBoundary = (frequency: number, { lowerBound, upperBound }: Boundary) => {
|
||||||
const slots: Dayjs[] = [];
|
const slots: Dayjs[] = [];
|
||||||
for (
|
for (let minutes = 0; lowerBound + minutes <= upperBound - frequency; minutes += frequency) {
|
||||||
let minutes = 0;
|
slots.push(
|
||||||
lowerBound + minutes <= upperBound - frequency;
|
<Dayjs>dayjs
|
||||||
minutes += frequency
|
.utc()
|
||||||
) {
|
.startOf("day")
|
||||||
slots.push(<Dayjs>dayjs.utc().startOf('day').add(lowerBound + minutes, 'minutes'));
|
.add(lowerBound + minutes, "minutes")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return slots;
|
return slots;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSlots = (
|
const getSlots = ({
|
||||||
{ inviteeDate, frequency, minimumBookingNotice, workingHours, }: GetSlotsType
|
inviteeDate,
|
||||||
): Dayjs[] => {
|
frequency,
|
||||||
|
minimumBookingNotice,
|
||||||
const startTime = (
|
workingHours,
|
||||||
dayjs.utc().isSame(dayjs(inviteeDate), 'day') ? inviteeDate.hour() * 60 + inviteeDate.minute() + minimumBookingNotice : 0
|
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);
|
const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency);
|
||||||
return getOverlaps(
|
return getOverlaps(
|
||||||
inviteeBounds, organizerBoundaries(workingHours, inviteeDate, inviteeBounds)
|
inviteeBounds,
|
||||||
).reduce(
|
organizerBoundaries(workingHours, inviteeDate, inviteeBounds, organizerTimeZone)
|
||||||
(slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary) ], []
|
|
||||||
).map(
|
|
||||||
(slot) => slot.utcOffset(dayjs(inviteeDate).utcOffset())
|
|
||||||
)
|
)
|
||||||
}
|
.reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], [])
|
||||||
|
.map((slot) => slot.utcOffset(dayjs(inviteeDate).utcOffset()));
|
||||||
|
};
|
||||||
|
|
||||||
export default getSlots;
|
export default getSlots;
|
||||||
|
|
|
@ -32,6 +32,7 @@ export default function Type(props): Type {
|
||||||
frequency: props.eventType.length,
|
frequency: props.eventType.length,
|
||||||
inviteeDate: dayjs.utc(today) as Dayjs,
|
inviteeDate: dayjs.utc(today) as Dayjs,
|
||||||
workingHours: props.workingHours,
|
workingHours: props.workingHours,
|
||||||
|
organizerTimeZone: props.eventType.timeZone,
|
||||||
minimumBookingNotice: 0,
|
minimumBookingNotice: 0,
|
||||||
}).length === 0,
|
}).length === 0,
|
||||||
[today, props.eventType.length, props.workingHours]
|
[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} |
|
{rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username} |
|
||||||
Calendso
|
Calendso
|
||||||
</title>
|
</title>
|
||||||
<meta name="title" content={"Meet " + (props.user.name || props.user.username) + " via Calendso"} />
|
<meta name="title" content={"Meet " + (props.user.name || props.user.username) + " via Calendso"} />
|
||||||
<meta name="description" content={props.eventType.description} />
|
<meta name="description" content={props.eventType.description} />
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://calendso/" />
|
<meta property="og:url" content="https://calendso/" />
|
||||||
<meta property="og:title" content={"Meet " + (props.user.name || props.user.username) + " via Calendso"}/>
|
<meta
|
||||||
<meta property="og:description" content={props.eventType.description}/>
|
property="og:title"
|
||||||
<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)} />
|
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:card" content="summary_large_image" />
|
||||||
<meta property="twitter:url" content="https://calendso/" />
|
<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: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>
|
</Head>
|
||||||
<main
|
<main
|
||||||
className={
|
className={
|
||||||
|
@ -184,6 +210,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
description: true,
|
description: true,
|
||||||
length: true,
|
length: true,
|
||||||
availability: true,
|
availability: true,
|
||||||
|
timeZone: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,81 +1,111 @@
|
||||||
import type {NextApiRequest, NextApiResponse} from 'next';
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import {getSession} from 'next-auth/client';
|
import { getSession } from "next-auth/client";
|
||||||
import prisma from '../../../lib/prisma';
|
import prisma from "../../../lib/prisma";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = await getSession({req: req});
|
const session = await getSession({ req: req });
|
||||||
if (!session) {
|
if (!session) {
|
||||||
res.status(401).json({message: "Not authenticated"});
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method == "PATCH" || req.method == "POST") {
|
if (req.method == "PATCH" || req.method == "POST") {
|
||||||
|
const data = {
|
||||||
const data = {
|
title: req.body.title,
|
||||||
title: req.body.title,
|
slug: req.body.slug,
|
||||||
slug: req.body.slug,
|
description: req.body.description,
|
||||||
description: req.body.description,
|
length: parseInt(req.body.length),
|
||||||
length: parseInt(req.body.length),
|
hidden: req.body.hidden,
|
||||||
hidden: req.body.hidden,
|
locations: req.body.locations,
|
||||||
locations: req.body.locations,
|
eventName: req.body.eventName,
|
||||||
eventName: req.body.eventName,
|
customInputs: !req.body.customInputs
|
||||||
customInputs: !req.body.customInputs
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: {
|
deleteMany: {
|
||||||
deleteMany: {
|
eventTypeId: req.body.id,
|
||||||
eventTypeId: req.body.id,
|
NOT: {
|
||||||
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 => ({
|
|
||||||
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 == "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" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Select, { OptionBase } from "react-select";
|
import Select, { OptionBase } from "react-select";
|
||||||
import prisma from "../../../lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { LocationType } from "../../../lib/location";
|
import { LocationType } from "@lib/location";
|
||||||
import Shell from "../../../components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import { getSession } from "next-auth/client";
|
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 { 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 { PlusIcon } from "@heroicons/react/solid";
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import { EventType, User, Availability } from "@prisma/client";
|
||||||
|
import { validJson } from "@lib/jsonUtils";
|
||||||
dayjs.extend(timezone);
|
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 router = useRouter();
|
||||||
|
|
||||||
const inputOptions: OptionBase[] = [
|
const inputOptions: OptionBase[] = [
|
||||||
|
@ -30,17 +70,17 @@ export default function EventType(props: any): JSX.Element {
|
||||||
{ value: EventTypeCustomInputType.Bool, label: "Checkbox" },
|
{ value: EventTypeCustomInputType.Bool, label: "Checkbox" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [enteredAvailability, setEnteredAvailability] = useState();
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
|
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
|
||||||
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionBase | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<OptionBase | undefined>(undefined);
|
||||||
const [selectedInputOption, setSelectedInputOption] = useState<OptionBase>(inputOptions[0]);
|
const [selectedInputOption, setSelectedInputOption] = useState<OptionBase>(inputOptions[0]);
|
||||||
const [locations, setLocations] = useState(props.eventType.locations || []);
|
const [locations, setLocations] = useState(eventType.locations || []);
|
||||||
const [schedule, setSchedule] = useState(undefined);
|
|
||||||
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
||||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
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 titleRef = useRef<HTMLInputElement>();
|
||||||
const slugRef = useRef<HTMLInputElement>();
|
const slugRef = useRef<HTMLInputElement>();
|
||||||
|
@ -49,60 +89,55 @@ export default function EventType(props: any): JSX.Element {
|
||||||
const isHiddenRef = useRef<HTMLInputElement>();
|
const isHiddenRef = useRef<HTMLInputElement>();
|
||||||
const eventNameRef = useRef<HTMLInputElement>();
|
const eventNameRef = useRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedTimeZone(eventType.timeZone || user.timeZone);
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function updateEventTypeHandler(event) {
|
async function updateEventTypeHandler(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const enteredTitle = titleRef.current.value;
|
const enteredTitle: string = titleRef.current.value;
|
||||||
const enteredSlug = slugRef.current.value;
|
const enteredSlug: string = slugRef.current.value;
|
||||||
const enteredDescription = descriptionRef.current.value;
|
const enteredDescription: string = descriptionRef.current.value;
|
||||||
const enteredLength = lengthRef.current.value;
|
const enteredLength: number = parseInt(lengthRef.current.value);
|
||||||
const enteredIsHidden = isHiddenRef.current.checked;
|
const enteredIsHidden: boolean = isHiddenRef.current.checked;
|
||||||
const enteredEventName = eventNameRef.current.value;
|
const enteredEventName: string = eventNameRef.current.value;
|
||||||
// TODO: Add validation
|
// 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", {
|
await fetch("/api/availability/eventtype", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload),
|
||||||
id: props.eventType.id,
|
|
||||||
title: enteredTitle,
|
|
||||||
slug: enteredSlug,
|
|
||||||
description: enteredDescription,
|
|
||||||
length: enteredLength,
|
|
||||||
hidden: enteredIsHidden,
|
|
||||||
locations,
|
|
||||||
eventName: enteredEventName,
|
|
||||||
customInputs,
|
|
||||||
}),
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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");
|
router.push("/availability");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +146,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
|
|
||||||
await fetch("/api/availability/eventtype", {
|
await fetch("/api/availability/eventtype", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify({ id: props.eventType.id }),
|
body: JSON.stringify({ id: eventType.id }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
@ -237,10 +272,10 @@ export default function EventType(props: any): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.eventType.title} | Event Type | Calendso</title>
|
<title>{eventType.title} | Event Type | Calendso</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Shell heading={"Event Type - " + props.eventType.title}>
|
<Shell heading={"Event Type - " + eventType.title}>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="col-span-3 sm:col-span-2">
|
<div className="col-span-3 sm:col-span-2">
|
||||||
<div className="bg-white overflow-hidden shadow rounded-lg mb-4">
|
<div className="bg-white overflow-hidden shadow rounded-lg mb-4">
|
||||||
|
@ -259,7 +294,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
required
|
required
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
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"
|
placeholder="Quick Chat"
|
||||||
defaultValue={props.eventType.title}
|
defaultValue={eventType.title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -270,7 +305,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<div className="flex rounded-md shadow-sm">
|
<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">
|
<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>
|
</span>
|
||||||
<input
|
<input
|
||||||
ref={slugRef}
|
ref={slugRef}
|
||||||
|
@ -279,7 +314,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
id="slug"
|
id="slug"
|
||||||
required
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -420,7 +455,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
id="description"
|
id="description"
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
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."
|
placeholder="A quick video meeting."
|
||||||
defaultValue={props.eventType.description}></textarea>
|
defaultValue={eventType.description}></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
@ -436,7 +471,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
required
|
required
|
||||||
className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md"
|
className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md"
|
||||||
placeholder="15"
|
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">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
|
||||||
minutes
|
minutes
|
||||||
|
@ -455,7 +490,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
id="title"
|
id="title"
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
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}"
|
placeholder="Meeting with {USER}"
|
||||||
defaultValue={props.eventType.eventName}
|
defaultValue={eventType.eventName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -514,7 +549,7 @@ export default function EventType(props: any): JSX.Element {
|
||||||
name="ishidden"
|
name="ishidden"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
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>
|
||||||
<div className="ml-3 text-sm">
|
<div className="ml-3 text-sm">
|
||||||
|
@ -531,9 +566,10 @@ export default function EventType(props: any): JSX.Element {
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2">How do you want to offer your availability for this event type?</h3>
|
<h3 className="mb-2">How do you want to offer your availability for this event type?</h3>
|
||||||
<Scheduler
|
<Scheduler
|
||||||
onChange={setSchedule}
|
setAvailability={setEnteredAvailability}
|
||||||
timeZone={props.user.timeZone}
|
setTimeZone={setSelectedTimeZone}
|
||||||
schedules={props.schedules}
|
timeZone={selectedTimeZone}
|
||||||
|
availability={availability}
|
||||||
/>
|
/>
|
||||||
<div className="py-4 flex justify-end">
|
<div className="py-4 flex justify-end">
|
||||||
<Link href="/availability">
|
<Link href="/availability">
|
||||||
|
@ -709,24 +745,18 @@ export default function EventType(props: any): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validJson = (jsonString: string) => {
|
export const getServerSideProps: GetServerSideProps<Props> = async ({ req, query }) => {
|
||||||
try {
|
const session = await getSession({ req });
|
||||||
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);
|
|
||||||
if (!session) {
|
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: {
|
where: {
|
||||||
email: session.user.email,
|
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: {
|
where: {
|
||||||
id: parseInt(context.query.type),
|
id: parseInt(query.type as string),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -754,9 +784,16 @@ export async function getServerSideProps(context) {
|
||||||
eventName: true,
|
eventName: true,
|
||||||
availability: true,
|
availability: true,
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
|
timeZone: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = await prisma.credential.findMany({
|
const credentials = await prisma.credential.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -808,18 +845,12 @@ export async function getServerSideProps(context) {
|
||||||
// Assuming it's Microsoft Teams.
|
// Assuming it's Microsoft Teams.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventType) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAvailability = (providesAvailability) =>
|
const getAvailability = (providesAvailability) =>
|
||||||
providesAvailability.availability && providesAvailability.availability.length
|
providesAvailability.availability && providesAvailability.availability.length
|
||||||
? providesAvailability.availability
|
? providesAvailability.availability
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const schedules = getAvailability(eventType) ||
|
const availability: Availability[] = getAvailability(eventType) ||
|
||||||
getAvailability(user) || [
|
getAvailability(user) || [
|
||||||
{
|
{
|
||||||
days: [0, 1, 2, 3, 4, 5, 6],
|
days: [0, 1, 2, 3, 4, 5, 6],
|
||||||
|
@ -832,8 +863,8 @@ export async function getServerSideProps(context) {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
eventType,
|
eventType,
|
||||||
schedules,
|
|
||||||
locationOptions,
|
locationOptions,
|
||||||
|
availability,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@ model EventType {
|
||||||
availability Availability[]
|
availability Availability[]
|
||||||
eventName String?
|
eventName String?
|
||||||
customInputs EventTypeCustomInput[]
|
customInputs EventTypeCustomInput[]
|
||||||
|
timeZone String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Credential {
|
model Credential {
|
||||||
|
|
Loading…
Reference in a new issue