[offers][feat] Add analysis to offers profile page (#416)

pull/417/head
Ai Ling 2 years ago committed by GitHub
parent 7c63e22b3a
commit c0f92584ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -110,9 +110,30 @@ export const educationFieldOptions = [
];
export enum FieldError {
NonNegativeNumber = 'Please fill in a non-negative number in this field.',
Number = 'Please fill in a number in this field.',
Required = 'Please fill in this field.',
NON_NEGATIVE_NUMBER = 'Please fill in a non-negative number in this field.',
NUMBER = 'Please fill in a number in this field.',
REQUIRED = 'Please fill in this field.',
}
export const OVERALL_TAB = 'Overall';
export enum ProfileDetailTab {
ANALYSIS = 'Offer Engine Analysis',
BACKGROUND = 'Background',
OFFERS = 'Offers',
}
export const profileDetailTabs = [
{
label: ProfileDetailTab.OFFERS,
value: ProfileDetailTab.OFFERS,
},
{
label: ProfileDetailTab.BACKGROUND,
value: ProfileDetailTab.BACKGROUND,
},
{
label: ProfileDetailTab.ANALYSIS,
value: ProfileDetailTab.ANALYSIS,
},
];

@ -2,11 +2,9 @@ import { useEffect } from 'react';
import { useState } from 'react';
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import OfferPercentileAnalysisText from './OfferPercentileAnalysisText';
import OfferProfileCard from './OfferProfileCard';
import { OVERALL_TAB } from '../../constants';
import { OVERALL_TAB } from '../constants';
import type {
Analysis,
@ -29,10 +27,18 @@ function OfferAnalysisContent({
tab,
}: OfferAnalysisContentProps) {
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
if (tab === OVERALL_TAB) {
return (
<p className="m-10">
You are the first to submit an offer for your job title and YOE! Check
back later when there are more submissions.
</p>
);
}
return (
<p className="m-10">
You are the first to submit an offer for these companies! Check back
later when there are more submissions.
You are the first to submit an offer for this company, job title and
YOE! Check back later when there are more submissions.
</p>
);
}
@ -55,12 +61,17 @@ function OfferAnalysisContent({
}
type OfferAnalysisProps = Readonly<{
profileId?: string;
allAnalysis?: ProfileAnalysis | null;
isError: boolean;
isLoading: boolean;
}>;
export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
export default function OfferAnalysis({
allAnalysis,
isError,
isLoading,
}: OfferAnalysisProps) {
const [tab, setTab] = useState(OVERALL_TAB);
const [allAnalysis, setAllAnalysis] = useState<ProfileAnalysis | null>(null);
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
useEffect(() => {
@ -77,22 +88,6 @@ export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
}
}, [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,
@ -107,18 +102,13 @@ export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
return (
analysis && (
<div>
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
Result
</h5>
{getAnalysisResult.isError && (
{isError && (
<p className="m-10 text-center">
An error occurred while generating profile analysis.
</p>
)}
{getAnalysisResult.isLoading && (
<Spinner className="m-10" display="block" size="lg" />
)}
{!getAnalysisResult.isError && !getAnalysisResult.isLoading && (
{isLoading && <Spinner className="m-10" display="block" size="lg" />}
{!isError && !isLoading && (
<div>
<Tabs
label="Result Navigation"

@ -0,0 +1,29 @@
import { OVERALL_TAB } from '../constants';
import type { Analysis } from '~/types/offers';
type OfferPercentileAnalysisTextProps = Readonly<{
companyName: string;
offerAnalysis: Analysis;
tab: string;
}>;
export default function OfferPercentileAnalysisText({
tab,
companyName,
offerAnalysis: { noOfOffers, percentile },
}: OfferPercentileAnalysisTextProps) {
return tab === OVERALL_TAB ? (
<p>
Your highest offer is from <b>{companyName}</b>, which is{' '}
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
offers received for the same job title and YOE(±1) in the last year.
</p>
) : (
<p>
Your offer from <b>{companyName}</b> is <b>{percentile.toFixed(1)}</b>{' '}
percentile out of <b>{noOfOffers}</b> offers received in {companyName} for
the same job title and YOE(±1) in the last year.
</p>
);
}

@ -1,10 +1,14 @@
import {
BuildingOffice2Icon,
CalendarDaysIcon,
} from '@heroicons/react/24/outline';
import { JobType } from '@prisma/client';
import { HorizontalDivider } from '~/../../../packages/ui/dist';
import { convertMoneyToString } from '~/utils/offers/currency';
import { formatDate } from '~/utils/offers/time';
import ProfilePhotoHolder from '../../profile/ProfilePhotoHolder';
import ProfilePhotoHolder from '../profile/ProfilePhotoHolder';
import type { AnalysisOffer } from '~/types/offers';
@ -27,29 +31,37 @@ export default function OfferProfileCard({
},
}: 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">
<div className="my-5 block rounded-lg bg-white p-4 px-8 shadow-md">
<div className="flex items-center gap-x-5">
<div>
<ProfilePhotoHolder size="sm" />
</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>
<p className="font-bold">{profileName}</p>
<div className="flex flex-row">
<BuildingOffice2Icon className="mr-2 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>{previousCompanies[0]}</span>
</div>
<div className="flex flex-row">
<CalendarDaysIcon className="mr-2 h-5" />
<span className="mr-2 font-bold">YOE:</span>
<span>{totalYoe}</span>
</div>
</div>
</div>
<HorizontalDivider />
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
<div className="flex items-end justify-between">
<div className="col-span-1 row-span-3">
<p className="text-sm font-semibold">{title}</p>
<p className="text-xs ">
<p className="font-bold">{title}</p>
<p>
Company: {company.name}, {location}
</p>
<p className="text-xs ">Level: {level}</p>
<p>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">{formatDate(monthYearReceived)}</p>
<p className="text-end text-xl">
{jobType === JobType.FULLTIME
? `${convertMoneyToString(income)} / year`

@ -16,7 +16,7 @@ type OfferProfileSaveProps = Readonly<{
token?: string;
}>;
export default function OfferProfileSave({
export default function OffersProfileSave({
profileId,
token,
}: OfferProfileSaveProps) {

@ -6,8 +6,7 @@ import { JobType } from '@prisma/client';
import { Button } from '@tih/ui';
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
import OfferAnalysis from '~/components/offers/offersSubmission/analysis/OfferAnalysis';
import OfferProfileSave from '~/components/offers/offersSubmission/OfferProfileSave';
import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave';
import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm';
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
import type {
@ -20,7 +19,12 @@ import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
import type { CreateOfferProfileResponse } from '~/types/offers';
import OfferAnalysis from '../offerAnalysis/OfferAnalysis';
import type {
CreateOfferProfileResponse,
ProfileAnalysis,
} from '~/types/offers';
const defaultOfferValues = {
comments: '',
@ -78,6 +82,7 @@ export default function OffersSubmissionForm({
id: profileId || '',
token: token || '',
});
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () =>
@ -88,6 +93,18 @@ export default function OffersSubmissionForm({
});
const { handleSubmit, trigger } = formMethods;
const generateAnalysisMutation = trpc.useMutation(
['offers.analysis.generate'],
{
onError(error) {
console.error(error.message);
},
onSuccess(data) {
setAnalysis(data);
},
},
);
const formSteps: Array<FormStep> = [
{
component: (
@ -107,14 +124,21 @@ export default function OffersSubmissionForm({
label: 'Background',
},
{
component: <OfferAnalysis key={2} profileId={createProfileResponse.id} />,
component: (
<OfferAnalysis
key={2}
allAnalysis={analysis}
isError={generateAnalysisMutation.isError}
isLoading={generateAnalysisMutation.isLoading}
/>
),
hasNext: true,
hasPrevious: false,
label: 'Analysis',
},
{
component: (
<OfferProfileSave
<OffersProfileSave
key={3}
profileId={createProfileResponse.id || ''}
token={createProfileResponse.token}
@ -144,15 +168,6 @@ export default function OffersSubmissionForm({
scrollToTop();
};
const generateAnalysisMutation = trpc.useMutation(
['offers.analysis.generate'],
{
onError(error) {
console.error(error.message);
},
},
);
const mutationpath =
profileId && token ? 'offers.profile.update' : 'offers.profile.create';

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

@ -39,8 +39,8 @@ function YoeSection() {
required={true}
type="number"
{...register(`background.totalYoe`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -52,7 +52,7 @@ function YoeSection() {
label="Specific YOE 1"
type="number"
{...register(`background.specificYoes.0.yoe`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -68,7 +68,7 @@ function YoeSection() {
label="Specific YOE 2"
type="number"
{...register(`background.specificYoes.1.yoe`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -128,7 +128,7 @@ function FullTimeJobFields() {
startAddOnType="label"
type="number"
{...register(`background.experiences.0.totalCompensation.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -158,7 +158,7 @@ function FullTimeJobFields() {
label="Duration (months)"
type="number"
{...register(`background.experiences.0.durationInMonths`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -211,7 +211,7 @@ function InternshipJobFields() {
startAddOnType="label"
type="number"
{...register(`background.experiences.0.monthlySalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>

@ -72,7 +72,7 @@ function FullTimeOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.offersFullTime.title`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormTextInput
@ -81,7 +81,7 @@ function FullTimeOfferDetailsForm({
placeholder="e.g. Front End"
required={true}
{...register(`offers.${index}.offersFullTime.specialization`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -99,7 +99,7 @@ function FullTimeOfferDetailsForm({
placeholder="e.g. L4, Junior"
required={true}
{...register(`offers.${index}.offersFullTime.level`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -112,7 +112,7 @@ function FullTimeOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.location`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormMonthYearPicker
@ -120,7 +120,7 @@ function FullTimeOfferDetailsForm({
monthRequired={true}
yearLabel=""
{...register(`offers.${index}.monthYearReceived`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -135,7 +135,7 @@ function FullTimeOfferDetailsForm({
{...register(
`offers.${index}.offersFullTime.totalCompensation.currency`,
{
required: FieldError.Required,
required: FieldError.REQUIRED,
},
)}
/>
@ -153,8 +153,8 @@ function FullTimeOfferDetailsForm({
{...register(
`offers.${index}.offersFullTime.totalCompensation.value`,
{
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
},
)}
@ -171,7 +171,7 @@ function FullTimeOfferDetailsForm({
{...register(
`offers.${index}.offersFullTime.baseSalary.currency`,
{
required: FieldError.Required,
required: FieldError.REQUIRED,
},
)}
/>
@ -185,8 +185,8 @@ function FullTimeOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersFullTime.baseSalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -198,7 +198,7 @@ function FullTimeOfferDetailsForm({
label="Currency"
options={CURRENCY_OPTIONS}
{...register(`offers.${index}.offersFullTime.bonus.currency`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
}
@ -211,8 +211,8 @@ function FullTimeOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersFullTime.bonus.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -226,7 +226,7 @@ function FullTimeOfferDetailsForm({
label="Currency"
options={CURRENCY_OPTIONS}
{...register(`offers.${index}.offersFullTime.stocks.currency`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
}
@ -239,8 +239,8 @@ function FullTimeOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersFullTime.stocks.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -300,7 +300,7 @@ function InternshipOfferDetailsForm({
required={true}
{...register(`offers.${index}.offersIntern.title`, {
minLength: 1,
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormTextInput
@ -310,7 +310,7 @@ function InternshipOfferDetailsForm({
required={true}
{...register(`offers.${index}.offersIntern.specialization`, {
minLength: 1,
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -330,7 +330,7 @@ function InternshipOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.location`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -343,7 +343,7 @@ function InternshipOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.offersIntern.internshipCycle`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormSelect
@ -354,7 +354,7 @@ function InternshipOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.offersIntern.startYear`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -365,7 +365,7 @@ function InternshipOfferDetailsForm({
monthRequired={true}
yearLabel=""
{...register(`offers.${index}.monthYearReceived`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -380,7 +380,7 @@ function InternshipOfferDetailsForm({
{...register(
`offers.${index}.offersIntern.monthlySalary.currency`,
{
required: FieldError.Required,
required: FieldError.REQUIRED,
},
)}
/>
@ -396,8 +396,8 @@ function InternshipOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersIntern.monthlySalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>

@ -58,52 +58,64 @@ export default function OfferCard({
}
function BottomSection() {
if (
!totalCompensation &&
!monthlySalary &&
!negotiationStrategy &&
!otherComment
) {
return null;
}
return (
<div className="px-8">
<div className="flex flex-col py-2">
<div className="flex flex-row">
<CurrencyDollarIcon className="mr-1 h-5" />
<p>
{totalCompensation
? `TC: ${totalCompensation}`
: `Monthly Salary: ${monthlySalary}`}
</p>
<>
<HorizontalDivider />
<div className="px-8">
<div className="flex flex-col py-2">
{totalCompensation ||
(monthlySalary && (
<div className="flex flex-row">
<CurrencyDollarIcon className="mr-1 h-5" />
<p>
{totalCompensation && `TC: ${totalCompensation}`}
{monthlySalary && `Monthly Salary: ${monthlySalary}`}
</p>
</div>
))}
{totalCompensation && (
<div className="ml-6 flex flex-row font-light text-gray-400">
<p>
Base / year: {base} Stocks / year: {stocks} Bonus / year:{' '}
{bonus}
</p>
</div>
)}
</div>
{totalCompensation && (
<div className="ml-6 flex flex-row font-light text-gray-400">
<p>
Base / year: {base} Stocks / year: {stocks} Bonus / year:{' '}
{bonus}
</p>
{negotiationStrategy && (
<div className="flex flex-col py-2">
<div className="flex flex-row">
<ScaleIcon className="h-5 w-5" />
<span className="overflow-wrap ml-2">
"{negotiationStrategy}"
</span>
</div>
</div>
)}
</div>
{negotiationStrategy && (
<div className="flex flex-col py-2">
<div className="flex flex-row">
<ScaleIcon className="h-5 w-5" />
<span className="overflow-wrap ml-2">
"{negotiationStrategy}"
</span>
{otherComment && (
<div className="flex flex-col py-2">
<div className="flex flex-row">
<ChatBubbleBottomCenterTextIcon className="h-5 w-5" />
<span className="overflow-wrap ml-2">"{otherComment}"</span>
</div>
</div>
</div>
)}
{otherComment && (
<div className="flex flex-col py-2">
<div className="flex flex-row">
<ChatBubbleBottomCenterTextIcon className="h-8 w-8" />
<span className="overflow-wrap ml-2">"{otherComment}"</span>
</div>
</div>
)}
</div>
)}
</div>
</>
);
}
return (
<div className="mx-8 my-4 block rounded-lg bg-white py-4 shadow-md">
<UpperSection />
<HorizontalDivider />
<BottomSection />
</div>
);

@ -1,5 +1,10 @@
import { AcademicCapIcon, BriefcaseIcon } from '@heroicons/react/24/outline';
import { Spinner } from '@tih/ui';
import { useState } from 'react';
import {
AcademicCapIcon,
ArrowPathIcon,
BriefcaseIcon,
} from '@heroicons/react/24/outline';
import { Button, Spinner } from '@tih/ui';
import EducationCard from '~/components/offers/profile/EducationCard';
import OfferCard from '~/components/offers/profile/OfferCard';
@ -8,22 +13,143 @@ import type {
OfferDisplayData,
} from '~/components/offers/types';
import type { ProfileAnalysis } from '~/types/offers';
import { trpc } from '~/utils/trpc';
type ProfileHeaderProps = Readonly<{
import { ProfileDetailTab } from '../constants';
import OfferAnalysis from '../offerAnalysis/OfferAnalysis';
import { ProfileAnalysis } from '~/types/offers';
type ProfileOffersProps = Readonly<{
offers: Array<OfferDisplayData>;
}>;
function ProfileOffers({ offers }: ProfileOffersProps) {
if (offers.length !== 0) {
return (
<>
{offers.map((offer) => (
<OfferCard key={offer.id} offer={offer} />
))}
</>
);
}
return (
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">No offer is attached.</span>
</div>
);
}
type ProfileBackgroundProps = Readonly<{
background?: BackgroundDisplayData;
}>;
function ProfileBackground({ background }: ProfileBackgroundProps) {
if (!background?.experiences?.length && !background?.educations?.length) {
return (
<div className="mx-8 my-4">
<p>No background information available.</p>
</div>
);
}
return (
<>
{background?.experiences?.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">Work Experience</span>
</div>
<OfferCard offer={background.experiences[0]} />
</>
)}
{background?.educations?.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<AcademicCapIcon className="mr-1 h-5" />
<span className="font-bold">Education</span>
</div>
<EducationCard education={background.educations[0]} />
</>
)}
</>
);
}
type ProfileAnalysisProps = Readonly<{
analysis?: ProfileAnalysis;
isEditable: boolean;
profileId: string;
}>;
function ProfileAnalysis({
analysis: profileAnalysis,
profileId,
isEditable,
}: ProfileAnalysisProps) {
const [analysis, setAnalysis] = useState(profileAnalysis);
const generateAnalysisMutation = trpc.useMutation(
['offers.analysis.generate'],
{
onError(error) {
console.error(error.message);
},
onSuccess(data) {
if (data) {
setAnalysis(data);
}
},
},
);
if (generateAnalysisMutation.isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
return (
<div className="mx-8 my-4">
<OfferAnalysis allAnalysis={analysis} isError={false} isLoading={false} />
{isEditable && (
<div className="flex justify-end">
<Button
addonPosition="start"
icon={ArrowPathIcon}
label="Refresh Analysis"
variant="secondary"
onClick={() => generateAnalysisMutation.mutate({ profileId })}
/>
</div>
)}
</div>
);
}
type ProfileDetailsProps = Readonly<{
analysis?: ProfileAnalysis;
background?: BackgroundDisplayData;
isEditable: boolean;
isLoading: boolean;
offers: Array<OfferDisplayData>;
selectedTab: string;
profileId: string;
selectedTab: ProfileDetailTab;
}>;
export default function ProfileDetails({
analysis,
background,
isLoading,
offers,
selectedTab,
}: ProfileHeaderProps) {
profileId,
isEditable,
}: ProfileDetailsProps) {
if (isLoading) {
return (
<div className="col-span-10 pt-4">
@ -31,46 +157,20 @@ export default function ProfileDetails({
</div>
);
}
if (selectedTab === 'offers') {
if (offers.length !== 0) {
return (
<>
{offers.map((offer) => (
<OfferCard key={offer.id} offer={offer} />
))}
</>
);
}
return (
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">No offer is attached.</span>
</div>
);
if (selectedTab === ProfileDetailTab.OFFERS) {
return <ProfileOffers offers={offers} />;
}
if (selectedTab === ProfileDetailTab.BACKGROUND) {
return <ProfileBackground background={background} />;
}
if (selectedTab === 'background') {
if (selectedTab === ProfileDetailTab.ANALYSIS) {
return (
<>
{background?.experiences && background?.experiences.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">Work Experience</span>
</div>
<OfferCard offer={background.experiences[0]} />
</>
)}
{background?.educations && background?.educations.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<AcademicCapIcon className="mr-1 h-5" />
<span className="font-bold">Education</span>
</div>
<EducationCard education={background.educations[0]} />
</>
)}
</>
<ProfileAnalysis
analysis={analysis}
isEditable={isEditable}
profileId={profileId}
/>
);
}
return <div>Detail page for {selectedTab}</div>;
return null;
}

@ -13,13 +13,16 @@ import type { BackgroundDisplayData } from '~/components/offers/types';
import { getProfileEditPath } from '~/utils/offers/link';
import type { ProfileDetailTab } from '../constants';
import { profileDetailTabs } from '../constants';
type ProfileHeaderProps = Readonly<{
background?: BackgroundDisplayData;
handleDelete: () => void;
isEditable: boolean;
isLoading: boolean;
selectedTab: string;
setSelectedTab: (tab: string) => void;
selectedTab: ProfileDetailTab;
setSelectedTab: (tab: ProfileDetailTab) => void;
}>;
export default function ProfileHeader({
@ -139,9 +142,9 @@ export default function ProfileHeader({
<BuildingOffice2Icon className="mr-2.5 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>
{`${experiences[0]?.companyName || ''} ${
experiences[0]?.jobLevel || ''
} ${experiences[0]?.jobTitle || ''}`}
{`${experiences[0].companyName || ''} ${
experiences[0].jobLevel || ''
} ${experiences[0].jobTitle || ''}`}
</span>
</div>
)}
@ -165,20 +168,7 @@ export default function ProfileHeader({
<div className="mt-8">
<Tabs
label="Profile Detail Navigation"
tabs={[
{
label: 'Offers',
value: 'offers',
},
{
label: 'Background',
value: 'background',
},
{
label: 'Offer Engine Analysis',
value: 'offerEngineAnalysis',
},
]}
tabs={profileDetailTabs}
value={selectedTab}
onChange={(value) => setSelectedTab(value)}
/>

@ -2,6 +2,7 @@ import Error from 'next/error';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { ProfileDetailTab } from '~/components/offers/constants';
import ProfileComments from '~/components/offers/profile/ProfileComments';
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
@ -27,7 +28,9 @@ export default function OfferProfile() {
const [background, setBackground] = useState<BackgroundDisplayData>();
const [offers, setOffers] = useState<Array<OfferDisplayData>>([]);
const [selectedTab, setSelectedTab] = useState('offers');
const [selectedTab, setSelectedTab] = useState<ProfileDetailTab>(
ProfileDetailTab.OFFERS,
);
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
const getProfileQuery = trpc.useQuery(
@ -163,8 +166,10 @@ export default function OfferProfile() {
<ProfileDetails
analysis={analysis}
background={background}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
offers={offers}
profileId={offerProfileId as string}
selectedTab={selectedTab}
/>
</div>

@ -321,18 +321,18 @@ export const offersAnalysisRouter = createRouter()
similarOffers,
);
const overallPercentile =
similarOffers.length === 0
similarOffers.length <= 1
? 100
: (100 * overallIndex) / similarOffers.length;
: 100 - (100 * overallIndex) / (similarOffers.length - 1);
const companyIndex = searchOfferPercentile(
overallHighestOffer,
similarCompanyOffers,
);
const companyPercentile =
similarCompanyOffers.length === 0
similarCompanyOffers.length <= 1
? 100
: (100 * companyIndex) / similarCompanyOffers.length;
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
// FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE
// e.g. If there only 4 offers, it gives the 2nd and 3rd offer

Loading…
Cancel
Save