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}
/>