mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-27 20:22:33 +08:00
[offers][fix] Fix offers UI (#460)
This commit is contained in:
@ -19,12 +19,14 @@ type OfferAnalysisData = {
|
|||||||
|
|
||||||
type OfferAnalysisContentProps = Readonly<{
|
type OfferAnalysisContentProps = Readonly<{
|
||||||
analysis: OfferAnalysisData;
|
analysis: OfferAnalysisData;
|
||||||
|
isSubmission: boolean;
|
||||||
tab: string;
|
tab: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
function OfferAnalysisContent({
|
function OfferAnalysisContent({
|
||||||
analysis: { offer, offerAnalysis },
|
analysis: { offer, offerAnalysis },
|
||||||
tab,
|
tab,
|
||||||
|
isSubmission,
|
||||||
}: OfferAnalysisContentProps) {
|
}: OfferAnalysisContentProps) {
|
||||||
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
|
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
|
||||||
if (tab === OVERALL_TAB) {
|
if (tab === OVERALL_TAB) {
|
||||||
@ -46,16 +48,30 @@ function OfferAnalysisContent({
|
|||||||
<>
|
<>
|
||||||
<OfferPercentileAnalysisText
|
<OfferPercentileAnalysisText
|
||||||
companyName={offer.company.name}
|
companyName={offer.company.name}
|
||||||
|
isSubmission={isSubmission}
|
||||||
offerAnalysis={offerAnalysis}
|
offerAnalysis={offerAnalysis}
|
||||||
tab={tab}
|
tab={tab}
|
||||||
/>
|
/>
|
||||||
<p className="mt-5">Here are some of the top offers relevant to you:</p>
|
<p className="mt-5">
|
||||||
|
{isSubmission
|
||||||
|
? 'Here are some of the top offers relevant to you:'
|
||||||
|
: 'Relevant top offers:'}
|
||||||
|
</p>
|
||||||
{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => (
|
{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => (
|
||||||
<OfferProfileCard
|
<OfferProfileCard
|
||||||
key={topPercentileOffer.id}
|
key={topPercentileOffer.id}
|
||||||
offerProfile={topPercentileOffer}
|
offerProfile={topPercentileOffer}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{/* {offerAnalysis.topPercentileOffers.length > 0 && (
|
||||||
|
<div className="mb-4 flex justify-end">
|
||||||
|
<Button
|
||||||
|
icon={EllipsisHorizontalIcon}
|
||||||
|
label="View more offers"
|
||||||
|
variant="tertiary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -64,12 +80,14 @@ type OfferAnalysisProps = Readonly<{
|
|||||||
allAnalysis?: ProfileAnalysis | null;
|
allAnalysis?: ProfileAnalysis | null;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isSubmission?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function OfferAnalysis({
|
export default function OfferAnalysis({
|
||||||
allAnalysis,
|
allAnalysis,
|
||||||
isError,
|
isError,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isSubmission = false,
|
||||||
}: OfferAnalysisProps) {
|
}: OfferAnalysisProps) {
|
||||||
const [tab, setTab] = useState(OVERALL_TAB);
|
const [tab, setTab] = useState(OVERALL_TAB);
|
||||||
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
|
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
|
||||||
@ -117,7 +135,11 @@ export default function OfferAnalysis({
|
|||||||
onChange={setTab}
|
onChange={setTab}
|
||||||
/>
|
/>
|
||||||
<HorizontalDivider className="mb-5" />
|
<HorizontalDivider className="mb-5" />
|
||||||
<OfferAnalysisContent analysis={analysis} tab={tab} />
|
<OfferAnalysisContent
|
||||||
|
analysis={analysis}
|
||||||
|
isSubmission={isSubmission}
|
||||||
|
tab={tab}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@ import type { Analysis } from '~/types/offers';
|
|||||||
|
|
||||||
type OfferPercentileAnalysisTextProps = Readonly<{
|
type OfferPercentileAnalysisTextProps = Readonly<{
|
||||||
companyName: string;
|
companyName: string;
|
||||||
|
isSubmission: boolean;
|
||||||
offerAnalysis: Analysis;
|
offerAnalysis: Analysis;
|
||||||
tab: string;
|
tab: string;
|
||||||
}>;
|
}>;
|
||||||
@ -12,18 +13,21 @@ export default function OfferPercentileAnalysisText({
|
|||||||
tab,
|
tab,
|
||||||
companyName,
|
companyName,
|
||||||
offerAnalysis: { noOfOffers, percentile },
|
offerAnalysis: { noOfOffers, percentile },
|
||||||
|
isSubmission,
|
||||||
}: OfferPercentileAnalysisTextProps) {
|
}: OfferPercentileAnalysisTextProps) {
|
||||||
return tab === OVERALL_TAB ? (
|
return tab === OVERALL_TAB ? (
|
||||||
<p>
|
<p>
|
||||||
Your highest offer is from <b>{companyName}</b>, which is{' '}
|
{isSubmission ? 'Your' : "This profile's"} highest offer is from{' '}
|
||||||
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
|
<b>{companyName}</b>, which is <b>{percentile.toFixed(1)}</b> percentile
|
||||||
offers received for the same job title and YOE(±1) in the last year.
|
out of <b>{noOfOffers}</b> offers received for the same job title and
|
||||||
|
YOE(±1) in the last year.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
Your offer from <b>{companyName}</b> is <b>{percentile.toFixed(1)}</b>{' '}
|
{isSubmission ? 'Your' : 'The'} offer from <b>{companyName}</b> is{' '}
|
||||||
percentile out of <b>{noOfOffers}</b> offers received in {companyName} for
|
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
|
||||||
the same job title and YOE(±1) in the last year.
|
offers received in {companyName} for the same job title and YOE(±1) in the
|
||||||
|
last year.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ 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 { JobTypeLabel } from '../types';
|
||||||
|
|
||||||
import type { AnalysisOffer } from '~/types/offers';
|
import type { AnalysisOffer } from '~/types/offers';
|
||||||
|
|
||||||
@ -34,7 +35,12 @@ export default function OfferProfileCard({
|
|||||||
},
|
},
|
||||||
}: OfferProfileCardProps) {
|
}: OfferProfileCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="my-5 block rounded-lg bg-white p-4 px-8 shadow-md">
|
// <a
|
||||||
|
// className="my-5 block rounded-lg bg-white p-4 px-8 shadow-md"
|
||||||
|
// href={`/offers/profile/${id}`}
|
||||||
|
// rel="noreferrer"
|
||||||
|
// target="_blank">
|
||||||
|
<div className="my-5 block rounded-lg bg-white p-4 px-8 shadow-lg">
|
||||||
<div className="flex items-center gap-x-5">
|
<div className="flex items-center gap-x-5">
|
||||||
<div>
|
<div>
|
||||||
<ProfilePhotoHolder size="sm" />
|
<ProfilePhotoHolder size="sm" />
|
||||||
@ -58,7 +64,8 @@ export default function OfferProfileCard({
|
|||||||
<div className="flex items-end justify-between">
|
<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="font-bold">
|
<p className="font-bold">
|
||||||
{getLabelForJobTitleType(title as JobTitleType)}
|
{getLabelForJobTitleType(title as JobTitleType)}{' '}
|
||||||
|
{`(${JobTypeLabel[jobType]})`}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Company: {company.name}, {location}
|
Company: {company.name}, {location}
|
||||||
|
@ -22,6 +22,7 @@ export default function OffersSubmissionAnalysis({
|
|||||||
allAnalysis={analysis}
|
allAnalysis={analysis}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
isSubmission={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -27,6 +27,7 @@ import { trpc } from '~/utils/trpc';
|
|||||||
const defaultOfferValues = {
|
const defaultOfferValues = {
|
||||||
comments: '',
|
comments: '',
|
||||||
companyId: '',
|
companyId: '',
|
||||||
|
jobTitle: '',
|
||||||
jobType: JobType.FULLTIME,
|
jobType: JobType.FULLTIME,
|
||||||
location: '',
|
location: '',
|
||||||
monthYearReceived: {
|
monthYearReceived: {
|
||||||
@ -39,11 +40,38 @@ const defaultOfferValues = {
|
|||||||
export const defaultFullTimeOfferValues = {
|
export const defaultFullTimeOfferValues = {
|
||||||
...defaultOfferValues,
|
...defaultOfferValues,
|
||||||
jobType: JobType.FULLTIME,
|
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 = {
|
export const defaultInternshipOfferValues = {
|
||||||
...defaultOfferValues,
|
...defaultOfferValues,
|
||||||
jobType: JobType.INTERN,
|
jobType: JobType.INTERN,
|
||||||
|
offersIntern: {
|
||||||
|
internshipCycle: null,
|
||||||
|
monthlySalary: {
|
||||||
|
currency: 'SGD',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
startYear: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultOfferProfileValues = {
|
const defaultOfferProfileValues = {
|
||||||
@ -198,6 +226,32 @@ export default function OffersSubmissionForm({
|
|||||||
scrollToTop();
|
scrollToTop();
|
||||||
}, [step]);
|
}, [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 (
|
return (
|
||||||
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
|
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
|
||||||
<div className="mb-20 flex justify-center">
|
<div className="mb-20 flex justify-center">
|
||||||
@ -210,7 +264,7 @@ export default function OffersSubmissionForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form className="text-sm" onSubmit={handleSubmit(onSubmit)}>
|
||||||
{steps[step]}
|
{steps[step]}
|
||||||
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
} from '~/components/offers/constants';
|
} from '~/components/offers/constants';
|
||||||
import type { BackgroundPostData } from '~/components/offers/types';
|
import type { BackgroundPostData } from '~/components/offers/types';
|
||||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
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 JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -92,23 +94,47 @@ function FullTimeJobFields() {
|
|||||||
background: BackgroundPostData;
|
background: BackgroundPostData;
|
||||||
}>();
|
}>();
|
||||||
const experiencesField = formState.errors.background?.experiences?.[0];
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||||
<div>
|
<div>
|
||||||
<JobTitlesTypeahead
|
<JobTitlesTypeahead
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchJobTitle,
|
||||||
setValue(`background.experiences.0.title`, value)
|
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||||
}
|
value: watchJobTitle,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue('background.experiences.0.title', option.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CompaniesTypeahead
|
<CompaniesTypeahead
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchCompanyId,
|
||||||
setValue(`background.experiences.0.companyId`, value)
|
label: watchCompanyName,
|
||||||
}
|
value: watchCompanyId,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue('background.experiences.0.companyId', option.value);
|
||||||
|
setValue('background.experiences.0.companyName', option.label);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -175,23 +201,46 @@ function InternshipJobFields() {
|
|||||||
}>();
|
}>();
|
||||||
const experiencesField = formState.errors.background?.experiences?.[0];
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||||
<div>
|
<div>
|
||||||
<JobTitlesTypeahead
|
<JobTitlesTypeahead
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchJobTitle,
|
||||||
setValue(`background.experiences.0.title`, value)
|
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||||
}
|
value: watchJobTitle,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue('background.experiences.0.title', option.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CompaniesTypeahead
|
<CompaniesTypeahead
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchCompanyId,
|
||||||
setValue(`background.experiences.0.companyId`, value)
|
label: watchCompanyName,
|
||||||
}
|
value: watchCompanyId,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue('background.experiences.0.companyId', option.value);
|
||||||
|
setValue('background.experiences.0.companyName', option.label);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,8 @@ import { JobType } from '@prisma/client';
|
|||||||
import { Button, Dialog } from '@tih/ui';
|
import { Button, Dialog } from '@tih/ui';
|
||||||
|
|
||||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
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 JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -51,6 +53,15 @@ function FullTimeOfferDetailsForm({
|
|||||||
}>();
|
}>();
|
||||||
const offerFields = formState.errors.offers?.[index];
|
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({
|
const watchCurrency = useWatch({
|
||||||
name: `offers.${index}.offersFullTime.totalCompensation.currency`,
|
name: `offers.${index}.offersFullTime.totalCompensation.currency`,
|
||||||
});
|
});
|
||||||
@ -70,10 +81,16 @@ function FullTimeOfferDetailsForm({
|
|||||||
<div>
|
<div>
|
||||||
<JobTitlesTypeahead
|
<JobTitlesTypeahead
|
||||||
required={true}
|
required={true}
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchJobTitle,
|
||||||
setValue(`offers.${index}.offersFullTime.title`, value)
|
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||||
}
|
value: watchJobTitle,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue(`offers.${index}.offersFullTime.title`, option.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
@ -90,10 +107,17 @@ function FullTimeOfferDetailsForm({
|
|||||||
<div>
|
<div>
|
||||||
<CompaniesTypeahead
|
<CompaniesTypeahead
|
||||||
required={true}
|
required={true}
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchCompanyId,
|
||||||
setValue(`offers.${index}.companyId`, value)
|
label: watchCompanyName,
|
||||||
}
|
value: watchCompanyId,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue(`offers.${index}.companyId`, option.value);
|
||||||
|
setValue(`offers.${index}.companyName`, option.label);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
@ -270,19 +294,34 @@ function InternshipOfferDetailsForm({
|
|||||||
const { register, formState, setValue } = useFormContext<{
|
const { register, formState, setValue } = useFormContext<{
|
||||||
offers: Array<OfferFormData>;
|
offers: Array<OfferFormData>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const offerFields = formState.errors.offers?.[index];
|
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 (
|
return (
|
||||||
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
|
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
|
||||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||||
<div>
|
<div>
|
||||||
<JobTitlesTypeahead
|
<JobTitlesTypeahead
|
||||||
required={true}
|
required={true}
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchJobTitle,
|
||||||
setValue(`offers.${index}.offersIntern.title`, value)
|
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||||
}
|
value: watchJobTitle,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue(`offers.${index}.offersIntern.title`, option.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -290,10 +329,17 @@ function InternshipOfferDetailsForm({
|
|||||||
<div>
|
<div>
|
||||||
<CompaniesTypeahead
|
<CompaniesTypeahead
|
||||||
required={true}
|
required={true}
|
||||||
// @ts-ignore TODO(offers): handle potentially null value.
|
value={{
|
||||||
onSelect={({ value }) =>
|
id: watchCompanyId,
|
||||||
setValue(`offers.${index}.companyId`, value)
|
label: watchCompanyName,
|
||||||
}
|
value: watchCompanyId,
|
||||||
|
}}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setValue(`offers.${index}.companyId`, option.value);
|
||||||
|
setValue(`offers.${index}.companyName`, option.label);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
import { HorizontalDivider } from '@tih/ui';
|
import { HorizontalDivider } from '@tih/ui';
|
||||||
|
|
||||||
import type { OfferDisplayData } from '~/components/offers/types';
|
import type { OfferDisplayData } from '~/components/offers/types';
|
||||||
|
import { JobTypeLabel } from '~/components/offers/types';
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
offer: OfferDisplayData;
|
offer: OfferDisplayData;
|
||||||
@ -20,6 +21,7 @@ export default function OfferCard({
|
|||||||
duration,
|
duration,
|
||||||
jobTitle,
|
jobTitle,
|
||||||
jobLevel,
|
jobLevel,
|
||||||
|
jobType,
|
||||||
location,
|
location,
|
||||||
receivedMonth,
|
receivedMonth,
|
||||||
totalCompensation,
|
totalCompensation,
|
||||||
@ -40,7 +42,10 @@ export default function OfferCard({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-6 flex flex-row">
|
<div className="ml-6 flex flex-row">
|
||||||
<p>{jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}</p>
|
<p>
|
||||||
|
{jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}{' '}
|
||||||
|
{jobType && `(${JobTypeLabel[jobType]})`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!duration && receivedMonth && (
|
{!duration && receivedMonth && (
|
||||||
|
@ -10,6 +10,7 @@ import { Button, Dialog, Spinner, Tabs } from '@tih/ui';
|
|||||||
|
|
||||||
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
||||||
import type { BackgroundDisplayData } from '~/components/offers/types';
|
import type { BackgroundDisplayData } from '~/components/offers/types';
|
||||||
|
import { JobTypeLabel } from '~/components/offers/types';
|
||||||
|
|
||||||
import { getProfileEditPath } from '~/utils/offers/link';
|
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?"
|
title="Are you sure you want to delete this offer profile?"
|
||||||
onClose={() => setIsDialogOpen(false)}>
|
onClose={() => setIsDialogOpen(false)}>
|
||||||
<div>
|
<div>
|
||||||
All comments will be gone. You will not be able to access or
|
All information and comments in this offer profile will be
|
||||||
recover it.
|
deleted. You will not be able to access or recover them.
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
@ -144,7 +145,11 @@ export default function ProfileHeader({
|
|||||||
<span>
|
<span>
|
||||||
{`${experiences[0].companyName || ''} ${
|
{`${experiences[0].companyName || ''} ${
|
||||||
experiences[0].jobLevel || ''
|
experiences[0].jobLevel || ''
|
||||||
} ${experiences[0].jobTitle || ''}`}
|
} ${experiences[0].jobTitle || ''} ${
|
||||||
|
experiences[0].jobType
|
||||||
|
? `(${JobTypeLabel[experiences[0].jobType]})`
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -45,6 +45,7 @@ export type BackgroundPostData = {
|
|||||||
|
|
||||||
type ExperiencePostData = {
|
type ExperiencePostData = {
|
||||||
companyId?: string | null;
|
companyId?: string | null;
|
||||||
|
companyName?: string | null;
|
||||||
durationInMonths?: number | null;
|
durationInMonths?: number | null;
|
||||||
id?: string;
|
id?: string;
|
||||||
jobType?: string | null;
|
jobType?: string | null;
|
||||||
@ -76,6 +77,7 @@ type SpecificYoe = SpecificYoePostData;
|
|||||||
export type OfferPostData = {
|
export type OfferPostData = {
|
||||||
comments: string;
|
comments: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
companyName?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
jobType: JobType;
|
jobType: JobType;
|
||||||
location: string;
|
location: string;
|
||||||
@ -129,6 +131,7 @@ export type OfferDisplayData = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
jobLevel?: string | null;
|
jobLevel?: string | null;
|
||||||
jobTitle?: string | null;
|
jobTitle?: string | null;
|
||||||
|
jobType?: JobType;
|
||||||
location?: string | null;
|
location?: string | null;
|
||||||
monthlySalary?: string | null;
|
monthlySalary?: string | null;
|
||||||
negotiationStrategy?: string | null;
|
negotiationStrategy?: string | null;
|
||||||
|
@ -24,9 +24,6 @@ import type { Profile, ProfileAnalysis, ProfileOffer } from '~/types/offers';
|
|||||||
|
|
||||||
export default function OfferProfile() {
|
export default function OfferProfile() {
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const ErrorPage = (
|
|
||||||
<Error statusCode={404} title="Requested profile does not exist." />
|
|
||||||
);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { offerProfileId, token = '' } = router.query;
|
const { offerProfileId, token = '' } = router.query;
|
||||||
const [isEditable, setIsEditable] = useState(false);
|
const [isEditable, setIsEditable] = useState(false);
|
||||||
@ -126,6 +123,7 @@ export default function OfferProfile() {
|
|||||||
jobTitle: experience.title
|
jobTitle: experience.title
|
||||||
? getLabelForJobTitleType(experience.title as JobTitleType)
|
? getLabelForJobTitleType(experience.title as JobTitleType)
|
||||||
: null,
|
: null,
|
||||||
|
jobType: experience.jobType || undefined,
|
||||||
monthlySalary: experience.monthlySalary
|
monthlySalary: experience.monthlySalary
|
||||||
? convertMoneyToString(experience.monthlySalary)
|
? convertMoneyToString(experience.monthlySalary)
|
||||||
: null,
|
: null,
|
||||||
@ -177,7 +175,11 @@ export default function OfferProfile() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getProfileQuery.isError && ErrorPage}
|
{getProfileQuery.isError && (
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<Error statusCode={404} title="Requested profile does not exist" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!getProfileQuery.isError && (
|
{!getProfileQuery.isError && (
|
||||||
<div className="mb-4 flex flex h-screen w-screen items-center justify-center divide-x">
|
<div className="mb-4 flex flex h-screen w-screen items-center justify-center divide-x">
|
||||||
<div className="h-full w-2/3 divide-y">
|
<div className="h-full w-2/3 divide-y">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Error from 'next/error';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { JobType } from '@prisma/client';
|
import { JobType } from '@prisma/client';
|
||||||
@ -36,6 +37,7 @@ export default function OffersEditPage() {
|
|||||||
? [{ jobType: JobType.FULLTIME }]
|
? [{ jobType: JobType.FULLTIME }]
|
||||||
: experiences.map((exp) => ({
|
: experiences.map((exp) => ({
|
||||||
companyId: exp.company?.id,
|
companyId: exp.company?.id,
|
||||||
|
companyName: exp.company?.name,
|
||||||
durationInMonths: exp.durationInMonths,
|
durationInMonths: exp.durationInMonths,
|
||||||
id: exp.id,
|
id: exp.id,
|
||||||
jobType: exp.jobType,
|
jobType: exp.jobType,
|
||||||
@ -53,6 +55,7 @@ export default function OffersEditPage() {
|
|||||||
offers: data.offers.map((offer) => ({
|
offers: data.offers.map((offer) => ({
|
||||||
comments: offer.comments,
|
comments: offer.comments,
|
||||||
companyId: offer.company.id,
|
companyId: offer.company.id,
|
||||||
|
companyName: offer.company.name,
|
||||||
id: offer.id,
|
id: offer.id,
|
||||||
jobType: offer.jobType,
|
jobType: offer.jobType,
|
||||||
location: offer.location,
|
location: offer.location,
|
||||||
@ -74,6 +77,11 @@ export default function OffersEditPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{getProfileResult.isError && (
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<Error statusCode={404} title="Requested profile does not exist" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{getProfileResult.isLoading && (
|
{getProfileResult.isLoading && (
|
||||||
<div className="flex w-full justify-center">
|
<div className="flex w-full justify-center">
|
||||||
<Spinner className="m-10" display="block" size="lg" />
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
Reference in New Issue
Block a user