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,
|
semi: true,
|
||||||
printWidth: 110,
|
printWidth: 110,
|
||||||
arrowParens: "always",
|
arrowParens: "always",
|
||||||
importOrder: ["^@ee/(.*)$", "^@lib/(.*)$", "^@components/(.*)$", "^[./]"],
|
importOrder: ["^@ee/(.*)$", "^@lib/(.*)$", "^@components/(.*)$", "^@(server|trcp)/(.*)$", "^[./]"],
|
||||||
importOrderSeparation: true,
|
importOrderSeparation: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
/* legacy and soon deprecated, please refactor to use <Dialog> only */
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { CheckIcon, InformationCircleIcon } from "@heroicons/react/outline";
|
import { CheckIcon, InformationCircleIcon } from "@heroicons/react/outline";
|
||||||
import { Fragment, ReactNode } from "react";
|
import { Fragment, ReactNode } from "react";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated please refactor to use <Dialog> only
|
||||||
|
*/
|
||||||
export default function Modal(props: {
|
export default function Modal(props: {
|
||||||
heading: ReactNode;
|
heading: ReactNode;
|
||||||
description: 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:
|
// ℹ️ Type-only import:
|
||||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
// 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 { createReactQueryHooks } from "@trpc/react";
|
||||||
import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server";
|
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`.
|
* 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 trpc = createReactQueryHooks<AppRouter>();
|
||||||
|
|
||||||
// export const transformer = superjson;
|
export const transformer = superjson;
|
||||||
/**
|
/**
|
||||||
* This is a helper method to infer the output of a query resolver
|
* This is a helper method to infer the output of a query resolver
|
||||||
* @example type HelloOutput = inferQueryOutput<'hello'>
|
* @example type HelloOutput = inferQueryOutput<'hello'>
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
"react-use-intercom": "1.4.0",
|
"react-use-intercom": "1.4.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"stripe": "^8.168.0",
|
"stripe": "^8.168.0",
|
||||||
|
"superjson": "1.7.5",
|
||||||
"tsdav": "1.0.6",
|
"tsdav": "1.0.6",
|
||||||
"tslog": "^3.2.1",
|
"tslog": "^3.2.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { ssg } from "@server/ssg";
|
|
||||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -13,6 +12,8 @@ import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
|
|
||||||
|
import { ssg } from "@server/ssg";
|
||||||
|
|
||||||
export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
||||||
const { username } = props;
|
const { username } = props;
|
||||||
// data of query below will be will be prepopulated b/c of `getStaticProps`
|
// 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 { httpBatchLink } from "@trpc/client/links/httpBatchLink";
|
||||||
import { loggerLink } from "@trpc/client/links/loggerLink";
|
import { loggerLink } from "@trpc/client/links/loggerLink";
|
||||||
import { withTRPC } from "@trpc/next";
|
import { withTRPC } from "@trpc/next";
|
||||||
|
@ -7,10 +6,13 @@ import { Maybe } from "@trpc/server";
|
||||||
import { appWithTranslation } from "next-i18next";
|
import { appWithTranslation } from "next-i18next";
|
||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import type { AppProps as NextAppProps } from "next/app";
|
import type { AppProps as NextAppProps } from "next/app";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
import AppProviders from "@lib/app-providers";
|
import AppProviders from "@lib/app-providers";
|
||||||
import { seoConfig } from "@lib/config/next-seo.config";
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
|
|
||||||
|
import type { AppRouter } from "@server/routers/_app";
|
||||||
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
|
||||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
// 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 type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import prisma from "@lib/prisma";
|
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) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = await getSession({ req: req });
|
const session = await getSession({ req });
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
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") {
|
if (req.method == "PATCH" || req.method == "POST") {
|
||||||
const data = {
|
const data: Prisma.EventTypeUpdateInput = {
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
slug: req.body.slug.trim(),
|
slug: req.body.slug.trim(),
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
|
@ -51,39 +91,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
disableGuests: req.body.disableGuests,
|
disableGuests: req.body.disableGuests,
|
||||||
locations: req.body.locations,
|
locations: req.body.locations,
|
||||||
eventName: req.body.eventName,
|
eventName: req.body.eventName,
|
||||||
customInputs: !req.body.customInputs
|
customInputs: handleCustomInputs(req.body.customInputs as EventTypeCustomInput[], req.body.id),
|
||||||
? 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,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
periodType: req.body.periodType,
|
periodType: req.body.periodType,
|
||||||
periodDays: req.body.periodDays,
|
periodDays: req.body.periodDays,
|
||||||
periodStartDate: req.body.periodStartDate,
|
periodStartDate: req.body.periodStartDate,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* This file contains tRPC's HTTP response handler
|
* This file contains tRPC's HTTP response handler
|
||||||
*/
|
*/
|
||||||
|
import * as trpcNext from "@trpc/server/adapters/next";
|
||||||
|
|
||||||
import { createContext } from "@server/createContext";
|
import { createContext } from "@server/createContext";
|
||||||
import { appRouter } from "@server/routers/_app";
|
import { appRouter } from "@server/routers/_app";
|
||||||
import * as trpcNext from "@trpc/server/adapters/next";
|
|
||||||
|
|
||||||
export default trpcNext.createNextApiHandler({
|
export default trpcNext.createNextApiHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { resizeBase64Image } from "@server/lib/resizeBase64Image";
|
|
||||||
import { pick } from "lodash";
|
import { pick } from "lodash";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
import { resizeBase64Image } from "@server/lib/resizeBase64Image";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = await getSession({ req: req });
|
const session = await getSession({ req: req });
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
UserAddIcon,
|
UserAddIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
import { EventTypeCustomInput, EventTypeCustomInputType, Prisma, SchedulingType } from "@prisma/client";
|
import { EventTypeCustomInput, Prisma, SchedulingType } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
|
@ -48,10 +48,11 @@ import { defaultAvatarSrc } from "@lib/profile";
|
||||||
import { AdvancedOptions, EventTypeInput } from "@lib/types/event-type";
|
import { AdvancedOptions, EventTypeInput } from "@lib/types/event-type";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
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 Modal from "@components/Modal";
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
|
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
import { Scheduler } from "@components/ui/Scheduler";
|
import { Scheduler } from "@components/ui/Scheduler";
|
||||||
import Switch from "@components/ui/Switch";
|
import Switch from "@components/ui/Switch";
|
||||||
|
@ -86,13 +87,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
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, {
|
const updateMutation = useMutation(updateEventType, {
|
||||||
onSuccess: async ({ eventType }) => {
|
onSuccess: async ({ eventType }) => {
|
||||||
await router.push("/event-types");
|
await router.push("/event-types");
|
||||||
|
@ -121,12 +115,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
|
|
||||||
const [enteredAvailability, setEnteredAvailability] = useState();
|
const [enteredAvailability, setEnteredAvailability] = useState();
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
|
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
||||||
const [selectedInputOption, setSelectedInputOption] = useState<OptionTypeBase>(inputOptions[0]);
|
|
||||||
const [locations, setLocations] = useState(eventType.locations || []);
|
const [locations, setLocations] = useState(eventType.locations || []);
|
||||||
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
||||||
|
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||||
);
|
);
|
||||||
|
@ -217,12 +210,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
setShowLocationModal(false);
|
setShowLocationModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeAddCustomModal = () => {
|
|
||||||
setSelectedInputOption(inputOptions[0]);
|
|
||||||
setShowAddCustomModal(false);
|
|
||||||
setSelectedCustomInput(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeSuccessModal = () => {
|
const closeSuccessModal = () => {
|
||||||
setSuccessModalOpen(false);
|
setSuccessModalOpen(false);
|
||||||
};
|
};
|
||||||
|
@ -252,12 +239,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
setLocations(locations.filter((location) => location.type !== selectedLocation.type));
|
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 = () => {
|
const LocationOptions = () => {
|
||||||
if (!selectedLocation) {
|
if (!selectedLocation) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -293,29 +274,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
return null;
|
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) => {
|
const removeCustom = (index: number) => {
|
||||||
customInputs.splice(index, 1);
|
customInputs.splice(index, 1);
|
||||||
setCustomInputs([...customInputs]);
|
setCustomInputs([...customInputs]);
|
||||||
|
@ -422,7 +380,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
id="length"
|
id="length"
|
||||||
required
|
required
|
||||||
placeholder="15"
|
placeholder="15"
|
||||||
defaultValue={eventType.length}
|
defaultValue={eventType.length || 15}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -679,12 +637,15 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
onClick={() => {
|
||||||
onClick={() => openEditCustomModel(customInput)}
|
setSelectedCustomInput(customInput);
|
||||||
className="mr-2 text-sm text-primary-600">
|
setSelectedCustomInputModalOpen(true);
|
||||||
|
}}
|
||||||
|
color="minimal"
|
||||||
|
type="button">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
<button type="button" onClick={() => removeCustom(idx)}>
|
<button type="button" onClick={() => removeCustom(idx)}>
|
||||||
<XIcon className="w-6 h-6 pl-1 border-l-2 hover:text-red-500 " />
|
<XIcon className="w-6 h-6 pl-1 border-l-2 hover:text-red-500 " />
|
||||||
</button>
|
</button>
|
||||||
|
@ -693,15 +654,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCustomInput(undefined);
|
||||||
|
setSelectedCustomInputModalOpen(true);
|
||||||
|
}}
|
||||||
|
color="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
className="flex px-3 py-2 rounded-sm bg-neutral-100"
|
StartIcon={PlusIcon}>
|
||||||
onClick={() => setShowAddCustomModal(true)}>
|
Add an input
|
||||||
<PlusIcon className="h-4 w-4 mt-0.5 text-neutral-900" />
|
</Button>
|
||||||
<span className="ml-1 text-sm font-medium text-neutral-700">
|
|
||||||
Add an input
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1035,111 +997,51 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showAddCustomModal && (
|
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
||||||
<div
|
<DialogContent asChild>
|
||||||
className="fixed inset-0 z-50 overflow-y-auto"
|
<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">
|
||||||
aria-labelledby="modal-title"
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
role="dialog"
|
<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">
|
||||||
aria-modal="true">
|
<PlusIcon className="w-6 h-6 text-primary-600" />
|
||||||
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
</div>
|
||||||
<div
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
className="fixed inset-0 z-0 transition-opacity bg-gray-500 bg-opacity-75"
|
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
aria-hidden="true"
|
Add new custom input field
|
||||||
/>
|
</h3>
|
||||||
|
<div>
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
<p className="text-sm text-gray-400">This input will be shown when booking this event</p>
|
||||||
​
|
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
||||||
</div>
|
</DialogContent>
|
||||||
)}
|
</Dialog>
|
||||||
</Shell>
|
</Shell>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -520,6 +520,7 @@ const CreateNewEventDialog = ({
|
||||||
required
|
required
|
||||||
className="block w-full pr-20 border-gray-300 rounded-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
className="block w-full pr-20 border-gray-300 rounded-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||||
placeholder="15"
|
placeholder="15"
|
||||||
|
defaultValue={15}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-gray-400">
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-gray-400">
|
||||||
minutes
|
minutes
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* This file contains the root router of your tRPC-backend
|
* This file contains the root router of your tRPC-backend
|
||||||
*/
|
*/
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
import { createRouter } from "../createRouter";
|
import { createRouter } from "../createRouter";
|
||||||
import { bookingRouter } from "./booking";
|
import { bookingRouter } from "./booking";
|
||||||
import { viewerRouter } from "./viewer";
|
import { viewerRouter } from "./viewer";
|
||||||
|
@ -16,7 +18,7 @@ export const appRouter = createRouter()
|
||||||
* Add data transformers
|
* Add data transformers
|
||||||
* @link https://trpc.io/docs/data-transformers
|
* @link https://trpc.io/docs/data-transformers
|
||||||
*/
|
*/
|
||||||
// .transformer(superjson)
|
.transformer(superjson)
|
||||||
/**
|
/**
|
||||||
* Optionally do custom error (type safe!) formatting
|
* Optionally do custom error (type safe!) formatting
|
||||||
* @link https://trpc.io/docs/error-formatting
|
* @link https://trpc.io/docs/error-formatting
|
||||||
|
|
|
@ -7911,6 +7911,14 @@ stylis@^4.0.3:
|
||||||
version "4.0.10"
|
version "4.0.10"
|
||||||
resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz"
|
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:
|
supports-color@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
|
||||||
|
|
Loading…
Reference in a new issue