[offers][feat] Enhance submit offers form (#366)

* [eslint] Replace no-shadow with typescript no-shadow

* [offers][feat] Add auto scroll to top

* [offers][feat] Add error messages for text input fields

* [offers][fix] Add warning dialogs

* [offers][fix] Auto change currency according to TC currency

* [offers][fix] Add select error messages and fix date picker labels

* [offers][fix] Fix console warnings
pull/372/head
Ai Ling 2 years ago committed by GitHub
parent 596a555d78
commit f179c4ef1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,7 +7,7 @@ export function Breadcrumbs({ stepLabels, currentStep }: BreadcrumbsProps) {
return ( return (
<div className="flex space-x-1"> <div className="flex space-x-1">
{stepLabels.map((label, index) => ( {stepLabels.map((label, index) => (
<> <div key={label}>
{index === currentStep ? ( {index === currentStep ? (
<p className="text-sm text-purple-700">{label}</p> <p className="text-sm text-purple-700">{label}</p>
) : ( ) : (
@ -16,7 +16,7 @@ export function Breadcrumbs({ stepLabels, currentStep }: BreadcrumbsProps) {
{index !== stepLabels.length - 1 && ( {index !== stepLabels.length - 1 && (
<p className="text-sm text-gray-400">{'>'}</p> <p className="text-sm text-gray-400">{'>'}</p>
)} )}
</> </div>
))} ))}
</div> </div>
); );

@ -1,7 +1,6 @@
import type { ProductNavigationItems } from '~/components/global/ProductNavigation'; import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
const navigation: ProductNavigationItems = [ const navigation: ProductNavigationItems = [
{ href: '/offers', name: 'Home' },
{ href: '/offers/submit', name: 'Benchmark your offer' }, { href: '/offers/submit', name: 'Benchmark your offer' },
]; ];

@ -1,13 +1,9 @@
import { EducationBackgroundType } from './types'; import { EducationBackgroundType } from './types';
const emptyOption = { export const emptyOption = '----';
label: '----',
value: '',
};
// TODO: use enums // TODO: use enums
export const titleOptions = [ export const titleOptions = [
emptyOption,
{ {
label: 'Software engineer', label: 'Software engineer',
value: 'Software engineer', value: 'Software engineer',
@ -27,7 +23,6 @@ export const titleOptions = [
]; ];
export const companyOptions = [ export const companyOptions = [
emptyOption,
{ {
label: 'Amazon', label: 'Amazon',
value: 'cl93patjt0000txewdi601mub', value: 'cl93patjt0000txewdi601mub',
@ -51,7 +46,6 @@ export const companyOptions = [
]; ];
export const locationOptions = [ export const locationOptions = [
emptyOption,
{ {
label: 'Singapore, Singapore', label: 'Singapore, Singapore',
value: 'Singapore, Singapore', value: 'Singapore, Singapore',
@ -67,7 +61,6 @@ export const locationOptions = [
]; ];
export const internshipCycleOptions = [ export const internshipCycleOptions = [
emptyOption,
{ {
label: 'Summer', label: 'Summer',
value: 'Summer', value: 'Summer',
@ -91,7 +84,6 @@ export const internshipCycleOptions = [
]; ];
export const yearOptions = [ export const yearOptions = [
emptyOption,
{ {
label: '2021', label: '2021',
value: '2021', value: '2021',
@ -110,17 +102,14 @@ export const yearOptions = [
}, },
]; ];
const educationBackgroundTypes = Object.entries(EducationBackgroundType).map( export const educationLevelOptions = Object.entries(
([key, value]) => ({ EducationBackgroundType,
label: key, ).map(([key, value]) => ({
value, label: key,
}), value,
); }));
export const educationLevelOptions = [emptyOption, ...educationBackgroundTypes];
export const educationFieldOptions = [ export const educationFieldOptions = [
emptyOption,
{ {
label: 'Computer Science', label: 'Computer Science',
value: 'Computer Science', value: 'Computer Science',
@ -134,3 +123,9 @@ export const educationFieldOptions = [
value: 'Business Analytics', value: 'Business Analytics',
}, },
]; ];
export enum FieldError {
NonNegativeNumber = 'Please fill in a non-negative number in this field.',
Number = 'Please fill in a number in this field.',
Required = 'Please fill in this field.',
}

@ -1,14 +1,11 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import type { import type { FieldValues, UseFieldArrayReturn } from 'react-hook-form';
FieldValues, import { useWatch } from 'react-hook-form';
UseFieldArrayRemove,
UseFieldArrayReturn,
} from 'react-hook-form';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFieldArray } from 'react-hook-form'; import { useFieldArray } from 'react-hook-form';
import { PlusIcon } from '@heroicons/react/20/solid'; import { PlusIcon } from '@heroicons/react/20/solid';
import { TrashIcon } from '@heroicons/react/24/outline'; import { TrashIcon } from '@heroicons/react/24/outline';
import { Button } from '@tih/ui'; import { Button, Dialog } from '@tih/ui';
import FormMonthYearPicker from './components/FormMonthYearPicker'; import FormMonthYearPicker from './components/FormMonthYearPicker';
import FormSelect from './components/FormSelect'; import FormSelect from './components/FormSelect';
@ -16,74 +13,110 @@ import FormTextArea from './components/FormTextArea';
import FormTextInput from './components/FormTextInput'; import FormTextInput from './components/FormTextInput';
import { import {
companyOptions, companyOptions,
emptyOption,
FieldError,
internshipCycleOptions, internshipCycleOptions,
locationOptions, locationOptions,
titleOptions, titleOptions,
yearOptions, yearOptions,
} from '../constants'; } from '../constants';
import type { OfferDetailsFormData } from '../types'; import type {
FullTimeOfferDetailsFormData,
InternshipOfferDetailsFormData,
} from '../types';
import { JobTypeLabel } from '../types';
import { JobType } from '../types'; import { JobType } from '../types';
import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum'; import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum';
type FullTimeOfferDetailsFormProps = Readonly<{ type FullTimeOfferDetailsFormProps = Readonly<{
index: number; index: number;
remove: UseFieldArrayRemove; setDialogOpen: (isOpen: boolean) => void;
}>; }>;
function FullTimeOfferDetailsForm({ function FullTimeOfferDetailsForm({
index, index,
remove, setDialogOpen,
}: FullTimeOfferDetailsFormProps) { }: FullTimeOfferDetailsFormProps) {
const { register } = useFormContext<{ const { register, formState, setValue } = useFormContext<{
offers: Array<OfferDetailsFormData>; offers: Array<FullTimeOfferDetailsFormData>;
}>(); }>();
const offerFields = formState.errors.offers?.[index];
const watchCurrency = useWatch({
name: `offers.${index}.job.totalCompensation.currency`,
});
useEffect(() => {
setValue(`offers.${index}.job.base.currency`, watchCurrency);
setValue(`offers.${index}.job.bonus.currency`, watchCurrency);
setValue(`offers.${index}.job.stocks.currency`, watchCurrency);
}, [watchCurrency, index, setValue]);
return ( return (
<div className="my-5 rounded-lg border border-gray-200 px-10 py-5"> <div className="my-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.title?.message}
label="Title" label="Title"
options={titleOptions} options={titleOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.title`, { {...register(`offers.${index}.job.title`, {
required: true, required: FieldError.Required,
})} })}
/> />
<FormTextInput <FormTextInput
errorMessage={offerFields?.job?.specialization?.message}
label="Focus / Specialization" label="Focus / Specialization"
placeholder="e.g. Front End" placeholder="e.g. Front End"
required={true} required={true}
{...register(`offers.${index}.job.specialization`, { {...register(`offers.${index}.job.specialization`, {
required: true, required: FieldError.Required,
})} })}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.companyId?.message}
label="Company" label="Company"
options={companyOptions} options={companyOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.companyId`, { required: true })} {...register(`offers.${index}.companyId`, {
required: FieldError.Required,
})}
/> />
<FormTextInput <FormTextInput
errorMessage={offerFields?.job?.level?.message}
label="Level" label="Level"
placeholder="e.g. L4, Junior" placeholder="e.g. L4, Junior"
required={true} required={true}
{...register(`offers.${index}.job.level`, { required: true })} {...register(`offers.${index}.job.level`, {
required: FieldError.Required,
})}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.location?.message}
label="Location" label="Location"
options={locationOptions} options={locationOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.location`, { required: true })} {...register(`offers.${index}.location`, {
required: FieldError.Required,
})}
/> />
<FormMonthYearPicker <FormMonthYearPicker
{...register(`offers.${index}.monthYearReceived`, { required: true })} monthLabel="Date Received"
monthRequired={true}
yearLabel=""
{...register(`offers.${index}.monthYearReceived`, {
required: FieldError.Required,
})}
/> />
</div> </div>
<div className="mb-5"> <div className="mb-5">
@ -95,19 +128,21 @@ function FullTimeOfferDetailsForm({
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.totalCompensation.currency`, { {...register(`offers.${index}.job.totalCompensation.currency`, {
required: true, required: FieldError.Required,
})} })}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.totalCompensation?.value?.message}
label="Total Compensation (Annual)" label="Total Compensation (Annual)"
placeholder="0.00" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.totalCompensation.value`, { {...register(`offers.${index}.job.totalCompensation.value`, {
required: true, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -121,19 +156,21 @@ function FullTimeOfferDetailsForm({
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.base.currency`, { {...register(`offers.${index}.job.base.currency`, {
required: true, required: FieldError.Required,
})} })}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.base?.value?.message}
label="Base Salary (Annual)" label="Base Salary (Annual)"
placeholder="0.00" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.base.value`, { {...register(`offers.${index}.job.base.value`, {
required: true, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -145,19 +182,21 @@ function FullTimeOfferDetailsForm({
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.bonus.currency`, { {...register(`offers.${index}.job.bonus.currency`, {
required: true, required: FieldError.Required,
})} })}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.bonus?.value?.message}
label="Bonus (Annual)" label="Bonus (Annual)"
placeholder="0.00" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.bonus.value`, { {...register(`offers.${index}.job.bonus.value`, {
required: true, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -171,19 +210,21 @@ function FullTimeOfferDetailsForm({
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.stocks.currency`, { {...register(`offers.${index}.job.stocks.currency`, {
required: true, required: FieldError.Required,
})} })}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.stocks?.value?.message}
label="Stocks (Annual)" label="Stocks (Annual)"
placeholder="0.00" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.stocks.value`, { {...register(`offers.${index}.job.stocks.value`, {
required: true, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -208,7 +249,7 @@ function FullTimeOfferDetailsForm({
icon={TrashIcon} icon={TrashIcon}
label="Delete" label="Delete"
variant="secondary" variant="secondary"
onClick={() => remove(index)} onClick={() => setDialogOpen(true)}
/> />
)} )}
</div> </div>
@ -216,125 +257,103 @@ function FullTimeOfferDetailsForm({
); );
} }
type OfferDetailsFormArrayProps = Readonly<{
fieldArrayValues: UseFieldArrayReturn<FieldValues, 'offers', 'id'>;
jobType: JobType;
}>;
function OfferDetailsFormArray({
fieldArrayValues,
jobType,
}: OfferDetailsFormArrayProps) {
const { append, remove, fields } = fieldArrayValues;
return (
<div>
{fields.map((item, index) =>
jobType === JobType.FullTime ? (
<FullTimeOfferDetailsForm
key={`offer.${item.id}`}
index={index}
remove={remove}
/>
) : (
<InternshipOfferDetailsForm
key={`offer.${item.id}`}
index={index}
remove={remove}
/>
),
)}
<Button
display="block"
icon={PlusIcon}
label="Add another offer"
size="lg"
variant="tertiary"
onClick={() => append({})}
/>
</div>
);
}
type InternshipOfferDetailsFormProps = Readonly<{ type InternshipOfferDetailsFormProps = Readonly<{
index: number; index: number;
remove: UseFieldArrayRemove; setDialogOpen: (isOpen: boolean) => void;
}>; }>;
function InternshipOfferDetailsForm({ function InternshipOfferDetailsForm({
index, index,
remove, setDialogOpen,
}: InternshipOfferDetailsFormProps) { }: InternshipOfferDetailsFormProps) {
const { register } = useFormContext<{ const { register, formState } = useFormContext<{
offers: Array<OfferDetailsFormData>; offers: Array<InternshipOfferDetailsFormData>;
}>(); }>();
const offerFields = formState.errors.offers?.[index];
return ( return (
<div className="my-5 rounded-lg border border-gray-200 px-10 py-5"> <div className="my-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.title?.message}
label="Title" label="Title"
options={titleOptions} options={titleOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.title`, { {...register(`offers.${index}.job.title`, {
minLength: 1, minLength: 1,
required: true, required: FieldError.Required,
})} })}
/> />
<FormTextInput <FormTextInput
errorMessage={offerFields?.job?.specialization?.message}
label="Focus / Specialization" label="Focus / Specialization"
placeholder="e.g. Front End" placeholder="e.g. Front End"
required={true} required={true}
{...register(`offers.${index}.job.specialization`, { {...register(`offers.${index}.job.specialization`, {
minLength: 1, minLength: 1,
required: true, required: FieldError.Required,
})} })}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.companyId?.message}
label="Company" label="Company"
options={companyOptions} options={companyOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.companyId`, { {...register(`offers.${index}.companyId`, {
required: true, required: FieldError.Required,
})} })}
/> />
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.location?.message}
label="Location" label="Location"
options={locationOptions} options={locationOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.location`, { {...register(`offers.${index}.location`, {
required: true, required: FieldError.Required,
})} })}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.internshipCycle?.message}
label="Internship Cycle" label="Internship Cycle"
options={internshipCycleOptions} options={internshipCycleOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.internshipCycle`, { {...register(`offers.${index}.job.internshipCycle`, {
required: true, required: FieldError.Required,
})} })}
/> />
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.startYear?.message}
label="Internship Year" label="Internship Year"
options={yearOptions} options={yearOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.startYear`, { {...register(`offers.${index}.job.startYear`, {
required: true, required: FieldError.Required,
})} })}
/> />
</div> </div>
<div className="mb-5 flex items-center space-x-9"> <div className="mb-5">
<p className="text-sm">Date received:</p>
<FormMonthYearPicker <FormMonthYearPicker
{...register(`offers.${index}.monthYearReceived`, { required: true })} monthLabel="Date Received"
monthRequired={true}
yearLabel=""
{...register(`offers.${index}.monthYearReceived`, {
required: FieldError.Required,
})}
/> />
</div> </div>
<div className="mb-5"> <div className="mb-5">
@ -346,19 +365,21 @@ function InternshipOfferDetailsForm({
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.monthlySalary.currency`, { {...register(`offers.${index}.job.monthlySalary.currency`, {
required: true, required: FieldError.Required,
})} })}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.monthlySalary?.value?.message}
label="Salary (Monthly)" label="Salary (Monthly)"
placeholder="0.00" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.monthlySalary.value`, { {...register(`offers.${index}.job.monthlySalary.value`, {
required: true, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -383,7 +404,9 @@ function InternshipOfferDetailsForm({
icon={TrashIcon} icon={TrashIcon}
label="Delete" label="Delete"
variant="secondary" variant="secondary"
onClick={() => remove(index)} onClick={() => {
setDialogOpen(true);
}}
/> />
)} )}
</div> </div>
@ -391,20 +414,97 @@ function InternshipOfferDetailsForm({
); );
} }
type OfferDetailsFormArrayProps = Readonly<{
fieldArrayValues: UseFieldArrayReturn<FieldValues, 'offers', 'id'>;
jobType: JobType;
}>;
function OfferDetailsFormArray({
fieldArrayValues,
jobType,
}: OfferDetailsFormArrayProps) {
const { append, remove, fields } = fieldArrayValues;
const [isDialogOpen, setDialogOpen] = useState(false);
return (
<div>
{fields.map((item, index) => {
return (
<div key={item.id}>
{jobType === JobType.FullTime ? (
<FullTimeOfferDetailsForm
index={index}
setDialogOpen={setDialogOpen}
/>
) : (
<InternshipOfferDetailsForm
index={index}
setDialogOpen={setDialogOpen}
/>
)}
<Dialog
isShown={isDialogOpen}
primaryButton={
<Button
display="block"
label="OK"
variant="primary"
onClick={() => {
remove(index);
setDialogOpen(false);
}}
/>
}
secondaryButton={
<Button
display="block"
label="Cancel"
variant="tertiary"
onClick={() => setDialogOpen(false)}
/>
}
title="Remove this offer"
onClose={() => setDialogOpen(false)}>
<p>
Are you sure you want to remove this offer? This action cannot
be reversed.
</p>
</Dialog>
</div>
);
})}
<Button
display="block"
icon={PlusIcon}
label="Add another offer"
size="lg"
variant="tertiary"
onClick={() => append({})}
/>
</div>
);
}
export default function OfferDetailsForm() { export default function OfferDetailsForm() {
const [jobType, setJobType] = useState(JobType.FullTime); const [jobType, setJobType] = useState(JobType.FullTime);
const [isDialogOpen, setDialogOpen] = useState(false);
const { control, register } = useFormContext(); const { control, register } = useFormContext();
const fieldArrayValues = useFieldArray({ control, name: 'offers' }); const fieldArrayValues = useFieldArray({ control, name: 'offers' });
const changeJobType = (jobTypeChosen: JobType) => () => { const toggleJobType = () => {
if (jobType === jobTypeChosen) { if (jobType === JobType.FullTime) {
return; setJobType(JobType.Internship);
} else {
setJobType(JobType.FullTime);
} }
setJobType(jobTypeChosen);
fieldArrayValues.remove(); fieldArrayValues.remove();
}; };
const switchJobTypeLabel = () =>
jobType === JobType.FullTime
? JobTypeLabel.INTERNSHIP
: JobTypeLabel.FULLTIME;
return ( return (
<div className="mb-5"> <div className="mb-5">
<h5 className="mb-8 text-center text-4xl font-bold text-gray-900"> <h5 className="mb-8 text-center text-4xl font-bold text-gray-900">
@ -414,20 +514,30 @@ export default function OfferDetailsForm() {
<div className="mx-5 w-1/3"> <div className="mx-5 w-1/3">
<Button <Button
display="block" display="block"
label="Full-time" label={JobTypeLabel.FULLTIME}
size="md" size="md"
variant={jobType === JobType.FullTime ? 'secondary' : 'tertiary'} variant={jobType === JobType.FullTime ? 'secondary' : 'tertiary'}
onClick={changeJobType(JobType.FullTime)} onClick={() => {
if (jobType === JobType.FullTime) {
return;
}
setDialogOpen(true);
}}
{...register(`offers.${0}.jobType`)} {...register(`offers.${0}.jobType`)}
/> />
</div> </div>
<div className="mx-5 w-1/3"> <div className="mx-5 w-1/3">
<Button <Button
display="block" display="block"
label="Internship" label={JobTypeLabel.INTERNSHIP}
size="md" size="md"
variant={jobType === JobType.Internship ? 'secondary' : 'tertiary'} variant={jobType === JobType.Internship ? 'secondary' : 'tertiary'}
onClick={changeJobType(JobType.Internship)} onClick={() => {
if (jobType === JobType.Internship) {
return;
}
setDialogOpen(true);
}}
{...register(`offers.${0}.jobType`)} {...register(`offers.${0}.jobType`)}
/> />
</div> </div>
@ -436,6 +546,32 @@ export default function OfferDetailsForm() {
fieldArrayValues={fieldArrayValues} fieldArrayValues={fieldArrayValues}
jobType={jobType} jobType={jobType}
/> />
<Dialog
isShown={isDialogOpen}
primaryButton={
<Button
display="block"
label="Switch"
variant="primary"
onClick={() => {
toggleJobType();
setDialogOpen(false);
}}
/>
}
secondaryButton={
<Button
display="block"
label="Cancel"
variant="tertiary"
onClick={() => setDialogOpen(false)}
/>
}
title={`Switch to ${switchJobTypeLabel()}`}
onClose={() => setDialogOpen(false)}>
{`Are you sure you want to switch to ${switchJobTypeLabel()}? The data you
entered in the ${JobTypeLabel[jobType]} section will disappear.`}
</Dialog>
</div> </div>
); );
} }

@ -1,4 +1,5 @@
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import { forwardRef } from 'react';
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import MonthYearPicker from '~/components/shared/MonthYearPicker'; import MonthYearPicker from '~/components/shared/MonthYearPicker';
@ -14,7 +15,7 @@ type FormMonthYearPickerProps = Omit<
name: string; name: string;
}; };
export default function FormMonthYearPicker({ function FormMonthYearPickerWithRef({
name, name,
...rest ...rest
}: FormMonthYearPickerProps) { }: FormMonthYearPickerProps) {
@ -35,3 +36,7 @@ export default function FormMonthYearPicker({
/> />
); );
} }
const FormMonthYearPicker = forwardRef(FormMonthYearPickerWithRef);
export default FormMonthYearPicker;

@ -1,4 +1,3 @@
/* eslint-disable no-shadow */
import type { MonthYear } from '~/components/shared/MonthYearPicker'; import type { MonthYear } from '~/components/shared/MonthYearPicker';
/* /*
@ -10,6 +9,11 @@ export enum JobType {
Internship = 'INTERNSHIP', Internship = 'INTERNSHIP',
} }
export const JobTypeLabel = {
FULLTIME: 'Full-time',
INTERNSHIP: 'Internship',
};
export enum EducationBackgroundType { export enum EducationBackgroundType {
Bachelor = 'Bachelor', Bachelor = 'Bachelor',
Diploma = 'Diploma', Diploma = 'Diploma',
@ -43,16 +47,27 @@ type InternshipJobData = {
title: string; title: string;
}; };
export type OfferDetailsFormData = { type OfferDetailsGeneralData = {
comments: string; comments: string;
companyId: string; companyId: string;
job: FullTimeJobData | InternshipJobData;
jobType: string; jobType: string;
location: string; location: string;
monthYearReceived: MonthYear; monthYearReceived: MonthYear;
negotiationStrategy: string; negotiationStrategy: string;
}; };
export type FullTimeOfferDetailsFormData = OfferDetailsGeneralData & {
job: FullTimeJobData;
};
export type InternshipOfferDetailsFormData = OfferDetailsGeneralData & {
job: InternshipJobData;
};
export type OfferDetailsFormData =
| FullTimeOfferDetailsFormData
| InternshipOfferDetailsFormData;
export type OfferDetailsPostData = Omit< export type OfferDetailsPostData = Omit<
OfferDetailsFormData, OfferDetailsFormData,
'monthYearReceived' 'monthYearReceived'

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useRef, useState } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
@ -51,6 +51,9 @@ type FormStep = {
export default function OffersSubmissionPage() { export default function OffersSubmissionPage() {
const [formStep, setFormStep] = useState(0); const [formStep, setFormStep] = useState(0);
const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
const formMethods = useForm<OfferProfileFormData>({ const formMethods = useForm<OfferProfileFormData>({
defaultValues: defaultOfferValues, defaultValues: defaultOfferValues,
mode: 'all', mode: 'all',
@ -94,9 +97,13 @@ export default function OffersSubmissionPage() {
} }
} }
setFormStep(formStep + 1); setFormStep(formStep + 1);
scrollToTop();
}; };
const previousStep = () => setFormStep(formStep - 1); const previousStep = () => {
setFormStep(formStep - 1);
scrollToTop();
};
const createMutation = trpc.useMutation(['offers.profile.create'], { const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(error) { onError(error) {
@ -105,6 +112,7 @@ export default function OffersSubmissionPage() {
onSuccess() { onSuccess() {
alert('offer profile submit success!'); alert('offer profile submit success!');
setFormStep(formStep + 1); setFormStep(formStep + 1);
scrollToTop();
}, },
}); });
@ -135,7 +143,7 @@ export default function OffersSubmissionPage() {
}; };
return ( return (
<div className="fixed h-full w-full overflow-y-scroll"> <div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
<div className="mb-20 flex justify-center"> <div className="mb-20 flex justify-center">
<div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg"> <div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg">
<div className="mb-4 flex justify-end"> <div className="mb-4 flex justify-end">

@ -43,7 +43,7 @@ module.exports = {
'no-else-return': [ERROR, { allowElseIf: false }], 'no-else-return': [ERROR, { allowElseIf: false }],
'no-extra-boolean-cast': ERROR, 'no-extra-boolean-cast': ERROR,
'no-lonely-if': ERROR, 'no-lonely-if': ERROR,
'no-shadow': ERROR, 'no-shadow': OFF,
'no-unused-vars': OFF, // Use @typescript-eslint/no-unused-vars instead. 'no-unused-vars': OFF, // Use @typescript-eslint/no-unused-vars instead.
'object-shorthand': ERROR, 'object-shorthand': ERROR,
'one-var': [ERROR, 'never'], 'one-var': [ERROR, 'never'],
@ -100,6 +100,7 @@ module.exports = {
'@typescript-eslint/no-for-in-array': ERROR, '@typescript-eslint/no-for-in-array': ERROR,
'@typescript-eslint/no-non-null-assertion': OFF, '@typescript-eslint/no-non-null-assertion': OFF,
'@typescript-eslint/no-unused-vars': [ERROR, { argsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': [ERROR, { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-shadow': ERROR,
'@typescript-eslint/prefer-optional-chain': ERROR, '@typescript-eslint/prefer-optional-chain': ERROR,
'@typescript-eslint/require-array-sort-compare': ERROR, '@typescript-eslint/require-array-sort-compare': ERROR,
'@typescript-eslint/restrict-plus-operands': ERROR, '@typescript-eslint/restrict-plus-operands': ERROR,

Loading…
Cancel
Save