Merge pull request #1 from brandonegg/enable-custom-icon-uploads

Enable custom icon uploads
This commit is contained in:
Brandon Egger 2023-08-22 18:27:28 -05:00 committed by GitHub
commit 3a5bda8b2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 25 deletions

View File

@ -63,11 +63,17 @@ type Manufacturer {
notice String? notice String?
} }
type Photo {
name String
data Bytes
}
model AuditoryResource { model AuditoryResource {
id String @id @default(auto()) @map("_id") @db.ObjectId id String @id @default(auto()) @map("_id") @db.ObjectId
icon String icon String
name String name String
description String description String
photo Photo?
manufacturer Manufacturer? manufacturer Manufacturer?
ages RangeInput ages RangeInput
skills Skill[] skills Skill[]

View File

@ -9,12 +9,39 @@ import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
import Image from "next/image"; 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 { type ChangeEvent } from "react"; import { useEffect, type ChangeEvent, useState } 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";
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,
};
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,
showMoreInfo, showMoreInfo,
@ -46,13 +73,7 @@ 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">
<Image <ResourcePhoto resource={resource} />
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
src={`/resource_logos/${resource.icon}`}
alt={`${resource.name} logo`}
width={512}
height={512}
/>
<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>

View File

@ -1,7 +1,9 @@
import { type NextApiHandler } from "next"; import { type NextApiHandler } from "next";
import formidable from "formidable"; import formidable from "formidable";
import * as path from "path"; import * as fs from "fs";
import { prisma } from "~/server/db"; import { prisma } from "~/server/db";
import { getServerAuthSession } from "~/server/auth";
import { Role } from "@prisma/client";
/** /**
* Returns filename for a given filepath. * Returns filename for a given filepath.
@ -17,6 +19,12 @@ const handler: NextApiHandler = async (req, res) => {
return; return;
} }
const authSession = await getServerAuthSession({ req, res });
if (!authSession?.user || authSession.user.role !== Role.ADMIN) {
res.writeHead(401, "Not authorized");
return;
}
const { id } = req.query; const { id } = req.query;
if (Array.isArray(id) || !id) { if (Array.isArray(id) || !id) {
@ -29,17 +37,21 @@ const handler: NextApiHandler = async (req, res) => {
keepExtensions: true, keepExtensions: true,
}); });
const localUploadPath: Promise<string> = new Promise((resolve, reject) => { const uploadPhoto: Promise<formidable.File> = new Promise(
form.parse(req, (_err, _fields, files) => { (resolve, reject) => {
const photo = Array.isArray(files.photo) ? files.photo[0] : files.photo; form.parse(req, (_err, _fields, files) => {
if (!photo) { const photo = Array.isArray(files.photo) ? files.photo[0] : files.photo;
reject("Invalid file type sent (or none provided)"); if (!photo) {
return; reject("Invalid file type sent (or none provided)");
} return;
}
resolve(path.join("uploads", getFileName(photo.filepath))); resolve(photo);
}); });
}); }
);
const photoBuffer = fs.readFileSync((await uploadPhoto).filepath);
try { try {
await prisma.auditoryResource.update({ await prisma.auditoryResource.update({
@ -47,7 +59,12 @@ const handler: NextApiHandler = async (req, res) => {
id, id,
}, },
data: { data: {
icon: await localUploadPath, photo: {
name: getFileName(
(await uploadPhoto).originalFilename ?? "resource ICON"
),
data: photoBuffer,
},
}, },
}); });
} catch (error: unknown) { } catch (error: unknown) {

View File

@ -60,9 +60,6 @@ const EditResourcePage = (
}); });
const onSubmit: SubmitHandler<ResourceUpdateInput> = async (data) => { const onSubmit: SubmitHandler<ResourceUpdateInput> = async (data) => {
// TODO: Fix file upload, currently it is not updating correctly on the server side
// May also need to look into re-rendering static pages when icon changes
// Also need to add authentication of route!
if (updateIconFile) { if (updateIconFile) {
const data = new FormData(); const data = new FormData();
data.append("photo", updateIconFile); data.append("photo", updateIconFile);
@ -75,8 +72,6 @@ const EditResourcePage = (
} }
); );
console.log("uploading icon");
if (uploadResponse.status !== 200) { if (uploadResponse.status !== 200) {
setServerError( setServerError(
"Failed uploading resource icon file. Changes did not save!" "Failed uploading resource icon file. Changes did not save!"

View File

@ -137,6 +137,7 @@ export const auditoryResourceRouter = createTRPCRouter({
}), }),
]); ]);
// TODO: The issue here is the photo binary data can't be sent over tRPC which will cause the request to be unparsable by the client
return { return {
count, count,
resources, resources,

View File

@ -17,6 +17,15 @@ const getBaseUrl = () => {
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
}; };
superjson.registerCustom<Buffer, number[]>(
{
isApplicable: (v): v is Buffer => v instanceof Buffer,
serialize: (v) => [...v],
deserialize: (v) => Buffer.from(v),
},
"buffer"
);
/** A set of type-safe react-query hooks for your tRPC API. */ /** A set of type-safe react-query hooks for your tRPC API. */
export const api = createTRPCNext<AppRouter>({ export const api = createTRPCNext<AppRouter>({
config() { config() {