update render logic for resource photo
This commit is contained in:
parent
34a2bd7361
commit
e3d73ecc5c
54
src/components/ResourcePhoto.tsx
Normal file
54
src/components/ResourcePhoto.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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">
|
||||||
|
@ -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 />
|
||||||
|
@ -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({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user