diff --git a/.gitignore b/.gitignore index 366d5cc45b..84c55a0123 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /node_modules /.pnp .pnp.js +/.yarn # testing /coverage diff --git a/components/Avatar.tsx b/components/Avatar.tsx deleted file mode 100644 index b1f58d6322..0000000000 --- a/components/Avatar.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { defaultAvatarSrc } from "@lib/profile"; - -export type AvatarProps = { - className?: string; - imageSrc?: string; - displayName: string; - gravatarFallbackMd5?: string; -}; - -export default function Avatar({ imageSrc, displayName, gravatarFallbackMd5, className = "" }: AvatarProps) { - return ( - - - - {gravatarFallbackMd5 && ( - {displayName} - )} - - - ); -} diff --git a/components/Shell.tsx b/components/Shell.tsx index e1794ff026..a8eb7219c0 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -19,7 +19,7 @@ import { import Logo from "./Logo"; import classNames from "@lib/classNames"; import { Toaster } from "react-hot-toast"; -import Avatar from "@components/Avatar"; +import Avatar from "@components/ui/Avatar"; import { User } from "@prisma/client"; import { HeadSeo } from "@components/seo/head-seo"; diff --git a/components/booking/AvailableTimes.tsx b/components/booking/AvailableTimes.tsx index 7eadb552d1..83266bad74 100644 --- a/components/booking/AvailableTimes.tsx +++ b/components/booking/AvailableTimes.tsx @@ -1,9 +1,10 @@ import Link from "next/link"; import { useRouter } from "next/router"; -import Slots from "./Slots"; +import { useSlots } from "@lib/hooks/useSlots"; import { ExclamationIcon } from "@heroicons/react/solid"; import React from "react"; import Loader from "@components/Loader"; +import { SchedulingType } from "@prisma/client"; const AvailableTimes = ({ date, @@ -12,17 +13,18 @@ const AvailableTimes = ({ minimumBookingNotice, workingHours, timeFormat, - user, - organizerTimeZone, + users, + schedulingType, }) => { const router = useRouter(); const { rescheduleUid } = router.query; - const { slots, isFullyBooked, hasErrors } = Slots({ + const { slots, loading, error } = useSlots({ date, eventLength, + schedulingType, workingHours, - organizerTimeZone, + users, minimumBookingNotice, }); @@ -34,43 +36,52 @@ const AvailableTimes = ({ {date.format(", DD MMMM")} - {slots.length > 0 && - slots.map((slot) => ( -
- - - {slot.format(timeFormat)} - - -
- ))} - {isFullyBooked && ( + {!loading && + slots?.length > 0 && + slots.map((slot) => { + const bookingUrl = { + pathname: "book", + query: { + ...router.query, + date: slot.time.format(), + type: eventTypeId, + }, + }; + + if (rescheduleUid) { + bookingUrl.query.rescheduleUid = rescheduleUid; + } + + if (schedulingType === SchedulingType.ROUND_ROBIN) { + bookingUrl.query.user = slot.users; + } + + return ( +
+ + + {slot.time.format(timeFormat)} + + +
+ ); + })} + {!loading && !error && !slots.length && (
-

{user.name} is all booked today.

+

All booked today.

)} - {!isFullyBooked && slots.length === 0 && !hasErrors && } + {loading && } - {hasErrors && ( + {error && (
-

- Could not load the available time slots.{" "} - - Contact {user.name} via e-mail - -

+

Could not load the available time slots.

diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx index e4bd374af9..b64815f803 100644 --- a/components/booking/DatePicker.tsx +++ b/components/booking/DatePicker.tsx @@ -5,6 +5,7 @@ import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; import getSlots from "@lib/slots"; import dayjsBusinessDays from "dayjs-business-days"; +import classNames from "@lib/classNames"; dayjs.extend(dayjsBusinessDays); dayjs.extend(utc); @@ -15,7 +16,6 @@ const DatePicker = ({ onDatePicked, workingHours, organizerTimeZone, - inviteeTimeZone, eventLength, date, periodType = "unlimited", @@ -25,28 +25,23 @@ const DatePicker = ({ periodCountCalendarDays, minimumBookingNotice, }) => { - const [calendar, setCalendar] = useState([]); - const [selectedMonth, setSelectedMonth] = useState(); - const [selectedDate, setSelectedDate] = useState(); + const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]); + + const [selectedMonth, setSelectedMonth] = useState( + date + ? periodType === "range" + ? dayjs(periodStartDate).utcOffset(date.utcOffset()).month() + : date.month() + : dayjs().month() /* High chance server is going to have the same month */ + ); useEffect(() => { - 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()); + if (dayjs().month() !== selectedMonth) { + setSelectedMonth(dayjs().month()); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - if (selectedDate) onDatePicked(selectedDate); - }, [selectedDate]); - // Handle month changes const incrementMonth = () => { setSelectedMonth(selectedMonth + 1); @@ -56,24 +51,27 @@ const DatePicker = ({ setSelectedMonth(selectedMonth - 1); }; + const inviteeDate = (): Dayjs => (date || dayjs()).month(selectedMonth); + useEffect(() => { - if (!selectedMonth) { - // wish next had a way of dealing with this magically; - return; + // Create placeholder elements for empty days in first week + let weekdayOfFirst = inviteeDate().date(1).day(); + if (weekStart === "Monday") { + weekdayOfFirst -= 1; + if (weekdayOfFirst < 0) weekdayOfFirst = 6; } - const inviteeDate = dayjs().tz(inviteeTimeZone).month(selectedMonth); + const days = Array(weekdayOfFirst).fill(null); const isDisabled = (day: number) => { - const date: Dayjs = inviteeDate.date(day); - + const date: Dayjs = inviteeDate().date(day); switch (periodType) { case "rolling": { const periodRollingEndDay = periodCountCalendarDays ? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day") : dayjs().tz(organizerTimeZone).businessDaysAdd(periodDays, "days").endOf("day"); return ( - date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) || + date.endOf("day").isBefore(dayjs().utcOffsett(date.utcOffset())) || date.endOf("day").isAfter(periodRollingEndDay) || !getSlots({ inviteeDate: date, @@ -89,7 +87,7 @@ const DatePicker = ({ 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(dayjs().utcOffset(date.utcOffset())) || date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay) || !getSlots({ @@ -105,7 +103,7 @@ const DatePicker = ({ case "unlimited": default: return ( - date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) || + date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) || !getSlots({ inviteeDate: date, frequency: eventLength, @@ -117,81 +115,35 @@ const DatePicker = ({ } }; - // Set up calendar - const daysInMonth = inviteeDate.daysInMonth(); - const days = []; + const daysInMonth = inviteeDate().daysInMonth(); for (let i = 1; i <= daysInMonth; i++) { - days.push(i); + days.push({ disabled: isDisabled(i), date: i }); } - // Create placeholder elements for empty days in first week - let weekdayOfFirst = inviteeDate.date(1).day(); - if (weekStart === "Monday") { - weekdayOfFirst -= 1; - if (weekdayOfFirst < 0) weekdayOfFirst = 6; - } - const emptyDays = Array(weekdayOfFirst) - .fill(null) - .map((day, i) => ( -
- {null} -
- )); + setDays(days); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedMonth]); - // Combine placeholder days with actual days - setCalendar([ - ...emptyDays, - ...days.map((day) => ( -
- -
- )), - ]); - }, [selectedMonth, inviteeTimeZone, selectedDate]); - - return selectedMonth ? ( + return (
- - {dayjs().month(selectedMonth).format("MMMM")} - - {dayjs().month(selectedMonth).format("YYYY")} + {inviteeDate().format("MMMM")} + {inviteeDate().format("YYYY")}
))}
-
{calendar}
+
+ {days.map((day, idx) => ( +
+ {day === null ? ( +
+ ) : ( + + )} +
+ ))} +
- ) : null; + ); }; export default DatePicker; diff --git a/components/booking/Slots.tsx b/components/booking/Slots.tsx index 29ed9e7274..f3677bdea4 100644 --- a/components/booking/Slots.tsx +++ b/components/booking/Slots.tsx @@ -29,9 +29,7 @@ const Slots = ({ eventLength, minimumBookingNotice, date, workingHours, organize setIsFullyBooked(false); setHasErrors(false); fetch( - `/api/availability/${user}?dateFrom=${date.startOf("day").utc().startOf("day").format()}&dateTo=${date - .endOf("day") - .utc() + `/api/availability/${user}?dateFrom=${date.startOf("day").format()}&dateTo=${date .endOf("day") .format()}` ) diff --git a/components/booking/pages/AvailabilityPage.tsx b/components/booking/pages/AvailabilityPage.tsx new file mode 100644 index 0000000000..ef29431e5b --- /dev/null +++ b/components/booking/pages/AvailabilityPage.tsx @@ -0,0 +1,232 @@ +// Get router variables +import { useRouter } from "next/router"; +import { useEffect, useState, useMemo } from "react"; +import { EventType } from "@prisma/client"; +import dayjs, { Dayjs } from "dayjs"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import utc from "dayjs/plugin/utc"; +import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; +import { ChevronDownIcon, ChevronUpIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid"; +import DatePicker from "@components/booking/DatePicker"; +import { isBrandingHidden } from "@lib/isBrandingHidden"; +import PoweredByCalendso from "@components/ui/PoweredByCalendso"; +import { timeZone } from "@lib/clock"; +import AvailableTimes from "@components/booking/AvailableTimes"; +import TimeOptions from "@components/booking/TimeOptions"; +import * as Collapsible from "@radix-ui/react-collapsible"; +import { HeadSeo } from "@components/seo/head-seo"; +import { asStringOrNull } from "@lib/asStringOrNull"; +import useTheme from "@lib/hooks/useTheme"; +import AvatarGroup from "@components/ui/AvatarGroup"; + +dayjs.extend(utc); +dayjs.extend(customParseFormat); + +type AvailabilityPageProps = { + eventType: EventType; + profile: { + name: string; + image: string; + theme?: string; + }; + workingHours: []; +}; + +const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPageProps) => { + const router = useRouter(); + const { rescheduleUid } = router.query; + const themeLoaded = useTheme(profile.theme); + + const selectedDate = useMemo(() => { + const dateString = asStringOrNull(router.query.date); + if (dateString) { + // todo some extra validation maybe. + const utcOffsetAsDate = dayjs(dateString.substr(11, 14), "Hmm"); + const utcOffset = parseInt( + dateString.substr(10, 1) + (utcOffsetAsDate.hour() * 60 + utcOffsetAsDate.minute()) + ); + const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffset, true); + return date.isValid() ? date : null; + } + return null; + }, [router.query.date]); + + const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); + const [timeFormat, setTimeFormat] = useState("h:mma"); + const telemetry = useTelemetry(); + + useEffect(() => { + handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true"); + telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters())); + }, [telemetry]); + + const changeDate = (newDate: Dayjs) => { + telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.dateSelected, collectPageParameters())); + router.replace( + { + query: { + ...router.query, + date: newDate.format("YYYY-MM-DDZZ"), + }, + }, + undefined, + { + shallow: true, + } + ); + }; + + const handleSelectTimeZone = (selectedTimeZone: string): void => { + if (selectedDate) { + changeDate(selectedDate.tz(selectedTimeZone, true)); + } + timeZone(selectedTimeZone); + setIsTimeOptionsOpen(false); + }; + + const handleToggle24hClock = (is24hClock: boolean) => { + setTimeFormat(is24hClock ? "HH:mm" : "h:mma"); + }; + + return ( + themeLoaded && ( + <> + +
+
+
+ {/* mobile: details */} +
+
+ user.name !== profile.name) + .map((user) => ({ + title: user.name, + image: user.avatar, + })) + )} + size={9} + truncateAfter={5} + /> +
+

{profile.name}

+
+ {eventType.title} +
+ + {eventType.length} minutes +
+
+
+
+

{eventType.description}

+
+ +
+
+ user.name !== profile.name) + .map((user) => ({ + title: user.name, + image: user.avatar, + })) + )} + size={16} + truncateAfter={3} + /> +

{profile.name}

+

+ {eventType.title} +

+

+ + {eventType.length} minutes +

+ + + +

{eventType.description}

+
+ + +
+ +
+ + {selectedDate && ( + + )} +
+
+ {eventType.users.length && isBrandingHidden(eventType.users[0]) && } +
+
+ + ) + ); + + function TimezoneDropdown() { + return ( + + + + {timeZone()} + {isTimeOptionsOpen ? ( + + ) : ( + + )} + + + + + + ); + } +}; + +export default AvailabilityPage; diff --git a/components/booking/pages/BookingPage.tsx b/components/booking/pages/BookingPage.tsx new file mode 100644 index 0000000000..b704b3b91a --- /dev/null +++ b/components/booking/pages/BookingPage.tsx @@ -0,0 +1,416 @@ +import Head from "next/head"; +import { useRouter } from "next/router"; +import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid"; +import { EventTypeCustomInputType } from "@prisma/client"; +import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; +import { useEffect, useState } from "react"; +import dayjs from "dayjs"; +import "react-phone-number-input/style.css"; +import PhoneInput from "react-phone-number-input"; +import { LocationType } from "@lib/location"; +import { Button } from "@components/ui/Button"; +import { ReactMultiEmail } from "react-multi-email"; +import { asStringOrNull } from "@lib/asStringOrNull"; +import { timeZone } from "@lib/clock"; +import useTheme from "@lib/hooks/useTheme"; +import AvatarGroup from "@components/ui/AvatarGroup"; + +const BookingPage = (props: any): JSX.Element => { + const router = useRouter(); + const { rescheduleUid } = router.query; + const themeLoaded = useTheme(props.profile.theme); + + const date = asStringOrNull(router.query.date); + const timeFormat = asStringOrNull(router.query.clock) === "24h" ? "H:mm" : "h:mma"; + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); + const [guestToggle, setGuestToggle] = useState(false); + const [guestEmails, setGuestEmails] = useState([]); + const locations = props.eventType.locations || []; + + const [selectedLocation, setSelectedLocation] = useState( + locations.length === 1 ? locations[0].type : "" + ); + + const telemetry = useTelemetry(); + + useEffect(() => { + telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters())); + }, []); + + function toggleGuestEmailInput() { + setGuestToggle(!guestToggle); + } + + const locationInfo = (type: LocationType) => locations.find((location) => location.type === type); + + // TODO: Move to translations + const locationLabels = { + [LocationType.InPerson]: "In-person meeting", + [LocationType.Phone]: "Phone call", + [LocationType.GoogleMeet]: "Google Meet", + [LocationType.Zoom]: "Zoom Video", + }; + + const bookingHandler = (event) => { + const book = async () => { + setLoading(true); + setError(false); + let notes = ""; + if (props.eventType.customInputs) { + notes = props.eventType.customInputs + .map((input) => { + const data = event.target["custom_" + input.id]; + if (data) { + if (input.type === EventTypeCustomInputType.BOOL) { + return input.label + "\n" + (data.checked ? "Yes" : "No"); + } else { + return input.label + "\n" + data.value; + } + } + }) + .join("\n\n"); + } + if (!!notes && !!event.target.notes.value) { + notes += "\n\nAdditional notes:\n" + event.target.notes.value; + } else { + notes += event.target.notes.value; + } + + const payload = { + start: dayjs(date).format(), + end: dayjs(date).add(props.eventType.length, "minute").format(), + name: event.target.name.value, + email: event.target.email.value, + notes: notes, + guests: guestEmails, + eventTypeId: props.eventType.id, + rescheduleUid: rescheduleUid, + timeZone: timeZone(), + }; + + if (router.query.user) { + payload.user = router.query.user; + } + + if (selectedLocation) { + switch (selectedLocation) { + case LocationType.Phone: + payload["location"] = event.target.phone.value; + break; + + case LocationType.InPerson: + payload["location"] = locationInfo(selectedLocation).address; + break; + + // Catches all other location types, such as Google Meet, Zoom etc. + default: + payload["location"] = selectedLocation; + } + } + + telemetry.withJitsu((jitsu) => + jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters()) + ); + + /*const res = await */ fetch("/api/book/event", { + body: JSON.stringify(payload), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + // TODO When the endpoint is fixed, change this to await the result again + //if (res.ok) { + let successUrl = `/success?date=${encodeURIComponent(date)}&type=${props.eventType.id}&user=${ + props.profile.slug + }&reschedule=${!!rescheduleUid}&name=${payload.name}`; + if (payload["location"]) { + if (payload["location"].includes("integration")) { + successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow."); + } else { + successUrl += "&location=" + encodeURIComponent(payload["location"]); + } + } + + await router.push(successUrl); + }; + + event.preventDefault(); + book(); + }; + + return ( + themeLoaded && ( +
+ + + {rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with {props.profile.name}{" "} + | Calendso + + + + +
+
+
+
+ user.name !== props.profile.name) + .map((user) => ({ + image: user.avatar, + title: user.name, + })) + )} + /> +

{props.profile.name}

+

+ {props.eventType.title} +

+

+ + {props.eventType.length} minutes +

+ {selectedLocation === LocationType.InPerson && ( +

+ + {locationInfo(selectedLocation).address} +

+ )} +

+ + {dayjs(date) + .tz(timeZone()) + .format(timeFormat + ", dddd DD MMMM YYYY")} +

+

{props.eventType.description}

+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ {locations.length > 1 && ( +
+ + Location + + {locations.map((location) => ( + + ))} +
+ )} + {selectedLocation === LocationType.Phone && ( +
+ +
+ { + /* DO NOT REMOVE: Callback required by PhoneInput, comment added to satisfy eslint:no-empty-function */ + }} + /> +
+
+ )} + {props.eventType.customInputs && + props.eventType.customInputs + .sort((a, b) => a.id - b.id) + .map((input) => ( +
+ {input.type !== EventTypeCustomInputType.BOOL && ( + + )} + {input.type === EventTypeCustomInputType.TEXTLONG && ( + +
+
+
+ +
+
+ +
+ {team &&
} + {team && ( +
+
+
+ +
+ +
+ +
+
+ +
+
+ setUsers(options.map((option) => option.value))} + defaultValue={eventType.users.map((user: User) => ({ + value: user.id, + label: user.name, + avatar: user.avatar, + }))} + options={teamMembers.map((user: User) => ({ + value: user.id, + label: user.name, + avatar: user.avatar, + }))} + id="users" + placeholder="Add attendees" + /> +
+
+
+ )} + {({ open }) => ( <> @@ -703,7 +770,7 @@ const EventTypePage = (props: inferSSRProps) => { key={period.type} value={period} className={({ checked }) => - classnames( + classNames( checked ? "border-secondary-200 z-10" : "border-gray-200", "relative min-h-14 flex items-center cursor-pointer focus:outline-none" ) @@ -711,7 +778,7 @@ const EventTypePage = (props: inferSSRProps) => { {({ active, checked }) => ( <>
) => {
@@ -833,7 +900,7 @@ const EventTypePage = (props: inferSSRProps) => { label="Hide event type" /> @@ -843,7 +910,11 @@ const EventTypePage = (props: inferSSRProps) => { + +
+
+
+
+ + {({ open }) => ( + <> +
+ + Open options + +
+ + + +
+ + {({ active }) => ( + + + )} + + + {({ active }) => ( + + )} + +
+
+
+ + )} +
+
+
+ + ))} + +
+ ); + + return ( +
+ + Event Types | Calendso + + + + ) + }> + {props.user.plan === "FREE" && typeof window !== "undefined" && ( + You need to upgrade your plan to have more than one active event type.} + message={ + <> + To upgrade go to{" "} + + {`${window.location.origin}/upgrade`} + + + } + className="my-4" + /> + )} + {props.eventTypes && + props.eventTypes.map((input) => ( + <> + {/* hide list heading when there is only one (current user) */} + {(props.eventTypes.length !== 1 || input.teamId) && ( + + )} + + + ))} + + {props.eventTypes.length === 0 && } + +
+ ); +}; + +const CreateNewEventDialog = ({ profiles, canAddEvents }) => { const router = useRouter(); + const teamId: number | null = Number(router.query.teamId) || null; + const modalOpen = useToggleQuery("new"); + const createMutation = useMutation(createEventType, { onSuccess: async ({ eventType }) => { - await router.push("/event-types/" + eventType.slug); + await router.push("/event-types/" + eventType.id); showToast(`${eventType.title} event type created successfully`, "success"); }, onError: (err: HttpError) => { @@ -46,42 +292,76 @@ const EventTypesPage = (props: inferSSRProps) => { showToast(message, "error"); }, }); - const modalOpen = useToggleQuery("new"); const slugRef = useRef(null); - if (loading) { - return ; - } - - const renderEventDialog = () => ( + return ( { router.push(isOpen ? modalOpen.hrefOn : modalOpen.hrefOff); }}> - - - - + {!profiles.filter((profile) => profile.teamId).length && ( + + )} + {profiles.filter((profile) => profile.teamId).length > 0 && ( + + + + + + + Create an event type under +
+ your name or a team. +
+ + {profiles.map((profile) => ( + + router.push({ + pathname: router.pathname, + query: { + ...router.query, + new: "1", + eventPage: profile.slug, + ...(profile.teamId + ? { + teamId: profile.teamId, + } + : {}), + }, + }) + }> + + {profile.name} + + ))} +
+
+ )}
-

Create a new event type for people to book times with.

@@ -92,18 +372,23 @@ const EventTypesPage = (props: inferSSRProps) => { e.preventDefault(); const target = e.target as unknown as Record< - "title" | "slug" | "description" | "length", + "title" | "slug" | "description" | "length" | "schedulingType", { value: string } >; - const body = { + const payload = { title: target.title.value, slug: target.slug.value, description: target.description.value, length: parseInt(target.length.value), }; - createMutation.mutate(body); + if (router.query.teamId) { + payload.teamId = parseInt(asStringOrNull(router.query.teamId), 10); + payload.schedulingType = target.schedulingType.value; + } + + createMutation.mutate(payload); }}>
@@ -116,14 +401,13 @@ const EventTypesPage = (props: inferSSRProps) => { if (!slugRef.current) { return; } - const slug = e.target.value.replace(/\s+/g, "-").toLowerCase(); - slugRef.current.value = slug; + slugRef.current.value = e.target.value.replace(/\s+/g, "-").toLowerCase(); }} type="text" name="title" id="title" required - className="block w-full border-gray-300 rounded-sm shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm" + className="shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full sm:text-sm border-gray-300 rounded-sm" placeholder="Quick Chat" />
@@ -134,16 +418,16 @@ const EventTypesPage = (props: inferSSRProps) => {
- - {location.hostname}/{user.username}/ + + {location.hostname}/{router.query.eventPage || profiles[0].slug}/
@@ -156,524 +440,67 @@ const EventTypesPage = (props: inferSSRProps) => { + className="shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full sm:text-sm border-gray-300 rounded-sm" + placeholder="A quick video meeting." + />
-
+
-
+
minutes
-
+ {teamId && ( +
+ + + + Collective +

Schedule meetings when all selected team members are available.

+
+ + Round Robin +

Cycle meetings between multiple team members.

+
+
+
+ )} +
- +
); - - return ( -
- - {props.user.plan === "FREE" && ( - You need to upgrade your plan to have more than one active event type.} - message={ - <> - To upgrade go to{" "} - - calendso.com/upgrade - - - } - className="my-4" - /> - )} -
-
    - {types.map((item) => ( -
  • -
    -
    - - - -
    -

    {item.title}

    - {item.hidden && ( - - Hidden - - )} -
    -
    -
    -
    -
    -
    - {item.description && ( -
    -
    - )} -
    -
    -
    - - -
    -
    - - - - - - - - - -
    -
    -
    - - {({ open }) => ( - <> -
    - - Open options - -
    - - - -
    - - {({ active }) => ( - - - )} - - - {({ active }) => ( - - )} - - {/**/} - {/* {({ active }) => (*/} - {/* */} - {/* */} -
    - {/*
    */} - {/* */} - {/* {({ active }) => (*/} - {/* */} - {/* */} - {/*
    */} -
    -
    - - )} -
    -
    -
    -
    -
  • - ))} -
-
- {types.length === 0 && ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Create your first event type

-

- Event types enable you to share links that show available times on your calendar and allow - people to make bookings with you. -

- {renderEventDialog()} -
-
- )} -
-
- ); }; -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const { req } = context; - const session = await getSession({ req }); - - if (!session?.user?.id) { - return { - redirect: { - permanent: false, - destination: "/auth/login", - }, - } as const; +export async function getServerSideProps(context) { + const session = await getSession(context); + if (!session) { + return { redirect: { permanent: false, destination: "/auth/login" } }; } const user = await prisma.user.findUnique({ @@ -683,12 +510,74 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => select: { id: true, username: true, + name: true, startTime: true, endTime: true, bufferTime: true, + avatar: true, completedOnboarding: true, createdDate: true, plan: true, + teams: { + where: { + accepted: true, + }, + select: { + role: true, + team: { + select: { + id: true, + name: true, + slug: true, + logo: true, + members: { + select: { + userId: true, + }, + }, + eventTypes: { + select: { + id: true, + title: true, + description: true, + length: true, + schedulingType: true, + slug: true, + hidden: true, + users: { + select: { + id: true, + avatar: true, + name: true, + }, + }, + }, + }, + }, + }, + }, + }, + eventTypes: { + where: { + team: null, + }, + select: { + id: true, + title: true, + description: true, + length: true, + schedulingType: true, + slug: true, + hidden: true, + users: { + select: { + id: true, + avatar: true, + name: true, + }, + }, + }, + }, }, }); @@ -711,9 +600,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => } as const; } + let eventTypes = []; + + // backwards compatibility, TMP: const typesRaw = await prisma.eventType.findMany({ where: { - userId: user.id, + userId: session.user.id, }, select: { id: true, @@ -722,34 +614,73 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => description: true, length: true, hidden: true, + users: { + select: { + id: true, + avatar: true, + name: true, + }, + }, }, }); - const types = typesRaw.map((type, index) => - user.plan === "FREE" && index > 0 - ? { - ...type, - $disabled: true, - } - : { - ...type, - $disabled: false, - } + eventTypes.push({ + teamId: null, + profile: { + slug: user.username, + name: user.name, + image: user.avatar, + }, + eventTypes: user.eventTypes.concat(typesRaw).map((type, index) => + user.plan === "FREE" && index > 0 + ? { + ...type, + $disabled: true, + } + : { + ...type, + $disabled: false, + } + ), + }); + + eventTypes = [].concat( + eventTypes, + user.teams.map((membership) => ({ + teamId: membership.team.id, + profile: { + name: membership.team.name, + image: membership.team.logo || "", + slug: "team/" + membership.team.slug, + }, + metadata: { + membershipCount: membership.team.members.length, + readOnly: membership.role !== "OWNER", + }, + eventTypes: membership.team.eventTypes, + })) ); const userObj = Object.assign({}, user, { createdDate: user.createdDate.toString(), }); - const canAddEvents = user.plan !== "FREE" || types.length < 1; + const canAddEvents = user.plan !== "FREE" || eventTypes.length < 1; return { props: { - user: userObj, - types, canAddEvents, + user: userObj, + // don't display event teams without event types, + eventTypes: eventTypes.filter((groupBy) => groupBy.eventTypes.length > 0), + // so we can show a dropdown when the user has teams + profiles: eventTypes.map((group) => ({ + teamId: group.teamId, + ...group.profile, + ...group.metadata, + })), }, }; -}; +} export default EventTypesPage; diff --git a/pages/reschedule/[uid].tsx b/pages/reschedule/[uid].tsx index 87cfe4916a..9decdaacc7 100644 --- a/pages/reschedule/[uid].tsx +++ b/pages/reschedule/[uid].tsx @@ -1,5 +1,6 @@ import { GetServerSidePropsContext } from "next"; -import prisma from "../../lib/prisma"; +import prisma from "@lib/prisma"; +import { asStringOrNull } from "@lib/asStringOrNull"; export default function Type() { // Just redirect to the schedule page to reschedule it. @@ -7,14 +8,28 @@ export default function Type() { } export async function getServerSideProps(context: GetServerSidePropsContext) { - const booking = await prisma.booking.findFirst({ + const booking = await prisma.booking.findUnique({ where: { - uid: context.query.uid as string, + uid: asStringOrNull(context.query.uid), }, select: { id: true, - user: { select: { username: true } }, - eventType: { select: { slug: true } }, + eventType: { + select: { + users: { + select: { + username: true, + }, + }, + slug: true, + team: { + select: { + slug: true, + }, + }, + }, + }, + user: true, title: true, description: true, startTime: true, @@ -22,16 +37,21 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { attendees: true, }, }); - if (!booking?.user || !booking.eventType) { + + if (!booking.eventType) { return { notFound: true, }; } + const eventType = booking.eventType; + + const eventPage = + (eventType.team ? "team/" + eventType.team.slug : booking.user.username) + "/" + booking.eventType.slug; + return { redirect: { - destination: - "/" + booking.user.username + "/" + booking.eventType.slug + "?rescheduleUid=" + context.query.uid, + destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid, permanent: false, }, }; diff --git a/pages/sandbox/RadioArea.tsx b/pages/sandbox/RadioArea.tsx new file mode 100644 index 0000000000..2dd4617517 --- /dev/null +++ b/pages/sandbox/RadioArea.tsx @@ -0,0 +1,103 @@ +import * as RadioArea from "@components/ui/form/radio-area"; +import Head from "next/head"; +import React, { useState } from "react"; + +const selectOptions = [ + { + value: "rabbit", + label: "Rabbit", + description: "Fast and hard.", + }, + { + value: "turtle", + label: "Turtle", + description: "Slow and steady.", + }, +]; + +export default function RadioAreaPage() { + const [formData, setFormData] = useState({}); + + const onSubmit = (e) => { + e.preventDefault(); + }; + + return ( + <> + + + +
+

RadioArea component

+
+ setFormData({ ...formData, radioGroup_1 })} + className="flex space-x-4 max-w-screen-md" + name="radioGroup_1"> + + radioGroup_1_radio_1 +

Description #1

+
+ + radioGroup_1_radio_2 +

Description #2

+
+ + radioGroup_1_radio_3 +

Description #3

+
+
+ setFormData({ ...formData, radioGroup_2 })} + className="flex space-x-4 max-w-screen-md" + name="radioGroup_2"> + + radioGroup_1_radio_1 +

Description #1

+
+ + radioGroup_1_radio_2 +

Description #2

+
+ + radioGroup_1_radio_3 +

Description #3

+
+
+
+

Disabled RadioAreaSelect

+ +
+
+

RadioArea disabled with custom placeholder

+ +
+
+

RadioArea with options

+ + setFormData({ ...formData, turtleOrRabbitWinsTheRace }) + } + options={selectOptions} + placeholder="Does the rabbit or the turtle win the race?"> +
+ +
+

RadioArea with default selected (disabled for clarity)

+ +
+
+
{JSON.stringify(formData)}
+
+ + ); +} diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 79ef270110..3960ee506a 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -4,7 +4,7 @@ import prisma from "@lib/prisma"; import Modal from "@components/Modal"; import Shell from "@components/Shell"; import SettingsShell from "@components/Settings"; -import Avatar from "@components/Avatar"; +import Avatar from "@components/ui/Avatar"; import { getSession } from "@lib/auth"; import Select from "react-select"; import TimezoneSelect from "react-timezone-select"; diff --git a/pages/success.tsx b/pages/success.tsx index a986f10c0e..e8a311ca1f 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -11,11 +11,11 @@ import toArray from "dayjs/plugin/toArray"; import timezone from "dayjs/plugin/timezone"; import { createEvent } from "ics"; import { getEventName } from "@lib/event"; -import Theme from "@components/Theme"; -import { GetServerSidePropsContext } from "next"; -import { asStringOrNull } from "../lib/asStringOrNull"; +import useTheme from "@lib/hooks/useTheme"; +import { asStringOrNull } from "@lib/asStringOrNull"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import { isBrandingHidden } from "@lib/isBrandingHidden"; +import { EventType } from "@prisma/client"; dayjs.extend(utc); dayjs.extend(toArray); @@ -26,8 +26,8 @@ export default function Success(props: inferSSRProps) const { location, name } = router.query; const [is24h, setIs24h] = useState(false); - const [date, setDate] = useState(dayjs.utc(router.query.date)); - const { isReady } = Theme(props.user.theme); + const [date, setDate] = useState(dayjs.utc(asStringOrNull(router.query.date))); + const { isReady } = useTheme(props.profile.theme); useEffect(() => { setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess())); @@ -99,9 +99,9 @@ export default function Success(props: inferSSRProps)

{props.eventType.requiresConfirmation - ? `${ - props.user.name || props.user.username - } still needs to confirm or reject the booking.` + ? props.profile.name !== null + ? `${props.profile.name} still needs to confirm or reject the booking.` + : "Your booking still needs to be confirmed or rejected." : `We emailed you and the other attendees a calendar invitation with all the details.`}

@@ -224,7 +224,7 @@ export default function Success(props: inferSSRProps)
)} - {!isBrandingHidden(props.user) && ( + {!props.hideBranding && ( @@ -239,35 +239,14 @@ export default function Success(props: inferSSRProps) ); } -export async function getServerSideProps(context: GetServerSidePropsContext) { - const username = asStringOrNull(context.query.user); +export async function getServerSideProps(context) { const typeId = parseInt(asStringOrNull(context.query.type) ?? ""); - if (!username || isNaN(typeId)) { + if (isNaN(typeId)) { return { notFound: true, }; } - - const user = await prisma.user.findUnique({ - where: { - username, - }, - select: { - username: true, - name: true, - bio: true, - avatar: true, - hideBranding: true, - theme: true, - plan: true, - }, - }); - if (!user) { - return { - notFound: true, - }; - } - const eventType = await prisma.eventType.findUnique({ + const eventType: EventType = await prisma.eventType.findUnique({ where: { id: typeId, }, @@ -278,17 +257,61 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { length: true, eventName: true, requiresConfirmation: true, + userId: true, + users: { + select: { + name: true, + hideBranding: true, + plan: true, + theme: true, + }, + }, + team: { + select: { + name: true, + hideBranding: true, + }, + }, }, }); + if (!eventType) { return { notFound: true, }; } + if (!eventType.users.length && eventType.userId) { + eventType.users.push( + await prisma.user.findUnique({ + where: { + id: eventType.userId, + }, + select: { + theme: true, + hideBranding: true, + name: true, + plan: true, + }, + }) + ); + } + + if (!eventType.users.length) { + return { + notFound: true, + }; + } + + const profile = { + name: eventType.team?.name || eventType.users[0]?.name || null, + theme: (!eventType.team?.name && eventType.users[0]?.theme) || null, + }; + return { props: { - user, + hideBranding: eventType.team ? eventType.team.hideBranding : isBrandingHidden(eventType.users[0]), + profile, eventType, }, }; diff --git a/pages/team/[idOrSlug].tsx b/pages/team/[idOrSlug].tsx deleted file mode 100644 index 601a06df82..0000000000 --- a/pages/team/[idOrSlug].tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { GetServerSideProps } from "next"; -import { HeadSeo } from "@components/seo/head-seo"; -import Theme from "@components/Theme"; -import { getTeam } from "@lib/teams/getTeam"; -import Team from "@components/team/screens/Team"; - -export default function Page(props) { - const { isReady } = Theme(); - - return ( - isReady && ( -
- -
- -
-
- ) - ); -} - -export const getServerSideProps: GetServerSideProps = async (context) => { - const teamIdOrSlug = Array.isArray(context.query?.idOrSlug) - ? context.query.idOrSlug.pop() - : context.query.idOrSlug; - - const team = await getTeam(teamIdOrSlug); - - if (!team) { - return { - notFound: true, - }; - } - - return { - props: { - team, - }, - }; -}; - -// Auxiliary methods -export function getRandomColorCode(): string { - let color = "#"; - for (let idx = 0; idx < 6; idx++) { - color += Math.floor(Math.random() * 10); - } - return color; -} diff --git a/pages/team/[slug].tsx b/pages/team/[slug].tsx new file mode 100644 index 0000000000..eabae5f0ad --- /dev/null +++ b/pages/team/[slug].tsx @@ -0,0 +1,153 @@ +import { InferGetServerSidePropsType } from "next"; +import Link from "next/link"; +import { HeadSeo } from "@components/seo/head-seo"; +import useTheme from "@lib/hooks/useTheme"; +import { ArrowRightIcon } from "@heroicons/react/solid"; +import prisma from "@lib/prisma"; +import Avatar from "@components/ui/Avatar"; +import Text from "@components/ui/Text"; +import React from "react"; +import { defaultAvatarSrc } from "@lib/profile"; +import EventTypeDescription from "@components/eventtype/EventTypeDescription"; +import Team from "@components/team/screens/Team"; +import { useToggleQuery } from "@lib/hooks/useToggleQuery"; +import AvatarGroup from "@components/ui/AvatarGroup"; + +function TeamPage({ team }: InferGetServerSidePropsType) { + const { isReady } = useTheme(); + const showMembers = useToggleQuery("members"); + + const eventTypes = ( + + ); + + return ( + isReady && ( +
+ +
+
+ + {team.name} +
+ {(showMembers.isOn || !team.eventTypes.length) && } + {!showMembers.isOn && team.eventTypes.length && ( +
+ {eventTypes} + +
+ )} +
+
+ ) + ); +} + +export const getServerSideProps = async (context) => { + const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug; + + const teamSelectInput = { + id: true, + name: true, + slug: true, + logo: true, + members: { + select: { + user: { + select: { + username: true, + avatar: true, + name: true, + id: true, + bio: true, + }, + }, + }, + }, + eventTypes: { + where: { + hidden: false, + }, + select: { + id: true, + title: true, + description: true, + length: true, + slug: true, + schedulingType: true, + users: { + select: { + id: true, + name: true, + avatar: true, + email: true, + }, + }, + }, + }, + }; + + const team = await prisma.team.findUnique({ + where: { + slug, + }, + select: teamSelectInput, + }); + + if (!team) { + return { + notFound: true, + }; + } + + team.eventTypes = team.eventTypes.map((type) => ({ + ...type, + users: type.users.map((user) => ({ + ...user, + avatar: user.avatar || defaultAvatarSrc({ email: user.email }), + })), + })); + + return { + props: { + team, + }, + }; +}; + +export default TeamPage; diff --git a/pages/team/[slug]/[type].tsx b/pages/team/[slug]/[type].tsx new file mode 100644 index 0000000000..8ace6e5e09 --- /dev/null +++ b/pages/team/[slug]/[type].tsx @@ -0,0 +1,90 @@ +import { Availability, EventType } from "@prisma/client"; +import prisma from "@lib/prisma"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { asStringOrNull } from "@lib/asStringOrNull"; +import AvailabilityPage from "@components/booking/pages/AvailabilityPage"; + +export default function TeamType(props: InferGetServerSidePropsType) { + return ; +} + +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + // get query params and typecast them to string + // (would be even better to assert them instead of typecasting) + const slugParam = asStringOrNull(context.query.slug); + const typeParam = asStringOrNull(context.query.type); + + if (!slugParam || !typeParam) { + throw new Error(`File is not named [idOrSlug]/[user]`); + } + + const team = await prisma.team.findFirst({ + where: { + slug: slugParam, + }, + select: { + id: true, + name: true, + slug: true, + logo: true, + eventTypes: { + where: { + slug: typeParam, + }, + select: { + id: true, + users: { + select: { + id: true, + name: true, + avatar: true, + username: true, + timeZone: true, + }, + }, + title: true, + availability: true, + description: true, + length: true, + schedulingType: true, + }, + }, + }, + }); + + if (!team || team.eventTypes.length != 1) { + return { + notFound: true, + } as const; + } + + const profile = { + name: team.name, + slug: team.slug, + image: team.logo || null, + }; + + const eventType: EventType = team.eventTypes[0]; + + const getWorkingHours = (providesAvailability: { availability: Availability[] }) => + providesAvailability.availability && providesAvailability.availability.length + ? providesAvailability.availability + : null; + + const workingHours = getWorkingHours(eventType) || []; + workingHours.sort((a, b) => a.startTime - b.startTime); + + const eventTypeObject = Object.assign({}, eventType, { + periodStartDate: eventType.periodStartDate?.toString() ?? null, + periodEndDate: eventType.periodEndDate?.toString() ?? null, + }); + + return { + props: { + profile, + team, + eventType: eventTypeObject, + workingHours, + }, + }; +}; diff --git a/pages/team/[slug]/book.tsx b/pages/team/[slug]/book.tsx new file mode 100644 index 0000000000..49daa763ad --- /dev/null +++ b/pages/team/[slug]/book.tsx @@ -0,0 +1,90 @@ +import prisma from "@lib/prisma"; +import { EventType } from "@prisma/client"; +import "react-phone-number-input/style.css"; +import BookingPage from "@components/booking/pages/BookingPage"; +import { InferGetServerSidePropsType } from "next"; + +export default function TeamBookingPage(props: InferGetServerSidePropsType) { + return ; +} + +export async function getServerSideProps(context) { + const eventTypeId = parseInt(context.query.type); + if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) { + return { + notFound: true, + } as const; + } + + const eventType: EventType = await prisma.eventType.findUnique({ + where: { + id: eventTypeId, + }, + select: { + id: true, + title: true, + slug: true, + description: true, + length: true, + locations: true, + customInputs: true, + periodType: true, + periodDays: true, + periodStartDate: true, + periodEndDate: true, + periodCountCalendarDays: true, + team: { + select: { + slug: true, + name: true, + logo: true, + }, + }, + users: { + select: { + avatar: true, + name: true, + }, + }, + }, + }); + + const eventTypeObject = [eventType].map((e) => { + return { + ...e, + periodStartDate: e.periodStartDate?.toString() ?? null, + periodEndDate: e.periodEndDate?.toString() ?? null, + }; + })[0]; + + let booking = null; + + if (context.query.rescheduleUid) { + booking = await prisma.booking.findFirst({ + where: { + uid: context.query.rescheduleUid, + }, + select: { + description: true, + attendees: { + select: { + email: true, + name: true, + }, + }, + }, + }); + } + + return { + props: { + profile: { + ...eventTypeObject.team, + slug: "team/" + eventTypeObject.slug, + image: eventTypeObject.team.logo, + }, + eventType: eventTypeObject, + booking, + }, + }; +} diff --git a/prisma/migrations/20210908042159_teams_feature/migration.sql b/prisma/migrations/20210908042159_teams_feature/migration.sql new file mode 100644 index 0000000000..8a784951e7 --- /dev/null +++ b/prisma/migrations/20210908042159_teams_feature/migration.sql @@ -0,0 +1,30 @@ +-- CreateEnum +CREATE TYPE "SchedulingType" AS ENUM ('roundRobin', 'collective'); + +-- DropForeignKey +ALTER TABLE "EventType" DROP CONSTRAINT "EventType_userId_fkey"; + +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "schedulingType" "SchedulingType", +ADD COLUMN "teamId" INTEGER; + +-- CreateTable +CREATE TABLE "_user_eventtype" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_user_eventtype_AB_unique" ON "_user_eventtype"("A", "B"); + +-- CreateIndex +CREATE INDEX "_user_eventtype_B_index" ON "_user_eventtype"("B"); + +-- AddForeignKey +ALTER TABLE "EventType" ADD FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_user_eventtype" ADD FOREIGN KEY ("A") REFERENCES "EventType"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_user_eventtype" ADD FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20210908235519_undo_unique_user_id_slug/migration.sql b/prisma/migrations/20210908235519_undo_unique_user_id_slug/migration.sql new file mode 100644 index 0000000000..d4c51ae506 --- /dev/null +++ b/prisma/migrations/20210908235519_undo_unique_user_id_slug/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "EventType.userId_slug_unique"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4777c2e893..e66f9646a9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,11 @@ generator client { provider = "prisma-client-js" } +enum SchedulingType { + ROUND_ROBIN @map("roundRobin") + COLLECTIVE @map("collective") +} + model EventType { id Int @id @default(autoincrement()) title String @@ -18,8 +23,10 @@ model EventType { locations Json? length Int hidden Boolean @default(false) - user User? @relation(fields: [userId], references: [id]) + users User[] @relation("user_eventtype") userId Int? + team Team? @relation(fields: [teamId], references: [id]) + teamId Int? bookings Booking[] availability Availability[] eventName String? @@ -32,9 +39,8 @@ model EventType { periodCountCalendarDays Boolean? requiresConfirmation Boolean @default(false) minimumBookingNotice Int @default(120) + schedulingType SchedulingType? Schedule Schedule[] - - @@unique([userId, slug]) } model Credential { @@ -68,7 +74,7 @@ model User { hideBranding Boolean @default(false) theme String? createdDate DateTime @default(now()) @map(name: "created") - eventTypes EventType[] + eventTypes EventType[] @relation("user_eventtype") credentials Credential[] teams Membership[] bookings Booking[] @@ -79,17 +85,19 @@ model User { plan UserPlan @default(PRO) Schedule Schedule[] + @@map(name: "users") } model Team { - id Int @default(autoincrement()) @id + id Int @id @default(autoincrement()) name String? - slug String? @unique + slug String? @unique logo String? bio String? - hideBranding Boolean @default(false) + hideBranding Boolean @default(false) members Membership[] + eventTypes EventType[] } enum MembershipRole { diff --git a/scripts/seed.ts b/scripts/seed.ts index 4413d63e29..84bef5af5c 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -1,9 +1,59 @@ import { hashPassword } from "../lib/auth"; -import { Prisma, PrismaClient } from "@prisma/client"; +import { Prisma, PrismaClient, UserPlan } from "@prisma/client"; +import dayjs from "dayjs"; +import { uuid } from "short-uuid"; const prisma = new PrismaClient(); +async function createBookingForEventType(opts: { + uid: string; + title: string; + slug: string; + startTime: Date | string; + endTime: Date | string; + userEmail: string; +}) { + const eventType = await prisma.eventType.findFirst({ + where: { + slug: opts.slug, + }, + }); + + if (!eventType) { + // should not happen + throw new Error("Eventtype missing"); + } + + const bookingData: Prisma.BookingCreateArgs["data"] = { + uid: opts.uid, + title: opts.title, + startTime: opts.startTime, + endTime: opts.endTime, + user: { + connect: { + email: opts.userEmail, + }, + }, + attendees: { + create: { + email: opts.userEmail, + name: "Some name", + timeZone: "Europe/London", + }, + }, + eventType: { + connect: { + id: eventType.id, + }, + }, + }; + + await prisma.booking.create({ + data: bookingData, + }); +} + async function createUserAndEventType(opts: { - user: Omit & { password: string; email: string }; + user: { email: string; password: string; username: string; plan: UserPlan }; eventTypes: Array; }) { const userData: Prisma.UserCreateArgs["data"] = { @@ -24,16 +74,34 @@ async function createUserAndEventType(opts: { for (const rawData of opts.eventTypes) { const eventTypeData: Prisma.EventTypeCreateArgs["data"] = { ...rawData }; eventTypeData.userId = user.id; - await prisma.eventType.upsert({ + + const eventType = await prisma.eventType.findFirst({ where: { - userId_slug: { - slug: eventTypeData.slug, - userId: user.id, + slug: eventTypeData.slug, + users: { + some: { + id: eventTypeData.userId, + }, }, }, - update: eventTypeData, - create: eventTypeData, + select: { + id: true, + }, }); + + if (eventType) { + await prisma.eventType.update({ + where: { + id: eventType.id, + }, + data: eventTypeData, + }); + } else { + await prisma.eventType.create({ + data: eventTypeData, + }); + } + console.log( `\tšŸ“† Event type ${eventTypeData.slug}, length ${eventTypeData.length}: http://localhost:3000/${user.username}/${eventTypeData.slug}` ); @@ -104,6 +172,16 @@ async function main() { }, ], }); + + await createBookingForEventType({ + title: "30min", + slug: "30min", + startTime: dayjs().add(1, "day").toDate(), + endTime: dayjs().add(1, "day").add(60, "minutes").toDate(), + uid: uuid(), + userEmail: "pro@example.com", + }); + await createUserAndEventType({ user: { email: "trial@example.com", diff --git a/yarn.lock b/yarn.lock index 8532018c01..e5f7d6949b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -177,9 +177,9 @@ js-tokens "^4.0.0" "@babel/parser@^7.1.0", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5", "@babel/parser@^7.7.2": - version "7.15.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.5.tgz#d33a58ca69facc05b26adfe4abebfed56c1c2dac" - integrity sha512-2hQstc6I7T6tQsWzlboMh3SgMRPaS4H6H7cPQsJkdzTzEGqQrpLDsE2BGASU5sBPoEQyHzeqU6C8uKbFeEk6sg== + version "7.15.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.6.tgz#043b9aa3c303c0722e5377fef9197f4cf1796549" + integrity sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -326,9 +326,9 @@ to-fast-properties "^2.0.0" "@babel/types@^7.0.0", "@babel/types@^7.15.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.15.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.4.tgz#74eeb86dbd6748d2741396557b9860e57fce0a0d" - integrity sha512-0f1HJFuGmmbrKTCZtbm3cU+b/AqdEYk5toj5iQur58xkVMlS0JWaKxTBSmCXd47uiN7vbcozAupm6Mvs80GNhw== + version "7.15.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f" + integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== dependencies: "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" @@ -835,13 +835,6 @@ "@babel/runtime" "^7.13.10" csstype "^3.0.4" -"@radix-ui/primitive@0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-0.0.5.tgz#8464fb4db04401bde72d36e27e05714080668d40" - integrity sha512-VeL6A5LpKYRJhDDj5tCTnzP3zm+FnvybsAkgBHQ4LUPPBnqRdWLoyKpZhlwFze/z22QHINaTIcE9Z/fTcrUR1g== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-0.1.0.tgz#6206b97d379994f0d1929809db035733b337e543" @@ -901,13 +894,6 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-compose-refs@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz#cff6e780a0f73778b976acff2c2a5b6551caab95" - integrity sha512-eyclbh+b77k+69Dk72q3694OHrn9B3QsoIRx7ywX341U9RK1ThgQjMFZoPtmZNQTksXHLNEiefR8hGVeFyInGg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-context@0.0.5": version "0.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-0.0.5.tgz#7c15f46795d7765dabfaf6f9c53791ad28c521c2" @@ -915,61 +901,73 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-context@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-0.1.0.tgz#670a7a2a63f8380a7cb5ff0bce87d51bdb065c5c" - integrity sha512-o8h7SP6ePEBLC33BsHiuFqW898c+wiyBiY2ZC2xFJUUnHj1Z6XrQdZCNjm3/VuhljMkPrIA5xC4swVWBo/gzOA== +"@radix-ui/react-dialog@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.0.20.tgz#b26607bea68fc20067d06fab996bac7f1acf68c1" + integrity sha512-fXgWxWyvmNiimxrFGdvUNve0tyQEFyPwrNgkSi6Xiha9cX8sqWdiYWq500zhzUQQFJVS7No73ylx8kgrI7SoLw== dependencies: "@babel/runtime" "^7.13.10" - -"@radix-ui/react-dialog@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.1.0.tgz#1e0471e03abc9012f2a2dc1644f7e844ccf44c94" - integrity sha512-yy833v6mSkqlhdDR7R0+sZJZd5OyEzsjeJfAuJoWRMSW2/2S78vTUgk1sRTXzT+6unoQOQ9teevURNjwAfX0Ug== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "0.1.0" - "@radix-ui/react-compose-refs" "0.1.0" - "@radix-ui/react-context" "0.1.0" - "@radix-ui/react-dismissable-layer" "0.1.0" - "@radix-ui/react-focus-guards" "0.1.0" - "@radix-ui/react-focus-scope" "0.1.0" - "@radix-ui/react-id" "0.1.0" - "@radix-ui/react-portal" "0.1.0" - "@radix-ui/react-presence" "0.1.0" - "@radix-ui/react-primitive" "0.1.0" - "@radix-ui/react-use-controllable-state" "0.1.0" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-dismissable-layer" "0.0.15" + "@radix-ui/react-focus-guards" "0.0.7" + "@radix-ui/react-focus-scope" "0.0.15" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-portal" "0.0.15" + "@radix-ui/react-presence" "0.0.15" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-slot" "0.0.12" + "@radix-ui/react-use-controllable-state" "0.0.6" aria-hidden "^1.1.1" react-remove-scroll "^2.4.0" -"@radix-ui/react-dismissable-layer@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.1.0.tgz#ab2ec7490a56f7b46afa8dea08d109b9e4643c3b" - integrity sha512-xQSXEP7rHkAe0sY1Ggd9CS0IuYXhjks0e+mtPu6LgJBXhlOTDVj4MeJC8fKAP+H1sKMygcrEEagb6M5GXEDvzg== +"@radix-ui/react-dismissable-layer@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.0.15.tgz#02c0e68684d60933c82b5af6793c87a5f9ee0750" + integrity sha512-2zABi8rh/t6liFfRLBw6h+B7MNNFxVQrgYfWRMs1elNX41z3G2vLoBlWdqGzAlYrtqEr/6CL4pQfhwVtd7rNGw== dependencies: "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "0.1.0" - "@radix-ui/react-primitive" "0.1.0" - "@radix-ui/react-use-body-pointer-events" "0.1.0" - "@radix-ui/react-use-callback-ref" "0.1.0" - "@radix-ui/react-use-escape-keydown" "0.1.0" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-body-pointer-events" "0.0.7" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-escape-keydown" "0.0.6" -"@radix-ui/react-focus-guards@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.0.tgz#ba3b6f902cba7826569f8edc21ff8223dece7def" - integrity sha512-kRx/swAjEfBpQ3ns7J3H4uxpXuWCqN7MpALiSDOXiyo2vkWv0L9sxvbpZeTulINuE3CGMzicVMuNc/VWXjFKOg== +"@radix-ui/react-dropdown-menu@^0.0.23": + version "0.0.23" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.0.23.tgz#cd171b96750b4f26e3a481a8ecfb622b797d1e1f" + integrity sha512-zb/eavkvQpRYXfInh20q84b/1zEitlJlbdIHqVCNxMhhRDxa4wCyhLUlx400jR0s6Hl7EmU6WaNY4VYfdskrUQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-menu" "0.0.22" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-controllable-state" "0.0.6" + +"@radix-ui/react-focus-guards@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.0.7.tgz#285ed081c877587acd4ee7e6d8260bdf9044e922" + integrity sha512-enAsmrUunptHVzPLTuZqwTP/X3WFBnyJ/jP9W+0g+bRvI3o7V1kxNc+T2Rp1eRTFBW+lUNnt08qkugPytyTRog== dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-focus-scope@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.1.0.tgz#24cb6433b4b5c733cdadc34cf36f9cd01ab9beb1" - integrity sha512-lquiYfEnkpqLDR9oO/h78OAY73jedZHVlBHJi2RZeSg3YM1UyyyGx+adZD+VWNphA/oEQG/RE5b7DteF4hhG8Q== +"@radix-ui/react-focus-scope@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.0.15.tgz#60917075e53ee72d2a473fba88eb31e7aaf7d841" + integrity sha512-zNgEe1lyLPfxa003VD8lCXaadGqCYhboA3X1WDNGes74lzJgLOPJgzLI0F/ksSokkx/yDDdReyOWui3/LCTqTw== dependencies: "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "0.1.0" - "@radix-ui/react-primitive" "0.1.0" - "@radix-ui/react-use-callback-ref" "0.1.0" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-callback-ref" "0.0.5" "@radix-ui/react-id@0.0.6": version "0.0.6" @@ -978,13 +976,6 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-id@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-0.1.0.tgz#d01067520fb8f4b09da3f914bfe6cb0f88c26721" - integrity sha512-SubMSz7rAtl6w8qZ9YBRbDe9GjW36JugBsc6aYqng8tFydvNtkuBMj86zN/x5QiomMo+r8ylBVvuWzRkS0WbBA== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-label@0.0.15": version "0.0.15" resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-0.0.15.tgz#ab70d7cd93d6ebaf2e1007cca70e9b1858bcb932" @@ -996,6 +987,32 @@ "@radix-ui/react-polymorphic" "0.0.13" "@radix-ui/react-primitive" "0.0.15" +"@radix-ui/react-menu@0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.0.22.tgz#5414b9be618a6f82bfeea80bac522854cfdd94f3" + integrity sha512-+aejYCzIKMbzk0MZYis0xXEpeLvAIf2/cpAgyzw7Fh+vEzRA4g4eKLLY/1yAxvyModnXBygaNKDQx1V0OlTIng== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-collection" "0.0.15" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-dismissable-layer" "0.0.15" + "@radix-ui/react-focus-guards" "0.0.7" + "@radix-ui/react-focus-scope" "0.0.15" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-popper" "0.0.18" + "@radix-ui/react-portal" "0.0.15" + "@radix-ui/react-presence" "0.0.15" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-roving-focus" "0.0.16" + "@radix-ui/react-slot" "0.0.12" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-direction" "0.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "^2.4.0" + "@radix-ui/react-polymorphic@0.0.13": version "0.0.13" resolved "https://registry.yarnpkg.com/@radix-ui/react-polymorphic/-/react-polymorphic-0.0.13.tgz#d010d48281626191c9513f11db5d82b37662418a" @@ -1016,15 +1033,6 @@ "@radix-ui/react-use-size" "0.1.0" "@radix-ui/rect" "0.1.0" -"@radix-ui/react-portal@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-0.1.0.tgz#5f72fa2f9837df9a5e27ca9ff7a63393ff8e1f0b" - integrity sha512-HiSDaQVlhoZWvp5Wy0JPPojqo31Z3efs890oyYkpKgRDWDdMYHzEWYZxC7pB60a6c6yM5JzjJc0bP7o6bye+/Q== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "0.1.0" - "@radix-ui/react-use-layout-effect" "0.1.0" - "@radix-ui/react-presence@0.0.15": version "0.0.15" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.0.15.tgz#4ff12feb436f1499148feb11c3a63a5d8fab568a" @@ -1034,15 +1042,6 @@ "@radix-ui/react-compose-refs" "0.0.5" "@radix-ui/react-use-layout-effect" "0.0.5" -"@radix-ui/react-presence@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.1.0.tgz#e7931009cbaa383f17be7d9863da9f0424efae7b" - integrity sha512-MIj5dywsSB1mWi7f9EqsxNoR5hfIScquYANbMdRmzxqNQzq2UrO8GEhOMXPo99YssdfpK9d0Pc9nLNwsFyq5Eg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "0.1.0" - "@radix-ui/react-use-layout-effect" "0.1.0" - "@radix-ui/react-primitive@0.0.15": version "0.0.15" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.0.15.tgz#c0cf609ee565a32969d20943e2697b42a04fbdf3" @@ -1051,13 +1050,21 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-polymorphic" "0.0.13" -"@radix-ui/react-primitive@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.1.0.tgz#4e6fb04ede36845cf3a061311a4f879c2051c1c5" - integrity sha512-ydO24k5Cvry5RCMfm5OJNnIwvxSIUuUZ3Kf6bu1GaSsDfBKiv5JeuQkipURW28KlX7I85Jr/I02JlE+Ec4HmWA== +"@radix-ui/react-roving-focus@0.0.16": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.0.16.tgz#79c7ee71cf9a3c7d55eefa562189c8de80252066" + integrity sha512-9kYHWfxMM7RreNiT8kxS/ivv077Nc9N3od8slJpBvfNuybLxLlHB0QdWbwaceM6hBm2MmRdfL5VlUndDRE9S7g== dependencies: "@babel/runtime" "^7.13.10" - "@radix-ui/react-slot" "0.1.0" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-collection" "0.0.15" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-controllable-state" "0.0.6" "@radix-ui/react-slider@^0.0.17": version "0.0.17" @@ -1086,14 +1093,6 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "0.0.5" -"@radix-ui/react-slot@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-0.1.0.tgz#56965f2af80576f9e3fcdbba839ef7fccbd3b577" - integrity sha512-ZuvAUhSK9EAE42b3+K7k7w4nF1uF+Wd4bFj2OCE1aSiW3tgiu7ZU+J61m2+RIDps0WLu95PUx6eZrnpfqBXFRg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "0.1.0" - "@radix-ui/react-switch@^0.0.15": version "0.0.15" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-0.0.15.tgz#675e0abd509ac211f6c9193fab786f17bd335de3" @@ -1132,13 +1131,13 @@ "@radix-ui/react-use-rect" "0.1.0" "@radix-ui/react-visually-hidden" "0.1.0" -"@radix-ui/react-use-body-pointer-events@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.0.tgz#29b211464493f8ca5149ce34b96b95abbc97d741" - integrity sha512-svPyoHCcwOq/vpWNEvdH/yD91vN9p8BtiozNQbjVmJRxQ/vS12zqk70AxTGWe+2ZKHq2sggpEQNTv1JHyVFlnQ== +"@radix-ui/react-use-body-pointer-events@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.7.tgz#e4249690ca0db85c969400e867476206feda4d1e" + integrity sha512-mXAGyb8mhVjRqtpKPeZePuvee40bgsWpt378oQrIcLU1uZNbNX9eyrIPnnL9OMLAvxqloAOClVj0PZ1bMQmfDw== dependencies: "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-layout-effect" "0.1.0" + "@radix-ui/react-use-layout-effect" "0.0.5" "@radix-ui/react-use-callback-ref@0.0.5": version "0.0.5" @@ -1147,13 +1146,6 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-use-callback-ref@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.1.0.tgz#934b6e123330f5b3a6b116460e6662cbc663493f" - integrity sha512-Va041McOFFl+aV+sejvl0BS2aeHx86ND9X/rVFmEFQKTXCp6xgUK0NGUAGcgBlIjnJSbMYPGEk1xKSSlVcN2Aw== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-controllable-state@0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.0.6.tgz#c4b16bc911a25889333388a684a04df937e5fec7" @@ -1162,14 +1154,6 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "0.0.5" -"@radix-ui/react-use-controllable-state@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz#4fced164acfc69a4e34fb9d193afdab973a55de1" - integrity sha512-zv7CX/PgsRl46a52Tl45TwqwVJdmqnlQEQhaYMz/yBOD2sx2gCkCFSoF/z9mpnYWmS6DTLNTg5lIps3fV6EnXg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-callback-ref" "0.1.0" - "@radix-ui/react-use-direction@0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-direction/-/react-use-direction-0.0.1.tgz#9ac72eb6d9902ed505c8a34048981d94f9433e14" @@ -1192,13 +1176,6 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-use-layout-effect@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz#ebf71bd6d2825de8f1fbb984abf2293823f0f223" - integrity sha512-+wdeS51Y+E1q1Wmd+1xSSbesZkpVj4jsg0BojCbopWvgq5iBvixw5vgemscdh58ep98BwUbsFYnrywFhV9yrVg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-previous@0.0.5": version "0.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.0.5.tgz#75191d1fa0ac24c560fe8cfbaa2f1174858cbb2f" @@ -1384,9 +1361,9 @@ integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw== "@types/node@*", "@types/node@^16.6.1": - version "16.9.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.0.tgz#d9512fe037472dcb58931ce19f837348db828a62" - integrity sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag== + version "16.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" + integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g== "@types/node@^14.14.31": version "14.17.15" @@ -3023,9 +3000,9 @@ efrt-unpack@2.2.0: integrity sha512-9xUSSj7qcUxz+0r4X3+bwUNttEfGfK5AH+LVa1aTpqdAfrN5VhROYCfcF+up4hp5OL7IUKcZJJrzAGipQRDoiQ== electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.830: - version "1.3.833" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.833.tgz#e1394eb32ab8a9430ffd7d5adf632ce6c3b05e18" - integrity sha512-h+9aVaUHjyunLqtCjJF2jrJ73tYcJqo2cCGKtVAXH9WmnBsb8hiChRQ0P1uXjdxR6Wcfxibephy41c1YlZA/pA== + version "1.3.834" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.834.tgz#57a94f4a6529c926a8c332b184b291230a3f2140" + integrity sha512-9hnYJOlj2zbVn59Oy1R2mW/jntsRG7Gy56/aAOq8s29DzDYW/kOrq/ryJXGAQRRMg4MreHjI63XavGZTsnPubg== elliptic@^6.5.3: version "6.5.4"