[offers][feat] Integrate offers analysis into offers submission (#398)

* [offers][fix] Fix minor issues in form

* [offers][fix] Use companies typeahead in form

* [offers][feat] Fix types and integrate offers analysis

* [offers][fix] Fix generate analysis API test
pull/395/head
Ai Ling 2 years ago committed by GitHub
parent 4fa350964f
commit 992d457b8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,27 @@
import type { Analysis } from '~/types/offers';
type OfferPercentileAnalysisProps = Readonly<{
companyName: string;
offerAnalysis: Analysis;
tab: string;
}>;
export default function OfferPercentileAnalysis({
tab,
companyName,
offerAnalysis: { noOfOffers, percentile },
}: OfferPercentileAnalysisProps) {
return tab === 'Overall' ? (
<p>
Your highest offer is from {companyName}, which is {percentile} percentile
out of {noOfOffers} offers received for the same job type, same level, and
same YOE(+/-1) in the last year.
</p>
) : (
<p>
Your offer from {companyName} is {percentile} percentile out of{' '}
{noOfOffers} offers received in {companyName} for the same job type, same
level, and same YOE(+/-1) in the last year.
</p>
);
}

@ -0,0 +1,61 @@
import { UserCircleIcon } from '@heroicons/react/24/outline';
import { HorizontalDivider } from '~/../../../packages/ui/dist';
import { formatDate } from '~/utils/offers/time';
import { JobType } from '../types';
import type { AnalysisOffer } from '~/types/offers';
type OfferProfileCardProps = Readonly<{
offerProfile: AnalysisOffer;
}>;
export default function OfferProfileCard({
offerProfile: {
company,
income,
profileName,
totalYoe,
level,
monthYearReceived,
jobType,
location,
title,
previousCompanies,
},
}: OfferProfileCardProps) {
return (
<div className="my-5 block rounded-lg border p-4">
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
<div className="col-span-1">
<UserCircleIcon width={50} />
</div>
<div className="col-span-10">
<p className="text-sm font-semibold">{profileName}</p>
<p className="text-xs ">Previous company: {previousCompanies[0]}</p>
<p className="text-xs ">YOE: {totalYoe} year(s)</p>
</div>
</div>
<HorizontalDivider />
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
<div className="col-span-1 row-span-3">
<p className="text-sm font-semibold">{title}</p>
<p className="text-xs ">
Company: {company.name}, {location}
</p>
<p className="text-xs ">Level: {level}</p>
</div>
<div className="col-span-1 row-span-3">
<p className="text-end text-sm">{formatDate(monthYearReceived)}</p>
<p className="text-end text-xl">
{jobType === JobType.FullTime
? `$${income} / year`
: `$${income} / month`}
</p>
</div>
</div>
</div>
);
}

@ -22,29 +22,6 @@ export const titleOptions = [
}, },
]; ];
export const companyOptions = [
{
label: 'Amazon',
value: 'cl93patjt0000txewdi601mub',
},
{
label: 'Microsoft',
value: 'cl93patjt0001txewkglfjsro',
},
{
label: 'Apple',
value: 'cl93patjt0002txewf3ug54m8',
},
{
label: 'Google',
value: 'cl93patjt0003txewyiaky7xx',
},
{
label: 'Meta',
value: 'cl93patjt0004txew88wkcqpu',
},
];
export const locationOptions = [ export const locationOptions = [
{ {
label: 'Singapore, Singapore', label: 'Singapore, Singapore',
@ -86,26 +63,26 @@ export const internshipCycleOptions = [
export const yearOptions = [ export const yearOptions = [
{ {
label: '2021', label: '2021',
value: '2021', value: 2021,
}, },
{ {
label: '2022', label: '2022',
value: '2022', value: 2022,
}, },
{ {
label: '2023', label: '2023',
value: '2023', value: 2023,
}, },
{ {
label: '2024', label: '2024',
value: '2024', value: 2024,
}, },
]; ];
export const educationLevelOptions = Object.entries( export const educationLevelOptions = Object.entries(
EducationBackgroundType, EducationBackgroundType,
).map(([key, value]) => ({ ).map(([, value]) => ({
label: key, label: value,
value, value,
})); }));
@ -129,3 +106,5 @@ export enum FieldError {
Number = 'Please fill in a number in this field.', Number = 'Please fill in a number in this field.',
Required = 'Please fill in this field.', Required = 'Please fill in this field.',
} }
export const OVERALL_TAB = 'Overall';

@ -4,7 +4,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
import MonthYearPicker from '~/components/shared/MonthYearPicker'; import MonthYearPicker from '~/components/shared/MonthYearPicker';
import { getCurrentMonth, getCurrentYear } from '../../../../utils/offers/time'; import { getCurrentMonth, getCurrentYear } from '../../../utils/offers/time';
type MonthYearPickerProps = ComponentProps<typeof MonthYearPicker>; type MonthYearPickerProps = ComponentProps<typeof MonthYearPicker>;

@ -1,100 +0,0 @@
import { useState } from 'react';
import { UserCircleIcon } from '@heroicons/react/20/solid';
import { HorizontalDivider, Tabs } from '@tih/ui';
const tabs = [
{
label: 'Overall',
value: 'overall',
},
{
label: 'Shopee',
value: 'company-id',
},
];
function OfferPercentileAnalysis() {
const result = {
company: 'Shopee',
numberOfOffers: 105,
percentile: 56,
};
return (
<p>
Your highest offer is from {result.company}, which is {result.percentile}{' '}
percentile out of {result.numberOfOffers} offers received in Singapore for
the same job type, same level, and same YOE in the last year.
</p>
);
}
function OfferProfileCard() {
return (
<div className="my-5 block rounded-lg border p-4">
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
<div className="col-span-1">
<UserCircleIcon width={50} />
</div>
<div className="col-span-10">
<p className="text-sm font-semibold">profile-name</p>
<p className="text-xs ">Previous company: Meta, Singapore</p>
<p className="text-xs ">YOE: 4 years</p>
</div>
</div>
<HorizontalDivider />
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
<div className="col-span-1 row-span-3">
<p className="text-sm font-semibold">Software engineer</p>
<p className="text-xs ">Company: Google, Singapore</p>
<p className="text-xs ">Level: G4</p>
</div>
<div className="col-span-1 row-span-3">
<p className="text-end text-sm">Sept 2022</p>
<p className="text-end text-xl">$125,000 / year</p>
</div>
</div>
</div>
);
}
function TopOfferProfileList() {
return (
<>
<OfferProfileCard />
<OfferProfileCard />
</>
);
}
function OfferAnalysisContent() {
return (
<>
<OfferPercentileAnalysis />
<TopOfferProfileList />
</>
);
}
export default function OfferAnalysis() {
const [tab, setTab] = useState('Overall');
return (
<div>
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
Result
</h5>
<div>
<Tabs
label="Result Navigation"
tabs={tabs}
value={tab}
onChange={setTab}
/>
<HorizontalDivider className="mb-5" />
<OfferAnalysisContent />
</div>
</div>
);
}

@ -2,21 +2,28 @@ import { useFormContext, useWatch } from 'react-hook-form';
import { Collapsible, RadioList } from '@tih/ui'; import { Collapsible, RadioList } from '@tih/ui';
import { import {
companyOptions,
educationFieldOptions, educationFieldOptions,
educationLevelOptions, educationLevelOptions,
emptyOption,
FieldError,
locationOptions, locationOptions,
titleOptions, titleOptions,
} from '~/components/offers/constants'; } from '~/components/offers/constants';
import FormRadioList from '~/components/offers/forms/components/FormRadioList'; import type { BackgroundPostData } from '~/components/offers/types';
import FormSelect from '~/components/offers/forms/components/FormSelect';
import FormTextInput from '~/components/offers/forms/components/FormTextInput';
import { JobType } from '~/components/offers/types'; import { JobType } from '~/components/offers/types';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum'; import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum';
import FormRadioList from '../forms/FormRadioList';
import FormSelect from '../forms/FormSelect';
import FormTextInput from '../forms/FormTextInput';
function YoeSection() { function YoeSection() {
const { register } = useFormContext(); const { register, formState } = useFormContext<{
background: BackgroundPostData;
}>();
const backgroundFields = formState.errors.background;
return ( return (
<> <>
<h6 className="mb-2 text-left text-xl font-medium text-gray-400"> <h6 className="mb-2 text-left text-xl font-medium text-gray-400">
@ -26,53 +33,62 @@ function YoeSection() {
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5"> <div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="mb-2 grid grid-cols-3 space-x-3"> <div className="mb-2 grid grid-cols-3 space-x-3">
<FormTextInput <FormTextInput
errorMessage={backgroundFields?.totalYoe?.message}
label="Total YOE" label="Total YOE"
placeholder="0" placeholder="0"
required={true}
type="number" type="number"
{...register(`background.totalYoe`, { {...register(`background.totalYoe`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
</div> </div>
<div className="grid grid-cols-1 space-x-3"> <Collapsible label="Add specific YOEs by domain">
<Collapsible label="Add specific YOEs by domain"> <div className="mb-5 grid grid-cols-2 space-x-3">
<div className="mb-5 grid grid-cols-2 space-x-3"> <FormTextInput
<FormTextInput errorMessage={backgroundFields?.specificYoes?.[0]?.yoe?.message}
label="Specific YOE 1" label="Specific YOE 1"
type="number" type="number"
{...register(`background.specificYoes.0.yoe`, { {...register(`background.specificYoes.0.yoe`, {
valueAsNumber: true, min: { message: FieldError.NonNegativeNumber, value: 0 },
})} valueAsNumber: true,
/> })}
<FormTextInput />
label="Specific Domain 1" <FormTextInput
placeholder="e.g. Frontend" label="Specific Domain 1"
{...register(`background.specificYoes.0.domain`)} placeholder="e.g. Frontend"
/> {...register(`background.specificYoes.0.domain`)}
</div> />
<div className="mb-5 grid grid-cols-2 space-x-3"> </div>
<FormTextInput <div className="mb-5 grid grid-cols-2 space-x-3">
label="Specific YOE 2" <FormTextInput
type="number" errorMessage={backgroundFields?.specificYoes?.[1]?.yoe?.message}
{...register(`background.specificYoes.1.yoe`, { label="Specific YOE 2"
valueAsNumber: true, type="number"
})} {...register(`background.specificYoes.1.yoe`, {
/> min: { message: FieldError.NonNegativeNumber, value: 0 },
<FormTextInput valueAsNumber: true,
label="Specific Domain 2" })}
placeholder="e.g. Backend" />
{...register(`background.specificYoes.1.domain`)} <FormTextInput
/> label="Specific Domain 2"
</div> placeholder="e.g. Backend"
</Collapsible> {...register(`background.specificYoes.1.domain`)}
</div> />
</div>
</Collapsible>
</div> </div>
</> </>
); );
} }
function FullTimeJobFields() { function FullTimeJobFields() {
const { register } = useFormContext(); const { register, setValue, formState } = useFormContext<{
background: BackgroundPostData;
}>();
const experiencesField = formState.errors.background?.experiences?.[0];
return ( return (
<> <>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
@ -80,14 +96,16 @@ function FullTimeJobFields() {
display="block" display="block"
label="Title" label="Title"
options={titleOptions} options={titleOptions}
placeholder={emptyOption}
{...register(`background.experiences.0.title`)} {...register(`background.experiences.0.title`)}
/> />
<FormSelect <div>
display="block" <CompaniesTypeahead
label="Company" onSelect={({ value }) =>
options={companyOptions} setValue(`background.experiences.0.companyId`, value)
{...register(`background.experiences.0.companyId`)} }
/> />
</div>
</div> </div>
<div className="mb-5 grid grid-cols-1 space-x-3"> <div className="mb-5 grid grid-cols-1 space-x-3">
<FormTextInput <FormTextInput
@ -103,12 +121,14 @@ function FullTimeJobFields() {
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={experiencesField?.totalCompensation?.value?.message}
label="Total Compensation (Annual)" label="Total Compensation (Annual)"
placeholder="0.00" placeholder="0.00"
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`background.experiences.0.totalCompensation.value`, { {...register(`background.experiences.0.totalCompensation.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -134,9 +154,11 @@ function FullTimeJobFields() {
{...register(`background.experiences.0.location`)} {...register(`background.experiences.0.location`)}
/> />
<FormTextInput <FormTextInput
errorMessage={experiencesField?.durationInMonths?.message}
label="Duration (months)" label="Duration (months)"
type="number" type="number"
{...register(`background.experiences.0.durationInMonths`, { {...register(`background.experiences.0.durationInMonths`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -147,7 +169,11 @@ function FullTimeJobFields() {
} }
function InternshipJobFields() { function InternshipJobFields() {
const { register } = useFormContext(); const { register, setValue, formState } = useFormContext<{
background: BackgroundPostData;
}>();
const experiencesField = formState.errors.background?.experiences?.[0];
return ( return (
<> <>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
@ -155,14 +181,16 @@ function InternshipJobFields() {
display="block" display="block"
label="Title" label="Title"
options={titleOptions} options={titleOptions}
placeholder={emptyOption}
{...register(`background.experiences.0.title`)} {...register(`background.experiences.0.title`)}
/> />
<FormSelect <div>
display="block" <CompaniesTypeahead
label="Company" onSelect={({ value }) =>
options={companyOptions} setValue(`background.experiences.0.companyId`, value)
{...register(`background.experiences.0.company`)} }
/> />
</div>
</div> </div>
<div className="mb-5 grid grid-cols-1 space-x-3"> <div className="mb-5 grid grid-cols-1 space-x-3">
<FormTextInput <FormTextInput
@ -176,12 +204,16 @@ function InternshipJobFields() {
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={experiencesField?.monthlySalary?.value?.message}
label="Salary (Monthly)" label="Salary (Monthly)"
placeholder="0.00" placeholder="0.00"
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`background.experiences.0.monthlySalary.value`)} {...register(`background.experiences.0.monthlySalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
valueAsNumber: true,
})}
/> />
</div> </div>
<Collapsible label="Add more details"> <Collapsible label="Add more details">
@ -195,6 +227,7 @@ function InternshipJobFields() {
display="block" display="block"
label="Location" label="Location"
options={locationOptions} options={locationOptions}
placeholder={emptyOption}
{...register(`background.experiences.0.location`)} {...register(`background.experiences.0.location`)}
/> />
</div> </div>
@ -231,7 +264,7 @@ function CurrentJobSection() {
<RadioList.Item <RadioList.Item
key="Internship" key="Internship"
label="Internship" label="Internship"
value={JobType.Internship} value={JobType.Intern}
/> />
</FormRadioList> </FormRadioList>
</div> </div>
@ -258,12 +291,14 @@ function EducationSection() {
display="block" display="block"
label="Education Level" label="Education Level"
options={educationLevelOptions} options={educationLevelOptions}
placeholder={emptyOption}
{...register(`background.educations.0.type`)} {...register(`background.educations.0.type`)}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Field" label="Field"
options={educationFieldOptions} options={educationFieldOptions}
placeholder={emptyOption}
{...register(`background.educations.0.field`)} {...register(`background.educations.0.field`)}
/> />
</div> </div>
@ -287,9 +322,9 @@ export default function BackgroundForm() {
<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">
Help us better gauge your offers Help us better gauge your offers
</h5> </h5>
<h6 className="mx-10 mb-8 text-center text-lg font-light text-gray-600"> <h6 className="text-md mx-10 mb-8 text-center font-light text-gray-600">
This section is optional, but your background information helps us This section is mostly optional, but your background information helps
benchmark your offers. us benchmark your offers.
</h6> </h6>
<div> <div>
<YoeSection /> <YoeSection />

@ -0,0 +1,138 @@
import Error from 'next/error';
import { useEffect } from 'react';
import { useState } from 'react';
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import OfferPercentileAnalysis from '../analysis/OfferPercentileAnalysis';
import OfferProfileCard from '../analysis/OfferProfileCard';
import { OVERALL_TAB } from '../constants';
import type {
Analysis,
AnalysisHighestOffer,
ProfileAnalysis,
} from '~/types/offers';
type OfferAnalysisData = {
offer?: AnalysisHighestOffer;
offerAnalysis?: Analysis;
};
type OfferAnalysisContentProps = Readonly<{
analysis: OfferAnalysisData;
tab: string;
}>;
function OfferAnalysisContent({
analysis: { offer, offerAnalysis },
tab,
}: OfferAnalysisContentProps) {
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
return (
<p className="m-10">
You are the first to submit an offer for these companies! Check back
later when there are more submissions.
</p>
);
}
return (
<>
<OfferPercentileAnalysis
companyName={offer.company.name}
offerAnalysis={offerAnalysis}
tab={tab}
/>
{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => (
<OfferProfileCard
key={topPercentileOffer.id}
offerProfile={topPercentileOffer}
/>
))}
</>
);
}
type OfferAnalysisProps = Readonly<{
profileId?: string;
}>;
export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
const [tab, setTab] = useState(OVERALL_TAB);
const [allAnalysis, setAllAnalysis] = useState<ProfileAnalysis | null>(null);
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
useEffect(() => {
if (tab === OVERALL_TAB) {
setAnalysis({
offer: allAnalysis?.overallHighestOffer,
offerAnalysis: allAnalysis?.overallAnalysis,
});
} else {
setAnalysis({
offer: allAnalysis?.overallHighestOffer,
offerAnalysis: allAnalysis?.companyAnalysis[0],
});
}
}, [tab, allAnalysis]);
if (!profileId) {
return null;
}
const getAnalysisResult = trpc.useQuery(
['offers.analysis.get', { profileId }],
{
onError(error) {
console.error(error.message);
},
onSuccess(data) {
setAllAnalysis(data);
},
},
);
const tabOptions = [
{
label: OVERALL_TAB,
value: OVERALL_TAB,
},
{
label: allAnalysis?.overallHighestOffer.company.name || '',
value: allAnalysis?.overallHighestOffer.company.id || '',
},
];
return (
<>
{getAnalysisResult.isError && (
<Error
statusCode={404}
title="An error occurred while generating profile analysis."
/>
)}
{!getAnalysisResult.isError && analysis && (
<div>
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
Result
</h5>
{getAnalysisResult.isLoading ? (
<Spinner className="m-10" display="block" size="lg" />
) : (
<div>
<Tabs
label="Result Navigation"
tabs={tabOptions}
value={tab}
onChange={setTab}
/>
<HorizontalDivider className="mb-5" />
<OfferAnalysisContent analysis={analysis} tab={tab} />
</div>
)}
</div>
)}
</>
);
}

@ -1,5 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { FieldValues, UseFieldArrayReturn } from 'react-hook-form'; import type {
FieldValues,
UseFieldArrayRemove,
UseFieldArrayReturn,
} from 'react-hook-form';
import { useWatch } from 'react-hook-form'; import { useWatch } from 'react-hook-form';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFieldArray } from 'react-hook-form'; import { useFieldArray } from 'react-hook-form';
@ -7,17 +11,14 @@ import { PlusIcon } from '@heroicons/react/20/solid';
import { TrashIcon } from '@heroicons/react/24/outline'; import { TrashIcon } from '@heroicons/react/24/outline';
import { Button, Dialog } from '@tih/ui'; import { Button, Dialog } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import { import {
defaultFullTimeOfferValues, defaultFullTimeOfferValues,
defaultInternshipOfferValues, defaultInternshipOfferValues,
} from '~/pages/offers/submit'; } from '~/pages/offers/submit';
import FormMonthYearPicker from './components/FormMonthYearPicker';
import FormSelect from './components/FormSelect';
import FormTextArea from './components/FormTextArea';
import FormTextInput from './components/FormTextInput';
import { import {
companyOptions,
emptyOption, emptyOption,
FieldError, FieldError,
internshipCycleOptions, internshipCycleOptions,
@ -25,36 +26,40 @@ import {
titleOptions, titleOptions,
yearOptions, yearOptions,
} from '../constants'; } from '../constants';
import type { import FormMonthYearPicker from '../forms/FormMonthYearPicker';
FullTimeOfferDetailsFormData, import FormSelect from '../forms/FormSelect';
InternshipOfferDetailsFormData, import FormTextArea from '../forms/FormTextArea';
} from '../types'; import FormTextInput from '../forms/FormTextInput';
import type { OfferFormData } from '../types';
import { JobTypeLabel } from '../types'; import { JobTypeLabel } from '../types';
import { JobType } from '../types'; import { JobType } from '../types';
import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum'; import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum';
type FullTimeOfferDetailsFormProps = Readonly<{ type FullTimeOfferDetailsFormProps = Readonly<{
index: number; index: number;
setDialogOpen: (isOpen: boolean) => void; remove: UseFieldArrayRemove;
}>; }>;
function FullTimeOfferDetailsForm({ function FullTimeOfferDetailsForm({
index, index,
setDialogOpen, remove,
}: FullTimeOfferDetailsFormProps) { }: FullTimeOfferDetailsFormProps) {
const { register, formState, setValue } = useFormContext<{ const { register, formState, setValue } = useFormContext<{
offers: Array<FullTimeOfferDetailsFormData>; offers: Array<OfferFormData>;
}>(); }>();
const offerFields = formState.errors.offers?.[index]; const offerFields = formState.errors.offers?.[index];
const watchCurrency = useWatch({ const watchCurrency = useWatch({
name: `offers.${index}.job.totalCompensation.currency`, name: `offers.${index}.offersFullTime.totalCompensation.currency`,
}); });
useEffect(() => { useEffect(() => {
setValue(`offers.${index}.job.base.currency`, watchCurrency); setValue(
setValue(`offers.${index}.job.bonus.currency`, watchCurrency); `offers.${index}.offersFullTime.baseSalary.currency`,
setValue(`offers.${index}.job.stocks.currency`, watchCurrency); watchCurrency,
);
setValue(`offers.${index}.offersFullTime.bonus.currency`, watchCurrency);
setValue(`offers.${index}.offersFullTime.stocks.currency`, watchCurrency);
}, [watchCurrency, index, setValue]); }, [watchCurrency, index, setValue]);
return ( return (
@ -62,48 +67,44 @@ function FullTimeOfferDetailsForm({
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.title?.message} errorMessage={offerFields?.offersFullTime?.title?.message}
label="Title" label="Title"
options={titleOptions} options={titleOptions}
placeholder={emptyOption} placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.title`, { {...register(`offers.${index}.offersFullTime.title`, {
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
<FormTextInput <FormTextInput
errorMessage={offerFields?.job?.specialization?.message} errorMessage={offerFields?.offersFullTime?.specialization?.message}
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}.offersFullTime.specialization`, {
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 flex grid grid-cols-2 space-x-3">
<FormSelect <div>
display="block" <CompaniesTypeahead
errorMessage={offerFields?.companyId?.message} onSelect={({ value }) =>
label="Company" setValue(`offers.${index}.companyId`, value)
options={companyOptions} }
placeholder={emptyOption} />
required={true} </div>
{...register(`offers.${index}.companyId`, {
required: FieldError.Required,
})}
/>
<FormTextInput <FormTextInput
errorMessage={offerFields?.job?.level?.message} errorMessage={offerFields?.offersFullTime?.level?.message}
label="Level" label="Level"
placeholder="e.g. L4, Junior" placeholder="e.g. L4, Junior"
required={true} required={true}
{...register(`offers.${index}.job.level`, { {...register(`offers.${index}.offersFullTime.level`, {
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 flex grid grid-cols-2 items-start space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.location?.message} errorMessage={offerFields?.location?.message}
@ -132,24 +133,32 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true} isLabelHidden={true}
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.totalCompensation.currency`, { {...register(
required: FieldError.Required, `offers.${index}.offersFullTime.totalCompensation.currency`,
})} {
required: FieldError.Required,
},
)}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.totalCompensation?.value?.message} errorMessage={
offerFields?.offersFullTime?.totalCompensation?.value?.message
}
label="Total Compensation (Annual)" label="Total Compensation (Annual)"
placeholder="0" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.totalCompensation.value`, { {...register(
min: { message: FieldError.NonNegativeNumber, value: 0 }, `offers.${index}.offersFullTime.totalCompensation.value`,
required: FieldError.Required, {
valueAsNumber: true, min: { message: FieldError.NonNegativeNumber, value: 0 },
})} required: FieldError.Required,
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">
@ -160,20 +169,23 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true} isLabelHidden={true}
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.base.currency`, { {...register(
required: FieldError.Required, `offers.${index}.offersFullTime.baseSalary.currency`,
})} {
required: FieldError.Required,
},
)}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.base?.value?.message} errorMessage={offerFields?.offersFullTime?.baseSalary?.value?.message}
label="Base Salary (Annual)" label="Base Salary (Annual)"
placeholder="0" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.base.value`, { {...register(`offers.${index}.offersFullTime.baseSalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 }, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required, required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
@ -186,20 +198,20 @@ 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}.offersFullTime.bonus.currency`, {
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.bonus?.value?.message} errorMessage={offerFields?.offersFullTime?.bonus?.value?.message}
label="Bonus (Annual)" label="Bonus (Annual)"
placeholder="0" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.bonus.value`, { {...register(`offers.${index}.offersFullTime.bonus.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 }, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required, required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
@ -214,20 +226,20 @@ 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}.offersFullTime.stocks.currency`, {
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.stocks?.value?.message} errorMessage={offerFields?.offersFullTime?.stocks?.value?.message}
label="Stocks (Annual)" label="Stocks (Annual)"
placeholder="0" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.stocks.value`, { {...register(`offers.${index}.offersFullTime.stocks.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 }, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required, required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
@ -254,7 +266,7 @@ function FullTimeOfferDetailsForm({
icon={TrashIcon} icon={TrashIcon}
label="Delete" label="Delete"
variant="secondary" variant="secondary"
onClick={() => setDialogOpen(true)} onClick={() => remove(index)}
/> />
)} )}
</div> </div>
@ -264,15 +276,15 @@ function FullTimeOfferDetailsForm({
type InternshipOfferDetailsFormProps = Readonly<{ type InternshipOfferDetailsFormProps = Readonly<{
index: number; index: number;
setDialogOpen: (isOpen: boolean) => void; remove: UseFieldArrayRemove;
}>; }>;
function InternshipOfferDetailsForm({ function InternshipOfferDetailsForm({
index, index,
setDialogOpen, remove,
}: InternshipOfferDetailsFormProps) { }: InternshipOfferDetailsFormProps) {
const { register, formState } = useFormContext<{ const { register, formState, setValue } = useFormContext<{
offers: Array<InternshipOfferDetailsFormData>; offers: Array<OfferFormData>;
}>(); }>();
const offerFields = formState.errors.offers?.[index]; const offerFields = formState.errors.offers?.[index];
@ -282,39 +294,35 @@ function InternshipOfferDetailsForm({
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.title?.message} errorMessage={offerFields?.offersIntern?.title?.message}
label="Title" label="Title"
options={titleOptions} options={titleOptions}
placeholder={emptyOption} placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.title`, { {...register(`offers.${index}.offersIntern.title`, {
minLength: 1, minLength: 1,
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
<FormTextInput <FormTextInput
errorMessage={offerFields?.job?.specialization?.message} errorMessage={offerFields?.offersIntern?.specialization?.message}
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}.offersIntern.specialization`, {
minLength: 1, minLength: 1,
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
</div> </div>
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <div>
display="block" <CompaniesTypeahead
errorMessage={offerFields?.companyId?.message} onSelect={({ value }) =>
label="Company" setValue(`offers.${index}.companyId`, value)
options={companyOptions} }
placeholder={emptyOption} />
required={true} </div>
{...register(`offers.${index}.companyId`, {
required: FieldError.Required,
})}
/>
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.location?.message} errorMessage={offerFields?.location?.message}
@ -330,24 +338,25 @@ function InternshipOfferDetailsForm({
<div className="mb-5 grid grid-cols-2 space-x-3"> <div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.internshipCycle?.message} errorMessage={offerFields?.offersIntern?.internshipCycle?.message}
label="Internship Cycle" label="Internship Cycle"
options={internshipCycleOptions} options={internshipCycleOptions}
placeholder={emptyOption} placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.internshipCycle`, { {...register(`offers.${index}.offersIntern.internshipCycle`, {
required: FieldError.Required, required: FieldError.Required,
})} })}
/> />
<FormSelect <FormSelect
display="block" display="block"
errorMessage={offerFields?.job?.startYear?.message} errorMessage={offerFields?.offersIntern?.startYear?.message}
label="Internship Year" label="Internship Year"
options={yearOptions} options={yearOptions}
placeholder={emptyOption} placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.job.startYear`, { {...register(`offers.${index}.offersIntern.startYear`, {
required: FieldError.Required, required: FieldError.Required,
valueAsNumber: true,
})} })}
/> />
</div> </div>
@ -369,20 +378,25 @@ function InternshipOfferDetailsForm({
isLabelHidden={true} isLabelHidden={true}
label="Currency" label="Currency"
options={CURRENCY_OPTIONS} options={CURRENCY_OPTIONS}
{...register(`offers.${index}.job.monthlySalary.currency`, { {...register(
required: FieldError.Required, `offers.${index}.offersIntern.monthlySalary.currency`,
})} {
required: FieldError.Required,
},
)}
/> />
} }
endAddOnType="element" endAddOnType="element"
errorMessage={offerFields?.job?.monthlySalary?.value?.message} errorMessage={
offerFields?.offersIntern?.monthlySalary?.value?.message
}
label="Salary (Monthly)" label="Salary (Monthly)"
placeholder="0" placeholder="0"
required={true} required={true}
startAddOn="$" startAddOn="$"
startAddOnType="label" startAddOnType="label"
type="number" type="number"
{...register(`offers.${index}.job.monthlySalary.value`, { {...register(`offers.${index}.offersIntern.monthlySalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 }, min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required, required: FieldError.Required,
valueAsNumber: true, valueAsNumber: true,
@ -410,7 +424,7 @@ function InternshipOfferDetailsForm({
label="Delete" label="Delete"
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setDialogOpen(true); remove(index);
}} }}
/> />
)} )}
@ -429,7 +443,6 @@ function OfferDetailsFormArray({
jobType, jobType,
}: OfferDetailsFormArrayProps) { }: OfferDetailsFormArrayProps) {
const { append, remove, fields } = fieldArrayValues; const { append, remove, fields } = fieldArrayValues;
const [isDialogOpen, setDialogOpen] = useState(false);
return ( return (
<div> <div>
@ -437,44 +450,10 @@ function OfferDetailsFormArray({
return ( return (
<div key={item.id}> <div key={item.id}>
{jobType === JobType.FullTime ? ( {jobType === JobType.FullTime ? (
<FullTimeOfferDetailsForm <FullTimeOfferDetailsForm index={index} remove={remove} />
index={index}
setDialogOpen={setDialogOpen}
/>
) : ( ) : (
<InternshipOfferDetailsForm <InternshipOfferDetailsForm index={index} remove={remove} />
index={index}
setDialogOpen={setDialogOpen}
/>
)} )}
<Dialog
isShown={isDialogOpen}
primaryButton={
<Button
display="block"
label="OK"
variant="primary"
onClick={() => {
remove(index);
setDialogOpen(false);
}}
/>
}
secondaryButton={
<Button
display="block"
label="Cancel"
variant="tertiary"
onClick={() => setDialogOpen(false)}
/>
}
title="Remove this offer"
onClose={() => setDialogOpen(false)}>
<p>
Are you sure you want to remove this offer? This action cannot
be reversed.
</p>
</Dialog>
</div> </div>
); );
})} })}
@ -501,22 +480,21 @@ export default function OfferDetailsForm() {
const [isDialogOpen, setDialogOpen] = useState(false); const [isDialogOpen, setDialogOpen] = useState(false);
const { control } = useFormContext(); const { control } = useFormContext();
const fieldArrayValues = useFieldArray({ control, name: 'offers' }); const fieldArrayValues = useFieldArray({ control, name: 'offers' });
const { append, remove } = fieldArrayValues;
const toggleJobType = () => { const toggleJobType = () => {
fieldArrayValues.remove(); remove();
if (jobType === JobType.FullTime) { if (jobType === JobType.FullTime) {
setJobType(JobType.Internship); setJobType(JobType.Intern);
fieldArrayValues.append(defaultInternshipOfferValues); append(defaultInternshipOfferValues);
} else { } else {
setJobType(JobType.FullTime); setJobType(JobType.FullTime);
fieldArrayValues.append(defaultFullTimeOfferValues); append(defaultFullTimeOfferValues);
} }
}; };
const switchJobTypeLabel = () => const switchJobTypeLabel = () =>
jobType === JobType.FullTime jobType === JobType.FullTime ? JobTypeLabel.INTERN : JobTypeLabel.FULLTIME;
? JobTypeLabel.INTERNSHIP
: JobTypeLabel.FULLTIME;
return ( return (
<div className="mb-5"> <div className="mb-5">
@ -541,11 +519,11 @@ export default function OfferDetailsForm() {
<div className="mx-5 w-1/3"> <div className="mx-5 w-1/3">
<Button <Button
display="block" display="block"
label={JobTypeLabel.INTERNSHIP} label={JobTypeLabel.INTERN}
size="md" size="md"
variant={jobType === JobType.Internship ? 'secondary' : 'tertiary'} variant={jobType === JobType.Intern ? 'secondary' : 'tertiary'}
onClick={() => { onClick={() => {
if (jobType === JobType.Internship) { if (jobType === JobType.Intern) {
return; return;
} }
setDialogOpen(true); setDialogOpen(true);

@ -52,7 +52,8 @@ export default function OfferProfileSave() {
<p className="mb-5 text-gray-900"> <p className="mb-5 text-gray-900">
If you do not want to keep the edit link, you can opt to save this If you do not want to keep the edit link, you can opt to save this
profile under your user accont. It will still only be editable by you. profile under your user account. It will still only be editable by
you.
</p> </p>
<div className="mb-20"> <div className="mb-20">
<Button <Button

@ -6,12 +6,12 @@ import type { MonthYear } from '~/components/shared/MonthYearPicker';
export enum JobType { export enum JobType {
FullTime = 'FULLTIME', FullTime = 'FULLTIME',
Internship = 'INTERNSHIP', Intern = 'INTERN',
} }
export const JobTypeLabel = { export const JobTypeLabel = {
FULLTIME: 'Full-time', FULLTIME: 'Full-time',
INTERNSHIP: 'Internship', INTERN: 'Internship',
}; };
export enum EducationBackgroundType { export enum EducationBackgroundType {
@ -20,110 +20,91 @@ export enum EducationBackgroundType {
Masters = 'Masters', Masters = 'Masters',
PhD = 'PhD', PhD = 'PhD',
Professional = 'Professional', Professional = 'Professional',
Seconday = 'Secondary', Secondary = 'Secondary',
SelfTaught = 'Self-taught', SelfTaught = 'Self-taught',
} }
export type Money = { export type OffersProfilePostData = {
currency: string; background: BackgroundPostData;
value: number; offers: Array<OfferPostData>;
};
type FullTimeJobData = {
base: Money;
bonus: Money;
level: string;
specialization: string;
stocks: Money;
title: string;
totalCompensation: Money;
};
type InternshipJobData = {
internshipCycle: string;
monthlySalary: Money;
specialization: string;
startYear: number;
title: string;
}; };
type OfferDetailsGeneralData = { export type OffersProfileFormData = {
comments: string; background: BackgroundPostData;
companyId: string; offers: Array<OfferFormData>;
jobType: string;
location: string;
monthYearReceived: MonthYear;
negotiationStrategy: string;
}; };
export type FullTimeOfferDetailsFormData = OfferDetailsGeneralData & { export type BackgroundPostData = {
job: FullTimeJobData; educations: Array<EducationPostData>;
experiences: Array<ExperiencePostData>;
specificYoes: Array<SpecificYoePostData>;
totalYoe: number;
}; };
export type InternshipOfferDetailsFormData = OfferDetailsGeneralData & { type ExperiencePostData = {
job: InternshipJobData; companyId?: string | null;
durationInMonths?: number | null;
jobType?: string | null;
level?: string | null;
location?: string | null;
monthlySalary?: Money | null;
specialization?: string | null;
title?: string | null;
totalCompensation?: Money | null;
totalCompensationId?: string | null;
}; };
export type OfferDetailsFormData = type EducationPostData = {
| FullTimeOfferDetailsFormData endDate?: Date | null;
| InternshipOfferDetailsFormData; field?: string | null;
school?: string | null;
export type OfferDetailsPostData = Omit< startDate?: Date | null;
OfferDetailsFormData, type?: string | null;
'monthYearReceived'
> & {
monthYearReceived: Date;
}; };
type SpecificYoe = { type SpecificYoePostData = {
domain: string; domain: string;
yoe: number; yoe: number;
}; };
type FullTimeExperience = { type SpecificYoe = SpecificYoePostData;
level?: string;
totalCompensation?: Money;
};
type InternshipExperience = {
monthlySalary?: Money;
};
type GeneralExperience = { export type OfferPostData = {
companyId?: string; comments: string;
durationInMonths?: number; companyId: string;
jobType?: string; jobType: string;
specialization?: string; location: string;
title?: string; monthYearReceived: Date;
negotiationStrategy: string;
offersFullTime?: OfferFullTimePostData | null;
offersIntern?: OfferInternPostData | null;
}; };
export type Experience = export type OfferFormData = Omit<OfferPostData, 'monthYearReceived'> & {
| (FullTimeExperience & GeneralExperience) monthYearReceived: MonthYear;
| (GeneralExperience & InternshipExperience);
type Education = {
endDate?: Date;
field?: string;
school?: string;
startDate?: Date;
type?: string;
}; };
type BackgroundFormData = { export type OfferFullTimePostData = {
educations: Array<Education>; baseSalary: Money;
experiences: Array<Experience>; bonus: Money;
specificYoes: Array<SpecificYoe>; level: string;
totalYoe?: number; specialization: string;
stocks: Money;
title: string;
totalCompensation: Money;
}; };
export type OfferProfileFormData = { export type OfferInternPostData = {
background: BackgroundFormData; internshipCycle: string;
offers: Array<OfferDetailsFormData>; monthlySalary: Money;
specialization: string;
startYear: number;
title: string;
}; };
export type OfferProfilePostData = { export type Money = {
background: BackgroundFormData; currency: string;
offers: Array<OfferDetailsPostData>; value: number;
}; };
type EducationDisplay = { type EducationDisplay = {

@ -5,13 +5,13 @@ import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button } from '@tih/ui'; import { Button } from '@tih/ui';
import { Breadcrumbs } from '~/components/offers/Breadcrumb'; import { Breadcrumbs } from '~/components/offers/Breadcrumb';
import BackgroundForm from '~/components/offers/forms/BackgroundForm'; import BackgroundForm from '~/components/offers/offers-submission/BackgroundForm';
import OfferAnalysis from '~/components/offers/forms/OfferAnalysis'; import OfferAnalysis from '~/components/offers/offers-submission/OfferAnalysis';
import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm'; import OfferDetailsForm from '~/components/offers/offers-submission/OfferDetailsForm';
import OfferProfileSave from '~/components/offers/forms/OfferProfileSave'; import OfferProfileSave from '~/components/offers/offers-submission/OfferProfileSave';
import type { import type {
OfferDetailsFormData, OfferFormData,
OfferProfileFormData, OffersProfileFormData,
} from '~/components/offers/types'; } from '~/components/offers/types';
import { JobType } from '~/components/offers/types'; import { JobType } from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker'; import type { Month } from '~/components/shared/MonthYearPicker';
@ -20,10 +20,11 @@ import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time'; import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import type { CreateOfferProfileResponse } from '~/types/offers';
const defaultOfferValues = { const defaultOfferValues = {
comments: '', comments: '',
companyId: '', companyId: '',
job: {},
jobType: JobType.FullTime, jobType: JobType.FullTime,
location: '', location: '',
monthYearReceived: { monthYearReceived: {
@ -40,7 +41,7 @@ export const defaultFullTimeOfferValues = {
export const defaultInternshipOfferValues = { export const defaultInternshipOfferValues = {
...defaultOfferValues, ...defaultOfferValues,
jobType: JobType.Internship, jobType: JobType.Intern,
}; };
const defaultOfferProfileValues = { const defaultOfferProfileValues = {
@ -61,10 +62,13 @@ type FormStep = {
export default function OffersSubmissionPage() { export default function OffersSubmissionPage() {
const [formStep, setFormStep] = useState(0); const [formStep, setFormStep] = useState(0);
const [createProfileResponse, setCreateProfileResponse] =
useState<CreateOfferProfileResponse>();
const pageRef = useRef<HTMLDivElement>(null); const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () => const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 }); pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
const formMethods = useForm<OfferProfileFormData>({ const formMethods = useForm<OffersProfileFormData>({
defaultValues: defaultOfferProfileValues, defaultValues: defaultOfferProfileValues,
mode: 'all', mode: 'all',
}); });
@ -84,7 +88,9 @@ export default function OffersSubmissionPage() {
label: 'Background', label: 'Background',
}, },
{ {
component: <OfferAnalysis key={2} />, component: (
<OfferAnalysis key={2} profileId={createProfileResponse?.id} />
),
hasNext: true, hasNext: true,
hasPrevious: false, hasPrevious: false,
label: 'Analysis', label: 'Analysis',
@ -115,18 +121,30 @@ export default function OffersSubmissionPage() {
scrollToTop(); scrollToTop();
}; };
const generateAnalysisMutation = trpc.useMutation(
['offers.analysis.generate'],
{
onError(error) {
console.error(error.message);
},
},
);
const createMutation = trpc.useMutation(['offers.profile.create'], { const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(error) { onError(error) {
console.error(error.message); console.error(error.message);
}, },
onSuccess() { onSuccess(data) {
alert('offer profile submit success!'); generateAnalysisMutation.mutate({
profileId: data?.id || '',
});
setCreateProfileResponse(data);
setFormStep(formStep + 1); setFormStep(formStep + 1);
scrollToTop(); scrollToTop();
}, },
}); });
const onSubmit: SubmitHandler<OfferProfileFormData> = async (data) => { const onSubmit: SubmitHandler<OffersProfileFormData> = async (data) => {
const result = await trigger(); const result = await trigger();
if (!result) { if (!result) {
return; return;
@ -142,7 +160,7 @@ export default function OffersSubmissionPage() {
background.experiences = []; background.experiences = [];
} }
const offers = data.offers.map((offer: OfferDetailsFormData) => ({ const offers = data.offers.map((offer: OfferFormData) => ({
...offer, ...offer,
monthYearReceived: new Date( monthYearReceived: new Date(
offer.monthYearReceived.year, offer.monthYearReceived.year,

@ -3,12 +3,15 @@ import React from 'react';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
function GenerateAnalysis() { function GenerateAnalysis() {
const analysis = trpc.useQuery([ const analysisMutation = trpc.useMutation(['offers.analysis.generate']);
'offers.analysis.generate',
{ profileId: 'cl98ywtbv0000tx1s4p18eol1' },
]);
return <div>{JSON.stringify(analysis.data)}</div>; return (
<div>
{JSON.stringify(
analysisMutation.mutate({ profileId: 'cl98ywtbv0000tx1s4p18eol1' }),
)}
</div>
);
} }
export default GenerateAnalysis; export default GenerateAnalysis;

@ -51,7 +51,106 @@ const searchOfferPercentile = (
}; };
export const offersAnalysisRouter = createRouter() export const offersAnalysisRouter = createRouter()
.query('generate', { .query('get', {
input: z.object({
profileId: z.string(),
}),
async resolve({ ctx, input }) {
const analysis = await ctx.prisma.offersAnalysis.findFirst({
include: {
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
},
topCompanyOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
topOverallOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
where: {
profileId: input.profileId,
},
});
if (!analysis) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No analysis found on this profile',
});
}
return profileAnalysisDtoMapper(analysis);
},
})
.mutation('generate', {
input: z.object({ input: z.object({
profileId: z.string(), profileId: z.string(),
}), }),
@ -366,105 +465,6 @@ export const offersAnalysisRouter = createRouter()
}, },
}); });
return profileAnalysisDtoMapper(analysis);
},
})
.query('get', {
input: z.object({
profileId: z.string(),
}),
async resolve({ ctx, input }) {
const analysis = await ctx.prisma.offersAnalysis.findFirst({
include: {
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
},
topCompanyOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
topOverallOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
where: {
profileId: input.profileId,
},
});
if (!analysis) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No analysis found on this profile',
});
}
return profileAnalysisDtoMapper(analysis); return profileAnalysisDtoMapper(analysis);
}, },
}); });

Loading…
Cancel
Save