mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-02-03 02:24:47 +08:00
[offers][feat] Add form validation for typeaheads (#508)
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
|
||||
|
||||
type Props = Omit<
|
||||
ComponentProps<typeof CitiesTypeahead>,
|
||||
'onSelect' | 'value'
|
||||
> & {
|
||||
names: { label: string; value: string };
|
||||
};
|
||||
|
||||
export default function FormCitiesTypeahead({ names, ...props }: Props) {
|
||||
const { setValue } = useFormContext();
|
||||
const watchCityId = useWatch({
|
||||
name: names.value,
|
||||
});
|
||||
const watchCityName = useWatch({
|
||||
name: names.label,
|
||||
});
|
||||
|
||||
return (
|
||||
<CitiesTypeahead
|
||||
label="Location"
|
||||
{...props}
|
||||
value={{
|
||||
id: watchCityId,
|
||||
label: watchCityName,
|
||||
value: watchCityId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
setValue(names.value, option?.value);
|
||||
setValue(names.label, option?.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||
|
||||
type Props = Omit<
|
||||
ComponentProps<typeof CompaniesTypeahead>,
|
||||
'onSelect' | 'value'
|
||||
> & {
|
||||
names: { label: string; value: string };
|
||||
};
|
||||
|
||||
export default function FormCompaniesTypeahead({ names, ...props }: Props) {
|
||||
const { setValue } = useFormContext();
|
||||
const watchCompanyId = useWatch({
|
||||
name: names.value,
|
||||
});
|
||||
const watchCompanyName = useWatch({
|
||||
name: names.label,
|
||||
});
|
||||
|
||||
return (
|
||||
<CompaniesTypeahead
|
||||
{...props}
|
||||
value={{
|
||||
id: watchCompanyId,
|
||||
label: watchCompanyName,
|
||||
value: watchCompanyId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
setValue(names.value, option?.value);
|
||||
setValue(names.label, option?.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||
|
||||
type Props = Omit<
|
||||
ComponentProps<typeof JobTitlesTypeahead>,
|
||||
'onSelect' | 'value'
|
||||
> & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function FormJobTitlesTypeahead({ name, ...props }: Props) {
|
||||
const { setValue } = useFormContext();
|
||||
const watchJobTitle = useWatch({
|
||||
name,
|
||||
});
|
||||
|
||||
return (
|
||||
<JobTitlesTypeahead
|
||||
{...props}
|
||||
value={{
|
||||
id: watchJobTitle,
|
||||
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||
value: watchJobTitle,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
setValue(name, option?.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -33,13 +33,13 @@ const defaultOfferValues = {
|
||||
cityId: '',
|
||||
comments: '',
|
||||
companyId: '',
|
||||
jobTitle: '',
|
||||
jobType: JobType.FULLTIME,
|
||||
monthYearReceived: {
|
||||
month: getCurrentMonth() as Month,
|
||||
year: getCurrentYear(),
|
||||
},
|
||||
negotiationStrategy: '',
|
||||
title: '',
|
||||
};
|
||||
|
||||
export const defaultFullTimeOfferValues = {
|
||||
@@ -108,6 +108,7 @@ export default function OffersSubmissionForm({
|
||||
const pageRef = useRef<HTMLDivElement>(null);
|
||||
const scrollToTop = () =>
|
||||
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
|
||||
|
||||
const formMethods = useForm<OffersProfileFormData>({
|
||||
defaultValues: initialOfferProfileValues,
|
||||
mode: 'all',
|
||||
|
||||
@@ -4,11 +4,6 @@ import { Collapsible, RadioList } from '@tih/ui';
|
||||
|
||||
import { FieldError } from '~/components/offers/constants';
|
||||
import type { BackgroundPostData } from '~/components/offers/types';
|
||||
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
|
||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||
|
||||
import {
|
||||
Currency,
|
||||
@@ -17,6 +12,9 @@ import {
|
||||
|
||||
import { EducationFieldOptions } from '../../EducationFields';
|
||||
import { EducationLevelOptions } from '../../EducationLevels';
|
||||
import FormCitiesTypeahead from '../../forms/FormCitiesTypeahead';
|
||||
import FormCompaniesTypeahead from '../../forms/FormCompaniesTypeahead';
|
||||
import FormJobTitlesTypeahead from '../../forms/FormJobTitlesTypeahead';
|
||||
import FormRadioList from '../../forms/FormRadioList';
|
||||
import FormSection from '../../forms/FormSection';
|
||||
import FormSelect from '../../forms/FormSelect';
|
||||
@@ -85,56 +83,19 @@ function YoeSection() {
|
||||
}
|
||||
|
||||
function FullTimeJobFields() {
|
||||
const { register, setValue, formState } = useFormContext<{
|
||||
const { register, formState } = useFormContext<{
|
||||
background: BackgroundPostData;
|
||||
}>();
|
||||
const experiencesField = formState.errors.background?.experiences?.[0];
|
||||
|
||||
const watchJobTitle = useWatch({
|
||||
name: 'background.experiences.0.title',
|
||||
});
|
||||
const watchCompanyId = useWatch({
|
||||
name: 'background.experiences.0.companyId',
|
||||
});
|
||||
const watchCompanyName = useWatch({
|
||||
name: 'background.experiences.0.companyName',
|
||||
});
|
||||
const watchCityId = useWatch({
|
||||
name: 'background.experiences.0.cityId',
|
||||
});
|
||||
const watchCityName = useWatch({
|
||||
name: 'background.experiences.0.cityName',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
|
||||
<JobTitlesTypeahead
|
||||
value={{
|
||||
id: watchJobTitle,
|
||||
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||
value: watchJobTitle,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue('background.experiences.0.title', option.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CompaniesTypeahead
|
||||
value={{
|
||||
id: watchCompanyId,
|
||||
label: watchCompanyName,
|
||||
value: watchCompanyId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue('background.experiences.0.companyId', option.value);
|
||||
setValue('background.experiences.0.companyName', option.label);
|
||||
} else {
|
||||
setValue('background.experiences.0.companyId', '');
|
||||
setValue('background.experiences.0.companyName', '');
|
||||
}
|
||||
<FormJobTitlesTypeahead name="background.experiences.0.title" />
|
||||
<FormCompaniesTypeahead
|
||||
names={{
|
||||
label: 'background.experiences.0.companyName',
|
||||
value: 'background.experiences.0.companyId',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -172,21 +133,10 @@ function FullTimeJobFields() {
|
||||
placeholder="e.g. L4, Junior"
|
||||
{...register(`background.experiences.0.level`)}
|
||||
/>
|
||||
<CitiesTypeahead
|
||||
label="Location"
|
||||
value={{
|
||||
id: watchCityId,
|
||||
label: watchCityName,
|
||||
value: watchCityId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue('background.experiences.0.cityId', option.value);
|
||||
setValue('background.experiences.0.cityName', option.label);
|
||||
} else {
|
||||
setValue('background.experiences.0.cityId', '');
|
||||
setValue('background.experiences.0.cityName', '');
|
||||
}
|
||||
<FormCitiesTypeahead
|
||||
names={{
|
||||
label: 'background.experiences.0.cityName',
|
||||
value: 'background.experiences.0.cityId',
|
||||
}}
|
||||
/>
|
||||
<FormTextInput
|
||||
@@ -205,53 +155,19 @@ function FullTimeJobFields() {
|
||||
}
|
||||
|
||||
function InternshipJobFields() {
|
||||
const { register, setValue, formState } = useFormContext<{
|
||||
const { register, formState } = useFormContext<{
|
||||
background: BackgroundPostData;
|
||||
}>();
|
||||
const experiencesField = formState.errors.background?.experiences?.[0];
|
||||
|
||||
const watchJobTitle = useWatch({
|
||||
name: 'background.experiences.0.title',
|
||||
});
|
||||
const watchCompanyId = useWatch({
|
||||
name: 'background.experiences.0.companyId',
|
||||
});
|
||||
const watchCompanyName = useWatch({
|
||||
name: 'background.experiences.0.companyName',
|
||||
});
|
||||
const watchCityId = useWatch({
|
||||
name: 'background.experiences.0.cityId',
|
||||
});
|
||||
const watchCityName = useWatch({
|
||||
name: 'background.experiences.0.cityName',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
|
||||
<JobTitlesTypeahead
|
||||
value={{
|
||||
id: watchJobTitle,
|
||||
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||
value: watchJobTitle,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue('background.experiences.0.title', option.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CompaniesTypeahead
|
||||
value={{
|
||||
id: watchCompanyId,
|
||||
label: watchCompanyName,
|
||||
value: watchCompanyId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue('background.experiences.0.companyId', option.value);
|
||||
setValue('background.experiences.0.companyName', option.label);
|
||||
}
|
||||
<FormJobTitlesTypeahead name="background.experiences.0.title" />
|
||||
<FormCompaniesTypeahead
|
||||
names={{
|
||||
label: 'background.experiences.0.companyName',
|
||||
value: 'background.experiences.0.companyId',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -280,21 +196,10 @@ function InternshipJobFields() {
|
||||
/>
|
||||
<Collapsible label="Add more details">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<CitiesTypeahead
|
||||
label="Location"
|
||||
value={{
|
||||
id: watchCityId,
|
||||
label: watchCityName,
|
||||
value: watchCityId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue('background.experiences.0.cityId', option.value);
|
||||
setValue('background.experiences.0.cityName', option.label);
|
||||
} else {
|
||||
setValue('background.experiences.0.cityId', '');
|
||||
setValue('background.experiences.0.cityName', '');
|
||||
}
|
||||
<FormCitiesTypeahead
|
||||
names={{
|
||||
label: 'background.experiences.0.cityName',
|
||||
value: 'background.experiences.0.cityId',
|
||||
}}
|
||||
/>
|
||||
<FormTextInput
|
||||
@@ -388,4 +293,4 @@ export default function BackgroundForm() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
UseFieldArrayRemove,
|
||||
UseFieldArrayReturn,
|
||||
} from 'react-hook-form';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
@@ -12,17 +13,14 @@ import { TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { JobType } from '@prisma/client';
|
||||
import { Button, Dialog, HorizontalDivider } from '@tih/ui';
|
||||
|
||||
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
|
||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||
|
||||
import {
|
||||
defaultFullTimeOfferValues,
|
||||
defaultInternshipOfferValues,
|
||||
} from '../OffersSubmissionForm';
|
||||
import { FieldError, JobTypeLabel } from '../../constants';
|
||||
import FormCitiesTypeahead from '../../forms/FormCitiesTypeahead';
|
||||
import FormCompaniesTypeahead from '../../forms/FormCompaniesTypeahead';
|
||||
import FormJobTitlesTypeahead from '../../forms/FormJobTitlesTypeahead';
|
||||
import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
|
||||
import FormSection from '../../forms/FormSection';
|
||||
import FormSelect from '../../forms/FormSelect';
|
||||
@@ -46,26 +44,11 @@ function FullTimeOfferDetailsForm({
|
||||
index,
|
||||
remove,
|
||||
}: FullTimeOfferDetailsFormProps) {
|
||||
const { register, formState, setValue } = useFormContext<{
|
||||
const { register, formState, setValue, control } = useFormContext<{
|
||||
offers: Array<OfferFormData>;
|
||||
}>();
|
||||
const offerFields = formState.errors.offers?.[index];
|
||||
|
||||
const watchJobTitle = useWatch({
|
||||
name: `offers.${index}.offersFullTime.title`,
|
||||
});
|
||||
const watchCompanyId = useWatch({
|
||||
name: `offers.${index}.companyId`,
|
||||
});
|
||||
const watchCompanyName = useWatch({
|
||||
name: `offers.${index}.companyName`,
|
||||
});
|
||||
const watchCityId = useWatch({
|
||||
name: `offers.${index}.cityId`,
|
||||
});
|
||||
const watchCityName = useWatch({
|
||||
name: `offers.${index}.cityName`,
|
||||
});
|
||||
const watchCurrency = useWatch({
|
||||
name: `offers.${index}.offersFullTime.totalCompensation.currency`,
|
||||
});
|
||||
@@ -83,18 +66,17 @@ function FullTimeOfferDetailsForm({
|
||||
<div className="space-y-8 rounded-lg border border-slate-200 p-6 sm:space-y-16 sm:p-8">
|
||||
<FormSection title="Company & Title Information">
|
||||
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
|
||||
<JobTitlesTypeahead
|
||||
required={true}
|
||||
value={{
|
||||
id: watchJobTitle,
|
||||
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||
value: watchJobTitle,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue(`offers.${index}.offersFullTime.title`, option.value);
|
||||
}
|
||||
}}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`offers.${index}.offersFullTime.title`}
|
||||
render={() => (
|
||||
<FormJobTitlesTypeahead
|
||||
errorMessage={offerFields?.offersFullTime?.title?.message}
|
||||
name={`offers.${index}.offersFullTime.title`}
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.offersFullTime?.level?.message}
|
||||
@@ -107,37 +89,35 @@ function FullTimeOfferDetailsForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
|
||||
<CompaniesTypeahead
|
||||
required={true}
|
||||
value={{
|
||||
id: watchCompanyId,
|
||||
label: watchCompanyName,
|
||||
value: watchCompanyId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue(`offers.${index}.companyId`, option.value);
|
||||
setValue(`offers.${index}.companyName`, option.label);
|
||||
}
|
||||
}}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`offers.${index}.companyId`}
|
||||
render={() => (
|
||||
<FormCompaniesTypeahead
|
||||
errorMessage={offerFields?.companyId?.message}
|
||||
names={{
|
||||
label: `offers.${index}.companyName`,
|
||||
value: `offers.${index}.companyId`,
|
||||
}}
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
<CitiesTypeahead
|
||||
label="Location"
|
||||
required={true}
|
||||
value={{
|
||||
id: watchCityId,
|
||||
label: watchCityName,
|
||||
value: watchCityId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue(`offers.${index}.cityId`, option.value);
|
||||
setValue(`offers.${index}.cityName`, option.label);
|
||||
} else {
|
||||
setValue(`offers.${index}.cityId`, '');
|
||||
setValue(`offers.${index}.cityName`, '');
|
||||
}
|
||||
}}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`offers.${index}.cityId`}
|
||||
render={() => (
|
||||
<FormCitiesTypeahead
|
||||
errorMessage={offerFields?.cityId?.message}
|
||||
names={{
|
||||
label: `offers.${index}.cityName`,
|
||||
value: `offers.${index}.companyId`,
|
||||
}}
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
@@ -303,76 +283,56 @@ function InternshipOfferDetailsForm({
|
||||
index,
|
||||
remove,
|
||||
}: InternshipOfferDetailsFormProps) {
|
||||
const { register, formState, setValue } = useFormContext<{
|
||||
const { register, formState, control } = useFormContext<{
|
||||
offers: Array<OfferFormData>;
|
||||
}>();
|
||||
const offerFields = formState.errors.offers?.[index];
|
||||
|
||||
const watchJobTitle = useWatch({
|
||||
name: `offers.${index}.offersIntern.title`,
|
||||
});
|
||||
const watchCompanyId = useWatch({
|
||||
name: `offers.${index}.companyId`,
|
||||
});
|
||||
const watchCompanyName = useWatch({
|
||||
name: `offers.${index}.companyName`,
|
||||
});
|
||||
const watchCityId = useWatch({
|
||||
name: `offers.${index}.cityId`,
|
||||
});
|
||||
const watchCityName = useWatch({
|
||||
name: `offers.${index}.cityName`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-8 rounded-lg border border-slate-200 p-6 sm:space-y-16 sm:p-8">
|
||||
<FormSection title="Company & Title Information">
|
||||
<JobTitlesTypeahead
|
||||
required={true}
|
||||
value={{
|
||||
id: watchJobTitle,
|
||||
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||
value: watchJobTitle,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue(`offers.${index}.offersIntern.title`, option.value);
|
||||
}
|
||||
}}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`offers.${index}.offersIntern.title`}
|
||||
render={() => (
|
||||
<FormJobTitlesTypeahead
|
||||
errorMessage={offerFields?.offersIntern?.title?.message}
|
||||
name={`offers.${index}.offersIntern.title`}
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
|
||||
<CompaniesTypeahead
|
||||
required={true}
|
||||
value={{
|
||||
id: watchCompanyId,
|
||||
label: watchCompanyName,
|
||||
value: watchCompanyId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue(`offers.${index}.companyId`, option.value);
|
||||
setValue(`offers.${index}.companyName`, option.label);
|
||||
}
|
||||
}}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`offers.${index}.companyId`}
|
||||
render={() => (
|
||||
<FormCompaniesTypeahead
|
||||
errorMessage={offerFields?.companyId?.message}
|
||||
names={{
|
||||
label: `offers.${index}.companyName`,
|
||||
value: `offers.${index}.companyId`,
|
||||
}}
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
<CitiesTypeahead
|
||||
label="Location"
|
||||
required={true}
|
||||
value={{
|
||||
id: watchCityId,
|
||||
label: watchCityName,
|
||||
value: watchCityId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setValue(`offers.${index}.cityId`, option.value);
|
||||
setValue(`offers.${index}.cityName`, option.label);
|
||||
} else {
|
||||
setValue(`offers.${index}.cityId`, '');
|
||||
setValue(`offers.${index}.cityName`, '');
|
||||
}
|
||||
}}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`offers.${index}.cityId`}
|
||||
render={() => (
|
||||
<FormCitiesTypeahead
|
||||
errorMessage={offerFields?.cityId?.message}
|
||||
names={{
|
||||
label: `offers.${index}.cityName`,
|
||||
value: `offers.${index}.companyId`,
|
||||
}}
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 sm:gap-6">
|
||||
|
||||
Reference in New Issue
Block a user