mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 04:33:42 +08:00
[offers][feat] Integrate offers profile edit (#403)
* [offers][fix] Fix offer analysis and save * [offers][fix] Fix profile view page * [offers][feat] Add offers profile edit
This commit is contained in:
@ -5,20 +5,20 @@ export const emptyOption = '----';
|
|||||||
// TODO: use enums
|
// TODO: use enums
|
||||||
export const titleOptions = [
|
export const titleOptions = [
|
||||||
{
|
{
|
||||||
label: 'Software engineer',
|
label: 'Software Engineer',
|
||||||
value: 'Software engineer',
|
value: 'Software Engineer',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Frontend engineer',
|
label: 'Frontend Engineer',
|
||||||
value: 'Frontend engineer',
|
value: 'Frontend Engineer',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Backend engineer',
|
label: 'Backend Engineer',
|
||||||
value: 'Backend engineer',
|
value: 'Backend Engineer',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Full-stack engineer',
|
label: 'Full-stack Engineer',
|
||||||
value: 'Full-stack engineer',
|
value: 'Full-stack Engineer',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -95,10 +95,18 @@ export const educationFieldOptions = [
|
|||||||
label: 'Information Security',
|
label: 'Information Security',
|
||||||
value: 'Information Security',
|
value: 'Information Security',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Information Systems',
|
||||||
|
value: 'Information Systems',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Business Analytics',
|
label: 'Business Analytics',
|
||||||
value: 'Business Analytics',
|
value: 'Business Analytics',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Data Science and Analytics',
|
||||||
|
value: 'Data Science and Analytics',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export enum FieldError {
|
export enum FieldError {
|
||||||
|
@ -1,13 +1,30 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { setTimeout } from 'timers';
|
import { setTimeout } from 'timers';
|
||||||
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/20/solid';
|
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/20/solid';
|
||||||
import { BookmarkSquareIcon, EyeIcon } from '@heroicons/react/24/outline';
|
import { BookmarkSquareIcon, EyeIcon } from '@heroicons/react/24/outline';
|
||||||
import { Button, TextInput } from '@tih/ui';
|
import { Button, TextInput } from '@tih/ui';
|
||||||
|
|
||||||
export default function OfferProfileSave() {
|
import {
|
||||||
|
copyProfileLink,
|
||||||
|
getProfileLink,
|
||||||
|
getProfilePath,
|
||||||
|
} from '~/utils/offers/link';
|
||||||
|
|
||||||
|
type OfferProfileSaveProps = Readonly<{
|
||||||
|
profileId: string;
|
||||||
|
token?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function OfferProfileSave({
|
||||||
|
profileId,
|
||||||
|
token,
|
||||||
|
}: OfferProfileSaveProps) {
|
||||||
const [linkCopied, setLinkCopied] = useState(false);
|
const [linkCopied, setLinkCopied] = useState(false);
|
||||||
const [isSaving, setSaving] = useState(false);
|
const [isSaving, setSaving] = useState(false);
|
||||||
const [isSaved, setSaved] = useState(false);
|
const [isSaved, setSaved] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const saveProfile = () => {
|
const saveProfile = () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -27,13 +44,13 @@ export default function OfferProfileSave() {
|
|||||||
To keep you offer profile strictly anonymous, only people who have the
|
To keep you offer profile strictly anonymous, only people who have the
|
||||||
link below can edit it.
|
link below can edit it.
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-20 grid grid-cols-12 gap-4">
|
<div className="mb-5 grid grid-cols-12 gap-4">
|
||||||
<div className="col-span-11">
|
<div className="col-span-11">
|
||||||
<TextInput
|
<TextInput
|
||||||
disabled={true}
|
disabled={true}
|
||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Edit link"
|
label="Edit link"
|
||||||
value="link.myprofile-auto-generate..."
|
value={getProfileLink(profileId, token)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -41,10 +58,12 @@ export default function OfferProfileSave() {
|
|||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Copy"
|
label="Copy"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => setLinkCopied(true)}
|
onClick={() => {
|
||||||
|
copyProfileLink(profileId, token), setLinkCopied(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5">
|
<div className="mb-20">
|
||||||
{linkCopied && (
|
{linkCopied && (
|
||||||
<p className="text-purple-700">Link copied to clipboard!</p>
|
<p className="text-purple-700">Link copied to clipboard!</p>
|
||||||
)}
|
)}
|
||||||
@ -60,13 +79,18 @@ export default function OfferProfileSave() {
|
|||||||
disabled={isSaved}
|
disabled={isSaved}
|
||||||
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
|
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
|
||||||
isLoading={isSaving}
|
isLoading={isSaving}
|
||||||
label="Save to user profile"
|
label={isSaved ? 'Saved to user profile' : 'Save to user profile'}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={saveProfile}
|
onClick={saveProfile}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<Button icon={EyeIcon} label="View your profile" variant="special" />
|
<Button
|
||||||
|
icon={EyeIcon}
|
||||||
|
label="View your profile"
|
||||||
|
variant="special"
|
||||||
|
onClick={() => router.push(getProfilePath(profileId, token))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -0,0 +1,243 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||||
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
|
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
|
||||||
|
import OfferAnalysis from '~/components/offers/offersSubmission/analysis/OfferAnalysis';
|
||||||
|
import OfferProfileSave from '~/components/offers/offersSubmission/OfferProfileSave';
|
||||||
|
import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm';
|
||||||
|
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
|
||||||
|
import type {
|
||||||
|
OfferFormData,
|
||||||
|
OffersProfileFormData,
|
||||||
|
} from '~/components/offers/types';
|
||||||
|
import { JobType } from '~/components/offers/types';
|
||||||
|
import type { Month } from '~/components/shared/MonthYearPicker';
|
||||||
|
|
||||||
|
import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
|
||||||
|
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import type { CreateOfferProfileResponse } from '~/types/offers';
|
||||||
|
|
||||||
|
const defaultOfferValues = {
|
||||||
|
comments: '',
|
||||||
|
companyId: '',
|
||||||
|
jobType: JobType.FullTime,
|
||||||
|
location: '',
|
||||||
|
monthYearReceived: {
|
||||||
|
month: getCurrentMonth() as Month,
|
||||||
|
year: getCurrentYear(),
|
||||||
|
},
|
||||||
|
negotiationStrategy: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultFullTimeOfferValues = {
|
||||||
|
...defaultOfferValues,
|
||||||
|
jobType: JobType.FullTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultInternshipOfferValues = {
|
||||||
|
...defaultOfferValues,
|
||||||
|
jobType: JobType.Intern,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOfferProfileValues = {
|
||||||
|
background: {
|
||||||
|
educations: [],
|
||||||
|
experiences: [{ jobType: JobType.FullTime }],
|
||||||
|
specificYoes: [],
|
||||||
|
totalYoe: 0,
|
||||||
|
},
|
||||||
|
offers: [defaultOfferValues],
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormStep = {
|
||||||
|
component: JSX.Element;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrevious: boolean;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
initialOfferProfileValues?: OffersProfileFormData;
|
||||||
|
profileId?: string;
|
||||||
|
token?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function OffersSubmissionForm({
|
||||||
|
initialOfferProfileValues = defaultOfferProfileValues,
|
||||||
|
profileId,
|
||||||
|
token,
|
||||||
|
}: Props) {
|
||||||
|
const [formStep, setFormStep] = useState(0);
|
||||||
|
const [createProfileResponse, setCreateProfileResponse] =
|
||||||
|
useState<CreateOfferProfileResponse>({
|
||||||
|
id: profileId || '',
|
||||||
|
token: token || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollToTop = () =>
|
||||||
|
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
|
||||||
|
const formMethods = useForm<OffersProfileFormData>({
|
||||||
|
defaultValues: initialOfferProfileValues,
|
||||||
|
mode: 'all',
|
||||||
|
});
|
||||||
|
const { handleSubmit, trigger } = formMethods;
|
||||||
|
|
||||||
|
const formSteps: Array<FormStep> = [
|
||||||
|
{
|
||||||
|
component: <OfferDetailsForm key={0} />,
|
||||||
|
hasNext: true,
|
||||||
|
hasPrevious: false,
|
||||||
|
label: 'Offer details',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: <BackgroundForm key={1} />,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrevious: true,
|
||||||
|
label: 'Background',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: <OfferAnalysis key={2} profileId={createProfileResponse.id} />,
|
||||||
|
hasNext: true,
|
||||||
|
hasPrevious: false,
|
||||||
|
label: 'Analysis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<OfferProfileSave
|
||||||
|
key={3}
|
||||||
|
profileId={createProfileResponse.id || ''}
|
||||||
|
token={createProfileResponse.token}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
hasNext: false,
|
||||||
|
hasPrevious: false,
|
||||||
|
label: 'Save',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const formStepsLabels = formSteps.map((step) => step.label);
|
||||||
|
|
||||||
|
const nextStep = async (currStep: number) => {
|
||||||
|
if (currStep === 0) {
|
||||||
|
const result = await trigger('offers');
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFormStep(formStep + 1);
|
||||||
|
scrollToTop();
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousStep = () => {
|
||||||
|
setFormStep(formStep - 1);
|
||||||
|
scrollToTop();
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateAnalysisMutation = trpc.useMutation(
|
||||||
|
['offers.analysis.generate'],
|
||||||
|
{
|
||||||
|
onError(error) {
|
||||||
|
console.error(error.message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutationpath =
|
||||||
|
profileId && token ? 'offers.profile.update' : 'offers.profile.create';
|
||||||
|
|
||||||
|
const createOrUpdateMutation = trpc.useMutation([mutationpath], {
|
||||||
|
onError(error) {
|
||||||
|
console.error(error.message);
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
generateAnalysisMutation.mutate({
|
||||||
|
profileId: data?.id || '',
|
||||||
|
});
|
||||||
|
setCreateProfileResponse(data);
|
||||||
|
setFormStep(formStep + 1);
|
||||||
|
scrollToTop();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<OffersProfileFormData> = async (data) => {
|
||||||
|
const result = await trigger();
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = removeInvalidMoneyData(data);
|
||||||
|
|
||||||
|
const background = cleanObject(data.background);
|
||||||
|
background.specificYoes = data.background.specificYoes.filter(
|
||||||
|
(specificYoe) => specificYoe.domain && specificYoe.yoe > 0,
|
||||||
|
);
|
||||||
|
if (Object.entries(background.experiences[0]).length === 1) {
|
||||||
|
background.experiences = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const offers = data.offers.map((offer: OfferFormData) => ({
|
||||||
|
...offer,
|
||||||
|
monthYearReceived: new Date(
|
||||||
|
offer.monthYearReceived.year,
|
||||||
|
offer.monthYearReceived.month - 1, // Convert month to monthIndex
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (profileId && token) {
|
||||||
|
createOrUpdateMutation.mutate({
|
||||||
|
background,
|
||||||
|
id: profileId,
|
||||||
|
offers,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createOrUpdateMutation.mutate({ background, offers });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
|
||||||
|
<div className="mb-20 flex justify-center">
|
||||||
|
<div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg">
|
||||||
|
<div className="mb-4 flex justify-end">
|
||||||
|
<Breadcrumbs currentStep={formStep} stepLabels={formStepsLabels} />
|
||||||
|
</div>
|
||||||
|
<FormProvider {...formMethods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{formSteps[formStep].component}
|
||||||
|
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
||||||
|
{formSteps[formStep].hasNext && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
disabled={false}
|
||||||
|
icon={ArrowRightIcon}
|
||||||
|
label="Next"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => nextStep(formStep)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{formStep === 1 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
icon={ArrowLeftIcon}
|
||||||
|
label="Previous"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={previousStep}
|
||||||
|
/>
|
||||||
|
<Button label="Submit" type="submit" variant="primary" />{' '}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,13 +1,12 @@
|
|||||||
import Error from 'next/error';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
|
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
|
||||||
|
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import OfferPercentileAnalysis from '../analysis/OfferPercentileAnalysis';
|
import OfferPercentileAnalysis from './OfferPercentileAnalysis';
|
||||||
import OfferProfileCard from '../analysis/OfferProfileCard';
|
import OfferProfileCard from './OfferProfileCard';
|
||||||
import { OVERALL_TAB } from '../constants';
|
import { OVERALL_TAB } from '../../constants';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Analysis,
|
Analysis,
|
||||||
@ -105,34 +104,32 @@ export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
analysis && (
|
||||||
{getAnalysisResult.isError && (
|
<div>
|
||||||
<Error
|
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||||
statusCode={404}
|
Result
|
||||||
title="An error occurred while generating profile analysis."
|
</h5>
|
||||||
/>
|
{getAnalysisResult.isError && (
|
||||||
)}
|
<p className="m-10 text-center">
|
||||||
{!getAnalysisResult.isError && analysis && (
|
An error occurred while generating profile analysis.
|
||||||
<div>
|
</p>
|
||||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
)}
|
||||||
Result
|
{getAnalysisResult.isLoading && (
|
||||||
</h5>
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
{getAnalysisResult.isLoading ? (
|
)}
|
||||||
<Spinner className="m-10" display="block" size="lg" />
|
{!getAnalysisResult.isError && !getAnalysisResult.isLoading && (
|
||||||
) : (
|
<div>
|
||||||
<div>
|
<Tabs
|
||||||
<Tabs
|
label="Result Navigation"
|
||||||
label="Result Navigation"
|
tabs={tabOptions}
|
||||||
tabs={tabOptions}
|
value={tab}
|
||||||
value={tab}
|
onChange={setTab}
|
||||||
onChange={setTab}
|
/>
|
||||||
/>
|
<HorizontalDivider className="mb-5" />
|
||||||
<HorizontalDivider className="mb-5" />
|
<OfferAnalysisContent analysis={analysis} tab={tab} />
|
||||||
<OfferAnalysisContent analysis={analysis} tab={tab} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ import { UserCircleIcon } from '@heroicons/react/24/outline';
|
|||||||
import { HorizontalDivider } from '~/../../../packages/ui/dist';
|
import { HorizontalDivider } from '~/../../../packages/ui/dist';
|
||||||
import { formatDate } from '~/utils/offers/time';
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
|
||||||
import { JobType } from '../types';
|
import { JobType } from '../../types';
|
||||||
|
|
||||||
import type { AnalysisOffer } from '~/types/offers';
|
import type { AnalysisOffer } from '~/types/offers';
|
||||||
|
|
@ -15,9 +15,9 @@ import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
|||||||
|
|
||||||
import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum';
|
import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum';
|
||||||
|
|
||||||
import FormRadioList from '../forms/FormRadioList';
|
import FormRadioList from '../../forms/FormRadioList';
|
||||||
import FormSelect from '../forms/FormSelect';
|
import FormSelect from '../../forms/FormSelect';
|
||||||
import FormTextInput from '../forms/FormTextInput';
|
import FormTextInput from '../../forms/FormTextInput';
|
||||||
|
|
||||||
function YoeSection() {
|
function YoeSection() {
|
||||||
const { register, formState } = useFormContext<{
|
const { register, formState } = useFormContext<{
|
@ -16,8 +16,7 @@ import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
|||||||
import {
|
import {
|
||||||
defaultFullTimeOfferValues,
|
defaultFullTimeOfferValues,
|
||||||
defaultInternshipOfferValues,
|
defaultInternshipOfferValues,
|
||||||
} from '~/pages/offers/submit';
|
} from '../OffersSubmissionForm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
emptyOption,
|
emptyOption,
|
||||||
FieldError,
|
FieldError,
|
||||||
@ -25,15 +24,15 @@ import {
|
|||||||
locationOptions,
|
locationOptions,
|
||||||
titleOptions,
|
titleOptions,
|
||||||
yearOptions,
|
yearOptions,
|
||||||
} from '../constants';
|
} from '../../constants';
|
||||||
import FormMonthYearPicker from '../forms/FormMonthYearPicker';
|
import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
|
||||||
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 type { OfferFormData } from '../types';
|
import type { OfferFormData } from '../../types';
|
||||||
import { JobTypeLabel } from '../types';
|
import { JobTypeLabel } from '../../types';
|
||||||
import { JobType } from '../types';
|
import { JobType } from '../../types';
|
||||||
import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum';
|
import { CURRENCY_OPTIONS } from '../../../../utils/offers/currency/CurrencyEnum';
|
||||||
|
|
||||||
type FullTimeOfferDetailsFormProps = Readonly<{
|
type FullTimeOfferDetailsFormProps = Readonly<{
|
||||||
index: number;
|
index: number;
|
@ -5,6 +5,7 @@ import { Button, HorizontalDivider, Spinner, TextArea } from '@tih/ui';
|
|||||||
|
|
||||||
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
|
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
|
||||||
|
|
||||||
|
import { copyProfileLink } from '~/utils/offers/link';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import type { OffersDiscussion, Reply } from '~/types/offers';
|
import type { OffersDiscussion, Reply } from '~/types/offers';
|
||||||
@ -84,19 +85,6 @@ export default function ProfileComments({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopyEditLink() {
|
|
||||||
// TODO: Add notification
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${window.location.origin}/offers/profile/${profileId}?token=${token}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCopyPublicLink() {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${window.location.origin}/offers/profile/${profileId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-10 pt-4">
|
<div className="col-span-10 pt-4">
|
||||||
@ -116,7 +104,7 @@ export default function ProfileComments({
|
|||||||
label="Copy profile edit link"
|
label="Copy profile edit link"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleCopyEditLink}
|
onClick={() => copyProfileLink(profileId, token)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@ -127,7 +115,7 @@ export default function ProfileComments({
|
|||||||
label="Copy public link"
|
label="Copy public link"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleCopyPublicLink}
|
onClick={() => copyProfileLink(profileId)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
|
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
|
||||||
|
@ -27,10 +27,10 @@ export default function ProfileDetails({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (selectedTab === 'offers') {
|
if (selectedTab === 'offers') {
|
||||||
if (offers && offers.length !== 0) {
|
if (offers.length !== 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{[...offers].map((offer) => (
|
{offers.map((offer) => (
|
||||||
<OfferCard key={offer.id} offer={offer} />
|
<OfferCard key={offer.id} offer={offer} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BookmarkSquareIcon,
|
BookmarkSquareIcon,
|
||||||
@ -11,6 +12,8 @@ import { Button, Dialog, Spinner, Tabs } from '@tih/ui';
|
|||||||
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
||||||
import type { BackgroundCard } from '~/components/offers/types';
|
import type { BackgroundCard } from '~/components/offers/types';
|
||||||
|
|
||||||
|
import { getProfileEditPath } from '~/utils/offers/link';
|
||||||
|
|
||||||
type ProfileHeaderProps = Readonly<{
|
type ProfileHeaderProps = Readonly<{
|
||||||
background?: BackgroundCard;
|
background?: BackgroundCard;
|
||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
@ -29,6 +32,12 @@ export default function ProfileHeader({
|
|||||||
setSelectedTab,
|
setSelectedTab,
|
||||||
}: ProfileHeaderProps) {
|
}: ProfileHeaderProps) {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const { offerProfileId = '', token = '' } = router.query;
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
router.push(getProfileEditPath(offerProfileId as string, token as string));
|
||||||
|
};
|
||||||
|
|
||||||
function renderActionList() {
|
function renderActionList() {
|
||||||
return (
|
return (
|
||||||
@ -48,6 +57,7 @@ export default function ProfileHeader({
|
|||||||
label="Edit"
|
label="Edit"
|
||||||
size="md"
|
size="md"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
|
onClick={handleEditClick}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -119,9 +129,11 @@ export default function ProfileHeader({
|
|||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<BuildingOffice2Icon className="mr-2.5 h-5" />
|
<BuildingOffice2Icon className="mr-2.5 h-5" />
|
||||||
<span className="mr-2 font-bold">Current:</span>
|
<span className="mr-2 font-bold">Current:</span>
|
||||||
<span>{`${background?.experiences[0].companyName ?? '-'} ${
|
<span>
|
||||||
background?.experiences[0].jobLevel
|
{`${background?.experiences[0]?.companyName ?? '-'} ${
|
||||||
} ${background?.experiences[0].jobTitle}`}</span>
|
background?.experiences[0]?.jobLevel || ''
|
||||||
|
} ${background?.experiences[0]?.jobTitle || ''}`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<CalendarDaysIcon className="mr-2.5 h-5" />
|
<CalendarDaysIcon className="mr-2.5 h-5" />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { convertCurrencyToString } from '~/utils/offers/currency';
|
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||||
import { formatDate } from '~/utils/offers/time';
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
|
||||||
import type { DashboardOffer } from '~/types/offers';
|
import type { DashboardOffer } from '~/types/offers';
|
||||||
@ -21,7 +21,7 @@ export default function OfferTableRow({
|
|||||||
</th>
|
</th>
|
||||||
<td className="py-4 px-6">{title}</td>
|
<td className="py-4 px-6">{title}</td>
|
||||||
<td className="py-4 px-6">{totalYoe}</td>
|
<td className="py-4 px-6">{totalYoe}</td>
|
||||||
<td className="py-4 px-6">{convertCurrencyToString(income)}</td>
|
<td className="py-4 px-6">{convertMoneyToString(income)}</td>
|
||||||
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td>
|
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td>
|
||||||
<td className="space-x-4 py-4 px-6">
|
<td className="space-x-4 py-4 px-6">
|
||||||
<Link
|
<Link
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Select } from '@tih/ui';
|
import { Select } from '@tih/ui';
|
||||||
|
|
||||||
|
import { titleOptions } from '~/components/offers/constants';
|
||||||
import OffersTitle from '~/components/offers/OffersTitle';
|
import OffersTitle from '~/components/offers/OffersTitle';
|
||||||
import OffersTable from '~/components/offers/table/OffersTable';
|
import OffersTable from '~/components/offers/table/OffersTable';
|
||||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||||
@ -20,24 +21,7 @@ export default function OffersHomePage() {
|
|||||||
<Select
|
<Select
|
||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Select a job title"
|
label="Select a job title"
|
||||||
options={[
|
options={titleOptions}
|
||||||
{
|
|
||||||
label: 'Software Engineer',
|
|
||||||
value: 'Software Engineer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Frontend Engineer',
|
|
||||||
value: 'Frontend Engineer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Backend Engineer',
|
|
||||||
value: 'Backend Engineer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Full-stack Engineer',
|
|
||||||
value: 'Full-stack Engineer',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={jobTitleFilter}
|
value={jobTitleFilter}
|
||||||
onChange={setjobTitleFilter}
|
onChange={setjobTitleFilter}
|
||||||
/>
|
/>
|
||||||
|
@ -7,7 +7,8 @@ import ProfileDetails from '~/components/offers/profile/ProfileDetails';
|
|||||||
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
|
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
|
||||||
import type { BackgroundCard, OfferEntity } from '~/components/offers/types';
|
import type { BackgroundCard, OfferEntity } from '~/components/offers/types';
|
||||||
|
|
||||||
import { convertCurrencyToString } from '~/utils/offers/currency';
|
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||||
|
import { getProfilePath } from '~/utils/offers/link';
|
||||||
import { formatDate } from '~/utils/offers/time';
|
import { formatDate } from '~/utils/offers/time';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ export default function OfferProfile() {
|
|||||||
}
|
}
|
||||||
// If the profile is not editable with a wrong token, redirect to the profile page
|
// If the profile is not editable with a wrong token, redirect to the profile page
|
||||||
if (!data?.isEditable && token !== '') {
|
if (!data?.isEditable && token !== '') {
|
||||||
router.push(`/offers/profile/${offerProfileId}`);
|
router.push(getProfilePath(offerProfileId as string));
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsEditable(data?.isEditable ?? false);
|
setIsEditable(data?.isEditable ?? false);
|
||||||
@ -48,10 +49,8 @@ export default function OfferProfile() {
|
|||||||
? data?.offers.map((res: ProfileOffer) => {
|
? data?.offers.map((res: ProfileOffer) => {
|
||||||
if (res.offersFullTime) {
|
if (res.offersFullTime) {
|
||||||
const filteredOffer: OfferEntity = {
|
const filteredOffer: OfferEntity = {
|
||||||
base: convertCurrencyToString(
|
base: convertMoneyToString(res.offersFullTime.baseSalary),
|
||||||
res.offersFullTime.baseSalary,
|
bonus: convertMoneyToString(res.offersFullTime.bonus),
|
||||||
),
|
|
||||||
bonus: convertCurrencyToString(res.offersFullTime.bonus),
|
|
||||||
companyName: res.company.name,
|
companyName: res.company.name,
|
||||||
id: res.offersFullTime.id,
|
id: res.offersFullTime.id,
|
||||||
jobLevel: res.offersFullTime.level,
|
jobLevel: res.offersFullTime.level,
|
||||||
@ -60,12 +59,11 @@ export default function OfferProfile() {
|
|||||||
negotiationStrategy: res.negotiationStrategy || '',
|
negotiationStrategy: res.negotiationStrategy || '',
|
||||||
otherComment: res.comments || '',
|
otherComment: res.comments || '',
|
||||||
receivedMonth: formatDate(res.monthYearReceived),
|
receivedMonth: formatDate(res.monthYearReceived),
|
||||||
stocks: convertCurrencyToString(res.offersFullTime.stocks),
|
stocks: convertMoneyToString(res.offersFullTime.stocks),
|
||||||
totalCompensation: convertCurrencyToString(
|
totalCompensation: convertMoneyToString(
|
||||||
res.offersFullTime.totalCompensation,
|
res.offersFullTime.totalCompensation,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return filteredOffer;
|
return filteredOffer;
|
||||||
}
|
}
|
||||||
const filteredOffer: OfferEntity = {
|
const filteredOffer: OfferEntity = {
|
||||||
@ -73,7 +71,7 @@ export default function OfferProfile() {
|
|||||||
id: res.offersIntern!.id,
|
id: res.offersIntern!.id,
|
||||||
jobTitle: res.offersIntern!.title,
|
jobTitle: res.offersIntern!.title,
|
||||||
location: res.location,
|
location: res.location,
|
||||||
monthlySalary: convertCurrencyToString(
|
monthlySalary: convertMoneyToString(
|
||||||
res.offersIntern!.monthlySalary,
|
res.offersIntern!.monthlySalary,
|
||||||
),
|
),
|
||||||
negotiationStrategy: res.negotiationStrategy || '',
|
negotiationStrategy: res.negotiationStrategy || '',
|
||||||
@ -88,46 +86,29 @@ export default function OfferProfile() {
|
|||||||
|
|
||||||
if (data?.background) {
|
if (data?.background) {
|
||||||
const transformedBackground = {
|
const transformedBackground = {
|
||||||
educations: [
|
educations: data.background.educations.map((education) => ({
|
||||||
{
|
endDate: education.endDate ? formatDate(education.endDate) : '-',
|
||||||
endDate: data?.background.educations[0].endDate
|
field: education.field || '-',
|
||||||
? formatDate(data.background.educations[0].endDate)
|
school: education.school || '-',
|
||||||
: '-',
|
startDate: education.startDate
|
||||||
field: data.background.educations[0].field || '-',
|
? formatDate(education.startDate)
|
||||||
school: data.background.educations[0].school || '-',
|
: '-',
|
||||||
startDate: data.background.educations[0].startDate
|
type: education.type || '-',
|
||||||
? formatDate(data.background.educations[0].startDate)
|
})),
|
||||||
: '-',
|
experiences: data.background.experiences.map((experience) => ({
|
||||||
type: data.background.educations[0].type || '-',
|
companyName: experience.company?.name ?? '-',
|
||||||
},
|
duration: String(experience.durationInMonths) ?? '-',
|
||||||
],
|
jobLevel: experience.level ?? '',
|
||||||
experiences: [
|
jobTitle: experience.title ?? '-',
|
||||||
data.background.experiences &&
|
monthlySalary: experience.monthlySalary
|
||||||
data.background.experiences.length > 0
|
? convertMoneyToString(experience.monthlySalary)
|
||||||
? {
|
: '-',
|
||||||
companyName:
|
totalCompensation: experience.totalCompensation
|
||||||
data.background.experiences[0].company?.name ?? '-',
|
? convertMoneyToString(experience.totalCompensation)
|
||||||
duration:
|
: '-',
|
||||||
String(data.background.experiences[0].durationInMonths) ??
|
})),
|
||||||
'-',
|
|
||||||
jobLevel: data.background.experiences[0].level ?? '',
|
|
||||||
jobTitle: data.background.experiences[0].title ?? '-',
|
|
||||||
monthlySalary: data.background.experiences[0].monthlySalary
|
|
||||||
? convertCurrencyToString(
|
|
||||||
data.background.experiences[0].monthlySalary,
|
|
||||||
)
|
|
||||||
: '-',
|
|
||||||
totalCompensation: data.background.experiences[0]
|
|
||||||
.totalCompensation
|
|
||||||
? convertCurrencyToString(
|
|
||||||
data.background.experiences[0].totalCompensation,
|
|
||||||
)
|
|
||||||
: '-',
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
],
|
|
||||||
profileName: data.profileName,
|
profileName: data.profileName,
|
||||||
specificYoes: data.background.specificYoes ?? [],
|
specificYoes: data.background.specificYoes,
|
||||||
totalYoe: String(data.background.totalYoe) || '-',
|
totalYoe: String(data.background.totalYoe) || '-',
|
||||||
};
|
};
|
||||||
setBackground(transformedBackground);
|
setBackground(transformedBackground);
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
|
||||||
|
import type { OffersProfileFormData } from '~/components/offers/types';
|
||||||
|
import { JobType } from '~/components/offers/types';
|
||||||
|
|
||||||
|
import { Spinner } from '~/../../../packages/ui/dist';
|
||||||
|
import { getProfilePath } from '~/utils/offers/link';
|
||||||
|
import { convertToMonthYear } from '~/utils/offers/time';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
export default function OffersEditPage() {
|
||||||
|
const [initialData, setInitialData] = useState<OffersProfileFormData>();
|
||||||
|
const router = useRouter();
|
||||||
|
const { offerProfileId, token = '' } = router.query;
|
||||||
|
|
||||||
|
const getProfileResult = trpc.useQuery(
|
||||||
|
[
|
||||||
|
'offers.profile.listOne',
|
||||||
|
{ profileId: offerProfileId as string, token: token as string },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onError(error) {
|
||||||
|
console.error(error.message);
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
const { educations, experiences, specificYoes, totalYoe } =
|
||||||
|
data.background!;
|
||||||
|
|
||||||
|
setInitialData({
|
||||||
|
background: {
|
||||||
|
educations,
|
||||||
|
experiences:
|
||||||
|
experiences.length === 0
|
||||||
|
? [{ jobType: JobType.FullTime }]
|
||||||
|
: experiences,
|
||||||
|
specificYoes,
|
||||||
|
totalYoe,
|
||||||
|
},
|
||||||
|
offers: data.offers.map((offer) => ({
|
||||||
|
comments: offer.comments,
|
||||||
|
companyId: offer.company.id,
|
||||||
|
id: offer.id,
|
||||||
|
jobType: offer.jobType,
|
||||||
|
location: offer.location,
|
||||||
|
monthYearReceived: convertToMonthYear(offer.monthYearReceived),
|
||||||
|
negotiationStrategy: offer.negotiationStrategy,
|
||||||
|
offersFullTime: offer.offersFullTime,
|
||||||
|
offersIntern: offer.offersIntern,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const profile = getProfileResult.data;
|
||||||
|
|
||||||
|
if (profile && !profile.isEditable) {
|
||||||
|
router.push(getProfilePath(profile.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{getProfileResult.isLoading && (
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!getProfileResult.isLoading && (
|
||||||
|
<OffersSubmissionForm
|
||||||
|
initialOfferProfileValues={initialData}
|
||||||
|
profileId={profile?.id}
|
||||||
|
token={profile?.editToken || undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,215 +1,5 @@
|
|||||||
import { useRef, useState } from 'react';
|
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
|
||||||
import { Button } from '@tih/ui';
|
|
||||||
|
|
||||||
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
|
|
||||||
import BackgroundForm from '~/components/offers/offers-submission/BackgroundForm';
|
|
||||||
import OfferAnalysis from '~/components/offers/offers-submission/OfferAnalysis';
|
|
||||||
import OfferDetailsForm from '~/components/offers/offers-submission/OfferDetailsForm';
|
|
||||||
import OfferProfileSave from '~/components/offers/offers-submission/OfferProfileSave';
|
|
||||||
import type {
|
|
||||||
OfferFormData,
|
|
||||||
OffersProfileFormData,
|
|
||||||
} from '~/components/offers/types';
|
|
||||||
import { JobType } from '~/components/offers/types';
|
|
||||||
import type { Month } from '~/components/shared/MonthYearPicker';
|
|
||||||
|
|
||||||
import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
|
|
||||||
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
|
|
||||||
import { trpc } from '~/utils/trpc';
|
|
||||||
|
|
||||||
import type { CreateOfferProfileResponse } from '~/types/offers';
|
|
||||||
|
|
||||||
const defaultOfferValues = {
|
|
||||||
comments: '',
|
|
||||||
companyId: '',
|
|
||||||
jobType: JobType.FullTime,
|
|
||||||
location: '',
|
|
||||||
monthYearReceived: {
|
|
||||||
month: getCurrentMonth() as Month,
|
|
||||||
year: getCurrentYear(),
|
|
||||||
},
|
|
||||||
negotiationStrategy: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultFullTimeOfferValues = {
|
|
||||||
...defaultOfferValues,
|
|
||||||
jobType: JobType.FullTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultInternshipOfferValues = {
|
|
||||||
...defaultOfferValues,
|
|
||||||
jobType: JobType.Intern,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultOfferProfileValues = {
|
|
||||||
background: {
|
|
||||||
educations: [],
|
|
||||||
experiences: [{ jobType: JobType.FullTime }],
|
|
||||||
specificYoes: [],
|
|
||||||
},
|
|
||||||
offers: [defaultOfferValues],
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormStep = {
|
|
||||||
component: JSX.Element;
|
|
||||||
hasNext: boolean;
|
|
||||||
hasPrevious: boolean;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OffersSubmissionPage() {
|
export default function OffersSubmissionPage() {
|
||||||
const [formStep, setFormStep] = useState(0);
|
return <OffersSubmissionForm />;
|
||||||
const [createProfileResponse, setCreateProfileResponse] =
|
|
||||||
useState<CreateOfferProfileResponse>();
|
|
||||||
|
|
||||||
const pageRef = useRef<HTMLDivElement>(null);
|
|
||||||
const scrollToTop = () =>
|
|
||||||
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
|
|
||||||
const formMethods = useForm<OffersProfileFormData>({
|
|
||||||
defaultValues: defaultOfferProfileValues,
|
|
||||||
mode: 'all',
|
|
||||||
});
|
|
||||||
const { handleSubmit, trigger } = formMethods;
|
|
||||||
|
|
||||||
const formSteps: Array<FormStep> = [
|
|
||||||
{
|
|
||||||
component: <OfferDetailsForm key={0} />,
|
|
||||||
hasNext: true,
|
|
||||||
hasPrevious: false,
|
|
||||||
label: 'Offer details',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: <BackgroundForm key={1} />,
|
|
||||||
hasNext: false,
|
|
||||||
hasPrevious: true,
|
|
||||||
label: 'Background',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: (
|
|
||||||
<OfferAnalysis key={2} profileId={createProfileResponse?.id} />
|
|
||||||
),
|
|
||||||
hasNext: true,
|
|
||||||
hasPrevious: false,
|
|
||||||
label: 'Analysis',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: <OfferProfileSave key={3} />,
|
|
||||||
hasNext: false,
|
|
||||||
hasPrevious: false,
|
|
||||||
label: 'Save',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const formStepsLabels = formSteps.map((step) => step.label);
|
|
||||||
|
|
||||||
const nextStep = async (currStep: number) => {
|
|
||||||
if (currStep === 0) {
|
|
||||||
const result = await trigger('offers');
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFormStep(formStep + 1);
|
|
||||||
scrollToTop();
|
|
||||||
};
|
|
||||||
|
|
||||||
const previousStep = () => {
|
|
||||||
setFormStep(formStep - 1);
|
|
||||||
scrollToTop();
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateAnalysisMutation = trpc.useMutation(
|
|
||||||
['offers.analysis.generate'],
|
|
||||||
{
|
|
||||||
onError(error) {
|
|
||||||
console.error(error.message);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const createMutation = trpc.useMutation(['offers.profile.create'], {
|
|
||||||
onError(error) {
|
|
||||||
console.error(error.message);
|
|
||||||
},
|
|
||||||
onSuccess(data) {
|
|
||||||
generateAnalysisMutation.mutate({
|
|
||||||
profileId: data?.id || '',
|
|
||||||
});
|
|
||||||
setCreateProfileResponse(data);
|
|
||||||
setFormStep(formStep + 1);
|
|
||||||
scrollToTop();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<OffersProfileFormData> = async (data) => {
|
|
||||||
const result = await trigger();
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
data = removeInvalidMoneyData(data);
|
|
||||||
|
|
||||||
const background = cleanObject(data.background);
|
|
||||||
background.specificYoes = data.background.specificYoes.filter(
|
|
||||||
(specificYoe) => specificYoe.domain && specificYoe.yoe > 0,
|
|
||||||
);
|
|
||||||
if (Object.entries(background.experiences[0]).length === 1) {
|
|
||||||
background.experiences = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const offers = data.offers.map((offer: OfferFormData) => ({
|
|
||||||
...offer,
|
|
||||||
monthYearReceived: new Date(
|
|
||||||
offer.monthYearReceived.year,
|
|
||||||
offer.monthYearReceived.month,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const postData = { background, offers };
|
|
||||||
|
|
||||||
createMutation.mutate(postData);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
|
|
||||||
<div className="mb-20 flex justify-center">
|
|
||||||
<div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg">
|
|
||||||
<div className="mb-4 flex justify-end">
|
|
||||||
<Breadcrumbs currentStep={formStep} stepLabels={formStepsLabels} />
|
|
||||||
</div>
|
|
||||||
<FormProvider {...formMethods}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
{formSteps[formStep].component}
|
|
||||||
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
|
||||||
{formSteps[formStep].hasNext && (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button
|
|
||||||
disabled={false}
|
|
||||||
icon={ArrowRightIcon}
|
|
||||||
label="Next"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => nextStep(formStep)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{formStep === 1 && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Button
|
|
||||||
icon={ArrowLeftIcon}
|
|
||||||
label="Previous"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={previousStep}
|
|
||||||
/>
|
|
||||||
<Button label="Submit" type="submit" variant="primary" />{' '}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ export const offersAnalysisRouter = createRouter()
|
|||||||
// TODO: Shift yoe out of background to make it mandatory
|
// TODO: Shift yoe out of background to make it mandatory
|
||||||
if (
|
if (
|
||||||
!overallHighestOffer.profile.background ||
|
!overallHighestOffer.profile.background ||
|
||||||
!overallHighestOffer.profile.background.totalYoe
|
overallHighestOffer.profile.background.totalYoe === undefined
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Money } from '~/components/offers/types';
|
import type { Money } from '~/components/offers/types';
|
||||||
|
|
||||||
export function convertCurrencyToString({ currency, value }: Money) {
|
export function convertMoneyToString({ currency, value }: Money) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
19
apps/portal/src/utils/offers/link.tsx
Normal file
19
apps/portal/src/utils/offers/link.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export function getProfileLink(profileId: string, token?: string) {
|
||||||
|
return `${window.location.origin}${getProfilePath(profileId, token)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyProfileLink(profileId: string, token?: string) {
|
||||||
|
// TODO: Add notification
|
||||||
|
navigator.clipboard.writeText(getProfileLink(profileId, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProfilePath(profileId: string, token?: string) {
|
||||||
|
if (token) {
|
||||||
|
return `/offers/profile/${profileId}?token=${token}`;
|
||||||
|
}
|
||||||
|
return `/offers/profile/${profileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProfileEditPath(profileId: string, token: string) {
|
||||||
|
return `/offers/profile/edit/${profileId}?token=${token}`;
|
||||||
|
}
|
@ -32,22 +32,20 @@ export function timeSinceNow(date: Date | number | string) {
|
|||||||
|
|
||||||
export function formatDate(value: Date | number | string) {
|
export function formatDate(value: Date | number | string) {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
// Const day = date.toLocaleString('default', { day: '2-digit' });
|
|
||||||
const month = date.toLocaleString('default', { month: 'short' });
|
const month = date.toLocaleString('default', { month: 'short' });
|
||||||
const year = date.toLocaleString('default', { year: 'numeric' });
|
const year = date.toLocaleString('default', { year: 'numeric' });
|
||||||
return `${month} ${year}`;
|
return `${month} ${year}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMonthYear({ month, year }: MonthYear) {
|
|
||||||
const monthString = month < 10 ? month.toString() : `0${month}`;
|
|
||||||
const yearString = year.toString();
|
|
||||||
return `${monthString}/${yearString}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentMonth() {
|
export function getCurrentMonth() {
|
||||||
return getMonth(Date.now());
|
// `getMonth` returns a zero-based month index
|
||||||
|
return getMonth(Date.now()) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentYear() {
|
export function getCurrentYear() {
|
||||||
return getYear(Date.now());
|
return getYear(Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertToMonthYear(date: Date) {
|
||||||
|
return { month: date.getMonth() + 1, year: date.getFullYear() } as MonthYear;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user