Merge branch 'main' into bugfix/unify-email-sending
This commit is contained in:
commit
b4ed8ae87e
51 changed files with 2713 additions and 1423 deletions
17
components/Theme.tsx
Normal file
17
components/Theme.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function Theme(theme?: string) {
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add(theme);
|
||||||
|
}
|
||||||
|
setIsReady(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isReady,
|
||||||
|
};
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ const AvailableTimes = ({
|
||||||
))}
|
))}
|
||||||
{isFullyBooked && (
|
{isFullyBooked && (
|
||||||
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
|
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
|
||||||
<h1 className="text-xl font">{user.name} is all booked today.</h1>
|
<h1 className="text-xl text-black dark:text-white">{user.name} is all booked today.</h1>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@ import dayjs, { 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 getSlots from "@lib/slots";
|
import getSlots from "@lib/slots";
|
||||||
|
import dayjsBusinessDays from "dayjs-business-days";
|
||||||
|
|
||||||
|
dayjs.extend(dayjsBusinessDays);
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
@ -15,13 +17,29 @@ const DatePicker = ({
|
||||||
organizerTimeZone,
|
organizerTimeZone,
|
||||||
inviteeTimeZone,
|
inviteeTimeZone,
|
||||||
eventLength,
|
eventLength,
|
||||||
|
date,
|
||||||
|
periodType = "unlimited",
|
||||||
|
periodStartDate,
|
||||||
|
periodEndDate,
|
||||||
|
periodDays,
|
||||||
|
periodCountCalendarDays,
|
||||||
}) => {
|
}) => {
|
||||||
const [calendar, setCalendar] = useState([]);
|
const [calendar, setCalendar] = useState([]);
|
||||||
const [selectedMonth, setSelectedMonth]: number = useState();
|
const [selectedMonth, setSelectedMonth] = useState<number>();
|
||||||
const [selectedDate, setSelectedDate]: Dayjs = useState();
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedMonth(dayjs().tz(inviteeTimeZone).month());
|
if (date) {
|
||||||
|
setSelectedDate(dayjs(date).tz(inviteeTimeZone));
|
||||||
|
setSelectedMonth(dayjs(date).tz(inviteeTimeZone).month());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (periodType === "range") {
|
||||||
|
setSelectedMonth(dayjs(periodStartDate).tz(inviteeTimeZone).month());
|
||||||
|
} else {
|
||||||
|
setSelectedMonth(dayjs().tz(inviteeTimeZone).month());
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -47,15 +65,52 @@ const DatePicker = ({
|
||||||
|
|
||||||
const isDisabled = (day: number) => {
|
const isDisabled = (day: number) => {
|
||||||
const date: Dayjs = inviteeDate.date(day);
|
const date: Dayjs = inviteeDate.date(day);
|
||||||
return (
|
|
||||||
date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
|
switch (periodType) {
|
||||||
!getSlots({
|
case "rolling": {
|
||||||
inviteeDate: date,
|
const periodRollingEndDay = periodCountCalendarDays
|
||||||
frequency: eventLength,
|
? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day")
|
||||||
workingHours,
|
: dayjs().tz(organizerTimeZone).businessDaysAdd(periodDays, "days").endOf("day");
|
||||||
organizerTimeZone,
|
return (
|
||||||
}).length
|
date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
|
||||||
);
|
date.endOf("day").isAfter(periodRollingEndDay) ||
|
||||||
|
!getSlots({
|
||||||
|
inviteeDate: date,
|
||||||
|
frequency: eventLength,
|
||||||
|
workingHours,
|
||||||
|
organizerTimeZone,
|
||||||
|
}).length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "range": {
|
||||||
|
const periodRangeStartDay = dayjs(periodStartDate).tz(organizerTimeZone).endOf("day");
|
||||||
|
const periodRangeEndDay = dayjs(periodEndDate).tz(organizerTimeZone).endOf("day");
|
||||||
|
return (
|
||||||
|
date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
|
||||||
|
date.endOf("day").isBefore(periodRangeStartDay) ||
|
||||||
|
date.endOf("day").isAfter(periodRangeEndDay) ||
|
||||||
|
!getSlots({
|
||||||
|
inviteeDate: date,
|
||||||
|
frequency: eventLength,
|
||||||
|
workingHours,
|
||||||
|
organizerTimeZone,
|
||||||
|
}).length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "unlimited":
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) ||
|
||||||
|
!getSlots({
|
||||||
|
inviteeDate: date,
|
||||||
|
frequency: eventLength,
|
||||||
|
workingHours,
|
||||||
|
organizerTimeZone,
|
||||||
|
}).length
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up calendar
|
// Set up calendar
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const Scheduler = ({
|
||||||
|
|
||||||
const OpeningHours = ({ idx, item }) => (
|
const OpeningHours = ({ idx, item }) => (
|
||||||
<li className="py-2 flex justify-between border-t">
|
<li className="py-2 flex justify-between border-t">
|
||||||
<div className="inline-flex ml-2">
|
<div className="flex flex-col space-y-4 lg:inline-flex ml-2 ">
|
||||||
<WeekdaySelect defaultValue={item.days} onSelect={(selected: number[]) => (item.days = selected)} />
|
<WeekdaySelect defaultValue={item.days} onSelect={(selected: number[]) => (item.days = selected)} />
|
||||||
<button className="ml-2 text-sm px-2" type="button" onClick={() => setEditSchedule(idx)}>
|
<button className="ml-2 text-sm px-2" type="button" onClick={() => setEditSchedule(idx)}>
|
||||||
{dayjs()
|
{dayjs()
|
||||||
|
@ -96,8 +96,8 @@ export const Scheduler = ({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded border flex">
|
<div className="rounded border flex">
|
||||||
<div className="w-3/5">
|
<div className="w-full">
|
||||||
<div className="w-3/4 p-2">
|
<div className=" p-2">
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||||
Timezone
|
Timezone
|
||||||
</label>
|
</label>
|
||||||
|
@ -120,13 +120,6 @@ export const Scheduler = ({
|
||||||
Add another
|
Add another
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-l p-2 w-2/5 text-sm bg-gray-50">
|
|
||||||
{/*<p className="font-bold mb-2">Add date overrides</p>
|
|
||||||
<p className="mb-2">
|
|
||||||
Add dates when your availability changes from your weekly hours
|
|
||||||
</p>
|
|
||||||
<button className="btn-sm btn-white">Add a date override</button>*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{editSchedule >= 0 && (
|
{editSchedule >= 0 && (
|
||||||
<SetTimesModal
|
<SetTimesModal
|
||||||
|
@ -136,9 +129,6 @@ export const Scheduler = ({
|
||||||
onExit={() => setEditSchedule(-1)}
|
onExit={() => setEditSchedule(-1)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/*{showDateOverrideModal &&
|
|
||||||
<DateOverrideModal />
|
|
||||||
}*/}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
12
components/ui/Text/Body/Body.tsx
Normal file
12
components/ui/Text/Body/Body.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Body: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--body"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Body;
|
2
components/ui/Text/Body/index.ts
Normal file
2
components/ui/Text/Body/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Body from "./Body";
|
||||||
|
export default Body;
|
12
components/ui/Text/Caption/Caption.tsx
Normal file
12
components/ui/Text/Caption/Caption.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Caption: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--caption"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Caption;
|
2
components/ui/Text/Caption/index.ts
Normal file
2
components/ui/Text/Caption/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Caption from "./Caption";
|
||||||
|
export default Caption;
|
12
components/ui/Text/Caption2/Caption2.tsx
Normal file
12
components/ui/Text/Caption2/Caption2.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Caption2: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--caption2"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Caption2;
|
2
components/ui/Text/Caption2/index.ts
Normal file
2
components/ui/Text/Caption2/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Caption2 from "./Caption2";
|
||||||
|
export default Caption2;
|
11
components/ui/Text/Footnote/Footnote.tsx
Normal file
11
components/ui/Text/Footnote/Footnote.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
|
||||||
|
const Footnote: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames("text--footnote", props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footnote;
|
2
components/ui/Text/Footnote/index.ts
Normal file
2
components/ui/Text/Footnote/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Footnote from "./Footnote";
|
||||||
|
export default Footnote;
|
12
components/ui/Text/Headline/Headline.tsx
Normal file
12
components/ui/Text/Headline/Headline.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Headline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--headline"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <h1 className={classes}>{props.children}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Headline;
|
2
components/ui/Text/Headline/index.ts
Normal file
2
components/ui/Text/Headline/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Headline from "./Headline";
|
||||||
|
export default Headline;
|
12
components/ui/Text/Largetitle/Largetitle.tsx
Normal file
12
components/ui/Text/Largetitle/Largetitle.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Largetitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--largetitle"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Largetitle;
|
2
components/ui/Text/Largetitle/index.ts
Normal file
2
components/ui/Text/Largetitle/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Largetitle from "./Largetitle";
|
||||||
|
export default Largetitle;
|
12
components/ui/Text/Overline/Overline.tsx
Normal file
12
components/ui/Text/Overline/Overline.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Overline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--overline"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Overline;
|
2
components/ui/Text/Overline/index.ts
Normal file
2
components/ui/Text/Overline/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Overline from "./Overline";
|
||||||
|
export default Overline;
|
12
components/ui/Text/Subheadline/Subheadline.tsx
Normal file
12
components/ui/Text/Subheadline/Subheadline.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Subheadline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--subheadline"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Subheadline;
|
2
components/ui/Text/Subheadline/index.ts
Normal file
2
components/ui/Text/Subheadline/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Subheadline from "./Subheadline";
|
||||||
|
export default Subheadline;
|
12
components/ui/Text/Subtitle/Subtitle.tsx
Normal file
12
components/ui/Text/Subtitle/Subtitle.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Subtitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--subtitle"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Subtitle;
|
2
components/ui/Text/Subtitle/index.ts
Normal file
2
components/ui/Text/Subtitle/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Subtitle from "./Subtitle";
|
||||||
|
export default Subtitle;
|
55
components/ui/Text/Text.module.css
Normal file
55
components/ui/Text/Text.module.css
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/* strong {
|
||||||
|
@apply font-medium;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.text {
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--body {
|
||||||
|
@apply text-lg leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--overline {
|
||||||
|
@apply text-sm uppercase font-semibold leading-snug tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--caption {
|
||||||
|
@apply text-sm text-gray-500 leading-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--caption2 {
|
||||||
|
@apply text-xs italic text-gray-500 leading-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--footnote {
|
||||||
|
@apply text-base font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--headline {
|
||||||
|
/* @apply text-base font-normal; */
|
||||||
|
@apply text-3xl leading-8 font-semibold tracking-tight text-gray-900 sm:text-4xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--subheadline {
|
||||||
|
@apply text-xl text-gray-500 leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--largetitle {
|
||||||
|
@apply text-2xl font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--subtitle {
|
||||||
|
@apply text-base font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--title {
|
||||||
|
@apply text-base font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--title2 {
|
||||||
|
@apply text-base font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text--title3 {
|
||||||
|
@apply text-xs font-semibold leading-tight;
|
||||||
|
}
|
166
components/ui/Text/Text.tsx
Normal file
166
components/ui/Text/Text.tsx
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import React from "react";
|
||||||
|
import Body from "./Body";
|
||||||
|
import Caption from "./Caption";
|
||||||
|
import Caption2 from "./Caption2";
|
||||||
|
import Footnote from "./Footnote";
|
||||||
|
import Headline from "./Headline";
|
||||||
|
import Largetitle from "./Largetitle";
|
||||||
|
import Overline from "./Overline";
|
||||||
|
import Subheadline from "./Subheadline";
|
||||||
|
import Subtitle from "./Subtitle";
|
||||||
|
import Title from "./Title";
|
||||||
|
import Title2 from "./Title2";
|
||||||
|
import Title3 from "./Title3";
|
||||||
|
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
variant?:
|
||||||
|
| "overline"
|
||||||
|
| "caption"
|
||||||
|
| "body"
|
||||||
|
| "caption2"
|
||||||
|
| "footnote"
|
||||||
|
| "headline"
|
||||||
|
| "largetitle"
|
||||||
|
| "subheadline"
|
||||||
|
| "subtitle"
|
||||||
|
| "title"
|
||||||
|
| "title2"
|
||||||
|
| "title3";
|
||||||
|
children: any;
|
||||||
|
text?: string;
|
||||||
|
tx?: string;
|
||||||
|
className?: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextProps = {
|
||||||
|
children: any;
|
||||||
|
text?: string;
|
||||||
|
tx?: string;
|
||||||
|
color?: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* static let largeTitle: Font
|
||||||
|
* A font with the large title text style.
|
||||||
|
*
|
||||||
|
* static let title: Font
|
||||||
|
* A font with the title text style.
|
||||||
|
*
|
||||||
|
* static let title2: Font
|
||||||
|
* Create a font for second level hierarchical headings.
|
||||||
|
*
|
||||||
|
* static let title3: Font
|
||||||
|
* Create a font for third level hierarchical headings.
|
||||||
|
*
|
||||||
|
* static let headline: Font
|
||||||
|
* A font with the headline text style.
|
||||||
|
*
|
||||||
|
* static let subheadline: Font
|
||||||
|
* A font with the subheadline text style.
|
||||||
|
*
|
||||||
|
* static let body: Font
|
||||||
|
* A font with the body text style.
|
||||||
|
*
|
||||||
|
* static let callout: Font
|
||||||
|
* A font with the callout text style.
|
||||||
|
*
|
||||||
|
* static let caption: Font
|
||||||
|
* A font with the caption text style.
|
||||||
|
*
|
||||||
|
* static let caption2: Font
|
||||||
|
* Create a font with the alternate caption text style.
|
||||||
|
*
|
||||||
|
* static let footnote: Font
|
||||||
|
* A font with the footnote text style.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Text: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
|
const classes = classnames(props?.className, props?.color);
|
||||||
|
|
||||||
|
switch (props?.variant) {
|
||||||
|
case "overline":
|
||||||
|
return (
|
||||||
|
<Overline className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Overline>
|
||||||
|
);
|
||||||
|
case "body":
|
||||||
|
return (
|
||||||
|
<Body className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Body>
|
||||||
|
);
|
||||||
|
case "caption":
|
||||||
|
return (
|
||||||
|
<Caption className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Caption>
|
||||||
|
);
|
||||||
|
case "caption2":
|
||||||
|
return (
|
||||||
|
<Caption2 className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Caption2>
|
||||||
|
);
|
||||||
|
case "footnote":
|
||||||
|
return (
|
||||||
|
<Footnote className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Footnote>
|
||||||
|
);
|
||||||
|
case "headline":
|
||||||
|
return (
|
||||||
|
<Headline className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
|
case "largetitle":
|
||||||
|
return (
|
||||||
|
<Largetitle className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Largetitle>
|
||||||
|
);
|
||||||
|
case "subheadline":
|
||||||
|
return (
|
||||||
|
<Subheadline className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Subheadline>
|
||||||
|
);
|
||||||
|
case "subtitle":
|
||||||
|
return (
|
||||||
|
<Subtitle className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Subtitle>
|
||||||
|
);
|
||||||
|
case "title":
|
||||||
|
return (
|
||||||
|
<Title className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Title>
|
||||||
|
);
|
||||||
|
case "title2":
|
||||||
|
return (
|
||||||
|
<Title2 className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Title2>
|
||||||
|
);
|
||||||
|
case "title3":
|
||||||
|
return (
|
||||||
|
<Title3 className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Title3>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Body className={classes} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Body>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Text;
|
12
components/ui/Text/Title/Title.tsx
Normal file
12
components/ui/Text/Title/Title.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Title: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--title"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Title;
|
2
components/ui/Text/Title/index.ts
Normal file
2
components/ui/Text/Title/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Title from "./Title";
|
||||||
|
export default Title;
|
12
components/ui/Text/Title2/Title2.tsx
Normal file
12
components/ui/Text/Title2/Title2.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Title2: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--title2"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Title2;
|
2
components/ui/Text/Title2/index.ts
Normal file
2
components/ui/Text/Title2/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Title2 from "./Title2";
|
||||||
|
export default Title2;
|
12
components/ui/Text/Title3/Title3.tsx
Normal file
12
components/ui/Text/Title3/Title3.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { TextProps } from "../Text";
|
||||||
|
import Styles from "../Text.module.css";
|
||||||
|
|
||||||
|
const Title3: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||||
|
const classes = classnames(Styles["text--title3"], props?.className, props?.color);
|
||||||
|
|
||||||
|
return <p className={classes}>{props.children}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Title3;
|
2
components/ui/Text/Title3/index.ts
Normal file
2
components/ui/Text/Title3/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Title3 from "./Title3";
|
||||||
|
export default Title3;
|
39
components/ui/Text/index.ts
Normal file
39
components/ui/Text/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import Text from "./Text";
|
||||||
|
export { Text };
|
||||||
|
export default Text;
|
||||||
|
|
||||||
|
import Title from "./Title";
|
||||||
|
export { Title };
|
||||||
|
|
||||||
|
import Title2 from "./Title2";
|
||||||
|
export { Title2 };
|
||||||
|
|
||||||
|
import Title3 from "./Title3";
|
||||||
|
export { Title3 };
|
||||||
|
|
||||||
|
import Largetitle from "./Largetitle";
|
||||||
|
export { Largetitle };
|
||||||
|
|
||||||
|
import Subtitle from "./Subtitle";
|
||||||
|
export { Subtitle };
|
||||||
|
|
||||||
|
import Headline from "./Headline";
|
||||||
|
export { Headline };
|
||||||
|
|
||||||
|
import Subheadline from "./Subheadline";
|
||||||
|
export { Subheadline };
|
||||||
|
|
||||||
|
import Caption from "./Caption";
|
||||||
|
export { Caption };
|
||||||
|
|
||||||
|
import Caption2 from "./Caption2";
|
||||||
|
export { Caption2 };
|
||||||
|
|
||||||
|
import Footnote from "./Footnote";
|
||||||
|
export { Footnote };
|
||||||
|
|
||||||
|
import Overline from "./Overline";
|
||||||
|
export { Overline };
|
||||||
|
|
||||||
|
import Body from "./Body";
|
||||||
|
export { Body };
|
|
@ -157,29 +157,7 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => {
|
||||||
optional.location = { displayName: event.location };
|
optional.location = { displayName: event.location };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return toRet;
|
||||||
subject: event.title,
|
|
||||||
body: {
|
|
||||||
contentType: "HTML",
|
|
||||||
content: event.description,
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
dateTime: event.startTime,
|
|
||||||
timeZone: event.organizer.timeZone,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
dateTime: event.endTime,
|
|
||||||
timeZone: event.organizer.timeZone,
|
|
||||||
},
|
|
||||||
attendees: event.attendees.map((attendee) => ({
|
|
||||||
emailAddress: {
|
|
||||||
address: attendee.email,
|
|
||||||
name: attendee.name,
|
|
||||||
},
|
|
||||||
type: "required",
|
|
||||||
})),
|
|
||||||
...optional,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const integrationType = "office365_calendar";
|
const integrationType = "office365_calendar";
|
||||||
|
|
|
@ -66,13 +66,13 @@ export default class EventOrganizerMail extends EventMail {
|
||||||
<a href="mailto:${this.calEvent.attendees[0].email}">${this.calEvent.attendees[0].email}</a><br />
|
<a href="mailto:${this.calEvent.attendees[0].email}">${this.calEvent.attendees[0].email}</a><br />
|
||||||
<br />` +
|
<br />` +
|
||||||
this.getAdditionalBody() +
|
this.getAdditionalBody() +
|
||||||
"<br />" +
|
|
||||||
`<strong>Invitee Time Zone:</strong><br />
|
`<strong>Invitee Time Zone:</strong><br />
|
||||||
${this.calEvent.attendees[0].timeZone}<br />
|
${this.calEvent.attendees[0].timeZone}<br />
|
||||||
<br />
|
<br />
|
||||||
<strong>Additional notes:</strong><br />
|
<strong>Additional notes:</strong><br />
|
||||||
${this.calEvent.description}
|
${this.calEvent.description}
|
||||||
` +
|
` +
|
||||||
|
"<br />" +
|
||||||
this.getAdditionalFooter() +
|
this.getAdditionalFooter() +
|
||||||
`
|
`
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -131,7 +131,7 @@ const getSlots = ({
|
||||||
)
|
)
|
||||||
.reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], [])
|
.reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], [])
|
||||||
.map((slot) =>
|
.map((slot) =>
|
||||||
slot.month(inviteeDate.month()).date(inviteeDate.date()).utcOffset(inviteeDate.utcOffset())
|
slot.utcOffset(inviteeDate.utcOffset()).month(inviteeDate.month()).date(inviteeDate.date())
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,17 +20,21 @@
|
||||||
"@tailwindcss/forms": "^0.2.1",
|
"@tailwindcss/forms": "^0.2.1",
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
|
"dayjs-business-days": "^1.0.4",
|
||||||
"googleapis": "^67.1.1",
|
"googleapis": "^67.1.1",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"ics": "^2.27.0",
|
"ics": "^2.27.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
"next": "^10.2.0",
|
"next": "^10.2.0",
|
||||||
"next-auth": "^3.13.2",
|
"next-auth": "^3.13.2",
|
||||||
"next-transpile-modules": "^7.0.0",
|
"next-transpile-modules": "^7.0.0",
|
||||||
"nodemailer": "^6.6.1",
|
"nodemailer": "^6.6.1",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
|
"react-dates": "^21.8.0",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-phone-number-input": "^3.1.21",
|
"react-phone-number-input": "^3.1.21",
|
||||||
"react-select": "^4.3.0",
|
"react-select": "^4.3.0",
|
||||||
|
@ -44,6 +48,7 @@
|
||||||
"@types/node": "^14.14.33",
|
"@types/node": "^14.14.33",
|
||||||
"@types/nodemailer": "^6.4.2",
|
"@types/nodemailer": "^6.4.2",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
|
"@types/react-dates": "^21.8.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||||
"@typescript-eslint/parser": "^4.27.0",
|
"@typescript-eslint/parser": "^4.27.0",
|
||||||
"autoprefixer": "^10.2.5",
|
"autoprefixer": "^10.2.5",
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { GetServerSideProps } from "next";
|
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 prisma from "../lib/prisma";
|
import prisma, { whereAndSelect } from "@lib/prisma";
|
||||||
import Avatar from "../components/Avatar";
|
import Avatar from "../components/Avatar";
|
||||||
|
import Theme from "@components/Theme";
|
||||||
|
|
||||||
export default function User(props): User {
|
export default function User(props): User {
|
||||||
|
const { isReady } = Theme(props.user.theme);
|
||||||
|
|
||||||
const eventTypes = props.eventTypes.map((type) => (
|
const eventTypes = props.eventTypes.map((type) => (
|
||||||
<li
|
<li
|
||||||
key={type.id}
|
key={type.id}
|
||||||
|
@ -21,50 +24,44 @@ export default function User(props): User {
|
||||||
</li>
|
</li>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
<div>
|
isReady && (
|
||||||
<Head>
|
<div>
|
||||||
<title>{props.user.name || props.user.username} | Calendso</title>
|
<Head>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<title>{props.user.name || props.user.username} | Calendso</title>
|
||||||
</Head>
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
<main className="max-w-2xl mx-auto my-24">
|
<main className="max-w-2xl mx-auto my-24">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<Avatar user={props.user} className="mx-auto w-24 h-24 rounded-full mb-4" />
|
<Avatar user={props.user} className="mx-auto w-24 h-24 rounded-full mb-4" />
|
||||||
<h1 className="text-3xl font-semibold text-gray-800 dark:text-white mb-1">
|
<h1 className="text-3xl font-semibold text-gray-800 dark:text-white mb-1">
|
||||||
{props.user.name || props.user.username}
|
{props.user.name || props.user.username}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-white">{props.user.bio}</p>
|
<p className="text-gray-600 dark:text-white">{props.user.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="shadow overflow-hidden rounded-md">
|
<div className="shadow overflow-hidden rounded-md">
|
||||||
<ul className="divide-y divide-gray-200 dark:divide-gray-900">{eventTypes}</ul>
|
<ul className="divide-y divide-gray-200 dark:divide-gray-900">{eventTypes}</ul>
|
||||||
{eventTypes.length == 0 && (
|
{eventTypes.length == 0 && (
|
||||||
<div className="p-8 text-center text-gray-400 dark:text-white">
|
<div className="p-8 text-center text-gray-400 dark:text-white">
|
||||||
<h2 className="font-semibold text-3xl text-gray-600">Uh oh!</h2>
|
<h2 className="font-semibold text-3xl text-gray-600">Uh oh!</h2>
|
||||||
<p className="max-w-md mx-auto">This user hasn't set up any event types yet.</p>
|
<p className="max-w-md mx-auto">This user hasn't set up any event types yet.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
const user = await prisma.user.findFirst({
|
const user = await whereAndSelect(
|
||||||
where: {
|
prisma.user.findFirst,
|
||||||
|
{
|
||||||
username: context.query.user.toLowerCase(),
|
username: context.query.user.toLowerCase(),
|
||||||
},
|
},
|
||||||
select: {
|
["id", "username", "email", "name", "bio", "avatar", "theme"]
|
||||||
id: true,
|
);
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
bio: true,
|
|
||||||
avatar: true,
|
|
||||||
eventTypes: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
@ -76,6 +73,11 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { GetServerSideProps } from "next";
|
import { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { ChevronDownIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid";
|
import { ChevronDownIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Dayjs } from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
|
||||||
import prisma, { whereAndSelect } from "@lib/prisma";
|
import prisma, { whereAndSelect } from "@lib/prisma";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
|
||||||
|
@ -13,18 +13,24 @@ import Avatar from "../../components/Avatar";
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
import DatePicker from "../../components/booking/DatePicker";
|
import DatePicker from "../../components/booking/DatePicker";
|
||||||
import PoweredByCalendso from "../../components/ui/PoweredByCalendso";
|
import PoweredByCalendso from "../../components/ui/PoweredByCalendso";
|
||||||
|
import Theme from "@components/Theme";
|
||||||
|
|
||||||
export default function Type(props): Type {
|
export default function Type(props): Type {
|
||||||
// Get router variables
|
// Get router variables
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
const { isReady } = Theme(props.user.theme);
|
||||||
|
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Dayjs>(() => {
|
||||||
|
return props.date && dayjs(props.date).isValid() ? dayjs(props.date) : null;
|
||||||
|
});
|
||||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
||||||
const [timeFormat, setTimeFormat] = useState("h:mma");
|
const [timeFormat, setTimeFormat] = useState("h:mma");
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
|
||||||
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters()));
|
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters()));
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
|
||||||
|
@ -33,6 +39,26 @@ export default function Type(props): Type {
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = selectedDate.utc().format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
date: formattedDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
shallow: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [selectedDate]);
|
||||||
|
|
||||||
const handleSelectTimeZone = (selectedTimeZone: string): void => {
|
const handleSelectTimeZone = (selectedTimeZone: string): void => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
setSelectedDate(selectedDate.tz(selectedTimeZone));
|
setSelectedDate(selectedDate.tz(selectedTimeZone));
|
||||||
|
@ -45,116 +71,127 @@ export default function Type(props): Type {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
isReady && (
|
||||||
<Head>
|
<div>
|
||||||
<title>
|
<Head>
|
||||||
{rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username} |
|
<title>
|
||||||
Calendso
|
{rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username}{" "}
|
||||||
</title>
|
| Calendso
|
||||||
<meta name="title" content={"Meet " + (props.user.name || props.user.username) + " via Calendso"} />
|
</title>
|
||||||
<meta name="description" content={props.eventType.description} />
|
<meta name="title" content={"Meet " + (props.user.name || props.user.username) + " via Calendso"} />
|
||||||
|
<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
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
content={"Meet " + (props.user.name || props.user.username) + " via Calendso"}
|
content={"Meet " + (props.user.name || props.user.username) + " via Calendso"}
|
||||||
/>
|
/>
|
||||||
<meta property="og:description" content={props.eventType.description} />
|
<meta property="og:description" content={props.eventType.description} />
|
||||||
<meta
|
<meta
|
||||||
property="og:image"
|
property="og:image"
|
||||||
content={
|
content={
|
||||||
"https://og-image-one-pi.vercel.app/" +
|
"https://og-image-one-pi.vercel.app/" +
|
||||||
encodeURIComponent(
|
encodeURIComponent(
|
||||||
"Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description
|
"Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description
|
||||||
).replace(/'/g, "%27") +
|
).replace(/'/g, "%27") +
|
||||||
".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" +
|
".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" +
|
||||||
encodeURIComponent(props.user.avatar)
|
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
|
<meta
|
||||||
property="twitter:title"
|
property="twitter:title"
|
||||||
content={"Meet " + (props.user.name || props.user.username) + " via Calendso"}
|
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
|
<meta
|
||||||
property="twitter:image"
|
property="twitter:image"
|
||||||
content={
|
content={
|
||||||
"https://og-image-one-pi.vercel.app/" +
|
"https://og-image-one-pi.vercel.app/" +
|
||||||
encodeURIComponent(
|
encodeURIComponent(
|
||||||
"Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description
|
"Meet **" + (props.user.name || props.user.username) + "** <br>" + props.eventType.description
|
||||||
).replace(/'/g, "%27") +
|
).replace(/'/g, "%27") +
|
||||||
".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" +
|
".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" +
|
||||||
encodeURIComponent(props.user.avatar)
|
encodeURIComponent(props.user.avatar)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<main
|
<main
|
||||||
className={
|
className={
|
||||||
"mx-auto my-0 sm:my-24 transition-max-width ease-in-out duration-500 " +
|
"mx-auto my-0 sm:my-24 transition-max-width ease-in-out duration-500 " +
|
||||||
(selectedDate ? "max-w-6xl" : "max-w-3xl")
|
(selectedDate ? "max-w-6xl" : "max-w-3xl")
|
||||||
}>
|
}>
|
||||||
<div className="dark:bg-gray-800 bg-white sm:shadow sm:rounded-lg">
|
<div className="dark:bg-gray-800 bg-white sm:shadow sm:rounded-lg">
|
||||||
<div className="sm:flex px-4 py-5 sm:p-4">
|
<div className="sm:flex px-4 py-5 sm:p-4">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
"pr-8 sm:border-r sm:dark:border-gray-900 " + (selectedDate ? "sm:w-1/3" : "sm:w-1/2")
|
"pr-8 sm:border-r sm:dark:border-gray-900 " + (selectedDate ? "sm:w-1/3" : "sm:w-1/2")
|
||||||
}>
|
}>
|
||||||
<Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" />
|
<Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" />
|
||||||
<h2 className="font-medium dark:text-gray-300 text-gray-500">{props.user.name}</h2>
|
<h2 className="font-medium dark:text-gray-300 text-gray-500">{props.user.name}</h2>
|
||||||
<h1 className="text-3xl font-semibold dark:text-white text-gray-800 mb-4">
|
<h1 className="text-3xl font-semibold dark:text-white text-gray-800 mb-4">
|
||||||
{props.eventType.title}
|
{props.eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mb-1 px-2 py-1 -ml-2">
|
<p className="text-gray-500 mb-1 px-2 py-1 -ml-2">
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{props.eventType.length} minutes
|
{props.eventType.length} minutes
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsTimeOptionsOpen(!isTimeOptionsOpen)}
|
onClick={() => setIsTimeOptionsOpen(!isTimeOptionsOpen)}
|
||||||
className="text-gray-500 mb-1 px-2 py-1 -ml-2">
|
className="text-gray-500 mb-1 px-2 py-1 -ml-2">
|
||||||
<GlobeIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<GlobeIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{timeZone()}
|
{timeZone()}
|
||||||
<ChevronDownIcon className="inline-block w-4 h-4 ml-1 -mt-1" />
|
<ChevronDownIcon className="inline-block w-4 h-4 ml-1 -mt-1" />
|
||||||
</button>
|
</button>
|
||||||
{isTimeOptionsOpen && (
|
{isTimeOptionsOpen && (
|
||||||
<TimeOptions
|
<TimeOptions
|
||||||
onSelectTimeZone={handleSelectTimeZone}
|
onSelectTimeZone={handleSelectTimeZone}
|
||||||
onToggle24hClock={handleToggle24hClock}
|
onToggle24hClock={handleToggle24hClock}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="dark:text-gray-200 text-gray-600 mt-3 mb-8">{props.eventType.description}</p>
|
||||||
|
</div>
|
||||||
|
<DatePicker
|
||||||
|
date={selectedDate}
|
||||||
|
periodType={props.eventType?.periodType}
|
||||||
|
periodStartDate={props.eventType?.periodStartDate}
|
||||||
|
periodEndDate={props.eventType?.periodEndDate}
|
||||||
|
periodDays={props.eventType?.periodDays}
|
||||||
|
periodCountCalendarDays={props.eventType?.periodCountCalendarDays}
|
||||||
|
weekStart={props.user.weekStart}
|
||||||
|
onDatePicked={changeDate}
|
||||||
|
workingHours={props.workingHours}
|
||||||
|
organizerTimeZone={props.eventType.timeZone || props.user.timeZone}
|
||||||
|
inviteeTimeZone={timeZone()}
|
||||||
|
eventLength={props.eventType.length}
|
||||||
|
/>
|
||||||
|
{selectedDate && (
|
||||||
|
<AvailableTimes
|
||||||
|
workingHours={props.workingHours}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
organizerTimeZone={props.eventType.timeZone || props.user.timeZone}
|
||||||
|
eventLength={props.eventType.length}
|
||||||
|
eventTypeId={props.eventType.id}
|
||||||
|
date={selectedDate}
|
||||||
|
user={props.user}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="dark:text-gray-200 text-gray-600 mt-3 mb-8">{props.eventType.description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
|
||||||
weekStart={props.user.weekStart}
|
|
||||||
onDatePicked={changeDate}
|
|
||||||
workingHours={props.workingHours}
|
|
||||||
organizerTimeZone={props.eventType.timeZone || props.user.timeZone}
|
|
||||||
inviteeTimeZone={timeZone()}
|
|
||||||
eventLength={props.eventType.length}
|
|
||||||
/>
|
|
||||||
{selectedDate && (
|
|
||||||
<AvailableTimes
|
|
||||||
workingHours={props.workingHours}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
organizerTimeZone={props.eventType.timeZone || props.user.timeZone}
|
|
||||||
eventLength={props.eventType.length}
|
|
||||||
eventTypeId={props.eventType.id}
|
|
||||||
date={selectedDate}
|
|
||||||
user={props.user}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{!props.user.hideBranding && <PoweredByCalendso />}
|
||||||
{!props.user.hideBranding && <PoweredByCalendso />}
|
</main>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const dateQuery = context.query?.date ?? null;
|
||||||
|
const date = Array.isArray(dateQuery) && dateQuery.length > 0 ? dateQuery.pop() : dateQuery;
|
||||||
|
|
||||||
const user = await whereAndSelect(
|
const user = await whereAndSelect(
|
||||||
prisma.user.findFirst,
|
prisma.user.findFirst,
|
||||||
{
|
{
|
||||||
|
@ -167,13 +204,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
"email",
|
"email",
|
||||||
"bio",
|
"bio",
|
||||||
"avatar",
|
"avatar",
|
||||||
"eventTypes",
|
|
||||||
"startTime",
|
"startTime",
|
||||||
"endTime",
|
"endTime",
|
||||||
"timeZone",
|
"timeZone",
|
||||||
"weekStart",
|
"weekStart",
|
||||||
"availability",
|
"availability",
|
||||||
"hideBranding",
|
"hideBranding",
|
||||||
|
"theme",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -189,7 +226,19 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
slug: context.query.type,
|
slug: context.query.type,
|
||||||
},
|
},
|
||||||
["id", "title", "description", "length", "availability", "timeZone"]
|
[
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"length",
|
||||||
|
"availability",
|
||||||
|
"timeZone",
|
||||||
|
"periodType",
|
||||||
|
"periodDays",
|
||||||
|
"periodStartDate",
|
||||||
|
"periodEndDate",
|
||||||
|
"periodCountCalendarDays",
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
|
@ -216,10 +265,16 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
|
||||||
workingHours.sort((a, b) => a.startTime - b.startTime);
|
workingHours.sort((a, b) => a.startTime - b.startTime);
|
||||||
|
|
||||||
|
const eventTypeObject = Object.assign({}, eventType, {
|
||||||
|
periodStartDate: eventType.periodStartDate?.toString() ?? null,
|
||||||
|
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
eventType,
|
date,
|
||||||
|
eventType: eventTypeObject,
|
||||||
workingHours,
|
workingHours,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ 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 { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid";
|
import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid";
|
||||||
import prisma from "../../lib/prisma";
|
import prisma, { whereAndSelect } from "../../lib/prisma";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
@ -14,6 +14,7 @@ import { LocationType } from "../../lib/location";
|
||||||
import Avatar from "../../components/Avatar";
|
import Avatar from "../../components/Avatar";
|
||||||
import Button from "../../components/ui/Button";
|
import Button from "../../components/ui/Button";
|
||||||
import { EventTypeCustomInputType } from "../../lib/eventTypeInput";
|
import { EventTypeCustomInputType } from "../../lib/eventTypeInput";
|
||||||
|
import Theme from "@components/Theme";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
@ -32,7 +33,10 @@ export default function Book(props: any): JSX.Element {
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationType>(
|
const [selectedLocation, setSelectedLocation] = useState<LocationType>(
|
||||||
locations.length === 1 ? locations[0].type : ""
|
locations.length === 1 ? locations[0].type : ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { isReady } = Theme(props.user.theme);
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreferredTimeZone(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess());
|
setPreferredTimeZone(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess());
|
||||||
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
|
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
|
||||||
|
@ -138,247 +142,247 @@ export default function Book(props: any): JSX.Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
isReady && (
|
||||||
<Head>
|
<div>
|
||||||
<title>
|
<Head>
|
||||||
{rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "}
|
<title>
|
||||||
{props.user.name || props.user.username} | Calendso
|
{rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "}
|
||||||
</title>
|
{props.user.name || props.user.username} | Calendso
|
||||||
<link rel="icon" href="/favicon.ico" />
|
</title>
|
||||||
</Head>
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
<main className="max-w-3xl mx-auto my-0 sm:my-24">
|
<main className="max-w-3xl mx-auto my-0 sm:my-24">
|
||||||
<div className="dark:bg-gray-800 bg-white overflow-hidden sm:shadow sm:rounded-lg">
|
<div className="dark:bg-gray-800 bg-white overflow-hidden sm:shadow sm:rounded-lg">
|
||||||
<div className="sm:flex px-4 py-5 sm:p-4">
|
<div className="sm:flex px-4 py-5 sm:p-4">
|
||||||
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-900">
|
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-900">
|
||||||
<Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" />
|
<Avatar user={props.user} className="w-16 h-16 rounded-full mb-4" />
|
||||||
<h2 className="font-medium dark:text-gray-300 text-gray-500">{props.user.name}</h2>
|
<h2 className="font-medium dark:text-gray-300 text-gray-500">{props.user.name}</h2>
|
||||||
<h1 className="text-3xl font-semibold dark:text-white text-gray-800 mb-4">
|
<h1 className="text-3xl font-semibold dark:text-white text-gray-800 mb-4">
|
||||||
{props.eventType.title}
|
{props.eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mb-2">
|
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
|
||||||
{props.eventType.length} minutes
|
|
||||||
</p>
|
|
||||||
{selectedLocation === LocationType.InPerson && (
|
|
||||||
<p className="text-gray-500 mb-2">
|
<p className="text-gray-500 mb-2">
|
||||||
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{locationInfo(selectedLocation).address}
|
{props.eventType.length} minutes
|
||||||
</p>
|
</p>
|
||||||
)}
|
{selectedLocation === LocationType.InPerson && (
|
||||||
<p className="text-blue-600 mb-4">
|
<p className="text-gray-500 mb-2">
|
||||||
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{preferredTimeZone &&
|
{locationInfo(selectedLocation).address}
|
||||||
dayjs(date)
|
</p>
|
||||||
.tz(preferredTimeZone)
|
|
||||||
.format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")}
|
|
||||||
</p>
|
|
||||||
<p className="dark:text-white text-gray-600 mb-8">{props.eventType.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
|
||||||
<form onSubmit={bookingHandler}>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="name" className="block text-sm font-medium dark:text-white text-gray-700">
|
|
||||||
Your name
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
required
|
|
||||||
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder="John Doe"
|
|
||||||
defaultValue={props.booking ? props.booking.attendees[0].name : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium dark:text-white text-gray-700">
|
|
||||||
Email address
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
id="email"
|
|
||||||
required
|
|
||||||
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder="you@example.com"
|
|
||||||
defaultValue={props.booking ? props.booking.attendees[0].email : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{locations.length > 1 && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<span className="block text-sm font-medium text-gray-700">Location</span>
|
|
||||||
{locations.map((location) => (
|
|
||||||
<label key={location.type} className="block">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
required
|
|
||||||
onChange={(e) => setSelectedLocation(e.target.value)}
|
|
||||||
className="location"
|
|
||||||
name="location"
|
|
||||||
value={location.type}
|
|
||||||
checked={selectedLocation === location.type}
|
|
||||||
/>
|
|
||||||
<span className="text-sm ml-2">{locationLabels[location.type]}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{selectedLocation === LocationType.Phone && (
|
<p className="text-blue-600 mb-4">
|
||||||
|
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
|
{preferredTimeZone &&
|
||||||
|
dayjs(date)
|
||||||
|
.tz(preferredTimeZone)
|
||||||
|
.format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")}
|
||||||
|
</p>
|
||||||
|
<p className="dark:text-white text-gray-600 mb-8">{props.eventType.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
||||||
|
<form onSubmit={bookingHandler}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label htmlFor="name" className="block text-sm font-medium dark:text-white text-gray-700">
|
||||||
htmlFor="phone"
|
Your name
|
||||||
className="block text-sm font-medium dark:text-white text-gray-700">
|
|
||||||
Phone Number
|
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<PhoneInput
|
<input
|
||||||
name="phone"
|
type="text"
|
||||||
placeholder="Enter phone number"
|
name="name"
|
||||||
id="phone"
|
id="name"
|
||||||
required
|
required
|
||||||
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
onChange={() => {
|
placeholder="John Doe"
|
||||||
/* DO NOT REMOVE: Callback required by PhoneInput, comment added to satisfy eslint:no-empty-function */
|
defaultValue={props.booking ? props.booking.attendees[0].name : ""}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="mb-4">
|
||||||
{props.eventType.customInputs &&
|
<label
|
||||||
props.eventType.customInputs
|
htmlFor="email"
|
||||||
.sort((a, b) => a.id - b.id)
|
className="block text-sm font-medium dark:text-white text-gray-700">
|
||||||
.map((input) => (
|
Email address
|
||||||
<div className="mb-4" key={"input-" + input.label.toLowerCase}>
|
</label>
|
||||||
{input.type !== EventTypeCustomInputType.Bool && (
|
<div className="mt-1">
|
||||||
<label
|
<input
|
||||||
htmlFor={input.label}
|
type="email"
|
||||||
className="block text-sm font-medium text-gray-700 mb-1">
|
name="email"
|
||||||
{input.label}
|
id="email"
|
||||||
</label>
|
required
|
||||||
)}
|
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
{input.type === EventTypeCustomInputType.TextLong && (
|
placeholder="you@example.com"
|
||||||
<textarea
|
defaultValue={props.booking ? props.booking.attendees[0].email : ""}
|
||||||
name={"custom_" + input.id}
|
/>
|
||||||
id={"custom_" + input.id}
|
|
||||||
required={input.required}
|
|
||||||
rows={3}
|
|
||||||
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder=""
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{input.type === EventTypeCustomInputType.Text && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name={"custom_" + input.id}
|
|
||||||
id={"custom_" + input.id}
|
|
||||||
required={input.required}
|
|
||||||
className="shadow-sm dark:bg-gray-700 dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder=""
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{input.type === EventTypeCustomInputType.Number && (
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name={"custom_" + input.id}
|
|
||||||
id={"custom_" + input.id}
|
|
||||||
required={input.required}
|
|
||||||
className="shadow-sm dark:bg-gray-700 dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder=""
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{input.type === EventTypeCustomInputType.Bool && (
|
|
||||||
<div className="flex items-center h-5">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name={"custom_" + input.id}
|
|
||||||
id={"custom_" + input.id}
|
|
||||||
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2"
|
|
||||||
placeholder=""
|
|
||||||
/>
|
|
||||||
<label htmlFor={input.label} className="block text-sm font-medium text-gray-700">
|
|
||||||
{input.label}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="mb-4">
|
|
||||||
<label
|
|
||||||
htmlFor="notes"
|
|
||||||
className="block text-sm font-medium dark:text-white text-gray-700 mb-1">
|
|
||||||
Additional notes
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
name="notes"
|
|
||||||
id="notes"
|
|
||||||
rows={3}
|
|
||||||
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
|
||||||
placeholder="Please share anything that will help prepare for our meeting."
|
|
||||||
defaultValue={props.booking ? props.booking.description : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start">
|
|
||||||
<Button type="submit" loading={loading} className="btn btn-primary">
|
|
||||||
{rescheduleUid ? "Reschedule" : "Confirm"}
|
|
||||||
</Button>
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
"/" +
|
|
||||||
props.user.username +
|
|
||||||
"/" +
|
|
||||||
props.eventType.slug +
|
|
||||||
(rescheduleUid ? "?rescheduleUid=" + rescheduleUid : "")
|
|
||||||
}>
|
|
||||||
<a className="ml-2 btn btn-white">Cancel</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{error && (
|
|
||||||
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mt-2">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<p className="text-sm text-yellow-700">
|
|
||||||
Could not {rescheduleUid ? "reschedule" : "book"} the meeting. Please try again or{" "}
|
|
||||||
<a
|
|
||||||
href={"mailto:" + props.user.email}
|
|
||||||
className="font-medium underline text-yellow-700 hover:text-yellow-600">
|
|
||||||
Contact {props.user.name} via e-mail
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{locations.length > 1 && (
|
||||||
)}
|
<div className="mb-4">
|
||||||
|
<span className="block text-sm font-medium text-gray-700">Location</span>
|
||||||
|
{locations.map((location) => (
|
||||||
|
<label key={location.type} className="block">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
required
|
||||||
|
onChange={(e) => setSelectedLocation(e.target.value)}
|
||||||
|
className="location"
|
||||||
|
name="location"
|
||||||
|
value={location.type}
|
||||||
|
checked={selectedLocation === location.type}
|
||||||
|
/>
|
||||||
|
<span className="text-sm ml-2">{locationLabels[location.type]}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedLocation === LocationType.Phone && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
htmlFor="phone"
|
||||||
|
className="block text-sm font-medium dark:text-white text-gray-700">
|
||||||
|
Phone Number
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<PhoneInput
|
||||||
|
name="phone"
|
||||||
|
placeholder="Enter phone number"
|
||||||
|
id="phone"
|
||||||
|
required
|
||||||
|
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
onChange={() => {
|
||||||
|
/* DO NOT REMOVE: Callback required by PhoneInput, comment added to satisfy eslint:no-empty-function */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.eventType.customInputs &&
|
||||||
|
props.eventType.customInputs
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
.map((input) => (
|
||||||
|
<div className="mb-4" key={"input-" + input.label.toLowerCase}>
|
||||||
|
{input.type !== EventTypeCustomInputType.Bool && (
|
||||||
|
<label
|
||||||
|
htmlFor={input.label}
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{input.label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.TextLong && (
|
||||||
|
<textarea
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
required={input.required}
|
||||||
|
rows={3}
|
||||||
|
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.Text && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
required={input.required}
|
||||||
|
className="shadow-sm dark:bg-gray-700 dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.Number && (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
required={input.required}
|
||||||
|
className="shadow-sm dark:bg-gray-700 dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.Bool && (
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name={"custom_" + input.id}
|
||||||
|
id={"custom_" + input.id}
|
||||||
|
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded mr-2"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={input.label}
|
||||||
|
className="block text-sm font-medium text-gray-700">
|
||||||
|
{input.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
htmlFor="notes"
|
||||||
|
className="block text-sm font-medium dark:text-white text-gray-700 mb-1">
|
||||||
|
Additional notes
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="notes"
|
||||||
|
id="notes"
|
||||||
|
rows={3}
|
||||||
|
className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="Please share anything that will help prepare for our meeting."
|
||||||
|
defaultValue={props.booking ? props.booking.description : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start">
|
||||||
|
<Button type="submit" loading={loading} className="btn btn-primary">
|
||||||
|
{rescheduleUid ? "Reschedule" : "Confirm"}
|
||||||
|
</Button>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
"/" +
|
||||||
|
props.user.username +
|
||||||
|
"/" +
|
||||||
|
props.eventType.slug +
|
||||||
|
(rescheduleUid ? "?rescheduleUid=" + rescheduleUid : "")
|
||||||
|
}>
|
||||||
|
<a className="ml-2 btn btn-white">Cancel</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{error && (
|
||||||
|
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mt-2">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-yellow-700">
|
||||||
|
Could not {rescheduleUid ? "reschedule" : "book"} the meeting. Please try again or{" "}
|
||||||
|
<a
|
||||||
|
href={"mailto:" + props.user.email}
|
||||||
|
className="font-medium underline text-yellow-700 hover:text-yellow-600">
|
||||||
|
Contact {props.user.name} via e-mail
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
const user = await prisma.user.findFirst({
|
const user = await whereAndSelect(
|
||||||
where: {
|
prisma.user.findFirst,
|
||||||
|
{
|
||||||
username: context.query.user,
|
username: context.query.user,
|
||||||
},
|
},
|
||||||
select: {
|
["username", "name", "email", "bio", "avatar", "theme"]
|
||||||
username: true,
|
);
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
bio: true,
|
|
||||||
avatar: true,
|
|
||||||
eventTypes: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventType = await prisma.eventType.findUnique({
|
const eventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
@ -392,9 +396,19 @@ export async function getServerSideProps(context) {
|
||||||
length: true,
|
length: true,
|
||||||
locations: true,
|
locations: true,
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
|
periodType: true,
|
||||||
|
periodDays: true,
|
||||||
|
periodStartDate: true,
|
||||||
|
periodEndDate: true,
|
||||||
|
periodCountCalendarDays: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const eventTypeObject = Object.assign({}, eventType, {
|
||||||
|
periodStartDate: eventType.periodStartDate?.toString() ?? null,
|
||||||
|
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
let booking = null;
|
let booking = null;
|
||||||
|
|
||||||
if (context.query.rescheduleUid) {
|
if (context.query.rescheduleUid) {
|
||||||
|
@ -417,7 +431,7 @@ export async function getServerSideProps(context) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
eventType,
|
eventType: eventTypeObject,
|
||||||
booking,
|
booking,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,6 +49,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
periodType: req.body.periodType,
|
||||||
|
periodDays: req.body.periodDays,
|
||||||
|
periodStartDate: req.body.periodStartDate,
|
||||||
|
periodEndDate: req.body.periodEndDate,
|
||||||
|
periodCountCalendarDays: req.body.periodCountCalendarDays,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (req.method == "POST") {
|
if (req.method == "POST") {
|
||||||
|
|
|
@ -13,6 +13,14 @@ import logger from "../../../lib/logger";
|
||||||
import EventManager, { EventResult } from "@lib/events/EventManager";
|
import EventManager, { EventResult } from "@lib/events/EventManager";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import dayjsBusinessDays from "dayjs-business-days";
|
||||||
|
|
||||||
|
dayjs.extend(dayjsBusinessDays);
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
const translator = short();
|
const translator = short();
|
||||||
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
||||||
|
|
||||||
|
@ -50,6 +58,32 @@ function isAvailable(busyTimes, time, length) {
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOutOfBounds(
|
||||||
|
time: dayjs.ConfigType,
|
||||||
|
{ periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone }
|
||||||
|
): boolean {
|
||||||
|
const date = dayjs(time);
|
||||||
|
|
||||||
|
switch (periodType) {
|
||||||
|
case "rolling": {
|
||||||
|
const periodRollingEndDay = periodCountCalendarDays
|
||||||
|
? dayjs().tz(timeZone).add(periodDays, "days").endOf("day")
|
||||||
|
: dayjs().tz(timeZone).businessDaysAdd(periodDays, "days").endOf("day");
|
||||||
|
return date.endOf("day").isAfter(periodRollingEndDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "range": {
|
||||||
|
const periodRangeStartDay = dayjs(periodStartDate).tz(timeZone).endOf("day");
|
||||||
|
const periodRangeEndDay = dayjs(periodEndDate).tz(timeZone).endOf("day");
|
||||||
|
return date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "unlimited":
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface GetLocationRequestFromIntegrationRequest {
|
interface GetLocationRequestFromIntegrationRequest {
|
||||||
location: string;
|
location: string;
|
||||||
}
|
}
|
||||||
|
@ -131,11 +165,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
dayjs(req.body.end).endOf("day").utc().format(),
|
dayjs(req.body.end).endOf("day").utc().format(),
|
||||||
selectedCalendars
|
selectedCalendars
|
||||||
);
|
);
|
||||||
const videoAvailability = await getBusyVideoTimes(
|
const videoAvailability = await getBusyVideoTimes(currentUser.credentials);
|
||||||
currentUser.credentials,
|
|
||||||
dayjs(req.body.start).startOf("day").utc().format(),
|
|
||||||
dayjs(req.body.end).endOf("day").utc().format()
|
|
||||||
);
|
|
||||||
let commonAvailability = [];
|
let commonAvailability = [];
|
||||||
|
|
||||||
if (hasCalendarIntegrations && hasVideoIntegrations) {
|
if (hasCalendarIntegrations && hasVideoIntegrations) {
|
||||||
|
@ -175,6 +205,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
eventName: true,
|
eventName: true,
|
||||||
title: true,
|
title: true,
|
||||||
length: true,
|
length: true,
|
||||||
|
periodType: true,
|
||||||
|
periodDays: true,
|
||||||
|
periodStartDate: true,
|
||||||
|
periodEndDate: true,
|
||||||
|
periodCountCalendarDays: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -237,6 +272,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return res.status(400).json(error);
|
return res.status(400).json(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timeOutOfBounds = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
timeOutOfBounds = isOutOfBounds(req.body.start, {
|
||||||
|
periodType: selectedEventType.periodType,
|
||||||
|
periodDays: selectedEventType.periodDays,
|
||||||
|
periodEndDate: selectedEventType.periodEndDate,
|
||||||
|
periodStartDate: selectedEventType.periodStartDate,
|
||||||
|
periodCountCalendarDays: selectedEventType.periodCountCalendarDays,
|
||||||
|
timeZone: currentUser.timeZone,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
log.debug({
|
||||||
|
message: "Unable set timeOutOfBounds. Using false. ",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeOutOfBounds) {
|
||||||
|
const error = {
|
||||||
|
errorCode: "BookingUserUnAvailable",
|
||||||
|
message: `${currentUser.name} is unavailable at this time.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug(`Booking ${user} failed`, error);
|
||||||
|
return res.status(400).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
let results: Array<EventResult> = [];
|
let results: Array<EventResult> = [];
|
||||||
let referencesToCreate = [];
|
let referencesToCreate = [];
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
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, { whereAndSelect } 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
const user = await prisma.user.findUnique({
|
const user = await whereAndSelect(
|
||||||
where: {
|
prisma.user.findUnique,
|
||||||
email: session.user.email,
|
{
|
||||||
|
id: session.user.id,
|
||||||
},
|
},
|
||||||
select: {
|
["id", "password"]
|
||||||
id: true,
|
);
|
||||||
password: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) { res.status(404).json({message: 'User not found'}); return; }
|
if (!user) {
|
||||||
|
res.status(404).json({ message: "User not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const username = req.body.username;
|
const username = req.body.username;
|
||||||
// username is changed: username is optional but it is necessary to be unique, enforce here
|
// username is changed: username is optional but it is necessary to be unique, enforce here
|
||||||
|
@ -29,10 +30,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const userConflict = await prisma.user.findFirst({
|
const userConflict = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (userConflict) {
|
if (userConflict) {
|
||||||
return res.status(409).json({ message: 'Username already taken' });
|
return res.status(409).json({ message: "Username already taken" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,8 +43,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const timeZone = req.body.timeZone;
|
const timeZone = req.body.timeZone;
|
||||||
const weekStart = req.body.weekStart;
|
const weekStart = req.body.weekStart;
|
||||||
const hideBranding = req.body.hideBranding;
|
const hideBranding = req.body.hideBranding;
|
||||||
|
const theme = req.body.theme;
|
||||||
|
|
||||||
const updateUser = await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
|
@ -52,11 +54,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
name,
|
name,
|
||||||
avatar,
|
avatar,
|
||||||
bio: description,
|
bio: description,
|
||||||
timeZone: timeZone,
|
timeZone,
|
||||||
weekStart: weekStart,
|
weekStart,
|
||||||
hideBranding: hideBranding,
|
hideBranding,
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({message: 'Profile updated successfully'});
|
return res.status(200).json({ message: "Profile updated successfully" });
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,13 @@ import utc from "dayjs/plugin/utc";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import { Availability, EventType, User } from "@prisma/client";
|
import { Availability, EventType, User } from "@prisma/client";
|
||||||
import { validJson } from "@lib/jsonUtils";
|
import { validJson } from "@lib/jsonUtils";
|
||||||
|
import Text from "@components/ui/Text";
|
||||||
|
import { RadioGroup } from "@headlessui/react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
import "react-dates/initialize";
|
||||||
|
import "react-dates/lib/css/_datepicker.css";
|
||||||
|
import { DateRangePicker, OrientationShape, toMomentObject } from "react-dates";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
@ -54,8 +61,28 @@ type EventTypeInput = {
|
||||||
customInputs: EventTypeCustomInput[];
|
customInputs: EventTypeCustomInput[];
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] };
|
availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] };
|
||||||
|
periodType?: string;
|
||||||
|
periodDays?: number;
|
||||||
|
periodStartDate?: Date | string;
|
||||||
|
periodEndDate?: Date | string;
|
||||||
|
periodCountCalendarDays?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PERIOD_TYPES = [
|
||||||
|
{
|
||||||
|
type: "rolling",
|
||||||
|
suffix: "into the future",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "range",
|
||||||
|
prefix: "Within a date range",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "unlimited",
|
||||||
|
prefix: "Indefinitely into the future",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function EventTypePage({
|
export default function EventTypePage({
|
||||||
user,
|
user,
|
||||||
eventType,
|
eventType,
|
||||||
|
@ -64,6 +91,7 @@ export default function EventTypePage({
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
console.log(eventType);
|
||||||
const inputOptions: OptionBase[] = [
|
const inputOptions: OptionBase[] = [
|
||||||
{ value: EventTypeCustomInputType.Text, label: "Text" },
|
{ value: EventTypeCustomInputType.Text, label: "Text" },
|
||||||
{ value: EventTypeCustomInputType.TextLong, label: "Multiline Text" },
|
{ value: EventTypeCustomInputType.TextLong, label: "Multiline Text" },
|
||||||
|
@ -71,6 +99,39 @@ export default function EventTypePage({
|
||||||
{ value: EventTypeCustomInputType.Bool, label: "Checkbox" },
|
{ value: EventTypeCustomInputType.Bool, label: "Checkbox" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [DATE_PICKER_ORIENTATION, setDatePickerOrientation] = useState<OrientationShape>("horizontal");
|
||||||
|
const [contentSize, setContentSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const handleResizeEvent = () => {
|
||||||
|
const elementWidth = parseFloat(getComputedStyle(document.body).width);
|
||||||
|
const elementHeight = parseFloat(getComputedStyle(document.body).height);
|
||||||
|
|
||||||
|
setContentSize({
|
||||||
|
width: elementWidth,
|
||||||
|
height: elementHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const throttledHandleResizeEvent = throttle(handleResizeEvent, 100);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleResizeEvent();
|
||||||
|
|
||||||
|
window.addEventListener("resize", throttledHandleResizeEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", throttledHandleResizeEvent);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentSize.width < 500) {
|
||||||
|
setDatePickerOrientation("vertical");
|
||||||
|
} else {
|
||||||
|
setDatePickerOrientation("horizontal");
|
||||||
|
}
|
||||||
|
}, [contentSize]);
|
||||||
|
|
||||||
const [enteredAvailability, setEnteredAvailability] = useState();
|
const [enteredAvailability, setEnteredAvailability] = useState();
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
|
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
|
||||||
|
@ -83,12 +144,37 @@ export default function EventTypePage({
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [periodStartDate, setPeriodStartDate] = useState(() => {
|
||||||
|
if (eventType.periodType === "range" && eventType?.periodStartDate) {
|
||||||
|
return toMomentObject(new Date(eventType.periodStartDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [periodEndDate, setPeriodEndDate] = useState(() => {
|
||||||
|
if (eventType.periodType === "range" && eventType.periodEndDate) {
|
||||||
|
return toMomentObject(new Date(eventType?.periodEndDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const [focusedInput, setFocusedInput] = useState(null);
|
||||||
|
const [periodType, setPeriodType] = useState(() => {
|
||||||
|
return (
|
||||||
|
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
|
||||||
|
PERIOD_TYPES.find((s) => s.type === "unlimited")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const titleRef = useRef<HTMLInputElement>();
|
const titleRef = useRef<HTMLInputElement>();
|
||||||
const slugRef = useRef<HTMLInputElement>();
|
const slugRef = useRef<HTMLInputElement>();
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>();
|
const descriptionRef = useRef<HTMLTextAreaElement>();
|
||||||
const lengthRef = useRef<HTMLInputElement>();
|
const lengthRef = useRef<HTMLInputElement>();
|
||||||
const isHiddenRef = useRef<HTMLInputElement>();
|
const isHiddenRef = useRef<HTMLInputElement>();
|
||||||
const eventNameRef = useRef<HTMLInputElement>();
|
const eventNameRef = useRef<HTMLInputElement>();
|
||||||
|
const periodDaysRef = useRef<HTMLInputElement>();
|
||||||
|
const periodDaysTypeRef = useRef<HTMLSelectElement>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(eventType.timeZone || user.timeZone);
|
setSelectedTimeZone(eventType.timeZone || user.timeZone);
|
||||||
|
@ -103,6 +189,22 @@ export default function EventTypePage({
|
||||||
const enteredLength: number = parseInt(lengthRef.current.value);
|
const enteredLength: number = parseInt(lengthRef.current.value);
|
||||||
const enteredIsHidden: boolean = isHiddenRef.current.checked;
|
const enteredIsHidden: boolean = isHiddenRef.current.checked;
|
||||||
const enteredEventName: string = eventNameRef.current.value;
|
const enteredEventName: string = eventNameRef.current.value;
|
||||||
|
|
||||||
|
const type = periodType.type;
|
||||||
|
const enteredPeriodDays = parseInt(periodDaysRef?.current?.value);
|
||||||
|
const enteredPeriodDaysType = Boolean(parseInt(periodDaysTypeRef?.current.value));
|
||||||
|
|
||||||
|
const enteredPeriodStartDate = periodStartDate ? periodStartDate.toDate() : null;
|
||||||
|
const enteredPeriodEndDate = periodEndDate ? periodEndDate.toDate() : null;
|
||||||
|
|
||||||
|
console.log("values", {
|
||||||
|
type,
|
||||||
|
periodDaysTypeRef,
|
||||||
|
enteredPeriodDays,
|
||||||
|
enteredPeriodDaysType,
|
||||||
|
enteredPeriodStartDate,
|
||||||
|
enteredPeriodEndDate,
|
||||||
|
});
|
||||||
// TODO: Add validation
|
// TODO: Add validation
|
||||||
|
|
||||||
const payload: EventTypeInput = {
|
const payload: EventTypeInput = {
|
||||||
|
@ -116,6 +218,11 @@ export default function EventTypePage({
|
||||||
eventName: enteredEventName,
|
eventName: enteredEventName,
|
||||||
customInputs,
|
customInputs,
|
||||||
timeZone: selectedTimeZone,
|
timeZone: selectedTimeZone,
|
||||||
|
periodType: type,
|
||||||
|
periodDays: enteredPeriodDays,
|
||||||
|
periodStartDate: enteredPeriodStartDate,
|
||||||
|
periodEndDate: enteredPeriodEndDate,
|
||||||
|
periodCountCalendarDays: enteredPeriodDaysType,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (enteredAvailability) {
|
if (enteredAvailability) {
|
||||||
|
@ -268,8 +375,8 @@ export default function EventTypePage({
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Shell heading={"Event Type - " + eventType.title}>
|
<Shell heading={"Event Type - " + eventType.title}>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="max-w-5xl mx-auto">
|
||||||
<div className="col-span-3 sm:col-span-2">
|
<div className="">
|
||||||
<div className="bg-white overflow-hidden shadow rounded-lg mb-4">
|
<div className="bg-white overflow-hidden shadow rounded-lg mb-4">
|
||||||
<div className="px-4 py-5 sm:p-6">
|
<div className="px-4 py-5 sm:p-6">
|
||||||
<form onSubmit={updateEventTypeHandler}>
|
<form onSubmit={updateEventTypeHandler}>
|
||||||
|
@ -330,7 +437,7 @@ export default function EventTypePage({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{locations.length > 0 && (
|
{locations.length > 0 && (
|
||||||
<ul className="w-96 mt-1">
|
<ul className="mt-1">
|
||||||
{locations.map((location) => (
|
{locations.map((location) => (
|
||||||
<li key={location.type} className="bg-blue-50 mb-2 p-2 border">
|
<li key={location.type} className="bg-blue-50 mb-2 p-2 border">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
@ -450,26 +557,6 @@ export default function EventTypePage({
|
||||||
defaultValue={eventType.description}></textarea>
|
defaultValue={eventType.description}></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="length" className="block text-sm font-medium text-gray-700">
|
|
||||||
Length
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 relative rounded-md shadow-sm">
|
|
||||||
<input
|
|
||||||
ref={lengthRef}
|
|
||||||
type="number"
|
|
||||||
name="length"
|
|
||||||
id="length"
|
|
||||||
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={eventType.length}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
|
|
||||||
minutes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="eventName" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="eventName" className="block text-sm font-medium text-gray-700">
|
||||||
Calendar entry name
|
Calendar entry name
|
||||||
|
@ -554,29 +641,151 @@ export default function EventTypePage({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="my-4" />
|
|
||||||
<div>
|
<fieldset className="my-8">
|
||||||
<h3 className="mb-2">How do you want to offer your availability for this event type?</h3>
|
<Text variant="largetitle">When can people book this event?</Text>
|
||||||
<Scheduler
|
<hr className="my-8" />
|
||||||
setAvailability={setEnteredAvailability}
|
<section className="space-y-12">
|
||||||
setTimeZone={setSelectedTimeZone}
|
<div className="mb-4">
|
||||||
timeZone={selectedTimeZone}
|
{/* <label htmlFor="period" className=""> */}
|
||||||
availability={availability}
|
<Text variant="subtitle">Date Range</Text>
|
||||||
/>
|
{/* </label> */}
|
||||||
<div className="py-4 flex justify-end">
|
<Text variant="title3">Invitees can schedule...</Text>
|
||||||
<Link href="/availability">
|
<div className="mt-1 relative ">
|
||||||
<a className="mr-2 btn btn-white">Cancel</a>
|
<RadioGroup value={periodType} onChange={setPeriodType}>
|
||||||
</Link>
|
<RadioGroup.Label className="sr-only">Date Range</RadioGroup.Label>
|
||||||
<button type="submit" className="btn btn-primary">
|
<div className="bg-white rounded-md -space-y-px">
|
||||||
Update
|
{PERIOD_TYPES.map((period) => (
|
||||||
</button>
|
<RadioGroup.Option
|
||||||
</div>
|
key={period.type}
|
||||||
</div>
|
value={period}
|
||||||
|
className={({ checked }) =>
|
||||||
|
classnames(
|
||||||
|
checked ? "bg-indigo-50 border-indigo-200 z-10" : "border-gray-200",
|
||||||
|
"relative py-4 px-2 lg:p-4 min-h-20 lg:flex items-center cursor-pointer focus:outline-none"
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
{({ active, checked }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={classnames(
|
||||||
|
checked
|
||||||
|
? "bg-indigo-600 border-transparent"
|
||||||
|
: "bg-white border-gray-300",
|
||||||
|
active ? "ring-2 ring-offset-2 ring-indigo-500" : "",
|
||||||
|
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
||||||
|
)}
|
||||||
|
aria-hidden="true">
|
||||||
|
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||||
|
</span>
|
||||||
|
<div className="lg:ml-3 flex flex-col">
|
||||||
|
<RadioGroup.Label
|
||||||
|
as="span"
|
||||||
|
className={classnames(
|
||||||
|
checked ? "text-indigo-900" : "text-gray-900",
|
||||||
|
"block text-sm font-light space-y-2 lg:space-y-0 lg:space-x-2"
|
||||||
|
)}>
|
||||||
|
<span>{period.prefix}</span>
|
||||||
|
{period.type === "rolling" && (
|
||||||
|
<div className="inline-flex">
|
||||||
|
<input
|
||||||
|
ref={periodDaysRef}
|
||||||
|
type="text"
|
||||||
|
name="periodDays"
|
||||||
|
id=""
|
||||||
|
className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-12 sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="30"
|
||||||
|
defaultValue={eventType.periodDays || 30}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
ref={periodDaysTypeRef}
|
||||||
|
id=""
|
||||||
|
name="periodDaysType"
|
||||||
|
className=" block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
||||||
|
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||||
|
<option value="1">calendar days</option>
|
||||||
|
<option value="0">business days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{checked && period.type === "range" && (
|
||||||
|
<div className="inline-flex space-x-2">
|
||||||
|
<DateRangePicker
|
||||||
|
orientation={DATE_PICKER_ORIENTATION}
|
||||||
|
startDate={periodStartDate}
|
||||||
|
startDateId="your_unique_start_date_id"
|
||||||
|
endDate={periodEndDate}
|
||||||
|
endDateId="your_unique_end_date_id"
|
||||||
|
onDatesChange={({ startDate, endDate }) => {
|
||||||
|
setPeriodStartDate(startDate);
|
||||||
|
setPeriodEndDate(endDate);
|
||||||
|
}}
|
||||||
|
focusedInput={focusedInput}
|
||||||
|
onFocusChange={(focusedInput) => {
|
||||||
|
setFocusedInput(focusedInput);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span>{period.suffix}</span>
|
||||||
|
</RadioGroup.Label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</RadioGroup.Option>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="my-8" />
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="length" className="block text-sm font-medium text-gray-700">
|
||||||
|
<Text variant="caption">Duration</Text>
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
ref={lengthRef}
|
||||||
|
type="number"
|
||||||
|
name="length"
|
||||||
|
id="length"
|
||||||
|
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={eventType.length}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
|
||||||
|
minutes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="my-8" />
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2">
|
||||||
|
How do you want to offer your availability for this event type?
|
||||||
|
</h3>
|
||||||
|
<Scheduler
|
||||||
|
setAvailability={setEnteredAvailability}
|
||||||
|
setTimeZone={setSelectedTimeZone}
|
||||||
|
timeZone={selectedTimeZone}
|
||||||
|
availability={availability}
|
||||||
|
/>
|
||||||
|
<div className="py-4 flex justify-end">
|
||||||
|
<Link href="/availability">
|
||||||
|
<a className="mr-2 btn btn-white">Cancel</a>
|
||||||
|
</Link>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="bg-white shadow sm:rounded-lg">
|
<div className="bg-white shadow sm:rounded-lg">
|
||||||
<div className="px-4 py-5 sm:p-6">
|
<div className="px-4 py-5 sm:p-6">
|
||||||
<h3 className="text-lg mb-2 leading-6 font-medium text-gray-900">Delete this event type</h3>
|
<h3 className="text-lg mb-2 leading-6 font-medium text-gray-900">Delete this event type</h3>
|
||||||
|
@ -777,6 +986,11 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ req, query
|
||||||
availability: true,
|
availability: true,
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
|
periodType: true,
|
||||||
|
periodDays: true,
|
||||||
|
periodStartDate: true,
|
||||||
|
periodEndDate: true,
|
||||||
|
periodCountCalendarDays: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -853,10 +1067,15 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ req, query
|
||||||
|
|
||||||
availability.sort((a, b) => a.startTime - b.startTime);
|
availability.sort((a, b) => a.startTime - b.startTime);
|
||||||
|
|
||||||
|
const eventTypeObject = Object.assign({}, eventType, {
|
||||||
|
periodStartDate: eventType.periodStartDate?.toString() ?? null,
|
||||||
|
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
eventType,
|
eventType: eventTypeObject,
|
||||||
locationOptions,
|
locationOptions,
|
||||||
availability,
|
availability,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,424 +1,550 @@
|
||||||
import Head from 'next/head';
|
import Head from "next/head";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import prisma from '../../lib/prisma';
|
import prisma from "../../lib/prisma";
|
||||||
import Modal from '../../components/Modal';
|
import Modal from "../../components/Modal";
|
||||||
import Shell from '../../components/Shell';
|
import Shell from "../../components/Shell";
|
||||||
import {useRouter} from 'next/router';
|
import { useRouter } from "next/router";
|
||||||
import {useRef, useState} from 'react';
|
import { useRef, useState } from "react";
|
||||||
import {getSession, useSession} from 'next-auth/client';
|
import { getSession, useSession } from "next-auth/client";
|
||||||
import {ClockIcon, PlusIcon} from '@heroicons/react/outline';
|
import { ClockIcon, PlusIcon } from "@heroicons/react/outline";
|
||||||
|
|
||||||
export default function Availability(props) {
|
export default function Availability(props) {
|
||||||
const [ session, loading ] = useSession();
|
const [, loading] = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||||
const [showChangeTimesModal, setShowChangeTimesModal] = useState(false);
|
const [showChangeTimesModal, setShowChangeTimesModal] = useState(false);
|
||||||
const titleRef = useRef<HTMLInputElement>();
|
const titleRef = useRef<HTMLInputElement>();
|
||||||
const slugRef = useRef<HTMLInputElement>();
|
const slugRef = useRef<HTMLInputElement>();
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>();
|
const descriptionRef = useRef<HTMLTextAreaElement>();
|
||||||
const lengthRef = useRef<HTMLInputElement>();
|
const lengthRef = useRef<HTMLInputElement>();
|
||||||
const isHiddenRef = useRef<HTMLInputElement>();
|
const isHiddenRef = useRef<HTMLInputElement>();
|
||||||
|
|
||||||
const startHoursRef = useRef<HTMLInputElement>();
|
const startHoursRef = useRef<HTMLInputElement>();
|
||||||
const startMinsRef = useRef<HTMLInputElement>();
|
const startMinsRef = useRef<HTMLInputElement>();
|
||||||
const endHoursRef = useRef<HTMLInputElement>();
|
const endHoursRef = useRef<HTMLInputElement>();
|
||||||
const endMinsRef = useRef<HTMLInputElement>();
|
const endMinsRef = useRef<HTMLInputElement>();
|
||||||
const bufferHoursRef = useRef<HTMLInputElement>();
|
const bufferHoursRef = useRef<HTMLInputElement>();
|
||||||
const bufferMinsRef = useRef<HTMLInputElement>();
|
const bufferMinsRef = useRef<HTMLInputElement>();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="loader"></div>;
|
return <div className="loader"></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAddModal() {
|
||||||
|
setShowAddModal(!showAddModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleChangeTimesModal() {
|
||||||
|
setShowChangeTimesModal(!showChangeTimesModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSuccessModal = () => {
|
||||||
|
setSuccessModalOpen(false);
|
||||||
|
router.replace(router.asPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
function convertMinsToHrsMins(mins) {
|
||||||
|
let h = Math.floor(mins / 60);
|
||||||
|
let m = mins % 60;
|
||||||
|
h = h < 10 ? "0" + h : h;
|
||||||
|
m = m < 10 ? "0" + m : m;
|
||||||
|
return `${h}:${m}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createEventTypeHandler(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;
|
||||||
|
|
||||||
|
// TODO: Add validation
|
||||||
|
|
||||||
|
await fetch("/api/availability/eventtype", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: enteredTitle,
|
||||||
|
slug: enteredSlug,
|
||||||
|
description: enteredDescription,
|
||||||
|
length: enteredLength,
|
||||||
|
hidden: enteredIsHidden,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enteredTitle && enteredLength) {
|
||||||
|
router.replace(router.asPath);
|
||||||
|
toggleAddModal();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleAddModal() {
|
async function updateStartEndTimesHandler(event) {
|
||||||
setShowAddModal(!showAddModal);
|
event.preventDefault();
|
||||||
}
|
|
||||||
|
|
||||||
function toggleChangeTimesModal() {
|
const enteredStartHours = parseInt(startHoursRef.current.value);
|
||||||
setShowChangeTimesModal(!showChangeTimesModal);
|
const enteredStartMins = parseInt(startMinsRef.current.value);
|
||||||
}
|
const enteredEndHours = parseInt(endHoursRef.current.value);
|
||||||
|
const enteredEndMins = parseInt(endMinsRef.current.value);
|
||||||
|
const enteredBufferHours = parseInt(bufferHoursRef.current.value);
|
||||||
|
const enteredBufferMins = parseInt(bufferMinsRef.current.value);
|
||||||
|
|
||||||
const closeSuccessModal = () => { setSuccessModalOpen(false); router.replace(router.asPath); }
|
const startMins = enteredStartHours * 60 + enteredStartMins;
|
||||||
|
const endMins = enteredEndHours * 60 + enteredEndMins;
|
||||||
|
const bufferMins = enteredBufferHours * 60 + enteredBufferMins;
|
||||||
|
|
||||||
function convertMinsToHrsMins (mins) {
|
// TODO: Add validation
|
||||||
let h = Math.floor(mins / 60);
|
|
||||||
let m = mins % 60;
|
|
||||||
h = h < 10 ? '0' + h : h;
|
|
||||||
m = m < 10 ? '0' + m : m;
|
|
||||||
return `${h}:${m}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createEventTypeHandler(event) {
|
await fetch("/api/availability/day", {
|
||||||
event.preventDefault();
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ start: startMins, end: endMins, buffer: bufferMins }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const enteredTitle = titleRef.current.value;
|
setShowChangeTimesModal(false);
|
||||||
const enteredSlug = slugRef.current.value;
|
setSuccessModalOpen(true);
|
||||||
const enteredDescription = descriptionRef.current.value;
|
}
|
||||||
const enteredLength = lengthRef.current.value;
|
|
||||||
const enteredIsHidden = isHiddenRef.current.checked;
|
|
||||||
|
|
||||||
// TODO: Add validation
|
return (
|
||||||
|
<div>
|
||||||
const response = await fetch('/api/availability/eventtype', {
|
<Head>
|
||||||
method: 'POST',
|
<title>Availability | Calendso</title>
|
||||||
body: JSON.stringify({title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden}),
|
<link rel="icon" href="/favicon.ico" />
|
||||||
headers: {
|
</Head>
|
||||||
'Content-Type': 'application/json'
|
<Shell heading="Availability">
|
||||||
}
|
<div className="mb-4 sm:flex sm:items-center sm:justify-between">
|
||||||
});
|
<h3 className="text-lg leading-6 font-medium text-white">Event Types</h3>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
if (enteredTitle && enteredLength) {
|
<button onClick={toggleAddModal} type="button" className="btn-sm btn-white">
|
||||||
router.replace(router.asPath);
|
New event type
|
||||||
toggleAddModal();
|
</button>
|
||||||
}
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
async function updateStartEndTimesHandler(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const enteredStartHours = parseInt(startHoursRef.current.value);
|
|
||||||
const enteredStartMins = parseInt(startMinsRef.current.value);
|
|
||||||
const enteredEndHours = parseInt(endHoursRef.current.value);
|
|
||||||
const enteredEndMins = parseInt(endMinsRef.current.value);
|
|
||||||
const enteredBufferHours = parseInt(bufferHoursRef.current.value);
|
|
||||||
const enteredBufferMins = parseInt(bufferMinsRef.current.value);
|
|
||||||
|
|
||||||
const startMins = enteredStartHours * 60 + enteredStartMins;
|
|
||||||
const endMins = enteredEndHours * 60 + enteredEndMins;
|
|
||||||
const bufferMins = enteredBufferHours * 60 + enteredBufferMins;
|
|
||||||
|
|
||||||
// TODO: Add validation
|
|
||||||
|
|
||||||
const response = await fetch('/api/availability/day', {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify({start: startMins, end: endMins, buffer: bufferMins}),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowChangeTimesModal(false);
|
|
||||||
setSuccessModalOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return(
|
|
||||||
<div>
|
|
||||||
<Head>
|
|
||||||
<title>Availability | Calendso</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<Shell heading="Availability">
|
|
||||||
<div className="mb-4 sm:flex sm:items-center sm:justify-between">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-white">
|
|
||||||
Event Types
|
|
||||||
</h3>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<button onClick={toggleAddModal} type="button" className="btn-sm btn-white">
|
|
||||||
New event type
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col mb-8">
|
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
|
||||||
<div className="shadow overflow-hidden border-b border-gray-200 rounded-lg">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Description
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Length
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="relative px-6 py-3">
|
|
||||||
<span className="sr-only">Edit</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{props.types.map((eventType) =>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
||||||
{eventType.title}
|
|
||||||
{eventType.hidden &&
|
|
||||||
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
|
||||||
Hidden
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
{eventType.description}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
{eventType.length} minutes
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<Link href={"/" + props.user.username + "/" + eventType.slug}><a target="_blank" className="text-blue-600 hover:text-blue-900 mr-2">View</a></Link>
|
|
||||||
<Link href={"/availability/event/" + eventType.id}><a className="text-blue-600 hover:text-blue-900">Edit</a></Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex">
|
|
||||||
<div className="w-1/2 mr-2 bg-white shadow rounded-lg">
|
|
||||||
<div className="px-4 py-5 sm:p-6">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Change the start and end times of your day
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2 max-w-xl text-sm text-gray-500">
|
|
||||||
<p>
|
|
||||||
Currently, your day is set to start at {convertMinsToHrsMins(props.user.startTime)} and end at {convertMinsToHrsMins(props.user.endTime)}.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-primary">
|
|
||||||
Change available times
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-1/2 ml-2 bg-white shadow rounded-lg">
|
|
||||||
<div className="px-4 py-5 sm:p-6">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Something doesn't look right?
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2 max-w-xl text-sm text-gray-500">
|
|
||||||
<p>
|
|
||||||
Troubleshoot your availability to explore why your times are showing as they are.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<Link href="/availability/troubleshoot">
|
|
||||||
<a className="btn btn-primary">
|
|
||||||
Launch troubleshooter
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showAddModal &&
|
|
||||||
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
||||||
|
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
|
||||||
<div className="sm:flex sm:items-start mb-4">
|
|
||||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<PlusIcon className="h-6 w-6 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
|
||||||
Add a new event type
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Create a new event type for people to book times with.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form onSubmit={createEventTypeHandler}>
|
|
||||||
<div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input ref={titleRef} type="text" name="title" id="title" 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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="slug" className="block text-sm font-medium text-gray-700">URL</label>
|
|
||||||
<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">
|
|
||||||
{location.hostname}/{props.user.username}/
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
ref={slugRef}
|
|
||||||
type="text"
|
|
||||||
name="slug"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">Description</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<textarea ref={descriptionRef} name="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" placeholder="A quick video meeting."></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="length" className="block text-sm font-medium text-gray-700">Length</label>
|
|
||||||
<div className="mt-1 relative rounded-md shadow-sm">
|
|
||||||
<input ref={lengthRef} type="number" name="length" id="length" 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" />
|
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
|
|
||||||
minutes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="my-8">
|
|
||||||
<div className="relative flex items-start">
|
|
||||||
<div className="flex items-center h-5">
|
|
||||||
<input
|
|
||||||
ref={isHiddenRef}
|
|
||||||
id="ishidden"
|
|
||||||
name="ishidden"
|
|
||||||
type="checkbox"
|
|
||||||
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 text-sm">
|
|
||||||
<label htmlFor="ishidden" className="font-medium text-gray-700">
|
|
||||||
Hide this event type
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-500">Hide the event type from your page, so it can only be booked through it's URL.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* TODO: Add an error message when required input fields empty*/}
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<button type="submit" className="btn btn-primary">
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
<button onClick={toggleAddModal} type="button" className="btn btn-white mr-2">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{showChangeTimesModal &&
|
|
||||||
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
||||||
|
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
|
||||||
<div className="sm:flex sm:items-start mb-4">
|
|
||||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<ClockIcon className="h-6 w-6 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
|
||||||
Change your available times
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Set the start and end time of your day and a minimum buffer between your meetings.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form onSubmit={updateStartEndTimesHandler}>
|
|
||||||
<div className="flex mb-4">
|
|
||||||
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="hours" className="sr-only">Hours</label>
|
|
||||||
<input ref={startHoursRef} type="number" name="hours" id="hours" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="9" defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[0]} />
|
|
||||||
</div>
|
|
||||||
<span className="mx-2 pt-1">:</span>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="minutes" className="sr-only">Minutes</label>
|
|
||||||
<input ref={startMinsRef} type="number" name="minutes" id="minutes" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="30" defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[1]} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex mb-4">
|
|
||||||
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="hours" className="sr-only">Hours</label>
|
|
||||||
<input ref={endHoursRef} type="number" name="hours" id="hours" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="17" defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[0]} />
|
|
||||||
</div>
|
|
||||||
<span className="mx-2 pt-1">:</span>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="minutes" className="sr-only">Minutes</label>
|
|
||||||
<input ref={endMinsRef} type="number" name="minutes" id="minutes" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="30" defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[1]} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex mb-4">
|
|
||||||
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Buffer</label>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="hours" className="sr-only">Hours</label>
|
|
||||||
<input ref={bufferHoursRef} type="number" name="hours" id="hours" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="0" defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[0]} />
|
|
||||||
</div>
|
|
||||||
<span className="mx-2 pt-1">:</span>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="minutes" className="sr-only">Minutes</label>
|
|
||||||
<input ref={bufferMinsRef} type="number" name="minutes" id="minutes" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="10" defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[1]} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<button type="submit" className="btn btn-primary">
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-white mr-2">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Modal heading="Start and end times changed" description="The start and end times for your day have been changed successfully." open={successModalOpen} handleClose={closeSuccessModal} />
|
|
||||||
</Shell>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="flex flex-col mb-8">
|
||||||
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block max-w-full min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="shadow overflow-hidden border-b border-gray-200 rounded-lg">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Length
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="relative px-6 py-3">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{props.types.map((eventType) => (
|
||||||
|
<tr key={eventType}>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 align-top">
|
||||||
|
{eventType.title}
|
||||||
|
{eventType.hidden && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
||||||
|
Hidden
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-500 align-top">{eventType.description}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 align-top">
|
||||||
|
{eventType.length} minutes
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium align-top">
|
||||||
|
<Link href={"/" + props.user.username + "/" + eventType.slug}>
|
||||||
|
<a target="_blank" className="text-blue-600 hover:text-blue-900 mr-2">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href={"/availability/event/" + eventType.id}>
|
||||||
|
<a className="text-blue-600 hover:text-blue-900">Edit</a>
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-1/2 mr-2 bg-white shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Change the start and end times of your day
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2 max-w-xl text-sm text-gray-500">
|
||||||
|
<p>
|
||||||
|
Currently, your day is set to start at {convertMinsToHrsMins(props.user.startTime)} and end
|
||||||
|
at {convertMinsToHrsMins(props.user.endTime)}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-primary">
|
||||||
|
Change available times
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-1/2 ml-2 bg-white shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Something doesn't look right?
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2 max-w-xl text-sm text-gray-500">
|
||||||
|
<p>Troubleshoot your availability to explore why your times are showing as they are.</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Link href="/availability/troubleshoot">
|
||||||
|
<a className="btn btn-primary">Launch troubleshooter</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showAddModal && (
|
||||||
|
<div
|
||||||
|
className="fixed z-10 inset-0 overflow-y-auto"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true">
|
||||||
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||||
|
aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
||||||
|
<div className="sm:flex sm:items-start mb-4">
|
||||||
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<PlusIcon className="h-6 w-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||||
|
Add a new event type
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Create a new event type for people to book times with.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={createEventTypeHandler}>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
|
||||||
|
Title
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<input
|
||||||
|
ref={titleRef}
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
id="title"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="slug" className="block text-sm font-medium text-gray-700">
|
||||||
|
URL
|
||||||
|
</label>
|
||||||
|
<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">
|
||||||
|
{location.hostname}/{props.user.username}/
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
ref={slugRef}
|
||||||
|
type="text"
|
||||||
|
name="slug"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<textarea
|
||||||
|
ref={descriptionRef}
|
||||||
|
name="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"
|
||||||
|
placeholder="A quick video meeting."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="length" className="block text-sm font-medium text-gray-700">
|
||||||
|
Length
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
ref={lengthRef}
|
||||||
|
type="number"
|
||||||
|
name="length"
|
||||||
|
id="length"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
|
||||||
|
minutes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="my-8">
|
||||||
|
<div className="relative flex items-start">
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<input
|
||||||
|
ref={isHiddenRef}
|
||||||
|
id="ishidden"
|
||||||
|
name="ishidden"
|
||||||
|
type="checkbox"
|
||||||
|
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor="ishidden" className="font-medium text-gray-700">
|
||||||
|
Hide this event type
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Hide the event type from your page, so it can only be booked through its URL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* TODO: Add an error message when required input fields empty*/}
|
||||||
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
<button onClick={toggleAddModal} type="button" className="btn btn-white mr-2">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showChangeTimesModal && (
|
||||||
|
<div
|
||||||
|
className="fixed z-10 inset-0 overflow-y-auto"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true">
|
||||||
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||||
|
aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
||||||
|
<div className="sm:flex sm:items-start mb-4">
|
||||||
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<ClockIcon className="h-6 w-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||||
|
Change your available times
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Set the start and end time of your day and a minimum buffer between your meetings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={updateStartEndTimesHandler}>
|
||||||
|
<div className="flex mb-4">
|
||||||
|
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="hours" className="sr-only">
|
||||||
|
Hours
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={startHoursRef}
|
||||||
|
type="number"
|
||||||
|
name="hours"
|
||||||
|
id="hours"
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="9"
|
||||||
|
defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[0]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="mx-2 pt-1">:</span>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="minutes" className="sr-only">
|
||||||
|
Minutes
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={startMinsRef}
|
||||||
|
type="number"
|
||||||
|
name="minutes"
|
||||||
|
id="minutes"
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="30"
|
||||||
|
defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[1]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex mb-4">
|
||||||
|
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="hours" className="sr-only">
|
||||||
|
Hours
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={endHoursRef}
|
||||||
|
type="number"
|
||||||
|
name="hours"
|
||||||
|
id="hours"
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="17"
|
||||||
|
defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[0]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="mx-2 pt-1">:</span>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="minutes" className="sr-only">
|
||||||
|
Minutes
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={endMinsRef}
|
||||||
|
type="number"
|
||||||
|
name="minutes"
|
||||||
|
id="minutes"
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="30"
|
||||||
|
defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[1]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex mb-4">
|
||||||
|
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Buffer</label>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="hours" className="sr-only">
|
||||||
|
Hours
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={bufferHoursRef}
|
||||||
|
type="number"
|
||||||
|
name="hours"
|
||||||
|
id="hours"
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="0"
|
||||||
|
defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[0]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="mx-2 pt-1">:</span>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="minutes" className="sr-only">
|
||||||
|
Minutes
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={bufferMinsRef}
|
||||||
|
type="number"
|
||||||
|
name="minutes"
|
||||||
|
id="minutes"
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
placeholder="10"
|
||||||
|
defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[1]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-white mr-2">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Modal
|
||||||
|
heading="Start and end times changed"
|
||||||
|
description="The start and end times for your day have been changed successfully."
|
||||||
|
open={successModalOpen}
|
||||||
|
handleClose={closeSuccessModal}
|
||||||
|
/>
|
||||||
|
</Shell>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
const session = await getSession(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 = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: session.user.email,
|
email: session.user.email,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true,
|
endTime: true,
|
||||||
bufferTime: true
|
bufferTime: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const types = await prisma.eventType.findMany({
|
const types = await prisma.eventType.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
description: true,
|
description: true,
|
||||||
length: true,
|
length: true,
|
||||||
hidden: true
|
hidden: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
props: {user, types}, // will be passed to the page component as props
|
props: { user, types }, // will be passed to the page component as props
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
653
pages/index.tsx
653
pages/index.tsx
|
@ -1,353 +1,368 @@
|
||||||
import Head from 'next/head';
|
import Head from "next/head";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import prisma from '../lib/prisma';
|
import prisma from "../lib/prisma";
|
||||||
import Shell from '../components/Shell';
|
import Shell from "../components/Shell";
|
||||||
import {getSession, useSession} from 'next-auth/client';
|
import { getSession, useSession } from "next-auth/client";
|
||||||
import {CheckIcon, ClockIcon, InformationCircleIcon} from '@heroicons/react/outline';
|
import { CheckIcon, ClockIcon, InformationCircleIcon } from "@heroicons/react/outline";
|
||||||
import DonateBanner from '../components/DonateBanner';
|
import DonateBanner from "../components/DonateBanner";
|
||||||
|
|
||||||
function classNames(...classes) {
|
function classNames(...classes) {
|
||||||
return classes.filter(Boolean).join(' ')
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home(props) {
|
export default function Home(props) {
|
||||||
const [session, loading] = useSession();
|
const [session, loading] = useSession();
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="loader"></div>;
|
return <div className="loader"></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertMinsToHrsMins(mins) {
|
function convertMinsToHrsMins(mins) {
|
||||||
let h = Math.floor(mins / 60);
|
let h = Math.floor(mins / 60);
|
||||||
let m = mins % 60;
|
let m = mins % 60;
|
||||||
h = h < 10 ? '0' + h : h;
|
h = h < 10 ? "0" + h : h;
|
||||||
m = m < 10 ? '0' + m : m;
|
m = m < 10 ? "0" + m : m;
|
||||||
return `${h}:${m}`;
|
return `${h}:${m}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ name: 'Event Types', stat: props.eventTypeCount },
|
{ name: "Event Types", stat: props.eventTypeCount },
|
||||||
{ name: 'Integrations', stat: props.integrationCount },
|
{ name: "Integrations", stat: props.integrationCount },
|
||||||
{ name: 'Available Hours', stat: Math.round(((props.user.endTime - props.user.startTime) / 60) * 100) / 100 + ' hours' },
|
{
|
||||||
|
name: "Available Hours",
|
||||||
|
stat: Math.round(((props.user.endTime - props.user.startTime) / 60) * 100) / 100 + " hours",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let timeline = [];
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
timeline = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: "Add your first",
|
||||||
|
target: "integration",
|
||||||
|
href: "/integrations",
|
||||||
|
icon: props.integrationCount != 0 ? CheckIcon : InformationCircleIcon,
|
||||||
|
iconBackground: props.integrationCount != 0 ? "bg-green-400" : "bg-gray-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: "Add one or more",
|
||||||
|
target: "event types",
|
||||||
|
href: "/availability",
|
||||||
|
icon: props.eventTypeCount != 0 ? CheckIcon : InformationCircleIcon,
|
||||||
|
iconBackground: props.eventTypeCount != 0 ? "bg-green-400" : "bg-gray-400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
content: "Complete your",
|
||||||
|
target: "profile",
|
||||||
|
href: "/settings/profile",
|
||||||
|
icon: session.user.image ? CheckIcon : InformationCircleIcon,
|
||||||
|
iconBackground: session.user.image ? "bg-green-400" : "bg-gray-400",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
timeline = [];
|
||||||
|
}
|
||||||
|
|
||||||
let timeline = [];
|
return (
|
||||||
|
<div>
|
||||||
|
<Head>
|
||||||
|
<title>Calendso</title>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
if (session) {
|
<Shell heading="Dashboard">
|
||||||
timeline = [
|
<div className="md:grid grid-cols-3 gap-4">
|
||||||
{
|
<div className="col-span-2">
|
||||||
id: 1,
|
<div className="rounded-lg bg-white shadow dark:bg-gray-800">
|
||||||
content: 'Add your first',
|
<div className="pt-5 pb-2 px-6 sm:flex sm:items-center sm:justify-between">
|
||||||
target: 'integration',
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Your stats</h3>
|
||||||
href: '/integrations',
|
|
||||||
icon: props.integrationCount != 0 ? CheckIcon : InformationCircleIcon,
|
|
||||||
iconBackground: props.integrationCount != 0 ? 'bg-green-400' : 'bg-gray-400',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
content: 'Add one or more',
|
|
||||||
target: 'event types',
|
|
||||||
href: '/availability',
|
|
||||||
icon: props.eventTypeCount != 0 ? CheckIcon : InformationCircleIcon,
|
|
||||||
iconBackground: props.eventTypeCount != 0 ? 'bg-green-400' : 'bg-gray-400',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
content: 'Complete your',
|
|
||||||
target: 'profile',
|
|
||||||
href: '/settings/profile',
|
|
||||||
icon: session.user.image ? CheckIcon : InformationCircleIcon,
|
|
||||||
iconBackground: session.user.image ? 'bg-green-400' : 'bg-gray-400',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
timeline = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Head>
|
|
||||||
<title>Calendso</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<Shell heading="Dashboard">
|
|
||||||
<div className="md:grid grid-cols-3 gap-4">
|
|
||||||
<div className="col-span-2">
|
|
||||||
<div className="rounded-lg bg-white shadow dark:bg-gray-800">
|
|
||||||
<div className="pt-5 pb-2 px-6 sm:flex sm:items-center sm:justify-between">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Your stats</h3>
|
|
||||||
</div>
|
|
||||||
<dl className="grid grid-cols-1 overflow-hidden divide-y divide-gray-200 dark:divide-gray-900 md:grid-cols-3 md:divide-y-0 md:divide-x">
|
|
||||||
{stats.map((item) => (
|
|
||||||
<div key={item.name} className="px-4 py-5 sm:p-6">
|
|
||||||
<dt className="text-base font-normal dark:text-white text-gray-900">{item.name}</dt>
|
|
||||||
<dd className="mt-1 flex justify-between items-baseline md:block lg:flex">
|
|
||||||
<div className="flex items-baseline text-2xl font-semibold text-blue-600">
|
|
||||||
{item.stat}
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 bg-white shadow dark:bg-gray-800 overflow-hidden rounded-md">
|
<dl className="grid grid-cols-1 overflow-hidden divide-y divide-gray-200 dark:divide-gray-900 md:grid-cols-3 md:divide-y-0 md:divide-x">
|
||||||
<div className="pt-5 pb-2 px-6 sm:flex sm:items-center sm:justify-between">
|
{stats.map((item) => (
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
<div key={item.name} className="px-4 py-5 sm:p-6">
|
||||||
Your event types
|
<dt className="text-base font-normal dark:text-white text-gray-900">{item.name}</dt>
|
||||||
</h3>
|
<dd className="mt-1 flex justify-between items-baseline md:block lg:flex">
|
||||||
</div>
|
<div className="flex items-baseline text-2xl font-semibold text-blue-600">
|
||||||
<ul className="divide-y divide-gray-200">
|
{item.stat}
|
||||||
{props.eventTypes.map((type) => (
|
</div>
|
||||||
<li key={type.id}>
|
</dd>
|
||||||
<div className="px-4 py-4 flex items-center sm:px-6">
|
</div>
|
||||||
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
|
))}
|
||||||
<div className="truncate">
|
</dl>
|
||||||
<div className="flex text-sm">
|
</div>
|
||||||
<p className="font-medium text-blue-600 truncate">{type.title}</p>
|
<div className="mt-8 bg-white shadow dark:bg-gray-800 overflow-hidden rounded-md">
|
||||||
<p className="ml-1 flex-shrink-0 font-normal text-gray-500">
|
<div className="pt-5 pb-2 px-6 sm:flex sm:items-center sm:justify-between">
|
||||||
in {type.description}
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||||
</p>
|
Your event types
|
||||||
</div>
|
</h3>
|
||||||
<div className="mt-2 flex">
|
</div>
|
||||||
<div className="flex items-center text-sm text-gray-500">
|
<ul className="divide-y divide-gray-200">
|
||||||
<ClockIcon
|
{props.eventTypes.map((type) => (
|
||||||
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
|
<li key={type.id}>
|
||||||
aria-hidden="true"
|
<div className="px-4 py-4 flex items-center sm:px-6">
|
||||||
/>
|
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<p>{type.length} minutes</p>
|
<div className="truncate">
|
||||||
</div>
|
<div className="flex text-sm">
|
||||||
|
<p className="flex-shrink-0 font-medium text-blue-600 truncate">{type.title}</p>
|
||||||
|
<p className="ml-1 font-normal text-gray-500 truncate">in {type.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex">
|
||||||
|
<div className="flex items-center text-sm text-gray-500">
|
||||||
|
<ClockIcon
|
||||||
|
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p>{type.length} minutes</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-5 flex-shrink-0">
|
|
||||||
<Link href={"/" + session.user.username + "/" + type.slug}>
|
|
||||||
<a target="_blank" className="text-blue-600 hover:text-blue-900 mr-2 font-medium">
|
|
||||||
View
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{props.eventTypes.length == 0 && (
|
|
||||||
<div className="text-center text-gray-400 py-12">
|
|
||||||
<p>You haven't created any event types.</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="ml-5 flex-shrink-0">
|
||||||
</ul>
|
<Link href={"/availability/event/" + type.id}>
|
||||||
</div>
|
<a className="text-blue-600 hover:text-blue-900 mr-2 font-medium">Edit</a>
|
||||||
<div className="mt-8 bg-white dark:bg-gray-800 shadow overflow-hidden rounded-md p-6 mb-8 md:mb-0">
|
</Link>
|
||||||
<div className="md:flex">
|
<Link href={"/" + session.user.username + "/" + type.slug}>
|
||||||
<div className="md:w-1/2 self-center mb-8 md:mb-0">
|
<a target="_blank" className="text-blue-600 hover:text-blue-900 mr-2 font-medium">
|
||||||
<h2 className="text-2xl dark:text-white font-semibold">Getting started</h2>
|
View
|
||||||
<p className="text-gray-600 dark:text-gray-200 text-sm">
|
</a>
|
||||||
Steps you should take to get started with Calendso.
|
</Link>
|
||||||
</p>
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{props.eventTypes.length == 0 && (
|
||||||
|
<div className="text-center text-gray-400 py-12">
|
||||||
|
<p>You haven't created any event types.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-1/2">
|
)}
|
||||||
<div className="flow-root">
|
</ul>
|
||||||
<ul className="-mb-8">
|
</div>
|
||||||
{timeline.map((event, eventIdx) => (
|
<div className="mt-8 bg-white dark:bg-gray-800 shadow overflow-hidden rounded-md p-6 mb-8 md:mb-0">
|
||||||
<li key={event.id}>
|
<div className="md:flex">
|
||||||
<div className="relative pb-8">
|
<div className="md:w-1/2 self-center mb-8 md:mb-0">
|
||||||
{eventIdx !== timeline.length - 1 ? (
|
<h2 className="text-2xl dark:text-white font-semibold">Getting started</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-200 text-sm">
|
||||||
|
Steps you should take to get started with Calendso.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="md:w-1/2">
|
||||||
|
<div className="flow-root">
|
||||||
|
<ul className="-mb-8">
|
||||||
|
{timeline.map((event, eventIdx) => (
|
||||||
|
<li key={event.id}>
|
||||||
|
<div className="relative pb-8">
|
||||||
|
{eventIdx !== timeline.length - 1 ? (
|
||||||
|
<span
|
||||||
|
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200 dark:bg-gray-900"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="relative flex space-x-3">
|
||||||
|
<div>
|
||||||
<span
|
<span
|
||||||
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200 dark:bg-gray-900"
|
className={classNames(
|
||||||
aria-hidden="true"
|
event.iconBackground,
|
||||||
/>
|
"h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white dark:ring-gray-800"
|
||||||
) : null}
|
)}>
|
||||||
<div className="relative flex space-x-3">
|
<event.icon className="h-5 w-5 text-white" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
|
||||||
<div>
|
<div>
|
||||||
<span
|
<p className="text-sm text-gray-500 dark:text-gray-200">
|
||||||
className={classNames(
|
{event.content}{" "}
|
||||||
event.iconBackground,
|
<Link href={event.href}>
|
||||||
"h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white dark:ring-gray-800"
|
<a className="font-medium dark:text-white text-gray-900">
|
||||||
)}>
|
{event.target}
|
||||||
<event.icon className="h-5 w-5 text-white" aria-hidden="true" />
|
</a>
|
||||||
</span>
|
</Link>
|
||||||
</div>
|
</p>
|
||||||
<div className="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-200">
|
|
||||||
{event.content}{" "}
|
|
||||||
<Link href={event.href}>
|
|
||||||
<a className="font-medium dark:text-white text-gray-900">
|
|
||||||
{event.target}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 md:py-7 sm:px-6">
|
|
||||||
<div className="mb-4 sm:flex sm:items-center sm:justify-between">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Your day</h3>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<Link href="/availability">
|
|
||||||
<a className="text-sm text-gray-400">Configure</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-2xl font-semibold text-gray-600 dark:text-white">
|
|
||||||
Offering time slots between{" "}
|
|
||||||
<span className="text-blue-600">{convertMinsToHrsMins(props.user.startTime)}</span> and{" "}
|
|
||||||
<span className="text-blue-600">{convertMinsToHrsMins(props.user.endTime)}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 md:py-7 sm:px-6">
|
|
||||||
<div className="mb-8 sm:flex sm:items-center sm:justify-between">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
|
||||||
Your integrations
|
|
||||||
</h3>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<Link href="/integrations">
|
|
||||||
<a className="text-sm text-gray-400">View more</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul className="divide-y divide-gray-200">
|
|
||||||
{props.credentials.map((integration) => (
|
|
||||||
<li className="pb-4 flex">
|
|
||||||
{integration.type == "google_calendar" && (
|
|
||||||
<img
|
|
||||||
className="h-10 w-10 mr-2"
|
|
||||||
src="integrations/google-calendar.png"
|
|
||||||
alt="Google Calendar"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{integration.type == "office365_calendar" && (
|
|
||||||
<img
|
|
||||||
className="h-10 w-10 mr-2"
|
|
||||||
src="integrations/office-365.png"
|
|
||||||
alt="Office 365 / Outlook.com Calendar"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{integration.type == "zoom_video" && (
|
|
||||||
<img className="h-10 w-10 mr-2" src="integrations/zoom.png" alt="Zoom" />
|
|
||||||
)}
|
|
||||||
<div className="ml-3">
|
|
||||||
{integration.type == "office365_calendar" && (
|
|
||||||
<p className="text-sm font-medium text-gray-900">
|
|
||||||
Office 365 / Outlook.com Calendar
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{integration.type == "google_calendar" && (
|
|
||||||
<p className="text-sm font-medium text-gray-900">Google Calendar</p>
|
|
||||||
)}
|
|
||||||
{integration.type == "zoom_video" && (
|
|
||||||
<p className="text-sm font-medium text-gray-900">Zoom</p>
|
|
||||||
)}
|
|
||||||
{integration.type.endsWith("_calendar") && (
|
|
||||||
<p className="text-sm text-gray-500">Calendar Integration</p>
|
|
||||||
)}
|
|
||||||
{integration.type.endsWith("_video") && (
|
|
||||||
<p className="text-sm text-gray-500">Video Conferencing</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{props.credentials.length == 0 && (
|
|
||||||
<div className="text-center text-gray-400 py-2">
|
|
||||||
<p>You haven't added any integrations.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 md:py-7 sm:px-6">
|
|
||||||
<div className="mb-4 sm:flex sm:items-center sm:justify-between">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
|
||||||
Your event types
|
|
||||||
</h3>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<Link href="/availability">
|
|
||||||
<a className="text-sm text-gray-400">View more</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul className="divide-y divide-gray-200">
|
|
||||||
{props.eventTypes.map((type) => (
|
|
||||||
<li
|
|
||||||
key={type.id}
|
|
||||||
className="relative py-5 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600">
|
|
||||||
<div className="flex justify-between space-x-3">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<a href="#" className="block focus:outline-none">
|
|
||||||
<span className="absolute inset-0" aria-hidden="true" />
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
||||||
{type.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 truncate">{type.description}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<span className="flex-shrink-0 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
{type.length} minutes
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{props.eventTypes.length == 0 && (
|
|
||||||
<div className="text-center text-gray-400 py-2">
|
|
||||||
<p>You haven't created any event types.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<DonateBanner />
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 md:py-7 sm:px-6">
|
||||||
</Shell>
|
<div className="mb-4 sm:flex sm:items-center sm:justify-between">
|
||||||
</div>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Your day</h3>
|
||||||
);
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<Link href="/availability">
|
||||||
|
<a className="text-sm text-gray-400">Configure</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-semibold text-gray-600 dark:text-white">
|
||||||
|
Offering time slots between{" "}
|
||||||
|
<span className="text-blue-600">{convertMinsToHrsMins(props.user.startTime)}</span> and{" "}
|
||||||
|
<span className="text-blue-600">{convertMinsToHrsMins(props.user.endTime)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 md:py-7 sm:px-6">
|
||||||
|
<div className="mb-8 sm:flex sm:items-center sm:justify-between">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||||
|
Your integrations
|
||||||
|
</h3>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<Link href="/integrations">
|
||||||
|
<a className="text-sm text-gray-400">View more</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="divide-y divide-gray-200">
|
||||||
|
{props.credentials.map((integration) => (
|
||||||
|
<li key={integration.type} className="pb-4 flex">
|
||||||
|
{integration.type == "google_calendar" && (
|
||||||
|
<img
|
||||||
|
className="h-10 w-10 mr-2"
|
||||||
|
src="integrations/google-calendar.png"
|
||||||
|
alt="Google Calendar"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{integration.type == "office365_calendar" && (
|
||||||
|
<img
|
||||||
|
className="h-10 w-10 mr-2"
|
||||||
|
src="integrations/office-365.png"
|
||||||
|
alt="Office 365 / Outlook.com Calendar"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{integration.type == "zoom_video" && (
|
||||||
|
<img className="h-10 w-10 mr-2" src="integrations/zoom.png" alt="Zoom" />
|
||||||
|
)}
|
||||||
|
<div className="ml-3">
|
||||||
|
{integration.type == "office365_calendar" && (
|
||||||
|
<p className="text-sm font-medium text-gray-900">Office 365 / Outlook.com Calendar</p>
|
||||||
|
)}
|
||||||
|
{integration.type == "google_calendar" && (
|
||||||
|
<p className="text-sm font-medium text-gray-900">Google Calendar</p>
|
||||||
|
)}
|
||||||
|
{integration.type == "zoom_video" && (
|
||||||
|
<p className="text-sm font-medium text-gray-900">Zoom</p>
|
||||||
|
)}
|
||||||
|
{integration.type.endsWith("_calendar") && (
|
||||||
|
<p className="text-sm text-gray-500">Calendar Integration</p>
|
||||||
|
)}
|
||||||
|
{integration.type.endsWith("_video") && (
|
||||||
|
<p className="text-sm text-gray-500">Video Conferencing</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{props.credentials.length == 0 && (
|
||||||
|
<div className="text-center text-gray-400 py-2">
|
||||||
|
<p>You haven't added any integrations.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 md:py-7 sm:px-6">
|
||||||
|
<div className="mb-4 sm:flex sm:items-center sm:justify-between">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||||
|
Your event types
|
||||||
|
</h3>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<Link href="/availability">
|
||||||
|
<a className="text-sm text-gray-400">View more</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="divide-y divide-gray-200">
|
||||||
|
{props.eventTypes.map((type) => (
|
||||||
|
<li
|
||||||
|
key={type.id}
|
||||||
|
className="relative py-5 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600">
|
||||||
|
<div className="flex justify-between space-x-3">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<a href="#" className="block focus:outline-none">
|
||||||
|
<span className="absolute inset-0" aria-hidden="true" />
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{type.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 truncate">{type.description}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<span className="flex-shrink-0 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{type.length} minutes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{props.eventTypes.length == 0 && (
|
||||||
|
<div className="text-center text-gray-400 py-2">
|
||||||
|
<p>You haven't created any event types.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DonateBanner />
|
||||||
|
</Shell>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
|
|
||||||
let user = [];
|
let user = [];
|
||||||
let credentials = [];
|
let credentials = [];
|
||||||
let eventTypes = [];
|
let eventTypes = [];
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
user = await prisma.user.findFirst({
|
user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: session.user.email,
|
email: session.user.email,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true
|
endTime: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
credentials = await prisma.credential.findMany({
|
credentials = await prisma.credential.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
type: true
|
type: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
eventTypes = await prisma.eventType.findMany({
|
eventTypes = (
|
||||||
where: {
|
await prisma.eventType.findMany({
|
||||||
userId: session.user.id,
|
where: {
|
||||||
}
|
userId: session.user.id,
|
||||||
});
|
},
|
||||||
}
|
})
|
||||||
return {
|
).map((eventType) => {
|
||||||
props: { user, credentials, eventTypes, eventTypeCount: eventTypes.length, integrationCount: credentials.length }, // will be passed to the page component as props
|
return {
|
||||||
}
|
...eventType,
|
||||||
}
|
periodStartDate: eventType.periodStartDate?.toString() ?? null,
|
||||||
|
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
user,
|
||||||
|
credentials,
|
||||||
|
eventTypes,
|
||||||
|
eventTypeCount: eventTypes.length,
|
||||||
|
integrationCount: credentials.length,
|
||||||
|
}, // will be passed to the page component as props
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { GetServerSideProps } from "next";
|
import { GetServerSideProps } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import prisma from "../../lib/prisma";
|
import prisma, { whereAndSelect } from "@lib/prisma";
|
||||||
import Modal from "../../components/Modal";
|
import Modal from "../../components/Modal";
|
||||||
import Shell from "../../components/Shell";
|
import Shell from "../../components/Shell";
|
||||||
import SettingsShell from "../../components/Settings";
|
import SettingsShell from "../../components/Settings";
|
||||||
import Avatar from "../../components/Avatar";
|
import Avatar from "../../components/Avatar";
|
||||||
import { getSession } from "next-auth/client";
|
import { getSession } from "next-auth/client";
|
||||||
|
import Select from "react-select";
|
||||||
import TimezoneSelect from "react-timezone-select";
|
import TimezoneSelect from "react-timezone-select";
|
||||||
import { UsernameInput } from "../../components/ui/UsernameInput";
|
import { UsernameInput } from "../../components/ui/UsernameInput";
|
||||||
import ErrorAlert from "../../components/ui/alerts/Error";
|
import ErrorAlert from "../../components/ui/alerts/Error";
|
||||||
|
@ -18,12 +19,25 @@ export default function Settings(props) {
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>();
|
const descriptionRef = useRef<HTMLTextAreaElement>();
|
||||||
const avatarRef = useRef<HTMLInputElement>();
|
const avatarRef = useRef<HTMLInputElement>();
|
||||||
const hideBrandingRef = useRef<HTMLInputElement>();
|
const hideBrandingRef = useRef<HTMLInputElement>();
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState({ value: "" });
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
|
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
|
||||||
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState(props.user.weekStart || "Sunday");
|
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: "" });
|
||||||
|
|
||||||
const [hasErrors, setHasErrors] = useState(false);
|
const [hasErrors, setHasErrors] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
|
const themeOptions = [
|
||||||
|
{ value: "light", label: "Light" },
|
||||||
|
{ value: "dark", label: "Dark" },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedTheme(
|
||||||
|
props.user.theme ? themeOptions.find((theme) => theme.value === props.user.theme) : null
|
||||||
|
);
|
||||||
|
setSelectedWeekStartDay({ value: props.user.weekStart, label: props.user.weekStart });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const closeSuccessModal = () => {
|
const closeSuccessModal = () => {
|
||||||
setSuccessModalOpen(false);
|
setSuccessModalOpen(false);
|
||||||
};
|
};
|
||||||
|
@ -43,7 +57,7 @@ export default function Settings(props) {
|
||||||
const enteredDescription = descriptionRef.current.value;
|
const enteredDescription = descriptionRef.current.value;
|
||||||
const enteredAvatar = avatarRef.current.value;
|
const enteredAvatar = avatarRef.current.value;
|
||||||
const enteredTimeZone = selectedTimeZone.value;
|
const enteredTimeZone = selectedTimeZone.value;
|
||||||
const enteredWeekStartDay = selectedWeekStartDay;
|
const enteredWeekStartDay = selectedWeekStartDay.value;
|
||||||
const enteredHideBranding = hideBrandingRef.current.checked;
|
const enteredHideBranding = hideBrandingRef.current.checked;
|
||||||
|
|
||||||
// TODO: Add validation
|
// TODO: Add validation
|
||||||
|
@ -58,6 +72,7 @@ export default function Settings(props) {
|
||||||
timeZone: enteredTimeZone,
|
timeZone: enteredTimeZone,
|
||||||
weekStart: enteredWeekStartDay,
|
weekStart: enteredWeekStartDay,
|
||||||
hideBranding: enteredHideBranding,
|
hideBranding: enteredHideBranding,
|
||||||
|
theme: selectedTheme ? selectedTheme.value : null,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -124,9 +139,8 @@ export default function Settings(props) {
|
||||||
name="about"
|
name="about"
|
||||||
placeholder="A little something about yourself."
|
placeholder="A little something about yourself."
|
||||||
rows={3}
|
rows={3}
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">
|
defaultValue={props.user.bio}
|
||||||
{props.user.bio}
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"></textarea>
|
||||||
</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -147,14 +161,48 @@ export default function Settings(props) {
|
||||||
First Day of Week
|
First Day of Week
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<select
|
<Select
|
||||||
id="weekStart"
|
id="weekStart"
|
||||||
value={selectedWeekStartDay}
|
value={selectedWeekStartDay}
|
||||||
onChange={(e) => setSelectedWeekStartDay(e.target.value)}
|
onChange={setSelectedWeekStartDay}
|
||||||
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"
|
||||||
<option value="Sunday">Sunday</option>
|
options={[
|
||||||
<option value="Monday">Monday</option>
|
{ value: "Sunday", label: "Sunday" },
|
||||||
</select>
|
{ value: "Monday", label: "Monday" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="theme" className="block text-sm font-medium text-gray-700">
|
||||||
|
Single Theme
|
||||||
|
</label>
|
||||||
|
<div className="my-1">
|
||||||
|
<Select
|
||||||
|
id="theme"
|
||||||
|
isDisabled={!selectedTheme}
|
||||||
|
defaultValue={selectedTheme || themeOptions[0]}
|
||||||
|
onChange={setSelectedTheme}
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
options={themeOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex items-start">
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<input
|
||||||
|
id="theme-adjust-os"
|
||||||
|
name="theme-adjust-os"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])}
|
||||||
|
defaultChecked={!selectedTheme}
|
||||||
|
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor="theme-adjust-os" className="font-medium text-gray-700">
|
||||||
|
Automatically adjust theme based on invitee preferences
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -257,22 +305,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await whereAndSelect(
|
||||||
where: {
|
prisma.user.findFirst,
|
||||||
email: session.user.email,
|
{
|
||||||
|
id: session.user.id,
|
||||||
},
|
},
|
||||||
select: {
|
["id", "username", "name", "email", "bio", "avatar", "timeZone", "weekStart", "hideBranding", "theme"]
|
||||||
id: true,
|
);
|
||||||
username: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
bio: true,
|
|
||||||
avatar: true,
|
|
||||||
timeZone: true,
|
|
||||||
weekStart: true,
|
|
||||||
hideBranding: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { user }, // will be passed to the page component as props
|
props: { user }, // will be passed to the page component as props
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import prisma from "../lib/prisma";
|
import prisma, { whereAndSelect } from "../lib/prisma";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { CheckIcon } from "@heroicons/react/outline";
|
import { CheckIcon } from "@heroicons/react/outline";
|
||||||
|
@ -11,6 +11,7 @@ import toArray from "dayjs/plugin/toArray";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import { createEvent } from "ics";
|
import { createEvent } from "ics";
|
||||||
import { getEventName } from "../lib/event";
|
import { getEventName } from "../lib/event";
|
||||||
|
import Theme from "@components/Theme";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(toArray);
|
dayjs.extend(toArray);
|
||||||
|
@ -22,6 +23,7 @@ export default function Success(props) {
|
||||||
|
|
||||||
const [is24h, setIs24h] = useState(false);
|
const [is24h, setIs24h] = useState(false);
|
||||||
const [date, setDate] = useState(dayjs.utc(router.query.date));
|
const [date, setDate] = useState(dayjs.utc(router.query.date));
|
||||||
|
const { isReady } = Theme(props.user.theme);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
|
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
|
||||||
|
@ -31,7 +33,7 @@ export default function Success(props) {
|
||||||
const eventName = getEventName(name, props.eventType.title, props.eventType.eventName);
|
const eventName = getEventName(name, props.eventType.title, props.eventType.eventName);
|
||||||
|
|
||||||
function eventLink(): string {
|
function eventLink(): string {
|
||||||
let optional = {};
|
const optional = {};
|
||||||
if (location) {
|
if (location) {
|
||||||
optional["location"] = location;
|
optional["location"] = location;
|
||||||
}
|
}
|
||||||
|
@ -57,187 +59,186 @@ export default function Success(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
isReady && (
|
||||||
<Head>
|
<div>
|
||||||
<title>Booking Confirmed | {eventName} | Calendso</title>
|
<Head>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<title>Booking Confirmed | {eventName} | Calendso</title>
|
||||||
</Head>
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<main className="max-w-3xl mx-auto my-24">
|
</Head>
|
||||||
<div className="fixed z-10 inset-0 overflow-y-auto">
|
<main className="max-w-3xl mx-auto my-24">
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||||
<div className="fixed inset-0 my-4 sm:my-0 transition-opacity" aria-hidden="true">
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
<div className="fixed inset-0 my-4 sm:my-0 transition-opacity" aria-hidden="true">
|
||||||
​
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
</span>
|
​
|
||||||
<div
|
</span>
|
||||||
className="inline-block align-bottom dark:bg-gray-800 bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
|
<div
|
||||||
role="dialog"
|
className="inline-block align-bottom dark:bg-gray-800 bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
|
||||||
aria-modal="true"
|
role="dialog"
|
||||||
aria-labelledby="modal-headline">
|
aria-modal="true"
|
||||||
<div>
|
aria-labelledby="modal-headline">
|
||||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
<div>
|
||||||
<CheckIcon className="h-6 w-6 text-green-600" />
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||||
</div>
|
<CheckIcon className="h-6 w-6 text-green-600" />
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
|
||||||
<h3
|
|
||||||
className="text-lg leading-6 font-medium dark:text-white text-gray-900"
|
|
||||||
id="modal-headline">
|
|
||||||
Booking confirmed
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
|
||||||
You are scheduled in with {props.user.name || props.user.username}.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 border-t border-b dark:border-gray-900 py-4">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h2 className="text-lg font-medium text-gray-600 dark:text-gray-100 mb-2">
|
<h3
|
||||||
{eventName}
|
className="text-lg leading-6 font-medium dark:text-white text-gray-900"
|
||||||
</h2>
|
id="modal-headline">
|
||||||
<p className="text-gray-500 dark:text-gray-50 mb-1">
|
Booking confirmed
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
</h3>
|
||||||
{props.eventType.length} minutes
|
<div className="mt-2">
|
||||||
</p>
|
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
{location && (
|
You are scheduled in with {props.user.name || props.user.username}.
|
||||||
<p className="text-gray-500 mb-1">
|
|
||||||
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
|
||||||
{location}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
</div>
|
||||||
<p className="text-gray-500 dark:text-gray-50">
|
<div className="mt-4 border-t border-b dark:border-gray-900 py-4">
|
||||||
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<h2 className="text-lg font-medium text-gray-600 dark:text-gray-100 mb-2">
|
||||||
{date.format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")}
|
{eventName}
|
||||||
</p>
|
</h2>
|
||||||
|
<p className="text-gray-500 dark:text-gray-50 mb-1">
|
||||||
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
|
{props.eventType.length} minutes
|
||||||
|
</p>
|
||||||
|
{location && (
|
||||||
|
<p className="text-gray-500 mb-1">
|
||||||
|
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
|
{location}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-gray-500 dark:text-gray-50">
|
||||||
|
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
|
{date.format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-5 sm:mt-0 pt-2 text-center">
|
||||||
<div className="mt-5 sm:mt-0 pt-2 text-center">
|
<span className="font-medium text-gray-500 dark:text-gray-50">Add to your calendar</span>
|
||||||
<span className="font-medium text-gray-500 dark:text-gray-50">Add to your calendar</span>
|
<div className="flex mt-2">
|
||||||
<div className="flex mt-2">
|
<Link
|
||||||
<Link
|
href={
|
||||||
href={
|
`https://calendar.google.com/calendar/r/eventedit?dates=${date
|
||||||
`https://calendar.google.com/calendar/r/eventedit?dates=${date
|
.utc()
|
||||||
.utc()
|
.format("YYYYMMDDTHHmmss[Z]")}/${date
|
||||||
.format("YYYYMMDDTHHmmss[Z]")}/${date
|
.add(props.eventType.length, "minute")
|
||||||
.add(props.eventType.length, "minute")
|
.utc()
|
||||||
.utc()
|
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
||||||
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
props.eventType.description
|
||||||
props.eventType.description
|
}` + (location ? "&location=" + encodeURIComponent(location) : "")
|
||||||
}` + (location ? "&location=" + encodeURIComponent(location) : "")
|
}>
|
||||||
}>
|
<a className="mx-2 btn-wide btn-white">
|
||||||
<a className="mx-2 btn-wide btn-white">
|
<svg
|
||||||
<svg
|
className="inline-block w-4 h-4 mr-1 -mt-1"
|
||||||
className="inline-block w-4 h-4 mr-1 -mt-1"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24">
|
||||||
viewBox="0 0 24 24">
|
<title>Google</title>
|
||||||
<title>Google</title>
|
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
||||||
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
</svg>
|
||||||
</svg>
|
</a>
|
||||||
</a>
|
</Link>
|
||||||
</Link>
|
<Link
|
||||||
<Link
|
href={
|
||||||
href={
|
encodeURI(
|
||||||
encodeURI(
|
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
|
||||||
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
|
props.eventType.description +
|
||||||
props.eventType.description +
|
"&enddt=" +
|
||||||
"&enddt=" +
|
date.add(props.eventType.length, "minute").format() +
|
||||||
date.add(props.eventType.length, "minute").format() +
|
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
||||||
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
date.format() +
|
||||||
date.format() +
|
"&subject=" +
|
||||||
"&subject=" +
|
eventName
|
||||||
eventName
|
) + (location ? "&location=" + location : "")
|
||||||
) + (location ? "&location=" + location : "")
|
}>
|
||||||
}>
|
<a className="mx-2 btn-wide btn-white">
|
||||||
<a className="mx-2 btn-wide btn-white">
|
<svg
|
||||||
<svg
|
className="inline-block w-4 h-4 mr-1 -mt-1"
|
||||||
className="inline-block w-4 h-4 mr-1 -mt-1"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24">
|
||||||
viewBox="0 0 24 24">
|
<title>Microsoft Outlook</title>
|
||||||
<title>Microsoft Outlook</title>
|
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
||||||
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
</svg>
|
||||||
</svg>
|
</a>
|
||||||
</a>
|
</Link>
|
||||||
</Link>
|
<Link
|
||||||
<Link
|
href={
|
||||||
href={
|
encodeURI(
|
||||||
encodeURI(
|
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
|
||||||
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
|
props.eventType.description +
|
||||||
props.eventType.description +
|
"&enddt=" +
|
||||||
"&enddt=" +
|
date.add(props.eventType.length, "minute").format() +
|
||||||
date.add(props.eventType.length, "minute").format() +
|
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
||||||
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
date.format() +
|
||||||
date.format() +
|
"&subject=" +
|
||||||
"&subject=" +
|
eventName
|
||||||
eventName
|
) + (location ? "&location=" + location : "")
|
||||||
) + (location ? "&location=" + location : "")
|
}>
|
||||||
}>
|
<a className="mx-2 btn-wide btn-white">
|
||||||
<a className="mx-2 btn-wide btn-white">
|
<svg
|
||||||
<svg
|
className="inline-block w-4 h-4 mr-1 -mt-1"
|
||||||
className="inline-block w-4 h-4 mr-1 -mt-1"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24">
|
||||||
viewBox="0 0 24 24">
|
<title>Microsoft Office</title>
|
||||||
<title>Microsoft Office</title>
|
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
||||||
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
</svg>
|
||||||
</svg>
|
</a>
|
||||||
</a>
|
</Link>
|
||||||
</Link>
|
<Link href={"data:text/calendar," + eventLink()}>
|
||||||
<Link href={"data:text/calendar," + eventLink()}>
|
<a className="mx-2 btn-wide btn-white" download={props.eventType.title + ".ics"}>
|
||||||
<a className="mx-2 btn-wide btn-white" download={props.eventType.title + ".ics"}>
|
<svg
|
||||||
<svg
|
version="1.1"
|
||||||
version="1.1"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 1000 1000"
|
||||||
viewBox="0 0 1000 1000"
|
className="inline-block w-4 h-4 mr-1 -mt-1">
|
||||||
className="inline-block w-4 h-4 mr-1 -mt-1">
|
<title>Other</title>
|
||||||
<title>Other</title>
|
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
||||||
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
</svg>
|
||||||
</svg>
|
</a>
|
||||||
</a>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!props.user.hideBranding && (
|
||||||
|
<div className="mt-4 pt-4 border-t dark:border-gray-900 text-gray-400 text-center text-xs dark:text-white">
|
||||||
|
<a href="https://checkout.calendso.com">Create your own booking link with Calendso</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!props.user.hideBranding && (
|
|
||||||
<div className="mt-4 pt-4 border-t dark:border-gray-900 text-gray-400 text-center text-xs dark:text-white">
|
|
||||||
<a href="https://checkout.calendso.com">Create your own booking link with Calendso</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
const user = await prisma.user.findFirst({
|
const user = context.query.user
|
||||||
where: {
|
? await whereAndSelect(
|
||||||
username: context.query.user,
|
prisma.user.findFirst,
|
||||||
},
|
{
|
||||||
select: {
|
username: context.query.user,
|
||||||
username: true,
|
},
|
||||||
name: true,
|
["username", "name", "bio", "avatar", "hideBranding", "theme"]
|
||||||
bio: true,
|
)
|
||||||
avatar: true,
|
: null;
|
||||||
eventTypes: true,
|
|
||||||
hideBranding: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventType = await prisma.eventType.findUnique({
|
if (!user) {
|
||||||
where: {
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventType = await whereAndSelect(
|
||||||
|
prisma.eventType.findUnique,
|
||||||
|
{
|
||||||
id: parseInt(context.query.type),
|
id: parseInt(context.query.type),
|
||||||
},
|
},
|
||||||
select: {
|
["id", "title", "description", "length", "eventName"]
|
||||||
id: true,
|
);
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
length: true,
|
|
||||||
eventName: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "theme" TEXT;
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EventType" ADD COLUMN "periodCountCalendarDays" BOOLEAN,
|
||||||
|
ADD COLUMN "periodDays" INTEGER,
|
||||||
|
ADD COLUMN "periodEndDate" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "periodStartDate" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "periodType" TEXT DEFAULT E'unlimited';
|
|
@ -25,6 +25,11 @@ model EventType {
|
||||||
eventName String?
|
eventName String?
|
||||||
customInputs EventTypeCustomInput[]
|
customInputs EventTypeCustomInput[]
|
||||||
timeZone String?
|
timeZone String?
|
||||||
|
periodType String? @default("unlimited") // unlimited | rolling | range
|
||||||
|
periodStartDate DateTime?
|
||||||
|
periodEndDate DateTime?
|
||||||
|
periodDays Int?
|
||||||
|
periodCountCalendarDays Boolean?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Credential {
|
model Credential {
|
||||||
|
@ -50,6 +55,7 @@ model User {
|
||||||
endTime Int @default(1440)
|
endTime Int @default(1440)
|
||||||
bufferTime Int @default(0)
|
bufferTime Int @default(0)
|
||||||
hideBranding Boolean @default(false)
|
hideBranding Boolean @default(false)
|
||||||
|
theme String?
|
||||||
createdDate DateTime @default(now()) @map(name: "created")
|
createdDate DateTime @default(now()) @map(name: "created")
|
||||||
eventTypes EventType[]
|
eventTypes EventType[]
|
||||||
credentials Credential[]
|
credentials Credential[]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "jit",
|
mode: "jit",
|
||||||
purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
|
purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
|
||||||
darkMode: "media",
|
darkMode: "class",
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
@ -28,9 +28,32 @@ module.exports = {
|
||||||
900: "#01579b",
|
900: "#01579b",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
maxHeight: {
|
maxHeight: (theme) => ({
|
||||||
|
0: "0",
|
||||||
97: "25rem",
|
97: "25rem",
|
||||||
},
|
...theme("spacing"),
|
||||||
|
full: "100%",
|
||||||
|
screen: "100vh",
|
||||||
|
}),
|
||||||
|
minHeight: (theme) => ({
|
||||||
|
0: "0",
|
||||||
|
...theme("spacing"),
|
||||||
|
full: "100%",
|
||||||
|
screen: "100vh",
|
||||||
|
}),
|
||||||
|
minWidth: (theme) => ({
|
||||||
|
0: "0",
|
||||||
|
...theme("spacing"),
|
||||||
|
full: "100%",
|
||||||
|
screen: "100vw",
|
||||||
|
}),
|
||||||
|
maxWidth: (theme, { breakpoints }) => ({
|
||||||
|
0: "0",
|
||||||
|
...theme("spacing"),
|
||||||
|
...breakpoints(theme("screens")),
|
||||||
|
full: "100%",
|
||||||
|
screen: "100vw",
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
|
|
264
yarn.lock
264
yarn.lock
|
@ -893,6 +893,31 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
|
||||||
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
|
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
|
||||||
|
|
||||||
|
"@types/react-dates@^21.8.3":
|
||||||
|
version "21.8.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-dates/-/react-dates-21.8.3.tgz#dc4e71f83d09979b1c4f355c267e52a850d0fe2c"
|
||||||
|
integrity sha512-MSG/A5UCXepPw5a9BtdOXfCCSMcQ5+oQIkm0K2u39sf4EJbsgngUg1zcoY3amxa6Hz0EWZkZOiExK/92J6hxUw==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
"@types/react-outside-click-handler" "*"
|
||||||
|
moment "^2.26.0"
|
||||||
|
|
||||||
|
"@types/react-outside-click-handler@*":
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#ccf0014032fc6ec286210f8a05d26a5c1f94cc96"
|
||||||
|
integrity sha512-BxQpd5GsbA9rjqLcM4lYp70VnvahgjMUeJ4OKi0A7QOsDLD2yUPswOVixtDpmvCu0PkRTfvMoStIR3gKC/L3XQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react@*":
|
||||||
|
version "17.0.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f"
|
||||||
|
integrity sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/react@^17.0.3":
|
"@types/react@^17.0.3":
|
||||||
version "17.0.11"
|
version "17.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
|
||||||
|
@ -1069,6 +1094,21 @@ aggregate-error@^3.0.0:
|
||||||
clean-stack "^2.0.0"
|
clean-stack "^2.0.0"
|
||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
|
|
||||||
|
airbnb-prop-types@^2.10.0, airbnb-prop-types@^2.14.0, airbnb-prop-types@^2.15.0:
|
||||||
|
version "2.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
|
||||||
|
integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
|
||||||
|
dependencies:
|
||||||
|
array.prototype.find "^2.1.1"
|
||||||
|
function.prototype.name "^1.1.2"
|
||||||
|
is-regex "^1.1.0"
|
||||||
|
object-is "^1.1.2"
|
||||||
|
object.assign "^4.1.0"
|
||||||
|
object.entries "^1.1.2"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
prop-types-exact "^1.2.0"
|
||||||
|
react-is "^16.13.1"
|
||||||
|
|
||||||
ajv@^6.10.0, ajv@^6.12.4:
|
ajv@^6.10.0, ajv@^6.12.4:
|
||||||
version "6.12.6"
|
version "6.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||||
|
@ -1196,6 +1236,23 @@ array-union@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||||
|
|
||||||
|
array.prototype.find@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c"
|
||||||
|
integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==
|
||||||
|
dependencies:
|
||||||
|
define-properties "^1.1.3"
|
||||||
|
es-abstract "^1.17.4"
|
||||||
|
|
||||||
|
array.prototype.flat@^1.2.1:
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
|
||||||
|
integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.0"
|
||||||
|
define-properties "^1.1.3"
|
||||||
|
es-abstract "^1.18.0-next.1"
|
||||||
|
|
||||||
array.prototype.flatmap@^1.2.4:
|
array.prototype.flatmap@^1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9"
|
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9"
|
||||||
|
@ -1397,6 +1454,11 @@ braces@^3.0.1, braces@~3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
brcast@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/brcast/-/brcast-2.0.2.tgz#2db16de44140e418dc37fab10beec0369e78dcef"
|
||||||
|
integrity sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg==
|
||||||
|
|
||||||
brorand@^1.0.1, brorand@^1.1.0:
|
brorand@^1.0.1, brorand@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||||
|
@ -1663,7 +1725,7 @@ classnames@2.2.6:
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||||
|
|
||||||
classnames@^2.2.5:
|
classnames@^2.2.5, classnames@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
@ -1796,6 +1858,11 @@ console-browserify@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
||||||
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
|
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
|
||||||
|
|
||||||
|
"consolidated-events@^1.1.1 || ^2.0.0":
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
|
||||||
|
integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==
|
||||||
|
|
||||||
constants-browserify@1.0.0, constants-browserify@^1.0.0:
|
constants-browserify@1.0.0, constants-browserify@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||||
|
@ -1958,6 +2025,11 @@ data-urls@^2.0.0:
|
||||||
whatwg-mimetype "^2.3.0"
|
whatwg-mimetype "^2.3.0"
|
||||||
whatwg-url "^8.0.0"
|
whatwg-url "^8.0.0"
|
||||||
|
|
||||||
|
dayjs-business-days@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs-business-days/-/dayjs-business-days-1.0.4.tgz#36e93e7566149e175c1541d92ce16e12145412bf"
|
||||||
|
integrity sha512-kmb8Hvlhmv7INc2YXWew4SaEBJprqJ9C48CdlO1NTsqQa31ZEcO48ziFa3YFr5W9rdwB1nUmd5iIkLwgCkJ8nA==
|
||||||
|
|
||||||
dayjs@^1.10.4:
|
dayjs@^1.10.4:
|
||||||
version "1.10.5"
|
version "1.10.5"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986"
|
||||||
|
@ -1992,12 +2064,17 @@ deep-is@^0.1.3, deep-is@~0.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||||
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
||||||
|
|
||||||
|
deepmerge@^1.5.2:
|
||||||
|
version "1.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
|
||||||
|
integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
|
||||||
|
|
||||||
deepmerge@^4.2.2:
|
deepmerge@^4.2.2:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
||||||
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
||||||
|
|
||||||
define-properties@^1.1.3:
|
define-properties@^1.1.2, define-properties@^1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
|
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
|
||||||
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
|
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
|
||||||
|
@ -2072,6 +2149,11 @@ dir-glob@^3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type "^4.0.0"
|
path-type "^4.0.0"
|
||||||
|
|
||||||
|
direction@^1.0.2:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/direction/-/direction-1.0.4.tgz#2b86fb686967e987088caf8b89059370d4837442"
|
||||||
|
integrity sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==
|
||||||
|
|
||||||
dlv@^1.1.3:
|
dlv@^1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
|
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
|
||||||
|
@ -2091,6 +2173,13 @@ doctrine@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
|
|
||||||
|
document.contains@^1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/document.contains/-/document.contains-1.0.2.tgz#4260abad67a6ae9e135c1be83d68da0db169d5f0"
|
||||||
|
integrity sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q==
|
||||||
|
dependencies:
|
||||||
|
define-properties "^1.1.3"
|
||||||
|
|
||||||
dom-helpers@^5.0.1:
|
dom-helpers@^5.0.1:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||||
|
@ -2188,6 +2277,14 @@ enquirer@^2.3.5, enquirer@^2.3.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-colors "^4.1.1"
|
ansi-colors "^4.1.1"
|
||||||
|
|
||||||
|
enzyme-shallow-equal@^1.0.0:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e"
|
||||||
|
integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==
|
||||||
|
dependencies:
|
||||||
|
has "^1.0.3"
|
||||||
|
object-is "^1.1.2"
|
||||||
|
|
||||||
error-ex@^1.3.1:
|
error-ex@^1.3.1:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||||
|
@ -2195,7 +2292,7 @@ error-ex@^1.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish "^0.2.1"
|
is-arrayish "^0.2.1"
|
||||||
|
|
||||||
es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
|
es-abstract@^1.17.4, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
|
||||||
version "1.18.3"
|
version "1.18.3"
|
||||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0"
|
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0"
|
||||||
integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==
|
integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==
|
||||||
|
@ -2620,11 +2717,26 @@ function-bind@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||||
|
|
||||||
|
function.prototype.name@^1.1.2:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.4.tgz#e4ea839b9d3672ae99d0efd9f38d9191c5eaac83"
|
||||||
|
integrity sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.2"
|
||||||
|
define-properties "^1.1.3"
|
||||||
|
es-abstract "^1.18.0-next.2"
|
||||||
|
functions-have-names "^1.2.2"
|
||||||
|
|
||||||
functional-red-black-tree@^1.0.1:
|
functional-red-black-tree@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||||
|
|
||||||
|
functions-have-names@^1.2.2:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
|
||||||
|
integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
|
||||||
|
|
||||||
futoin-hkdf@^1.3.2:
|
futoin-hkdf@^1.3.2:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.3.3.tgz#6ee1c9c105dfa0995ba4f80633cf1c0c32defcb2"
|
resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.3.3.tgz#6ee1c9c105dfa0995ba4f80633cf1c0c32defcb2"
|
||||||
|
@ -2721,6 +2833,14 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
|
global-cache@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/global-cache/-/global-cache-1.2.1.tgz#39ca020d3dd7b3f0934c52b75363f8d53312c16d"
|
||||||
|
integrity sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA==
|
||||||
|
dependencies:
|
||||||
|
define-properties "^1.1.2"
|
||||||
|
is-symbol "^1.0.1"
|
||||||
|
|
||||||
globals@^11.1.0:
|
globals@^11.1.0:
|
||||||
version "11.12.0"
|
version "11.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||||
|
@ -2883,7 +3003,7 @@ hmac-drbg@^1.0.1:
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
hoist-non-react-statics@^3.3.1:
|
hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
|
@ -3186,7 +3306,7 @@ is-potential-custom-element-name@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
||||||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
||||||
|
|
||||||
is-regex@^1.1.3:
|
is-regex@^1.1.0, is-regex@^1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
|
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
|
||||||
integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
|
integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
|
||||||
|
@ -3209,13 +3329,18 @@ is-string@^1.0.5, is-string@^1.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
|
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
|
||||||
integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
|
integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
|
||||||
|
|
||||||
is-symbol@^1.0.2, is-symbol@^1.0.3:
|
is-symbol@^1.0.1, is-symbol@^1.0.2, is-symbol@^1.0.3:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
|
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
|
||||||
integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
|
integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols "^1.0.2"
|
has-symbols "^1.0.2"
|
||||||
|
|
||||||
|
is-touch-device@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-touch-device/-/is-touch-device-1.0.1.tgz#9a2fd59f689e9a9bf6ae9a86924c4ba805a42eab"
|
||||||
|
integrity sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw==
|
||||||
|
|
||||||
is-typed-array@^1.1.3:
|
is-typed-array@^1.1.3:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e"
|
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e"
|
||||||
|
@ -4045,6 +4170,11 @@ lodash.sortby@^4.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
|
lodash.throttle@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||||
|
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
|
||||||
|
|
||||||
lodash.toarray@^4.4.0:
|
lodash.toarray@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
|
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
|
||||||
|
@ -4060,7 +4190,7 @@ lodash.truncate@^4.4.2:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
||||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
||||||
|
|
||||||
lodash@^4.17.13, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
lodash@^4.1.1, lodash@^4.17.13, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
@ -4210,6 +4340,11 @@ modern-normalize@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7"
|
resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7"
|
||||||
integrity sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==
|
integrity sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==
|
||||||
|
|
||||||
|
moment@>=1.6.0, moment@^2.26.0:
|
||||||
|
version "2.29.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||||
|
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
@ -4458,7 +4593,7 @@ object-inspect@^1.10.3, object-inspect@^1.9.0:
|
||||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
|
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
|
||||||
integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
|
integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
|
||||||
|
|
||||||
object-is@^1.0.1:
|
object-is@^1.0.1, object-is@^1.1.2:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
|
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
|
||||||
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
|
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
|
||||||
|
@ -4471,7 +4606,7 @@ object-keys@^1.0.12, object-keys@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||||
|
|
||||||
object.assign@^4.1.2:
|
object.assign@^4.1.0, object.assign@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
|
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
|
||||||
integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
|
integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
|
||||||
|
@ -4481,7 +4616,7 @@ object.assign@^4.1.2:
|
||||||
has-symbols "^1.0.1"
|
has-symbols "^1.0.1"
|
||||||
object-keys "^1.1.1"
|
object-keys "^1.1.1"
|
||||||
|
|
||||||
object.entries@^1.1.4:
|
object.entries@^1.1.2, object.entries@^1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.4.tgz#43ccf9a50bc5fd5b649d45ab1a579f24e088cafd"
|
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.4.tgz#43ccf9a50bc5fd5b649d45ab1a579f24e088cafd"
|
||||||
integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==
|
integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==
|
||||||
|
@ -4500,7 +4635,7 @@ object.fromentries@^2.0.4:
|
||||||
es-abstract "^1.18.0-next.2"
|
es-abstract "^1.18.0-next.2"
|
||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
|
|
||||||
object.values@^1.1.4:
|
object.values@^1.0.4, object.values@^1.1.0, object.values@^1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
|
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
|
||||||
integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
|
integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
|
||||||
|
@ -4691,6 +4826,11 @@ pbkdf2@^3.0.3:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
|
performance-now@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||||
|
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
|
||||||
|
|
||||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
|
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
||||||
|
@ -4888,6 +5028,15 @@ prompts@^2.0.1:
|
||||||
kleur "^3.0.3"
|
kleur "^3.0.3"
|
||||||
sisteransi "^1.0.5"
|
sisteransi "^1.0.5"
|
||||||
|
|
||||||
|
prop-types-exact@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
|
||||||
|
integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
|
||||||
|
dependencies:
|
||||||
|
has "^1.0.3"
|
||||||
|
object.assign "^4.1.0"
|
||||||
|
reflect.ownkeys "^0.2.0"
|
||||||
|
|
||||||
prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
|
prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||||
version "15.7.2"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
|
@ -4976,6 +5125,13 @@ quick-lru@^5.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
||||||
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
||||||
|
|
||||||
|
raf@^3.4.1:
|
||||||
|
version "3.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||||
|
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
|
||||||
|
dependencies:
|
||||||
|
performance-now "^2.1.0"
|
||||||
|
|
||||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||||
|
@ -5001,6 +5157,27 @@ raw-body@2.4.1:
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
unpipe "1.0.0"
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
react-dates@^21.8.0:
|
||||||
|
version "21.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.8.0.tgz#355c3c7a243a7c29568fe00aca96231e171a5e94"
|
||||||
|
integrity sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw==
|
||||||
|
dependencies:
|
||||||
|
airbnb-prop-types "^2.15.0"
|
||||||
|
consolidated-events "^1.1.1 || ^2.0.0"
|
||||||
|
enzyme-shallow-equal "^1.0.0"
|
||||||
|
is-touch-device "^1.0.1"
|
||||||
|
lodash "^4.1.1"
|
||||||
|
object.assign "^4.1.0"
|
||||||
|
object.values "^1.1.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
raf "^3.4.1"
|
||||||
|
react-moment-proptypes "^1.6.0"
|
||||||
|
react-outside-click-handler "^1.2.4"
|
||||||
|
react-portal "^4.2.0"
|
||||||
|
react-with-direction "^1.3.1"
|
||||||
|
react-with-styles "^4.1.0"
|
||||||
|
react-with-styles-interface-css "^6.0.0"
|
||||||
|
|
||||||
react-dom@17.0.1:
|
react-dom@17.0.1:
|
||||||
version "17.0.1"
|
version "17.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
|
||||||
|
@ -5017,7 +5194,7 @@ react-input-autosize@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.5.8"
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1:
|
react-is@16.13.1, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
@ -5027,6 +5204,24 @@ react-is@^17.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
|
|
||||||
|
react-moment-proptypes@^1.6.0:
|
||||||
|
version "1.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz#7ba4076147f6b5998f0d4f51d302d6d8c62049fd"
|
||||||
|
integrity sha512-Er940DxWoObfIqPrZNfwXKugjxMIuk1LAuEzn23gytzV6hKS/sw108wibi9QubfMN4h+nrlje8eUCSbQRJo2fQ==
|
||||||
|
dependencies:
|
||||||
|
moment ">=1.6.0"
|
||||||
|
|
||||||
|
react-outside-click-handler@^1.2.4:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#3831d541ac059deecd38ec5423f81e80ad60e115"
|
||||||
|
integrity sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ==
|
||||||
|
dependencies:
|
||||||
|
airbnb-prop-types "^2.15.0"
|
||||||
|
consolidated-events "^1.1.1 || ^2.0.0"
|
||||||
|
document.contains "^1.0.1"
|
||||||
|
object.values "^1.1.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
react-phone-number-input@^3.1.21:
|
react-phone-number-input@^3.1.21:
|
||||||
version "3.1.23"
|
version "3.1.23"
|
||||||
resolved "https://registry.yarnpkg.com/react-phone-number-input/-/react-phone-number-input-3.1.23.tgz#8e8d2b7fb98d94514721d05ca5c58bd31f8d06e1"
|
resolved "https://registry.yarnpkg.com/react-phone-number-input/-/react-phone-number-input-3.1.23.tgz#8e8d2b7fb98d94514721d05ca5c58bd31f8d06e1"
|
||||||
|
@ -5038,6 +5233,13 @@ react-phone-number-input@^3.1.21:
|
||||||
libphonenumber-js "^1.9.19"
|
libphonenumber-js "^1.9.19"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
|
react-portal@^4.2.0:
|
||||||
|
version "4.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.1.tgz#12c1599238c06fb08a9800f3070bea2a3f78b1a6"
|
||||||
|
integrity sha512-fE9kOBagwmTXZ3YGRYb4gcMy+kSA+yLO0xnPankjRlfBv4uCpFXqKPfkpsGQQR15wkZ9EssnvTOl1yMzbkxhPQ==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
react-refresh@0.8.3:
|
react-refresh@0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
|
@ -5075,6 +5277,39 @@ react-transition-group@^4.3.0:
|
||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
react-with-direction@^1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-with-direction/-/react-with-direction-1.3.1.tgz#9fd414564f0ffe6947e5ff176f6132dd83f8b8df"
|
||||||
|
integrity sha512-aGcM21ZzhqeXFvDCfPj0rVNYuaVXfTz5D3Rbn0QMz/unZe+CCiLHthrjQWO7s6qdfXORgYFtmS7OVsRgSk5LXQ==
|
||||||
|
dependencies:
|
||||||
|
airbnb-prop-types "^2.10.0"
|
||||||
|
brcast "^2.0.2"
|
||||||
|
deepmerge "^1.5.2"
|
||||||
|
direction "^1.0.2"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
object.assign "^4.1.0"
|
||||||
|
object.values "^1.0.4"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
react-with-styles-interface-css@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-with-styles-interface-css/-/react-with-styles-interface-css-6.0.0.tgz#b53da7fa8359d452cb934cface8738acaef7b5fe"
|
||||||
|
integrity sha512-6khSG1Trf4L/uXOge/ZAlBnq2O2PEXlQEqAhCRbvzaQU4sksIkdwpCPEl6d+DtP3+IdhyffTWuHDO9lhe1iYvA==
|
||||||
|
dependencies:
|
||||||
|
array.prototype.flat "^1.2.1"
|
||||||
|
global-cache "^1.2.1"
|
||||||
|
|
||||||
|
react-with-styles@^4.1.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-with-styles/-/react-with-styles-4.2.0.tgz#0b8a8e5d94d082518b9f564f6fcf6103e28096c5"
|
||||||
|
integrity sha512-tZCTY27KriRNhwHIbg1NkSdTTOSfXDg6Z7s+Q37mtz0Ym7Sc7IOr3PzVt4qJhJMW6Nkvfi3g34FuhtiGAJCBQA==
|
||||||
|
dependencies:
|
||||||
|
airbnb-prop-types "^2.14.0"
|
||||||
|
hoist-non-react-statics "^3.2.1"
|
||||||
|
object.assign "^4.1.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-with-direction "^1.3.1"
|
||||||
|
|
||||||
react@17.0.1:
|
react@17.0.1:
|
||||||
version "17.0.1"
|
version "17.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
||||||
|
@ -5132,6 +5367,11 @@ reflect-metadata@^0.1.13:
|
||||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
||||||
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
|
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
|
||||||
|
|
||||||
|
reflect.ownkeys@^0.2.0:
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
||||||
|
integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
|
||||||
|
|
||||||
regenerator-runtime@^0.13.4:
|
regenerator-runtime@^0.13.4:
|
||||||
version "0.13.7"
|
version "0.13.7"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||||
|
|
Loading…
Reference in a new issue