[resumes][feat] upload pdf file into file storage (#321)

* [resumes][feat] upload pdf file into file storage

* [resumes][fix] fix file upload failure

* [resumes][chore] update .env.local.example

* [resumes][fix] process file transfer over next.js

* [resumes][feat] file upload

* [resumes][chore] cleanup

* [resumes][feat] add GET method for file-storage API

* [portal[chore] Update env.example file

* [resumes][chore] cleanup

* [portal][chore] update yarn lock file
pull/333/head
Keane Chan 2 years ago committed by GitHub
parent fbf1517901
commit 384981716d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,3 +8,7 @@ NEXTAUTH_URL=http://localhost:3000
# Next Auth GitHub Provider
GITHUB_CLIENT_ID=a5164b1943b5413ff2f5
GITHUB_CLIENT_SECRET=
# Supabase
SUPABASE_URL=
SUPABASE_ANON_KEY=

@ -16,13 +16,16 @@
"@heroicons/react": "^2.0.11",
"@next-auth/prisma-adapter": "^1.0.4",
"@prisma/client": "^4.4.0",
"@supabase/supabase-js": "^1.35.7",
"@tih/ui": "*",
"@trpc/client": "^9.27.2",
"@trpc/next": "^9.27.2",
"@trpc/react": "^9.27.2",
"@trpc/server": "^9.27.2",
"axios": "^1.1.2",
"clsx": "^1.2.1",
"date-fns": "^2.29.3",
"formidable": "^2.0.1",
"next": "12.3.1",
"next-auth": "~4.10.3",
"react": "18.2.0",
@ -36,6 +39,7 @@
"devDependencies": {
"@tih/tailwind-config": "*",
"@tih/tsconfig": "*",
"@types/formidable": "^2.0.5",
"@types/node": "^18.0.0",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",

@ -1,9 +1,12 @@
import { useState } from 'react';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import type { PDFDocumentProxy } from 'react-pdf/node_modules/pdfjs-dist';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button, Spinner } from '@tih/ui';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;
type Props = Readonly<{
@ -13,16 +16,30 @@ type Props = Readonly<{
export default function ResumePdf({ url }: Props) {
const [numPages, setNumPages] = useState(0);
const [pageNumber] = useState(1);
const [file, setFile] = useState<File>();
const onPdfLoadSuccess = (pdf: PDFDocumentProxy) => {
setNumPages(pdf.numPages);
};
useEffect(() => {
async function fetchData() {
await axios
.get(`/api/file-storage?key=${RESUME_STORAGE_KEY}&url=${url}`, {
responseType: 'blob',
})
.then((res) => {
setFile(res.data);
});
}
fetchData();
}, [url]);
return (
<div>
<Document
className="h-[calc(100vh-17rem)] overflow-scroll"
file={url}
file={file}
loading={<Spinner display="block" label="" size="lg" />}
onLoadSuccess={onPdfLoadSuccess}>
<Page pageNumber={pageNumber} />

@ -0,0 +1 @@
export const RESUME_STORAGE_KEY = 'resumes';

@ -7,11 +7,13 @@ import { z } from 'zod';
*/
export const serverSchema = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'test', 'production']),
NEXTAUTH_SECRET: z.string(),
NEXTAUTH_URL: z.string().url(),
GITHUB_CLIENT_ID: z.string(),
GITHUB_CLIENT_SECRET: z.string(),
NEXTAUTH_SECRET: z.string(),
NEXTAUTH_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'test', 'production']),
SUPABASE_ANON_KEY: z.string(),
SUPABASE_URL: z.string().url(),
});
/**

@ -0,0 +1,65 @@
import formidable from 'formidable';
import * as fs from 'fs';
import type { NextApiRequest, NextApiResponse } from 'next';
import { supabase } from '~/utils/supabase';
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === 'POST') {
try {
const form = formidable({ keepExtensions: true });
form.parse(req, async (err, fields, files) => {
if (err) {
throw err;
}
const { key } = fields;
const { file } = files;
const parsedFile: formidable.File =
file instanceof Array ? file[0] : file;
const filePath = `${Date.now()}-${parsedFile.originalFilename}`;
const convertedFile = fs.readFileSync(parsedFile.filepath);
const { error } = await supabase.storage
.from(key as string)
.upload(filePath, convertedFile);
if (error) {
throw error;
}
return res.status(200).json({
url: filePath,
});
});
} catch (error: unknown) {
return Promise.reject(error);
}
}
if (req.method === 'GET') {
const { key, url } = req.query;
const { data, error } = await supabase.storage
.from(`public/${key as string}`)
.download(url as string);
if (error || data == null) {
throw error;
}
const arrayBuffer = await data.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
res.status(200).send(buffer);
}
}

@ -1,3 +1,4 @@
import axios from 'axios';
import clsx from 'clsx';
import Head from 'next/head';
import { useRouter } from 'next/router';
@ -13,6 +14,7 @@ import {
ROLES,
} from '~/components/resumes/browse/constants';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
import { trpc } from '~/utils/trpc';
const TITLE_PLACEHOLDER =
@ -49,8 +51,24 @@ export default function SubmitResumeForm() {
} = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = async (data) => {
if (resumeFile == null) {
return;
}
const formData = new FormData();
formData.append('key', RESUME_STORAGE_KEY);
formData.append('file', resumeFile);
const res = await axios.post('/api/file-storage', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
const { url } = res.data;
await resumeCreateMutation.mutate({
...data,
url,
});
router.push('/resumes');
};

@ -12,14 +12,13 @@ export const resumesResumeUserRouter = createProtectedRouter().mutation(
location: z.string(),
role: z.string(),
title: z.string(),
url: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user.id;
// TODO: Store file in file storage and retrieve URL
return await ctx.prisma.resumesResume.create({
data: {
...input,
url: '',
userId,
},
});

@ -0,0 +1,8 @@
import { createClient } from '@supabase/supabase-js';
import { env } from '~/env/server.mjs';
const { SUPABASE_URL, SUPABASE_ANON_KEY } = env;
// Create a single supabase client for interacting with the file storage
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save