diff --git a/src/components/ResourcePhoto.tsx b/src/components/ResourcePhoto.tsx new file mode 100644 index 0000000..379f8ab --- /dev/null +++ b/src/components/ResourcePhoto.tsx @@ -0,0 +1,54 @@ +import { type AuditoryResource } from "@prisma/client"; +import { useEffect, useState } from "react"; +import Image from "next/image"; + +type ResourcePhotoProps = ( + | { + photo: AuditoryResource["photo"]; + src: string | undefined; + } + | { + src: string; + photo: null; + } +) & { name: string }; + +export const ResourcePhoto = (input: ResourcePhotoProps) => { + const [blobSrc, setBlobSrc] = useState(undefined); + + useEffect(() => { + if (!input.photo?.data) { + return; + } + + const blob = new Blob([input.photo.data], { type: "image/png" }); + setBlobSrc(URL.createObjectURL(blob)); + }, [input.photo]); + + const commonProps = { + width: 512, + height: 512, + }; + + if (input.photo?.data) { + return ( + // Required because blob image processed by client, not server + // eslint-disable-next-line @next/next/no-img-element + {`${input.name} + ); + } + + return ( + {`${input.name} + ); +}; diff --git a/src/components/ResourceTable.tsx b/src/components/ResourceTable.tsx index 7a47cd3..444a50c 100644 --- a/src/components/ResourceTable.tsx +++ b/src/components/ResourceTable.tsx @@ -6,54 +6,14 @@ import { type Manufacturer, } from "@prisma/client"; import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; -import Image from "next/image"; import Link from "next/link"; import { translateEnumPlatform, translateEnumSkill } from "~/utils/enumWordLut"; -import { useEffect, type ChangeEvent, useState } from "react"; +import { type ChangeEvent } from "react"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { type ParsedUrlQuery, type ParsedUrlQueryInput } from "querystring"; import { useRouter } from "next/router"; import { PriceIcon } from "~/prices/Icons"; - -export const ResourcePhoto = ({ resource }: { resource: AuditoryResource }) => { - const [blobSrc, setBlobSrc] = useState(undefined); - - useEffect(() => { - if (!resource.photo?.data) { - return; - } - - const blob = new Blob([resource.photo.data], { type: "image/png" }); - setBlobSrc(URL.createObjectURL(blob)); - }, [resource.photo]); - - const commonProps = { - width: 512, - height: 512, - }; - - if (blobSrc) { - return ( - // Required because blob image processed by client, not server - // eslint-disable-next-line @next/next/no-img-element - {`${resource.name} - ); - } - - return ( - {`${resource.name} - ); -}; +import { ResourcePhoto } from "./ResourcePhoto"; export const ResourceInfo = ({ resource, @@ -86,7 +46,11 @@ export const ResourceInfo = ({ {showMoreInfo ? (
- + more info @@ -94,12 +58,10 @@ export const ResourceInfo = ({ ) : (
- {`${resource.name}
)} diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx index 8dc74d7..8d60d1a 100644 --- a/src/components/admin/resources/form.tsx +++ b/src/components/admin/resources/form.tsx @@ -5,7 +5,6 @@ import { type PlatformLink, Platform, } from "@prisma/client"; -import Image from "next/image"; import { PencilSquareIcon, XCircleIcon as XCircleSolid, @@ -41,6 +40,7 @@ import { import Modal from "react-modal"; import { type RouterInputs } from "~/utils/api"; import { PlatformLinkButton } from "~/pages/resources/[id]"; +import { ResourcePhoto } from "~/components/ResourcePhoto"; // Required for accessibility // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access @@ -54,15 +54,13 @@ export type ResourceUpdateInput = RouterInputs["auditoryResource"]["update"]; * File needs to be path relative to resource_logos/ */ const SelectImageInput = ({ - file, + resource, setIconFile, }: { - file?: string; + resource?: ResourceUpdateInput; setIconFile: (file: File) => void; }) => { - const [previewImg, setPreviewImg] = useState( - `/resource_logos/${file ?? ""}` - ); + const [previewImg, setPreviewImg] = useState(undefined); const onChange = (event: ChangeEvent) => { if (!event.target.files || !event.target.files[0]) { @@ -85,13 +83,19 @@ const SelectImageInput = ({ htmlFor="resource-image-file" className="bg-whit group relative cursor-pointer overflow-hidden rounded-xl border border-neutral-400 drop-shadow-lg" > - {`resource + {!previewImg ? ( + + ) : ( + + )}
@@ -346,7 +350,7 @@ function ResourceSummarySubForm({
- +

diff --git a/src/pages/resources/[id]/edit.tsx b/src/pages/resources/[id]/edit.tsx index a8d9d15..28aa5c3 100644 --- a/src/pages/resources/[id]/edit.tsx +++ b/src/pages/resources/[id]/edit.tsx @@ -1,16 +1,8 @@ import { XCircleIcon } from "@heroicons/react/20/solid"; -import { type AuditoryResource } from "@prisma/client"; -import { createServerSideHelpers } from "@trpc/react-query/server"; -import { - type GetServerSideProps, - type InferGetServerSidePropsType, -} from "next"; import Footer from "~/components/Footer"; import Header from "~/components/Header"; import { AdminBarLayout } from "~/components/admin/ControlBar"; 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, @@ -19,33 +11,14 @@ import { import { useState } from "react"; import { useForm, type SubmitHandler } from "react-hook-form"; import { api } from "~/utils/api"; +import { useRouter } from "next/router"; -export const getServerSideProps: GetServerSideProps<{ - resource: AuditoryResource; -}> = async (context) => { - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - prisma, - session: null, - }, - }); +const EditResourcePage = () => { + const router = useRouter(); + const id = router.query["id"]?.toString() ?? ""; - const id = context.params?.id as string; + const { data: resource } = api.auditoryResource.byId.useQuery({ id }); - const resource = await helpers.auditoryResource.byId.fetch({ id }); - - return { - props: { - resource, - }, - }; -}; - -const EditResourcePage = ( - props: InferGetServerSidePropsType -) => { - const { resource } = props; const [updateIconFile, setIconFile] = useState(undefined); const [serverError, setServerError] = useState(undefined); const formMethods = useForm({ @@ -64,6 +37,10 @@ const EditResourcePage = ( const data = new FormData(); data.append("photo", updateIconFile); + if (!resource?.id) { + throw Error("Resource data missing for photo to upload"); + } + const uploadResponse = await fetch( `/api/resources/photo/${resource.id}`, { @@ -83,6 +60,11 @@ const EditResourcePage = ( mutate(data); }; + if (!resource) { + // TODO: Error page if resource does not exist + return <>; + } + return ( <>
diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts index 1252dae..8d7d2b0 100644 --- a/src/server/api/routers/auditoryResources.ts +++ b/src/server/api/routers/auditoryResources.ts @@ -62,6 +62,12 @@ export const auditoryResourceRouter = createTRPCRouter({ 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), + }) + .optional(), platform_links: z .array( z.object({