[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