[offers][feat] Integrate offers create API and fix form UI (#358)

pull/360/head
Ai Ling 2 years ago committed by GitHub
parent 34c8c7d605
commit 4330fb5448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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',
},
];

@ -1,9 +1,9 @@
import { useFormContext, useWatch } from 'react-hook-form';
import { Collapsible, RadioList } from '@tih/ui';
import FormRadioList from './FormRadioList';
import FormSelect from './FormSelect';
import FormTextInput from './FormTextInput';
import FormRadioList from './components/FormRadioList';
import FormSelect from './components/FormSelect';
import FormTextInput from './components/FormTextInput';
import {
companyOptions,
educationFieldOptions,
@ -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>

@ -86,8 +86,7 @@ export default function OfferAnalysis() {
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
Result
</h5>
<div className="mx-40">
<div>
<Tabs
label="Result Navigation"
tabs={tabs}

@ -10,9 +10,10 @@ import { PlusIcon } from '@heroicons/react/20/solid';
import { TrashIcon } from '@heroicons/react/24/outline';
import { Button } from '@tih/ui';
import FormSelect from './FormSelect';
import FormTextArea from './FormTextArea';
import FormTextInput from './FormTextInput';
import FormMonthYearPicker from './components/FormMonthYearPicker';
import FormSelect from './components/FormSelect';
import FormTextArea from './components/FormTextArea';
import FormTextInput from './components/FormTextInput';
import {
companyOptions,
internshipCycleOptions,
@ -20,9 +21,9 @@ import {
titleOptions,
yearOptions,
} from '../constants';
import type { FullTimeOfferFormData, InternshipOfferFormData } from '../types';
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;
@ -34,7 +35,7 @@ function FullTimeOfferDetailsForm({
remove,
}: FullTimeOfferDetailsFormProps) {
const { register } = useFormContext<{
offers: Array<FullTimeOfferFormData>;
offers: Array<OfferDetailsFormData>;
}>();
return (
@ -81,10 +82,7 @@ function FullTimeOfferDetailsForm({
required={true}
{...register(`offers.${index}.location`, { required: true })}
/>
<FormTextInput
label="Month Received"
placeholder="MMM/YYYY"
required={true}
<FormMonthYearPicker
{...register(`offers.${index}.monthYearReceived`, { required: true })}
/>
</div>
@ -110,6 +108,7 @@ function FullTimeOfferDetailsForm({
type="number"
{...register(`offers.${index}.job.totalCompensation.value`, {
required: true,
valueAsNumber: true,
})}
/>
</div>
@ -121,7 +120,9 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true}
label="Currency"
options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.base.currency`)}
{...register(`offers.${index}.job.base.currency`, {
required: true,
})}
/>
}
endAddOnType="element"
@ -131,7 +132,10 @@ function FullTimeOfferDetailsForm({
startAddOn="$"
startAddOnType="label"
type="number"
{...register(`offers.${index}.job.base.value`)}
{...register(`offers.${index}.job.base.value`, {
required: true,
valueAsNumber: true,
})}
/>
<FormTextInput
endAddOn={
@ -140,7 +144,9 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true}
label="Currency"
options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.bonus.currency`)}
{...register(`offers.${index}.job.bonus.currency`, {
required: true,
})}
/>
}
endAddOnType="element"
@ -150,7 +156,10 @@ function FullTimeOfferDetailsForm({
startAddOn="$"
startAddOnType="label"
type="number"
{...register(`offers.${index}.job.bonus.value`)}
{...register(`offers.${index}.job.bonus.value`, {
required: true,
valueAsNumber: true,
})}
/>
</div>
<div className="mb-5 grid grid-cols-2 space-x-3">
@ -161,7 +170,9 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true}
label="Currency"
options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.stocks.currency`)}
{...register(`offers.${index}.job.stocks.currency`, {
required: true,
})}
/>
}
endAddOnType="element"
@ -171,7 +182,10 @@ function FullTimeOfferDetailsForm({
startAddOn="$"
startAddOnType="label"
type="number"
{...register(`offers.${index}.job.stocks.value`)}
{...register(`offers.${index}.job.stocks.value`, {
required: true,
valueAsNumber: true,
})}
/>
</div>
<div className="mb-5">
@ -251,7 +265,7 @@ function InternshipOfferDetailsForm({
remove,
}: InternshipOfferDetailsFormProps) {
const { register } = useFormContext<{
offers: Array<InternshipOfferFormData>;
offers: Array<OfferDetailsFormData>;
}>();
return (
@ -262,13 +276,19 @@ function InternshipOfferDetailsForm({
label="Title"
options={titleOptions}
required={true}
{...register(`offers.${index}.job.title`)}
{...register(`offers.${index}.job.title`, {
minLength: 1,
required: true,
})}
/>
<FormTextInput
label="Focus / Specialization"
placeholder="e.g. Front End"
required={true}
{...register(`offers.${index}.job.specialization`)}
{...register(`offers.${index}.job.specialization`, {
minLength: 1,
required: true,
})}
/>
</div>
<div className="mb-5 grid grid-cols-2 space-x-3">
@ -277,40 +297,44 @@ 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-3 space-x-3">
<FormTextInput
label="Date Received"
placeholder="MMM/YYYY"
required={true}
{...register(`offers.${index}.monthYearReceived`)}
/>
<div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect
display="block"
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">
<p className="text-sm">Date received:</p>
<FormMonthYearPicker
{...register(`offers.${index}.monthYearReceived`, { required: true })}
/>
</div>
<div className="mb-5">
@ -321,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"
@ -331,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">

@ -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 { 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,4 +1,6 @@
/* eslint-disable no-shadow */
import type { MonthYear } from '../shared/MonthYearPicker';
/*
* Offer Profile
*/
@ -18,7 +20,7 @@ export enum EducationBackgroundType {
SelfTaught = 'Self-taught',
}
type Money = {
export type Money = {
currency: string;
value: number;
};
@ -33,16 +35,6 @@ type FullTimeJobData = {
totalCompensation: Money;
};
export type FullTimeOfferFormData = {
comments: string;
companyId: string;
job: FullTimeJobData;
jobType: string;
location: string;
monthYearReceived: string;
negotiationStrategy: string;
};
type InternshipJobData = {
internshipCycle: string;
monthlySalary: Money;
@ -51,17 +43,22 @@ type InternshipJobData = {
title: string;
};
export type InternshipOfferFormData = {
export type OfferDetailsFormData = {
comments: string;
companyId: string;
job: InternshipJobData;
job: FullTimeJobData | InternshipJobData;
jobType: string;
location: string;
monthYearReceived: string;
monthYearReceived: MonthYear;
negotiationStrategy: string;
};
type OfferDetailsFormData = FullTimeOfferFormData | InternshipOfferFormData;
export type OfferDetailsPostData = Omit<
OfferDetailsFormData,
'monthYearReceived'
> & {
monthYearReceived: Date;
};
type SpecificYoe = {
domain: string;
@ -98,8 +95,8 @@ type Education = {
};
type BackgroundFormData = {
education: Education;
experience: Experience;
educations: Array<Education>;
experiences: Array<Experience>;
specificYoes: Array<SpecificYoe>;
totalYoe: number;
};
@ -108,3 +105,8 @@ export type SubmitOfferFormData = {
background: BackgroundFormData;
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 OffersTitle from '~/components/offers/OffersTitle';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
export default function OffersHomePage() {
const [jobTitleFilter, setjobTitleFilter] = useState('Software engineers');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [companyFilter, setCompanyFilter] = useState('All companies');
return (
@ -13,7 +15,7 @@ export default function OffersHomePage() {
<div className="grid-rows grid h-1/2 bg-gray-100">
<OffersTitle />
<div className="flex items-start justify-center">
<div className="mt-4 flex items-center">
<div className="mt-4 flex items-end">
Viewing offers for
<div className="mx-4">
<Select
@ -43,25 +45,8 @@ export default function OffersHomePage() {
</div>
in
<div className="ml-4">
<Select
isLabelHidden={true}
label="Select a company"
options={[
{
label: 'All companies',
value: 'All companies',
},
{
label: 'Shopee',
value: 'Shopee',
},
{
label: 'Meta',
value: 'Meta',
},
]}
value={companyFilter}
onChange={setCompanyFilter}
<CompaniesTypeahead
onSelect={({ value }) => setCompanyFilter(value)}
/>
</div>
</div>

@ -8,7 +8,15 @@ 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 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 (
@ -23,53 +31,94 @@ const defaultOfferValues = {
{
comments: '',
companyId: '',
job: {
base: {
currency: 'USD',
value: 0,
},
bonus: {
currency: 'USD',
value: 0,
},
level: '',
specialization: '',
stocks: {
currency: 'USD',
value: 0,
},
title: '',
totalCompensation: {
currency: 'USD',
value: 0,
},
},
job: {},
jobType: 'FULLTIME',
location: '',
monthYearReceived: '',
monthYearReceived: {
month: getCurrentMonth() as Month,
year: getCurrentYear(),
},
negotiationStrategy: '',
},
],
};
type FormStep = {
component: JSX.Element;
hasNext: boolean;
hasPrevious: boolean;
};
export default function OffersSubmissionPage() {
const [formStep, setFormStep] = useState(0);
const formMethods = useForm<SubmitOfferFormData>({
defaultValues: defaultOfferValues,
mode: 'all',
});
const { handleSubmit, trigger } = formMethods;
const formSteps: Array<FormStep> = [
{
component: <OfferDetailsForm key={0} />,
hasNext: true,
hasPrevious: false,
},
{
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 nextStep = async (currStep: number) => {
if (currStep === 0) {
const result = await trigger('offers');
if (!result) {
return;
}
}
setFormStep(formStep + 1);
};
const nextStep = () => setFormStep(formStep + 1);
const previousStep = () => setFormStep(formStep - 1);
const formComponents = [
<OfferDetailsForm key={0} />,
<BackgroundForm key={1} />,
<OfferAnalysis key={2} />,
<OfferProfileSave key={3} />,
];
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,
);
const onSubmit: SubmitHandler<SubmitOfferFormData> = async () => {
nextStep();
createMutation.mutate(postData);
};
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">
<Breadcrumbs />
<FormProvider {...formMethods}>
<form onSubmit={formMethods.handleSubmit(onSubmit)}>
{formComponents[formStep]}
<form onSubmit={handleSubmit(onSubmit)}>
{formSteps[formStep].component}
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
{(formStep === 0 || formStep === 2) && (
{formSteps[formStep].hasNext && (
<div className="flex justify-end">
<Button
disabled={false}
icon={ArrowRightIcon}
label="Next"
variant="secondary"
onClick={nextStep}
onClick={() => nextStep(formStep)}
/>
</div>
)}

@ -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;
}

@ -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());
}
Loading…
Cancel
Save