[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}
</h2>
<p className="text-sm text-slate-500">
<span>Created at {formatDate(createdAt)}</span>
<span>Created in {formatDate(createdAt)}</span>
</p>
</div>
</div>

@ -1,10 +1,13 @@
import { useEffect } 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 OfferProfileCard from './OfferProfileCard';
import { OVERALL_TAB } from '../constants';
import { YOE_CATEGORY } from '../table/types';
import type { AnalysisUnit, ProfileAnalysis } from '~/types/offers';
@ -19,6 +22,16 @@ function OfferAnalysisContent({
tab,
isSubmission,
}: 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 (tab === OVERALL_TAB) {
return (
@ -55,15 +68,22 @@ function OfferAnalysisContent({
offerProfile={topPercentileOffer}
/>
))}
{/* {offerAnalysis.topPercentileOffers.length > 0 && (
{analysis.topPercentileOffers.length > 0 && (
<div className="mb-4 flex justify-end">
<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"
rel="noreferrer"
target="_blank"
variant="tertiary"
/>
</div>
)} */}
)}
</>
);
}

@ -1,4 +1,10 @@
import {
ArrowTrendingUpIcon,
BuildingOfficeIcon,
MapPinIcon,
} from '@heroicons/react/20/solid';
import {
ArrowTopRightOnSquareIcon,
BuildingOffice2Icon,
CalendarDaysIcon,
} from '@heroicons/react/24/outline';
@ -7,9 +13,8 @@ import { JobType } from '@prisma/client';
import type { JobTitleType } 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 { getCompanyDisplayText } from '~/utils/offers/string';
import { formatDate } from '~/utils/offers/time';
import { JobTypeLabel } from '../constants';
@ -36,52 +41,109 @@ export default function OfferProfileCard({
profileId,
},
}: OfferProfileCardProps) {
return (
<a
className="my-5 block rounded-lg border bg-white p-4 px-8 shadow-md"
href={`/offers/profile/${profileId}`}
rel="noreferrer"
target="_blank">
<div className="flex items-center gap-x-5">
<div>
<ProfilePhotoHolder size="sm" />
</div>
<div className="col-span-10">
<p className="font-bold">{profileName}</p>
{previousCompanies.length > 0 && (
<div className="flex flex-row">
<BuildingOffice2Icon className="mr-2 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>{previousCompanies[0]}</span>
function UpperSection() {
return (
<div className="border-b px-4 py-5 sm:px-6">
<div className="-ml-4 -mt-4 flex flex-wrap items-center justify-between sm:flex-nowrap">
<div className="ml-4 mt-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<ProfilePhotoHolder size="sm" />
</div>
<div className="ml-4">
<h2 className="text-lg font-medium leading-6 text-slate-900">
{profileName}
</h2>
<p className="flex text-sm text-slate-500">
<CalendarDaysIcon className="mr-2 h-5" />
<span className="mr-2 font-bold">YOE:</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 className="flex flex-row">
<CalendarDaysIcon className="mr-2 h-5" />
<span className="mr-2 font-bold">YOE:</span>
<span>{totalYoe}</span>
</div>
<div className="ml-4 mt-4 flex flex-shrink-0">
<Button
href={`/offers/profile/${profileId}`}
icon={ArrowTopRightOnSquareIcon}
isLabelHidden={true}
label="View Profile"
rel="noreferrer"
size="md"
target="_blank"
variant="tertiary"
/>
</div>
</div>
</div>
);
}
<HorizontalDivider />
<div className="flex items-end justify-between">
<div className="col-span-1 row-span-3">
<p className="font-bold">
{getLabelForJobTitleType(title as JobTitleType)}{' '}
{`(${JobTypeLabel[jobType]})`}
</p>
<p>{`Company: ${getCompanyDisplayText(company.name, location)}`}</p>
{level && <p>Level: {level}</p>}
</div>
<div className="col-span-1 row-span-3">
<p className="text-end">{formatDate(monthYearReceived)}</p>
<p className="text-end text-xl">
{jobType === JobType.FULLTIME
? `${convertMoneyToString(income)} / year`
: `${convertMoneyToString(income)} / month`}
</p>
function BottomSection() {
return (
<div className="px-4 py-4 sm:px-6">
<div className="flex items-end justify-between">
<div className="col-span-1 row-span-3">
<h4 className="font-medium">
{getLabelForJobTitleType(title as JobTitleType)}{' '}
{jobType && <>({JobTypeLabel[jobType]})</>}
</h4>
<div className="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-4">
{company?.name && (
<div className="mt-2 flex items-center text-sm text-slate-500">
<BuildingOfficeIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/>
{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>
</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 type {
OfferFormData,
OfferPostData,
OffersProfileFormData,
} from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker';
import { Currency } from '~/utils/offers/currency/CurrencyEnum';
import {
cleanObject,
removeEmptyObjects,
@ -25,6 +27,8 @@ import {
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
export const DEFAULT_CURRENCY = Currency.SGD;
const defaultOfferValues = {
cityId: '',
comments: '',
@ -43,21 +47,17 @@ export const defaultFullTimeOfferValues = {
jobType: JobType.FULLTIME,
offersFullTime: {
baseSalary: {
currency: 'SGD',
value: null,
currency: DEFAULT_CURRENCY,
},
bonus: {
currency: 'SGD',
value: null,
currency: DEFAULT_CURRENCY,
},
level: '',
stocks: {
currency: 'SGD',
value: null,
currency: DEFAULT_CURRENCY,
},
totalCompensation: {
currency: 'SGD',
value: null,
currency: DEFAULT_CURRENCY,
},
},
};
@ -66,16 +66,15 @@ export const defaultInternshipOfferValues = {
...defaultOfferValues,
jobType: JobType.INTERN,
offersIntern: {
internshipCycle: null,
internshipCycle: '',
monthlySalary: {
currency: 'SGD',
value: null,
currency: DEFAULT_CURRENCY,
},
startYear: null,
},
};
const defaultOfferProfileValues = {
const defaultOfferProfileValues: OffersProfileFormData = {
background: {
educations: [],
experiences: [{ jobType: JobType.FULLTIME }],
@ -116,7 +115,7 @@ export default function OffersSubmissionForm({
const {
handleSubmit,
trigger,
formState: { isSubmitting },
formState: { isSubmitting, isDirty },
} = formMethods;
const generateAnalysisMutation = trpc.useMutation(
@ -218,7 +217,7 @@ export default function OffersSubmissionForm({
offer.monthYearReceived.year,
offer.monthYearReceived.month - 1, // Convert month to monthIndex
),
}));
})) as Array<OfferPostData>;
if (params.profileId && params.token) {
createOrUpdateMutation.mutate({
@ -254,11 +253,14 @@ export default function OffersSubmissionForm({
const warningText =
'Leave this page? Changes that you made will not be saved.';
const handleWindowClose = (e: BeforeUnloadEvent) => {
if (!isDirty) {
return;
}
e.preventDefault();
return (e.returnValue = warningText);
};
const handleRouteChange = (url: string) => {
if (url.includes('/offers/submit/result')) {
if (url.includes('/offers/submit/result') || !isDirty) {
return;
}
if (window.confirm(warningText)) {
@ -274,7 +276,7 @@ export default function OffersSubmissionForm({
router.events.off('routeChangeStart', handleRouteChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [isDirty]);
return generateAnalysisMutation.isLoading ? (
<Spinner className="m-10" display="block" size="lg" />

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

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

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

@ -3,7 +3,9 @@ import { useRouter } from 'next/router';
import { useState } from 'react';
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 { Spinner } from '~/../../../packages/ui/dist';
@ -44,9 +46,16 @@ export default function OffersEditPage() {
id: exp.id,
jobType: exp.jobType,
level: exp.level,
monthlySalary: exp.monthlySalary,
monthlySalary: {
currency: exp.monthlySalary?.currency || DEFAULT_CURRENCY,
value: exp.monthlySalary?.value,
},
title: exp.title,
totalCompensation: exp.totalCompensation,
totalCompensation: {
currency:
exp.totalCompensation?.currency || DEFAULT_CURRENCY,
value: exp.totalCompensation?.value,
},
})),
id,
specificYoes,

Loading…
Cancel
Save