[offers][feat] Tweak UI and add View More button to OEA (#507)

* [offers][feat] Tweak UI and minor refactoring

* [offers][feat] Add view more button to OEA
pull/508/head
Ai Ling 2 years ago committed by GitHub
parent f4e5d2ddb1
commit fec915cffa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -63,7 +63,7 @@ export default function DashboardProfileCard({
{profileName} {profileName}
</h2> </h2>
<p className="text-sm text-slate-500"> <p className="text-sm text-slate-500">
<span>Created at {formatDate(createdAt)}</span> <span>Created in {formatDate(createdAt)}</span>
</p> </p>
</div> </div>
</div> </div>

@ -1,10 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { Alert, HorizontalDivider, Spinner, Tabs } from '@tih/ui'; import { ArrowUpRightIcon } from '@heroicons/react/24/outline';
import { JobType } from '@prisma/client';
import { Alert, Button, HorizontalDivider, Spinner, Tabs } from '@tih/ui';
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 { YOE_CATEGORY } from '../table/types';
import type { AnalysisUnit, ProfileAnalysis } from '~/types/offers'; import type { AnalysisUnit, ProfileAnalysis } from '~/types/offers';
@ -19,6 +22,16 @@ function OfferAnalysisContent({
tab, tab,
isSubmission, isSubmission,
}: OfferAnalysisContentProps) { }: OfferAnalysisContentProps) {
const { companyId, companyName, title, totalYoe, jobType } = analysis;
const yoeCategory =
jobType === JobType.INTERN
? ''
: totalYoe <= 2
? YOE_CATEGORY.ENTRY
: totalYoe <= 5
? YOE_CATEGORY.MID
: YOE_CATEGORY.SENIOR;
if (!analysis || analysis.noOfOffers === 0) { if (!analysis || analysis.noOfOffers === 0) {
if (tab === OVERALL_TAB) { if (tab === OVERALL_TAB) {
return ( return (
@ -55,15 +68,22 @@ function OfferAnalysisContent({
offerProfile={topPercentileOffer} offerProfile={topPercentileOffer}
/> />
))} ))}
{/* {offerAnalysis.topPercentileOffers.length > 0 && ( {analysis.topPercentileOffers.length > 0 && (
<div className="mb-4 flex justify-end"> <div className="mb-4 flex justify-end">
<Button <Button
icon={EllipsisHorizontalIcon} href={
tab === OVERALL_TAB
? `/offers?jobTitleId=${title}&sortBy=-totalCompensation&yoeCategory=${yoeCategory}`
: `/offers?companyId=${companyId}&companyName=${companyName}&jobTitleId=${title}&sortBy=-totalCompensation&yoeCategory=${yoeCategory}`
}
icon={ArrowUpRightIcon}
label="View more offers" label="View more offers"
rel="noreferrer"
target="_blank"
variant="tertiary" variant="tertiary"
/> />
</div> </div>
)} */} )}
</> </>
); );
} }

@ -1,4 +1,10 @@
import { import {
ArrowTrendingUpIcon,
BuildingOfficeIcon,
MapPinIcon,
} from '@heroicons/react/20/solid';
import {
ArrowTopRightOnSquareIcon,
BuildingOffice2Icon, BuildingOffice2Icon,
CalendarDaysIcon, CalendarDaysIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
@ -7,9 +13,8 @@ import { JobType } from '@prisma/client';
import type { JobTitleType } from '~/components/shared/JobTitles'; import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
import { HorizontalDivider } from '~/../../../packages/ui/dist'; import { Button } from '~/../../../packages/ui/dist';
import { convertMoneyToString } from '~/utils/offers/currency'; import { convertMoneyToString } from '~/utils/offers/currency';
import { getCompanyDisplayText } from '~/utils/offers/string';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { JobTypeLabel } from '../constants'; import { JobTypeLabel } from '../constants';
@ -36,52 +41,109 @@ export default function OfferProfileCard({
profileId, profileId,
}, },
}: OfferProfileCardProps) { }: OfferProfileCardProps) {
return ( function UpperSection() {
<a return (
className="my-5 block rounded-lg border bg-white p-4 px-8 shadow-md" <div className="border-b px-4 py-5 sm:px-6">
href={`/offers/profile/${profileId}`} <div className="-ml-4 -mt-4 flex flex-wrap items-center justify-between sm:flex-nowrap">
rel="noreferrer" <div className="ml-4 mt-4">
target="_blank"> <div className="flex items-center">
<div className="flex items-center gap-x-5"> <div className="flex-shrink-0">
<div> <ProfilePhotoHolder size="sm" />
<ProfilePhotoHolder size="sm" /> </div>
</div> <div className="ml-4">
<div className="col-span-10"> <h2 className="text-lg font-medium leading-6 text-slate-900">
<p className="font-bold">{profileName}</p> {profileName}
{previousCompanies.length > 0 && ( </h2>
<div className="flex flex-row"> <p className="flex text-sm text-slate-500">
<BuildingOffice2Icon className="mr-2 h-5" /> <CalendarDaysIcon className="mr-2 h-5" />
<span className="mr-2 font-bold">Current:</span> <span className="mr-2 font-bold">YOE:</span>
<span>{previousCompanies[0]}</span> <span>{totalYoe}</span>
{previousCompanies.length > 0 && (
<>
<BuildingOffice2Icon className="ml-4 mr-2 h-5" />
<span className="mr-2 font-bold">Previous:</span>
<span>{previousCompanies[0]}</span>
</>
)}
</p>
</div>
</div> </div>
)} </div>
<div className="flex flex-row"> <div className="ml-4 mt-4 flex flex-shrink-0">
<CalendarDaysIcon className="mr-2 h-5" /> <Button
<span className="mr-2 font-bold">YOE:</span> href={`/offers/profile/${profileId}`}
<span>{totalYoe}</span> icon={ArrowTopRightOnSquareIcon}
isLabelHidden={true}
label="View Profile"
rel="noreferrer"
size="md"
target="_blank"
variant="tertiary"
/>
</div> </div>
</div> </div>
</div> </div>
);
}
<HorizontalDivider /> function BottomSection() {
<div className="flex items-end justify-between"> return (
<div className="col-span-1 row-span-3"> <div className="px-4 py-4 sm:px-6">
<p className="font-bold"> <div className="flex items-end justify-between">
{getLabelForJobTitleType(title as JobTitleType)}{' '} <div className="col-span-1 row-span-3">
{`(${JobTypeLabel[jobType]})`} <h4 className="font-medium">
</p> {getLabelForJobTitleType(title as JobTitleType)}{' '}
<p>{`Company: ${getCompanyDisplayText(company.name, location)}`}</p> {jobType && <>({JobTypeLabel[jobType]})</>}
{level && <p>Level: {level}</p>} </h4>
</div> <div className="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-4">
<div className="col-span-1 row-span-3"> {company?.name && (
<p className="text-end">{formatDate(monthYearReceived)}</p> <div className="mt-2 flex items-center text-sm text-slate-500">
<p className="text-end text-xl"> <BuildingOfficeIcon
{jobType === JobType.FULLTIME aria-hidden="true"
? `${convertMoneyToString(income)} / year` className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
: `${convertMoneyToString(income)} / month`} />
</p> {company.name}
</div>
)}
{location && (
<div className="mt-2 flex items-center text-sm text-slate-500">
<MapPinIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/>
{location.cityName}
</div>
)}
{level && (
<div className="mt-2 flex items-center text-sm text-slate-500">
<ArrowTrendingUpIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/>
{level}
</div>
)}
</div>
</div>
<div className="col-span-1 row-span-3">
<p className="text-end text-lg font-medium leading-6 text-slate-900">
{jobType === JobType.FULLTIME
? `${convertMoneyToString(income)} / year`
: `${convertMoneyToString(income)} / month`}
</p>
<p className="text-end text-sm text-slate-500">
{formatDate(monthYearReceived)}
</p>
</div>
</div> </div>
</div> </div>
</a> );
}
return (
<div className="my-5 block rounded-lg border border-slate-200 bg-white">
<UpperSection />
<BottomSection />
</div>
); );
} }

@ -13,10 +13,12 @@ import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm'; import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
import type { import type {
OfferFormData, OfferFormData,
OfferPostData,
OffersProfileFormData, OffersProfileFormData,
} from '~/components/offers/types'; } from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker'; import type { Month } from '~/components/shared/MonthYearPicker';
import { Currency } from '~/utils/offers/currency/CurrencyEnum';
import { import {
cleanObject, cleanObject,
removeEmptyObjects, removeEmptyObjects,
@ -25,6 +27,8 @@ import {
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time'; import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
export const DEFAULT_CURRENCY = Currency.SGD;
const defaultOfferValues = { const defaultOfferValues = {
cityId: '', cityId: '',
comments: '', comments: '',
@ -43,21 +47,17 @@ export const defaultFullTimeOfferValues = {
jobType: JobType.FULLTIME, jobType: JobType.FULLTIME,
offersFullTime: { offersFullTime: {
baseSalary: { baseSalary: {
currency: 'SGD', currency: DEFAULT_CURRENCY,
value: null,
}, },
bonus: { bonus: {
currency: 'SGD', currency: DEFAULT_CURRENCY,
value: null,
}, },
level: '', level: '',
stocks: { stocks: {
currency: 'SGD', currency: DEFAULT_CURRENCY,
value: null,
}, },
totalCompensation: { totalCompensation: {
currency: 'SGD', currency: DEFAULT_CURRENCY,
value: null,
}, },
}, },
}; };
@ -66,16 +66,15 @@ export const defaultInternshipOfferValues = {
...defaultOfferValues, ...defaultOfferValues,
jobType: JobType.INTERN, jobType: JobType.INTERN,
offersIntern: { offersIntern: {
internshipCycle: null, internshipCycle: '',
monthlySalary: { monthlySalary: {
currency: 'SGD', currency: DEFAULT_CURRENCY,
value: null,
}, },
startYear: null, startYear: null,
}, },
}; };
const defaultOfferProfileValues = { const defaultOfferProfileValues: OffersProfileFormData = {
background: { background: {
educations: [], educations: [],
experiences: [{ jobType: JobType.FULLTIME }], experiences: [{ jobType: JobType.FULLTIME }],
@ -116,7 +115,7 @@ export default function OffersSubmissionForm({
const { const {
handleSubmit, handleSubmit,
trigger, trigger,
formState: { isSubmitting }, formState: { isSubmitting, isDirty },
} = formMethods; } = formMethods;
const generateAnalysisMutation = trpc.useMutation( const generateAnalysisMutation = trpc.useMutation(
@ -218,7 +217,7 @@ export default function OffersSubmissionForm({
offer.monthYearReceived.year, offer.monthYearReceived.year,
offer.monthYearReceived.month - 1, // Convert month to monthIndex offer.monthYearReceived.month - 1, // Convert month to monthIndex
), ),
})); })) as Array<OfferPostData>;
if (params.profileId && params.token) { if (params.profileId && params.token) {
createOrUpdateMutation.mutate({ createOrUpdateMutation.mutate({
@ -254,11 +253,14 @@ export default function OffersSubmissionForm({
const warningText = const warningText =
'Leave this page? Changes that you made will not be saved.'; 'Leave this page? Changes that you made will not be saved.';
const handleWindowClose = (e: BeforeUnloadEvent) => { const handleWindowClose = (e: BeforeUnloadEvent) => {
if (!isDirty) {
return;
}
e.preventDefault(); e.preventDefault();
return (e.returnValue = warningText); return (e.returnValue = warningText);
}; };
const handleRouteChange = (url: string) => { const handleRouteChange = (url: string) => {
if (url.includes('/offers/submit/result')) { if (url.includes('/offers/submit/result') || !isDirty) {
return; return;
} }
if (window.confirm(warningText)) { if (window.confirm(warningText)) {
@ -274,7 +276,7 @@ export default function OffersSubmissionForm({
router.events.off('routeChangeStart', handleRouteChange); router.events.off('routeChangeStart', handleRouteChange);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [isDirty]);
return generateAnalysisMutation.isLoading ? ( return generateAnalysisMutation.isLoading ? (
<Spinner className="m-10" display="block" size="lg" /> <Spinner className="m-10" display="block" size="lg" />

@ -117,6 +117,7 @@ function FullTimeOfferDetailsForm({
onSelect={(option) => { onSelect={(option) => {
if (option) { if (option) {
setValue(`offers.${index}.companyId`, option.value); setValue(`offers.${index}.companyId`, option.value);
setValue(`offers.${index}.companyName`, option.label);
} }
}} }}
/> />
@ -550,7 +551,6 @@ export default function OfferDetailsForm() {
if (newJobType === jobType) { if (newJobType === jobType) {
return; return;
} }
setDialogOpen(true); setDialogOpen(true);
}} }}
/> />

@ -131,9 +131,8 @@ function ProfileAnalysis({
{isEditable && ( {isEditable && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
addonPosition="start"
icon={ArrowPathIcon} icon={ArrowPathIcon}
label="Regenerate Analysis" label="Regenerate analysis"
variant="secondary" variant="secondary"
onClick={() => generateAnalysisMutation.mutate({ profileId })} onClick={() => generateAnalysisMutation.mutate({ profileId })}
/> />

@ -4,16 +4,99 @@ import type { MonthYear } from '~/components/shared/MonthYearPicker';
import type { Location } from '~/types/offers'; import type { Location } from '~/types/offers';
export type OffersProfilePostData = { /**
background: BackgroundPostData; * Form data types
*/
export type OffersProfileFormData = {
background: BackgroundFormData;
id?: string; id?: string;
offers: Array<OfferPostData>; offers: Array<OfferFormData>;
}; };
export type OffersProfileFormData = { export type BackgroundFormData = {
educations: Array<EducationFormData>;
experiences: Array<ExperienceFormData>;
id?: string;
specificYoes: Array<SpecificYoeFormData>;
totalYoe: number;
};
type EducationFormData = {
endDate?: Date | null;
field?: string | null;
school?: string | null;
startDate?: Date | null;
type?: string | null;
};
type ExperienceFormData = {
cityId?: string | null;
cityName?: string | null;
companyId?: string | null;
companyName?: string | null;
durationInMonths?: number | null;
id?: string;
jobType?: string | null;
level?: string | null;
monthlySalary?: MoneyFormData | null;
title?: string | null;
totalCompensation?: MoneyFormData | null;
totalCompensationId?: string | null;
};
type SpecificYoeFormData = {
domain: string;
id?: string;
yoe: number;
};
export type OfferFormData = {
cityId: string;
cityName?: string;
comments: string;
companyId: string;
companyName?: string;
id?: string;
jobType: JobType;
monthYearReceived: MonthYear;
negotiationStrategy: string;
offersFullTime?: OfferFullTimeFormData | null;
offersIntern?: OfferInternFormData | null;
};
export type OfferFullTimeFormData = {
baseSalary?: MoneyFormData | null;
bonus?: MoneyFormData | null;
id?: string;
level: string;
stocks?: MoneyFormData | null;
title: string;
totalCompensation: MoneyFormData;
};
export type OfferInternFormData = {
id?: string;
internshipCycle: string;
monthlySalary: MoneyFormData;
startYear: number;
title: string;
};
type MoneyFormData = {
currency: string;
id?: string;
value?: number;
};
/**
* Post request data types
*/
export type OffersProfilePostData = {
background: BackgroundPostData; background: BackgroundPostData;
id?: string; id?: string;
offers: Array<OfferFormData>; offers: Array<OfferPostData>;
}; };
export type BackgroundPostData = { export type BackgroundPostData = {
@ -24,6 +107,8 @@ export type BackgroundPostData = {
totalYoe: number; totalYoe: number;
}; };
type EducationPostData = EducationFormData;
type ExperiencePostData = { type ExperiencePostData = {
cityId?: string | null; cityId?: string | null;
cityName?: string | null; cityName?: string | null;
@ -39,47 +124,26 @@ type ExperiencePostData = {
totalCompensationId?: string | null; totalCompensationId?: string | null;
}; };
type EducationPostData = { type SpecificYoePostData = SpecificYoeFormData;
endDate?: Date | null;
field?: string | null;
id?: string;
school?: string | null;
startDate?: Date | null;
type?: string | null;
};
type SpecificYoePostData = {
domain: string;
id?: string;
yoe: number;
};
type SpecificYoe = SpecificYoePostData;
export type OfferPostData = { export type OfferPostData = {
cityId: string; cityId: string;
cityName?: string;
comments: string; comments: string;
companyId: string; companyId: string;
companyName?: string;
id?: string; id?: string;
jobType: JobType; jobType: JobType;
monthYearReceived: Date; monthYearReceived: Date;
negotiationStrategy: string; negotiationStrategy: string;
offersFullTime?: OfferFullTimePostData | null; offersFullTime?: OfferFullTimePostData;
offersIntern?: OfferInternPostData | null; offersIntern?: OfferInternPostData;
};
export type OfferFormData = Omit<OfferPostData, 'monthYearReceived'> & {
monthYearReceived: MonthYear;
}; };
export type OfferFullTimePostData = { export type OfferFullTimePostData = {
baseSalary: Money | null; baseSalary: Money;
bonus: Money | null; bonus: Money;
id?: string; id?: string;
level: string; level: string;
stocks: Money | null; stocks: Money;
title: string; title: string;
totalCompensation: Money; totalCompensation: Money;
}; };
@ -98,6 +162,10 @@ export type Money = {
value: number; value: number;
}; };
/**
* Display data types
*/
export type EducationDisplayData = { export type EducationDisplayData = {
endDate?: string | null; endDate?: string | null;
field?: string | null; field?: string | null;
@ -128,7 +196,7 @@ export type BackgroundDisplayData = {
educations: Array<EducationDisplayData>; educations: Array<EducationDisplayData>;
experiences: Array<OfferDisplayData>; experiences: Array<OfferDisplayData>;
profileName: string; profileName: string;
specificYoes: Array<SpecificYoe>; specificYoes: Array<SpecificYoePostData>;
totalYoe: number; totalYoe: number;
}; };

@ -3,7 +3,9 @@ import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm'; import OffersSubmissionForm, {
DEFAULT_CURRENCY,
} from '~/components/offers/offersSubmission/OffersSubmissionForm';
import type { OffersProfileFormData } from '~/components/offers/types'; import type { OffersProfileFormData } from '~/components/offers/types';
import { Spinner } from '~/../../../packages/ui/dist'; import { Spinner } from '~/../../../packages/ui/dist';
@ -44,9 +46,16 @@ export default function OffersEditPage() {
id: exp.id, id: exp.id,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
monthlySalary: exp.monthlySalary, monthlySalary: {
currency: exp.monthlySalary?.currency || DEFAULT_CURRENCY,
value: exp.monthlySalary?.value,
},
title: exp.title, title: exp.title,
totalCompensation: exp.totalCompensation, totalCompensation: {
currency:
exp.totalCompensation?.currency || DEFAULT_CURRENCY,
value: exp.totalCompensation?.value,
},
})), })),
id, id,
specificYoes, specificYoes,

Loading…
Cancel
Save