mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 04:33:42 +08:00
[offers][feat] Add analysis to offers profile page (#416)
This commit is contained in:
@ -110,9 +110,30 @@ export const educationFieldOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export enum FieldError {
|
export enum FieldError {
|
||||||
NonNegativeNumber = 'Please fill in a non-negative number 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.',
|
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';
|
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 { useState } from 'react';
|
||||||
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
|
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
|
||||||
|
|
||||||
import { trpc } from '~/utils/trpc';
|
|
||||||
|
|
||||||
import OfferPercentileAnalysisText from './OfferPercentileAnalysisText';
|
import OfferPercentileAnalysisText from './OfferPercentileAnalysisText';
|
||||||
import OfferProfileCard from './OfferProfileCard';
|
import OfferProfileCard from './OfferProfileCard';
|
||||||
import { OVERALL_TAB } from '../../constants';
|
import { OVERALL_TAB } from '../constants';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Analysis,
|
Analysis,
|
||||||
@ -29,10 +27,18 @@ function OfferAnalysisContent({
|
|||||||
tab,
|
tab,
|
||||||
}: OfferAnalysisContentProps) {
|
}: OfferAnalysisContentProps) {
|
||||||
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
|
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 (
|
return (
|
||||||
<p className="m-10">
|
<p className="m-10">
|
||||||
You are the first to submit an offer for these companies! Check back
|
You are the first to submit an offer for this company, job title and
|
||||||
later when there are more submissions.
|
YOE! Check back later when there are more submissions.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -55,12 +61,17 @@ function OfferAnalysisContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OfferAnalysisProps = Readonly<{
|
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 [tab, setTab] = useState(OVERALL_TAB);
|
||||||
const [allAnalysis, setAllAnalysis] = useState<ProfileAnalysis | null>(null);
|
|
||||||
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
|
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -77,22 +88,6 @@ export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
|
|||||||
}
|
}
|
||||||
}, [tab, allAnalysis]);
|
}, [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 = [
|
const tabOptions = [
|
||||||
{
|
{
|
||||||
label: OVERALL_TAB,
|
label: OVERALL_TAB,
|
||||||
@ -107,18 +102,13 @@ export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
|
|||||||
return (
|
return (
|
||||||
analysis && (
|
analysis && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
{isError && (
|
||||||
Result
|
|
||||||
</h5>
|
|
||||||
{getAnalysisResult.isError && (
|
|
||||||
<p className="m-10 text-center">
|
<p className="m-10 text-center">
|
||||||
An error occurred while generating profile analysis.
|
An error occurred while generating profile analysis.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{getAnalysisResult.isLoading && (
|
{isLoading && <Spinner className="m-10" display="block" size="lg" />}
|
||||||
<Spinner className="m-10" display="block" size="lg" />
|
{!isError && !isLoading && (
|
||||||
)}
|
|
||||||
{!getAnalysisResult.isError && !getAnalysisResult.isLoading && (
|
|
||||||
<div>
|
<div>
|
||||||
<Tabs
|
<Tabs
|
||||||
label="Result Navigation"
|
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 { JobType } from '@prisma/client';
|
||||||
|
|
||||||
import { HorizontalDivider } from '~/../../../packages/ui/dist';
|
import { HorizontalDivider } from '~/../../../packages/ui/dist';
|
||||||
import { convertMoneyToString } from '~/utils/offers/currency';
|
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||||
import { formatDate } from '~/utils/offers/time';
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
|
||||||
import ProfilePhotoHolder from '../../profile/ProfilePhotoHolder';
|
import ProfilePhotoHolder from '../profile/ProfilePhotoHolder';
|
||||||
|
|
||||||
import type { AnalysisOffer } from '~/types/offers';
|
import type { AnalysisOffer } from '~/types/offers';
|
||||||
|
|
||||||
@ -27,29 +31,37 @@ export default function OfferProfileCard({
|
|||||||
},
|
},
|
||||||
}: OfferProfileCardProps) {
|
}: OfferProfileCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="my-5 block rounded-lg border p-4">
|
<div className="my-5 block rounded-lg bg-white p-4 px-8 shadow-md">
|
||||||
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
|
<div className="flex items-center gap-x-5">
|
||||||
<div className="col-span-1">
|
<div>
|
||||||
<ProfilePhotoHolder size="sm" />
|
<ProfilePhotoHolder size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-10">
|
<div className="col-span-10">
|
||||||
<p className="text-sm font-semibold">{profileName}</p>
|
<p className="font-bold">{profileName}</p>
|
||||||
<p className="text-xs ">Previous company: {previousCompanies[0]}</p>
|
<div className="flex flex-row">
|
||||||
<p className="text-xs ">YOE: {totalYoe} year(s)</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HorizontalDivider />
|
<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">
|
<div className="col-span-1 row-span-3">
|
||||||
<p className="text-sm font-semibold">{title}</p>
|
<p className="font-bold">{title}</p>
|
||||||
<p className="text-xs ">
|
<p>
|
||||||
Company: {company.name}, {location}
|
Company: {company.name}, {location}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs ">Level: {level}</p>
|
<p>Level: {level}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 row-span-3">
|
<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">
|
<p className="text-end text-xl">
|
||||||
{jobType === JobType.FULLTIME
|
{jobType === JobType.FULLTIME
|
||||||
? `${convertMoneyToString(income)} / year`
|
? `${convertMoneyToString(income)} / year`
|
@ -16,7 +16,7 @@ type OfferProfileSaveProps = Readonly<{
|
|||||||
token?: string;
|
token?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function OfferProfileSave({
|
export default function OffersProfileSave({
|
||||||
profileId,
|
profileId,
|
||||||
token,
|
token,
|
||||||
}: OfferProfileSaveProps) {
|
}: OfferProfileSaveProps) {
|
@ -6,8 +6,7 @@ import { JobType } from '@prisma/client';
|
|||||||
import { Button } from '@tih/ui';
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
|
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
|
||||||
import OfferAnalysis from '~/components/offers/offersSubmission/analysis/OfferAnalysis';
|
import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave';
|
||||||
import OfferProfileSave from '~/components/offers/offersSubmission/OfferProfileSave';
|
|
||||||
import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm';
|
import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm';
|
||||||
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
|
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
|
||||||
import type {
|
import type {
|
||||||
@ -20,7 +19,12 @@ 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';
|
import OfferAnalysis from '../offerAnalysis/OfferAnalysis';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CreateOfferProfileResponse,
|
||||||
|
ProfileAnalysis,
|
||||||
|
} from '~/types/offers';
|
||||||
|
|
||||||
const defaultOfferValues = {
|
const defaultOfferValues = {
|
||||||
comments: '',
|
comments: '',
|
||||||
@ -78,6 +82,7 @@ export default function OffersSubmissionForm({
|
|||||||
id: profileId || '',
|
id: profileId || '',
|
||||||
token: token || '',
|
token: token || '',
|
||||||
});
|
});
|
||||||
|
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
|
||||||
|
|
||||||
const pageRef = useRef<HTMLDivElement>(null);
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollToTop = () =>
|
const scrollToTop = () =>
|
||||||
@ -88,6 +93,18 @@ export default function OffersSubmissionForm({
|
|||||||
});
|
});
|
||||||
const { handleSubmit, trigger } = formMethods;
|
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> = [
|
const formSteps: Array<FormStep> = [
|
||||||
{
|
{
|
||||||
component: (
|
component: (
|
||||||
@ -107,14 +124,21 @@ export default function OffersSubmissionForm({
|
|||||||
label: 'Background',
|
label: 'Background',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: <OfferAnalysis key={2} profileId={createProfileResponse.id} />,
|
component: (
|
||||||
|
<OfferAnalysis
|
||||||
|
key={2}
|
||||||
|
allAnalysis={analysis}
|
||||||
|
isError={generateAnalysisMutation.isError}
|
||||||
|
isLoading={generateAnalysisMutation.isLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
hasNext: true,
|
hasNext: true,
|
||||||
hasPrevious: false,
|
hasPrevious: false,
|
||||||
label: 'Analysis',
|
label: 'Analysis',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: (
|
component: (
|
||||||
<OfferProfileSave
|
<OffersProfileSave
|
||||||
key={3}
|
key={3}
|
||||||
profileId={createProfileResponse.id || ''}
|
profileId={createProfileResponse.id || ''}
|
||||||
token={createProfileResponse.token}
|
token={createProfileResponse.token}
|
||||||
@ -144,15 +168,6 @@ export default function OffersSubmissionForm({
|
|||||||
scrollToTop();
|
scrollToTop();
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateAnalysisMutation = trpc.useMutation(
|
|
||||||
['offers.analysis.generate'],
|
|
||||||
{
|
|
||||||
onError(error) {
|
|
||||||
console.error(error.message);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mutationpath =
|
const mutationpath =
|
||||||
profileId && token ? 'offers.profile.update' : 'offers.profile.create';
|
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}
|
required={true}
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`background.totalYoe`, {
|
{...register(`background.totalYoe`, {
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -52,7 +52,7 @@ function YoeSection() {
|
|||||||
label="Specific YOE 1"
|
label="Specific YOE 1"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`background.specificYoes.0.yoe`, {
|
{...register(`background.specificYoes.0.yoe`, {
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -68,7 +68,7 @@ function YoeSection() {
|
|||||||
label="Specific YOE 2"
|
label="Specific YOE 2"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`background.specificYoes.1.yoe`, {
|
{...register(`background.specificYoes.1.yoe`, {
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -128,7 +128,7 @@ function FullTimeJobFields() {
|
|||||||
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 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -158,7 +158,7 @@ function FullTimeJobFields() {
|
|||||||
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 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -211,7 +211,7 @@ function InternshipJobFields() {
|
|||||||
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 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -72,7 +72,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
placeholder={emptyOption}
|
placeholder={emptyOption}
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.offersFullTime.title`, {
|
{...register(`offers.${index}.offersFullTime.title`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
@ -81,7 +81,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
placeholder="e.g. Front End"
|
placeholder="e.g. Front End"
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.offersFullTime.specialization`, {
|
{...register(`offers.${index}.offersFullTime.specialization`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -99,7 +99,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
placeholder="e.g. L4, Junior"
|
placeholder="e.g. L4, Junior"
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.offersFullTime.level`, {
|
{...register(`offers.${index}.offersFullTime.level`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -112,7 +112,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
placeholder={emptyOption}
|
placeholder={emptyOption}
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.location`, {
|
{...register(`offers.${index}.location`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FormMonthYearPicker
|
<FormMonthYearPicker
|
||||||
@ -120,7 +120,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
monthRequired={true}
|
monthRequired={true}
|
||||||
yearLabel=""
|
yearLabel=""
|
||||||
{...register(`offers.${index}.monthYearReceived`, {
|
{...register(`offers.${index}.monthYearReceived`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -135,7 +135,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
{...register(
|
{...register(
|
||||||
`offers.${index}.offersFullTime.totalCompensation.currency`,
|
`offers.${index}.offersFullTime.totalCompensation.currency`,
|
||||||
{
|
{
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -153,8 +153,8 @@ function FullTimeOfferDetailsForm({
|
|||||||
{...register(
|
{...register(
|
||||||
`offers.${index}.offersFullTime.totalCompensation.value`,
|
`offers.${index}.offersFullTime.totalCompensation.value`,
|
||||||
{
|
{
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
@ -171,7 +171,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
{...register(
|
{...register(
|
||||||
`offers.${index}.offersFullTime.baseSalary.currency`,
|
`offers.${index}.offersFullTime.baseSalary.currency`,
|
||||||
{
|
{
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -185,8 +185,8 @@ function FullTimeOfferDetailsForm({
|
|||||||
startAddOnType="label"
|
startAddOnType="label"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`offers.${index}.offersFullTime.baseSalary.value`, {
|
{...register(`offers.${index}.offersFullTime.baseSalary.value`, {
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -198,7 +198,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
label="Currency"
|
label="Currency"
|
||||||
options={CURRENCY_OPTIONS}
|
options={CURRENCY_OPTIONS}
|
||||||
{...register(`offers.${index}.offersFullTime.bonus.currency`, {
|
{...register(`offers.${index}.offersFullTime.bonus.currency`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -211,8 +211,8 @@ function FullTimeOfferDetailsForm({
|
|||||||
startAddOnType="label"
|
startAddOnType="label"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`offers.${index}.offersFullTime.bonus.value`, {
|
{...register(`offers.${index}.offersFullTime.bonus.value`, {
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -226,7 +226,7 @@ function FullTimeOfferDetailsForm({
|
|||||||
label="Currency"
|
label="Currency"
|
||||||
options={CURRENCY_OPTIONS}
|
options={CURRENCY_OPTIONS}
|
||||||
{...register(`offers.${index}.offersFullTime.stocks.currency`, {
|
{...register(`offers.${index}.offersFullTime.stocks.currency`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -239,8 +239,8 @@ function FullTimeOfferDetailsForm({
|
|||||||
startAddOnType="label"
|
startAddOnType="label"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`offers.${index}.offersFullTime.stocks.value`, {
|
{...register(`offers.${index}.offersFullTime.stocks.value`, {
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -300,7 +300,7 @@ function InternshipOfferDetailsForm({
|
|||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.offersIntern.title`, {
|
{...register(`offers.${index}.offersIntern.title`, {
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
@ -310,7 +310,7 @@ function InternshipOfferDetailsForm({
|
|||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.offersIntern.specialization`, {
|
{...register(`offers.${index}.offersIntern.specialization`, {
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -330,7 +330,7 @@ function InternshipOfferDetailsForm({
|
|||||||
placeholder={emptyOption}
|
placeholder={emptyOption}
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.location`, {
|
{...register(`offers.${index}.location`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -343,7 +343,7 @@ function InternshipOfferDetailsForm({
|
|||||||
placeholder={emptyOption}
|
placeholder={emptyOption}
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.offersIntern.internshipCycle`, {
|
{...register(`offers.${index}.offersIntern.internshipCycle`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
@ -354,7 +354,7 @@ function InternshipOfferDetailsForm({
|
|||||||
placeholder={emptyOption}
|
placeholder={emptyOption}
|
||||||
required={true}
|
required={true}
|
||||||
{...register(`offers.${index}.offersIntern.startYear`, {
|
{...register(`offers.${index}.offersIntern.startYear`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -365,7 +365,7 @@ function InternshipOfferDetailsForm({
|
|||||||
monthRequired={true}
|
monthRequired={true}
|
||||||
yearLabel=""
|
yearLabel=""
|
||||||
{...register(`offers.${index}.monthYearReceived`, {
|
{...register(`offers.${index}.monthYearReceived`, {
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -380,7 +380,7 @@ function InternshipOfferDetailsForm({
|
|||||||
{...register(
|
{...register(
|
||||||
`offers.${index}.offersIntern.monthlySalary.currency`,
|
`offers.${index}.offersIntern.monthlySalary.currency`,
|
||||||
{
|
{
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -396,8 +396,8 @@ function InternshipOfferDetailsForm({
|
|||||||
startAddOnType="label"
|
startAddOnType="label"
|
||||||
type="number"
|
type="number"
|
||||||
{...register(`offers.${index}.offersIntern.monthlySalary.value`, {
|
{...register(`offers.${index}.offersIntern.monthlySalary.value`, {
|
||||||
min: { message: FieldError.NonNegativeNumber, value: 0 },
|
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
|
||||||
required: FieldError.Required,
|
required: FieldError.REQUIRED,
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -58,52 +58,64 @@ export default function OfferCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BottomSection() {
|
function BottomSection() {
|
||||||
return (
|
if (
|
||||||
<div className="px-8">
|
!totalCompensation &&
|
||||||
<div className="flex flex-col py-2">
|
!monthlySalary &&
|
||||||
<div className="flex flex-row">
|
!negotiationStrategy &&
|
||||||
<CurrencyDollarIcon className="mr-1 h-5" />
|
!otherComment
|
||||||
<p>
|
) {
|
||||||
{totalCompensation
|
return null;
|
||||||
? `TC: ${totalCompensation}`
|
}
|
||||||
: `Monthly Salary: ${monthlySalary}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{totalCompensation && (
|
return (
|
||||||
<div className="ml-6 flex flex-row font-light text-gray-400">
|
<>
|
||||||
<p>
|
<HorizontalDivider />
|
||||||
Base / year: {base} ⋅ Stocks / year: {stocks} ⋅ Bonus / year:{' '}
|
<div className="px-8">
|
||||||
{bonus}
|
<div className="flex flex-col py-2">
|
||||||
</p>
|
{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>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
)}
|
)}
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx-8 my-4 block rounded-lg bg-white py-4 shadow-md">
|
<div className="mx-8 my-4 block rounded-lg bg-white py-4 shadow-md">
|
||||||
<UpperSection />
|
<UpperSection />
|
||||||
<HorizontalDivider />
|
|
||||||
<BottomSection />
|
<BottomSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { AcademicCapIcon, BriefcaseIcon } from '@heroicons/react/24/outline';
|
import { useState } from 'react';
|
||||||
import { Spinner } from '@tih/ui';
|
import {
|
||||||
|
AcademicCapIcon,
|
||||||
|
ArrowPathIcon,
|
||||||
|
BriefcaseIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { Button, Spinner } from '@tih/ui';
|
||||||
|
|
||||||
import EducationCard from '~/components/offers/profile/EducationCard';
|
import EducationCard from '~/components/offers/profile/EducationCard';
|
||||||
import OfferCard from '~/components/offers/profile/OfferCard';
|
import OfferCard from '~/components/offers/profile/OfferCard';
|
||||||
@ -8,22 +13,143 @@ import type {
|
|||||||
OfferDisplayData,
|
OfferDisplayData,
|
||||||
} from '~/components/offers/types';
|
} 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;
|
analysis?: ProfileAnalysis;
|
||||||
background?: BackgroundDisplayData;
|
background?: BackgroundDisplayData;
|
||||||
|
isEditable: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
offers: Array<OfferDisplayData>;
|
offers: Array<OfferDisplayData>;
|
||||||
selectedTab: string;
|
profileId: string;
|
||||||
|
selectedTab: ProfileDetailTab;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function ProfileDetails({
|
export default function ProfileDetails({
|
||||||
|
analysis,
|
||||||
background,
|
background,
|
||||||
isLoading,
|
isLoading,
|
||||||
offers,
|
offers,
|
||||||
selectedTab,
|
selectedTab,
|
||||||
}: ProfileHeaderProps) {
|
profileId,
|
||||||
|
isEditable,
|
||||||
|
}: ProfileDetailsProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-10 pt-4">
|
<div className="col-span-10 pt-4">
|
||||||
@ -31,46 +157,20 @@ export default function ProfileDetails({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (selectedTab === 'offers') {
|
if (selectedTab === ProfileDetailTab.OFFERS) {
|
||||||
if (offers.length !== 0) {
|
return <ProfileOffers offers={offers} />;
|
||||||
return (
|
}
|
||||||
<>
|
if (selectedTab === ProfileDetailTab.BACKGROUND) {
|
||||||
{offers.map((offer) => (
|
return <ProfileBackground background={background} />;
|
||||||
<OfferCard key={offer.id} offer={offer} />
|
}
|
||||||
))}
|
if (selectedTab === ProfileDetailTab.ANALYSIS) {
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-8 my-4 flex flex-row">
|
<ProfileAnalysis
|
||||||
<BriefcaseIcon className="mr-1 h-5" />
|
analysis={analysis}
|
||||||
<span className="font-bold">No offer is attached.</span>
|
isEditable={isEditable}
|
||||||
</div>
|
profileId={profileId}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (selectedTab === 'background') {
|
return null;
|
||||||
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]} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <div>Detail page for {selectedTab}</div>;
|
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,16 @@ import type { BackgroundDisplayData } from '~/components/offers/types';
|
|||||||
|
|
||||||
import { getProfileEditPath } from '~/utils/offers/link';
|
import { getProfileEditPath } from '~/utils/offers/link';
|
||||||
|
|
||||||
|
import type { ProfileDetailTab } from '../constants';
|
||||||
|
import { profileDetailTabs } from '../constants';
|
||||||
|
|
||||||
type ProfileHeaderProps = Readonly<{
|
type ProfileHeaderProps = Readonly<{
|
||||||
background?: BackgroundDisplayData;
|
background?: BackgroundDisplayData;
|
||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
selectedTab: string;
|
selectedTab: ProfileDetailTab;
|
||||||
setSelectedTab: (tab: string) => void;
|
setSelectedTab: (tab: ProfileDetailTab) => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function ProfileHeader({
|
export default function ProfileHeader({
|
||||||
@ -139,9 +142,9 @@ export default function ProfileHeader({
|
|||||||
<BuildingOffice2Icon className="mr-2.5 h-5" />
|
<BuildingOffice2Icon className="mr-2.5 h-5" />
|
||||||
<span className="mr-2 font-bold">Current:</span>
|
<span className="mr-2 font-bold">Current:</span>
|
||||||
<span>
|
<span>
|
||||||
{`${experiences[0]?.companyName || ''} ${
|
{`${experiences[0].companyName || ''} ${
|
||||||
experiences[0]?.jobLevel || ''
|
experiences[0].jobLevel || ''
|
||||||
} ${experiences[0]?.jobTitle || ''}`}
|
} ${experiences[0].jobTitle || ''}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -165,20 +168,7 @@ export default function ProfileHeader({
|
|||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Tabs
|
<Tabs
|
||||||
label="Profile Detail Navigation"
|
label="Profile Detail Navigation"
|
||||||
tabs={[
|
tabs={profileDetailTabs}
|
||||||
{
|
|
||||||
label: 'Offers',
|
|
||||||
value: 'offers',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Background',
|
|
||||||
value: 'background',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Offer Engine Analysis',
|
|
||||||
value: 'offerEngineAnalysis',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={selectedTab}
|
value={selectedTab}
|
||||||
onChange={(value) => setSelectedTab(value)}
|
onChange={(value) => setSelectedTab(value)}
|
||||||
/>
|
/>
|
||||||
|
@ -2,6 +2,7 @@ import Error from 'next/error';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { ProfileDetailTab } from '~/components/offers/constants';
|
||||||
import ProfileComments from '~/components/offers/profile/ProfileComments';
|
import ProfileComments from '~/components/offers/profile/ProfileComments';
|
||||||
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
|
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
|
||||||
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
|
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
|
||||||
@ -27,7 +28,9 @@ export default function OfferProfile() {
|
|||||||
const [background, setBackground] = useState<BackgroundDisplayData>();
|
const [background, setBackground] = useState<BackgroundDisplayData>();
|
||||||
const [offers, setOffers] = useState<Array<OfferDisplayData>>([]);
|
const [offers, setOffers] = useState<Array<OfferDisplayData>>([]);
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState('offers');
|
const [selectedTab, setSelectedTab] = useState<ProfileDetailTab>(
|
||||||
|
ProfileDetailTab.OFFERS,
|
||||||
|
);
|
||||||
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
|
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
|
||||||
|
|
||||||
const getProfileQuery = trpc.useQuery(
|
const getProfileQuery = trpc.useQuery(
|
||||||
@ -163,8 +166,10 @@ export default function OfferProfile() {
|
|||||||
<ProfileDetails
|
<ProfileDetails
|
||||||
analysis={analysis}
|
analysis={analysis}
|
||||||
background={background}
|
background={background}
|
||||||
|
isEditable={isEditable}
|
||||||
isLoading={getProfileQuery.isLoading}
|
isLoading={getProfileQuery.isLoading}
|
||||||
offers={offers}
|
offers={offers}
|
||||||
|
profileId={offerProfileId as string}
|
||||||
selectedTab={selectedTab}
|
selectedTab={selectedTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -321,18 +321,18 @@ export const offersAnalysisRouter = createRouter()
|
|||||||
similarOffers,
|
similarOffers,
|
||||||
);
|
);
|
||||||
const overallPercentile =
|
const overallPercentile =
|
||||||
similarOffers.length === 0
|
similarOffers.length <= 1
|
||||||
? 100
|
? 100
|
||||||
: (100 * overallIndex) / similarOffers.length;
|
: 100 - (100 * overallIndex) / (similarOffers.length - 1);
|
||||||
|
|
||||||
const companyIndex = searchOfferPercentile(
|
const companyIndex = searchOfferPercentile(
|
||||||
overallHighestOffer,
|
overallHighestOffer,
|
||||||
similarCompanyOffers,
|
similarCompanyOffers,
|
||||||
);
|
);
|
||||||
const companyPercentile =
|
const companyPercentile =
|
||||||
similarCompanyOffers.length === 0
|
similarCompanyOffers.length <= 1
|
||||||
? 100
|
? 100
|
||||||
: (100 * companyIndex) / similarCompanyOffers.length;
|
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
|
||||||
|
|
||||||
// FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE
|
// FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE
|
||||||
// e.g. If there only 4 offers, it gives the 2nd and 3rd offer
|
// e.g. If there only 4 offers, it gives the 2nd and 3rd offer
|
||||||
|
Reference in New Issue
Block a user