mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-25 03:07:49 +08:00
[resumes][feat] add required fields and use text area (#329)
* [resumes][feat] add required fields and update UI * [resumes][refactor] use same lists
This commit is contained in:
@ -23,19 +23,33 @@ export const ROLES = [
|
|||||||
label: 'Full-Stack Engineer',
|
label: 'Full-Stack Engineer',
|
||||||
value: 'Full-Stack Engineer',
|
value: 'Full-Stack Engineer',
|
||||||
},
|
},
|
||||||
{ checked: false, label: 'Frontend Engineer', value: 'frontend-engineer' },
|
{ checked: false, label: 'Frontend Engineer', value: 'Frontend Engineer' },
|
||||||
{ checked: false, label: 'Backend Engineer', value: 'backend-engineer' },
|
{ checked: false, label: 'Backend Engineer', value: 'Backend Engineer' },
|
||||||
{ checked: false, label: 'DevOps Engineer', value: 'devops-engineer' },
|
{ checked: false, label: 'DevOps Engineer', value: 'DevOps Engineer' },
|
||||||
{ checked: false, label: 'iOS Engineer', value: 'ios-engineer' },
|
{ checked: false, label: 'iOS Engineer', value: 'iOS Engineer' },
|
||||||
{ checked: false, label: 'Android Engineer', value: 'android-engineer' },
|
{ checked: false, label: 'Android Engineer', value: 'Android Engineer' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EXPERIENCE = [
|
export const EXPERIENCE = [
|
||||||
{ checked: false, label: 'Freshman', value: 'freshman' },
|
{ checked: false, label: 'Freshman', value: 'Freshman' },
|
||||||
{ checked: false, label: 'Sophomore', value: 'sophomore' },
|
{ checked: false, label: 'Sophomore', value: 'Sophomore' },
|
||||||
{ checked: false, label: 'Junior', value: 'junior' },
|
{ checked: false, label: 'Junior', value: 'Junior' },
|
||||||
{ checked: false, label: 'Senior', value: 'senior' },
|
{ checked: false, label: 'Senior', value: 'Senior' },
|
||||||
{ checked: false, label: 'Fresh Grad (0-1 years)', value: 'freshgrad' },
|
{
|
||||||
|
checked: false,
|
||||||
|
label: 'Fresh Grad (0-1 years)',
|
||||||
|
value: 'Fresh Grad (0-1 years)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
label: 'Mid-level (2 - 5 years)',
|
||||||
|
value: 'Mid-level (2 - 5 years)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
label: 'Senior (5+ years)',
|
||||||
|
value: 'Senior (5+ years)',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const LOCATION = [
|
export const LOCATION = [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Button, Dialog, TextInput } from '@tih/ui';
|
import { Button, Dialog, TextArea } from '@tih/ui';
|
||||||
|
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
@ -86,45 +86,39 @@ export default function CommentsForm({
|
|||||||
<form
|
<form
|
||||||
className="w-full space-y-8 divide-y divide-gray-200"
|
className="w-full space-y-8 divide-y divide-gray-200"
|
||||||
onSubmit={handleSubmit(onSubmit)}>
|
onSubmit={handleSubmit(onSubmit)}>
|
||||||
{/* TODO: Convert TextInput to TextArea */}
|
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<TextInput
|
<TextArea
|
||||||
{...(register('general'), {})}
|
{...(register('general'), {})}
|
||||||
label="General"
|
label="General"
|
||||||
placeholder="General comments about the resume"
|
placeholder="General comments about the resume"
|
||||||
type="text"
|
|
||||||
onChange={(value) => onValueChange('general', value)}
|
onChange={(value) => onValueChange('general', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextArea
|
||||||
{...(register('education'), {})}
|
{...(register('education'), {})}
|
||||||
label="Education"
|
label="Education"
|
||||||
placeholder="Comments about the Education section"
|
placeholder="Comments about the Education section"
|
||||||
type="text"
|
|
||||||
onChange={(value) => onValueChange('education', value)}
|
onChange={(value) => onValueChange('education', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextArea
|
||||||
{...(register('experience'), {})}
|
{...(register('experience'), {})}
|
||||||
label="Experience"
|
label="Experience"
|
||||||
placeholder="Comments about the Experience section"
|
placeholder="Comments about the Experience section"
|
||||||
type="text"
|
|
||||||
onChange={(value) => onValueChange('experience', value)}
|
onChange={(value) => onValueChange('experience', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextArea
|
||||||
{...(register('projects'), {})}
|
{...(register('projects'), {})}
|
||||||
label="Projects"
|
label="Projects"
|
||||||
placeholder="Comments about the Projects section"
|
placeholder="Comments about the Projects section"
|
||||||
type="text"
|
|
||||||
onChange={(value) => onValueChange('projects', value)}
|
onChange={(value) => onValueChange('projects', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextArea
|
||||||
{...(register('skills'), {})}
|
{...(register('skills'), {})}
|
||||||
label="Skills"
|
label="Skills"
|
||||||
placeholder="Comments about the Skills section"
|
placeholder="Comments about the Skills section"
|
||||||
type="text"
|
|
||||||
onChange={(value) => onValueChange('skills', value)}
|
onChange={(value) => onValueChange('skills', value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -283,7 +283,10 @@ export default function ResumeHomePage() {
|
|||||||
<ul role="list">
|
<ul role="list">
|
||||||
{resumes.map((resumeObj) => (
|
{resumes.map((resumeObj) => (
|
||||||
<li key={resumeObj.id}>
|
<li key={resumeObj.id}>
|
||||||
<BrowseListItem href="#" resumeInfo={resumeObj} />
|
<BrowseListItem
|
||||||
|
href={`resumes/${resumeObj.id}`}
|
||||||
|
resumeInfo={resumeObj}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { PaperClipIcon } from '@heroicons/react/24/outline';
|
import { PaperClipIcon } from '@heroicons/react/24/outline';
|
||||||
import { Button, Select, TextInput } from '@tih/ui';
|
import { Button, Select, TextArea, TextInput } from '@tih/ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EXPERIENCE,
|
||||||
|
LOCATION,
|
||||||
|
ROLES,
|
||||||
|
} from '~/components/resumes/browse/constants';
|
||||||
|
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
@ -13,7 +20,7 @@ const TITLE_PLACEHOLDER =
|
|||||||
const ADDITIONAL_INFO_PLACEHOLDER = `e.g. I’m 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 ADDITIONAL_INFO_PLACEHOLDER = `e.g. I’m 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 10MB.';
|
const FILE_UPLOAD_ERROR = 'Please upload a PDF file that is less than 10MB.';
|
||||||
|
|
||||||
const MAX_FILE_SIZE_LIMIT = 10485760;
|
const MAX_FILE_SIZE_LIMIT = 10000000;
|
||||||
|
|
||||||
type IFormInput = {
|
type IFormInput = {
|
||||||
additionalInfo?: string;
|
additionalInfo?: string;
|
||||||
@ -25,52 +32,6 @@ type IFormInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function SubmitResumeForm() {
|
export default function SubmitResumeForm() {
|
||||||
// TODO: Use enums instead
|
|
||||||
const roleItems = [
|
|
||||||
{
|
|
||||||
label: 'Frontend Engineer',
|
|
||||||
value: 'Frontend Engineer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Full-Stack Engineer',
|
|
||||||
value: 'Full-Stack Engineer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Backend Engineer',
|
|
||||||
value: 'Backend Engineer',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const experienceItems = [
|
|
||||||
{
|
|
||||||
label: 'Fresh Graduate (0 - 1 years)',
|
|
||||||
value: 'Fresh Graduate (0 - 1 years)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Mid',
|
|
||||||
value: 'Mid',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Senior',
|
|
||||||
value: 'Senior',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const locationItems = [
|
|
||||||
{
|
|
||||||
label: 'United States',
|
|
||||||
value: 'United States',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Singapore',
|
|
||||||
value: 'Singapore',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'India',
|
|
||||||
value: 'India',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create');
|
const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -139,6 +100,7 @@ export default function SubmitResumeForm() {
|
|||||||
errorMessage={errors?.title && 'Title cannot be empty!'}
|
errorMessage={errors?.title && 'Title cannot be empty!'}
|
||||||
label="Title"
|
label="Title"
|
||||||
placeholder={TITLE_PLACEHOLDER}
|
placeholder={TITLE_PLACEHOLDER}
|
||||||
|
required={true}
|
||||||
onChange={(val) => setValue('title', val)}
|
onChange={(val) => setValue('title', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +108,8 @@ export default function SubmitResumeForm() {
|
|||||||
<Select
|
<Select
|
||||||
{...register('role', { required: true })}
|
{...register('role', { required: true })}
|
||||||
label="Role"
|
label="Role"
|
||||||
options={roleItems}
|
options={ROLES}
|
||||||
|
required={true}
|
||||||
onChange={(val) => setValue('role', val)}
|
onChange={(val) => setValue('role', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -154,7 +117,8 @@ export default function SubmitResumeForm() {
|
|||||||
<Select
|
<Select
|
||||||
{...register('experience', { required: true })}
|
{...register('experience', { required: true })}
|
||||||
label="Experience Level"
|
label="Experience Level"
|
||||||
options={experienceItems}
|
options={EXPERIENCE}
|
||||||
|
required={true}
|
||||||
onChange={(val) => setValue('experience', val)}
|
onChange={(val) => setValue('experience', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -163,27 +127,39 @@ export default function SubmitResumeForm() {
|
|||||||
{...register('location', { required: true })}
|
{...register('location', { required: true })}
|
||||||
label="Location"
|
label="Location"
|
||||||
name="location"
|
name="location"
|
||||||
options={locationItems}
|
options={LOCATION}
|
||||||
|
required={true}
|
||||||
onChange={(val) => setValue('location', val)}
|
onChange={(val) => setValue('location', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-medium text-slate-700">
|
<p className="text-sm font-medium text-slate-700">
|
||||||
Upload resume (PDF format)
|
Upload resume (PDF format)
|
||||||
|
<span aria-hidden="true" className="text-danger-500">
|
||||||
|
{' '}
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="mt-2 flex justify-center rounded-md border-2 border-dashed border-gray-300 px-6 pt-5 pb-6">
|
<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="space-y-1 text-center">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<PaperClipIcon className="m-auto h-8 w-8 text-gray-600" />
|
<PaperClipIcon className="m-auto h-8 w-8 text-gray-600" />
|
||||||
{resumeFile && <p>{resumeFile.name}</p>}
|
{resumeFile && <p>{resumeFile.name}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex text-sm text-gray-600">
|
<div className="flex justify-center text-sm">
|
||||||
<label
|
<label
|
||||||
className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500"
|
className="rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2"
|
||||||
htmlFor="file-upload">
|
htmlFor="file-upload">
|
||||||
<span>Upload a file</span>
|
<p className="cursor-pointer font-medium text-indigo-600 hover:text-indigo-500">
|
||||||
|
Upload a file
|
||||||
|
</p>
|
||||||
<input
|
<input
|
||||||
{...register('file', { required: true })}
|
{...register('file', { required: true })}
|
||||||
|
accept="application/pdf"
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
name="file-upload"
|
name="file-upload"
|
||||||
@ -191,7 +167,6 @@ export default function SubmitResumeForm() {
|
|||||||
onChange={onUploadFile}
|
onChange={onUploadFile}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p className="pl-1">or drag and drop</p>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500">PDF up to 10MB</p>
|
<p className="text-xs text-gray-500">PDF up to 10MB</p>
|
||||||
</div>
|
</div>
|
||||||
@ -201,8 +176,7 @@ export default function SubmitResumeForm() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
{/* TODO: Use TextInputArea instead */}
|
<TextArea
|
||||||
<TextInput
|
|
||||||
{...register('additionalInfo')}
|
{...register('additionalInfo')}
|
||||||
label="Additional Information"
|
label="Additional Information"
|
||||||
placeholder={ADDITIONAL_INFO_PLACEHOLDER}
|
placeholder={ADDITIONAL_INFO_PLACEHOLDER}
|
||||||
|
Reference in New Issue
Block a user