[offers][feat] Integrate offer profile create API

pull/358/head
Ai Ling 3 years ago
parent 7891c2d2ed
commit 201f6487fc

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui'; import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui';
import CurrencySelector from '~/components/offers/util/currency/CurrencySelector'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
type TableRow = { type TableRow = {
company: string; company: string;

@ -29,24 +29,24 @@ export const titleOptions = [
export const companyOptions = [ export const companyOptions = [
emptyOption, emptyOption,
{ {
label: 'Bytedance', label: 'Amazon',
value: 'id-abc123', value: 'cl93patjt0000txewdi601mub',
}, },
{ {
label: 'Google', label: 'Microsoft',
value: 'id-abc567', value: 'cl93patjt0001txewkglfjsro',
}, },
{ {
label: 'Meta', label: 'Apple',
value: 'id-abc456', value: 'cl93patjt0002txewf3ug54m8',
}, },
{ {
label: 'Shopee', label: 'Google',
value: 'id-abc345', value: 'cl93patjt0003txewyiaky7xx',
}, },
{ {
label: 'Tik Tok', label: 'Meta',
value: 'id-abc678', value: 'cl93patjt0004txew88wkcqpu',
}, },
]; ];

@ -12,7 +12,7 @@ import {
titleOptions, titleOptions,
} from '../constants'; } from '../constants';
import { JobType } from '../types'; import { JobType } from '../types';
import { CURRENCY_OPTIONS } from '../util/currency/CurrencyEnum'; import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum';
function YoeSection() { function YoeSection() {
const { register } = useFormContext(); const { register } = useFormContext();
@ -28,7 +28,9 @@ function YoeSection() {
label="Total YOE" label="Total YOE"
placeholder="0" placeholder="0"
type="number" type="number"
{...register(`background.totalYoe`)} {...register(`background.totalYoe`, {
valueAsNumber: true,
})}
/> />
</div> </div>
<div className="grid grid-cols-1 space-x-3"> <div className="grid grid-cols-1 space-x-3">
@ -37,7 +39,9 @@ function YoeSection() {
<FormTextInput <FormTextInput
label="Specific YOE 1" label="Specific YOE 1"
type="number" type="number"
{...register(`background.specificYoes.0.yoe`)} {...register(`background.specificYoes.0.yoe`, {
valueAsNumber: true,
})}
/> />
<FormTextInput <FormTextInput
label="Specific Domain 1" label="Specific Domain 1"
@ -49,7 +53,9 @@ function YoeSection() {
<FormTextInput <FormTextInput
label="Specific YOE 2" label="Specific YOE 2"
type="number" type="number"
{...register(`background.specificYoes.1.yoe`)} {...register(`background.specificYoes.1.yoe`, {
valueAsNumber: true,
})}
/> />
<FormTextInput <FormTextInput
label="Specific Domain 2" label="Specific Domain 2"
@ -73,13 +79,13 @@ function FullTimeJobFields() {
display="block" display="block"
label="Title" label="Title"
options={titleOptions} options={titleOptions}
{...register(`background.experience.title`)} {...register(`background.experiences.0.title`)}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Company" label="Company"
options={companyOptions} options={companyOptions}
{...register(`background.experience.companyId`)} {...register(`background.experiences.0.companyId`)}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-1 space-x-3"> <div className="mb-5 grid grid-cols-1 space-x-3">
@ -90,7 +96,9 @@ function FullTimeJobFields() {
isLabelHidden={true} isLabelHidden={true}
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`background.experience.totalCompensation.currency`)} {...register(
`background.experiences.0.totalCompensation.currency`,
)}
/> />
} }
endAddOnType="element" endAddOnType="element"
@ -99,7 +107,9 @@ function FullTimeJobFields() {
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`background.experience.totalCompensation.value`)} {...register(`background.experiences.0.totalCompensation.value`, {
valueAsNumber: true,
})}
/> />
</div> </div>
<Collapsible label="Add more details"> <Collapsible label="Add more details">
@ -107,12 +117,12 @@ function FullTimeJobFields() {
<FormTextInput <FormTextInput
label="Focus / Specialization" label="Focus / Specialization"
placeholder="e.g. Front End" placeholder="e.g. Front End"
{...register(`background.experience.specialization`)} {...register(`background.experiences.0.specialization`)}
/> />
<FormTextInput <FormTextInput
label="Level" label="Level"
placeholder="e.g. L4, Junior" placeholder="e.g. L4, Junior"
{...register(`background.experience.level`)} {...register(`background.experiences.0.level`)}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
@ -120,12 +130,14 @@ function FullTimeJobFields() {
display="block" display="block"
label="Location" label="Location"
options={locationOptions} options={locationOptions}
{...register(`background.experience.location`)} {...register(`background.experiences.0.location`)}
/> />
<FormTextInput <FormTextInput
label="Duration (months)" label="Duration (months)"
type="number" type="number"
{...register(`background.experience.durationInMonths`)} {...register(`background.experiences.0.durationInMonths`, {
valueAsNumber: true,
})}
/> />
</div> </div>
</Collapsible> </Collapsible>
@ -142,13 +154,13 @@ function InternshipJobFields() {
display="block" display="block"
label="Title" label="Title"
options={titleOptions} options={titleOptions}
{...register(`background.experience.title`)} {...register(`background.experiences.0.title`)}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Company" label="Company"
options={companyOptions} options={companyOptions}
{...register(`background.experience.company`)} {...register(`background.experiences.0.company`)}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-1 space-x-3"> <div className="mb-5 grid grid-cols-1 space-x-3">
@ -159,7 +171,7 @@ function InternshipJobFields() {
isLabelHidden={true} isLabelHidden={true}
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`background.experience.monthlySalary.currency`)} {...register(`background.experiences.0.monthlySalary.currency`)}
/> />
} }
endAddOnType="element" endAddOnType="element"
@ -168,7 +180,7 @@ function InternshipJobFields() {
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`background.experience.monthlySalary.value`)} {...register(`background.experiences.0.monthlySalary.value`)}
/> />
</div> </div>
<Collapsible label="Add more details"> <Collapsible label="Add more details">
@ -176,13 +188,13 @@ function InternshipJobFields() {
<FormTextInput <FormTextInput
label="Focus / Specialization" label="Focus / Specialization"
placeholder="e.g. Front End" placeholder="e.g. Front End"
{...register(`background.experience.specialization`)} {...register(`background.experiences.0.specialization`)}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Location" label="Location"
options={locationOptions} options={locationOptions}
{...register(`background.experience.location`)} {...register(`background.experiences.0.location`)}
/> />
</div> </div>
</Collapsible> </Collapsible>
@ -194,7 +206,7 @@ function CurrentJobSection() {
const { register } = useFormContext(); const { register } = useFormContext();
const watchJobType = useWatch({ const watchJobType = useWatch({
defaultValue: JobType.FullTime, defaultValue: JobType.FullTime,
name: 'background.experience.jobType', name: 'background.experiences.0.jobType',
}); });
return ( return (
@ -209,7 +221,7 @@ function CurrentJobSection() {
isLabelHidden={true} isLabelHidden={true}
label="Job Type" label="Job Type"
orientation="horizontal" orientation="horizontal"
{...register('background.experience.jobType')}> {...register('background.experiences.0.jobType')}>
<RadioList.Item <RadioList.Item
key="Full-time" key="Full-time"
label="Full-time" label="Full-time"
@ -245,13 +257,13 @@ function EducationSection() {
display="block" display="block"
label="Education Level" label="Education Level"
options={educationLevelOptions} options={educationLevelOptions}
{...register(`background.education.type`)} {...register(`background.educations.0.type`)}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Field" label="Field"
options={educationFieldOptions} options={educationFieldOptions}
{...register(`background.education.field`)} {...register(`background.educations.0.field`)}
/> />
</div> </div>
<Collapsible label="Add more details"> <Collapsible label="Add more details">
@ -259,7 +271,7 @@ function EducationSection() {
<FormTextInput <FormTextInput
label="School" label="School"
placeholder="e.g. National University of Singapore" placeholder="e.g. National University of Singapore"
{...register(`background.experience.specialization`)} {...register(`background.educations.0.school`)}
/> />
</div> </div>
</Collapsible> </Collapsible>

@ -23,7 +23,7 @@ import {
} from '../constants'; } from '../constants';
import type { OfferDetailsFormData } from '../types'; import type { OfferDetailsFormData } from '../types';
import { JobType } from '../types'; import { JobType } from '../types';
import { CURRENCY_OPTIONS } from '../util/currency/CurrencyEnum'; import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum';
type FullTimeOfferDetailsFormProps = Readonly<{ type FullTimeOfferDetailsFormProps = Readonly<{
index: number; index: number;
@ -108,6 +108,7 @@ function FullTimeOfferDetailsForm({
type="number" type="number"
{...register(`offers.${index}.job.totalCompensation.value`, { {...register(`offers.${index}.job.totalCompensation.value`, {
required: true, required: true,
valueAsNumber: true,
})} })}
/> />
</div> </div>
@ -131,7 +132,10 @@ function FullTimeOfferDetailsForm({
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.base.value`, { required: true })} {...register(`offers.${index}.job.base.value`, {
required: true,
valueAsNumber: true,
})}
/> />
<FormTextInput <FormTextInput
endAddOn={ endAddOn={
@ -152,7 +156,10 @@ function FullTimeOfferDetailsForm({
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.bonus.value`, { required: true })} {...register(`offers.${index}.job.bonus.value`, {
required: true,
valueAsNumber: true,
})}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
@ -175,7 +182,10 @@ function FullTimeOfferDetailsForm({
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.stocks.value`, { required: true })} {...register(`offers.${index}.job.stocks.value`, {
required: true,
valueAsNumber: true,
})}
/> />
</div> </div>
<div className="mb-5"> <div className="mb-5">
@ -287,16 +297,18 @@ function InternshipOfferDetailsForm({
label="Company" label="Company"
options={companyOptions} options={companyOptions}
required={true} required={true}
value="Shopee" {...register(`offers.${index}.companyId`, {
{...register(`offers.${index}.companyId`)} required: true,
})}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Location" label="Location"
options={locationOptions} options={locationOptions}
required={true} required={true}
value="Singapore, Singapore" {...register(`offers.${index}.location`, {
{...register(`offers.${index}.location`)} required: true,
})}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
@ -305,16 +317,18 @@ function InternshipOfferDetailsForm({
label="Internship Cycle" label="Internship Cycle"
options={internshipCycleOptions} options={internshipCycleOptions}
required={true} required={true}
value="Summer" {...register(`offers.${index}.job.internshipCycle`, {
{...register(`offers.${index}.job.internshipCycle`)} required: true,
})}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Internship Year" label="Internship Year"
options={yearOptions} options={yearOptions}
required={true} required={true}
value="2023" {...register(`offers.${index}.job.startYear`, {
{...register(`offers.${index}.job.startYear`)} required: true,
})}
/> />
</div> </div>
<div className="mb-5 flex items-center space-x-9"> <div className="mb-5 flex items-center space-x-9">
@ -331,7 +345,9 @@ function InternshipOfferDetailsForm({
isLabelHidden={true} isLabelHidden={true}
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.monthlySalary.currency`)} {...register(`offers.${index}.job.monthlySalary.currency`, {
required: true,
})}
/> />
} }
endAddOnType="element" endAddOnType="element"
@ -341,7 +357,10 @@ function InternshipOfferDetailsForm({
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.monthlySalary.value`)} {...register(`offers.${index}.job.monthlySalary.value`, {
required: true,
valueAsNumber: true,
})}
/> />
</div> </div>
<div className="mb-5"> <div className="mb-5">

@ -1,10 +1,9 @@
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';
import { getCurrentMonth, getCurrentYear } from '../../util/time'; import { getCurrentMonth, getCurrentYear } from '../../../../utils/offers/time';
type MonthYearPickerProps = ComponentProps<typeof MonthYearPicker>; type MonthYearPickerProps = ComponentProps<typeof MonthYearPicker>;
@ -15,7 +14,7 @@ type FormMonthYearPickerProps = Omit<
name: string; name: string;
}; };
function FormMonthYearPickerWithRef({ export default function FormMonthYearPicker({
name, name,
...rest ...rest
}: FormMonthYearPickerProps) { }: FormMonthYearPickerProps) {
@ -36,7 +35,3 @@ function FormMonthYearPickerWithRef({
/> />
); );
} }
const FormMonthYearPicker = forwardRef(FormMonthYearPickerWithRef);
export default FormMonthYearPicker;

@ -1,5 +1,4 @@
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import { forwardRef } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { RadioList } from '@tih/ui'; import { RadioList } from '@tih/ui';
@ -7,7 +6,7 @@ type RadioListProps = ComponentProps<typeof RadioList>;
type FormRadioListProps = Omit<RadioListProps, 'onChange'>; type FormRadioListProps = Omit<RadioListProps, 'onChange'>;
function FormRadioListWithRef({ name, ...rest }: FormRadioListProps) { export default function FormRadioList({ name, ...rest }: FormRadioListProps) {
const { setValue } = useFormContext(); const { setValue } = useFormContext();
return ( return (
<RadioList <RadioList
@ -17,7 +16,3 @@ function FormRadioListWithRef({ name, ...rest }: FormRadioListProps) {
/> />
); );
} }
const FormRadioList = forwardRef(FormRadioListWithRef);
export default FormRadioList;

@ -1,10 +1,10 @@
/* eslint-disable no-shadow */ /* eslint-disable no-shadow */
import type { MonthYear } from '../shared/MonthYearPicker';
/* /*
* Offer Profile * Offer Profile
*/ */
import type { MonthYear } from '../shared/MonthYearPicker';
export enum JobType { export enum JobType {
FullTime = 'FULLTIME', FullTime = 'FULLTIME',
Internship = 'INTERNSHIP', Internship = 'INTERNSHIP',
@ -20,7 +20,7 @@ export enum EducationBackgroundType {
SelfTaught = 'Self-taught', SelfTaught = 'Self-taught',
} }
type Money = { export type Money = {
currency: string; currency: string;
value: number; value: number;
}; };
@ -53,6 +53,13 @@ export type OfferDetailsFormData = {
negotiationStrategy: string; negotiationStrategy: string;
}; };
export type OfferDetailsPostData = Omit<
OfferDetailsFormData,
'monthYearReceived'
> & {
monthYearReceived: Date;
};
type SpecificYoe = { type SpecificYoe = {
domain: string; domain: string;
yoe: number; yoe: number;
@ -88,8 +95,8 @@ type Education = {
}; };
type BackgroundFormData = { type BackgroundFormData = {
education: Education; educations: Array<Education>;
experience: Experience; experiences: Array<Experience>;
specificYoes: Array<SpecificYoe>; specificYoes: Array<SpecificYoe>;
totalYoe: number; totalYoe: number;
}; };
@ -98,3 +105,8 @@ export type SubmitOfferFormData = {
background: BackgroundFormData; background: BackgroundFormData;
offers: Array<OfferDetailsFormData>; offers: Array<OfferDetailsFormData>;
}; };
export type OfferPostData = {
background: BackgroundFormData;
offers: Array<OfferDetailsPostData>;
};

@ -8,10 +8,16 @@ 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 { SubmitOfferFormData } from '~/components/offers/types'; import type {
import { getCurrentMonth, getCurrentYear } from '~/components/offers/util/time'; OfferDetailsFormData,
SubmitOfferFormData,
} 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 { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
function Breadcrumbs() { function Breadcrumbs() {
return ( return (
<p className="mb-4 text-right text-sm text-gray-400"> <p className="mb-4 text-right text-sm text-gray-400">
@ -82,12 +88,37 @@ export default function OffersSubmissionPage() {
const previousStep = () => setFormStep(formStep - 1); const previousStep = () => setFormStep(formStep - 1);
const onSubmit: SubmitHandler<SubmitOfferFormData> = async () => { const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(error) {
console.error(error.message);
},
onSuccess() {
alert('offer profile submit success!');
setFormStep(formStep + 1);
},
});
const onSubmit: SubmitHandler<SubmitOfferFormData> = async (data) => {
const result = await trigger(); const result = await trigger();
if (!result) { if (!result) {
return; return;
} }
setFormStep(formStep + 1); data = removeInvalidMoneyData(data);
const background = cleanObject(data.background);
const offers = data.offers.map((offer: OfferDetailsFormData) => ({
...offer,
monthYearReceived: new Date(
offer.monthYearReceived.year,
offer.monthYearReceived.month,
),
}));
const postData = { background, offers };
postData.background.specificYoes = data.background.specificYoes.filter(
(specificYoe) => specificYoe.domain && specificYoe.yoe > 0,
);
createMutation.mutate(postData);
}; };
return ( return (

@ -1,6 +1,6 @@
import { Select } from '@tih/ui'; import { Select } from '@tih/ui';
import { Currency } from '~/components/offers/util/currency/CurrencyEnum'; import { Currency } from '~/utils/offers/currency/CurrencyEnum';
const currencyOptions = Object.entries(Currency).map(([key, value]) => ({ const currencyOptions = Object.entries(Currency).map(([key, value]) => ({
label: key, label: key,

@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Removes empty objects, empty strings, `null`, `undefined`, and `NaN` values from an object.
* Does not remove empty arrays.
* @param object
* @returns object without empty values or objects.
*/
export function cleanObject(object: any) {
Object.entries(object).forEach(([k, v]) => {
if ((v && typeof v === 'object') || Array.isArray(v)) {
cleanObject(v);
}
if (
(v &&
typeof v === 'object' &&
!Object.keys(v).length &&
!Array.isArray(v)) ||
v === null ||
v === undefined ||
v === '' ||
v !== v
) {
if (Array.isArray(object)) {
const index = object.indexOf(v);
object.splice(index, 1);
} else if (!(v instanceof Date)) {
delete object[k];
}
}
});
return object;
}
/**
* Removes invalid money data from an object.
* If currency is present but value is not present, money object is removed.
* @param object
* @returns object without invalid money data.
*/
export function removeInvalidMoneyData(object: any) {
Object.entries(object).forEach(([k, v]) => {
if ((v && typeof v === 'object') || Array.isArray(v)) {
removeInvalidMoneyData(v);
}
if (k === 'currency') {
if (object.value === undefined) {
delete object[k];
} else if (object.value === null || object.value !== object.value) {
delete object[k];
delete object.value;
}
}
});
return object;
}
Loading…
Cancel
Save