[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 { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui';
import CurrencySelector from '~/components/offers/util/currency/CurrencySelector';
import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
type TableRow = {
company: string;

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

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

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

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

@ -1,5 +1,4 @@
import type { ComponentProps } from 'react';
import { forwardRef } from 'react';
import { useFormContext } from 'react-hook-form';
import { RadioList } from '@tih/ui';
@ -7,7 +6,7 @@ type RadioListProps = ComponentProps<typeof RadioList>;
type FormRadioListProps = Omit<RadioListProps, 'onChange'>;
function FormRadioListWithRef({ name, ...rest }: FormRadioListProps) {
export default function FormRadioList({ name, ...rest }: FormRadioListProps) {
const { setValue } = useFormContext();
return (
<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 */
import type { MonthYear } from '../shared/MonthYearPicker';
/*
* Offer Profile
*/
import type { MonthYear } from '../shared/MonthYearPicker';
export enum JobType {
FullTime = 'FULLTIME',
Internship = 'INTERNSHIP',
@ -20,7 +20,7 @@ export enum EducationBackgroundType {
SelfTaught = 'Self-taught',
}
type Money = {
export type Money = {
currency: string;
value: number;
};
@ -53,6 +53,13 @@ export type OfferDetailsFormData = {
negotiationStrategy: string;
};
export type OfferDetailsPostData = Omit<
OfferDetailsFormData,
'monthYearReceived'
> & {
monthYearReceived: Date;
};
type SpecificYoe = {
domain: string;
yoe: number;
@ -88,8 +95,8 @@ type Education = {
};
type BackgroundFormData = {
education: Education;
experience: Experience;
educations: Array<Education>;
experiences: Array<Experience>;
specificYoes: Array<SpecificYoe>;
totalYoe: number;
};
@ -98,3 +105,8 @@ export type SubmitOfferFormData = {
background: BackgroundFormData;
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 OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm';
import OfferProfileSave from '~/components/offers/forms/OfferProfileSave';
import type { SubmitOfferFormData } from '~/components/offers/types';
import { getCurrentMonth, getCurrentYear } from '~/components/offers/util/time';
import type {
OfferDetailsFormData,
SubmitOfferFormData,
} 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">
@ -82,12 +88,37 @@ export default function OffersSubmissionPage() {
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();
if (!result) {
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 (

@ -1,6 +1,6 @@
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]) => ({
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