Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

MCSS-70: Add Application Resume Upload #71

Merged
merged 3 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
// Fix all autofixable errors on file save
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
"source.fixAll": "explicit",
"source.organizeImports": "never"
},
// Fix formatting issues on file save
"editor.formatOnSave": true,
Expand Down
17 changes: 16 additions & 1 deletion api/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { APITemplate } from '@/api/types'
import { CustomFetch } from '@/api/useFetch'
import { ApplicationGetResp, ApplicationUpdateReq } from '@/types/Application'
import {
ApplicationGetResp,
ApplicationUpdateReq,
ResumeGetResp,
ResumeUpdateResp,
} from '@/types/Application'
import { EmailVerifyReq, EmailVerifyResp } from '@/types/Email'
import { EventListResp } from '@/types/Event'
import { PhotoListResp } from '@/types/Photo'
Expand Down Expand Up @@ -28,6 +33,16 @@ const application = (customFetch: CustomFetch) =>
const res = await customFetch('POST', 'DH_BE', '/application-update', args)
return res.data as {}
},
resumeGet: async () => {
const res = await customFetch('GET', 'DH_BE', '/resume-get')
return res.data as ResumeGetResp
},
resumeUpdate: async (args: FormData) => {
const res = await customFetch('POST', 'DH_BE', '/resume-update', args, {
isForm: true,
})
return res.data as ResumeUpdateResp
},
} as const)

const email = (customFetch: CustomFetch) =>
Expand Down
12 changes: 9 additions & 3 deletions api/useFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Props = {
base: BaseURL
url: string
body?: any
isForm?: string
isForm?: boolean
}

const fetchHelper = async (props: Props): Promise<{ data: any; error: any; statusCode: any }> => {
Expand Down Expand Up @@ -68,14 +68,20 @@ const fetchHelper = async (props: Props): Promise<{ data: any; error: any; statu
/** IMPORTANT: Do not use this directly, use useAPI */
const useFetch = () => {
return useCallback(
async (method: Method, base: BaseURL, url: string, body?: Object, isForm?: string) => {
async (
method: Method,
base: BaseURL,
url: string,
body?: Object,
options?: { isForm: boolean }
) => {
try {
return await fetchHelper({
method,
base,
url,
body,
isForm,
isForm: options?.isForm,
})
} catch (error) {
throw error
Expand Down
4 changes: 2 additions & 2 deletions components/Dashboard/RegistrationForms/AboutYou/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const AboutYou = (props: Props) => {
<Grid container direction="column" gap="1.5rem">
<Typography variant="h2">Profile Details</Typography>
<Typography variant="h3" color="text.secondary" gutterBottom>
💍 Tell me about yourself girlll
💍 Tell me about yourself girl
</Typography>
<Controller
name="age"
Expand Down Expand Up @@ -237,7 +237,7 @@ const AboutYou = (props: Props) => {
<Grid container direction="column" gap="1.5rem">
<Typography variant="h2">Location</Typography>
<Typography variant="h3" color="text.secondary" gutterBottom>
🫣 So I can meet your mama
😝 Where's the party at?
</Typography>
<Controller
name="city"
Expand Down
18 changes: 15 additions & 3 deletions components/Dashboard/RegistrationForms/Experience/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Typography from '@mui/material/Typography'
import FormCheckbox from '@/components/Dashboard/RegistrationForms/FormComponents/FormCheckbox'
import FormDynamicSelect from '@/components/Dashboard/RegistrationForms/FormComponents/FormDynamicSelect'
import FormMultiSelect from '@/components/Dashboard/RegistrationForms/FormComponents/FormMultiSelect'
import FormResumeUpload from '@/components/Dashboard/RegistrationForms/FormComponents/FormResumeUpload'
import FormSelect from '@/components/Dashboard/RegistrationForms/FormComponents/FormSelect'
import FormTextField from '@/components/Dashboard/RegistrationForms/FormComponents/FormTextField'
import {
Expand All @@ -17,6 +18,7 @@ import {
interestsOptions,
OTHER_SPECIFY,
programOptions,
ResumeUpdateResp,
schoolOptions,
teamPreferenceOptions,
} from '@/types/Application'
Expand Down Expand Up @@ -92,7 +94,7 @@ const ExperienceForm = (props: Props) => {
options={schoolOptions}
errors={errors}
setOtherField={(val: string) => {
form.setValue('school_other', val)
setValue('school_other', val, { shouldValidate: true })
}}
inputRef={ref}
{...field}
Expand Down Expand Up @@ -124,7 +126,7 @@ const ExperienceForm = (props: Props) => {
options={programOptions}
errors={errors}
setOtherField={(val: string) => {
form.setValue('program_other', val)
setValue('program_other', val, { shouldValidate: true })
}}
inputRef={ref}
{...field}
Expand All @@ -148,7 +150,17 @@ const ExperienceForm = (props: Props) => {
<Typography variant="h3" color="text.secondary" gutterBottom>
🚀 Flex that Hello World python script
</Typography>
<Typography> --- Resume goes here --- </Typography>
<FormResumeUpload
name={getValues('resume_file_name')}
link={getValues('resume_link')}
updateCount={getValues('resume_update_count')}
error={errors.resume_link}
onSuccess={(resp: ResumeUpdateResp) => {
setValue('resume_file_name', resp.resume_file_name, { shouldValidate: true })
setValue('resume_link', resp.resume_link, { shouldValidate: true })
setValue('resume_update_count', resp.resume_update_count, { shouldValidate: true })
}}
/>
<Controller
name="resume_consent"
control={control}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const FormDynamicSelect = (props: Props) => {
onInputChange={(e, value, reason) => {
if (reason === 'input') setInput(value)
}}
value={value}
value={value || null}
filterOptions={filterOptions}
forcePopupIcon
autoComplete
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { useRef } from 'react'
import { FieldError } from 'react-hook-form'

import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
import UploadFileIcon from '@mui/icons-material/UploadFile'
import Alert from '@mui/material/Alert'
import Button from '@mui/material/Button'
import CircularProgress from '@mui/material/CircularProgress'
import Collapse from '@mui/material/Collapse'
import Fab from '@mui/material/Fab'
import Grid from '@mui/material/Grid'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'

import { APIError } from '@/api/types'
import LoadingButton from '@/components/Dashboard/LoadingButton'
import { useToast } from '@/contexts/Toast'
import { useResumeUpdate } from '@/hooks/Application/useResumeUpdate'
import theme from '@/styles/theme'
import { ResumeUpdateResp } from '@/types/Application'

const MAX_FILE_SIZE = 2000000 // 2 MB
const MAX_UPDATE_COUNT = 3

type Props = {
name: string
link: string
updateCount: number
error?: FieldError
onSuccess: (resp: ResumeUpdateResp) => void
}

const FormResumeUpload = (props: Props) => {
const { name, link, updateCount = 0, error, onSuccess } = props
const { setToast } = useToast()

const { isLoading, mutate: updateResume } = useResumeUpdate()

const inputRef = useRef<HTMLInputElement>(null)

const onFileSelected = async (e: React.FormEvent<HTMLInputElement>) => {
if (!e.currentTarget?.files) return
const file = e.currentTarget.files[0]
if (!file) return

// Reset the input field so that the same file can be handled again
if (inputRef.current) {
inputRef.current.value = ''
}

const fileName = file.name
if (fileName.length > 128) {
setToast({
type: 'error',
message: 'File name must be 128 characters or less',
autoHide: false,
})
return
}

if (fileName.split('.').pop() !== 'pdf') {
setToast({
type: 'error',
message: 'File type must be PDF',
autoHide: false,
})
return
}

if (file.size > MAX_FILE_SIZE) {
setToast({
type: 'error',
message: 'Max file size is 2 MB',
autoHide: false,
})
return
}

const data = new FormData()
data.append('file', file)
updateResume(data, {
onSuccess: (resp) => {
// want toast on success so it hides any errors that didn't auto-hide
if (updateCount === resp.resume_update_count) {
setToast({
type: 'info',
message: 'No changes detected in resume',
})
} else {
setToast({
type: 'success',
message: 'Resume successfully uploaded',
})
onSuccess(resp)
}
},
onError: (err) => {
setToast({
type: 'error',
message:
(err as APIError).apiError.status == 400
? 'Bad Request. Please review your resume and try again.'
: 'Oops, something went wrong. Please try again.',
autoHide: false,
})
},
})
}

return (
<>
<Grid container flexDirection="column">
<input type="file" ref={inputRef} onChange={onFileSelected} accept=".pdf" hidden />
<Collapse in={updateCount < MAX_UPDATE_COUNT}>
<Alert severity="info" sx={{ mb: '1rem' }}>
<Grid container flexDirection="column">
{`${MAX_UPDATE_COUNT - updateCount} update(s) remaining (bc we're broke bois).`}
<Typography color="secondary" variant="caption">
ACCEPTED FILE FORMAT: PDF - MAX SIZE 2 MB
</Typography>
</Grid>
</Alert>
</Collapse>
<Collapse
in={updateCount > 0}
sx={{
width: '100%',
'& .MuiCollapse-wrapperInner': {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '1rem',
},
}}
>
<Button
variant="outlined"
fullWidth
href={link}
rel="noopener"
target="_blank"
startIcon={<CloudDownloadIcon />}
sx={{ justifyContent: 'start', borderRadius: '1rem' }}
>
<Typography
variant="button"
color="text.secondary"
textAlign="left"
fontSize="0.75rem"
noWrap
>
Download Resume
<Typography color="primary" noWrap sx={{ textDecoration: 'underline' }}>
{name}
</Typography>
</Typography>
</Button>
{updateCount < MAX_UPDATE_COUNT && (
<Tooltip title="Upload New Resume">
<Fab
disabled={isLoading}
onClick={(e) => {
e.stopPropagation()
inputRef.current?.click()
}}
sx={{ minWidth: '56px', zIndex: 1 }}
>
{isLoading ? <CircularProgress size={24} color="inherit" /> : <UploadFileIcon />}
</Fab>
</Tooltip>
)}
</Collapse>
<Collapse in={updateCount == 0}>
<LoadingButton
variant="outlined"
fullWidth
loading={isLoading}
disabled={updateCount >= MAX_UPDATE_COUNT}
onClick={(e) => {
e.stopPropagation()
inputRef.current?.click()
}}
sx={{
border: '1px dashed rgba(255, 255, 255, 0.4)',
...(error && { border: `1px dashed ${theme.palette.error.main}` }),
flexDirection: 'column',
p: '1rem',
}}
>
<CloudUploadIcon fontSize="large" />
<Typography variant="button">Upload Resume</Typography>
</LoadingButton>
{error && <Typography className="formError">{error.message}</Typography>}
</Collapse>
</Grid>
</>
)
}

export default FormResumeUpload
Loading
Loading