From 7b51ee7e8865b70981a18c91c9938dd3aed38343 Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Fri, 14 Oct 2022 21:47:05 +0800 Subject: [PATCH] [resumes][feat] drag and drop for file upload (#378) * [resumes][feat] drag and drop for file upload * [resumes][chore] use .tsx instead for landing page * [resumes][feat] use expandable text for additionalnfo * [resumes][refactor] clean up submit form * [resumes][fix] fix file upload error * [feat][resumes] change button to Submit Resume * [resumes][fix] fix expandable text --- apps/portal/package.json | 1 + .../resumes/shared/ResumeExpandableText.tsx | 22 +- .../submit-form/SubmissionGuidelines.tsx | 30 +++ apps/portal/src/pages/resumes/[resumeId].tsx | 5 +- apps/portal/src/pages/resumes/browse.tsx | 4 +- .../pages/resumes/{index.jsx => index.tsx} | 0 apps/portal/src/pages/resumes/submit.tsx | 212 +++++++++--------- yarn.lock | 21 ++ 8 files changed, 170 insertions(+), 125 deletions(-) create mode 100644 apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx rename apps/portal/src/pages/resumes/{index.jsx => index.tsx} (100%) 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/components/resumes/shared/ResumeExpandableText.tsx b/apps/portal/src/components/resumes/shared/ResumeExpandableText.tsx index 82c7c9df..2e79eb51 100644 --- a/apps/portal/src/components/resumes/shared/ResumeExpandableText.tsx +++ b/apps/portal/src/components/resumes/shared/ResumeExpandableText.tsx @@ -10,7 +10,7 @@ export default function ResumeExpandableText({ children, }: ResumeExpandableTextProps) { const ref = useRef(null); - const [descriptionExpanded, setDescriptionExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); const [descriptionOverflow, setDescriptionOverflow] = useState(false); useLayoutEffect(() => { @@ -20,29 +20,27 @@ export default function ResumeExpandableText({ }, [ref]); const onSeeActionClicked = () => { - setDescriptionExpanded(!descriptionExpanded); + setIsExpanded((prevExpanded) => !prevExpanded); }; return ( - <> +
{children} {descriptionOverflow && ( -
-
- {descriptionExpanded ? 'See Less' : 'See More'} -
-
+

+ {isExpanded ? 'See Less' : 'See More'} +

)} - +
); } diff --git a/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx b/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx new file mode 100644 index 00000000..9220adb7 --- /dev/null +++ b/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx @@ -0,0 +1,30 @@ +export default function SubmissionGuidelines() { + return ( +
+

Submission Guidelines

+

+ Before you submit, please review and acknolwedge our + submission guidelines + stated below. +

+

+ + Ensure that you do not divulge any of your + personal particulars. +

+

+ + Ensure that you do not divulge any + + {' '} + company's proprietary and confidential information + + . +

+

+ + Proof-read your resumes to look for grammatical/spelling errors. +

+
+ ); +} diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index 40f7b630..bdb0a94d 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -16,6 +16,7 @@ import { Spinner } from '@tih/ui'; import ResumeCommentsSection from '~/components/resumes/comments/ResumeCommentsSection'; import ResumePdf from '~/components/resumes/ResumePdf'; +import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText'; import { trpc } from '~/utils/trpc'; @@ -158,7 +159,9 @@ export default function ResumeReviewPage() { aria-hidden="true" className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400" /> - {detailsQuery.data.additionalInfo} + + {detailsQuery.data.additionalInfo} + )}
diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx index 0331c720..7b08ba02 100644 --- a/apps/portal/src/pages/resumes/browse.tsx +++ b/apps/portal/src/pages/resumes/browse.tsx @@ -240,10 +240,10 @@ export default function ResumeHomePage() {
diff --git a/apps/portal/src/pages/resumes/index.jsx b/apps/portal/src/pages/resumes/index.tsx similarity index 100% rename from apps/portal/src/pages/resumes/index.jsx rename to apps/portal/src/pages/resumes/index.tsx diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index ad9ffa49..49763d8d 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -3,7 +3,9 @@ import clsx from 'clsx'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, 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'; @@ -16,11 +18,13 @@ import { TextInput, } from '@tih/ui'; +import type { FilterOption } from '~/components/resumes/browse/resumeConstants'; import { EXPERIENCE, LOCATION, ROLE, } from '~/components/resumes/browse/resumeConstants'; +import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines'; import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys'; import { trpc } from '~/utils/trpc'; @@ -43,11 +47,20 @@ type IFormInput = { title: string; }; -export default function SubmitResumeForm() { - const { data: session, status } = useSession(); - const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create'); - const router = useRouter(); +type SelectorType = 'experience' | 'location' | 'role'; +type SelectorOptions = { + key: SelectorType; + label: string; + options: Array; +}; +const selectors: Array = [ + { key: 'role', label: 'Role', options: ROLE }, + { key: 'experience', label: 'Experience Level', options: EXPERIENCE }, + { key: 'location', label: 'Location', options: LOCATION }, +]; + +export default function SubmitResumeForm() { const [resumeFile, setResumeFile] = useState(); const [isLoading, setIsLoading] = useState(false); const [invalidFileUploadError, setInvalidFileUploadError] = useState< @@ -55,13 +68,9 @@ export default function SubmitResumeForm() { >(null); const [isDialogShown, setIsDialogShown] = useState(false); - useEffect(() => { - if (status !== 'loading') { - if (session?.user?.id == null) { - router.push('/api/auth/signin'); - } - } - }, [router, session, status]); + const { data: session, status } = useSession(); + const router = useRouter(); + const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create'); const { register, @@ -75,9 +84,38 @@ export default function SubmitResumeForm() { }, }); + const onFileDrop = useCallback( + (acceptedFiles: Array, fileRejections: Array) => { + if (fileRejections.length === 0) { + setInvalidFileUploadError(''); + setResumeFile(acceptedFiles[0]); + setValue('file', acceptedFiles[0]); + } else { + setInvalidFileUploadError(FILE_UPLOAD_ERROR); + } + }, + [setValue], + ); + + 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) { + router.push('/api/auth/signin'); + } + } + }, [router, session, status]); + const onSubmit: SubmitHandler = async (data) => { if (resumeFile == null) { - console.error('Resume file is empty'); return; } setIsLoading(true); @@ -103,62 +141,53 @@ export default function SubmitResumeForm() { url, }, { - onError: (error) => { + onError(error) { console.error(error); }, - onSettled: () => { + onSettled() { setIsLoading(false); }, - onSuccess: () => { - router.push('/resumes'); + onSuccess() { + 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); + setInvalidFileUploadError(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(() => { @@ -179,6 +208,7 @@ export default function SubmitResumeForm() {
+ {/* Reset Dialog component */} } secondaryButton={ @@ -204,6 +234,7 @@ export default function SubmitResumeForm() {

Upload a resume

+ {/* Title Section */}
setValue('title', val)} />
-
- setValue('experience', val)} - /> -
-
- setValue(item.key, val)} + /> +
+ ))} + {/* Upload Resume Section */}

Upload resume (PDF format)

+ {/* Upload Resume Box */}
@@ -265,7 +281,7 @@ export default function SubmitResumeForm() { ) : (

{resumeFile.name}

@@ -276,20 +292,25 @@ export default function SubmitResumeForm() {
@@ -302,6 +323,7 @@ export default function SubmitResumeForm() {

{fileUploadError}

)}
+ {/* Additional Info Section */}