add react forms and basic implementation of update endpoint

This commit is contained in:
Brandon Egger 2023-06-06 00:26:00 -05:00
parent 6cbcc7eb21
commit cabddd777e
6 changed files with 157 additions and 39 deletions

14
package-lock.json generated
View File

@ -23,7 +23,7 @@
"next-auth": "^4.19.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-hook-form": "^7.44.3",
"superjson": "1.9.1",
"zod": "^3.20.6"
},
@ -4624,9 +4624,9 @@
}
},
"node_modules/react-hook-form": {
"version": "7.43.9",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.9.tgz",
"integrity": "sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==",
"version": "7.44.3",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.44.3.tgz",
"integrity": "sha512-/tHId6p2ViAka1wECMw8FEPn/oz/w226zehHrJyQ1oIzCBNMIJCaj6ZkQcv+MjDxYh9MWR7RQic7Qqwe4a5nkw==",
"engines": {
"node": ">=12.22.0"
},
@ -8890,9 +8890,9 @@
}
},
"react-hook-form": {
"version": "7.43.9",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.9.tgz",
"integrity": "sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==",
"version": "7.44.3",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.44.3.tgz",
"integrity": "sha512-/tHId6p2ViAka1wECMw8FEPn/oz/w226zehHrJyQ1oIzCBNMIJCaj6ZkQcv+MjDxYh9MWR7RQic7Qqwe4a5nkw==",
"requires": {}
},
"react-is": {

View File

@ -30,7 +30,7 @@
"next-auth": "^4.19.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-hook-form": "^7.44.3",
"superjson": "1.9.1",
"zod": "^3.20.6"
},

View File

@ -1,5 +1,24 @@
import Link from "next/link";
const AdminActionBody = ({
label,
symbol,
}: {
label: string;
symbol: JSX.Element | undefined;
}) => {
return (
<>
<span className="my-auto inline-block h-fit align-middle text-sm leading-8 text-white group-hover:text-black">
{label}
</span>
<span className="inline-block align-middle text-white group-hover:text-black">
{symbol}
</span>
</>
);
};
const AdminActionLink = ({
label,
href,
@ -13,6 +32,25 @@ const AdminActionLink = ({
<Link
className="py-auto group my-auto h-full space-x-2 rounded-lg border border-neutral-400 bg-neutral-800 px-2 hover:border-neutral-800 hover:bg-white"
href={href}
>
<AdminActionBody label={label} symbol={symbol} />
</Link>
);
};
const AdminActionButton = ({
label,
onClick,
symbol,
}: {
label: string;
onClick: () => void;
symbol: JSX.Element | undefined;
}) => {
return (
<button
className="py-auto group my-auto h-full space-x-2 rounded-lg border border-neutral-400 bg-neutral-800 px-2 hover:border-neutral-800 hover:bg-white"
onClick={onClick}
>
<span className="my-auto inline-block h-fit align-middle text-sm leading-8 text-white group-hover:text-black">
{label}
@ -20,8 +58,8 @@ const AdminActionLink = ({
<span className="inline-block align-middle text-white group-hover:text-black">
{symbol}
</span>
</Link>
</button>
);
};
export { AdminActionLink };
export { AdminActionLink, AdminActionButton };

View File

@ -1,9 +1,4 @@
import {
PaymentType,
type AuditoryResource,
SkillLevel,
Skill,
} from "@prisma/client";
import { PaymentType, SkillLevel, Skill } from "@prisma/client";
import Image from "next/image";
import { PencilSquareIcon } from "@heroicons/react/24/solid";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
@ -17,6 +12,10 @@ import {
import { InfoInputLine } from "~/components/forms/textInput";
import { PriceIcon } from "~/prices/Icons";
import { useState } from "react";
import { type UseFormRegister } from "react-hook-form";
import { type RouterInputs } from "~/utils/api";
export type ResourceUpdateInput = RouterInputs["auditoryResource"]["update"];
/**
* Renders the image selector for resource form.
@ -109,11 +108,13 @@ const PaymentTypeOption = ({
/**
* Resource summary inputs - ie description, manufacturer, etc.
*/
const ResourceSummarySubForm = ({
function ResourceSummarySubForm({
register,
resource,
}: {
resource?: AuditoryResource;
}) => {
register: UseFormRegister<ResourceUpdateInput>;
resource?: ResourceUpdateInput;
}) {
return (
<div className="space-y-4 px-4">
<div className="flex flex-row space-x-4 sm:mt-4">
@ -144,7 +145,7 @@ const ResourceSummarySubForm = ({
<MultiSelector
label="Price Category"
defaultValue={
resource?.payment_options.toString() ?? PaymentType.FREE.toString()
resource?.payment_options?.toString() ?? PaymentType.FREE.toString()
}
>
<PaymentTypeOption type={PaymentType.FREE} label="Free" />
@ -189,12 +190,14 @@ const ResourceSummarySubForm = ({
</MultiSelectorMany>
</div>
);
};
}
const ResourceDescriptionSubForm = ({
register,
resource,
}: {
resource?: AuditoryResource;
register: UseFormRegister<ResourceUpdateInput>;
resource?: ResourceUpdateInput;
}) => {
const [dropdownOpen, toggleDropdown] = useState(false);
@ -203,6 +206,7 @@ const ResourceDescriptionSubForm = ({
<label className="text-md font-semibold">Description</label>
<div className="relative mt-4 overflow-hidden rounded-xl border border-neutral-400 bg-neutral-200 text-left shadow">
<button
type="button"
onClick={() => {
toggleDropdown(!dropdownOpen);
}}
@ -217,28 +221,34 @@ const ResourceDescriptionSubForm = ({
<ChevronDownIcon className="mx-2 my-auto w-4 text-white group-hover:animate-bounce" />
</button>
<textarea
{...register("description", { required: true })}
className={
"h-48 w-full rounded-b-xl p-2" + (dropdownOpen ? " hidden" : "")
}
>
{resource?.description}
</textarea>
/>
<textarea
{...register("manufacturer.notice")}
className={
"h-48 w-full rounded-b-xl bg-neutral-800 p-2 text-white" +
(dropdownOpen ? "" : " hidden")
}
>
{resource?.manufacturer?.notice}
</textarea>
/>
</div>
</div>
);
};
const ResourceForm = ({ resource }: { resource?: AuditoryResource }) => {
const ResourceForm = ({
resource,
register,
error,
}: {
resource?: ResourceUpdateInput;
register: UseFormRegister<ResourceUpdateInput>;
error?: string;
}) => {
return (
<div className="mx-auto flex max-w-2xl flex-col flex-col-reverse py-1 sm:flex-row sm:divide-x sm:py-4">
<form className="mx-auto flex max-w-2xl flex-col flex-col-reverse py-1 sm:flex-row sm:divide-x sm:py-4">
<div className="my-5 mr-4 flex flex-col text-lg font-bold">
<ResourceLinkSubForm /> {/** //resource={resource} /> */}
</div>
@ -247,11 +257,11 @@ const ResourceForm = ({ resource }: { resource?: AuditoryResource }) => {
General
</h1>
<div className="justify-left mx-auto flex max-w-lg flex-col space-y-4 pb-5">
<ResourceSummarySubForm resource={resource} />
<ResourceDescriptionSubForm resource={resource} />
<ResourceSummarySubForm register={register} resource={resource} />
<ResourceDescriptionSubForm register={register} resource={resource} />
</div>
</div>
</div>
</form>
);
};

View File

@ -8,11 +8,17 @@ import {
import Footer from "~/components/Footer";
import Header from "~/components/Header";
import { AdminBarLayout } from "~/components/admin/ControlBar";
import { AdminActionLink } from "~/components/admin/common";
import { AdminActionButton, AdminActionLink } from "~/components/admin/common";
import { appRouter } from "~/server/api/root";
import { prisma } from "~/server/db";
import Image from "next/image";
import { ResourceForm } from "~/components/admin/resources/form";
import {
ResourceForm,
type ResourceUpdateInput,
} from "~/components/admin/resources/form";
import { useState } from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { api } from "~/utils/api";
export const getServerSideProps: GetServerSideProps<{
resource: AuditoryResource;
@ -40,13 +46,29 @@ const EditResourcePage = (
props: InferGetServerSidePropsType<typeof getServerSideProps>
) => {
const { resource } = props;
const [serverError, setServerError] = useState<string | undefined>(undefined);
const { register, getValues } = useForm<ResourceUpdateInput>({
defaultValues: resource as ResourceUpdateInput,
});
const { mutate } = api.auditoryResource.update.useMutation({
onSuccess: async (data) => {
// todo:
},
onError: (error) => setServerError(error.message),
});
const onSubmit: SubmitHandler<ResourceUpdateInput> = (data) => {
console.log("mutating");
mutate(data);
};
return (
<>
<Header />
<AdminBarLayout
actions={[
<AdminActionLink
<AdminActionButton
key="save"
symbol={
<span className="flex">
@ -67,7 +89,9 @@ const EditResourcePage = (
</span>
}
label="Save"
href={`/resources/${resource.id}`}
onClick={() => {
onSubmit(getValues());
}}
/>,
<AdminActionLink
key="cancel"
@ -78,7 +102,11 @@ const EditResourcePage = (
]}
>
<main className="mb-12">
<ResourceForm resource={resource} />
<ResourceForm
register={register}
error={serverError}
resource={resource as ResourceUpdateInput}
/>
</main>
</AdminBarLayout>
<Footer />

View File

@ -3,10 +3,15 @@ import {
Skill,
Platform,
type AuditoryResource,
PaymentType,
} from "@prisma/client";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "~/server/api/trpc";
export const auditoryResourceRouter = createTRPCRouter({
byId: publicProcedure
@ -25,6 +30,43 @@ export const auditoryResourceRouter = createTRPCRouter({
return ctx.prisma.auditoryResource.findMany();
}),
update: protectedProcedure
.input(
z.object({
id: z.string(),
icon: z.string().optional(),
name: z.string().optional(),
description: z.string().optional(),
manufacturer: z
.object({
name: z.string(),
required: z.boolean(),
notice: z.string().optional().nullable(),
})
.optional(),
ages: z
.object({ min: z.number().int(), max: z.number().int() })
.optional(),
skills: z.array(z.nativeEnum(Skill)).optional(),
skill_levels: z.array(z.nativeEnum(SkillLevel)).optional(),
payment_options: z.array(z.nativeEnum(PaymentType)).optional(),
platform_links: z
.array(
z.object({ platform: z.nativeEnum(Platform), link: z.string() })
)
.optional(),
})
)
.mutation(({ input, ctx }) => {
console.log(input);
// return await ctx.prisma.auditoryResource.update({
// where: {
// id: input.id,
// },
// data: { ...input },
// });
}),
search: publicProcedure
.input(
z.object({