update render logic for resource photo

This commit is contained in:
Brandon Egger 2023-08-23 18:34:24 -05:00
parent 34a2bd7361
commit e3d73ecc5c
5 changed files with 103 additions and 95 deletions

View File

@ -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<string | undefined>(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
<img
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
src={blobSrc ?? ""}
alt={`${input.name} logo`}
{...commonProps}
/>
);
}
return (
<Image
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
src={blobSrc ?? `/resource_logos/${input.src ?? "logo_not_found.png"}`}
alt={`${input.name} logo`}
{...commonProps}
/>
);
};

View File

@ -6,54 +6,14 @@ import {
type Manufacturer, type Manufacturer,
} from "@prisma/client"; } from "@prisma/client";
import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { translateEnumPlatform, translateEnumSkill } from "~/utils/enumWordLut"; 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 { ChevronDownIcon } from "@heroicons/react/24/outline";
import { type ParsedUrlQuery, type ParsedUrlQueryInput } from "querystring"; import { type ParsedUrlQuery, type ParsedUrlQueryInput } from "querystring";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { PriceIcon } from "~/prices/Icons"; import { PriceIcon } from "~/prices/Icons";
import { ResourcePhoto } from "./ResourcePhoto";
export const ResourcePhoto = ({ resource }: { resource: AuditoryResource }) => {
const [blobSrc, setBlobSrc] = useState<string | undefined>(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
<img
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
src={blobSrc ?? `/resource_logos/${resource.icon}`}
alt={`${resource.name} logo`}
{...commonProps}
/>
);
}
return (
<Image
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
src={blobSrc ?? `/resource_logos/${resource.icon}`}
alt={`${resource.name} logo`}
{...commonProps}
/>
);
};
export const ResourceInfo = ({ export const ResourceInfo = ({
resource, resource,
@ -86,7 +46,11 @@ export const ResourceInfo = ({
{showMoreInfo ? ( {showMoreInfo ? (
<Link href={`resources/${resource.id}`}> <Link href={`resources/${resource.id}`}>
<div className="flex w-20 flex-col justify-center space-y-2 sm:w-28"> <div className="flex w-20 flex-col justify-center space-y-2 sm:w-28">
<ResourcePhoto resource={resource} /> <ResourcePhoto
name={resource.name}
photo={resource.photo}
src={resource.icon}
/>
<span className="block rounded-lg border border-neutral-900 bg-neutral-900 py-[1px] text-center text-white hover:bg-neutral-500 print:hidden"> <span className="block rounded-lg border border-neutral-900 bg-neutral-900 py-[1px] text-center text-white hover:bg-neutral-500 print:hidden">
more info more info
</span> </span>
@ -94,12 +58,10 @@ export const ResourceInfo = ({
</Link> </Link>
) : ( ) : (
<div className="flex w-20 flex-col justify-center space-y-2 sm:w-28"> <div className="flex w-20 flex-col justify-center space-y-2 sm:w-28">
<Image <ResourcePhoto
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg" name={resource.name}
src={`/resource_logos/${resource.icon}`} photo={resource.photo}
alt={`${resource.name} logo`} src={resource.icon}
width={512}
height={512}
/> />
</div> </div>
)} )}

View File

@ -5,7 +5,6 @@ import {
type PlatformLink, type PlatformLink,
Platform, Platform,
} from "@prisma/client"; } from "@prisma/client";
import Image from "next/image";
import { import {
PencilSquareIcon, PencilSquareIcon,
XCircleIcon as XCircleSolid, XCircleIcon as XCircleSolid,
@ -41,6 +40,7 @@ import {
import Modal from "react-modal"; import Modal from "react-modal";
import { type RouterInputs } from "~/utils/api"; import { type RouterInputs } from "~/utils/api";
import { PlatformLinkButton } from "~/pages/resources/[id]"; import { PlatformLinkButton } from "~/pages/resources/[id]";
import { ResourcePhoto } from "~/components/ResourcePhoto";
// Required for accessibility // Required for accessibility
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access // 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/ * File needs to be path relative to resource_logos/
*/ */
const SelectImageInput = ({ const SelectImageInput = ({
file, resource,
setIconFile, setIconFile,
}: { }: {
file?: string; resource?: ResourceUpdateInput;
setIconFile: (file: File) => void; setIconFile: (file: File) => void;
}) => { }) => {
const [previewImg, setPreviewImg] = useState<string | undefined>( const [previewImg, setPreviewImg] = useState<string | undefined>(undefined);
`/resource_logos/${file ?? ""}`
);
const onChange = (event: ChangeEvent<HTMLInputElement>) => { const onChange = (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.files || !event.target.files[0]) { if (!event.target.files || !event.target.files[0]) {
@ -85,13 +83,19 @@ const SelectImageInput = ({
htmlFor="resource-image-file" htmlFor="resource-image-file"
className="bg-whit group relative cursor-pointer overflow-hidden rounded-xl border border-neutral-400 drop-shadow-lg" className="bg-whit group relative cursor-pointer overflow-hidden rounded-xl border border-neutral-400 drop-shadow-lg"
> >
<Image {!previewImg ? (
className="w-full" <ResourcePhoto
src={previewImg ?? ""} name={resource?.name ?? "n/a"}
alt={`resource logo`} photo={resource?.photo ?? null}
width={512} src={resource?.icon}
height={512} />
/> ) : (
<ResourcePhoto
photo={null}
src={previewImg}
name={resource?.name ?? "n/a"}
/>
)}
<div className="absolute bottom-0 left-0 right-0 top-0 hidden place-items-center group-hover:grid group-hover:bg-white/70"> <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" /> <PencilSquareIcon className="w-16 text-black/50" />
</div> </div>
@ -346,7 +350,7 @@ function ResourceSummarySubForm({
<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">
<div className="flex w-20 flex-col justify-center space-y-2 sm:w-28"> <div className="flex w-20 flex-col justify-center space-y-2 sm:w-28">
<SelectImageInput file={resource?.icon} setIconFile={setIconFile} /> <SelectImageInput resource={resource} setIconFile={setIconFile} />
</div> </div>
<div className="flex flex-col justify-center overflow-hidden rounded-xl border border-neutral-400 bg-white drop-shadow-lg sm:w-[300px] md:w-[400px]"> <div className="flex flex-col justify-center overflow-hidden rounded-xl border border-neutral-400 bg-white drop-shadow-lg sm:w-[300px] md:w-[400px]">
<h2 className="border-b border-neutral-300 px-2 text-center font-semibold"> <h2 className="border-b border-neutral-300 px-2 text-center font-semibold">

View File

@ -1,16 +1,8 @@
import { XCircleIcon } from "@heroicons/react/20/solid"; 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 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 { AdminActionButton, 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 Image from "next/image";
import { import {
ResourceForm, ResourceForm,
@ -19,33 +11,14 @@ import {
import { useState } from "react"; import { useState } from "react";
import { useForm, type SubmitHandler } from "react-hook-form"; import { useForm, type SubmitHandler } from "react-hook-form";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { useRouter } from "next/router";
export const getServerSideProps: GetServerSideProps<{ const EditResourcePage = () => {
resource: AuditoryResource; const router = useRouter();
}> = async (context) => { const id = router.query["id"]?.toString() ?? "";
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
prisma,
session: null,
},
});
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<typeof getServerSideProps>
) => {
const { resource } = props;
const [updateIconFile, setIconFile] = useState<File | undefined>(undefined); const [updateIconFile, setIconFile] = useState<File | undefined>(undefined);
const [serverError, setServerError] = useState<string | undefined>(undefined); const [serverError, setServerError] = useState<string | undefined>(undefined);
const formMethods = useForm<ResourceUpdateInput>({ const formMethods = useForm<ResourceUpdateInput>({
@ -64,6 +37,10 @@ const EditResourcePage = (
const data = new FormData(); const data = new FormData();
data.append("photo", updateIconFile); data.append("photo", updateIconFile);
if (!resource?.id) {
throw Error("Resource data missing for photo to upload");
}
const uploadResponse = await fetch( const uploadResponse = await fetch(
`/api/resources/photo/${resource.id}`, `/api/resources/photo/${resource.id}`,
{ {
@ -83,6 +60,11 @@ const EditResourcePage = (
mutate(data); mutate(data);
}; };
if (!resource) {
// TODO: Error page if resource does not exist
return <></>;
}
return ( return (
<> <>
<Header /> <Header />

View File

@ -62,6 +62,12 @@ export const auditoryResourceRouter = createTRPCRouter({
skills: z.array(z.nativeEnum(Skill)).optional(), skills: z.array(z.nativeEnum(Skill)).optional(),
skill_levels: z.array(z.nativeEnum(SkillLevel)).optional(), skill_levels: z.array(z.nativeEnum(SkillLevel)).optional(),
payment_options: z.array(z.nativeEnum(PaymentType)).optional(), payment_options: z.array(z.nativeEnum(PaymentType)).optional(),
photo: z
.object({
name: z.string(),
data: z.instanceof(Buffer),
})
.optional(),
platform_links: z platform_links: z
.array( .array(
z.object({ z.object({