Merge branch 'main' into stuart/seed-db

pull/501/head^2
Stuart Long Chay Boon 3 years ago
commit f88ecaf6d5

@ -0,0 +1,16 @@
import { emptyOption } from './constants';
export const EducationFieldLabels = [
'Business Analytics',
'Computer Science',
'Data Science and Analytics',
'Information Security',
'Information Systems',
];
export const EducationFieldOptions = [emptyOption].concat(
EducationFieldLabels.map((label) => ({
label,
value: label.replace(/\s+/g, '-').toLowerCase(),
})),
);

@ -0,0 +1,18 @@
import { emptyOption } from './constants';
export const EducationLevelLabels = [
'Bachelor',
'Diploma',
'Masters',
'PhD',
'Professional',
'Secondary',
'Self-taught',
];
export const EducationLevelOptions = [emptyOption].concat(
EducationLevelLabels.map((label) => ({
label,
value: label.replace(/\s+/g, '-').toLowerCase(),
})),
);

@ -0,0 +1,18 @@
import { emptyOption } from './constants';
export const InternshipCycleLabels = [
'Spring',
'Summer',
'Fall',
'Winter',
'Half year',
'Full year',
'Others',
];
export const InternshipCycleOptions = [emptyOption].concat(
InternshipCycleLabels.map((label) => ({
label,
value: label.replace(/\s+/g, '-').toLowerCase(),
})),
);

@ -1,7 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import { JobTypeLabel } from './types'; import { JobTypeLabel } from '~/components/offers/constants';
type Props = Readonly<{ type Props = Readonly<{
onChange: (jobType: JobType) => void; onChange: (jobType: JobType) => void;

@ -0,0 +1,8 @@
const NUM_YEARS = 5;
export const FutureYearsOptions = Array.from({ length: NUM_YEARS }, (_, i) => {
const year = new Date().getFullYear() + i;
return {
label: String(year),
value: year,
};
});

@ -1,78 +1,14 @@
import { EducationBackgroundType } from './types'; export const HOME_URL = '/offers';
export const emptyOption = '----'; export const JobTypeLabel = {
FULLTIME: 'Full-time',
INTERN: 'Internship',
};
export const internshipCycleOptions = [ export const emptyOption = {
{ label: '',
label: 'Summer', value: '',
value: 'Summer', };
},
{
label: 'Winter',
value: 'Winter',
},
{
label: 'Spring',
value: 'Spring',
},
{
label: 'Fall',
value: 'Fall',
},
{
label: 'Full year',
value: 'Full year',
},
];
export const yearOptions = [
{
label: '2021',
value: 2021,
},
{
label: '2022',
value: 2022,
},
{
label: '2023',
value: 2023,
},
{
label: '2024',
value: 2024,
},
];
export const educationLevelOptions = Object.entries(
EducationBackgroundType,
).map(([, value]) => ({
label: value,
value,
}));
export const educationFieldOptions = [
{
label: 'Computer Science',
value: 'Computer Science',
},
{
label: 'Information Security',
value: 'Information Security',
},
{
label: 'Information Systems',
value: 'Information Systems',
},
{
label: 'Business Analytics',
value: 'Business Analytics',
},
{
label: 'Data Science and Analytics',
value: 'Data Science and Analytics',
},
];
export enum FieldError { export enum FieldError {
NON_NEGATIVE_NUMBER = 'Please fill in a non-negative number in this field.', NON_NEGATIVE_NUMBER = 'Please fill in a non-negative number in this field.',

@ -37,28 +37,28 @@ export default function DashboardProfileCard({
</h4> </h4>
<div className="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-4"> <div className="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-4">
{company?.name && ( {company?.name && (
<div className="mt-2 flex items-center text-sm text-gray-500"> <div className="mt-2 flex items-center text-sm text-slate-500">
<BuildingOfficeIcon <BuildingOfficeIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/> />
{company.name} {company.name}
</div> </div>
)} )}
{location && ( {location && (
<div className="mt-2 flex items-center text-sm text-gray-500"> <div className="mt-2 flex items-center text-sm text-slate-500">
<MapPinIcon <MapPinIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/> />
{location.cityName} {location.cityName}
</div> </div>
)} )}
{level && ( {level && (
<div className="mt-2 flex items-center text-sm text-gray-500"> <div className="mt-2 flex items-center text-sm text-slate-500">
<ArrowTrendingUpIcon <ArrowTrendingUpIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/> />
{level} {level}
</div> </div>

@ -2,7 +2,7 @@ import type { StaticImageData } from 'next/image';
import Image from 'next/image'; import Image from 'next/image';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { HOME_URL } from '~/components/offers/types'; import { HOME_URL } from '../constants';
type LeftTextCardProps = Readonly<{ type LeftTextCardProps = Readonly<{
description: string; description: string;

@ -2,7 +2,7 @@ import type { StaticImageData } from 'next/image';
import Image from 'next/image'; import Image from 'next/image';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { HOME_URL } from '~/components/offers/types'; import { HOME_URL } from '../constants';
type RightTextCarddProps = Readonly<{ type RightTextCarddProps = Readonly<{
description: string; description: string;

@ -9,10 +9,11 @@ import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
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 { getCompanyDisplayText } from '~/utils/offers/string';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { JobTypeLabel } from '../constants';
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';
@ -69,7 +70,7 @@ export default function OfferProfileCard({
{getLabelForJobTitleType(title as JobTitleType)}{' '} {getLabelForJobTitleType(title as JobTitleType)}{' '}
{`(${JobTypeLabel[jobType]})`} {`(${JobTypeLabel[jobType]})`}
</p> </p>
<p>{`Company: ${company.name}, ${location}`}</p> <p>{`Company: ${getCompanyDisplayText(company.name, location)}`}</p>
{level && <p>Level: {level}</p>} {level && <p>Level: {level}</p>}
</div> </div>
<div className="col-span-1 row-span-3"> <div className="col-span-1 row-span-3">

@ -93,7 +93,6 @@ export default function OffersProfileSave({
<div className="mt-4 flex gap-4"> <div className="mt-4 flex gap-4">
<div className="grow"> <div className="grow">
<TextInput <TextInput
disabled={true}
isLabelHidden={true} isLabelHidden={true}
label="Edit link" label="Edit link"
value={getProfileLink(profileId, token)} value={getProfileLink(profileId, token)}

@ -4,11 +4,11 @@ import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import { Button } from '@tih/ui'; import { Button, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import type { BreadcrumbStep } from '~/components/offers/Breadcrumb'; import type { BreadcrumbStep } from '~/components/offers/Breadcrumbs';
import { Breadcrumbs } from '~/components/offers/Breadcrumb'; import { Breadcrumbs } from '~/components/offers/Breadcrumbs';
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 {
@ -102,8 +102,9 @@ export default function OffersSubmissionForm({
token: editToken, token: editToken,
}); });
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const { event: gaEvent } = useGoogleAnalytics();
const { showToast } = useToast();
const router = useRouter(); const router = useRouter();
const pageRef = useRef<HTMLDivElement>(null); const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () => const scrollToTop = () =>
@ -132,13 +133,7 @@ export default function OffersSubmissionForm({
}, },
); );
const steps = [ const steps = [<OfferDetailsForm key={0} />, <BackgroundForm key={1} />];
<OfferDetailsForm
key={0}
defaultJobType={initialOfferProfileValues.offers[0].jobType}
/>,
<BackgroundForm key={1} />,
];
const breadcrumbSteps: Array<BreadcrumbStep> = [ const breadcrumbSteps: Array<BreadcrumbStep> = [
{ {
@ -157,14 +152,14 @@ export default function OffersSubmissionForm({
}, },
]; ];
const goToNextStep = async (currStep: number) => { const setStepWithValidation = async (nextStep: number) => {
if (currStep === 0) { if (nextStep === 1) {
const result = await trigger('offers'); const result = await trigger('offers');
if (!result) { if (!result) {
return; return;
} }
} }
setStep(step + 1); setStep(nextStep);
}; };
const mutationpath = const mutationpath =
@ -175,10 +170,24 @@ export default function OffersSubmissionForm({
const createOrUpdateMutation = trpc.useMutation([mutationpath], { const createOrUpdateMutation = trpc.useMutation([mutationpath], {
onError(error) { onError(error) {
console.error(error.message); console.error(error.message);
showToast({
title:
editProfileId && editToken
? 'Error updating offer profile.'
: 'Error creating offer profile',
variant: 'failure',
});
}, },
onSuccess(data) { onSuccess(data) {
setParams({ profileId: data.id, token: data.token }); setParams({ profileId: data.id, token: data.token });
setIsSubmitted(true); setIsSubmitted(true);
showToast({
title:
editProfileId && editToken
? 'Offer profile updated successfully!'
: 'Offer profile created successfully!',
variant: 'success',
});
}, },
}); });
@ -270,7 +279,7 @@ export default function OffersSubmissionForm({
<div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8"> <div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8">
<Breadcrumbs <Breadcrumbs
currentStep={step} currentStep={step}
setStep={setStep} setStep={setStepWithValidation}
steps={breadcrumbSteps} steps={breadcrumbSteps}
/> />
</div> </div>
@ -288,7 +297,7 @@ export default function OffersSubmissionForm({
label="Next" label="Next"
variant="primary" variant="primary"
onClick={() => { onClick={() => {
goToNextStep(step); setStepWithValidation(step + 1);
gaEvent({ gaEvent({
action: 'offers.profile_submission_navigate_next', action: 'offers.profile_submission_navigate_next',
category: 'submission', category: 'submission',

@ -2,12 +2,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import { Collapsible, RadioList } from '@tih/ui'; import { Collapsible, RadioList } from '@tih/ui';
import { import { FieldError } from '~/components/offers/constants';
educationFieldOptions,
educationLevelOptions,
emptyOption,
FieldError,
} from '~/components/offers/constants';
import type { BackgroundPostData } from '~/components/offers/types'; import type { BackgroundPostData } from '~/components/offers/types';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead'; import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
@ -20,6 +15,8 @@ import {
CURRENCY_OPTIONS, CURRENCY_OPTIONS,
} from '~/utils/offers/currency/CurrencyEnum'; } from '~/utils/offers/currency/CurrencyEnum';
import { EducationFieldOptions } from '../../EducationFields';
import { EducationLevelOptions } from '../../EducationLevels';
import FormRadioList from '../../forms/FormRadioList'; import FormRadioList from '../../forms/FormRadioList';
import FormSection from '../../forms/FormSection'; import FormSection from '../../forms/FormSection';
import FormSelect from '../../forms/FormSelect'; import FormSelect from '../../forms/FormSelect';
@ -134,6 +131,9 @@ function FullTimeJobFields() {
if (option) { if (option) {
setValue('background.experiences.0.companyId', option.value); setValue('background.experiences.0.companyId', option.value);
setValue('background.experiences.0.companyName', option.label); setValue('background.experiences.0.companyName', option.label);
} else {
setValue('background.experiences.0.companyId', '');
setValue('background.experiences.0.companyName', '');
} }
}} }}
/> />
@ -343,15 +343,13 @@ function EducationSection() {
<FormSelect <FormSelect
display="block" display="block"
label="Education Level" label="Education Level"
options={educationLevelOptions} options={EducationLevelOptions}
placeholder={emptyOption}
{...register(`background.educations.0.type`)} {...register(`background.educations.0.type`)}
/> />
<FormSelect <FormSelect
display="block" display="block"
label="Field" label="Field"
options={educationFieldOptions} options={EducationFieldOptions}
placeholder={emptyOption}
{...register(`background.educations.0.field`)} {...register(`background.educations.0.field`)}
/> />
</div> </div>

@ -22,20 +22,16 @@ import {
defaultFullTimeOfferValues, defaultFullTimeOfferValues,
defaultInternshipOfferValues, defaultInternshipOfferValues,
} from '../OffersSubmissionForm'; } from '../OffersSubmissionForm';
import { import { FieldError, JobTypeLabel } from '../../constants';
emptyOption,
FieldError,
internshipCycleOptions,
yearOptions,
} from '../../constants';
import FormMonthYearPicker from '../../forms/FormMonthYearPicker'; import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
import FormSection from '../../forms/FormSection'; import FormSection from '../../forms/FormSection';
import FormSelect from '../../forms/FormSelect'; import FormSelect from '../../forms/FormSelect';
import FormTextArea from '../../forms/FormTextArea'; import FormTextArea from '../../forms/FormTextArea';
import FormTextInput from '../../forms/FormTextInput'; import FormTextInput from '../../forms/FormTextInput';
import { InternshipCycleOptions } from '../../InternshipCycles';
import JobTypeTabs from '../../JobTypeTabs'; import JobTypeTabs from '../../JobTypeTabs';
import type { OfferFormData } from '../../types'; import type { OfferFormData } from '../../types';
import { JobTypeLabel } from '../../types'; import { FutureYearsOptions } from '../../Years';
import { import {
Currency, Currency,
CURRENCY_OPTIONS, CURRENCY_OPTIONS,
@ -384,8 +380,7 @@ function InternshipOfferDetailsForm({
display="block" display="block"
errorMessage={offerFields?.offersIntern?.internshipCycle?.message} errorMessage={offerFields?.offersIntern?.internshipCycle?.message}
label="Internship Cycle" label="Internship Cycle"
options={internshipCycleOptions} options={InternshipCycleOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.offersIntern.internshipCycle`, { {...register(`offers.${index}.offersIntern.internshipCycle`, {
required: FieldError.REQUIRED, required: FieldError.REQUIRED,
@ -395,8 +390,7 @@ function InternshipOfferDetailsForm({
display="block" display="block"
errorMessage={offerFields?.offersIntern?.startYear?.message} errorMessage={offerFields?.offersIntern?.startYear?.message}
label="Internship Year" label="Internship Year"
options={yearOptions} options={FutureYearsOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.offersIntern.startYear`, { {...register(`offers.${index}.offersIntern.startYear`, {
required: FieldError.REQUIRED, required: FieldError.REQUIRED,
@ -522,14 +516,11 @@ function OfferDetailsFormArray({
); );
} }
type OfferDetailsFormProps = Readonly<{ export default function OfferDetailsForm() {
defaultJobType?: JobType; const watchJobType = useWatch({
}>; name: `offers.0.jobType`,
});
export default function OfferDetailsForm({ const [jobType, setJobType] = useState(watchJobType as JobType);
defaultJobType = JobType.FULLTIME,
}: OfferDetailsFormProps) {
const [jobType, setJobType] = useState(defaultJobType);
const [isDialogOpen, setDialogOpen] = useState(false); const [isDialogOpen, setDialogOpen] = useState(false);
const { control } = useFormContext(); const { control } = useFormContext();
const fieldArrayValues = useFieldArray({ control, name: 'offers' }); const fieldArrayValues = useFieldArray({ control, name: 'offers' });
@ -576,8 +567,8 @@ export default function OfferDetailsForm({
label="Switch" label="Switch"
variant="primary" variant="primary"
onClick={() => { onClick={() => {
toggleJobType();
setDialogOpen(false); setDialogOpen(false);
toggleJobType();
}} }}
/> />
} }

@ -13,12 +13,12 @@ export default function EducationCard({
education: { type, field, startDate, endDate, school }, education: { type, field, startDate, endDate, school },
}: Props) { }: Props) {
return ( return (
<div className="mx-8 my-4 block rounded-lg bg-white py-4 shadow-md"> <div className="block rounded-lg border border-slate-200 bg-white p-4 text-sm ">
<div className="flex justify-between px-8"> <div className="flex justify-between">
<div className="flex flex-col"> <div>
<div className="flex flex-row"> <div className="flex items-center">
<LightBulbIcon className="mr-1 h-5" /> <LightBulbIcon className="mr-1 h-5" />
<span className="ml-1 font-bold"> <span className="text-semibold ml-1">
{field ? `${type ?? 'N/A'}, ${field}` : type ?? `N/A`} {field ? `${type ?? 'N/A'}, ${field}` : type ?? `N/A`}
</span> </span>
</div> </div>

@ -1,13 +1,13 @@
import { import {
BuildingOffice2Icon, ArrowTrendingUpIcon,
ChatBubbleBottomCenterTextIcon, BuildingOfficeIcon,
CurrencyDollarIcon, MapPinIcon,
ScaleIcon, } from '@heroicons/react/20/solid';
} from '@heroicons/react/24/outline';
import { HorizontalDivider } from '@tih/ui';
import { JobTypeLabel } from '~/components/offers/constants';
import type { OfferDisplayData } from '~/components/offers/types'; import type { OfferDisplayData } from '~/components/offers/types';
import { JobTypeLabel } from '~/components/offers/types';
import { getLocationDisplayText } from '~/utils/offers/string';
type Props = Readonly<{ type Props = Readonly<{
offer: OfferDisplayData; offer: OfferDisplayData;
@ -33,34 +33,56 @@ export default function OfferCard({
}: Props) { }: Props) {
function UpperSection() { function UpperSection() {
return ( return (
<div className="flex justify-between px-8"> <div className="px-4 py-5 sm:px-6">
<div className="flex flex-col"> <div className="flex justify-between">
<div className="flex flex-row"> <div>
<span> <h3 className="text-lg font-medium leading-6 text-slate-900">
<BuildingOffice2Icon className="mr-3 h-5" /> {jobTitle} {jobType && <>({JobTypeLabel[jobType]})</>}
</span> </h3>
<span className="font-bold"> <div className="mt-1 flex flex-row flex-wrap space-x-4 sm:mt-0">
{location ? `${companyName}, ${location.cityName}` : companyName} {companyName && (
</span> <div className="mt-2 flex items-center text-sm text-slate-500">
</div> <BuildingOfficeIcon
<div className="ml-8 flex flex-row"> aria-hidden="true"
<p> className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
{jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}{' '} />
{jobType && `(${JobTypeLabel[jobType]})`} {companyName}
</p>
</div> </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"
/>
{getLocationDisplayText(location)}
</div> </div>
)}
{jobLevel && (
<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"
/>
{jobLevel}
</div>
)}
</div>
</div>
<div className="space-y-2">
{!duration && receivedMonth && ( {!duration && receivedMonth && (
<div className="font-light text-slate-400"> <div className="text-sm text-slate-500">
<p>{receivedMonth}</p> <p>{receivedMonth}</p>
</div> </div>
)} )}
{duration && ( {duration && (
<div className="font-light text-slate-400"> <div className="text-sm text-slate-500">
<p>{`${duration} months`}</p> <p>{`${duration} months`}</p>
</div> </div>
)} )}
</div> </div>
</div>
</div>
); );
} }
@ -75,60 +97,69 @@ export default function OfferCard({
} }
return ( return (
<> <div className="border-t border-slate-200 px-4 py-5 sm:px-6">
<HorizontalDivider /> <dl className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-4">
<div className="px-8"> {totalCompensation && (
<div className="flex flex-col py-2"> <div className="col-span-1">
{(totalCompensation || monthlySalary) && ( <dt className="text-sm font-medium text-slate-500">
<div className="flex flex-row"> Total Compensation
<span> </dt>
<CurrencyDollarIcon className="mr-3 h-5" /> <dd className="mt-1 text-sm text-slate-900">
</span> {totalCompensation}
<span> </dd>
<p>
{totalCompensation && `TC: ${totalCompensation}`}
{monthlySalary && `Monthly Salary: ${monthlySalary}`}
</p>
</span>
</div> </div>
)} )}
{(base || stocks || bonus) && totalCompensation && ( {monthlySalary && (
<div className="ml-8 flex flex-row font-light"> <div className="col-span-1">
<p> <dt className="text-sm font-medium text-slate-500">
Base / year: {base ?? 'N/A'} Stocks / year:{' '} Monthly Salary
{stocks ?? 'N/A'} Bonus / year: {bonus ?? 'N/A'} </dt>
</p> <dd className="mt-1 text-sm text-slate-900">{monthlySalary}</dd>
</div> </div>
)} )}
{base && (
<div className="col-span-1">
<dt className="text-sm font-medium text-slate-500">
Base Salary
</dt>
<dd className="mt-1 text-sm text-slate-900">{base}</dd>
</div> </div>
{negotiationStrategy && ( )}
<div className="flex flex-col py-2"> {stocks && (
<div className="flex flex-row"> <div className="col-span-1">
<span> <dt className="text-sm font-medium text-slate-500">Stocks</dt>
<ScaleIcon className="h-5 w-5" /> <dd className="mt-1 text-sm text-slate-900">{stocks}</dd>
</span>
<span className="overflow-wrap ml-2">
"{negotiationStrategy}"
</span>
</div> </div>
)}
{bonus && (
<div className="col-span-1">
<dt className="text-sm font-medium text-slate-500">Bonus</dt>
<dd className="mt-1 text-sm text-slate-900">{bonus}</dd>
</div> </div>
)} )}
{otherComment && ( {negotiationStrategy && (
<div className="flex flex-col py-2"> <div className="col-span-2">
<div className="flex flex-row"> <dt className="text-sm font-medium text-slate-500">
<span> Negotiation Strategy
<ChatBubbleBottomCenterTextIcon className="h-5 w-5" /> </dt>
</span> <dd className="mt-1 text-sm text-slate-900">
<span className="overflow-wrap ml-2">"{otherComment}"</span> {negotiationStrategy}
</dd>
</div> </div>
)}
{otherComment && (
<div className="col-span-2">
<dt className="text-sm font-medium text-slate-500">Others</dt>
<dd className="mt-1 text-sm text-slate-900">{otherComment}</dd>
</div> </div>
)} )}
</dl>
</div> </div>
</>
); );
} }
return ( return (
<div className="mx-8 my-4 block rounded-md border-b border-gray-300 bg-white py-4"> <div className="block rounded-lg border border-slate-200 bg-white">
<UpperSection /> <UpperSection />
<BottomSection /> <BottomSection />
</div> </div>

@ -110,10 +110,10 @@ export default function ProfileComments({
); );
} }
return ( return (
<div className="bh-white h-fit px-4 lg:h-[calc(100vh-4.5rem)] lg:overflow-y-auto"> <div className="bh-white h-fit p-4 lg:h-[calc(100vh-4.5rem)] lg:overflow-y-auto">
<div className="bg-white pt-4 lg:sticky lg:top-0"> <div className="bg-white lg:sticky lg:top-0">
<div className="flex justify-end"> <div className="flex justify-end">
<div className="grid w-fit space-y-2 lg:grid-cols-1 lg:grid-cols-2 lg:space-y-0 lg:space-x-4"> <div className="grid w-fit grid-cols-1 space-y-2 md:grid-cols-2 md:space-y-0 md:space-x-4">
<div className="col-span-1 flex justify-end"> <div className="col-span-1 flex justify-end">
{isEditable && ( {isEditable && (
<Tooltip tooltipContent="Copy this link to edit your profile later"> <Tooltip tooltipContent="Copy this link to edit your profile later">
@ -169,7 +169,8 @@ export default function ProfileComments({
</div> </div>
</div> </div>
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2> <div className="mt-2 mb-6 bg-white">
<h2 className="text-2xl font-bold">Discussions</h2>
{isEditable || session?.user?.name ? ( {isEditable || session?.user?.name ? (
<div> <div>
<TextArea <TextArea
@ -210,16 +211,20 @@ export default function ProfileComments({
/> />
)} )}
</div> </div>
<div className="w-full"> </div>
<section className="w-full">
<ul className="space-y-8" role="list">
{replies?.map((reply: Reply) => ( {replies?.map((reply: Reply) => (
<li key={reply.id}>
<ExpandableCommentCard <ExpandableCommentCard
key={reply.id}
comment={reply} comment={reply}
profileId={profileId} profileId={profileId}
token={isEditable ? token : undefined} token={isEditable ? token : undefined}
/> />
</li>
))} ))}
</div> </ul>
</section>
</div> </div>
); );
} }

@ -25,19 +25,19 @@ type ProfileOffersProps = Readonly<{
}>; }>;
function ProfileOffers({ offers }: ProfileOffersProps) { function ProfileOffers({ offers }: ProfileOffersProps) {
if (offers.length !== 0) { if (offers.length === 0) {
return ( return (
<> <div className="p-4">
{offers.map((offer) => ( <p className="font-semibold">No offers are attached.</p>
<OfferCard key={offer.id} offer={offer} /> </div>
))}
</>
); );
} }
return ( return (
<div className="mx-8 my-4 flex flex-row"> <div className="space-y-4 p-4">
<BriefcaseIcon className="mr-1 h-5" /> {offers.map((offer) => (
<span className="font-bold">No offer is attached.</span> <OfferCard key={offer.id} offer={offer} />
))}
</div> </div>
); );
} }
@ -49,33 +49,37 @@ type ProfileBackgroundProps = Readonly<{
function ProfileBackground({ background }: ProfileBackgroundProps) { function ProfileBackground({ background }: ProfileBackgroundProps) {
if (!background?.experiences?.length && !background?.educations?.length) { if (!background?.experiences?.length && !background?.educations?.length) {
return ( return (
<div className="mx-8 my-4"> <div className="p-4">
<p>No background information available.</p> <p>No background information available.</p>
</div> </div>
); );
} }
return ( return (
<> <div className="space-y-8 p-4">
{background?.experiences?.length > 0 && ( {background?.experiences?.length > 0 && (
<> <div className="space-y-2">
<div className="mx-8 my-4 flex flex-row"> <div className="flex items-center space-x-2 text-slate-500">
<BriefcaseIcon className="mr-1 h-5" /> <BriefcaseIcon className="h-5" />
<span className="font-bold">Work Experience</span> <h3 className="text-sm font-semibold uppercase tracking-wide">
Work Experience
</h3>
</div> </div>
<OfferCard offer={background.experiences[0]} /> <OfferCard offer={background.experiences[0]} />
</> </div>
)} )}
{background?.educations?.length > 0 && ( {background?.educations?.length > 0 && (
<> <div className="space-y-2">
<div className="mx-8 my-4 flex flex-row"> <div className="flex items-center space-x-2 text-slate-500">
<AcademicCapIcon className="mr-1 h-5" /> <AcademicCapIcon className="h-5" />
<span className="font-bold">Education</span> <h3 className="text-sm font-semibold uppercase tracking-wide">
Education
</h3>
</div> </div>
<EducationCard education={background.educations[0]} /> <EducationCard education={background.educations[0]} />
</> </div>
)} )}
</> </div>
); );
} }
@ -114,7 +118,7 @@ function ProfileAnalysis({
} }
return ( return (
<div className="mx-8 my-4"> <div className="p-4">
{!analysis ? ( {!analysis ? (
<p>No analysis available.</p> <p>No analysis available.</p>
) : ( ) : (
@ -165,12 +169,15 @@ export default function ProfileDetails({
</div> </div>
); );
} }
if (selectedTab === ProfileDetailTab.OFFERS) { if (selectedTab === ProfileDetailTab.OFFERS) {
return <ProfileOffers offers={offers} />; return <ProfileOffers offers={offers} />;
} }
if (selectedTab === ProfileDetailTab.BACKGROUND) { if (selectedTab === ProfileDetailTab.BACKGROUND) {
return <ProfileBackground background={background} />; return <ProfileBackground background={background} />;
} }
if (selectedTab === ProfileDetailTab.ANALYSIS) { if (selectedTab === ProfileDetailTab.ANALYSIS) {
return ( return (
<ProfileAnalysis <ProfileAnalysis

@ -13,10 +13,10 @@ import { Button, Dialog, Spinner, Tabs, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import type { ProfileDetailTab } from '~/components/offers/constants'; import type { ProfileDetailTab } from '~/components/offers/constants';
import { JobTypeLabel } from '~/components/offers/constants';
import { profileDetailTabs } from '~/components/offers/constants'; import { profileDetailTabs } from '~/components/offers/constants';
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 Tooltip from '~/components/offers/util/Tooltip'; import Tooltip from '~/components/offers/util/Tooltip';
import { getProfileEditPath } from '~/utils/offers/link'; import { getProfileEditPath } from '~/utils/offers/link';
@ -233,20 +233,20 @@ export default function ProfileHeader({
const { experiences, totalYoe, specificYoes, profileName } = background; const { experiences, totalYoe, specificYoes, profileName } = background;
return ( return (
<div className="grid-rows-2 bg-white p-4"> <div className="grid-rows-2 bg-white">
<div className="flex grid grid-cols-5 md:grid-cols-7"> <div className="grid grid-cols-5 p-4 md:grid-cols-7">
<div className="jsutify-start col-span-5 flex"> <div className="col-span-5 flex justify-start space-x-4">
<div className="ml-0 mr-2 mt-2 h-16 w-16 md:mx-4"> <div className="mt-2 h-16 w-16">
<ProfilePhotoHolder /> <ProfilePhotoHolder />
</div> </div>
<div> <div className="space-y-1">
<h2 className="flex text-2xl font-bold"> <h2 className="flex text-2xl font-bold">
{profileName ?? 'anonymous'} {profileName ?? 'anonymous'}
</h2> </h2>
{(experiences[0]?.companyName || {(experiences[0]?.companyName ||
experiences[0]?.jobLevel || experiences[0]?.jobLevel ||
experiences[0]?.jobTitle) && ( experiences[0]?.jobTitle) && (
<div className="flex flex-row"> <div className="flex items-center text-sm text-slate-600">
<span> <span>
<BuildingOffice2Icon className="mr-2.5 h-5 w-5" /> <BuildingOffice2Icon className="mr-2.5 h-5 w-5" />
</span> </span>
@ -262,7 +262,7 @@ export default function ProfileHeader({
</p> </p>
</div> </div>
)} )}
<div className="flex flex-row"> <div className="flex items-center text-sm text-slate-600">
<CalendarDaysIcon className="mr-2.5 h-5" /> <CalendarDaysIcon className="mr-2.5 h-5" />
<p> <p>
<span className="mr-2 font-bold">YOE:</span> <span className="mr-2 font-bold">YOE:</span>
@ -286,7 +286,7 @@ export default function ProfileHeader({
</div> </div>
)} )}
</div> </div>
<div className="mt-4"> <div className="border-t border-slate-200 p-4">
<Tabs <Tabs
label="Profile Detail Navigation" label="Profile Detail Navigation"
tabs={profileDetailTabs} tabs={profileDetailTabs}

@ -1,14 +1,9 @@
import { signIn, useSession } from 'next-auth/react'; import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react'; import { useState } from 'react';
import { import { Button, Dialog, TextArea, useToast } from '@tih/ui';
ChatBubbleBottomCenterIcon,
TrashIcon,
} from '@heroicons/react/24/outline';
import { Button, Dialog, HorizontalDivider, TextArea, useToast } from '@tih/ui';
import { timeSinceNow } from '~/utils/offers/time'; import { timeSinceNow } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
import { trpc } from '../../../../utils/trpc';
import type { Reply } from '~/types/offers'; import type { Reply } from '~/types/offers';
@ -125,48 +120,59 @@ export default function CommentCard({
} }
return ( return (
<> <div className="flex space-x-3">
<div className="flex pl-2"> {/* <div className="flex-shrink-0">
<div className="flex w-full flex-col"> <img
<div className="flex flex-row font-bold"> alt=""
className="h-10 w-10 rounded-full"
src={`https://images.unsplash.com/photo-${comment.imageId}?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80`}
/>
</div> */}
<div>
<div className="text-sm">
<p className="font-medium text-slate-900">
{user?.name ?? 'unknown user'} {user?.name ?? 'unknown user'}
</p>
</div> </div>
<div className="mt-2 mb-2 flex flex-row ">{message}</div> <div className="mt-1 text-sm text-slate-700">
<div className="flex flex-row items-center justify-start space-x-4 "> <p className="break-all">{message}</p>
<div className="flex flex-col text-sm font-light text-slate-400">{`${timeSinceNow( </div>
createdAt, <div className="mt-2 space-x-2 text-sm">
)} ago`}</div> <span className="font-medium text-slate-500">
{timeSinceNow(createdAt)} ago
</span>{' '}
<span className="font-medium text-slate-500">&middot;</span>{' '}
{replyLength > 0 && ( {replyLength > 0 && (
<div <>
className="text-primary-600 flex cursor-pointer flex-col text-sm hover:underline" <button
className="font-medium text-slate-900"
type="button"
onClick={handleExpanded}> onClick={handleExpanded}>
{isExpanded ? `Hide replies` : `View replies (${replyLength})`} {isExpanded ? `Hide replies` : `View replies (${replyLength})`}
</div> </button>
<span className="font-medium text-slate-500">&middot;</span>{' '}
</>
)} )}
{!disableReply && ( {!disableReply && (
<div className="flex flex-col"> <>
<Button <button
icon={ChatBubbleBottomCenterIcon} className="font-medium text-slate-900"
isLabelHidden={true} type="button"
label="Reply" onClick={() => setIsReplying(!isReplying)}>
size="sm" Reply
variant="tertiary" </button>
onClick={() => setIsReplying(!isReplying)} <span className="font-medium text-slate-500">&middot;</span>{' '}
/> </>
</div>
)} )}
{deletable && ( {deletable && (
<> <>
<Button <button
className="font-medium text-slate-900"
disabled={deleteCommentMutation.isLoading} disabled={deleteCommentMutation.isLoading}
icon={TrashIcon} type="button"
isLabelHidden={true} onClick={() => setIsDialogOpen(true)}>
isLoading={deleteCommentMutation.isLoading} {deleteCommentMutation.isLoading ? 'Deleting...' : 'Delete'}
label="Delete" </button>
size="sm"
variant="tertiary"
onClick={() => setIsDialogOpen(true)}
/>
{isDialogOpen && ( {isDialogOpen && (
<Dialog <Dialog
isShown={isDialogOpen} isShown={isDialogOpen}
@ -199,6 +205,12 @@ export default function CommentCard({
</div> </div>
{!disableReply && isReplying && ( {!disableReply && isReplying && (
<div className="mt-2 mr-2"> <div className="mt-2 mr-2">
<form
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
handleReply();
}}>
<TextArea <TextArea
isLabelHidden={true} isLabelHidden={true}
label="Comment" label="Comment"
@ -225,11 +237,10 @@ export default function CommentCard({
/> />
</div> </div>
</div> </div>
</form>
</div> </div>
)} )}
</div> </div>
</div> </div>
<HorizontalDivider />
</>
); );
} }

@ -26,18 +26,22 @@ export default function ExpandableCommentCard({
replyLength={comment.replies?.length ?? 0} replyLength={comment.replies?.length ?? 0}
token={token} token={token}
/> />
{comment.replies && ( {comment.replies && comment.replies.length > 0 && isExpanded && (
<div className="pl-8"> <div className="pt-4">
{isExpanded && <div className="border-l-4 border-slate-200 pl-4">
comment.replies.map((reply) => ( <ul className="space-y-4" role="list">
{comment.replies.map((reply) => (
<li key={reply.id}>
<CommentCard <CommentCard
key={reply.id}
comment={reply} comment={reply}
disableReply={true} disableReply={true}
profileId={profileId} profileId={profileId}
token={token} token={token}
/> />
</li>
))} ))}
</ul>
</div>
</div> </div>
)} )}
</div> </div>

@ -42,9 +42,9 @@ export default function OfferTableRow({
<td className="py-4 px-4">{convertMoneyToString(income)}</td> <td className="py-4 px-4">{convertMoneyToString(income)}</td>
{jobType === JobType.FULLTIME && ( {jobType === JobType.FULLTIME && (
<td className="py-4 px-4"> <td className="py-4 px-4">
{`${baseSalary && convertMoneyToString(baseSalary)} / ${ {`${convertMoneyToString(baseSalary)} / ${convertMoneyToString(
bonus && convertMoneyToString(bonus) bonus,
} / ${stocks && convertMoneyToString(stocks)}`} )} / ${convertMoneyToString(stocks)}`}
</td> </td>
)} )}
<td className="py-4 px-4">{formatDate(monthYearReceived)}</td> <td className="py-4 px-4">{formatDate(monthYearReceived)}</td>

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import { DropdownMenu, Spinner } from '@tih/ui'; import { DropdownMenu, Spinner, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import OffersTablePagination from '~/components/offers/table/OffersTablePagination'; import OffersTablePagination from '~/components/offers/table/OffersTablePagination';
@ -66,6 +66,7 @@ export default function OffersTable({
event?.preventDefault(); event?.preventDefault();
}, [yoeCategory]); }, [yoeCategory]);
const { showToast } = useToast();
trpc.useQuery( trpc.useQuery(
[ [
'offers.list', 'offers.list',
@ -81,8 +82,11 @@ export default function OffersTable({
}, },
], ],
{ {
onError: (err) => { onError: () => {
alert(err); showToast({
title: 'Error loading the page.',
variant: 'failure',
});
}, },
onSuccess: (response: GetOffersResponse) => { onSuccess: (response: GetOffersResponse) => {
setOffers(response.data); setOffers(response.data);
@ -95,7 +99,7 @@ export default function OffersTable({
function renderFilters() { function renderFilters() {
return ( return (
<div className="flex items-center justify-between p-4 text-sm sm:grid-cols-4 md:text-base"> <div className="flex items-center justify-between p-4 text-xs text-slate-700 sm:grid-cols-4 sm:text-sm md:text-base">
<DropdownMenu <DropdownMenu
align="start" align="start"
label={ label={
@ -200,13 +204,13 @@ export default function OffersTable({
} }
return ( return (
<thead className="text-slate-700"> <thead className="font-semibold">
<tr className="divide-x divide-slate-200"> <tr className="divide-x divide-slate-200">
{columns.map((header, index) => ( {columns.map((header, index) => (
<th <th
key={header} key={header}
className={clsx( className={clsx(
'bg-slate-100 py-3 px-4', 'whitespace-nowrap bg-slate-100 py-3 px-4',
// Make last column sticky. // Make last column sticky.
index === columns.length - 1 && index === columns.length - 1 &&
'sticky right-0 drop-shadow md:drop-shadow-none', 'sticky right-0 drop-shadow md:drop-shadow-none',
@ -235,7 +239,7 @@ export default function OffersTable({
</div> </div>
) : ( ) : (
<div className="overflow-x-auto text-slate-600"> <div className="overflow-x-auto text-slate-600">
<table className="w-full divide-y divide-slate-200 border-y border-slate-200 text-left"> <table className="w-full divide-y divide-slate-200 border-y border-slate-200 text-left text-xs text-slate-700 sm:text-sm md:text-base">
{renderHeader()} {renderHeader()}
<tbody> <tbody>
{offers.map((offer) => ( {offers.map((offer) => (
@ -247,9 +251,6 @@ export default function OffersTable({
(offers.length === 0 && ( (offers.length === 0 && (
<div className="py-16 text-lg"> <div className="py-16 text-lg">
<div className="flex justify-center">No data yet🥺</div> <div className="flex justify-center">No data yet🥺</div>
<div className="flex justify-center">
Please try another set of filters.
</div>
</div> </div>
))} ))}
</div> </div>

@ -26,7 +26,7 @@ export default function OffersTablePagination({
<div className="mb-2 text-sm font-normal text-slate-500 md:mb-0"> <div className="mb-2 text-sm font-normal text-slate-500 md:mb-0">
Showing Showing
<span className="font-semibold text-slate-900"> <span className="font-semibold text-slate-900">
{` ${startNumber} - ${endNumber} `} {` ${endNumber > 0 ? startNumber : 0} - ${endNumber} `}
</span> </span>
{`of `} {`of `}
<span className="font-semibold text-slate-900"> <span className="font-semibold text-slate-900">

@ -4,27 +4,6 @@ import type { MonthYear } from '~/components/shared/MonthYearPicker';
import type { Location } from '~/types/offers'; import type { Location } from '~/types/offers';
export const HOME_URL = '/offers';
/*
* Offer Profile
*/
export const JobTypeLabel = {
FULLTIME: 'Full-time',
INTERN: 'Internship',
};
export enum EducationBackgroundType {
Bachelor = 'Bachelor',
Diploma = 'Diploma',
Masters = 'Masters',
PhD = 'PhD',
Professional = 'Professional',
Secondary = 'Secondary',
SelfTaught = 'Self-taught',
}
export type OffersProfilePostData = { export type OffersProfilePostData = {
background: BackgroundPostData; background: BackgroundPostData;
id?: string; id?: string;

@ -57,7 +57,7 @@ export default function ResumeCommentsList({
} }
return ( return (
<div className="mb-8 flow-root h-[calc(100vh-13rem)] w-full flex-col space-y-10 overflow-y-auto overflow-x-hidden pb-16"> <div className="flow-root w-full flex-col space-y-10 overflow-y-auto overflow-x-hidden lg:h-[calc(100vh-13rem)]">
{RESUME_COMMENTS_SECTIONS.map(({ label, value }) => { {RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
const comments = commentsQuery.data const comments = commentsQuery.data
? commentsQuery.data.filter((comment: ResumeComment) => { ? commentsQuery.data.filter((comment: ResumeComment) => {

@ -87,6 +87,7 @@ const analysisOfferDtoMapper = (
background?.experiences background?.experiences
?.filter((exp) => exp.company != null) ?.filter((exp) => exp.company != null)
.map((exp) => exp.company?.name ?? '') ?? [], .map((exp) => exp.company?.name ?? '') ?? [],
profileId: offer.profileId,
profileName, profileName,
title: title:
offer.jobType === JobType.FULLTIME offer.jobType === JobType.FULLTIME
@ -95,7 +96,10 @@ const analysisOfferDtoMapper = (
totalYoe: background?.totalYoe ?? -1, totalYoe: background?.totalYoe ?? -1,
}; };
if (offer.offersFullTime?.totalCompensation) { if (
offer.offersFullTime?.totalCompensation &&
offer.jobType === JobType.FULLTIME
) {
analysisOfferDto.income.value = analysisOfferDto.income.value =
offer.offersFullTime.totalCompensation.value; offer.offersFullTime.totalCompensation.value;
analysisOfferDto.income.currency = analysisOfferDto.income.currency =
@ -105,7 +109,10 @@ const analysisOfferDtoMapper = (
offer.offersFullTime.totalCompensation.baseValue; offer.offersFullTime.totalCompensation.baseValue;
analysisOfferDto.income.baseCurrency = analysisOfferDto.income.baseCurrency =
offer.offersFullTime.totalCompensation.baseCurrency; offer.offersFullTime.totalCompensation.baseCurrency;
} else if (offer.offersIntern?.monthlySalary) { } else if (
offer.offersIntern?.monthlySalary &&
offer.jobType === JobType.INTERN
) {
analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value; analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value;
analysisOfferDto.income.currency = analysisOfferDto.income.currency =
offer.offersIntern.monthlySalary.currency; offer.offersIntern.monthlySalary.currency;
@ -369,11 +376,13 @@ export const experienceDtoMapper = (
experience.location != null experience.location != null
? locationDtoMapper(experience.location) ? locationDtoMapper(experience.location)
: null, : null,
monthlySalary: experience.monthlySalary monthlySalary:
experience.monthlySalary && experience.jobType === JobType.INTERN
? valuationDtoMapper(experience.monthlySalary) ? valuationDtoMapper(experience.monthlySalary)
: null, : null,
title: experience.title, title: experience.title,
totalCompensation: experience.totalCompensation totalCompensation:
experience.totalCompensation && experience.jobType === JobType.FULLTIME
? valuationDtoMapper(experience.totalCompensation) ? valuationDtoMapper(experience.totalCompensation)
: null, : null,
}; };
@ -460,11 +469,11 @@ export const profileOfferDtoMapper = (
location: locationDtoMapper(offer.location), location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
negotiationStrategy: offer.negotiationStrategy, negotiationStrategy: offer.negotiationStrategy,
offersFullTime: offer.offersFullTime, offersFullTime: null,
offersIntern: offer.offersIntern, offersIntern: null,
}; };
if (offer.offersFullTime) { if (offer.offersFullTime && offer.jobType === JobType.FULLTIME) {
profileOfferDto.offersFullTime = { profileOfferDto.offersFullTime = {
baseSalary: baseSalary:
offer.offersFullTime?.baseSalary != null offer.offersFullTime?.baseSalary != null
@ -485,7 +494,7 @@ export const profileOfferDtoMapper = (
offer.offersFullTime.totalCompensation, offer.offersFullTime.totalCompensation,
), ),
}; };
} else if (offer.offersIntern) { } else if (offer.offersIntern && offer.jobType === JobType.INTERN) {
profileOfferDto.offersIntern = { profileOfferDto.offersIntern = {
id: offer.offersIntern.id, id: offer.offersIntern.id,
internshipCycle: offer.offersIntern.internshipCycle, internshipCycle: offer.offersIntern.internshipCycle,
@ -701,30 +710,27 @@ export const dashboardOfferDtoMapper = (
totalYoe: offer.profile.background?.totalYoe ?? -1, totalYoe: offer.profile.background?.totalYoe ?? -1,
}; };
if (offer.offersFullTime) { if (offer.offersFullTime && offer.jobType === JobType.FULLTIME) {
dashboardOfferDto.income = valuationDtoMapper( dashboardOfferDto.income = valuationDtoMapper(
offer.offersFullTime.totalCompensation, offer.offersFullTime.totalCompensation,
); );
if (offer.offersFullTime.baseSalary) { if (offer.offersFullTime.baseSalary) {
dashboardOfferDto.baseSalary = valuationDtoMapper( dashboardOfferDto.baseSalary = valuationDtoMapper(
offer.offersFullTime.baseSalary offer.offersFullTime.baseSalary,
); );
} }
if (offer.offersFullTime.bonus) { if (offer.offersFullTime.bonus) {
dashboardOfferDto.bonus = valuationDtoMapper( dashboardOfferDto.bonus = valuationDtoMapper(offer.offersFullTime.bonus);
offer.offersFullTime.bonus
);
} }
if (offer.offersFullTime.stocks) { if (offer.offersFullTime.stocks) {
dashboardOfferDto.stocks = valuationDtoMapper( dashboardOfferDto.stocks = valuationDtoMapper(
offer.offersFullTime.stocks offer.offersFullTime.stocks,
); );
} }
} else if (offer.offersIntern) { } else if (offer.offersIntern && offer.jobType === JobType.INTERN) {
dashboardOfferDto.income = valuationDtoMapper( dashboardOfferDto.income = valuationDtoMapper(
offer.offersIntern.monthlySalary, offer.offersIntern.monthlySalary,
); );
@ -736,12 +742,12 @@ export const dashboardOfferDtoMapper = (
export const getOffersResponseMapper = ( export const getOffersResponseMapper = (
data: Array<DashboardOffer>, data: Array<DashboardOffer>,
paging: Paging, paging: Paging,
jobType: JobType jobType: JobType,
) => { ) => {
const getOffersResponse: GetOffersResponse = { const getOffersResponse: GetOffersResponse = {
data, data,
jobType, jobType,
paging paging,
}; };
return getOffersResponse; return getOffersResponse;
}; };
@ -817,7 +823,10 @@ const userProfileOfferDtoMapper = (
: offer.offersIntern?.title ?? '', : offer.offersIntern?.title ?? '',
}; };
if (offer.offersFullTime?.totalCompensation) { if (
offer.offersFullTime?.totalCompensation &&
offer.jobType === JobType.FULLTIME
) {
mappedOffer.income.value = offer.offersFullTime.totalCompensation.value; mappedOffer.income.value = offer.offersFullTime.totalCompensation.value;
mappedOffer.income.currency = mappedOffer.income.currency =
offer.offersFullTime.totalCompensation.currency; offer.offersFullTime.totalCompensation.currency;
@ -826,7 +835,10 @@ const userProfileOfferDtoMapper = (
offer.offersFullTime.totalCompensation.baseValue; offer.offersFullTime.totalCompensation.baseValue;
mappedOffer.income.baseCurrency = mappedOffer.income.baseCurrency =
offer.offersFullTime.totalCompensation.baseCurrency; offer.offersFullTime.totalCompensation.baseCurrency;
} else if (offer.offersIntern?.monthlySalary) { } else if (
offer.offersIntern?.monthlySalary &&
offer.jobType === JobType.INTERN
) {
mappedOffer.income.value = offer.offersIntern.monthlySalary.value; mappedOffer.income.value = offer.offersIntern.monthlySalary.value;
mappedOffer.income.currency = offer.offersIntern.monthlySalary.currency; mappedOffer.income.currency = offer.offersIntern.monthlySalary.currency;
mappedOffer.income.id = offer.offersIntern.monthlySalary.id; mappedOffer.income.id = offer.offersIntern.monthlySalary.id;

@ -46,8 +46,8 @@ export default function ProfilesDashboard() {
if (userProfiles.length === 0) { if (userProfiles.length === 0) {
return ( return (
<div className="flex w-full"> <div className="flex w-full">
<div className="m-auto mx-auto w-full justify-center text-xl"> <div className="w-full justify-center space-y-8 py-16 text-xl">
<div className="mb-8 flex w-full flex-row justify-center"> <div className="flex w-full flex-row justify-center">
<h2>You have not saved any offer profiles yet.</h2> <h2>You have not saved any offer profiles yet.</h2>
</div> </div>
<div className="flex flex-row justify-center"> <div className="flex flex-row justify-center">

@ -8,12 +8,12 @@ import {
UsersIcon, UsersIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { HOME_URL } from '~/components/offers/constants';
import offersAnalysis from '~/components/offers/features/images/offers-analysis.png'; import offersAnalysis from '~/components/offers/features/images/offers-analysis.png';
import offersBrowse from '~/components/offers/features/images/offers-browse.png'; import offersBrowse from '~/components/offers/features/images/offers-browse.png';
import offersProfile from '~/components/offers/features/images/offers-profile.png'; import offersProfile from '~/components/offers/features/images/offers-profile.png';
import LeftTextCard from '~/components/offers/features/LeftTextCard'; import LeftTextCard from '~/components/offers/features/LeftTextCard';
import RightTextCard from '~/components/offers/features/RightTextCard'; import RightTextCard from '~/components/offers/features/RightTextCard';
import { HOME_URL } from '~/components/offers/types';
const features = [ const features = [
{ {

@ -59,7 +59,7 @@ export default function OffersHomePage() {
offers. offers.
</div> </div>
</div> </div>
<div className="mt-6 flex flex-col items-center justify-center space-y-2 text-base text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg"> <div className="mt-6 flex flex-col items-center justify-center space-y-2 text-sm text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg">
<span>Viewing offers for</span> <span>Viewing offers for</span>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<JobTitlesTypeahead <JobTitlesTypeahead

@ -6,6 +6,7 @@ import { Spinner, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { ProfileDetailTab } from '~/components/offers/constants'; import { ProfileDetailTab } from '~/components/offers/constants';
import { HOME_URL } 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';
@ -13,7 +14,6 @@ import type {
BackgroundDisplayData, BackgroundDisplayData,
OfferDisplayData, OfferDisplayData,
} from '~/components/offers/types'; } from '~/components/offers/types';
import { HOME_URL } from '~/components/offers/types';
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';
@ -132,6 +132,7 @@ export default function OfferProfile() {
? getLabelForJobTitleType(experience.title as JobTitleType) ? getLabelForJobTitleType(experience.title as JobTitleType)
: null, : null,
jobType: experience.jobType || undefined, jobType: experience.jobType || undefined,
location: experience.location,
monthlySalary: experience.monthlySalary monthlySalary: experience.monthlySalary
? convertMoneyToString(experience.monthlySalary) ? convertMoneyToString(experience.monthlySalary)
: null, : null,
@ -214,7 +215,7 @@ export default function OfferProfile() {
setSelectedTab={setSelectedTab} setSelectedTab={setSelectedTab}
/> />
</div> </div>
<div className="pb-4"> <div>
<ProfileDetails <ProfileDetails
analysis={analysis} analysis={analysis}
background={background} background={background}

@ -1,11 +1,12 @@
import Error from 'next/error';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { EyeIcon } from '@heroicons/react/24/outline'; import { EyeIcon } from '@heroicons/react/24/outline';
import { Button, Spinner } from '@tih/ui'; import { Button, Spinner } from '@tih/ui';
import type { BreadcrumbStep } from '~/components/offers/Breadcrumb'; import type { BreadcrumbStep } from '~/components/offers/Breadcrumbs';
import { Breadcrumbs } from '~/components/offers/Breadcrumb'; import { Breadcrumbs } from '~/components/offers/Breadcrumbs';
import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave'; import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave';
import OffersSubmissionAnalysis from '~/components/offers/offersSubmission/OffersSubmissionAnalysis'; import OffersSubmissionAnalysis from '~/components/offers/offersSubmission/OffersSubmissionAnalysis';
@ -21,12 +22,21 @@ export default function OffersSubmissionResult() {
token = token as string; token = token as string;
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null); const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
const [isValidToken, setIsValidToken] = useState(false);
const pageRef = useRef<HTMLDivElement>(null); const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () => const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 }); pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
// TODO: Check if the token is valid before showing this page const checkToken = trpc.useQuery(
['offers.profile.isValidToken', { profileId: offerProfileId, token }],
{
onSuccess(data) {
setIsValidToken(data);
},
},
);
const getAnalysis = trpc.useQuery( const getAnalysis = trpc.useQuery(
['offers.analysis.get', { profileId: offerProfileId }], ['offers.analysis.get', { profileId: offerProfileId }],
{ {
@ -69,7 +79,7 @@ export default function OffersSubmissionResult() {
return ( return (
<> <>
{getAnalysis.isLoading && ( {(checkToken.isLoading || getAnalysis.isLoading) && (
<div className="flex h-screen w-screen"> <div className="flex h-screen w-screen">
<div className="m-auto mx-auto w-screen justify-center font-medium text-slate-500"> <div className="m-auto mx-auto w-screen justify-center font-medium text-slate-500">
<Spinner display="block" size="lg" /> <Spinner display="block" size="lg" />
@ -77,7 +87,13 @@ export default function OffersSubmissionResult() {
</div> </div>
</div> </div>
)} )}
{!getAnalysis.isLoading && ( {checkToken.isSuccess && !isValidToken && (
<Error
statusCode={403}
title="You do not have permissions to access this page"
/>
)}
{getAnalysis.isSuccess && (
<div ref={pageRef} className="w-full"> <div ref={pageRef} className="w-full">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10"> <div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10">

@ -236,8 +236,8 @@ export default function ResumeReviewPage() {
<Head> <Head>
<title>{detailsQuery.data.title}</title> <title>{detailsQuery.data.title}</title>
</Head> </Head>
<main className="h-full flex-1 space-y-2 overflow-y-auto py-4 px-8 xl:px-12 2xl:pr-16"> <main className="h-full flex-1 space-y-2 py-4 px-8 xl:px-12 2xl:pr-16">
<div className="flex justify-between"> <div className="flex flex-wrap justify-between">
<h1 className="w-[60%] pr-2 text-2xl font-semibold leading-7 text-slate-900"> <h1 className="w-[60%] pr-2 text-2xl font-semibold leading-7 text-slate-900">
{detailsQuery.data.title} {detailsQuery.data.title}
</h1> </h1>

@ -395,7 +395,7 @@ export default function ResumeHomePage() {
leave="transition ease-in-out duration-300 transform" leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0" leaveFrom="translate-x-0"
leaveTo="translate-x-full"> leaveTo="translate-x-full">
<Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-scroll bg-white py-4 pb-12 shadow-xl"> <Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-auto bg-white py-4 pb-12 shadow-xl">
<div className="flex items-center justify-between px-4"> <div className="flex items-center justify-between px-4">
<h2 className="text-lg font-medium text-slate-900"> <h2 className="text-lg font-medium text-slate-900">
Quick access Quick access
@ -614,7 +614,7 @@ export default function ResumeHomePage() {
</div> </div>
</div> </div>
<div className="relative lg:left-64 lg:w-[calc(100%-16rem)]"> <div className="relative lg:left-64 lg:w-[calc(100%-16rem)]">
<div className="lg:border-grey-200 sticky top-0 z-10 flex flex-wrap items-center justify-between pt-6 pb-2 lg:border-b"> <div className="lg:border-grey-200 sticky top-16 z-10 flex flex-wrap items-center justify-between bg-slate-50 pt-6 pb-2 lg:border-b">
<div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none xl:pb-0"> <div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none xl:pb-0">
<div> <div>
<Tabs <Tabs
@ -705,13 +705,13 @@ export default function ResumeHomePage() {
{getEmptyDataText(tabsValue, searchValue, userFilters)} {getEmptyDataText(tabsValue, searchValue, userFilters)}
</div> </div>
) : ( ) : (
<div className="h-[calc(100vh-9rem)] pb-10 lg:h-[calc(100vh-6rem)]"> <div>
<div className="h-[85%] overflow-y-auto"> <div className="h-[85%] overflow-y-auto">
<div> <div>
<ResumeListItems resumes={getTabResumes()} /> <ResumeListItems resumes={getTabResumes()} />
</div> </div>
</div> </div>
<div className="flex h-[15%] items-center justify-center"> <div className="flex h-[15%] items-center justify-center p-4">
{getTabTotalPages() > 1 && ( {getTabTotalPages() > 1 && (
<div> <div>
<Pagination <Pagination

@ -246,7 +246,7 @@ export default function SubmitResumeForm({
</div> </div>
)} )}
{status === 'authenticated' && ( {status === 'authenticated' && (
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-auto"> <main className="flex-1">
<section <section
aria-labelledby="primary-heading" aria-labelledby="primary-heading"
className="flex h-full min-w-0 flex-1 flex-col lg:order-last"> className="flex h-full min-w-0 flex-1 flex-col lg:order-last">

@ -972,7 +972,19 @@ export const offersProfileRouter = createRouter()
for (const exp of input.background.experiences) { for (const exp of input.background.experiences) {
if (exp.id) { if (exp.id) {
// Update existing experience const currentExp = await ctx.prisma.offersExperience.findFirst({
where: {
id: exp.id
}
})
if (!currentExp) {
throw new trpc.TRPCError({
code: 'NOT_FOUND',
message: 'Experience does not exist',
});
}
await ctx.prisma.offersExperience.update({ await ctx.prisma.offersExperience.update({
data: { data: {
companyId: exp.companyId, // TODO: check if can change with connect or whether there is a difference companyId: exp.companyId, // TODO: check if can change with connect or whether there is a difference
@ -984,7 +996,77 @@ export const offersProfileRouter = createRouter()
id: exp.id, id: exp.id,
}, },
}); });
if (currentExp.jobType === exp.jobType) {
// Update existing experience
if (exp.monthlySalary) {
await ctx.prisma.offersExperience.update({
data: {
monthlySalary: {
upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
update: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
},
},
where: {
id: exp.id,
},
});
}
if (exp.totalCompensation) {
await ctx.prisma.offersExperience.update({
data: {
totalCompensation: {
upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
update: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
},
},
where: {
id: exp.id,
},
});
}
} else if (exp.jobType === JobType.INTERN) {
// Add 1 remove the other
if (exp.monthlySalary) { if (exp.monthlySalary) {
await ctx.prisma.offersExperience.update({ await ctx.prisma.offersExperience.update({
data: { data: {
@ -1019,6 +1101,16 @@ export const offersProfileRouter = createRouter()
}); });
} }
await ctx.prisma.offersExperience.update({
data: {
totalCompensation: undefined,
totalCompensationId: null,
},
where: {
id: exp.id
}
})
} else if (exp.jobType === JobType.FULLTIME) {
if (exp.totalCompensation) { if (exp.totalCompensation) {
await ctx.prisma.offersExperience.update({ await ctx.prisma.offersExperience.update({
data: { data: {
@ -1052,6 +1144,17 @@ export const offersProfileRouter = createRouter()
}, },
}); });
} }
await ctx.prisma.offersExperience.update({
data: {
monthlySalary: undefined,
monthlySalaryId: null
},
where: {
id: exp.id
}
})
}
} else if (!exp.id) { } else if (!exp.id) {
// Create new experience // Create new experience
if (exp.jobType === JobType.FULLTIME) { if (exp.jobType === JobType.FULLTIME) {
@ -1581,6 +1684,18 @@ export const offersProfileRouter = createRouter()
for (const offerToUpdate of input.offers) { for (const offerToUpdate of input.offers) {
if (offerToUpdate.id) { if (offerToUpdate.id) {
// Update existing offer // Update existing offer
const currentOffer = await ctx.prisma.offersOffer.findFirst({
where: {
id: offerToUpdate.id
}
})
if (!currentOffer) {
throw new trpc.TRPCError({
code: 'NOT_FOUND',
message: 'Offer to update does not exist',
});
}
await ctx.prisma.offersOffer.update({ await ctx.prisma.offersOffer.update({
data: { data: {
comments: offerToUpdate.comments, comments: offerToUpdate.comments,
@ -1606,6 +1721,7 @@ export const offersProfileRouter = createRouter()
}, },
}); });
if (currentOffer.jobType === offerToUpdate.jobType) {
if (offerToUpdate.offersIntern?.monthlySalary != null) { if (offerToUpdate.offersIntern?.monthlySalary != null) {
await ctx.prisma.offersIntern.update({ await ctx.prisma.offersIntern.update({
data: { data: {
@ -1799,6 +1915,221 @@ export const offersProfileRouter = createRouter()
}, },
}); });
} }
} else if (currentOffer.jobType === JobType.FULLTIME) {
if (offerToUpdate.offersFullTime?.totalCompensation != null) {
await ctx.prisma.offersFullTime.update({
data: {
level: offerToUpdate.offersFullTime.level ?? undefined,
title: offerToUpdate.offersFullTime.title,
},
where: {
id: offerToUpdate.offersFullTime.id,
},
});
if (offerToUpdate.offersFullTime.baseSalary != null) {
await ctx.prisma.offersFullTime.update({
data: {
baseSalary: {
upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.baseSalary.value,
offerToUpdate.offersFullTime.baseSalary.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.baseSalary.currency,
value: offerToUpdate.offersFullTime.baseSalary.value,
},
update: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.baseSalary.value,
offerToUpdate.offersFullTime.baseSalary.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.baseSalary.currency,
value: offerToUpdate.offersFullTime.baseSalary.value,
},
},
},
},
where: {
id: offerToUpdate.offersFullTime.id,
},
});
}
if (offerToUpdate.offersFullTime.bonus != null) {
await ctx.prisma.offersFullTime.update({
data: {
bonus: {
upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.bonus.value,
offerToUpdate.offersFullTime.bonus.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.bonus.currency,
value: offerToUpdate.offersFullTime.bonus.value,
},
update: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.bonus.value,
offerToUpdate.offersFullTime.bonus.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.bonus.currency,
value: offerToUpdate.offersFullTime.bonus.value,
},
},
},
},
where: {
id: offerToUpdate.offersFullTime.id,
},
});
}
if (offerToUpdate.offersFullTime.stocks != null) {
await ctx.prisma.offersFullTime.update({
data: {
stocks: {
upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.stocks.value,
offerToUpdate.offersFullTime.stocks.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.stocks.currency,
value: offerToUpdate.offersFullTime.stocks.value,
},
update: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.stocks.value,
offerToUpdate.offersFullTime.stocks.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.stocks.currency,
value: offerToUpdate.offersFullTime.stocks.value,
},
},
},
},
where: {
id: offerToUpdate.offersFullTime.id,
},
});
}
await ctx.prisma.offersFullTime.update({
data: {
totalCompensation: {
upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.totalCompensation.value,
offerToUpdate.offersFullTime.totalCompensation
.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.totalCompensation
.currency,
value:
offerToUpdate.offersFullTime.totalCompensation.value,
},
update: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.totalCompensation.value,
offerToUpdate.offersFullTime.totalCompensation
.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.totalCompensation
.currency,
value:
offerToUpdate.offersFullTime.totalCompensation.value,
},
},
},
},
where: {
id: offerToUpdate.offersFullTime.id,
},
});
}
await ctx.prisma.offersOffer.update({
data: {
offersIntern: undefined,
offersInternId: null
},
where: {
id: offerToUpdate.id
}
})
} else if (currentOffer.jobType === JobType.INTERN) {
if (offerToUpdate.offersIntern?.monthlySalary != null) {
await ctx.prisma.offersIntern.update({
data: {
internshipCycle:
offerToUpdate.offersIntern.internshipCycle ?? undefined,
monthlySalary: {
upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersIntern.monthlySalary.value,
offerToUpdate.offersIntern.monthlySalary.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersIntern.monthlySalary.currency,
value: offerToUpdate.offersIntern.monthlySalary.value,
},
update: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersIntern.monthlySalary.value,
offerToUpdate.offersIntern.monthlySalary.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersIntern.monthlySalary.currency,
value: offerToUpdate.offersIntern.monthlySalary.value,
},
},
},
startYear: offerToUpdate.offersIntern.startYear ?? undefined,
title: offerToUpdate.offersIntern.title,
},
where: {
id: offerToUpdate.offersIntern.id,
},
});
}
await ctx.prisma.offersOffer.update({
data: {
offersFullTime: undefined,
offersFullTimeId: null
},
where: {
id: offerToUpdate.id
}
})
}
} else { } else {
// Create new offer // Create new offer
if ( if (

@ -181,6 +181,7 @@ export type AnalysisOffer = {
monthYearReceived: Date; monthYearReceived: Date;
negotiationStrategy: string; negotiationStrategy: string;
previousCompanies: Array<string>; previousCompanies: Array<string>;
profileId: string;
profileName: string; profileName: string;
title: string; title: string;
totalYoe: number; totalYoe: number;

@ -20,7 +20,7 @@ export default function CurrencySelector({
<Select <Select
display="inline" display="inline"
isLabelHidden={true} isLabelHidden={true}
label="Select fruit" label="Currency"
name="" name=""
options={currencyOptions} options={currencyOptions}
value={selectedCurrency} value={selectedCurrency}

@ -4,7 +4,11 @@ import { Currency } from './CurrencyEnum';
export const baseCurrencyString = Currency.USD.toString(); export const baseCurrencyString = Currency.USD.toString();
export function convertMoneyToString({ currency, value }: Money) { export function convertMoneyToString(money: Money | undefined) {
if (!money) {
return '-';
}
const { currency, value } = money;
if (!value) { if (!value) {
return '-'; return '-';
} }

@ -0,0 +1,37 @@
import type { JobType } from '@prisma/client';
import { JobTypeLabel } from '~/components/offers/constants';
import type { Location } from '~/types/offers';
export function joinWithComma(...strings: Array<string | null | undefined>) {
return strings.filter((value) => !!value).join(', ');
}
export function getLocationDisplayText({ cityName, countryName }: Location) {
return cityName === countryName
? cityName
: joinWithComma(cityName, countryName);
}
export function getCompanyDisplayText(
companyName?: string | null,
location?: Location | null,
) {
if (!location) {
return companyName;
}
return joinWithComma(companyName, getLocationDisplayText(location));
}
export function getJobDisplayText(
jobTitle?: string | null,
jobLevel?: string | null,
jobType?: JobType | null,
) {
let jobDisplay = joinWithComma(jobTitle, jobLevel);
if (jobType) {
jobDisplay = jobDisplay.concat(` (${JobTypeLabel[jobType]})`);
}
return jobDisplay;
}

@ -144,7 +144,7 @@ export default function Typeahead({
<Combobox.Input <Combobox.Input
aria-describedby={hasError ? errorId : undefined} aria-describedby={hasError ? errorId : undefined}
className={clsx( className={clsx(
'w-full border-none py-2 pl-3 pr-10 leading-5 focus:ring-0', 'w-full border-none py-2 pl-3 pr-10 text-[length:inherit] leading-5 focus:ring-0',
stateClasses[state].input, stateClasses[state].input,
textSizes[textSize], textSizes[textSize],
'disabled:cursor-not-allowed disabled:bg-transparent disabled:text-slate-500', 'disabled:cursor-not-allowed disabled:bg-transparent disabled:text-slate-500',

Loading…
Cancel
Save