mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 12:43:12 +08:00
[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
This commit is contained in:
@ -7,7 +7,7 @@ export function Breadcrumbs({ stepLabels, currentStep }: BreadcrumbsProps) {
|
||||
return (
|
||||
<div className="flex space-x-1">
|
||||
{stepLabels.map((label, index) => (
|
||||
<>
|
||||
<div key={label}>
|
||||
{index === currentStep ? (
|
||||
<p className="text-sm text-purple-700">{label}</p>
|
||||
) : (
|
||||
@ -16,7 +16,7 @@ export function Breadcrumbs({ stepLabels, currentStep }: BreadcrumbsProps) {
|
||||
{index !== stepLabels.length - 1 && (
|
||||
<p className="text-sm text-gray-400">{'>'}</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
|
||||
|
||||
const navigation: ProductNavigationItems = [
|
||||
{ href: '/offers', name: 'Home' },
|
||||
{ href: '/offers/submit', name: 'Benchmark your offer' },
|
||||
];
|
||||
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { EducationBackgroundType } from './types';
|
||||
|
||||
const emptyOption = {
|
||||
label: '----',
|
||||
value: '',
|
||||
};
|
||||
export const emptyOption = '----';
|
||||
|
||||
// TODO: use enums
|
||||
export const titleOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Software engineer',
|
||||
value: 'Software engineer',
|
||||
@ -27,7 +23,6 @@ export const titleOptions = [
|
||||
];
|
||||
|
||||
export const companyOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Amazon',
|
||||
value: 'cl93patjt0000txewdi601mub',
|
||||
@ -51,7 +46,6 @@ export const companyOptions = [
|
||||
];
|
||||
|
||||
export const locationOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Singapore, Singapore',
|
||||
value: 'Singapore, Singapore',
|
||||
@ -67,7 +61,6 @@ export const locationOptions = [
|
||||
];
|
||||
|
||||
export const internshipCycleOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Summer',
|
||||
value: 'Summer',
|
||||
@ -91,7 +84,6 @@ export const internshipCycleOptions = [
|
||||
];
|
||||
|
||||
export const yearOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: '2021',
|
||||
value: '2021',
|
||||
@ -110,17 +102,14 @@ export const yearOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const educationBackgroundTypes = Object.entries(EducationBackgroundType).map(
|
||||
([key, value]) => ({
|
||||
export const educationLevelOptions = Object.entries(
|
||||
EducationBackgroundType,
|
||||
).map(([key, value]) => ({
|
||||
label: key,
|
||||
value,
|
||||
}),
|
||||
);
|
||||
|
||||
export const educationLevelOptions = [emptyOption, ...educationBackgroundTypes];
|
||||
}));
|
||||
|
||||
export const educationFieldOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Computer Science',
|
||||
value: 'Computer Science',
|
||||
@ -134,3 +123,9 @@ export const educationFieldOptions = [
|
||||
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 type {
|
||||
FieldValues,
|
||||
UseFieldArrayRemove,
|
||||
UseFieldArrayReturn,
|
||||
} from 'react-hook-form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { FieldValues, UseFieldArrayReturn } from 'react-hook-form';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { PlusIcon } from '@heroicons/react/20/solid';
|
||||
import { TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { Button } from '@tih/ui';
|
||||
import { Button, Dialog } from '@tih/ui';
|
||||
|
||||
import FormMonthYearPicker from './components/FormMonthYearPicker';
|
||||
import FormSelect from './components/FormSelect';
|
||||
@ -16,74 +13,110 @@ import FormTextArea from './components/FormTextArea';
|
||||
import FormTextInput from './components/FormTextInput';
|
||||
import {
|
||||
companyOptions,
|
||||
emptyOption,
|
||||
FieldError,
|
||||
internshipCycleOptions,
|
||||
locationOptions,
|
||||
titleOptions,
|
||||
yearOptions,
|
||||
} from '../constants';
|
||||
import type { OfferDetailsFormData } from '../types';
|
||||
import type {
|
||||
FullTimeOfferDetailsFormData,
|
||||
InternshipOfferDetailsFormData,
|
||||
} from '../types';
|
||||
import { JobTypeLabel } from '../types';
|
||||
import { JobType } from '../types';
|
||||
import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum';
|
||||
|
||||
type FullTimeOfferDetailsFormProps = Readonly<{
|
||||
index: number;
|
||||
remove: UseFieldArrayRemove;
|
||||
setDialogOpen: (isOpen: boolean) => void;
|
||||
}>;
|
||||
|
||||
function FullTimeOfferDetailsForm({
|
||||
index,
|
||||
remove,
|
||||
setDialogOpen,
|
||||
}: FullTimeOfferDetailsFormProps) {
|
||||
const { register } = useFormContext<{
|
||||
offers: Array<OfferDetailsFormData>;
|
||||
const { register, formState, setValue } = useFormContext<{
|
||||
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 (
|
||||
<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">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.title?.message}
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.title`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.job?.specialization?.message}
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.specialization`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.companyId?.message}
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.companyId`, { required: true })}
|
||||
{...register(`offers.${index}.companyId`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.job?.level?.message}
|
||||
label="Level"
|
||||
placeholder="e.g. L4, Junior"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.level`, { required: true })}
|
||||
{...register(`offers.${index}.job.level`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.location?.message}
|
||||
label="Location"
|
||||
options={locationOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.location`, { required: true })}
|
||||
{...register(`offers.${index}.location`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormMonthYearPicker
|
||||
{...register(`offers.${index}.monthYearReceived`, { required: true })}
|
||||
monthLabel="Date Received"
|
||||
monthRequired={true}
|
||||
yearLabel=""
|
||||
{...register(`offers.${index}.monthYearReceived`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
@ -95,19 +128,21 @@ function FullTimeOfferDetailsForm({
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.totalCompensation.currency`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
errorMessage={offerFields?.job?.totalCompensation?.value?.message}
|
||||
label="Total Compensation (Annual)"
|
||||
placeholder="0.00"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.totalCompensation.value`, {
|
||||
required: true,
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
required: FieldError.Required,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -121,19 +156,21 @@ function FullTimeOfferDetailsForm({
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.base.currency`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
errorMessage={offerFields?.job?.base?.value?.message}
|
||||
label="Base Salary (Annual)"
|
||||
placeholder="0.00"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.base.value`, {
|
||||
required: true,
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
required: FieldError.Required,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -145,19 +182,21 @@ function FullTimeOfferDetailsForm({
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.bonus.currency`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
errorMessage={offerFields?.job?.bonus?.value?.message}
|
||||
label="Bonus (Annual)"
|
||||
placeholder="0.00"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.bonus.value`, {
|
||||
required: true,
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
required: FieldError.Required,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -171,19 +210,21 @@ function FullTimeOfferDetailsForm({
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.stocks.currency`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
errorMessage={offerFields?.job?.stocks?.value?.message}
|
||||
label="Stocks (Annual)"
|
||||
placeholder="0.00"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.stocks.value`, {
|
||||
required: true,
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
required: FieldError.Required,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -208,7 +249,7 @@ function FullTimeOfferDetailsForm({
|
||||
icon={TrashIcon}
|
||||
label="Delete"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</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<{
|
||||
index: number;
|
||||
remove: UseFieldArrayRemove;
|
||||
setDialogOpen: (isOpen: boolean) => void;
|
||||
}>;
|
||||
|
||||
function InternshipOfferDetailsForm({
|
||||
index,
|
||||
remove,
|
||||
setDialogOpen,
|
||||
}: InternshipOfferDetailsFormProps) {
|
||||
const { register } = useFormContext<{
|
||||
offers: Array<OfferDetailsFormData>;
|
||||
const { register, formState } = useFormContext<{
|
||||
offers: Array<InternshipOfferDetailsFormData>;
|
||||
}>();
|
||||
|
||||
const offerFields = formState.errors.offers?.[index];
|
||||
|
||||
return (
|
||||
<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">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.title?.message}
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.title`, {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
errorMessage={offerFields?.job?.specialization?.message}
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.specialization`, {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.companyId?.message}
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.companyId`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.location?.message}
|
||||
label="Location"
|
||||
options={locationOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.location`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.internshipCycle?.message}
|
||||
label="Internship Cycle"
|
||||
options={internshipCycleOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.internshipCycle`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
errorMessage={offerFields?.job?.startYear?.message}
|
||||
label="Internship Year"
|
||||
options={yearOptions}
|
||||
placeholder={emptyOption}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.startYear`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 flex items-center space-x-9">
|
||||
<p className="text-sm">Date received:</p>
|
||||
<div className="mb-5">
|
||||
<FormMonthYearPicker
|
||||
{...register(`offers.${index}.monthYearReceived`, { required: true })}
|
||||
monthLabel="Date Received"
|
||||
monthRequired={true}
|
||||
yearLabel=""
|
||||
{...register(`offers.${index}.monthYearReceived`, {
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
@ -346,19 +365,21 @@ function InternshipOfferDetailsForm({
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.monthlySalary.currency`, {
|
||||
required: true,
|
||||
required: FieldError.Required,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
errorMessage={offerFields?.job?.monthlySalary?.value?.message}
|
||||
label="Salary (Monthly)"
|
||||
placeholder="0.00"
|
||||
placeholder="0"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.monthlySalary.value`, {
|
||||
required: true,
|
||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
||||
required: FieldError.Required,
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
@ -383,7 +404,9 @@ function InternshipOfferDetailsForm({
|
||||
icon={TrashIcon}
|
||||
label="Delete"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
onClick={() => {
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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() {
|
||||
const [jobType, setJobType] = useState(JobType.FullTime);
|
||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||
const { control, register } = useFormContext();
|
||||
const fieldArrayValues = useFieldArray({ control, name: 'offers' });
|
||||
|
||||
const changeJobType = (jobTypeChosen: JobType) => () => {
|
||||
if (jobType === jobTypeChosen) {
|
||||
return;
|
||||
const toggleJobType = () => {
|
||||
if (jobType === JobType.FullTime) {
|
||||
setJobType(JobType.Internship);
|
||||
} else {
|
||||
setJobType(JobType.FullTime);
|
||||
}
|
||||
|
||||
setJobType(jobTypeChosen);
|
||||
fieldArrayValues.remove();
|
||||
};
|
||||
|
||||
const switchJobTypeLabel = () =>
|
||||
jobType === JobType.FullTime
|
||||
? JobTypeLabel.INTERNSHIP
|
||||
: JobTypeLabel.FULLTIME;
|
||||
|
||||
return (
|
||||
<div className="mb-5">
|
||||
<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">
|
||||
<Button
|
||||
display="block"
|
||||
label="Full-time"
|
||||
label={JobTypeLabel.FULLTIME}
|
||||
size="md"
|
||||
variant={jobType === JobType.FullTime ? 'secondary' : 'tertiary'}
|
||||
onClick={changeJobType(JobType.FullTime)}
|
||||
onClick={() => {
|
||||
if (jobType === JobType.FullTime) {
|
||||
return;
|
||||
}
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
{...register(`offers.${0}.jobType`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-5 w-1/3">
|
||||
<Button
|
||||
display="block"
|
||||
label="Internship"
|
||||
label={JobTypeLabel.INTERNSHIP}
|
||||
size="md"
|
||||
variant={jobType === JobType.Internship ? 'secondary' : 'tertiary'}
|
||||
onClick={changeJobType(JobType.Internship)}
|
||||
onClick={() => {
|
||||
if (jobType === JobType.Internship) {
|
||||
return;
|
||||
}
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
{...register(`offers.${0}.jobType`)}
|
||||
/>
|
||||
</div>
|
||||
@ -436,6 +546,32 @@ export default function OfferDetailsForm() {
|
||||
fieldArrayValues={fieldArrayValues}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import MonthYearPicker from '~/components/shared/MonthYearPicker';
|
||||
@ -14,7 +15,7 @@ type FormMonthYearPickerProps = Omit<
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function FormMonthYearPicker({
|
||||
function FormMonthYearPickerWithRef({
|
||||
name,
|
||||
...rest
|
||||
}: 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';
|
||||
|
||||
/*
|
||||
@ -10,6 +9,11 @@ export enum JobType {
|
||||
Internship = 'INTERNSHIP',
|
||||
}
|
||||
|
||||
export const JobTypeLabel = {
|
||||
FULLTIME: 'Full-time',
|
||||
INTERNSHIP: 'Internship',
|
||||
};
|
||||
|
||||
export enum EducationBackgroundType {
|
||||
Bachelor = 'Bachelor',
|
||||
Diploma = 'Diploma',
|
||||
@ -43,16 +47,27 @@ type InternshipJobData = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type OfferDetailsFormData = {
|
||||
type OfferDetailsGeneralData = {
|
||||
comments: string;
|
||||
companyId: string;
|
||||
job: FullTimeJobData | InternshipJobData;
|
||||
jobType: string;
|
||||
location: string;
|
||||
monthYearReceived: MonthYear;
|
||||
negotiationStrategy: string;
|
||||
};
|
||||
|
||||
export type FullTimeOfferDetailsFormData = OfferDetailsGeneralData & {
|
||||
job: FullTimeJobData;
|
||||
};
|
||||
|
||||
export type InternshipOfferDetailsFormData = OfferDetailsGeneralData & {
|
||||
job: InternshipJobData;
|
||||
};
|
||||
|
||||
export type OfferDetailsFormData =
|
||||
| FullTimeOfferDetailsFormData
|
||||
| InternshipOfferDetailsFormData;
|
||||
|
||||
export type OfferDetailsPostData = Omit<
|
||||
OfferDetailsFormData,
|
||||
'monthYearReceived'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||
@ -51,6 +51,9 @@ type FormStep = {
|
||||
|
||||
export default function OffersSubmissionPage() {
|
||||
const [formStep, setFormStep] = useState(0);
|
||||
const pageRef = useRef<HTMLDivElement>(null);
|
||||
const scrollToTop = () =>
|
||||
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
|
||||
const formMethods = useForm<OfferProfileFormData>({
|
||||
defaultValues: defaultOfferValues,
|
||||
mode: 'all',
|
||||
@ -94,9 +97,13 @@ export default function OffersSubmissionPage() {
|
||||
}
|
||||
}
|
||||
setFormStep(formStep + 1);
|
||||
scrollToTop();
|
||||
};
|
||||
|
||||
const previousStep = () => setFormStep(formStep - 1);
|
||||
const previousStep = () => {
|
||||
setFormStep(formStep - 1);
|
||||
scrollToTop();
|
||||
};
|
||||
|
||||
const createMutation = trpc.useMutation(['offers.profile.create'], {
|
||||
onError(error) {
|
||||
@ -105,6 +112,7 @@ export default function OffersSubmissionPage() {
|
||||
onSuccess() {
|
||||
alert('offer profile submit success!');
|
||||
setFormStep(formStep + 1);
|
||||
scrollToTop();
|
||||
},
|
||||
});
|
||||
|
||||
@ -135,7 +143,7 @@ export default function OffersSubmissionPage() {
|
||||
};
|
||||
|
||||
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="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">
|
||||
|
@ -43,7 +43,7 @@ module.exports = {
|
||||
'no-else-return': [ERROR, { allowElseIf: false }],
|
||||
'no-extra-boolean-cast': ERROR,
|
||||
'no-lonely-if': ERROR,
|
||||
'no-shadow': ERROR,
|
||||
'no-shadow': OFF,
|
||||
'no-unused-vars': OFF, // Use @typescript-eslint/no-unused-vars instead.
|
||||
'object-shorthand': ERROR,
|
||||
'one-var': [ERROR, 'never'],
|
||||
@ -100,6 +100,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-for-in-array': ERROR,
|
||||
'@typescript-eslint/no-non-null-assertion': OFF,
|
||||
'@typescript-eslint/no-unused-vars': [ERROR, { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-shadow': ERROR,
|
||||
'@typescript-eslint/prefer-optional-chain': ERROR,
|
||||
'@typescript-eslint/require-array-sort-compare': ERROR,
|
||||
'@typescript-eslint/restrict-plus-operands': ERROR,
|
||||
|
Reference in New Issue
Block a user