mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-29 13:13:54 +08:00
[offers][feat] Add plaintext breadcrumb (#364)
This commit is contained in:
23
apps/portal/src/components/offers/Breadcrumb.tsx
Normal file
23
apps/portal/src/components/offers/Breadcrumb.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
type BreadcrumbsProps = Readonly<{
|
||||||
|
currentStep: number;
|
||||||
|
stepLabels: Array<string>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function Breadcrumbs({ stepLabels, currentStep }: BreadcrumbsProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{stepLabels.map((label, index) => (
|
||||||
|
<>
|
||||||
|
{index === currentStep ? (
|
||||||
|
<p className="text-sm text-purple-700">{label}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-400">{label}</p>
|
||||||
|
)}
|
||||||
|
{index !== stepLabels.length - 1 && (
|
||||||
|
<p className="text-sm text-gray-400">{'>'}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -67,20 +67,20 @@ type SpecificYoe = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type FullTimeExperience = {
|
type FullTimeExperience = {
|
||||||
level: string;
|
level?: string;
|
||||||
totalCompensation: Money;
|
totalCompensation?: Money;
|
||||||
};
|
};
|
||||||
|
|
||||||
type InternshipExperience = {
|
type InternshipExperience = {
|
||||||
monthlySalary: Money;
|
monthlySalary?: Money;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GeneralExperience = {
|
type GeneralExperience = {
|
||||||
companyId: string;
|
companyId?: string;
|
||||||
durationInMonths: number;
|
durationInMonths?: number;
|
||||||
jobType: string;
|
jobType?: string;
|
||||||
specialization: string;
|
specialization?: string;
|
||||||
title: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Experience =
|
export type Experience =
|
||||||
@ -88,26 +88,26 @@ export type Experience =
|
|||||||
| (GeneralExperience & InternshipExperience);
|
| (GeneralExperience & InternshipExperience);
|
||||||
|
|
||||||
type Education = {
|
type Education = {
|
||||||
endDate: Date;
|
endDate?: Date;
|
||||||
field: string;
|
field?: string;
|
||||||
school: string;
|
school?: string;
|
||||||
startDate: Date;
|
startDate?: Date;
|
||||||
type: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BackgroundFormData = {
|
type BackgroundFormData = {
|
||||||
educations: Array<Education>;
|
educations: Array<Education>;
|
||||||
experiences: Array<Experience>;
|
experiences: Array<Experience>;
|
||||||
specificYoes: Array<SpecificYoe>;
|
specificYoes: Array<SpecificYoe>;
|
||||||
totalYoe: number;
|
totalYoe?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SubmitOfferFormData = {
|
export type OfferProfileFormData = {
|
||||||
background: BackgroundFormData;
|
background: BackgroundFormData;
|
||||||
offers: Array<OfferDetailsFormData>;
|
offers: Array<OfferDetailsFormData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OfferPostData = {
|
export type OfferProfilePostData = {
|
||||||
background: BackgroundFormData;
|
background: BackgroundFormData;
|
||||||
offers: Array<OfferDetailsPostData>;
|
offers: Array<OfferDetailsPostData>;
|
||||||
};
|
};
|
||||||
|
@ -4,35 +4,34 @@ import { FormProvider, useForm } from 'react-hook-form';
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||||
import { Button } from '@tih/ui';
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
|
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
|
||||||
import BackgroundForm from '~/components/offers/forms/BackgroundForm';
|
import BackgroundForm from '~/components/offers/forms/BackgroundForm';
|
||||||
import OfferAnalysis from '~/components/offers/forms/OfferAnalysis';
|
import OfferAnalysis from '~/components/offers/forms/OfferAnalysis';
|
||||||
import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm';
|
import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm';
|
||||||
import OfferProfileSave from '~/components/offers/forms/OfferProfileSave';
|
import OfferProfileSave from '~/components/offers/forms/OfferProfileSave';
|
||||||
import type {
|
import type {
|
||||||
OfferDetailsFormData,
|
OfferDetailsFormData,
|
||||||
SubmitOfferFormData,
|
OfferProfileFormData,
|
||||||
} from '~/components/offers/types';
|
} from '~/components/offers/types';
|
||||||
|
import { JobType } from '~/components/offers/types';
|
||||||
import type { Month } from '~/components/shared/MonthYearPicker';
|
import type { Month } from '~/components/shared/MonthYearPicker';
|
||||||
|
|
||||||
import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
|
import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
|
||||||
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
|
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
function Breadcrumbs() {
|
|
||||||
return (
|
|
||||||
<p className="mb-4 text-right text-sm text-gray-400">
|
|
||||||
{'Offer details > Background > Analysis > Save'}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOfferValues = {
|
const defaultOfferValues = {
|
||||||
|
background: {
|
||||||
|
educations: [],
|
||||||
|
experiences: [{ jobType: JobType.FullTime }],
|
||||||
|
specificYoes: [],
|
||||||
|
},
|
||||||
offers: [
|
offers: [
|
||||||
{
|
{
|
||||||
comments: '',
|
comments: '',
|
||||||
companyId: '',
|
companyId: '',
|
||||||
job: {},
|
job: {},
|
||||||
jobType: 'FULLTIME',
|
jobType: JobType.FullTime,
|
||||||
location: '',
|
location: '',
|
||||||
monthYearReceived: {
|
monthYearReceived: {
|
||||||
month: getCurrentMonth() as Month,
|
month: getCurrentMonth() as Month,
|
||||||
@ -47,11 +46,12 @@ type FormStep = {
|
|||||||
component: JSX.Element;
|
component: JSX.Element;
|
||||||
hasNext: boolean;
|
hasNext: boolean;
|
||||||
hasPrevious: boolean;
|
hasPrevious: boolean;
|
||||||
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function OffersSubmissionPage() {
|
export default function OffersSubmissionPage() {
|
||||||
const [formStep, setFormStep] = useState(0);
|
const [formStep, setFormStep] = useState(0);
|
||||||
const formMethods = useForm<SubmitOfferFormData>({
|
const formMethods = useForm<OfferProfileFormData>({
|
||||||
defaultValues: defaultOfferValues,
|
defaultValues: defaultOfferValues,
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
});
|
});
|
||||||
@ -62,20 +62,30 @@ export default function OffersSubmissionPage() {
|
|||||||
component: <OfferDetailsForm key={0} />,
|
component: <OfferDetailsForm key={0} />,
|
||||||
hasNext: true,
|
hasNext: true,
|
||||||
hasPrevious: false,
|
hasPrevious: false,
|
||||||
|
label: 'Offer details',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: <BackgroundForm key={1} />,
|
component: <BackgroundForm key={1} />,
|
||||||
hasNext: false,
|
hasNext: false,
|
||||||
hasPrevious: true,
|
hasPrevious: true,
|
||||||
|
label: 'Background',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: <OfferAnalysis key={2} />,
|
||||||
|
hasNext: true,
|
||||||
|
hasPrevious: false,
|
||||||
|
label: 'Analysis',
|
||||||
},
|
},
|
||||||
{ component: <OfferAnalysis key={2} />, hasNext: true, hasPrevious: false },
|
|
||||||
{
|
{
|
||||||
component: <OfferProfileSave key={3} />,
|
component: <OfferProfileSave key={3} />,
|
||||||
hasNext: false,
|
hasNext: false,
|
||||||
hasPrevious: false,
|
hasPrevious: false,
|
||||||
|
label: 'Save',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const formStepsLabels = formSteps.map((step) => step.label);
|
||||||
|
|
||||||
const nextStep = async (currStep: number) => {
|
const nextStep = async (currStep: number) => {
|
||||||
if (currStep === 0) {
|
if (currStep === 0) {
|
||||||
const result = await trigger('offers');
|
const result = await trigger('offers');
|
||||||
@ -98,7 +108,7 @@ export default function OffersSubmissionPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<SubmitOfferFormData> = async (data) => {
|
const onSubmit: SubmitHandler<OfferProfileFormData> = async (data) => {
|
||||||
const result = await trigger();
|
const result = await trigger();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
@ -118,6 +128,9 @@ export default function OffersSubmissionPage() {
|
|||||||
(specificYoe) => specificYoe.domain && specificYoe.yoe > 0,
|
(specificYoe) => specificYoe.domain && specificYoe.yoe > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Object.entries(postData.background.experiences[0]).length === 1) {
|
||||||
|
postData.background.experiences = [];
|
||||||
|
}
|
||||||
createMutation.mutate(postData);
|
createMutation.mutate(postData);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -125,7 +138,9 @@ export default function OffersSubmissionPage() {
|
|||||||
<div className="fixed h-full w-full overflow-y-scroll">
|
<div 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">
|
||||||
<Breadcrumbs />
|
<div className="mb-4 flex justify-end">
|
||||||
|
<Breadcrumbs currentStep={formStep} stepLabels={formStepsLabels} />
|
||||||
|
</div>
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{formSteps[formStep].component}
|
{formSteps[formStep].component}
|
||||||
|
@ -46,7 +46,11 @@ export function removeInvalidMoneyData(object: any) {
|
|||||||
if (k === 'currency') {
|
if (k === 'currency') {
|
||||||
if (object.value === undefined) {
|
if (object.value === undefined) {
|
||||||
delete object[k];
|
delete object[k];
|
||||||
} else if (object.value === null || object.value !== object.value) {
|
} else if (
|
||||||
|
object.value === null ||
|
||||||
|
object.value !== object.value ||
|
||||||
|
object.value === ''
|
||||||
|
) {
|
||||||
delete object[k];
|
delete object[k];
|
||||||
delete object.value;
|
delete object.value;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user