add react forms and basic implementation of update endpoint
This commit is contained in:
parent
6cbcc7eb21
commit
cabddd777e
14
package-lock.json
generated
14
package-lock.json
generated
@ -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": {
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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 };
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 />
|
||||||
|
@ -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({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user