Merge pull request #1 from brandonegg/enable-custom-icon-uploads
Enable custom icon uploads
This commit is contained in:
commit
3a5bda8b2e
@ -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[]
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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!"
|
||||||
|
@ -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,
|
||||||
|
@ -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() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user