From 350ffd420836384021cce4cf7c124495668254f4 Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Fri, 14 Oct 2022 18:29:12 +0800 Subject: [PATCH] [resumes][feat] drag and drop for file upload --- apps/portal/package.json | 1 + apps/portal/src/pages/resumes/submit.tsx | 82 +++++++++++++++--------- yarn.lock | 21 ++++++ 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/apps/portal/package.json b/apps/portal/package.json index 88ad7dfd..f1bdb9e7 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -30,6 +30,7 @@ "next-auth": "~4.10.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-dropzone": "^14.2.3", "react-hook-form": "^7.36.1", "react-pdf": "^5.7.2", "react-query": "^3.39.2", diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index ad9ffa49..0c1e3e01 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -4,6 +4,8 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; import { useEffect, useMemo, useState } from 'react'; +import type { FileRejection } from 'react-dropzone'; +import { useDropzone } from 'react-dropzone'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { PaperClipIcon } from '@heroicons/react/24/outline'; @@ -45,8 +47,8 @@ type IFormInput = { export default function SubmitResumeForm() { const { data: session, status } = useSession(); - const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create'); const router = useRouter(); + const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create'); const [resumeFile, setResumeFile] = useState(); const [isLoading, setIsLoading] = useState(false); @@ -55,6 +57,27 @@ export default function SubmitResumeForm() { >(null); const [isDialogShown, setIsDialogShown] = useState(false); + const onFileDrop = ( + acceptedFiles: Array, + fileRejections: Array, + ) => { + if (fileRejections.length === 0) { + setInvalidFileUploadError(''); + setResumeFile(acceptedFiles[0]); + } else { + setInvalidFileUploadError(FILE_UPLOAD_ERROR); + } + }; + + const { getRootProps, getInputProps } = useDropzone({ + accept: { + 'application/pdf': ['.pdf'], + }, + maxFiles: 1, + maxSize: FILE_SIZE_LIMIT_BYTES, + onDrop: onFileDrop, + }); + useEffect(() => { if (status !== 'loading') { if (session?.user?.id == null) { @@ -77,7 +100,6 @@ export default function SubmitResumeForm() { const onSubmit: SubmitHandler = async (data) => { if (resumeFile == null) { - console.error('Resume file is empty'); return; } setIsLoading(true); @@ -110,55 +132,45 @@ export default function SubmitResumeForm() { setIsLoading(false); }, onSuccess: () => { - router.push('/resumes'); + router.push('/resumes/browse'); }, }, ); }; - const onUploadFile = (event: React.ChangeEvent) => { - const file = event.target.files?.item(0); - if (file == null) { - return; - } - if (file.type !== 'application/pdf' || file.size > FILE_SIZE_LIMIT_BYTES) { - setInvalidFileUploadError(FILE_UPLOAD_ERROR); - return; - } - setInvalidFileUploadError(''); - setResumeFile(file); - }; - - const onClickReset = () => { + const onClickClear = () => { if (isDirty || resumeFile != null) { setIsDialogShown(true); } }; - const onClickProceedDialog = () => { + const onClickResetDialog = () => { setIsDialogShown(false); reset(); setResumeFile(null); }; - const onClickDownload = async () => { + const onClickDownload = async ( + event: React.MouseEvent, + ) => { if (resumeFile == null) { return; } + // Prevent click event from propagating up to dropzone + event.stopPropagation(); const url = window.URL.createObjectURL(resumeFile); const link = document.createElement('a'); link.href = url; link.setAttribute('download', resumeFile.name); - - // Append to html link element page document.body.appendChild(link); // Start download link.click(); - // Clean up and remove the link + // Clean up and remove the link and object URL link.remove(); + URL.revokeObjectURL(url); }; const fileUploadError = useMemo(() => { @@ -186,7 +198,7 @@ export default function SubmitResumeForm() { display="block" label="OK" variant="primary" - onClick={onClickProceedDialog} + onClick={onClickResetDialog} /> } secondaryButton={ @@ -254,9 +266,10 @@ export default function SubmitResumeForm() {

@@ -266,7 +279,7 @@ export default function SubmitResumeForm() {

+ onClick={(event) => onClickDownload(event)}> {resumeFile.name}

@@ -276,20 +289,25 @@ export default function SubmitResumeForm() {
@@ -354,7 +372,7 @@ export default function SubmitResumeForm() { label="Clear" size="md" variant="tertiary" - onClick={onClickReset} + onClick={onClickClear} />