[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 testpull/395/head
parent
4fa350964f
commit
992d457b8a
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in new issue