mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 20:52:00 +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 = {
|
||||
level: string;
|
||||
totalCompensation: Money;
|
||||
level?: string;
|
||||
totalCompensation?: Money;
|
||||
};
|
||||
|
||||
type InternshipExperience = {
|
||||
monthlySalary: Money;
|
||||
monthlySalary?: Money;
|
||||
};
|
||||
|
||||
type GeneralExperience = {
|
||||
companyId: string;
|
||||
durationInMonths: number;
|
||||
jobType: string;
|
||||
specialization: string;
|
||||
title: string;
|
||||
companyId?: string;
|
||||
durationInMonths?: number;
|
||||
jobType?: string;
|
||||
specialization?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type Experience =
|
||||
@ -88,26 +88,26 @@ export type Experience =
|
||||
| (GeneralExperience & InternshipExperience);
|
||||
|
||||
type Education = {
|
||||
endDate: Date;
|
||||
field: string;
|
||||
school: string;
|
||||
startDate: Date;
|
||||
type: string;
|
||||
endDate?: Date;
|
||||
field?: string;
|
||||
school?: string;
|
||||
startDate?: Date;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type BackgroundFormData = {
|
||||
educations: Array<Education>;
|
||||
experiences: Array<Experience>;
|
||||
specificYoes: Array<SpecificYoe>;
|
||||
totalYoe: number;
|
||||
totalYoe?: number;
|
||||
};
|
||||
|
||||
export type SubmitOfferFormData = {
|
||||
export type OfferProfileFormData = {
|
||||
background: BackgroundFormData;
|
||||
offers: Array<OfferDetailsFormData>;
|
||||
};
|
||||
|
||||
export type OfferPostData = {
|
||||
export type OfferProfilePostData = {
|
||||
background: BackgroundFormData;
|
||||
offers: Array<OfferDetailsPostData>;
|
||||
};
|
||||
|
@ -4,35 +4,34 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
|
||||
import BackgroundForm from '~/components/offers/forms/BackgroundForm';
|
||||
import OfferAnalysis from '~/components/offers/forms/OfferAnalysis';
|
||||
import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm';
|
||||
import OfferProfileSave from '~/components/offers/forms/OfferProfileSave';
|
||||
import type {
|
||||
OfferDetailsFormData,
|
||||
SubmitOfferFormData,
|
||||
OfferProfileFormData,
|
||||
} from '~/components/offers/types';
|
||||
import { JobType } from '~/components/offers/types';
|
||||
import type { Month } from '~/components/shared/MonthYearPicker';
|
||||
|
||||
import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
|
||||
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
|
||||
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 = {
|
||||
background: {
|
||||
educations: [],
|
||||
experiences: [{ jobType: JobType.FullTime }],
|
||||
specificYoes: [],
|
||||
},
|
||||
offers: [
|
||||
{
|
||||
comments: '',
|
||||
companyId: '',
|
||||
job: {},
|
||||
jobType: 'FULLTIME',
|
||||
jobType: JobType.FullTime,
|
||||
location: '',
|
||||
monthYearReceived: {
|
||||
month: getCurrentMonth() as Month,
|
||||
@ -47,11 +46,12 @@ type FormStep = {
|
||||
component: JSX.Element;
|
||||
hasNext: boolean;
|
||||
hasPrevious: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export default function OffersSubmissionPage() {
|
||||
const [formStep, setFormStep] = useState(0);
|
||||
const formMethods = useForm<SubmitOfferFormData>({
|
||||
const formMethods = useForm<OfferProfileFormData>({
|
||||
defaultValues: defaultOfferValues,
|
||||
mode: 'all',
|
||||
});
|
||||
@ -62,20 +62,30 @@ export default function OffersSubmissionPage() {
|
||||
component: <OfferDetailsForm key={0} />,
|
||||
hasNext: true,
|
||||
hasPrevious: false,
|
||||
label: 'Offer details',
|
||||
},
|
||||
{
|
||||
component: <BackgroundForm key={1} />,
|
||||
hasNext: false,
|
||||
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} />,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
label: 'Save',
|
||||
},
|
||||
];
|
||||
|
||||
const formStepsLabels = formSteps.map((step) => step.label);
|
||||
|
||||
const nextStep = async (currStep: number) => {
|
||||
if (currStep === 0) {
|
||||
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();
|
||||
if (!result) {
|
||||
return;
|
||||
@ -118,6 +128,9 @@ export default function OffersSubmissionPage() {
|
||||
(specificYoe) => specificYoe.domain && specificYoe.yoe > 0,
|
||||
);
|
||||
|
||||
if (Object.entries(postData.background.experiences[0]).length === 1) {
|
||||
postData.background.experiences = [];
|
||||
}
|
||||
createMutation.mutate(postData);
|
||||
};
|
||||
|
||||
@ -125,7 +138,9 @@ export default function OffersSubmissionPage() {
|
||||
<div 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">
|
||||
<Breadcrumbs />
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Breadcrumbs currentStep={formStep} stepLabels={formStepsLabels} />
|
||||
</div>
|
||||
<FormProvider {...formMethods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{formSteps[formStep].component}
|
||||
|
@ -46,7 +46,11 @@ export function removeInvalidMoneyData(object: any) {
|
||||
if (k === 'currency') {
|
||||
if (object.value === undefined) {
|
||||
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.value;
|
||||
}
|
||||
|
Reference in New Issue
Block a user