From a8fdca65cdaa1c6837b33e9715b336f661cad36e Mon Sep 17 00:00:00 2001 From: Ai Ling <50992674+ailing35@users.noreply.github.com> Date: Sun, 30 Oct 2022 20:18:46 +0800 Subject: [PATCH] [offers][fix] Fix offers UI (#460) --- .../offers/offerAnalysis/OfferAnalysis.tsx | 26 +++++- .../OfferPercentileAnalysisText.tsx | 16 ++-- .../offers/offerAnalysis/OfferProfileCard.tsx | 11 ++- .../OffersSubmissionAnalysis.tsx | 1 + .../offersSubmission/OffersSubmissionForm.tsx | 56 ++++++++++++- .../submissionForm/BackgroundForm.tsx | 81 +++++++++++++++---- .../submissionForm/OfferDetailsForm.tsx | 80 ++++++++++++++---- .../components/offers/profile/OfferCard.tsx | 7 +- .../offers/profile/ProfileHeader.tsx | 11 ++- apps/portal/src/components/offers/types.ts | 3 + .../pages/offers/profile/[offerProfileId].tsx | 10 ++- .../offers/profile/edit/[offerProfileId].tsx | 8 ++ 12 files changed, 258 insertions(+), 52 deletions(-) diff --git a/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx b/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx index 67c9c9e1..b8ae1c7c 100644 --- a/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx +++ b/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx @@ -19,12 +19,14 @@ type OfferAnalysisData = { type OfferAnalysisContentProps = Readonly<{ analysis: OfferAnalysisData; + isSubmission: boolean; tab: string; }>; function OfferAnalysisContent({ analysis: { offer, offerAnalysis }, tab, + isSubmission, }: OfferAnalysisContentProps) { if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) { if (tab === OVERALL_TAB) { @@ -46,16 +48,30 @@ function OfferAnalysisContent({ <> -

Here are some of the top offers relevant to you:

+

+ {isSubmission + ? 'Here are some of the top offers relevant to you:' + : 'Relevant top offers:'} +

{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => ( ))} + {/* {offerAnalysis.topPercentileOffers.length > 0 && ( +
+
+ )} */} ); } @@ -64,12 +80,14 @@ type OfferAnalysisProps = Readonly<{ allAnalysis?: ProfileAnalysis | null; isError: boolean; isLoading: boolean; + isSubmission?: boolean; }>; export default function OfferAnalysis({ allAnalysis, isError, isLoading, + isSubmission = false, }: OfferAnalysisProps) { const [tab, setTab] = useState(OVERALL_TAB); const [analysis, setAnalysis] = useState(null); @@ -117,7 +135,11 @@ export default function OfferAnalysis({ onChange={setTab} /> - + )} diff --git a/apps/portal/src/components/offers/offerAnalysis/OfferPercentileAnalysisText.tsx b/apps/portal/src/components/offers/offerAnalysis/OfferPercentileAnalysisText.tsx index d61af844..9b77f209 100644 --- a/apps/portal/src/components/offers/offerAnalysis/OfferPercentileAnalysisText.tsx +++ b/apps/portal/src/components/offers/offerAnalysis/OfferPercentileAnalysisText.tsx @@ -4,6 +4,7 @@ import type { Analysis } from '~/types/offers'; type OfferPercentileAnalysisTextProps = Readonly<{ companyName: string; + isSubmission: boolean; offerAnalysis: Analysis; tab: string; }>; @@ -12,18 +13,21 @@ export default function OfferPercentileAnalysisText({ tab, companyName, offerAnalysis: { noOfOffers, percentile }, + isSubmission, }: OfferPercentileAnalysisTextProps) { return tab === OVERALL_TAB ? (

- Your highest offer is from {companyName}, which is{' '} - {percentile.toFixed(1)} percentile out of {noOfOffers}{' '} - offers received for the same job title and YOE(±1) in the last year. + {isSubmission ? 'Your' : "This profile's"} highest offer is from{' '} + {companyName}, which is {percentile.toFixed(1)} percentile + out of {noOfOffers} offers received for the same job title and + YOE(±1) in the last year.

) : (

- Your offer from {companyName} is {percentile.toFixed(1)}{' '} - percentile out of {noOfOffers} offers received in {companyName} for - the same job title and YOE(±1) in the last year. + {isSubmission ? 'Your' : 'The'} offer from {companyName} is{' '} + {percentile.toFixed(1)} percentile out of {noOfOffers}{' '} + offers received in {companyName} for the same job title and YOE(±1) in the + last year.

); } diff --git a/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx b/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx index 13cbd2d8..6d57e2b8 100644 --- a/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx +++ b/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx @@ -12,6 +12,7 @@ import { convertMoneyToString } from '~/utils/offers/currency'; import { formatDate } from '~/utils/offers/time'; import ProfilePhotoHolder from '../profile/ProfilePhotoHolder'; +import { JobTypeLabel } from '../types'; import type { AnalysisOffer } from '~/types/offers'; @@ -34,7 +35,12 @@ export default function OfferProfileCard({ }, }: OfferProfileCardProps) { return ( -
+ // +
@@ -58,7 +64,8 @@ export default function OfferProfileCard({

- {getLabelForJobTitleType(title as JobTitleType)} + {getLabelForJobTitleType(title as JobTitleType)}{' '} + {`(${JobTypeLabel[jobType]})`}

Company: {company.name}, {location} diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx index d5aee9b0..82b4895f 100644 --- a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx @@ -22,6 +22,7 @@ export default function OffersSubmissionAnalysis({ allAnalysis={analysis} isError={isError} isLoading={isLoading} + isSubmission={true} />

); diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx index 2e9c90aa..4de2911d 100644 --- a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx @@ -27,6 +27,7 @@ import { trpc } from '~/utils/trpc'; const defaultOfferValues = { comments: '', companyId: '', + jobTitle: '', jobType: JobType.FULLTIME, location: '', monthYearReceived: { @@ -39,11 +40,38 @@ const defaultOfferValues = { export const defaultFullTimeOfferValues = { ...defaultOfferValues, jobType: JobType.FULLTIME, + offersFullTime: { + baseSalary: { + currency: 'SGD', + value: null, + }, + bonus: { + currency: 'SGD', + value: null, + }, + level: '', + stocks: { + currency: 'SGD', + value: null, + }, + totalCompensation: { + currency: 'SGD', + value: null, + }, + }, }; export const defaultInternshipOfferValues = { ...defaultOfferValues, jobType: JobType.INTERN, + offersIntern: { + internshipCycle: null, + monthlySalary: { + currency: 'SGD', + value: null, + }, + startYear: null, + }, }; const defaultOfferProfileValues = { @@ -198,6 +226,32 @@ export default function OffersSubmissionForm({ scrollToTop(); }, [step]); + useEffect(() => { + const warningText = + 'Leave this page? Changes that you made will not be saved.'; + const handleWindowClose = (e: BeforeUnloadEvent) => { + e.preventDefault(); + return (e.returnValue = warningText); + }; + const handleRouteChange = (url: string) => { + if (url.includes('/offers/submit/result')) { + return; + } + if (window.confirm(warningText)) { + return; + } + router.events.emit('routeChangeError'); + throw 'routeChange aborted.'; + }; + window.addEventListener('beforeunload', handleWindowClose); + router.events.on('routeChangeStart', handleRouteChange); + return () => { + window.removeEventListener('beforeunload', handleWindowClose); + router.events.off('routeChangeStart', handleRouteChange); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
@@ -210,7 +264,7 @@ export default function OffersSubmissionForm({ />
-
+ {steps[step]} {/*
{JSON.stringify(formMethods.watch(), null, 2)}
*/} {step === 0 && ( diff --git a/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx index b3304e1e..fc5bb109 100644 --- a/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx @@ -11,6 +11,8 @@ import { } from '~/components/offers/constants'; import type { BackgroundPostData } from '~/components/offers/types'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; +import type { JobTitleType } from '~/components/shared/JobTitles'; +import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead'; import { @@ -92,23 +94,47 @@ function FullTimeJobFields() { background: BackgroundPostData; }>(); const experiencesField = formState.errors.background?.experiences?.[0]; + + const watchJobTitle = useWatch({ + name: 'background.experiences.0.title', + }); + const watchCompanyId = useWatch({ + name: 'background.experiences.0.companyId', + }); + const watchCompanyName = useWatch({ + name: 'background.experiences.0.companyName', + }); + return ( <>
- setValue(`background.experiences.0.title`, value) - } + value={{ + id: watchJobTitle, + label: getLabelForJobTitleType(watchJobTitle as JobTitleType), + value: watchJobTitle, + }} + onSelect={(option) => { + if (option) { + setValue('background.experiences.0.title', option.value); + } + }} />
- setValue(`background.experiences.0.companyId`, value) - } + value={{ + id: watchCompanyId, + label: watchCompanyName, + value: watchCompanyId, + }} + onSelect={(option) => { + if (option) { + setValue('background.experiences.0.companyId', option.value); + setValue('background.experiences.0.companyName', option.label); + } + }} />
@@ -175,23 +201,46 @@ function InternshipJobFields() { }>(); const experiencesField = formState.errors.background?.experiences?.[0]; + const watchJobTitle = useWatch({ + name: 'background.experiences.0.title', + }); + const watchCompanyId = useWatch({ + name: 'background.experiences.0.companyId', + }); + const watchCompanyName = useWatch({ + name: 'background.experiences.0.companyName', + }); + return ( <>
- setValue(`background.experiences.0.title`, value) - } + value={{ + id: watchJobTitle, + label: getLabelForJobTitleType(watchJobTitle as JobTitleType), + value: watchJobTitle, + }} + onSelect={(option) => { + if (option) { + setValue('background.experiences.0.title', option.value); + } + }} />
- setValue(`background.experiences.0.companyId`, value) - } + value={{ + id: watchCompanyId, + label: watchCompanyName, + value: watchCompanyId, + }} + onSelect={(option) => { + if (option) { + setValue('background.experiences.0.companyId', option.value); + setValue('background.experiences.0.companyName', option.label); + } + }} />
diff --git a/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx index 9a910de6..63abe7d1 100644 --- a/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx @@ -13,6 +13,8 @@ import { JobType } from '@prisma/client'; import { Button, Dialog } from '@tih/ui'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; +import type { JobTitleType } from '~/components/shared/JobTitles'; +import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead'; import { @@ -51,6 +53,15 @@ function FullTimeOfferDetailsForm({ }>(); const offerFields = formState.errors.offers?.[index]; + const watchJobTitle = useWatch({ + name: `offers.${index}.offersFullTime.title`, + }); + const watchCompanyId = useWatch({ + name: `offers.${index}.companyId`, + }); + const watchCompanyName = useWatch({ + name: `offers.${index}.companyName`, + }); const watchCurrency = useWatch({ name: `offers.${index}.offersFullTime.totalCompensation.currency`, }); @@ -70,10 +81,16 @@ function FullTimeOfferDetailsForm({
- setValue(`offers.${index}.offersFullTime.title`, value) - } + value={{ + id: watchJobTitle, + label: getLabelForJobTitleType(watchJobTitle as JobTitleType), + value: watchJobTitle, + }} + onSelect={(option) => { + if (option) { + setValue(`offers.${index}.offersFullTime.title`, option.value); + } + }} />
- setValue(`offers.${index}.companyId`, value) - } + value={{ + id: watchCompanyId, + label: watchCompanyName, + value: watchCompanyId, + }} + onSelect={(option) => { + if (option) { + setValue(`offers.${index}.companyId`, option.value); + setValue(`offers.${index}.companyName`, option.label); + } + }} />
; }>(); - const offerFields = formState.errors.offers?.[index]; + const watchJobTitle = useWatch({ + name: `offers.${index}.offersIntern.title`, + }); + const watchCompanyId = useWatch({ + name: `offers.${index}.companyId`, + }); + const watchCompanyName = useWatch({ + name: `offers.${index}.companyName`, + }); + return (
- setValue(`offers.${index}.offersIntern.title`, value) - } + value={{ + id: watchJobTitle, + label: getLabelForJobTitleType(watchJobTitle as JobTitleType), + value: watchJobTitle, + }} + onSelect={(option) => { + if (option) { + setValue(`offers.${index}.offersIntern.title`, option.value); + } + }} />
@@ -290,10 +329,17 @@ function InternshipOfferDetailsForm({
- setValue(`offers.${index}.companyId`, value) - } + value={{ + id: watchCompanyId, + label: watchCompanyName, + value: watchCompanyId, + }} + onSelect={(option) => { + if (option) { + setValue(`offers.${index}.companyId`, option.value); + setValue(`offers.${index}.companyName`, option.label); + } + }} />
-

{jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}

+

+ {jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}{' '} + {jobType && `(${JobTypeLabel[jobType]})`} +

{!duration && receivedMonth && ( diff --git a/apps/portal/src/components/offers/profile/ProfileHeader.tsx b/apps/portal/src/components/offers/profile/ProfileHeader.tsx index 4a0d944b..dc5e8ddb 100644 --- a/apps/portal/src/components/offers/profile/ProfileHeader.tsx +++ b/apps/portal/src/components/offers/profile/ProfileHeader.tsx @@ -10,6 +10,7 @@ import { Button, Dialog, Spinner, Tabs } from '@tih/ui'; import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder'; import type { BackgroundDisplayData } from '~/components/offers/types'; +import { JobTypeLabel } from '~/components/offers/types'; import { getProfileEditPath } from '~/utils/offers/link'; @@ -95,8 +96,8 @@ export default function ProfileHeader({ title="Are you sure you want to delete this offer profile?" onClose={() => setIsDialogOpen(false)}>
- All comments will be gone. You will not be able to access or - recover it. + All information and comments in this offer profile will be + deleted. You will not be able to access or recover them.
)} @@ -144,7 +145,11 @@ export default function ProfileHeader({ {`${experiences[0].companyName || ''} ${ experiences[0].jobLevel || '' - } ${experiences[0].jobTitle || ''}`} + } ${experiences[0].jobTitle || ''} ${ + experiences[0].jobType + ? `(${JobTypeLabel[experiences[0].jobType]})` + : '' + }`}
)} diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts index 59704f33..366ca25b 100644 --- a/apps/portal/src/components/offers/types.ts +++ b/apps/portal/src/components/offers/types.ts @@ -45,6 +45,7 @@ export type BackgroundPostData = { type ExperiencePostData = { companyId?: string | null; + companyName?: string | null; durationInMonths?: number | null; id?: string; jobType?: string | null; @@ -76,6 +77,7 @@ type SpecificYoe = SpecificYoePostData; export type OfferPostData = { comments: string; companyId: string; + companyName?: string; id?: string; jobType: JobType; location: string; @@ -129,6 +131,7 @@ export type OfferDisplayData = { id?: string; jobLevel?: string | null; jobTitle?: string | null; + jobType?: JobType; location?: string | null; monthlySalary?: string | null; negotiationStrategy?: string | null; diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx index 77229cc1..b5d90466 100644 --- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx @@ -24,9 +24,6 @@ import type { Profile, ProfileAnalysis, ProfileOffer } from '~/types/offers'; export default function OfferProfile() { const { showToast } = useToast(); - const ErrorPage = ( - - ); const router = useRouter(); const { offerProfileId, token = '' } = router.query; const [isEditable, setIsEditable] = useState(false); @@ -126,6 +123,7 @@ export default function OfferProfile() { jobTitle: experience.title ? getLabelForJobTitleType(experience.title as JobTitleType) : null, + jobType: experience.jobType || undefined, monthlySalary: experience.monthlySalary ? convertMoneyToString(experience.monthlySalary) : null, @@ -177,7 +175,11 @@ export default function OfferProfile() { return ( <> - {getProfileQuery.isError && ErrorPage} + {getProfileQuery.isError && ( +
+ +
+ )} {!getProfileQuery.isError && (
diff --git a/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx index 245c0bb5..05e47b7c 100644 --- a/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx @@ -1,3 +1,4 @@ +import Error from 'next/error'; import { useRouter } from 'next/router'; import { useState } from 'react'; import { JobType } from '@prisma/client'; @@ -36,6 +37,7 @@ export default function OffersEditPage() { ? [{ jobType: JobType.FULLTIME }] : experiences.map((exp) => ({ companyId: exp.company?.id, + companyName: exp.company?.name, durationInMonths: exp.durationInMonths, id: exp.id, jobType: exp.jobType, @@ -53,6 +55,7 @@ export default function OffersEditPage() { offers: data.offers.map((offer) => ({ comments: offer.comments, companyId: offer.company.id, + companyName: offer.company.name, id: offer.id, jobType: offer.jobType, location: offer.location, @@ -74,6 +77,11 @@ export default function OffersEditPage() { return ( <> + {getProfileResult.isError && ( +
+ +
+ )} {getProfileResult.isLoading && (