Refactors custom input form & dialog (#853)
This commit is contained in:
parent
30f97117e8
commit
58de920951
14 changed files with 266 additions and 205 deletions
|
@ -7,6 +7,6 @@ module.exports = {
|
|||
semi: true,
|
||||
printWidth: 110,
|
||||
arrowParens: "always",
|
||||
importOrder: ["^@ee/(.*)$", "^@lib/(.*)$", "^@components/(.*)$", "^[./]"],
|
||||
importOrder: ["^@ee/(.*)$", "^@lib/(.*)$", "^@components/(.*)$", "^@(server|trcp)/(.*)$", "^[./]"],
|
||||
importOrderSeparation: true,
|
||||
};
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
/* legacy and soon deprecated, please refactor to use <Dialog> only */
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { CheckIcon, InformationCircleIcon } from "@heroicons/react/outline";
|
||||
import { Fragment, ReactNode } from "react";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
/**
|
||||
* @deprecated please refactor to use <Dialog> only
|
||||
*/
|
||||
export default function Modal(props: {
|
||||
heading: ReactNode;
|
||||
description: ReactNode;
|
||||
|
|
126
components/eventtype/CustomInputTypeForm.tsx
Normal file
126
components/eventtype/CustomInputTypeForm.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
||||
import React, { FC } from "react";
|
||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
||||
import Select, { OptionTypeBase } from "react-select";
|
||||
|
||||
const inputOptions: OptionTypeBase[] = [
|
||||
{ value: EventTypeCustomInputType.TEXT, label: "Text" },
|
||||
{ value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" },
|
||||
{ value: EventTypeCustomInputType.NUMBER, label: "Number" },
|
||||
{ value: EventTypeCustomInputType.BOOL, label: "Checkbox" },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
onSubmit: SubmitHandler<IFormInput>;
|
||||
onCancel: () => void;
|
||||
selectedCustomInput?: EventTypeCustomInput;
|
||||
}
|
||||
|
||||
type IFormInput = EventTypeCustomInput;
|
||||
|
||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
||||
const { selectedCustomInput } = props;
|
||||
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
|
||||
const { register, control, handleSubmit } = useForm<IFormInput>({
|
||||
defaultValues,
|
||||
});
|
||||
const selectedInputType = useWatch({ name: "type", control });
|
||||
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value)!;
|
||||
|
||||
const onCancel = () => {
|
||||
props.onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(props.onSubmit)}>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
||||
Input type
|
||||
</label>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
id="type"
|
||||
defaultValue={selectedInputOption}
|
||||
options={inputOptions}
|
||||
isSearchable={false}
|
||||
className="flex-1 block w-full min-w-0 mt-1 mb-2 border-gray-300 rounded-none focus:ring-primary-500 focus:border-primary-500 rounded-r-md sm:text-sm"
|
||||
onChange={(option) => field.onChange(option.value)}
|
||||
value={selectedInputOption}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
||||
Label
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
required
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
defaultValue={selectedCustomInput?.label}
|
||||
{...register("label", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(selectedInputType === EventTypeCustomInputType.TEXT ||
|
||||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
|
||||
<div className="mb-2">
|
||||
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
|
||||
Placeholder
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
id="placeholder"
|
||||
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||
defaultValue={selectedCustomInput?.placeholder}
|
||||
{...register("placeholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="required"
|
||||
type="checkbox"
|
||||
className="w-4 h-4 mr-2 border-gray-300 rounded focus:ring-primary-500 text-primary-600"
|
||||
defaultChecked={selectedCustomInput?.required ?? true}
|
||||
{...register("required")}
|
||||
/>
|
||||
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
|
||||
Is required
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
id="eventTypeId"
|
||||
value={selectedCustomInput?.eventTypeId || -1}
|
||||
{...register("eventTypeId", { valueAsNumber: true })}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="id"
|
||||
value={selectedCustomInput?.id || -1}
|
||||
{...register("id", { valueAsNumber: true })}
|
||||
/>
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
<button onClick={onCancel} type="button" className="mr-2 btn btn-white">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomInputTypeForm;
|
|
@ -1,8 +1,10 @@
|
|||
// ℹ️ Type-only import:
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
||||
import type { AppRouter } from "@server/routers/_app";
|
||||
import { createReactQueryHooks } from "@trpc/react";
|
||||
import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
|
||||
import type { AppRouter } from "@server/routers/_app";
|
||||
|
||||
/**
|
||||
* A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
|
||||
|
@ -10,7 +12,7 @@ import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server";
|
|||
*/
|
||||
export const trpc = createReactQueryHooks<AppRouter>();
|
||||
|
||||
// export const transformer = superjson;
|
||||
export const transformer = superjson;
|
||||
/**
|
||||
* This is a helper method to infer the output of a query resolver
|
||||
* @example type HelloOutput = inferQueryOutput<'hello'>
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
"react-use-intercom": "1.4.0",
|
||||
"short-uuid": "^4.2.0",
|
||||
"stripe": "^8.168.0",
|
||||
"superjson": "1.7.5",
|
||||
"tsdav": "1.0.6",
|
||||
"tslog": "^3.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import { ssg } from "@server/ssg";
|
||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
@ -13,6 +12,8 @@ import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
|||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
|
||||
import { ssg } from "@server/ssg";
|
||||
|
||||
export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
||||
const { username } = props;
|
||||
// data of query below will be will be prepopulated b/c of `getStaticProps`
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { AppRouter } from "@server/routers/_app";
|
||||
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
|
||||
import { loggerLink } from "@trpc/client/links/loggerLink";
|
||||
import { withTRPC } from "@trpc/next";
|
||||
|
@ -7,10 +6,13 @@ import { Maybe } from "@trpc/server";
|
|||
import { appWithTranslation } from "next-i18next";
|
||||
import { DefaultSeo } from "next-seo";
|
||||
import type { AppProps as NextAppProps } from "next/app";
|
||||
import superjson from "superjson";
|
||||
|
||||
import AppProviders from "@lib/app-providers";
|
||||
import { seoConfig } from "@lib/config/next-seo.config";
|
||||
|
||||
import type { AppRouter } from "@server/routers/_app";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||
|
@ -77,6 +79,10 @@ export default withTRPC<AppRouter>({
|
|||
},
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/data-transformers
|
||||
*/
|
||||
transformer: superjson,
|
||||
};
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,50 @@
|
|||
import { EventTypeCustomInput, Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: number) {
|
||||
if (!customInputs || customInputs?.length) return undefined;
|
||||
const cInputsIdsToDelete = customInputs.filter((input) => input.id > 0).map((e) => e.id);
|
||||
const cInputsToCreate = customInputs
|
||||
.filter((input) => input.id < 0)
|
||||
.map((input) => ({
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
}));
|
||||
const cInputsToUpdate = customInputs
|
||||
.filter((input) => input.id > 0)
|
||||
.map((input) => ({
|
||||
data: {
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
},
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
deleteMany: {
|
||||
eventTypeId,
|
||||
NOT: {
|
||||
id: { in: cInputsIdsToDelete },
|
||||
},
|
||||
},
|
||||
createMany: {
|
||||
data: cInputsToCreate,
|
||||
},
|
||||
update: cInputsToUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
const session = await getSession({ req });
|
||||
|
||||
if (!session) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
|
@ -41,7 +81,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
if (req.method == "PATCH" || req.method == "POST") {
|
||||
const data = {
|
||||
const data: Prisma.EventTypeUpdateInput = {
|
||||
title: req.body.title,
|
||||
slug: req.body.slug.trim(),
|
||||
description: req.body.description,
|
||||
|
@ -51,39 +91,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
disableGuests: req.body.disableGuests,
|
||||
locations: req.body.locations,
|
||||
eventName: req.body.eventName,
|
||||
customInputs: !req.body.customInputs
|
||||
? undefined
|
||||
: {
|
||||
deleteMany: {
|
||||
eventTypeId: req.body.id,
|
||||
NOT: {
|
||||
id: { in: req.body.customInputs.filter((input) => !!input.id).map((e) => e.id) },
|
||||
},
|
||||
},
|
||||
createMany: {
|
||||
data: req.body.customInputs
|
||||
.filter((input) => !input.id)
|
||||
.map((input) => ({
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
})),
|
||||
},
|
||||
update: req.body.customInputs
|
||||
.filter((input) => !!input.id)
|
||||
.map((input) => ({
|
||||
data: {
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
},
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
})),
|
||||
},
|
||||
customInputs: handleCustomInputs(req.body.customInputs as EventTypeCustomInput[], req.body.id),
|
||||
periodType: req.body.periodType,
|
||||
periodDays: req.body.periodDays,
|
||||
periodStartDate: req.body.periodStartDate,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/**
|
||||
* This file contains tRPC's HTTP response handler
|
||||
*/
|
||||
import * as trpcNext from "@trpc/server/adapters/next";
|
||||
|
||||
import { createContext } from "@server/createContext";
|
||||
import { appRouter } from "@server/routers/_app";
|
||||
import * as trpcNext from "@trpc/server/adapters/next";
|
||||
|
||||
export default trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { resizeBase64Image } from "@server/lib/resizeBase64Image";
|
||||
import { pick } from "lodash";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { resizeBase64Image } from "@server/lib/resizeBase64Image";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
UserAddIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { EventTypeCustomInput, EventTypeCustomInputType, Prisma, SchedulingType } from "@prisma/client";
|
||||
import { EventTypeCustomInput, Prisma, SchedulingType } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
@ -48,10 +48,11 @@ import { defaultAvatarSrc } from "@lib/profile";
|
|||
import { AdvancedOptions, EventTypeInput } from "@lib/types/event-type";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@components/Dialog";
|
||||
import Modal from "@components/Modal";
|
||||
import Shell from "@components/Shell";
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
||||
import Button from "@components/ui/Button";
|
||||
import { Scheduler } from "@components/ui/Scheduler";
|
||||
import Switch from "@components/ui/Switch";
|
||||
|
@ -86,13 +87,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
const router = useRouter();
|
||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
|
||||
const inputOptions: OptionTypeBase[] = [
|
||||
{ value: EventTypeCustomInputType.TEXT, label: "Text" },
|
||||
{ value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" },
|
||||
{ value: EventTypeCustomInputType.NUMBER, label: "Number" },
|
||||
{ value: EventTypeCustomInputType.BOOL, label: "Checkbox" },
|
||||
];
|
||||
|
||||
const updateMutation = useMutation(updateEventType, {
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.push("/event-types");
|
||||
|
@ -121,12 +115,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
|
||||
const [enteredAvailability, setEnteredAvailability] = useState();
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
||||
const [selectedInputOption, setSelectedInputOption] = useState<OptionTypeBase>(inputOptions[0]);
|
||||
const [locations, setLocations] = useState(eventType.locations || []);
|
||||
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||
);
|
||||
|
@ -217,12 +210,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
setShowLocationModal(false);
|
||||
};
|
||||
|
||||
const closeAddCustomModal = () => {
|
||||
setSelectedInputOption(inputOptions[0]);
|
||||
setShowAddCustomModal(false);
|
||||
setSelectedCustomInput(undefined);
|
||||
};
|
||||
|
||||
const closeSuccessModal = () => {
|
||||
setSuccessModalOpen(false);
|
||||
};
|
||||
|
@ -252,12 +239,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
setLocations(locations.filter((location) => location.type !== selectedLocation.type));
|
||||
};
|
||||
|
||||
const openEditCustomModel = (customInput: EventTypeCustomInput) => {
|
||||
setSelectedCustomInput(customInput);
|
||||
setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type)!);
|
||||
setShowAddCustomModal(true);
|
||||
};
|
||||
|
||||
const LocationOptions = () => {
|
||||
if (!selectedLocation) {
|
||||
return null;
|
||||
|
@ -293,29 +274,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const updateCustom = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const customInput: EventTypeCustomInput = {
|
||||
id: -1,
|
||||
eventTypeId: -1,
|
||||
label: e.currentTarget.label.value,
|
||||
placeholder: e.currentTarget.placeholder?.value,
|
||||
required: e.currentTarget.required.checked,
|
||||
type: e.currentTarget.type.value,
|
||||
};
|
||||
|
||||
if (selectedCustomInput) {
|
||||
selectedCustomInput.label = customInput.label;
|
||||
selectedCustomInput.placeholder = customInput.placeholder;
|
||||
selectedCustomInput.required = customInput.required;
|
||||
selectedCustomInput.type = customInput.type;
|
||||
} else {
|
||||
setCustomInputs(customInputs.concat(customInput));
|
||||
}
|
||||
closeAddCustomModal();
|
||||
};
|
||||
|
||||
const removeCustom = (index: number) => {
|
||||
customInputs.splice(index, 1);
|
||||
setCustomInputs([...customInputs]);
|
||||
|
@ -422,7 +380,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
id="length"
|
||||
required
|
||||
placeholder="15"
|
||||
defaultValue={eventType.length}
|
||||
defaultValue={eventType.length || 15}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
|
@ -679,12 +637,15 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditCustomModel(customInput)}
|
||||
className="mr-2 text-sm text-primary-600">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedCustomInput(customInput);
|
||||
setSelectedCustomInputModalOpen(true);
|
||||
}}
|
||||
color="minimal"
|
||||
type="button">
|
||||
Edit
|
||||
</button>
|
||||
</Button>
|
||||
<button type="button" onClick={() => removeCustom(idx)}>
|
||||
<XIcon className="w-6 h-6 pl-1 border-l-2 hover:text-red-500 " />
|
||||
</button>
|
||||
|
@ -693,15 +654,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</li>
|
||||
))}
|
||||
<li>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedCustomInput(undefined);
|
||||
setSelectedCustomInputModalOpen(true);
|
||||
}}
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="flex px-3 py-2 rounded-sm bg-neutral-100"
|
||||
onClick={() => setShowAddCustomModal(true)}>
|
||||
<PlusIcon className="h-4 w-4 mt-0.5 text-neutral-900" />
|
||||
<span className="ml-1 text-sm font-medium text-neutral-700">
|
||||
Add an input
|
||||
</span>
|
||||
</button>
|
||||
StartIcon={PlusIcon}>
|
||||
Add an input
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1035,111 +997,51 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showAddCustomModal && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div
|
||||
className="fixed inset-0 z-0 transition-opacity bg-gray-500 bg-opacity-75"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
||||
<div className="inline-block px-4 pt-5 pb-4 text-left align-bottom transition-all transform bg-white rounded-sm shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto rounded-full bg-secondary-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<PlusIcon className="w-6 h-6 text-primary-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
Add new custom input field
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">
|
||||
This input will be shown when booking this event
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
||||
<DialogContent asChild>
|
||||
<div className="inline-block px-4 pt-5 pb-4 text-left align-bottom transition-all transform bg-white rounded-sm shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto rounded-full bg-secondary-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<PlusIcon className="w-6 h-6 text-primary-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
Add new custom input field
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">This input will be shown when booking this event</p>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={updateCustom}>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
||||
Input type
|
||||
</label>
|
||||
<Select
|
||||
name="type"
|
||||
defaultValue={selectedInputOption}
|
||||
options={inputOptions}
|
||||
isSearchable={false}
|
||||
required
|
||||
className="flex-1 block w-full min-w-0 mt-1 mb-2 border-gray-300 rounded-none focus:ring-primary-500 focus:border-primary-500 rounded-r-md sm:text-sm"
|
||||
onChange={setSelectedInputOption}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
||||
Label
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="label"
|
||||
id="label"
|
||||
required
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
defaultValue={selectedCustomInput?.label}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(selectedInputOption.value === EventTypeCustomInputType.TEXT ||
|
||||
selectedInputOption.value === EventTypeCustomInputType.TEXTLONG) && (
|
||||
<div className="mb-2">
|
||||
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
|
||||
Placeholder
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="placeholder"
|
||||
id="placeholder"
|
||||
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||
defaultValue={selectedCustomInput?.placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="required"
|
||||
name="required"
|
||||
type="checkbox"
|
||||
className="w-4 h-4 mr-2 border-gray-300 rounded focus:ring-primary-500 text-primary-600"
|
||||
defaultChecked={selectedCustomInput?.required ?? true}
|
||||
/>
|
||||
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
|
||||
Is required
|
||||
</label>
|
||||
</div>
|
||||
<input type="hidden" name="id" id="id" value={selectedCustomInput?.id} />
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
<button onClick={closeAddCustomModal} type="button" className="mr-2 btn btn-white">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<CustomInputTypeForm
|
||||
selectedCustomInput={selectedCustomInput}
|
||||
onSubmit={(values) => {
|
||||
const customInput: EventTypeCustomInput = {
|
||||
id: -1,
|
||||
eventTypeId: -1,
|
||||
label: values.label,
|
||||
placeholder: values.placeholder,
|
||||
required: values.required,
|
||||
type: values.type,
|
||||
};
|
||||
|
||||
if (selectedCustomInput) {
|
||||
selectedCustomInput.label = customInput.label;
|
||||
selectedCustomInput.placeholder = customInput.placeholder;
|
||||
selectedCustomInput.required = customInput.required;
|
||||
selectedCustomInput.type = customInput.type;
|
||||
} else {
|
||||
setCustomInputs(customInputs.concat(customInput));
|
||||
}
|
||||
setSelectedCustomInputModalOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setSelectedCustomInputModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Shell>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -520,6 +520,7 @@ const CreateNewEventDialog = ({
|
|||
required
|
||||
className="block w-full pr-20 border-gray-300 rounded-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
placeholder="15"
|
||||
defaultValue={15}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-gray-400">
|
||||
minutes
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* This file contains the root router of your tRPC-backend
|
||||
*/
|
||||
import superjson from "superjson";
|
||||
|
||||
import { createRouter } from "../createRouter";
|
||||
import { bookingRouter } from "./booking";
|
||||
import { viewerRouter } from "./viewer";
|
||||
|
@ -16,7 +18,7 @@ export const appRouter = createRouter()
|
|||
* Add data transformers
|
||||
* @link https://trpc.io/docs/data-transformers
|
||||
*/
|
||||
// .transformer(superjson)
|
||||
.transformer(superjson)
|
||||
/**
|
||||
* Optionally do custom error (type safe!) formatting
|
||||
* @link https://trpc.io/docs/error-formatting
|
||||
|
|
|
@ -7911,6 +7911,14 @@ stylis@^4.0.3:
|
|||
version "4.0.10"
|
||||
resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz"
|
||||
|
||||
superjson@1.7.5:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.7.5.tgz#596d080fd3c010f6d991c53a292c03704f160649"
|
||||
integrity sha512-AHuFroOcMTK6LdG/irwXIHwH6Gof5nh42iywnhhf7hMZ6UJqFDRtJ82ViJg14UX3AG8vWRf4Dh3oPIJcqu16Nw==
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
|
||||
supports-color@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
|
||||
|
|
Loading…
Reference in a new issue