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

View File

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

View File

@ -1,5 +1,24 @@
import Link from "next/link"; 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 = ({ const AdminActionLink = ({
label, label,
href, href,
@ -13,6 +32,25 @@ const AdminActionLink = ({
<Link <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" 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} 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"> <span className="my-auto inline-block h-fit align-middle text-sm leading-8 text-white group-hover:text-black">
{label} {label}
@ -20,8 +58,8 @@ const AdminActionLink = ({
<span className="inline-block align-middle text-white group-hover:text-black"> <span className="inline-block align-middle text-white group-hover:text-black">
{symbol} {symbol}
</span> </span>
</Link> </button>
); );
}; };
export { AdminActionLink }; export { AdminActionLink, AdminActionButton };

View File

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

View File

@ -8,11 +8,17 @@ import {
import Footer from "~/components/Footer"; import Footer from "~/components/Footer";
import Header from "~/components/Header"; import Header from "~/components/Header";
import { AdminBarLayout } from "~/components/admin/ControlBar"; 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 { appRouter } from "~/server/api/root";
import { prisma } from "~/server/db"; import { prisma } from "~/server/db";
import Image from "next/image"; 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<{ export const getServerSideProps: GetServerSideProps<{
resource: AuditoryResource; resource: AuditoryResource;
@ -40,13 +46,29 @@ const EditResourcePage = (
props: InferGetServerSidePropsType<typeof getServerSideProps> props: InferGetServerSidePropsType<typeof getServerSideProps>
) => { ) => {
const { resource } = props; 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 ( return (
<> <>
<Header /> <Header />
<AdminBarLayout <AdminBarLayout
actions={[ actions={[
<AdminActionLink <AdminActionButton
key="save" key="save"
symbol={ symbol={
<span className="flex"> <span className="flex">
@ -67,7 +89,9 @@ const EditResourcePage = (
</span> </span>
} }
label="Save" label="Save"
href={`/resources/${resource.id}`} onClick={() => {
onSubmit(getValues());
}}
/>, />,
<AdminActionLink <AdminActionLink
key="cancel" key="cancel"
@ -78,7 +102,11 @@ const EditResourcePage = (
]} ]}
> >
<main className="mb-12"> <main className="mb-12">
<ResourceForm resource={resource} /> <ResourceForm
register={register}
error={serverError}
resource={resource as ResourceUpdateInput}
/>
</main> </main>
</AdminBarLayout> </AdminBarLayout>
<Footer /> <Footer />

View File

@ -3,10 +3,15 @@ import {
Skill, Skill,
Platform, Platform,
type AuditoryResource, type AuditoryResource,
PaymentType,
} from "@prisma/client"; } from "@prisma/client";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "~/server/api/trpc";
export const auditoryResourceRouter = createTRPCRouter({ export const auditoryResourceRouter = createTRPCRouter({
byId: publicProcedure byId: publicProcedure
@ -25,6 +30,43 @@ export const auditoryResourceRouter = createTRPCRouter({
return ctx.prisma.auditoryResource.findMany(); 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 search: publicProcedure
.input( .input(
z.object({ z.object({