[offers][feat] Add form validation for typeaheads (#508)

pull/509/head
Ai Ling 2 years ago committed by GitHub
parent 35a06c1185
commit 32bbb45f4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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
<Controller
control={control}
name={`offers.${index}.offersFullTime.title`}
render={() => (
<FormJobTitlesTypeahead
errorMessage={offerFields?.offersFullTime?.title?.message}
name={`offers.${index}.offersFullTime.title`}
required={true}
value={{
id: watchJobTitle,
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
value: watchJobTitle,
}}
onSelect={(option) => {
if (option) {
setValue(`offers.${index}.offersFullTime.title`, option.value);
}
}}
/>
)}
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,
<Controller
control={control}
name={`offers.${index}.companyId`}
render={() => (
<FormCompaniesTypeahead
errorMessage={offerFields?.companyId?.message}
names={{
label: `offers.${index}.companyName`,
value: `offers.${index}.companyId`,
}}
onSelect={(option) => {
if (option) {
setValue(`offers.${index}.companyId`, option.value);
setValue(`offers.${index}.companyName`, option.label);
}
}}
/>
<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`, '');
}
/>
)}
rules={{ required: true }}
/>
<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
<Controller
control={control}
name={`offers.${index}.offersIntern.title`}
render={() => (
<FormJobTitlesTypeahead
errorMessage={offerFields?.offersIntern?.title?.message}
name={`offers.${index}.offersIntern.title`}
required={true}
value={{
id: watchJobTitle,
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
value: watchJobTitle,
}}
onSelect={(option) => {
if (option) {
setValue(`offers.${index}.offersIntern.title`, option.value);
}
}}
/>
)}
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,
<Controller
control={control}
name={`offers.${index}.companyId`}
render={() => (
<FormCompaniesTypeahead
errorMessage={offerFields?.companyId?.message}
names={{
label: `offers.${index}.companyName`,
value: `offers.${index}.companyId`,
}}
onSelect={(option) => {
if (option) {
setValue(`offers.${index}.companyId`, option.value);
setValue(`offers.${index}.companyName`, option.label);
}
}}
/>
<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`, '');
}
/>
)}
rules={{ required: true }}
/>
<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">

Loading…
Cancel
Save