377 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import axios from 'axios';
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 type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { PaperClipIcon } from '@heroicons/react/24/outline';
import {
Button,
CheckboxInput,
Dialog,
Select,
TextArea,
TextInput,
} from '@tih/ui';
import {
EXPERIENCE,
LOCATION,
ROLE,
} from '~/components/resumes/browse/resumeConstants';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
import { trpc } from '~/utils/trpc';
const FILE_SIZE_LIMIT_MB = 3;
const FILE_SIZE_LIMIT_BYTES = FILE_SIZE_LIMIT_MB * 1000000;
const TITLE_PLACEHOLDER =
'e.g. Applying for Company XYZ, please help me to review!';
const ADDITIONAL_INFO_PLACEHOLDER = `e.g. Im applying for company XYZ. I have been resume-rejected by N companies that I have applied for. Please help me to review so company XYZ gives me an interview!`;
const FILE_UPLOAD_ERROR = `Please upload a PDF file that is less than ${FILE_SIZE_LIMIT_MB}MB.`;
type IFormInput = {
additionalInfo?: string;
experience: string;
file: File;
isChecked: boolean;
location: string;
role: string;
title: string;
};
export default function SubmitResumeForm() {
const { data: session, status } = useSession();
const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create');
const router = useRouter();
const [resumeFile, setResumeFile] = useState<File | null>();
const [isLoading, setIsLoading] = useState(false);
const [invalidFileUploadError, setInvalidFileUploadError] = useState<
string | null
>(null);
const [isDialogShown, setIsDialogShown] = useState(false);
useEffect(() => {
if (status !== 'loading') {
if (session?.user?.id == null) {
router.push('/api/auth/signin');
}
}
}, [router, session, status]);
const {
register,
handleSubmit,
setValue,
reset,
formState: { errors, isDirty },
} = useForm<IFormInput>({
defaultValues: {
isChecked: false,
},
});
const onSubmit: SubmitHandler<IFormInput> = async (data) => {
if (resumeFile == null) {
console.error('Resume file is empty');
return;
}
setIsLoading(true);
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;
resumeCreateMutation.mutate(
{
additionalInfo: data.additionalInfo,
experience: data.experience,
location: data.location,
role: data.role,
title: data.title,
url,
},
{
onError: (error) => {
console.error(error);
},
onSettled: () => {
setIsLoading(false);
},
onSuccess: () => {
router.push('/resumes');
},
},
);
};
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 = () => {
if (isDirty || resumeFile != null) {
setIsDialogShown(true);
}
};
const onClickProceedDialog = () => {
setIsDialogShown(false);
reset();
setResumeFile(null);
};
const onClickDownload = async () => {
if (resumeFile == null) {
return;
}
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
link.remove();
};
const fileUploadError = useMemo(() => {
if (invalidFileUploadError != null) {
return invalidFileUploadError;
}
if (errors?.file != null) {
return 'Resume cannot be empty!';
}
}, [errors?.file, invalidFileUploadError]);
return (
<>
<Head>
<title>Upload a Resume</title>
</Head>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-scroll">
<section
aria-labelledby="primary-heading"
className="flex h-full min-w-0 flex-1 flex-col lg:order-last">
<Dialog
isShown={isDialogShown}
primaryButton={
<Button
display="block"
label="OK"
variant="primary"
onClick={onClickProceedDialog}
/>
}
secondaryButton={
<Button
display="block"
label="Cancel"
variant="tertiary"
onClick={() => setIsDialogShown(false)}
/>
}
title="Are you sure you want to clear?"
onClose={() => setIsDialogShown(false)}>
Note that your current input will not be saved!
</Dialog>
<div className="mx-20 space-y-4 py-8">
<form onSubmit={handleSubmit(onSubmit)}>
<h1 className="mb-4 text-2xl font-bold">Upload a resume</h1>
<div className="mb-4">
<TextInput
{...register('title', { required: true })}
disabled={isLoading}
label="Title"
placeholder={TITLE_PLACEHOLDER}
required={true}
onChange={(val) => setValue('title', val)}
/>
</div>
<div className="mb-4">
<Select
{...register('role', { required: true })}
disabled={isLoading}
label="Role"
options={ROLE}
required={true}
onChange={(val) => setValue('role', val)}
/>
</div>
<div className="mb-4">
<Select
{...register('experience', { required: true })}
disabled={isLoading}
label="Experience Level"
options={EXPERIENCE}
required={true}
onChange={(val) => setValue('experience', val)}
/>
</div>
<div className="mb-4">
<Select
{...register('location', { required: true })}
disabled={isLoading}
label="Location"
name="location"
options={LOCATION}
required={true}
onChange={(val) => setValue('location', val)}
/>
</div>
<p className="text-sm font-medium text-slate-700">
Upload resume (PDF format)
<span aria-hidden="true" className="text-danger-500">
{' '}
*
</span>
</p>
<div className="mb-4">
<div
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',
)}>
<div className="space-y-1 text-center">
<div className="flex gap-2">
{resumeFile == null ? (
<PaperClipIcon className="m-auto h-8 w-8 text-gray-600" />
) : (
<div className="flex gap-2">
<p
className="cursor-pointer underline underline-offset-1 hover:text-indigo-600"
onClick={onClickDownload}>
{resumeFile.name}
</p>
</div>
)}
</div>
<div className="flex justify-center text-sm">
<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>
<input
{...register('file', { required: true })}
accept="application/pdf"
className="sr-only"
disabled={isLoading}
id="file-upload"
name="file-upload"
type="file"
onChange={onUploadFile}
/>
</label>
</div>
<p className="text-xs text-gray-500">
PDF up to {FILE_SIZE_LIMIT_MB}MB
</p>
</div>
</div>
{fileUploadError && (
<p className="text-danger-600 text-sm">{fileUploadError}</p>
)}
</div>
<div className="mb-8">
<TextArea
{...register('additionalInfo')}
disabled={isLoading}
label="Additional Information"
placeholder={ADDITIONAL_INFO_PLACEHOLDER}
onChange={(val) => setValue('additionalInfo', val)}
/>
</div>
<div className="mb-4 text-left text-sm text-slate-700">
<h2 className="mb-2 text-xl font-medium">
Submission Guidelines
</h2>
<p>
Before you submit, please review and acknolwedge our
<span className="font-bold"> submission guidelines </span>
stated below.
</p>
<p>
<span className="text-lg font-bold"> </span>
Ensure that you do not divulge any of your
<span className="font-bold"> personal particulars</span>.
</p>
<p>
<span className="text-lg font-bold"> </span>
Ensure that you do not divulge any
<span className="font-bold">
{' '}
company's proprietary and confidential information
</span>
.
</p>
<p>
<span className="text-lg font-bold">• </span>
Proof-read your resumes to look for grammatical/spelling
errors.
</p>
</div>
<CheckboxInput
{...register('isChecked', { required: true })}
disabled={isLoading}
label="I have read and will follow the guidelines stated."
onChange={(val) => setValue('isChecked', val)}
/>
<div className="mt-4 flex justify-end gap-4">
<Button
addonPosition="start"
disabled={isLoading}
display="inline"
label="Clear"
size="md"
variant="tertiary"
onClick={onClickReset}
/>
<Button
addonPosition="start"
disabled={isLoading}
display="inline"
isLoading={isLoading}
label="Submit"
size="md"
type="submit"
variant="primary"
/>
</div>
</form>
</div>
</section>
</main>
</>
);
}