mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 04:33:42 +08:00
[offers][fix] Use title typeahead, add default currency and remove specialization field (#423)
This commit is contained in:
@ -2,26 +2,6 @@ import { EducationBackgroundType } from './types';
|
||||
|
||||
export const emptyOption = '----';
|
||||
|
||||
// TODO: use enums
|
||||
export const titleOptions = [
|
||||
{
|
||||
label: 'Software Engineer',
|
||||
value: 'Software Engineer',
|
||||
},
|
||||
{
|
||||
label: 'Frontend Engineer',
|
||||
value: 'Frontend Engineer',
|
||||
},
|
||||
{
|
||||
label: 'Backend Engineer',
|
||||
value: 'Backend Engineer',
|
||||
},
|
||||
{
|
||||
label: 'Full-stack Engineer',
|
||||
value: 'Full-stack Engineer',
|
||||
},
|
||||
];
|
||||
|
||||
export const locationOptions = [
|
||||
{
|
||||
label: 'Singapore, Singapore',
|
||||
|
@ -115,7 +115,7 @@ export default function OffersSubmissionForm({
|
||||
),
|
||||
hasNext: true,
|
||||
hasPrevious: false,
|
||||
label: 'Offer details',
|
||||
label: 'Offers',
|
||||
},
|
||||
{
|
||||
component: <BackgroundForm key={1} />,
|
||||
@ -123,30 +123,35 @@ export default function OffersSubmissionForm({
|
||||
hasPrevious: true,
|
||||
label: 'Background',
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<OfferAnalysis
|
||||
key={2}
|
||||
allAnalysis={analysis}
|
||||
isError={generateAnalysisMutation.isError}
|
||||
isLoading={generateAnalysisMutation.isLoading}
|
||||
/>
|
||||
),
|
||||
hasNext: true,
|
||||
hasPrevious: false,
|
||||
label: 'Analysis',
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<OffersProfileSave
|
||||
key={3}
|
||||
key={2}
|
||||
profileId={createProfileResponse.id || ''}
|
||||
token={createProfileResponse.token}
|
||||
/>
|
||||
),
|
||||
hasNext: false,
|
||||
hasNext: true,
|
||||
hasPrevious: false,
|
||||
label: 'Save',
|
||||
label: 'Save profile',
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<div>
|
||||
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900">
|
||||
Result
|
||||
</h5>
|
||||
<OfferAnalysis
|
||||
key={3}
|
||||
allAnalysis={analysis}
|
||||
isError={generateAnalysisMutation.isError}
|
||||
isLoading={generateAnalysisMutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
hasNext: false,
|
||||
hasPrevious: true,
|
||||
label: 'Analysis',
|
||||
},
|
||||
];
|
||||
|
||||
@ -231,7 +236,7 @@ export default function OffersSubmissionForm({
|
||||
<FormProvider {...formMethods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{formSteps[formStep].component}
|
||||
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
||||
<pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre>
|
||||
{formSteps[formStep].hasNext && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
|
@ -8,13 +8,17 @@ import {
|
||||
emptyOption,
|
||||
FieldError,
|
||||
locationOptions,
|
||||
titleOptions,
|
||||
} from '~/components/offers/constants';
|
||||
import type { BackgroundPostData } from '~/components/offers/types';
|
||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||
|
||||
import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum';
|
||||
import {
|
||||
Currency,
|
||||
CURRENCY_OPTIONS,
|
||||
} from '~/utils/offers/currency/CurrencyEnum';
|
||||
|
||||
import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
|
||||
import FormRadioList from '../../forms/FormRadioList';
|
||||
import FormSelect from '../../forms/FormSelect';
|
||||
import FormTextInput from '../../forms/FormTextInput';
|
||||
@ -92,13 +96,13 @@ function FullTimeJobFields() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
{...register(`background.experiences.0.title`)}
|
||||
/>
|
||||
<div>
|
||||
<JobTitlesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
setValue(`background.experiences.0.title`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
@ -112,6 +116,7 @@ function FullTimeJobFields() {
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
defaultValue={Currency.SGD}
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
@ -177,13 +182,13 @@ function InternshipJobFields() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
{...register(`background.experiences.0.title`)}
|
||||
/>
|
||||
<div>
|
||||
<JobTitlesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
setValue(`background.experiences.0.title`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
onSelect={({ value }) =>
|
||||
@ -197,6 +202,7 @@ function InternshipJobFields() {
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
defaultValue={Currency.SGD}
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
@ -310,6 +316,22 @@ function EducationSection() {
|
||||
{...register(`background.educations.0.school`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 space-x-3">
|
||||
<FormMonthYearPicker
|
||||
monthLabel="Candidature Start"
|
||||
yearLabel=""
|
||||
{...register(`background.educations.0.startDate`, {
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
/>
|
||||
<FormMonthYearPicker
|
||||
monthLabel="Candidature End"
|
||||
yearLabel=""
|
||||
{...register(`background.educations.0.endDate`, {
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</>
|
||||
@ -319,13 +341,9 @@ function EducationSection() {
|
||||
export default function BackgroundForm() {
|
||||
return (
|
||||
<div>
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-slate-900">
|
||||
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900">
|
||||
Help us better gauge your offers
|
||||
</h5>
|
||||
<h6 className="text-md mx-10 mb-8 text-center font-light text-slate-600">
|
||||
This section is mostly optional, but your background information helps
|
||||
us benchmark your offers.
|
||||
</h6>
|
||||
<div>
|
||||
<YoeSection />
|
||||
<CurrentJobSection />
|
||||
|
@ -13,6 +13,7 @@ import { JobType } from '@prisma/client';
|
||||
import { Button, Dialog } from '@tih/ui';
|
||||
|
||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||
|
||||
import {
|
||||
defaultFullTimeOfferValues,
|
||||
@ -23,7 +24,6 @@ import {
|
||||
FieldError,
|
||||
internshipCycleOptions,
|
||||
locationOptions,
|
||||
titleOptions,
|
||||
yearOptions,
|
||||
} from '../../constants';
|
||||
import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
|
||||
@ -32,7 +32,10 @@ import FormTextArea from '../../forms/FormTextArea';
|
||||
import FormTextInput from '../../forms/FormTextInput';
|
||||
import type { OfferFormData } from '../../types';
|
||||
import { JobTypeLabel } from '../../types';
|
||||
import { CURRENCY_OPTIONS } from '../../../../utils/offers/currency/CurrencyEnum';
|
||||
import {
|
||||
Currency,
|
||||
CURRENCY_OPTIONS,
|
||||
} from '../../../../utils/offers/currency/CurrencyEnum';
|
||||
|
||||
type FullTimeOfferDetailsFormProps = Readonly<{
|
||||
index: number;
|
||||
@ -64,32 +67,11 @@ function FullTimeOfferDetailsForm({
|
||||
return (
|
||||
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.offersFullTime?.title?.message}
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.offersFullTime.title`, {
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.offersFullTime?.specialization?.message}
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.offersFullTime.specialization`, {
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 flex grid grid-cols-2 space-x-3">
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
<JobTitlesTypeahead
|
||||
required={true}
|
||||
onSelect={({ value }) =>
|
||||
setValue(`offers.${index}.companyId`, value)
|
||||
setValue(`offers.${index}.offersFullTime.title`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -103,7 +85,15 @@ function FullTimeOfferDetailsForm({
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 flex grid grid-cols-2 items-start space-x-3">
|
||||
<div className="mb-5 flex grid grid-cols-2 space-x-3">
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
required={true}
|
||||
onSelect={({ value }) =>
|
||||
setValue(`offers.${index}.companyId`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.location?.message}
|
||||
@ -115,6 +105,8 @@ function FullTimeOfferDetailsForm({
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 flex grid grid-cols-2 items-start space-x-3">
|
||||
<FormMonthYearPicker
|
||||
monthLabel="Date Received"
|
||||
monthRequired={true}
|
||||
@ -129,6 +121,7 @@ function FullTimeOfferDetailsForm({
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
defaultValue={Currency.SGD}
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
@ -165,14 +158,12 @@ function FullTimeOfferDetailsForm({
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
defaultValue={Currency.SGD}
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(
|
||||
`offers.${index}.offersFullTime.baseSalary.currency`,
|
||||
{
|
||||
required: FieldError.REQUIRED,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
}
|
||||
@ -180,13 +171,11 @@ function FullTimeOfferDetailsForm({
|
||||
errorMessage={offerFields?.offersFullTime?.baseSalary?.value?.message}
|
||||
label="Base Salary (Annual)"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.offersFullTime.baseSalary.value`, {
|
||||
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||
required: FieldError.REQUIRED,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -194,25 +183,22 @@ function FullTimeOfferDetailsForm({
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
defaultValue={Currency.SGD}
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.offersFullTime.bonus.currency`, {
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
{...register(`offers.${index}.offersFullTime.bonus.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
errorMessage={offerFields?.offersFullTime?.bonus?.value?.message}
|
||||
label="Bonus (Annual)"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.offersFullTime.bonus.value`, {
|
||||
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||
required: FieldError.REQUIRED,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -222,25 +208,22 @@ function FullTimeOfferDetailsForm({
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
defaultValue={Currency.SGD}
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.offersFullTime.stocks.currency`, {
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
{...register(`offers.${index}.offersFullTime.stocks.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
errorMessage={offerFields?.offersFullTime?.stocks?.value?.message}
|
||||
label="Stocks (Annual)"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.offersFullTime.stocks.value`, {
|
||||
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||
required: FieldError.REQUIRED,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -291,32 +274,19 @@ function InternshipOfferDetailsForm({
|
||||
return (
|
||||
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.offersIntern?.title?.message}
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.offersIntern.title`, {
|
||||
minLength: 1,
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.offersIntern?.specialization?.message}
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.offersIntern.specialization`, {
|
||||
minLength: 1,
|
||||
required: FieldError.REQUIRED,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<JobTitlesTypeahead
|
||||
required={true}
|
||||
onSelect={({ value }) =>
|
||||
setValue(`offers.${index}.offersIntern.title`, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<div>
|
||||
<CompaniesTypeahead
|
||||
required={true}
|
||||
onSelect={({ value }) =>
|
||||
setValue(`offers.${index}.companyId`, value)
|
||||
}
|
||||
@ -374,6 +344,7 @@ function InternshipOfferDetailsForm({
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
defaultValue={Currency.SGD}
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
|
||||
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||
import { formatDate } from '~/utils/offers/time';
|
||||
|
||||
@ -19,7 +22,9 @@ export default function OfferTableRow({
|
||||
scope="row">
|
||||
{company.name}
|
||||
</th>
|
||||
<td className="py-4 px-6">{title}</td>
|
||||
<td className="py-4 px-6">
|
||||
{getLabelForJobTitleType(title as JobTitleType)}
|
||||
</td>
|
||||
<td className="py-4 px-6">{totalYoe}</td>
|
||||
<td className="py-4 px-6">{convertMoneyToString(income)}</td>
|
||||
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td>
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { Select } from '@tih/ui';
|
||||
|
||||
import { titleOptions } from '~/components/offers/constants';
|
||||
import OffersTitle from '~/components/offers/OffersTitle';
|
||||
import OffersTable from '~/components/offers/table/OffersTable';
|
||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||
|
||||
export default function OffersHomePage() {
|
||||
const [jobTitleFilter, setjobTitleFilter] = useState('Software Engineer');
|
||||
const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
|
||||
const [companyFilter, setCompanyFilter] = useState('');
|
||||
|
||||
return (
|
||||
@ -18,19 +17,17 @@ export default function OffersHomePage() {
|
||||
<div className="mt-4 flex items-center">
|
||||
Viewing offers for
|
||||
<div className="mx-4">
|
||||
<Select
|
||||
<JobTitlesTypeahead
|
||||
isLabelHidden={true}
|
||||
label="Select a job title"
|
||||
options={titleOptions}
|
||||
value={jobTitleFilter}
|
||||
onChange={setjobTitleFilter}
|
||||
placeHolder="Software Engineer"
|
||||
onSelect={({ value }) => setjobTitleFilter(value)}
|
||||
/>
|
||||
</div>
|
||||
in
|
||||
<div className="ml-4">
|
||||
<CompaniesTypeahead
|
||||
isLabelHidden={true}
|
||||
placeHolder="All companies"
|
||||
placeHolder="All Companies"
|
||||
onSelect={({ value }) => setCompanyFilter(value)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -10,6 +10,8 @@ import type {
|
||||
BackgroundDisplayData,
|
||||
OfferDisplayData,
|
||||
} from '~/components/offers/types';
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
|
||||
import { useToast } from '~/../../../packages/ui/dist';
|
||||
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||
@ -62,7 +64,9 @@ export default function OfferProfile() {
|
||||
companyName: res.company.name,
|
||||
id: res.offersFullTime.id,
|
||||
jobLevel: res.offersFullTime.level,
|
||||
jobTitle: res.offersFullTime.title,
|
||||
jobTitle: getLabelForJobTitleType(
|
||||
res.offersFullTime.title as JobTitleType,
|
||||
),
|
||||
location: res.location,
|
||||
negotiationStrategy: res.negotiationStrategy,
|
||||
otherComment: res.comments,
|
||||
@ -77,7 +81,9 @@ export default function OfferProfile() {
|
||||
const filteredOffer: OfferDisplayData = {
|
||||
companyName: res.company.name,
|
||||
id: res.offersIntern!.id,
|
||||
jobTitle: res.offersIntern!.title,
|
||||
jobTitle: getLabelForJobTitleType(
|
||||
res.offersIntern!.title as JobTitleType,
|
||||
),
|
||||
location: res.location,
|
||||
monthlySalary: convertMoneyToString(
|
||||
res.offersIntern!.monthlySalary,
|
||||
@ -107,7 +113,9 @@ export default function OfferProfile() {
|
||||
companyName: experience.company?.name,
|
||||
duration: experience.durationInMonths,
|
||||
jobLevel: experience.level,
|
||||
jobTitle: experience.title,
|
||||
jobTitle: experience.title
|
||||
? getLabelForJobTitleType(experience.title as JobTitleType)
|
||||
: null,
|
||||
monthlySalary: experience.monthlySalary
|
||||
? convertMoneyToString(experience.monthlySalary)
|
||||
: null,
|
||||
|
Reference in New Issue
Block a user