Feature/field prefills (#1249)
* Needs more testing, but looks functional * Add metadata feature to booking create payload * Forward URL parameters given in link * Moved stringifying of custom inputs to backend
This commit is contained in:
parent
8d1d3fcc7a
commit
8c1b69cc0f
5 changed files with 310 additions and 237 deletions
|
@ -9,10 +9,11 @@ import { EventTypeCustomInputType } from "@prisma/client";
|
|||
import dayjs from "dayjs";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { stringify } from "querystring";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { ReactMultiEmail } from "react-multi-email";
|
||||
import { useMutation } from "react-query";
|
||||
|
||||
import { createPaymentLink } from "@ee/lib/stripe/client";
|
||||
|
||||
|
@ -23,10 +24,11 @@ import useTheme from "@lib/hooks/useTheme";
|
|||
import { LocationType } from "@lib/location";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
import { parseZone } from "@lib/parseZone";
|
||||
import slugify from "@lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { BookingCreateBody } from "@lib/types/booking";
|
||||
|
||||
import CustomBranding from "@components/CustomBranding";
|
||||
import { Form } from "@components/form/fields";
|
||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||
import { Button } from "@components/ui/Button";
|
||||
import PhoneInput from "@components/ui/form/PhoneInput";
|
||||
|
@ -39,31 +41,78 @@ type BookingPageProps = BookPageProps | TeamBookingPageProps;
|
|||
const BookingPage = (props: BookingPageProps) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const { rescheduleUid } = router.query;
|
||||
/*
|
||||
* This was too optimistic
|
||||
* I started, then I remembered what a beast book/event.ts is
|
||||
* Gave up shortly after. One day. Maybe.
|
||||
*
|
||||
const mutation = trpc.useMutation("viewer.bookEvent", {
|
||||
onSuccess: ({ booking }) => {
|
||||
// go to success page.
|
||||
},
|
||||
});*/
|
||||
const mutation = useMutation(createBooking, {
|
||||
onSuccess: async ({ attendees, paymentUid, ...responseData }) => {
|
||||
if (paymentUid) {
|
||||
return await router.push(
|
||||
createPaymentLink({
|
||||
paymentUid,
|
||||
date,
|
||||
name: attendees[0].name,
|
||||
absolute: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const location = (function humanReadableLocation(location) {
|
||||
if (!location) {
|
||||
return;
|
||||
}
|
||||
if (location.includes("integration")) {
|
||||
return t("web_conferencing_details_to_follow");
|
||||
}
|
||||
return location;
|
||||
})(responseData.location);
|
||||
|
||||
return router.push({
|
||||
pathname: "/success",
|
||||
query: {
|
||||
date,
|
||||
type: props.eventType.id,
|
||||
user: props.profile.slug,
|
||||
reschedule: !!rescheduleUid,
|
||||
name: attendees[0].name,
|
||||
email: attendees[0].email,
|
||||
location,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const rescheduleUid = router.query.rescheduleUid as string;
|
||||
const { isReady } = 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<LocationType>(
|
||||
locations.length === 1 ? locations[0].type : ""
|
||||
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
|
||||
const locations: { type: LocationType }[] = useMemo(
|
||||
() => (props.eventType.locations as { type: LocationType }[]) || [],
|
||||
[props.eventType.locations]
|
||||
);
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
if (router.query.guest) {
|
||||
setGuestToggle(true);
|
||||
}
|
||||
}, [router.query.guest]);
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.timeSelected, collectPageParameters()));
|
||||
}, []);
|
||||
|
||||
function toggleGuestEmailInput() {
|
||||
setGuestToggle(!guestToggle);
|
||||
}
|
||||
}, [telemetry]);
|
||||
|
||||
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
|
||||
|
||||
|
@ -76,113 +125,105 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
[LocationType.Daily]: "Daily.co 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 ? t("yes") : t("no"));
|
||||
} else {
|
||||
return input.label + "\n" + data.value;
|
||||
}
|
||||
}
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
if (!!notes && !!event.target.notes.value) {
|
||||
notes += `\n\n${t("additional_notes")}:\n` + event.target.notes.value;
|
||||
} else {
|
||||
notes += event.target.notes.value;
|
||||
}
|
||||
|
||||
const payload: BookingCreateBody = {
|
||||
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,
|
||||
timeZone: timeZone(),
|
||||
language: i18n.language,
|
||||
};
|
||||
if (typeof rescheduleUid === "string") payload.rescheduleUid = rescheduleUid;
|
||||
if (typeof router.query.user === "string") 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 content = await createBooking(payload).catch((e) => {
|
||||
console.error(e.message);
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
});
|
||||
|
||||
if (content?.id) {
|
||||
const params: { [k: string]: any } = {
|
||||
date,
|
||||
type: props.eventType.id,
|
||||
user: props.profile.slug,
|
||||
reschedule: !!rescheduleUid,
|
||||
name: payload.name,
|
||||
email: payload.email,
|
||||
};
|
||||
|
||||
if (payload["location"]) {
|
||||
if (payload["location"].includes("integration")) {
|
||||
params.location = t("web_conferencing_details_to_follow");
|
||||
} else {
|
||||
params.location = payload["location"];
|
||||
}
|
||||
}
|
||||
|
||||
const query = stringify(params);
|
||||
let successUrl = `/success?${query}`;
|
||||
|
||||
if (content?.paymentUid) {
|
||||
successUrl = createPaymentLink({
|
||||
paymentUid: content?.paymentUid,
|
||||
name: payload.name,
|
||||
date,
|
||||
absolute: false,
|
||||
});
|
||||
}
|
||||
|
||||
await router.push(successUrl);
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
}
|
||||
type BookingFormValues = {
|
||||
name: string;
|
||||
email: string;
|
||||
notes?: string;
|
||||
locationType?: LocationType;
|
||||
guests?: string[];
|
||||
phone?: string;
|
||||
customInputs?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
event.preventDefault();
|
||||
book();
|
||||
};
|
||||
|
||||
const bookingHandler = useCallback(_bookingHandler, [guestEmails]);
|
||||
// can be shortened using .filter(), except TypeScript doesn't know what to make of the types.
|
||||
const guests = router.query.guest
|
||||
? Array.isArray(router.query.guest)
|
||||
? router.query.guest
|
||||
: [router.query.guest]
|
||||
: [];
|
||||
|
||||
const bookingForm = useForm<BookingFormValues>({
|
||||
defaultValues: {
|
||||
name: (router.query.name as string) || "",
|
||||
email: (router.query.email as string) || "",
|
||||
notes: (router.query.notes as string) || "",
|
||||
guests,
|
||||
customInputs: props.eventType.customInputs.reduce(
|
||||
(customInputs, input) => ({
|
||||
...customInputs,
|
||||
[input.id]: router.query[slugify(input.label)],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const selectedLocation = useWatch({
|
||||
control: bookingForm.control,
|
||||
name: "locationType",
|
||||
defaultValue: ((): LocationType | undefined => {
|
||||
if (router.query.location) {
|
||||
return router.query.location as LocationType;
|
||||
}
|
||||
if (locations.length === 1) {
|
||||
return locations[0]?.type;
|
||||
}
|
||||
})(),
|
||||
});
|
||||
|
||||
const getLocationValue = (booking: Pick<BookingFormValues, "locationType" | "phone">) => {
|
||||
const { locationType } = booking;
|
||||
switch (locationType) {
|
||||
case LocationType.Phone: {
|
||||
return booking.phone;
|
||||
}
|
||||
case LocationType.InPerson: {
|
||||
return locationInfo(locationType).address;
|
||||
}
|
||||
// Catches all other location types, such as Google Meet, Zoom etc.
|
||||
default:
|
||||
return selectedLocation;
|
||||
}
|
||||
};
|
||||
|
||||
const bookEvent = (booking: BookingFormValues) => {
|
||||
telemetry.withJitsu((jitsu) =>
|
||||
jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())
|
||||
);
|
||||
|
||||
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
|
||||
// <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type.
|
||||
const metadata = Object.keys(router.query)
|
||||
.filter((key) => key.startsWith("metadata"))
|
||||
.reduce(
|
||||
(metadata, key) => ({
|
||||
...metadata,
|
||||
[key.substring("metadata[".length, key.length - 1)]: router.query[key],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
mutation.mutate({
|
||||
...booking,
|
||||
start: dayjs(date).format(),
|
||||
end: dayjs(date).add(props.eventType.length, "minute").format(),
|
||||
eventTypeId: props.eventType.id,
|
||||
timeZone: timeZone(),
|
||||
language: i18n.language,
|
||||
rescheduleUid,
|
||||
user: router.query.user as string,
|
||||
location: getLocationValue(booking),
|
||||
metadata,
|
||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
label: props.eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
value: booking.customInputs![inputId],
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -253,20 +294,20 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<p className="mb-8 text-gray-600 dark:text-white">{props.eventType.description}</p>
|
||||
</div>
|
||||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
||||
<form onSubmit={bookingHandler}>
|
||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("your_name")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
{...bookingForm.register("name")}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
required
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder="John Doe"
|
||||
defaultValue={props.booking ? props.booking.attendees[0].name : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -278,6 +319,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
{...bookingForm.register("email")}
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
|
@ -285,7 +327,6 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
required
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder="you@example.com"
|
||||
defaultValue={props.booking ? props.booking.attendees[0].email : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -294,16 +335,14 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("location")}
|
||||
</span>
|
||||
{locations.map((location) => (
|
||||
<label key={location.type} className="block">
|
||||
{locations.map((location, i) => (
|
||||
<label key={i} className="block">
|
||||
<input
|
||||
type="radio"
|
||||
required
|
||||
onChange={(e) => setSelectedLocation(e.target.value)}
|
||||
className="w-4 h-4 mr-2 text-black border-gray-300 location focus:ring-black"
|
||||
name="location"
|
||||
{...bookingForm.register("locationType", { required: true })}
|
||||
value={location.type}
|
||||
checked={selectedLocation === location.type}
|
||||
defaultChecked={selectedLocation === location.type}
|
||||
/>
|
||||
<span className="ml-2 text-sm dark:text-gray-500">
|
||||
{locationLabels[location.type]}
|
||||
|
@ -324,74 +363,78 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
</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 && (
|
||||
{props.eventType.customInputs
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((input) => (
|
||||
<div className="mb-4" key={input.id}>
|
||||
{input.type !== EventTypeCustomInputType.BOOL && (
|
||||
<label
|
||||
htmlFor={"custom_" + input.id}
|
||||
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
|
||||
{input.label}
|
||||
</label>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||
<textarea
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
rows={3}
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder={input.placeholder}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXT && (
|
||||
<input
|
||||
type="text"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder={input.placeholder}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.NUMBER && (
|
||||
<input
|
||||
type="number"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder=""
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.BOOL && (
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className="w-4 h-4 mr-2 text-black border-gray-300 rounded focus:ring-black"
|
||||
placeholder=""
|
||||
/>
|
||||
<label
|
||||
htmlFor={"custom_" + input.id}
|
||||
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
|
||||
{input.label}
|
||||
</label>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||
<textarea
|
||||
name={"custom_" + input.id}
|
||||
id={"custom_" + input.id}
|
||||
required={input.required}
|
||||
rows={3}
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder={input.placeholder}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXT && (
|
||||
<input
|
||||
type="text"
|
||||
name={"custom_" + input.id}
|
||||
id={"custom_" + input.id}
|
||||
required={input.required}
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder={input.placeholder}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.NUMBER && (
|
||||
<input
|
||||
type="number"
|
||||
name={"custom_" + input.id}
|
||||
id={"custom_" + input.id}
|
||||
required={input.required}
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder=""
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.BOOL && (
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={"custom_" + input.id}
|
||||
id={"custom_" + input.id}
|
||||
className="w-4 h-4 mr-2 text-black border-gray-300 rounded focus:ring-black"
|
||||
placeholder=""
|
||||
required={input.required}
|
||||
/>
|
||||
<label
|
||||
htmlFor={"custom_" + input.id}
|
||||
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
|
||||
{input.label}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!props.eventType.disableGuests && (
|
||||
<div className="mb-4">
|
||||
{!guestToggle && (
|
||||
<label
|
||||
onClick={toggleGuestEmailInput}
|
||||
onClick={() => setGuestToggle(!guestToggle)}
|
||||
htmlFor="guests"
|
||||
className="block mb-1 text-sm font-medium text-blue-500 dark:text-white hover:cursor-pointer">
|
||||
className="block mb-1 text-sm font-medium dark:text-white hover:cursor-pointer">
|
||||
{/*<UserAddIcon className="inline-block w-5 h-5 mr-1 -mt-1" />*/}
|
||||
{t("additional_guests")}
|
||||
</label>
|
||||
)}
|
||||
|
@ -402,27 +445,31 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("guests")}
|
||||
</label>
|
||||
<ReactMultiEmail
|
||||
className="relative"
|
||||
placeholder="guest@example.com"
|
||||
emails={guestEmails}
|
||||
onChange={(_emails: string[]) => {
|
||||
setGuestEmails(_emails);
|
||||
}}
|
||||
getLabel={(
|
||||
email: string,
|
||||
index: number,
|
||||
removeEmail: (index: number) => void
|
||||
) => {
|
||||
return (
|
||||
<div data-tag key={index}>
|
||||
{email}
|
||||
<span data-tag-handle onClick={() => removeEmail(index)}>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
<Controller
|
||||
control={bookingForm.control}
|
||||
name="guests"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ReactMultiEmail
|
||||
className="relative"
|
||||
placeholder="guest@example.com"
|
||||
emails={value}
|
||||
onChange={onChange}
|
||||
getLabel={(
|
||||
email: string,
|
||||
index: number,
|
||||
removeEmail: (index: number) => void
|
||||
) => {
|
||||
return (
|
||||
<div data-tag key={index}>
|
||||
{email}
|
||||
<span data-tag-handle onClick={() => removeEmail(index)}>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -435,24 +482,23 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
{t("additional_notes")}
|
||||
</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
{...bookingForm.register("notes")}
|
||||
id="notes"
|
||||
rows={3}
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
|
||||
placeholder={t("share_additional_notes")}
|
||||
defaultValue={props.booking ? props.booking.description : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
<Button type="submit" loading={loading}>
|
||||
<Button type="submit" loading={mutation.isLoading}>
|
||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||
</Button>
|
||||
<Button color="secondary" type="button" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{error && (
|
||||
</Form>
|
||||
{mutation.isError && (
|
||||
<div className="p-4 mt-2 border-l-4 border-yellow-400 bg-yellow-50">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { Booking } from "@prisma/client";
|
||||
|
||||
import { LocationType } from "@lib/location";
|
||||
import { Attendee, Booking } from "@prisma/client";
|
||||
|
||||
export type BookingConfirmBody = {
|
||||
confirmed: boolean;
|
||||
|
@ -11,18 +9,23 @@ export type BookingCreateBody = {
|
|||
email: string;
|
||||
end: string;
|
||||
eventTypeId: number;
|
||||
guests: string[];
|
||||
location: LocationType;
|
||||
guests?: string[];
|
||||
location: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
notes?: string;
|
||||
rescheduleUid?: string;
|
||||
start: string;
|
||||
timeZone: string;
|
||||
users?: string[];
|
||||
user?: string;
|
||||
language: string;
|
||||
customInputs: { label: string; value: string }[];
|
||||
metadata: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type BookingResponse = Booking & {
|
||||
paymentUid?: string;
|
||||
attendees: Attendee[];
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@ const sendPayload = async (
|
|||
triggerEvent: string,
|
||||
createdAt: string,
|
||||
subscriberUrl: string,
|
||||
data: Omit<CalendarEvent, "language">,
|
||||
data: Omit<CalendarEvent, "language"> & { metadata?: { [key: string]: string } },
|
||||
template?: string | null
|
||||
) => {
|
||||
if (!subscriberUrl || !data) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
@ -18,6 +19,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
const { isReady } = useTheme(props.user.theme);
|
||||
const { user, eventTypes } = props;
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
|
||||
|
@ -30,15 +32,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
avatar={user.avatar || undefined}
|
||||
/>
|
||||
{isReady && (
|
||||
<div className="bg-neutral-50 dark:bg-black h-screen">
|
||||
<main className="max-w-3xl mx-auto py-24 px-4">
|
||||
<div className="h-screen bg-neutral-50 dark:bg-black">
|
||||
<main className="max-w-3xl px-4 py-24 mx-auto">
|
||||
<div className="mb-8 text-center">
|
||||
<Avatar
|
||||
imageSrc={user.avatar}
|
||||
className="mx-auto w-24 h-24 rounded-full mb-4"
|
||||
className="w-24 h-24 mx-auto mb-4 rounded-full"
|
||||
alt={nameOrUsername}
|
||||
/>
|
||||
<h1 className="font-cal text-3xl font-bold text-neutral-900 dark:text-white mb-1">
|
||||
<h1 className="mb-1 text-3xl font-bold font-cal text-neutral-900 dark:text-white">
|
||||
{nameOrUsername}
|
||||
</h1>
|
||||
<p className="text-neutral-500 dark:text-white">{user.bio}</p>
|
||||
|
@ -47,9 +49,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
{eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
className="group relative dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 bg-white hover:bg-gray-50 border border-neutral-200 hover:border-brand rounded-sm">
|
||||
<ArrowRightIcon className="absolute transition-opacity h-4 w-4 right-3 top-3 text-black dark:text-white opacity-0 group-hover:opacity-100" />
|
||||
<Link href={`/${user.username}/${type.slug}`}>
|
||||
className="relative bg-white border rounded-sm group dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 hover:bg-gray-50 border-neutral-200 hover:border-brand">
|
||||
<ArrowRightIcon className="absolute w-4 h-4 text-black transition-opacity opacity-0 right-3 top-3 dark:text-white group-hover:opacity-100" />
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query: {
|
||||
...router.query,
|
||||
},
|
||||
}}>
|
||||
<a className="block px-6 py-4" data-testid="event-type-link">
|
||||
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||
<EventTypeDescription eventType={type} />
|
||||
|
@ -59,9 +67,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
))}
|
||||
</div>
|
||||
{eventTypes.length === 0 && (
|
||||
<div className="shadow overflow-hidden rounded-sm">
|
||||
<div className="overflow-hidden rounded-sm shadow">
|
||||
<div className="p-8 text-center text-gray-400 dark:text-white">
|
||||
<h2 className="font-cal font-semibold text-3xl text-gray-600 dark:text-white">
|
||||
<h2 className="text-3xl font-semibold text-gray-600 font-cal dark:text-white">
|
||||
{t("uh_oh")}
|
||||
</h2>
|
||||
<p className="max-w-md mx-auto">{t("no_event_types_have_been_setup")}</p>
|
||||
|
|
|
@ -238,7 +238,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
const invitee = [{ email: reqBody.email, name: reqBody.name, timeZone: reqBody.timeZone }];
|
||||
const guests = reqBody.guests.map((guest) => {
|
||||
const guests = (reqBody.guests || []).map((guest) => {
|
||||
const g = {
|
||||
email: guest,
|
||||
name: "",
|
||||
|
@ -269,10 +269,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
t,
|
||||
};
|
||||
|
||||
const description =
|
||||
reqBody.customInputs.reduce((str, input) => str + input.label + "\n" + input.value + "\n\n", "") +
|
||||
t("additional_notes") +
|
||||
":\n" +
|
||||
reqBody.notes;
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: eventType.title,
|
||||
title: getEventName(eventNameObject),
|
||||
description: reqBody.notes,
|
||||
description,
|
||||
startTime: reqBody.start,
|
||||
endTime: reqBody.end,
|
||||
organizer: {
|
||||
|
@ -513,13 +519,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const eventTrigger = rescheduleUid ? "BOOKING_RESCHEDULED" : "BOOKING_CREATED";
|
||||
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
|
||||
const subscribers = await getSubscribers(user.id, eventTrigger);
|
||||
console.log("evt:", evt);
|
||||
console.log("evt:", {
|
||||
...evt,
|
||||
metadata: reqBody.metadata,
|
||||
});
|
||||
const promises = subscribers.map((sub) =>
|
||||
sendPayload(eventTrigger, new Date().toISOString(), sub.subscriberUrl, evt, sub.payloadTemplate).catch(
|
||||
(e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e);
|
||||
}
|
||||
)
|
||||
sendPayload(
|
||||
eventTrigger,
|
||||
new Date().toISOString(),
|
||||
sub.subscriberUrl,
|
||||
{
|
||||
...evt,
|
||||
metadata: reqBody.metadata,
|
||||
},
|
||||
sub.payloadTemplate
|
||||
).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
|
||||
|
|
Loading…
Reference in a new issue