diff --git a/apps/portal/src/components/offers/OffersTable.tsx b/apps/portal/src/components/offers/OffersTable.tsx
index ded79547..5ef39748 100644
--- a/apps/portal/src/components/offers/OffersTable.tsx
+++ b/apps/portal/src/components/offers/OffersTable.tsx
@@ -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;
diff --git a/apps/portal/src/components/offers/constants.ts b/apps/portal/src/components/offers/constants.ts
index 2a09e9b5..a5dc2706 100644
--- a/apps/portal/src/components/offers/constants.ts
+++ b/apps/portal/src/components/offers/constants.ts
@@ -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',
},
];
diff --git a/apps/portal/src/components/offers/forms/BackgroundForm.tsx b/apps/portal/src/components/offers/forms/BackgroundForm.tsx
index 02d64af7..171823f1 100644
--- a/apps/portal/src/components/offers/forms/BackgroundForm.tsx
+++ b/apps/portal/src/components/offers/forms/BackgroundForm.tsx
@@ -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,
+ })}
/>
@@ -37,7 +39,9 @@ function YoeSection() {
@@ -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,
+ })}
/>
@@ -107,12 +117,12 @@ function FullTimeJobFields() {
@@ -120,12 +130,14 @@ function FullTimeJobFields() {
display="block"
label="Location"
options={locationOptions}
- {...register(`background.experience.location`)}
+ {...register(`background.experiences.0.location`)}
/>
@@ -142,13 +154,13 @@ function InternshipJobFields() {
display="block"
label="Title"
options={titleOptions}
- {...register(`background.experience.title`)}
+ {...register(`background.experiences.0.title`)}
/>
@@ -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`)}
/>
@@ -176,13 +188,13 @@ function InternshipJobFields() {
@@ -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')}>
@@ -259,7 +271,7 @@ function EducationSection() {
diff --git a/apps/portal/src/components/offers/forms/OfferDetailsForm.tsx b/apps/portal/src/components/offers/forms/OfferDetailsForm.tsx
index 4731c861..6348360f 100644
--- a/apps/portal/src/components/offers/forms/OfferDetailsForm.tsx
+++ b/apps/portal/src/components/offers/forms/OfferDetailsForm.tsx
@@ -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,
})}
/>
@@ -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,
+ })}
/>
@@ -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,
+ })}
/>
@@ -287,16 +297,18 @@ function InternshipOfferDetailsForm({
label="Company"
options={companyOptions}
required={true}
- value="Shopee"
- {...register(`offers.${index}.companyId`)}
+ {...register(`offers.${index}.companyId`, {
+ required: true,
+ })}
/>
@@ -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,
+ })}
/>
@@ -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,
+ })}
/>
diff --git a/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx b/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx
index 3ebc52da..62967117 100644
--- a/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx
+++ b/apps/portal/src/components/offers/forms/components/FormMonthYearPicker.tsx
@@ -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
;
@@ -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;
diff --git a/apps/portal/src/components/offers/forms/components/FormRadioList.tsx b/apps/portal/src/components/offers/forms/components/FormRadioList.tsx
index 9ce3065d..5fbbd53d 100644
--- a/apps/portal/src/components/offers/forms/components/FormRadioList.tsx
+++ b/apps/portal/src/components/offers/forms/components/FormRadioList.tsx
@@ -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;
type FormRadioListProps = Omit;
-function FormRadioListWithRef({ name, ...rest }: FormRadioListProps) {
+export default function FormRadioList({ name, ...rest }: FormRadioListProps) {
const { setValue } = useFormContext();
return (
);
}
-
-const FormRadioList = forwardRef(FormRadioListWithRef);
-
-export default FormRadioList;
diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts
index b514744e..82ee7140 100644
--- a/apps/portal/src/components/offers/types.ts
+++ b/apps/portal/src/components/offers/types.ts
@@ -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;
+ experiences: Array;
specificYoes: Array;
totalYoe: number;
};
@@ -98,3 +105,8 @@ export type SubmitOfferFormData = {
background: BackgroundFormData;
offers: Array;
};
+
+export type OfferPostData = {
+ background: BackgroundFormData;
+ offers: Array;
+};
diff --git a/apps/portal/src/pages/offers/submit.tsx b/apps/portal/src/pages/offers/submit.tsx
index 05b25555..c4949bb2 100644
--- a/apps/portal/src/pages/offers/submit.tsx
+++ b/apps/portal/src/pages/offers/submit.tsx
@@ -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 (
@@ -82,12 +88,37 @@ export default function OffersSubmissionPage() {
const previousStep = () => setFormStep(formStep - 1);
- const onSubmit: SubmitHandler = 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 = 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 (
diff --git a/apps/portal/src/components/offers/util/currency/CurrencyEnum.tsx b/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx
similarity index 100%
rename from apps/portal/src/components/offers/util/currency/CurrencyEnum.tsx
rename to apps/portal/src/utils/offers/currency/CurrencyEnum.tsx
diff --git a/apps/portal/src/components/offers/util/currency/CurrencySelector.tsx b/apps/portal/src/utils/offers/currency/CurrencySelector.tsx
similarity index 89%
rename from apps/portal/src/components/offers/util/currency/CurrencySelector.tsx
rename to apps/portal/src/utils/offers/currency/CurrencySelector.tsx
index 2ebe5bd5..2ba883b4 100644
--- a/apps/portal/src/components/offers/util/currency/CurrencySelector.tsx
+++ b/apps/portal/src/utils/offers/currency/CurrencySelector.tsx
@@ -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,
diff --git a/apps/portal/src/utils/offers/form.tsx b/apps/portal/src/utils/offers/form.tsx
new file mode 100644
index 00000000..16def65d
--- /dev/null
+++ b/apps/portal/src/utils/offers/form.tsx
@@ -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;
+}
diff --git a/apps/portal/src/components/offers/util/time/index.tsx b/apps/portal/src/utils/offers/time.tsx
similarity index 100%
rename from apps/portal/src/components/offers/util/time/index.tsx
rename to apps/portal/src/utils/offers/time.tsx