From 2e2f99e0ec3398bfabc8f0a011ea460c6dbb781f Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Wed, 30 Aug 2023 21:48:04 -0500
Subject: [PATCH 01/14] add admin bar with create button

---
 src/pages/resources/index.tsx | 83 +++++++++++++++++++++--------------
 1 file changed, 51 insertions(+), 32 deletions(-)

diff --git a/src/pages/resources/index.tsx b/src/pages/resources/index.tsx
index 87a1f4c..6779419 100644
--- a/src/pages/resources/index.tsx
+++ b/src/pages/resources/index.tsx
@@ -1,5 +1,5 @@
 import { LinkIcon } from "@heroicons/react/20/solid";
-import { PrinterIcon } from "@heroicons/react/24/solid";
+import { PrinterIcon, PlusCircleIcon } from "@heroicons/react/24/solid";
 import Link from "next/link";
 import { useRouter } from "next/router";
 import ResourceTable from "~/components/ResourceTable";
@@ -8,6 +8,42 @@ import { parseQueryData } from "~/utils/parseSearchForm";
 import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
 import { type AuditoryResource } from "@prisma/client";
 import { QueryWaitWrapper } from "~/components/LoadingWrapper";
+import { AdminBarLayout } from "~/components/admin/ControlBar";
+import { AdminActionLink } from "~/components/admin/common";
+
+const PageHeader = ({ printLink }: { printLink: string }) => {
+  return (
+    <div className="mb-2 flex flex-row justify-between p-2 print:hidden sm:mb-4 sm:p-4">
+      <section className="space-y-2">
+        <h1 className="text-3xl font-bold">All Resources</h1>
+        <div className="">
+          <p className="inline">Fill out the </p>
+          <Link
+            href="/resources/search"
+            className="inline rounded-lg border border-neutral-800 bg-neutral-200 px-2 py-[4px] hover:bg-neutral-900 hover:text-white"
+          >
+            search form
+            <LinkIcon className="inline w-4" />
+          </Link>
+          <p className="inline">
+            {" "}
+            for a list of auditory training resource recommendations.
+          </p>
+        </div>
+      </section>
+
+      <section className="mt-auto">
+        <Link
+          href={printLink}
+          className="inline-block whitespace-nowrap rounded-md border border-neutral-900 bg-yellow-200 px-4 py-2 align-middle font-semibold shadow shadow-black/50 duration-200 ease-out hover:bg-yellow-300 hover:shadow-md print:hidden sm:space-x-2"
+        >
+          <span className="hidden sm:inline-block">Print Results</span>
+          <PrinterIcon className="inline-block w-6" />
+        </Link>
+      </section>
+    </div>
+  );
+};
 
 const Resources = () => {
   const router = useRouter();
@@ -49,39 +85,22 @@ const Resources = () => {
 
   return (
     <HeaderFooterLayout>
-      <div className="mx-auto mb-12 mt-6 w-full max-w-6xl md:px-2">
-        <div className="mb-2 flex flex-row justify-between p-2 print:hidden sm:mb-4 sm:p-4">
-          <section className="space-y-2">
-            <h1 className="text-3xl font-bold">All Resources</h1>
-            <div className="">
-              <p className="inline">Fill out the </p>
-              <Link
-                href="/resources/search"
-                className="inline rounded-lg border border-neutral-800 bg-neutral-200 px-2 py-[4px] hover:bg-neutral-900 hover:text-white"
-              >
-                search form
-                <LinkIcon className="inline w-4" />
-              </Link>
-              <p className="inline">
-                {" "}
-                for a list of auditory training resource recommendations.
-              </p>
-            </div>
-          </section>
+      <AdminBarLayout
+        actions={[
+          <AdminActionLink
+            key="cancel"
+            symbol={<PlusCircleIcon className="w-4" />}
+            label="Create New"
+            href={`/resources/create`}
+          />,
+        ]}
+      >
+        <div className="mx-auto mb-12 mt-6 w-full max-w-6xl md:px-2">
+          <PageHeader printLink={printLink} />
 
-          <section className="mt-auto">
-            <Link
-              href={printLink}
-              className="inline-block whitespace-nowrap rounded-md border border-neutral-900 bg-yellow-200 px-4 py-2 align-middle font-semibold shadow shadow-black/50 duration-200 ease-out hover:bg-yellow-300 hover:shadow-md print:hidden sm:space-x-2"
-            >
-              <span className="hidden sm:inline-block">Print Results</span>
-              <PrinterIcon className="inline-block w-6" />
-            </Link>
-          </section>
+          <QueryWaitWrapper query={resourceQuery} Render={ConditionalTable} />
         </div>
-
-        <QueryWaitWrapper query={resourceQuery} Render={ConditionalTable} />
-      </div>
+      </AdminBarLayout>
     </HeaderFooterLayout>
   );
 };
-- 
2.47.2


From 6e4efe2842e842518f064bd1846c4b9a9a65f4ae Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Wed, 30 Aug 2023 22:07:56 -0500
Subject: [PATCH 02/14] reuse form to create resource create page

---
 src/components/admin/resources/form.tsx | 29 +++++++++------
 src/pages/resources/create.tsx          | 49 +++++++++++++++++++++++++
 2 files changed, 66 insertions(+), 12 deletions(-)
 create mode 100644 src/pages/resources/create.tsx

diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx
index 6d6ac69..ceb2fa5 100644
--- a/src/components/admin/resources/form.tsx
+++ b/src/components/admin/resources/form.tsx
@@ -86,15 +86,22 @@ const SelectImageInput = () => {
         htmlFor="resource-image-file"
         className="bg-whit group relative cursor-pointer overflow-hidden rounded-xl border border-neutral-400 drop-shadow-lg"
       >
-        <ResourcePhoto
-          name={name ?? "unknown resource logo"}
-          photo={photo ?? null}
-          src={icon}
-        />
-
-        <div className="absolute bottom-0 left-0 right-0 top-0 hidden place-items-center group-hover:grid group-hover:bg-white/70">
-          <PencilSquareIcon className="w-16 text-black/50" />
-        </div>
+        {photo ? (
+          <>
+            <ResourcePhoto
+              name={name ?? "unknown resource logo"}
+              photo={photo}
+              src={icon}
+            />
+            <div className="absolute bottom-0 left-0 right-0 top-0 hidden place-items-center group-hover:grid group-hover:bg-white/70">
+              <PencilSquareIcon className="w-16 text-black/50" />
+            </div>
+          </>
+        ) : (
+          <div className="grid aspect-square place-items-center hover:bg-white/70">
+            <PencilSquareIcon className="h-16 w-16 text-black/50" />
+          </div>
+        )}
       </label>
       <input
         onChange={onChange}
@@ -372,9 +379,7 @@ function ResourceSummarySubForm({
       <MultiSelectorMany
         details={register("payment_options", { required: true })}
         label="Price Category"
-        defaultValues={
-          resource?.payment_options ?? [PaymentType.FREE.toString()]
-        }
+        defaultValues={resource?.payment_options ?? []}
       >
         <PaymentTypeOption type={PaymentType.FREE} label="Free" />
         <PaymentTypeOption
diff --git a/src/pages/resources/create.tsx b/src/pages/resources/create.tsx
new file mode 100644
index 0000000..2a0ced8
--- /dev/null
+++ b/src/pages/resources/create.tsx
@@ -0,0 +1,49 @@
+import { XCircleIcon, PlusCircleIcon } from "@heroicons/react/20/solid";
+import { useState } from "react";
+import { type SubmitHandler, useForm } from "react-hook-form";
+import { AdminBarLayout } from "~/components/admin/ControlBar";
+import { AdminActionButton, AdminActionLink } from "~/components/admin/common";
+import {
+  ResourceForm,
+  type ResourceUpdateInput,
+} from "~/components/admin/resources/form";
+import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
+
+const EditResourcePage = () => {
+  const formMethods = useForm<ResourceUpdateInput>();
+
+  const [serverError, _setServerError] = useState<string | undefined>(undefined);
+
+  const onSubmit: SubmitHandler<ResourceUpdateInput> = () => {
+    // TODO: TRPC request to create resource
+  };
+
+  return (
+    <HeaderFooterLayout>
+      <AdminBarLayout
+        actions={[
+          <AdminActionButton
+            key="create"
+            symbol={<PlusCircleIcon className="w-4" />}
+            label="Create"
+            onClick={() => {
+              onSubmit(formMethods.getValues());
+            }}
+          />,
+          <AdminActionLink
+            key="cancel"
+            symbol={<XCircleIcon className="w-4" />}
+            label="Cancel"
+            href={`/resources`}
+          />,
+        ]}
+      >
+        <div className="mb-12">
+          <ResourceForm methods={formMethods} error={serverError} />
+        </div>
+      </AdminBarLayout>
+    </HeaderFooterLayout>
+  );
+};
+
+export default EditResourcePage;
-- 
2.47.2


From 7fc0895177554fd3921d928c85cd580b804e9ecd Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Wed, 30 Aug 2023 22:13:41 -0500
Subject: [PATCH 03/14] fix issue where edit page resource query was refetching
 causing icon file to be reset

---
 src/pages/resources/[id]/edit.tsx | 3 +++
 src/pages/resources/create.tsx    | 4 +++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/pages/resources/[id]/edit.tsx b/src/pages/resources/[id]/edit.tsx
index a5d33c2..471db47 100644
--- a/src/pages/resources/[id]/edit.tsx
+++ b/src/pages/resources/[id]/edit.tsx
@@ -25,6 +25,9 @@ const EditResourcePage = () => {
       retry(_failureCount, error) {
         return error.data?.httpStatus !== 404;
       },
+      refetchOnWindowFocus: false,
+      refetchOnMount: false,
+      refetchInterval: false,
     }
   );
 
diff --git a/src/pages/resources/create.tsx b/src/pages/resources/create.tsx
index 2a0ced8..9c8dcb9 100644
--- a/src/pages/resources/create.tsx
+++ b/src/pages/resources/create.tsx
@@ -12,7 +12,9 @@ import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
 const EditResourcePage = () => {
   const formMethods = useForm<ResourceUpdateInput>();
 
-  const [serverError, _setServerError] = useState<string | undefined>(undefined);
+  const [serverError, _setServerError] = useState<string | undefined>(
+    undefined
+  );
 
   const onSubmit: SubmitHandler<ResourceUpdateInput> = () => {
     // TODO: TRPC request to create resource
-- 
2.47.2


From c243fda8e1b878ae085ead600d4e0741bb4728de Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Wed, 30 Aug 2023 22:22:32 -0500
Subject: [PATCH 04/14] fix link add button alignment

---
 src/components/admin/resources/form.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx
index ceb2fa5..9e2d574 100644
--- a/src/components/admin/resources/form.tsx
+++ b/src/components/admin/resources/form.tsx
@@ -266,15 +266,15 @@ const ResourceLinkSubForm = () => {
         <h1 className="text-xl">Links</h1>
         <button
           type="button"
-          className="h-6 rounded-full border border-neutral-900 bg-neutral-200 px-2 leading-tight hover:bg-yellow-400"
+          className="flex h-6 flex-row items-center rounded-full border border-neutral-900 bg-neutral-200 px-2 leading-tight drop-shadow-sm hover:bg-yellow-400"
           onClick={() => {
             setLinkModalOpen(!linkModalOpen);
           }}
         >
-          <span className="my-auto inline-block align-middle text-sm font-normal text-neutral-700">
+          <span className="text-sm font-normal leading-3 text-neutral-700">
             Add
           </span>
-          <PlusIcon className="my-auto inline-block w-4 align-middle" />
+          <PlusIcon className="w-4 leading-3" />
         </button>
       </div>
 
-- 
2.47.2


From cd1dc2a555b42c578ca54930f46319720430d4a9 Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Mon, 4 Sep 2023 00:03:42 -0500
Subject: [PATCH 05/14] add create resource trpc function

---
 src/components/admin/common.tsx             |  5 +-
 src/components/admin/resources/form.tsx     |  5 +-
 src/components/forms/textInput.tsx          |  4 +-
 src/pages/resources/[id]/edit.tsx           |  4 +-
 src/pages/resources/create.tsx              | 41 ++++++++---
 src/server/api/routers/auditoryResources.ts | 81 +++++++++++----------
 6 files changed, 82 insertions(+), 58 deletions(-)

diff --git a/src/components/admin/common.tsx b/src/components/admin/common.tsx
index ce1ae95..0f8abd3 100644
--- a/src/components/admin/common.tsx
+++ b/src/components/admin/common.tsx
@@ -42,13 +42,16 @@ const AdminActionButton = ({
   label,
   onClick,
   symbol,
+  type = "button",
 }: {
   label: string;
-  onClick: () => void;
+  onClick?: () => void;
   symbol: JSX.Element | undefined;
+  type?: HTMLButtonElement["type"];
 }) => {
   return (
     <button
+      type={type}
       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}
     >
diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx
index 9e2d574..9bc14ec 100644
--- a/src/components/admin/resources/form.tsx
+++ b/src/components/admin/resources/form.tsx
@@ -47,6 +47,7 @@ import { ResourcePhoto } from "~/components/ResourcePhoto";
 Modal.setAppElement("#__next");
 
 export type ResourceUpdateInput = RouterInputs["auditoryResource"]["update"];
+export type ResourceCreateInput = RouterInputs["auditoryResource"]["create"];
 
 /**
  * Renders the image selector for resource form.
@@ -361,14 +362,14 @@ function ResourceSummarySubForm({
             <InfoInputLine
               details={register("manufacturer.name", { required: true })}
               placeholder="manufacturer"
-              value={resource?.manufacturer?.name ?? ""}
               hint="manufacturer"
+              value={resource?.name}
             />
           </span>
           <InfoInputLine
             details={register("name", { required: true })}
             placeholder="name"
-            value={resource?.name ?? ""}
+            value={resource?.name}
             hint="name"
           />
           <span className="my-1 block w-full text-center text-xs italic text-neutral-400">
diff --git a/src/components/forms/textInput.tsx b/src/components/forms/textInput.tsx
index 29e51ac..0bd6860 100644
--- a/src/components/forms/textInput.tsx
+++ b/src/components/forms/textInput.tsx
@@ -2,7 +2,9 @@ import { type HTMLInputTypeAttribute, useState } from "react";
 import {
   type UseFormRegisterReturn,
   type InternalFieldName,
+  useFormContext,
 } from "react-hook-form";
+import { ResourceCreateInput } from "../admin/resources/form";
 
 /**
  * Single line input for the fields found to the right of the
@@ -14,7 +16,7 @@ function InfoInputLine<TFieldName extends InternalFieldName>({
   hint,
   details,
 }: {
-  value: string;
+  value?: string | undefined;
   placeholder: string;
   hint?: string;
   details: UseFormRegisterReturn<TFieldName>;
diff --git a/src/pages/resources/[id]/edit.tsx b/src/pages/resources/[id]/edit.tsx
index 471db47..61d9b20 100644
--- a/src/pages/resources/[id]/edit.tsx
+++ b/src/pages/resources/[id]/edit.tsx
@@ -40,8 +40,8 @@ const EditResourcePage = () => {
     });
 
     const { mutate } = api.auditoryResource.update.useMutation({
-      onSuccess: async (_resData) => {
-        if (!data) {
+      onSuccess: async (resData) => {
+        if (!resData) {
           setServerError("An unexpected error has occured");
           return;
         }
diff --git a/src/pages/resources/create.tsx b/src/pages/resources/create.tsx
index 9c8dcb9..684a642 100644
--- a/src/pages/resources/create.tsx
+++ b/src/pages/resources/create.tsx
@@ -1,23 +1,40 @@
 import { XCircleIcon, PlusCircleIcon } from "@heroicons/react/20/solid";
-import { useState } from "react";
-import { type SubmitHandler, useForm } from "react-hook-form";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+import {
+  type SubmitHandler,
+  useForm,
+  type UseFormReturn,
+} from "react-hook-form";
 import { AdminBarLayout } from "~/components/admin/ControlBar";
 import { AdminActionButton, AdminActionLink } from "~/components/admin/common";
 import {
+  type ResourceCreateInput,
   ResourceForm,
   type ResourceUpdateInput,
 } from "~/components/admin/resources/form";
 import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
+import { api } from "~/utils/api";
 
 const EditResourcePage = () => {
-  const formMethods = useForm<ResourceUpdateInput>();
+  const router = useRouter();
+  const formMethods = useForm<ResourceCreateInput>();
 
-  const [serverError, _setServerError] = useState<string | undefined>(
-    undefined
-  );
+  const [serverError, setServerError] = useState<string | undefined>(undefined);
 
-  const onSubmit: SubmitHandler<ResourceUpdateInput> = () => {
-    // TODO: TRPC request to create resource
+  const { mutate } = api.auditoryResource.create.useMutation({
+    onSuccess: async (resData) => {
+      if (!resData) {
+        setServerError("An unexpected error has occured");
+      }
+
+      setServerError(undefined);
+      await router.push(`/resources/${resData.id}`);
+    },
+  });
+
+  const onSubmit: SubmitHandler<ResourceCreateInput> = (data) => {
+    mutate(data);
   };
 
   return (
@@ -28,9 +45,6 @@ const EditResourcePage = () => {
             key="create"
             symbol={<PlusCircleIcon className="w-4" />}
             label="Create"
-            onClick={() => {
-              onSubmit(formMethods.getValues());
-            }}
           />,
           <AdminActionLink
             key="cancel"
@@ -41,7 +55,10 @@ const EditResourcePage = () => {
         ]}
       >
         <div className="mb-12">
-          <ResourceForm methods={formMethods} error={serverError} />
+          <ResourceForm
+            methods={formMethods as UseFormReturn<ResourceUpdateInput>}
+            error={serverError}
+          />
         </div>
       </AdminBarLayout>
     </HeaderFooterLayout>
diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts
index fde2735..4f6a705 100644
--- a/src/server/api/routers/auditoryResources.ts
+++ b/src/server/api/routers/auditoryResources.ts
@@ -16,6 +16,38 @@ const emptyStringToUndefined = (val: string | undefined | null) => {
   return val;
 };
 
+const AuditoryResourceSchema = z.object({
+  id: z.string(),
+  icon: z.string().min(1),
+  name: z.string().min(1),
+  description: z.string().min(1),
+  manufacturer: z.object({
+    name: z.string().min(1),
+    required: z.boolean(),
+    notice: z
+      .string()
+
+      .nullable()
+      .transform(emptyStringToUndefined),
+  }),
+  ages: z.object({ min: z.number().int(), max: z.number().int() }),
+  skills: z.array(z.nativeEnum(Skill)),
+  skill_levels: z.array(z.nativeEnum(SkillLevel)),
+  payment_options: z.array(z.nativeEnum(PaymentType)),
+  photo: z
+    .object({
+      name: z.string(),
+      data: z.instanceof(Buffer),
+    })
+    .nullable(),
+  platform_links: z.array(
+    z.object({
+      platform: z.nativeEnum(Platform),
+      link: z.string().min(1),
+    })
+  ),
+});
+
 export const auditoryResourceRouter = createTRPCRouter({
   byId: publicProcedure
     .input(z.object({ id: z.string() }))
@@ -45,47 +77,16 @@ export const auditoryResourceRouter = createTRPCRouter({
     return ctx.prisma.auditoryResource.findMany();
   }),
 
+  create: protectedProcedure
+    .input(AuditoryResourceSchema)
+    .mutation(async ({ input, ctx }) => {
+      return await ctx.prisma.auditoryResource.create({
+        data: input,
+      });
+    }),
+
   update: protectedProcedure
-    .input(
-      z.object({
-        id: z.string(),
-        icon: z.string().min(1).optional(),
-        name: z.string().min(1).optional(),
-        description: z.string().min(1).optional(),
-        manufacturer: z
-          .object({
-            name: z.string().min(1),
-            required: z.boolean(),
-            notice: z
-              .string()
-              .optional()
-              .nullable()
-              .transform(emptyStringToUndefined),
-          })
-          .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(),
-        photo: z
-          .object({
-            name: z.string(),
-            data: z.instanceof(Buffer),
-          })
-          .nullable()
-          .optional(),
-        platform_links: z
-          .array(
-            z.object({
-              platform: z.nativeEnum(Platform),
-              link: z.string().min(1),
-            })
-          )
-          .optional(),
-      })
-    )
+    .input(AuditoryResourceSchema.partial())
     .mutation(async ({ input, ctx }) => {
       return await ctx.prisma.auditoryResource.update({
         where: {
-- 
2.47.2


From f7144e7cf4016c25901f2b37522a8ec8b536dda0 Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Mon, 4 Sep 2023 00:25:10 -0500
Subject: [PATCH 06/14] switch to handle submit

---
 src/components/admin/resources/form.tsx | 19 ++++++++++++-------
 src/components/forms/textInput.tsx      |  2 --
 src/pages/resources/create.tsx          |  7 ++++++-
 3 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx
index 9bc14ec..fa689f8 100644
--- a/src/components/admin/resources/form.tsx
+++ b/src/components/admin/resources/form.tsx
@@ -346,7 +346,10 @@ function ResourceSummarySubForm({
 }: {
   resource?: ResourceUpdateInput;
 }) {
-  const { register } = useFormContext<ResourceUpdateInput>();
+  const {
+    register,
+    formState: { errors },
+  } = useFormContext<ResourceUpdateInput>();
 
   return (
     <div className="space-y-4 px-4">
@@ -360,14 +363,16 @@ function ResourceSummarySubForm({
           </h2>
           <span className="text-md">
             <InfoInputLine
-              details={register("manufacturer.name", { required: true })}
+              details={register("manufacturer.name", {
+                required: "Field required",
+              })}
               placeholder="manufacturer"
               hint="manufacturer"
               value={resource?.name}
             />
           </span>
           <InfoInputLine
-            details={register("name", { required: true })}
+            details={register("name", { required: "Field required" })}
             placeholder="name"
             value={resource?.name}
             hint="name"
@@ -378,7 +383,7 @@ function ResourceSummarySubForm({
         </div>
       </div>
       <MultiSelectorMany
-        details={register("payment_options", { required: true })}
+        details={register("payment_options", { required: "Field required" })}
         label="Price Category"
         defaultValues={resource?.payment_options ?? []}
       >
@@ -394,7 +399,7 @@ function ResourceSummarySubForm({
       </MultiSelectorMany>
 
       <MultiSelectorMany
-        details={register("skill_levels", { required: true })}
+        details={register("skill_levels", { required: "Field required" })}
         label="Skill Level"
         defaultValues={resource?.skill_levels ?? []}
       >
@@ -410,7 +415,7 @@ function ResourceSummarySubForm({
       </MultiSelectorMany>
 
       <MultiSelectorMany
-        details={register("skills", { required: true })}
+        details={register("skills", { required: "Field required" })}
         label="Skills Covered"
         defaultValues={resource?.skills ?? []}
       >
@@ -452,7 +457,7 @@ const ResourceDescriptionSubForm = () => {
           <ChevronDownIcon className="mx-2 my-auto w-4 text-white group-hover:animate-bounce" />
         </button>
         <textarea
-          {...register("description", { required: true })}
+          {...register("description", { required: "Field required" })}
           className={
             "h-48 w-full rounded-b-xl p-2" + (dropdownOpen ? " hidden" : "")
           }
diff --git a/src/components/forms/textInput.tsx b/src/components/forms/textInput.tsx
index 0bd6860..4c3384e 100644
--- a/src/components/forms/textInput.tsx
+++ b/src/components/forms/textInput.tsx
@@ -2,9 +2,7 @@ import { type HTMLInputTypeAttribute, useState } from "react";
 import {
   type UseFormRegisterReturn,
   type InternalFieldName,
-  useFormContext,
 } from "react-hook-form";
-import { ResourceCreateInput } from "../admin/resources/form";
 
 /**
  * Single line input for the fields found to the right of the
diff --git a/src/pages/resources/create.tsx b/src/pages/resources/create.tsx
index 684a642..7b40181 100644
--- a/src/pages/resources/create.tsx
+++ b/src/pages/resources/create.tsx
@@ -1,6 +1,6 @@
 import { XCircleIcon, PlusCircleIcon } from "@heroicons/react/20/solid";
 import { useRouter } from "next/router";
-import { useEffect, useState } from "react";
+import { useState } from "react";
 import {
   type SubmitHandler,
   useForm,
@@ -45,6 +45,11 @@ const EditResourcePage = () => {
             key="create"
             symbol={<PlusCircleIcon className="w-4" />}
             label="Create"
+            onClick={() => {
+              formMethods
+                .handleSubmit(onSubmit)()
+                .catch((error) => console.error(error));
+            }}
           />,
           <AdminActionLink
             key="cancel"
-- 
2.47.2


From 2edc5d57b6abf56e7d99ac546fdb2c65955a5350 Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Mon, 4 Sep 2023 00:33:54 -0500
Subject: [PATCH 07/14] improve resource schema to reflect new icon structure

---
 prisma/schema.prisma                        |  2 +-
 src/components/ResourcePhoto.tsx            |  2 +-
 src/components/admin/resources/form.tsx     |  5 +---
 src/server/api/routers/auditoryResources.ts | 27 +++++++++------------
 4 files changed, 15 insertions(+), 21 deletions(-)

diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2ca430b..0a9465a 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -70,7 +70,7 @@ type Photo {
 
 model AuditoryResource {
   id              String         @id @default(auto()) @map("_id") @db.ObjectId
-  icon            String
+  icon            String?
   name            String
   description     String
   photo           Photo?
diff --git a/src/components/ResourcePhoto.tsx b/src/components/ResourcePhoto.tsx
index df56ba2..443b26d 100644
--- a/src/components/ResourcePhoto.tsx
+++ b/src/components/ResourcePhoto.tsx
@@ -8,7 +8,7 @@ type ResourcePhotoProps = (
       src: string | undefined;
     }
   | {
-      src: string;
+      src: string | undefined;
       photo: null;
     }
 ) & { name: string };
diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx
index fa689f8..cc24ff9 100644
--- a/src/components/admin/resources/form.tsx
+++ b/src/components/admin/resources/form.tsx
@@ -346,10 +346,7 @@ function ResourceSummarySubForm({
 }: {
   resource?: ResourceUpdateInput;
 }) {
-  const {
-    register,
-    formState: { errors },
-  } = useFormContext<ResourceUpdateInput>();
+  const { register } = useFormContext<ResourceUpdateInput>();
 
   return (
     <div className="space-y-4 px-4">
diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts
index 4f6a705..e631fa6 100644
--- a/src/server/api/routers/auditoryResources.ts
+++ b/src/server/api/routers/auditoryResources.ts
@@ -17,18 +17,13 @@ const emptyStringToUndefined = (val: string | undefined | null) => {
 };
 
 const AuditoryResourceSchema = z.object({
-  id: z.string(),
-  icon: z.string().min(1),
+  icon: z.string().min(1).optional(),
   name: z.string().min(1),
   description: z.string().min(1),
   manufacturer: z.object({
     name: z.string().min(1),
-    required: z.boolean(),
-    notice: z
-      .string()
-
-      .nullable()
-      .transform(emptyStringToUndefined),
+    required: z.boolean().default(false),
+    notice: z.string().nullable().transform(emptyStringToUndefined),
   }),
   ages: z.object({ min: z.number().int(), max: z.number().int() }),
   skills: z.array(z.nativeEnum(Skill)),
@@ -40,12 +35,14 @@ const AuditoryResourceSchema = z.object({
       data: z.instanceof(Buffer),
     })
     .nullable(),
-  platform_links: z.array(
-    z.object({
-      platform: z.nativeEnum(Platform),
-      link: z.string().min(1),
-    })
-  ),
+  platform_links: z
+    .array(
+      z.object({
+        platform: z.nativeEnum(Platform),
+        link: z.string().min(1),
+      })
+    )
+    .default([]),
 });
 
 export const auditoryResourceRouter = createTRPCRouter({
@@ -86,7 +83,7 @@ export const auditoryResourceRouter = createTRPCRouter({
     }),
 
   update: protectedProcedure
-    .input(AuditoryResourceSchema.partial())
+    .input(AuditoryResourceSchema.partial().extend({ id: z.string() }))
     .mutation(async ({ input, ctx }) => {
       return await ctx.prisma.auditoryResource.update({
         where: {
-- 
2.47.2


From 634f35657e9771126cfbe815e22cd088bb97db6e Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Tue, 5 Sep 2023 19:45:05 -0500
Subject: [PATCH 08/14] improve readability of server error for user

---
 src/pages/resources/[id]/edit.tsx | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/pages/resources/[id]/edit.tsx b/src/pages/resources/[id]/edit.tsx
index 61d9b20..e228ce7 100644
--- a/src/pages/resources/[id]/edit.tsx
+++ b/src/pages/resources/[id]/edit.tsx
@@ -49,7 +49,20 @@ const EditResourcePage = () => {
         setServerError(undefined);
         await router.push(`/resources/${data.id}`);
       },
-      onError: (error) => setServerError(error.message),
+      onError: (error) => {
+        try {
+          const zodErrors = JSON.parse(error.message) as unknown as { message: string }[];
+          setServerError(
+            zodErrors
+              .map((error) => {
+                return error.message;
+              })
+              .join(", ")
+          );
+        } catch {
+          setServerError(error.message);
+        }
+      },
     });
 
     const onSubmit: SubmitHandler<ResourceUpdateInput> = (data) => {
-- 
2.47.2


From cad4b78f4767b8ad358fa08fb71dca1190116dcf Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Tue, 5 Sep 2023 19:45:32 -0500
Subject: [PATCH 09/14] add input fields for age range

---
 src/components/admin/resources/form.tsx     | 28 +++++++++++++++++++++
 src/components/forms/inputLabel.tsx         | 16 ++++++++++++
 src/components/forms/selectors.tsx          | 11 +++-----
 src/components/forms/textInput.tsx          | 10 +++++---
 src/server/api/routers/auditoryResources.ts |  9 ++++++-
 5 files changed, 61 insertions(+), 13 deletions(-)
 create mode 100644 src/components/forms/inputLabel.tsx

diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx
index cc24ff9..9043c17 100644
--- a/src/components/admin/resources/form.tsx
+++ b/src/components/admin/resources/form.tsx
@@ -41,6 +41,7 @@ import Modal from "react-modal";
 import { type RouterInputs } from "~/utils/api";
 import { PlatformLinkButton } from "~/pages/resources/[id]";
 import { ResourcePhoto } from "~/components/ResourcePhoto";
+import { FieldLabel } from "~/components/forms/inputLabel";
 
 // Required for accessibility
 // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
@@ -379,6 +380,33 @@ function ResourceSummarySubForm({
           </span>
         </div>
       </div>
+
+      <div>
+        <FieldLabel
+          heading="Age Range"
+          subheading="Specify the minimum and maximum age range supported by the resource"
+        />
+        <div className="mt-2 flex flex-row space-x-4">
+          <GenericInput
+            type="number"
+            placeholder="minimum age"
+            details={register("ages.min", {
+              required: "Field required",
+              valueAsNumber: true,
+            })}
+          />
+          <span className="text-xl">-</span>
+          <GenericInput
+            type="number"
+            placeholder="maximum age"
+            details={register("ages.max", {
+              required: "Field required",
+              valueAsNumber: true,
+            })}
+          />
+        </div>
+      </div>
+
       <MultiSelectorMany
         details={register("payment_options", { required: "Field required" })}
         label="Price Category"
diff --git a/src/components/forms/inputLabel.tsx b/src/components/forms/inputLabel.tsx
new file mode 100644
index 0000000..17443c5
--- /dev/null
+++ b/src/components/forms/inputLabel.tsx
@@ -0,0 +1,16 @@
+export const FieldLabel = ({
+  heading,
+  subheading,
+}: {
+  heading: string;
+  subheading: string;
+}) => {
+  return (
+    <div>
+      <label className="text-md block font-semibold">{heading}</label>
+      <span className="block text-sm italic text-neutral-400">
+        {subheading}
+      </span>
+    </div>
+  );
+};
diff --git a/src/components/forms/selectors.tsx b/src/components/forms/selectors.tsx
index 59a3082..ff8a34d 100644
--- a/src/components/forms/selectors.tsx
+++ b/src/components/forms/selectors.tsx
@@ -4,6 +4,7 @@ import {
   useFormContext,
   type UseFormRegisterReturn,
 } from "react-hook-form";
+import { FieldLabel } from "./inputLabel";
 
 // generics
 interface ToStringable {
@@ -60,10 +61,7 @@ function MultiSelectorMany<T extends ToStringable>({
     <SelectorContext.Provider value={{ type: "many", updateCallback }}>
       <SelectedManyContext.Provider value={selected}>
         <div className="flex flex-col">
-          <label className="text-md block font-semibold">{label}</label>
-          <span className="block text-sm italic text-neutral-400">
-            Select all that apply
-          </span>
+          <FieldLabel heading={label} subheading="Select all that apply" />
           <input {...details} readOnly type="text" className="hidden" />
           <div className="mt-2 space-x-2 space-y-2 overflow-x-auto">
             {children}
@@ -100,10 +98,7 @@ function MultiSelector<T extends ToStringable>({
     >
       <SelectedUniqueContext.Provider value={selected}>
         <div className="flex flex-col">
-          <label className="text-md block font-semibold">{label}</label>
-          <span className="block text-sm italic text-neutral-400">
-            Select one from below
-          </span>
+          <FieldLabel heading={label} subheading="Select one from below" />
           <input {...details} readOnly type="text" className="hidden" />
           <div className="space-x-2 space-y-2 overflow-x-auto">{children}</div>
         </div>
diff --git a/src/components/forms/textInput.tsx b/src/components/forms/textInput.tsx
index 4c3384e..f479d36 100644
--- a/src/components/forms/textInput.tsx
+++ b/src/components/forms/textInput.tsx
@@ -50,16 +50,18 @@ function GenericInput<TFieldName extends InternalFieldName>({
   type = "text",
   details,
 }: {
-  label: string;
+  label?: string;
   placeholder?: string;
   type: HTMLInputTypeAttribute;
   details: UseFormRegisterReturn<TFieldName>;
 }) {
   return (
     <section className="w-full space-y-1">
-      <label className="text-md block px-1 font-semibold text-neutral-600">
-        {label}
-      </label>
+      {label ? (
+        <label className="text-md block px-1 font-semibold text-neutral-600">
+          {label}
+        </label>
+      ) : undefined}
       <input
         className="block h-8 w-full rounded-lg border border-neutral-600 px-2 py-1"
         {...details}
diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts
index e631fa6..cfeb071 100644
--- a/src/server/api/routers/auditoryResources.ts
+++ b/src/server/api/routers/auditoryResources.ts
@@ -25,7 +25,14 @@ const AuditoryResourceSchema = z.object({
     required: z.boolean().default(false),
     notice: z.string().nullable().transform(emptyStringToUndefined),
   }),
-  ages: z.object({ min: z.number().int(), max: z.number().int() }),
+  ages: z.object({ min: z.number().int(), max: z.number().int() }).refine(
+    (ages) => {
+      return ages.min < ages.max;
+    },
+    {
+      message: "Minimum supported age must be less than maximum supported age.",
+    }
+  ),
   skills: z.array(z.nativeEnum(Skill)),
   skill_levels: z.array(z.nativeEnum(SkillLevel)),
   payment_options: z.array(z.nativeEnum(PaymentType)),
-- 
2.47.2


From ee7268e724fd778031b56b5aead26f43107d5443 Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Tue, 5 Sep 2023 19:48:02 -0500
Subject: [PATCH 10/14] make input strict

---
 src/server/api/routers/auditoryResources.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts
index cfeb071..4da6867 100644
--- a/src/server/api/routers/auditoryResources.ts
+++ b/src/server/api/routers/auditoryResources.ts
@@ -82,7 +82,7 @@ export const auditoryResourceRouter = createTRPCRouter({
   }),
 
   create: protectedProcedure
-    .input(AuditoryResourceSchema)
+    .input(AuditoryResourceSchema.strict())
     .mutation(async ({ input, ctx }) => {
       return await ctx.prisma.auditoryResource.create({
         data: input,
-- 
2.47.2


From 8b423774535b939fa2135b51af78dcf59c484a60 Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Tue, 5 Sep 2023 20:03:38 -0500
Subject: [PATCH 11/14] reduce redundant trpc error handling code to function

---
 src/pages/resources/[id]/edit.tsx           | 14 ++------------
 src/pages/resources/create.tsx              |  8 +++++++-
 src/server/api/routers/auditoryResources.ts |  2 +-
 src/utils/parseTRPCError.ts                 | 12 ++++++++++++
 4 files changed, 22 insertions(+), 14 deletions(-)
 create mode 100644 src/utils/parseTRPCError.ts

diff --git a/src/pages/resources/[id]/edit.tsx b/src/pages/resources/[id]/edit.tsx
index e228ce7..7641bc5 100644
--- a/src/pages/resources/[id]/edit.tsx
+++ b/src/pages/resources/[id]/edit.tsx
@@ -13,6 +13,7 @@ import { useRouter } from "next/router";
 import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
 import { QueryWaitWrapper } from "~/components/LoadingWrapper";
 import { type AuditoryResource } from "@prisma/client";
+import { parseTRPCErrorMessage } from "~/utils/parseTRPCError";
 
 const EditResourcePage = () => {
   const router = useRouter();
@@ -50,18 +51,7 @@ const EditResourcePage = () => {
         await router.push(`/resources/${data.id}`);
       },
       onError: (error) => {
-        try {
-          const zodErrors = JSON.parse(error.message) as unknown as { message: string }[];
-          setServerError(
-            zodErrors
-              .map((error) => {
-                return error.message;
-              })
-              .join(", ")
-          );
-        } catch {
-          setServerError(error.message);
-        }
+        setServerError(parseTRPCErrorMessage(error.message));
       },
     });
 
diff --git a/src/pages/resources/create.tsx b/src/pages/resources/create.tsx
index 7b40181..454d26a 100644
--- a/src/pages/resources/create.tsx
+++ b/src/pages/resources/create.tsx
@@ -15,6 +15,7 @@ import {
 } from "~/components/admin/resources/form";
 import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
 import { api } from "~/utils/api";
+import { parseTRPCErrorMessage } from "~/utils/parseTRPCError";
 
 const EditResourcePage = () => {
   const router = useRouter();
@@ -31,6 +32,9 @@ const EditResourcePage = () => {
       setServerError(undefined);
       await router.push(`/resources/${resData.id}`);
     },
+    onError: (error) => {
+      setServerError(parseTRPCErrorMessage(error.message));
+    },
   });
 
   const onSubmit: SubmitHandler<ResourceCreateInput> = (data) => {
@@ -61,7 +65,9 @@ const EditResourcePage = () => {
       >
         <div className="mb-12">
           <ResourceForm
-            methods={formMethods as UseFormReturn<ResourceUpdateInput>}
+            methods={
+              formMethods as unknown as UseFormReturn<ResourceUpdateInput>
+            }
             error={serverError}
           />
         </div>
diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts
index 4da6867..a88bf5d 100644
--- a/src/server/api/routers/auditoryResources.ts
+++ b/src/server/api/routers/auditoryResources.ts
@@ -17,7 +17,7 @@ const emptyStringToUndefined = (val: string | undefined | null) => {
 };
 
 const AuditoryResourceSchema = z.object({
-  icon: z.string().min(1).optional(),
+  icon: z.string().min(1).optional().nullable(),
   name: z.string().min(1),
   description: z.string().min(1),
   manufacturer: z.object({
diff --git a/src/utils/parseTRPCError.ts b/src/utils/parseTRPCError.ts
new file mode 100644
index 0000000..5d29b30
--- /dev/null
+++ b/src/utils/parseTRPCError.ts
@@ -0,0 +1,12 @@
+export const parseTRPCErrorMessage = (message: string) => {
+  try {
+    const zodErrors = JSON.parse(message) as unknown as { message: string }[];
+    return zodErrors
+      .map((error) => {
+        return error.message;
+      })
+      .join(", ");
+  } catch {
+    return message;
+  }
+};
-- 
2.47.2


From f66c35a225de33711c1887d883bcab96ff88669e Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Tue, 5 Sep 2023 20:27:33 -0500
Subject: [PATCH 12/14] add confirm button for dangerous admin actions

---
 src/components/admin/common.tsx    | 51 +++++++++++++++++++++++++++++-
 src/pages/resources/[id]/edit.tsx  | 13 ++++++--
 src/pages/resources/[id]/index.tsx | 22 ++++++++++---
 src/pages/resources/create.tsx     | 13 ++++++--
 4 files changed, 88 insertions(+), 11 deletions(-)

diff --git a/src/components/admin/common.tsx b/src/components/admin/common.tsx
index 0f8abd3..71beaa8 100644
--- a/src/components/admin/common.tsx
+++ b/src/components/admin/common.tsx
@@ -1,4 +1,6 @@
+import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
 import Link from "next/link";
+import { useEffect, useState } from "react";
 
 const AdminActionBody = ({
   label,
@@ -38,6 +40,53 @@ const AdminActionLink = ({
   );
 };
 
+const AdminActionConfirmButton = ({
+  label,
+  onConfirm,
+  symbol,
+  type = "button",
+}: {
+  label: string;
+  onConfirm?: () => void;
+  symbol: JSX.Element | undefined;
+  type?: HTMLButtonElement["type"];
+}) => {
+  const [isConfirmView, setConfirmView] = useState(false);
+
+  useEffect(() => {
+    if (!isConfirmView) {
+      return;
+    }
+
+    setTimeout(() => {
+      if (isConfirmView) {
+        setConfirmView(false);
+      }
+    }, 5000);
+  }, [isConfirmView]);
+
+  if (isConfirmView) {
+    return (
+      <AdminActionButton
+        symbol={<ExclamationCircleIcon className="w-4 animate-ping" />}
+        label={`Confirm ${label}`}
+        onClick={onConfirm}
+        type={type}
+      />
+    );
+  }
+
+  return (
+    <AdminActionButton
+      symbol={symbol}
+      label={label}
+      onClick={() => {
+        setConfirmView(true);
+      }}
+    />
+  );
+};
+
 const AdminActionButton = ({
   label,
   onClick,
@@ -65,4 +114,4 @@ const AdminActionButton = ({
   );
 };
 
-export { AdminActionLink, AdminActionButton };
+export { AdminActionLink, AdminActionButton, AdminActionConfirmButton };
diff --git a/src/pages/resources/[id]/edit.tsx b/src/pages/resources/[id]/edit.tsx
index 7641bc5..817d3b0 100644
--- a/src/pages/resources/[id]/edit.tsx
+++ b/src/pages/resources/[id]/edit.tsx
@@ -1,6 +1,9 @@
 import { XCircleIcon } from "@heroicons/react/20/solid";
 import { AdminBarLayout } from "~/components/admin/ControlBar";
-import { AdminActionButton, AdminActionLink } from "~/components/admin/common";
+import {
+  AdminActionButton,
+  AdminActionConfirmButton,
+} from "~/components/admin/common";
 import Image from "next/image";
 import {
   ResourceForm,
@@ -87,11 +90,15 @@ const EditResourcePage = () => {
               onSubmit(formMethods.getValues());
             }}
           />,
-          <AdminActionLink
+          <AdminActionConfirmButton
             key="cancel"
             symbol={<XCircleIcon className="w-4" />}
             label="Cancel"
-            href={`/resources/${data.id}`}
+            onConfirm={() => {
+              router.push(`/resources/${data.id}`).catch((error) => {
+                console.error(error);
+              });
+            }}
           />,
         ]}
       >
diff --git a/src/pages/resources/[id]/index.tsx b/src/pages/resources/[id]/index.tsx
index b4471ba..9770b1c 100644
--- a/src/pages/resources/[id]/index.tsx
+++ b/src/pages/resources/[id]/index.tsx
@@ -6,10 +6,14 @@ import { type AuditoryResource, type PlatformLink } from "@prisma/client";
 import Image from "next/image";
 import Link from "next/link";
 import { AdminBarLayout } from "~/components/admin/ControlBar";
-import { AdminActionLink } from "~/components/admin/common";
+import {
+  AdminActionConfirmButton,
+  AdminActionLink,
+} from "~/components/admin/common";
 import { useRouter } from "next/router";
 import { HeaderFooterLayout } from "~/layouts/HeaderFooterLayout";
 import { QueryWaitWrapper } from "~/components/LoadingWrapper";
+import { TrashIcon } from "@heroicons/react/24/outline";
 
 export const PlatformLinkButton = ({
   platformLink,
@@ -122,13 +126,23 @@ const ResourceViewPage = () => {
   return (
     <HeaderFooterLayout>
       <AdminBarLayout
-        actions={
+        actions={[
           <AdminActionLink
+            key="edit"
             symbol={<PencilSquareIcon className="w-4" />}
             label="Edit Page"
             href={`${router.asPath}/edit`}
-          />
-        }
+          />,
+          <AdminActionConfirmButton
+            key="delete"
+            label="Delete"
+            symbol={<TrashIcon className="w-4" />}
+            onConfirm={() => {
+              // todo
+              console.log("deleting");
+            }}
+          />,
+        ]}
       >
         <div className="mb-12">
           <QueryWaitWrapper query={resourceQuery} Render={ConditionalView} />
diff --git a/src/pages/resources/create.tsx b/src/pages/resources/create.tsx
index 454d26a..0920379 100644
--- a/src/pages/resources/create.tsx
+++ b/src/pages/resources/create.tsx
@@ -7,7 +7,10 @@ import {
   type UseFormReturn,
 } from "react-hook-form";
 import { AdminBarLayout } from "~/components/admin/ControlBar";
-import { AdminActionButton, AdminActionLink } from "~/components/admin/common";
+import {
+  AdminActionButton,
+  AdminActionConfirmButton,
+} from "~/components/admin/common";
 import {
   type ResourceCreateInput,
   ResourceForm,
@@ -55,11 +58,15 @@ const EditResourcePage = () => {
                 .catch((error) => console.error(error));
             }}
           />,
-          <AdminActionLink
+          <AdminActionConfirmButton
             key="cancel"
             symbol={<XCircleIcon className="w-4" />}
             label="Cancel"
-            href={`/resources`}
+            onConfirm={() => {
+              router.push("/resources").catch((error) => {
+                console.error(error);
+              });
+            }}
           />,
         ]}
       >
-- 
2.47.2


From 2ef07fd37a3daa2bb2910f1cf6762b97e1f53b5b Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Tue, 5 Sep 2023 20:38:05 -0500
Subject: [PATCH 13/14] add delete function

---
 src/pages/resources/[id]/index.tsx          | 14 ++++++++++++--
 src/server/api/routers/auditoryResources.ts | 14 ++++++++++++++
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/src/pages/resources/[id]/index.tsx b/src/pages/resources/[id]/index.tsx
index 9770b1c..d40e43c 100644
--- a/src/pages/resources/[id]/index.tsx
+++ b/src/pages/resources/[id]/index.tsx
@@ -95,6 +95,15 @@ const ResourceViewPage = () => {
     }
   );
 
+  const { mutate: mutateDelete } = api.auditoryResource.delete.useMutation({
+    onSuccess: async () => {
+      await router.push(`/resources`);
+    },
+    onError: (error) => {
+      console.error(error);
+    },
+  });
+
   const ConditionalView = (data: AuditoryResource) => {
     return (
       <div className="mx-auto flex max-w-2xl flex-col flex-col-reverse divide-x py-4 sm:flex-row">
@@ -138,8 +147,9 @@ const ResourceViewPage = () => {
             label="Delete"
             symbol={<TrashIcon className="w-4" />}
             onConfirm={() => {
-              // todo
-              console.log("deleting");
+              mutateDelete({
+                id,
+              });
             }}
           />,
         ]}
diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts
index a88bf5d..9fb4728 100644
--- a/src/server/api/routers/auditoryResources.ts
+++ b/src/server/api/routers/auditoryResources.ts
@@ -89,6 +89,20 @@ export const auditoryResourceRouter = createTRPCRouter({
       });
     }),
 
+  delete: protectedProcedure
+    .input(
+      z.object({
+        id: z.string(),
+      })
+    )
+    .mutation(async ({ input, ctx }) => {
+      return await ctx.prisma.auditoryResource.delete({
+        where: {
+          id: input.id,
+        },
+      });
+    }),
+
   update: protectedProcedure
     .input(AuditoryResourceSchema.partial().extend({ id: z.string() }))
     .mutation(async ({ input, ctx }) => {
-- 
2.47.2


From 23e9f913235c6e991539c4b73ca1bf146ce5470f Mon Sep 17 00:00:00 2001
From: Brandon Egger <brandonegger64@gmail.com>
Date: Tue, 5 Sep 2023 21:47:50 -0500
Subject: [PATCH 14/14] minor styling changes

---
 src/components/admin/resources/form.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx
index 9043c17..13c124e 100644
--- a/src/components/admin/resources/form.tsx
+++ b/src/components/admin/resources/form.tsx
@@ -386,7 +386,7 @@ function ResourceSummarySubForm({
           heading="Age Range"
           subheading="Specify the minimum and maximum age range supported by the resource"
         />
-        <div className="mt-2 flex flex-row space-x-4">
+        <div className="mt-4 flex flex-row space-x-4">
           <GenericInput
             type="number"
             placeholder="minimum age"
-- 
2.47.2