mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-29 21:23:14 +08:00
[offers][feat] Integrate offers create API and fix form UI (#358)
This commit is contained in:
@ -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: 'Microsoft',
|
||||||
|
value: 'cl93patjt0001txewkglfjsro',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Apple',
|
||||||
|
value: 'cl93patjt0002txewf3ug54m8',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Google',
|
label: 'Google',
|
||||||
value: 'id-abc567',
|
value: 'cl93patjt0003txewyiaky7xx',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Meta',
|
label: 'Meta',
|
||||||
value: 'id-abc456',
|
value: 'cl93patjt0004txew88wkcqpu',
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Shopee',
|
|
||||||
value: 'id-abc345',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Tik Tok',
|
|
||||||
value: 'id-abc678',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
import { Collapsible, RadioList } from '@tih/ui';
|
import { Collapsible, RadioList } from '@tih/ui';
|
||||||
|
|
||||||
import FormRadioList from './FormRadioList';
|
import FormRadioList from './components/FormRadioList';
|
||||||
import FormSelect from './FormSelect';
|
import FormSelect from './components/FormSelect';
|
||||||
import FormTextInput from './FormTextInput';
|
import FormTextInput from './components/FormTextInput';
|
||||||
import {
|
import {
|
||||||
companyOptions,
|
companyOptions,
|
||||||
educationFieldOptions,
|
educationFieldOptions,
|
||||||
@ -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>
|
||||||
|
@ -86,8 +86,7 @@ export default function OfferAnalysis() {
|
|||||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||||
Result
|
Result
|
||||||
</h5>
|
</h5>
|
||||||
|
<div>
|
||||||
<div className="mx-40">
|
|
||||||
<Tabs
|
<Tabs
|
||||||
label="Result Navigation"
|
label="Result Navigation"
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
@ -10,9 +10,10 @@ import { PlusIcon } from '@heroicons/react/20/solid';
|
|||||||
import { TrashIcon } from '@heroicons/react/24/outline';
|
import { TrashIcon } from '@heroicons/react/24/outline';
|
||||||
import { Button } from '@tih/ui';
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
import FormSelect from './FormSelect';
|
import FormMonthYearPicker from './components/FormMonthYearPicker';
|
||||||
import FormTextArea from './FormTextArea';
|
import FormSelect from './components/FormSelect';
|
||||||
import FormTextInput from './FormTextInput';
|
import FormTextArea from './components/FormTextArea';
|
||||||
|
import FormTextInput from './components/FormTextInput';
|
||||||
import {
|
import {
|
||||||
companyOptions,
|
companyOptions,
|
||||||
internshipCycleOptions,
|
internshipCycleOptions,
|
||||||
@ -20,9 +21,9 @@ import {
|
|||||||
titleOptions,
|
titleOptions,
|
||||||
yearOptions,
|
yearOptions,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import type { FullTimeOfferFormData, InternshipOfferFormData } 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;
|
||||||
@ -34,7 +35,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
remove,
|
remove,
|
||||||
}: FullTimeOfferDetailsFormProps) {
|
}: FullTimeOfferDetailsFormProps) {
|
||||||
const { register } = useFormContext<{
|
const { register } = useFormContext<{
|
||||||
offers: Array<FullTimeOfferFormData>;
|
offers: Array<OfferDetailsFormData>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -81,10 +82,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.location`, { required: true })}
|
{...register(`offers.${index}.location`, { required: true })}
|
||||||
/>
|
/>
|
||||||
<FormTextInput
|
<FormMonthYearPicker
|
||||||
label="Month Received"
|
|
||||||
placeholder="MMM/YYYY"
|
|
||||||
required={true}
|
|
||||||
{...register(`offers.${index}.monthYearReceived`, { required: true })}
|
{...register(`offers.${index}.monthYearReceived`, { required: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -110,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>
|
||||||
@ -121,7 +120,9 @@ function FullTimeOfferDetailsForm({
|
|||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Currency"
|
label="Currency"
|
||||||
options={CURRENCY_OPTIONS}
|
options={CURRENCY_OPTIONS}
|
||||||
{...register(`offers.${index}.job.base.currency`)}
|
{...register(`offers.${index}.job.base.currency`, {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
endAddOnType="element"
|
endAddOnType="element"
|
||||||
@ -131,7 +132,10 @@ function FullTimeOfferDetailsForm({
|
|||||||
startAddOn="$"
|
startAddOn="$"
|
||||||
startAddOnType="label"
|
startAddOnType="label"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`offers.${index}.job.base.value`)}
|
{...register(`offers.${index}.job.base.value`, {
|
||||||
|
required: true,
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
endAddOn={
|
endAddOn={
|
||||||
@ -140,7 +144,9 @@ function FullTimeOfferDetailsForm({
|
|||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Currency"
|
label="Currency"
|
||||||
options={CURRENCY_OPTIONS}
|
options={CURRENCY_OPTIONS}
|
||||||
{...register(`offers.${index}.job.bonus.currency`)}
|
{...register(`offers.${index}.job.bonus.currency`, {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
endAddOnType="element"
|
endAddOnType="element"
|
||||||
@ -150,7 +156,10 @@ function FullTimeOfferDetailsForm({
|
|||||||
startAddOn="$"
|
startAddOn="$"
|
||||||
startAddOnType="label"
|
startAddOnType="label"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`offers.${index}.job.bonus.value`)}
|
{...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">
|
||||||
@ -161,7 +170,9 @@ function FullTimeOfferDetailsForm({
|
|||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Currency"
|
label="Currency"
|
||||||
options={CURRENCY_OPTIONS}
|
options={CURRENCY_OPTIONS}
|
||||||
{...register(`offers.${index}.job.stocks.currency`)}
|
{...register(`offers.${index}.job.stocks.currency`, {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
endAddOnType="element"
|
endAddOnType="element"
|
||||||
@ -171,7 +182,10 @@ function FullTimeOfferDetailsForm({
|
|||||||
startAddOn="$"
|
startAddOn="$"
|
||||||
startAddOnType="label"
|
startAddOnType="label"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`offers.${index}.job.stocks.value`)}
|
{...register(`offers.${index}.job.stocks.value`, {
|
||||||
|
required: true,
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
@ -251,7 +265,7 @@ function InternshipOfferDetailsForm({
|
|||||||
remove,
|
remove,
|
||||||
}: InternshipOfferDetailsFormProps) {
|
}: InternshipOfferDetailsFormProps) {
|
||||||
const { register } = useFormContext<{
|
const { register } = useFormContext<{
|
||||||
offers: Array<InternshipOfferFormData>;
|
offers: Array<OfferDetailsFormData>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -262,13 +276,19 @@ function InternshipOfferDetailsForm({
|
|||||||
label="Title"
|
label="Title"
|
||||||
options={titleOptions}
|
options={titleOptions}
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.job.title`)}
|
{...register(`offers.${index}.job.title`, {
|
||||||
|
minLength: 1,
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
label="Focus / Specialization"
|
label="Focus / Specialization"
|
||||||
placeholder="e.g. Front End"
|
placeholder="e.g. Front End"
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.job.specialization`)}
|
{...register(`offers.${index}.job.specialization`, {
|
||||||
|
minLength: 1,
|
||||||
|
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">
|
||||||
@ -277,40 +297,44 @@ 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-3 space-x-3">
|
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||||
<FormTextInput
|
|
||||||
label="Date Received"
|
|
||||||
placeholder="MMM/YYYY"
|
|
||||||
required={true}
|
|
||||||
{...register(`offers.${index}.monthYearReceived`)}
|
|
||||||
/>
|
|
||||||
<FormSelect
|
<FormSelect
|
||||||
display="block"
|
display="block"
|
||||||
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 className="mb-5 flex items-center space-x-9">
|
||||||
|
<p className="text-sm">Date received:</p>
|
||||||
|
<FormMonthYearPicker
|
||||||
|
{...register(`offers.${index}.monthYearReceived`, { required: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
@ -321,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"
|
||||||
@ -331,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">
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
import MonthYearPicker from '~/components/shared/MonthYearPicker';
|
||||||
|
|
||||||
|
import { getCurrentMonth, getCurrentYear } from '../../../../utils/offers/time';
|
||||||
|
|
||||||
|
type MonthYearPickerProps = ComponentProps<typeof MonthYearPicker>;
|
||||||
|
|
||||||
|
type FormMonthYearPickerProps = Omit<
|
||||||
|
MonthYearPickerProps,
|
||||||
|
'onChange' | 'value'
|
||||||
|
> & {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FormMonthYearPicker({
|
||||||
|
name,
|
||||||
|
...rest
|
||||||
|
}: FormMonthYearPickerProps) {
|
||||||
|
const { setValue } = useFormContext();
|
||||||
|
|
||||||
|
const value = useWatch({
|
||||||
|
defaultValue: { month: getCurrentMonth(), year: getCurrentYear() },
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MonthYearPicker
|
||||||
|
{...(rest as MonthYearPickerProps)}
|
||||||
|
value={value}
|
||||||
|
onChange={(val) => {
|
||||||
|
setValue(name, val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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,4 +1,6 @@
|
|||||||
/* eslint-disable no-shadow */
|
/* eslint-disable no-shadow */
|
||||||
|
import type { MonthYear } from '../shared/MonthYearPicker';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Offer Profile
|
* Offer Profile
|
||||||
*/
|
*/
|
||||||
@ -18,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;
|
||||||
};
|
};
|
||||||
@ -33,16 +35,6 @@ type FullTimeJobData = {
|
|||||||
totalCompensation: Money;
|
totalCompensation: Money;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FullTimeOfferFormData = {
|
|
||||||
comments: string;
|
|
||||||
companyId: string;
|
|
||||||
job: FullTimeJobData;
|
|
||||||
jobType: string;
|
|
||||||
location: string;
|
|
||||||
monthYearReceived: string;
|
|
||||||
negotiationStrategy: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type InternshipJobData = {
|
type InternshipJobData = {
|
||||||
internshipCycle: string;
|
internshipCycle: string;
|
||||||
monthlySalary: Money;
|
monthlySalary: Money;
|
||||||
@ -51,17 +43,22 @@ type InternshipJobData = {
|
|||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InternshipOfferFormData = {
|
export type OfferDetailsFormData = {
|
||||||
comments: string;
|
comments: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
job: InternshipJobData;
|
job: FullTimeJobData | InternshipJobData;
|
||||||
jobType: string;
|
jobType: string;
|
||||||
location: string;
|
location: string;
|
||||||
monthYearReceived: string;
|
monthYearReceived: MonthYear;
|
||||||
negotiationStrategy: string;
|
negotiationStrategy: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OfferDetailsFormData = FullTimeOfferFormData | InternshipOfferFormData;
|
export type OfferDetailsPostData = Omit<
|
||||||
|
OfferDetailsFormData,
|
||||||
|
'monthYearReceived'
|
||||||
|
> & {
|
||||||
|
monthYearReceived: Date;
|
||||||
|
};
|
||||||
|
|
||||||
type SpecificYoe = {
|
type SpecificYoe = {
|
||||||
domain: string;
|
domain: string;
|
||||||
@ -98,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;
|
||||||
};
|
};
|
||||||
@ -108,3 +105,8 @@ export type SubmitOfferFormData = {
|
|||||||
background: BackgroundFormData;
|
background: BackgroundFormData;
|
||||||
offers: Array<OfferDetailsFormData>;
|
offers: Array<OfferDetailsFormData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OfferPostData = {
|
||||||
|
background: BackgroundFormData;
|
||||||
|
offers: Array<OfferDetailsPostData>;
|
||||||
|
};
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
export function formatDate(value: Date | number | string) {
|
|
||||||
const date = new Date(value);
|
|
||||||
// Const day = date.toLocaleString('default', { day: '2-digit' });
|
|
||||||
const month = date.toLocaleString('default', { month: 'short' });
|
|
||||||
const year = date.toLocaleString('default', { year: 'numeric' });
|
|
||||||
return `${month} ${year}`;
|
|
||||||
}
|
|
@ -3,9 +3,11 @@ import { Select } from '@tih/ui';
|
|||||||
|
|
||||||
import OffersTable from '~/components/offers/OffersTable';
|
import OffersTable from '~/components/offers/OffersTable';
|
||||||
import OffersTitle from '~/components/offers/OffersTitle';
|
import OffersTitle from '~/components/offers/OffersTitle';
|
||||||
|
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||||
|
|
||||||
export default function OffersHomePage() {
|
export default function OffersHomePage() {
|
||||||
const [jobTitleFilter, setjobTitleFilter] = useState('Software engineers');
|
const [jobTitleFilter, setjobTitleFilter] = useState('Software engineers');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [companyFilter, setCompanyFilter] = useState('All companies');
|
const [companyFilter, setCompanyFilter] = useState('All companies');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -13,7 +15,7 @@ export default function OffersHomePage() {
|
|||||||
<div className="grid-rows grid h-1/2 bg-gray-100">
|
<div className="grid-rows grid h-1/2 bg-gray-100">
|
||||||
<OffersTitle />
|
<OffersTitle />
|
||||||
<div className="flex items-start justify-center">
|
<div className="flex items-start justify-center">
|
||||||
<div className="mt-4 flex items-center">
|
<div className="mt-4 flex items-end">
|
||||||
Viewing offers for
|
Viewing offers for
|
||||||
<div className="mx-4">
|
<div className="mx-4">
|
||||||
<Select
|
<Select
|
||||||
@ -43,25 +45,8 @@ export default function OffersHomePage() {
|
|||||||
</div>
|
</div>
|
||||||
in
|
in
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<Select
|
<CompaniesTypeahead
|
||||||
isLabelHidden={true}
|
onSelect={({ value }) => setCompanyFilter(value)}
|
||||||
label="Select a company"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: 'All companies',
|
|
||||||
value: 'All companies',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Shopee',
|
|
||||||
value: 'Shopee',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Meta',
|
|
||||||
value: 'Meta',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={companyFilter}
|
|
||||||
onChange={setCompanyFilter}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,15 @@ 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 {
|
||||||
|
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() {
|
function Breadcrumbs() {
|
||||||
return (
|
return (
|
||||||
@ -23,53 +31,94 @@ const defaultOfferValues = {
|
|||||||
{
|
{
|
||||||
comments: '',
|
comments: '',
|
||||||
companyId: '',
|
companyId: '',
|
||||||
job: {
|
job: {},
|
||||||
base: {
|
|
||||||
currency: 'USD',
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
bonus: {
|
|
||||||
currency: 'USD',
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
level: '',
|
|
||||||
specialization: '',
|
|
||||||
stocks: {
|
|
||||||
currency: 'USD',
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
title: '',
|
|
||||||
totalCompensation: {
|
|
||||||
currency: 'USD',
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
jobType: 'FULLTIME',
|
jobType: 'FULLTIME',
|
||||||
location: '',
|
location: '',
|
||||||
monthYearReceived: '',
|
monthYearReceived: {
|
||||||
|
month: getCurrentMonth() as Month,
|
||||||
|
year: getCurrentYear(),
|
||||||
|
},
|
||||||
negotiationStrategy: '',
|
negotiationStrategy: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FormStep = {
|
||||||
|
component: JSX.Element;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrevious: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
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<SubmitOfferFormData>({
|
||||||
defaultValues: defaultOfferValues,
|
defaultValues: defaultOfferValues,
|
||||||
|
mode: 'all',
|
||||||
});
|
});
|
||||||
|
const { handleSubmit, trigger } = formMethods;
|
||||||
|
|
||||||
const nextStep = () => setFormStep(formStep + 1);
|
const formSteps: Array<FormStep> = [
|
||||||
const previousStep = () => setFormStep(formStep - 1);
|
{
|
||||||
|
component: <OfferDetailsForm key={0} />,
|
||||||
const formComponents = [
|
hasNext: true,
|
||||||
<OfferDetailsForm key={0} />,
|
hasPrevious: false,
|
||||||
<BackgroundForm key={1} />,
|
},
|
||||||
<OfferAnalysis key={2} />,
|
{
|
||||||
<OfferProfileSave key={3} />,
|
component: <BackgroundForm key={1} />,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrevious: true,
|
||||||
|
},
|
||||||
|
{ component: <OfferAnalysis key={2} />, hasNext: true, hasPrevious: false },
|
||||||
|
{
|
||||||
|
component: <OfferProfileSave key={3} />,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrevious: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<SubmitOfferFormData> = async () => {
|
const nextStep = async (currStep: number) => {
|
||||||
nextStep();
|
if (currStep === 0) {
|
||||||
|
const result = await trigger('offers');
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFormStep(formStep + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousStep = () => setFormStep(formStep - 1);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 (
|
||||||
@ -78,16 +127,17 @@ export default function OffersSubmissionPage() {
|
|||||||
<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 />
|
<Breadcrumbs />
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form onSubmit={formMethods.handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{formComponents[formStep]}
|
{formSteps[formStep].component}
|
||||||
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
||||||
{(formStep === 0 || formStep === 2) && (
|
{formSteps[formStep].hasNext && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
disabled={false}
|
||||||
icon={ArrowRightIcon}
|
icon={ArrowRightIcon}
|
||||||
label="Next"
|
label="Next"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={nextStep}
|
onClick={() => nextStep(formStep)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -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,
|
56
apps/portal/src/utils/offers/form.tsx
Normal file
56
apps/portal/src/utils/offers/form.tsx
Normal file
@ -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;
|
||||||
|
}
|
25
apps/portal/src/utils/offers/time.tsx
Normal file
25
apps/portal/src/utils/offers/time.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { getMonth, getYear } from 'date-fns';
|
||||||
|
|
||||||
|
import type { MonthYear } from '~/components/shared/MonthYearPicker';
|
||||||
|
|
||||||
|
export function formatDate(value: Date | number | string) {
|
||||||
|
const date = new Date(value);
|
||||||
|
// Const day = date.toLocaleString('default', { day: '2-digit' });
|
||||||
|
const month = date.toLocaleString('default', { month: 'short' });
|
||||||
|
const year = date.toLocaleString('default', { year: 'numeric' });
|
||||||
|
return `${month} ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMonthYear({ month, year }: MonthYear) {
|
||||||
|
const monthString = month < 10 ? month.toString() : `0${month}`;
|
||||||
|
const yearString = year.toString();
|
||||||
|
return `${monthString}/${yearString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentMonth() {
|
||||||
|
return getMonth(Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentYear() {
|
||||||
|
return getYear(Date.now());
|
||||||
|
}
|
Reference in New Issue
Block a user