[resumes][feat] drag and drop for file upload

pull/378/head
Keane Chan 3 years ago
parent ff9cffa715
commit 350ffd4208
No known key found for this signature in database
GPG Key ID: 32718398E1E9F87C

@ -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",

@ -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<File | null>();
const [isLoading, setIsLoading] = useState(false);
@ -55,6 +57,27 @@ export default function SubmitResumeForm() {
>(null);
const [isDialogShown, setIsDialogShown] = useState(false);
const onFileDrop = (
acceptedFiles: Array<File>,
fileRejections: Array<FileRejection>,
) => {
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<IFormInput> = 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<HTMLInputElement>) => {
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<HTMLParagraphElement, 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() {
</p>
<div className="mb-4">
<div
{...getRootProps()}
className={clsx(
fileUploadError ? 'border-danger-600' : 'border-gray-300',
'mt-2 flex justify-center rounded-md border-2 border-dashed px-6 pt-5 pb-6',
'mt-2 flex justify-center rounded-md border-2 border-dashed px-6 pt-5 pb-6',
)}>
<div className="space-y-1 text-center">
<div className="flex gap-2">
@ -266,7 +279,7 @@ export default function SubmitResumeForm() {
<div className="flex gap-2">
<p
className="cursor-pointer underline underline-offset-1 hover:text-indigo-600"
onClick={onClickDownload}>
onClick={(event) => onClickDownload(event)}>
{resumeFile.name}
</p>
</div>
@ -276,20 +289,25 @@ export default function SubmitResumeForm() {
<label
className="rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2"
htmlFor="file-upload">
<p className="cursor-pointer font-medium text-indigo-600 hover:text-indigo-500">
{resumeFile == null
? 'Upload a file'
: 'Replace file'}
</p>
<div className="flex gap-1 ">
<p className="cursor-pointer font-medium text-indigo-600 hover:text-indigo-500">
{resumeFile == null
? 'Upload a file'
: 'Replace file'}
</p>
<span className="text-gray-500">
or drag and drop
</span>
</div>
<input
{...register('file', { required: true })}
{...getInputProps()}
accept="application/pdf"
className="sr-only"
disabled={isLoading}
id="file-upload"
name="file-upload"
type="file"
onChange={onUploadFile}
/>
</label>
</div>
@ -354,7 +372,7 @@ export default function SubmitResumeForm() {
label="Clear"
size="md"
variant="tertiary"
onClick={onClickReset}
onClick={onClickClear}
/>
<Button
addonPosition="start"

@ -4605,6 +4605,11 @@ atob@^2.1.2:
resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
autoprefixer@^10.3.7, autoprefixer@^10.4.12, autoprefixer@^10.4.7:
version "10.4.12"
resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.12.tgz"
@ -7733,6 +7738,13 @@ file-loader@^6.0.0, file-loader@^6.2.0:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
file-selector@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
dependencies:
tslib "^2.4.0"
file-system-cache@^1.0.5:
version "1.1.0"
resolved "https://registry.npmjs.org/file-system-cache/-/file-system-cache-1.1.0.tgz"
@ -12162,6 +12174,15 @@ react-dom@18.2.0, react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-dropzone@^14.2.3:
version "14.2.3"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b"
integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==
dependencies:
attr-accept "^2.2.2"
file-selector "^0.6.0"
prop-types "^15.8.1"
react-element-to-jsx-string@^14.3.4:
version "14.3.4"
resolved "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz"

Loading…
Cancel
Save