[offers][feat] Split offers submission and result pages ()

pull/440/head
Ai Ling 2 years ago committed by GitHub
parent 4b835db8a2
commit 4bbf2b42b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,21 +1,43 @@
export type BreadcrumbStep = {
label: string;
step?: number;
};
type BreadcrumbsProps = Readonly<{ type BreadcrumbsProps = Readonly<{
currentStep: number; currentStep: number;
stepLabels: Array<string>; setStep: (nextStep: number) => void;
steps: Array<BreadcrumbStep>;
}>; }>;
export function Breadcrumbs({ stepLabels, currentStep }: BreadcrumbsProps) { function getPrimaryText(text: string) {
return <p className="text-primary-700 text-sm">{text}</p>;
}
function getSlateText(text: string) {
return <p className="text-sm text-slate-400">{text}</p>;
}
function getTextWithLink(text: string, onClickHandler: () => void) {
return (
<p
className="hover:text-primary-700 cursor-pointer text-sm text-slate-400 hover:underline hover:underline-offset-2"
onClick={onClickHandler}>
{text}
</p>
);
}
export function Breadcrumbs({ steps, currentStep, setStep }: BreadcrumbsProps) {
return ( return (
<div className="flex space-x-1"> <div className="flex space-x-1">
{stepLabels.map((label, index) => ( {steps.map(({ label, step }, index) => (
<div key={label} className="flex space-x-1"> <div key={label} className="flex space-x-1">
{index === currentStep ? ( {step === currentStep
<p className="text-primary-700 text-sm">{label}</p> ? getPrimaryText(label)
) : ( : step !== undefined
<p className="text-sm text-slate-400">{label}</p> ? getTextWithLink(label, () => setStep(step))
)} : getSlateText(label)}
{index !== stepLabels.length - 1 && ( {index !== steps.length - 1 && getSlateText('>')}
<p className="text-sm text-slate-400">{'>'}</p>
)}
</div> </div>
))} ))}
</div> </div>

@ -1,32 +1,19 @@
import { useRouter } from 'next/router';
import { EyeIcon } from '@heroicons/react/24/outline';
import { Button } from '~/../../../packages/ui/dist';
import { getProfilePath } from '~/utils/offers/link';
import OfferAnalysis from '../offerAnalysis/OfferAnalysis'; import OfferAnalysis from '../offerAnalysis/OfferAnalysis';
import type { ProfileAnalysis } from '~/types/offers'; import type { ProfileAnalysis } from '~/types/offers';
type Props = Readonly<{ type Props = Readonly<{
analysis?: ProfileAnalysis | null; analysis?: ProfileAnalysis | null;
isError: boolean; isError: boolean;
isLoading: boolean; isLoading: boolean;
profileId?: string;
token?: string;
}>; }>;
export default function OffersSubmissionAnalysis({ export default function OffersSubmissionAnalysis({
analysis, analysis,
isError, isError,
isLoading, isLoading,
profileId = '',
token = '',
}: Props) { }: Props) {
const router = useRouter();
return ( return (
<div> <div className="mb-8">
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900"> <h5 className="mb-8 text-center text-4xl font-bold text-slate-900">
Result Result
</h5> </h5>
@ -36,14 +23,6 @@ export default function OffersSubmissionAnalysis({
isError={isError} isError={isError}
isLoading={isLoading} isLoading={isLoading}
/> />
<div className="mt-8 text-center">
<Button
icon={EyeIcon}
label="View your profile"
variant="special"
onClick={() => router.push(getProfilePath(profileId, token))}
/>
</div>
</div> </div>
); );
} }

@ -1,12 +1,13 @@
import { useRef, useState } from 'react'; import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
import type { SubmitHandler } from 'react-hook-form'; 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 } from '@tih/ui';
import type { BreadcrumbStep } from '~/components/offers/Breadcrumb';
import { Breadcrumbs } from '~/components/offers/Breadcrumb'; import { Breadcrumbs } from '~/components/offers/Breadcrumb';
import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave';
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 {
@ -23,10 +24,6 @@ import {
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time'; import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import OffersSubmissionAnalysis from './OffersSubmissionAnalysis';
import type { ProfileAnalysis } from '~/types/offers';
const defaultOfferValues = { const defaultOfferValues = {
comments: '', comments: '',
companyId: '', companyId: '',
@ -59,13 +56,6 @@ const defaultOfferProfileValues = {
offers: [defaultOfferValues], offers: [defaultOfferValues],
}; };
type FormStep = {
component: JSX.Element;
hasNext: boolean;
hasPrevious: boolean;
label: string;
};
type Props = Readonly<{ type Props = Readonly<{
initialOfferProfileValues?: OffersProfileFormData; initialOfferProfileValues?: OffersProfileFormData;
profileId?: string; profileId?: string;
@ -77,11 +67,14 @@ export default function OffersSubmissionForm({
profileId: editProfileId = '', profileId: editProfileId = '',
token: editToken = '', token: editToken = '',
}: Props) { }: Props) {
const [formStep, setFormStep] = useState(0); const [step, setStep] = useState(0);
const [profileId, setProfileId] = useState(editProfileId); const [params, setParams] = useState({
const [token, setToken] = useState(editToken); profileId: editProfileId,
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null); token: editToken,
});
const [isSubmitted, setIsSubmitted] = useState(false);
const router = useRouter();
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 });
@ -97,87 +90,61 @@ export default function OffersSubmissionForm({
onError(error) { onError(error) {
console.error(error.message); console.error(error.message);
}, },
onSuccess(data) { onSuccess() {
setAnalysis(data); router.push(
`/offers/submit/result/${params.profileId}?token=${params.token}`,
);
}, },
}, },
); );
const formSteps: Array<FormStep> = [ const steps = [
<OfferDetailsForm
key={0}
defaultJobType={initialOfferProfileValues.offers[0].jobType}
/>,
<BackgroundForm key={1} />,
];
const breadcrumbSteps: Array<BreadcrumbStep> = [
{ {
component: (
<OfferDetailsForm
key={0}
defaultJobType={initialOfferProfileValues.offers[0].jobType}
/>
),
hasNext: true,
hasPrevious: false,
label: 'Offers', label: 'Offers',
step: 0,
}, },
{ {
component: <BackgroundForm key={1} />,
hasNext: false,
hasPrevious: true,
label: 'Background', label: 'Background',
step: 1,
}, },
{ {
component: (
<OffersProfileSave key={2} profileId={profileId} token={token} />
),
hasNext: true,
hasPrevious: false,
label: 'Save profile', label: 'Save profile',
}, },
{ {
component: (
<OffersSubmissionAnalysis
analysis={analysis}
isError={generateAnalysisMutation.isError}
isLoading={generateAnalysisMutation.isLoading}
profileId={profileId}
token={token}
/>
),
hasNext: false,
hasPrevious: true,
label: 'Analysis', label: 'Analysis',
}, },
]; ];
const formStepsLabels = formSteps.map((step) => step.label); const goToNextStep = async (currStep: number) => {
const nextStep = async (currStep: number) => {
if (currStep === 0) { if (currStep === 0) {
const result = await trigger('offers'); const result = await trigger('offers');
if (!result) { if (!result) {
return; return;
} }
} }
setFormStep(formStep + 1); setStep(step + 1);
scrollToTop();
};
const previousStep = () => {
setFormStep(formStep - 1);
scrollToTop();
}; };
const mutationpath = const mutationpath =
profileId && token ? 'offers.profile.update' : 'offers.profile.create'; editProfileId && editToken
? 'offers.profile.update'
: 'offers.profile.create';
const createOrUpdateMutation = trpc.useMutation([mutationpath], { const createOrUpdateMutation = trpc.useMutation([mutationpath], {
onError(error) { onError(error) {
console.error(error.message); console.error(error.message);
}, },
onSuccess(data) { onSuccess(data) {
generateAnalysisMutation.mutate({ setParams({ profileId: data.id, token: data.token });
profileId: data?.id || '', setIsSubmitted(true);
});
setProfileId(data.id);
setToken(data.token);
setFormStep(formStep + 1);
scrollToTop();
}, },
}); });
@ -206,47 +173,64 @@ export default function OffersSubmissionForm({
), ),
})); }));
if (profileId && token) { if (params.profileId && params.token) {
createOrUpdateMutation.mutate({ createOrUpdateMutation.mutate({
background, background,
id: profileId, id: params.profileId,
offers, offers,
token, token: params.token,
}); });
} else { } else {
createOrUpdateMutation.mutate({ background, offers }); createOrUpdateMutation.mutate({ background, offers });
} }
}; };
useEffect(() => {
if (isSubmitted) {
generateAnalysisMutation.mutate({
profileId: params.profileId,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitted, params]);
useEffect(() => {
scrollToTop();
}, [step]);
return ( return (
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll"> <div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
<div className="mb-20 flex justify-center"> <div className="mb-20 flex justify-center">
<div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg"> <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"> <div className="mb-4 flex justify-end">
<Breadcrumbs currentStep={formStep} stepLabels={formStepsLabels} /> <Breadcrumbs
currentStep={step}
setStep={setStep}
steps={breadcrumbSteps}
/>
</div> </div>
<FormProvider {...formMethods}> <FormProvider {...formMethods}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
{formSteps[formStep].component} {steps[step]}
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */} {/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
{formSteps[formStep].hasNext && ( {step === 0 && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
disabled={false} disabled={false}
icon={ArrowRightIcon} icon={ArrowRightIcon}
label="Next" label="Next"
variant="secondary" variant="secondary"
onClick={() => nextStep(formStep)} onClick={() => goToNextStep(step)}
/> />
</div> </div>
)} )}
{formStep === 1 && ( {step === 1 && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button <Button
icon={ArrowLeftIcon} icon={ArrowLeftIcon}
label="Previous" label="Previous"
variant="secondary" variant="secondary"
onClick={previousStep} onClick={() => setStep(step - 1)}
/> />
<Button label="Submit" type="submit" variant="primary" />{' '} <Button label="Submit" type="submit" variant="primary" />{' '}
</div> </div>

@ -50,11 +50,11 @@ export default function OfferProfile() {
router.push(HOME_URL); router.push(HOME_URL);
} }
// 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(getProfilePath(offerProfileId as string)); router.push(getProfilePath(offerProfileId as string));
} }
setIsEditable(data?.isEditable ?? false); setIsEditable(data.isEditable);
const filteredOffers: Array<OfferDisplayData> = data const filteredOffers: Array<OfferDisplayData> = data
? data?.offers.map((res: ProfileOffer) => { ? data?.offers.map((res: ProfileOffer) => {

@ -0,0 +1,5 @@
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
export default function OffersSubmissionPage() {
return <OffersSubmissionForm />;
}

@ -0,0 +1,123 @@
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { EyeIcon } from '@heroicons/react/24/outline';
import { Button, Spinner } from '@tih/ui';
import type { BreadcrumbStep } from '~/components/offers/Breadcrumb';
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave';
import OffersSubmissionAnalysis from '~/components/offers/offersSubmission/OffersSubmissionAnalysis';
import { getProfilePath } from '~/utils/offers/link';
import { trpc } from '~/utils/trpc';
import type { ProfileAnalysis } from '~/types/offers';
export default function OffersSubmissionResult() {
const router = useRouter();
let { offerProfileId, token = '' } = router.query;
offerProfileId = offerProfileId as string;
token = token as string;
const [step, setStep] = useState(0);
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
// TODO: Check if the token is valid before showing this page
const getAnalysis = trpc.useQuery(
['offers.analysis.get', { profileId: offerProfileId }],
{
onSuccess(data) {
setAnalysis(data);
},
},
);
const steps = [
<OffersProfileSave key={0} profileId={offerProfileId} token={token} />,
<OffersSubmissionAnalysis
key={1}
analysis={analysis}
isError={getAnalysis.isError}
isLoading={getAnalysis.isLoading}
/>,
];
const breadcrumbSteps: Array<BreadcrumbStep> = [
{
label: 'Offers',
},
{
label: 'Background',
},
{
label: 'Save profile',
step: 0,
},
{
label: 'Analysis',
step: 1,
},
];
useEffect(() => {
scrollToTop();
}, [step]);
return (
<>
{getAnalysis.isLoading && (
<Spinner className="m-10" display="block" size="lg" />
)}
{!getAnalysis.isLoading && (
<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={step}
setStep={setStep}
steps={breadcrumbSteps}
/>
</div>
{steps[step]}
{step === 0 && (
<div className="flex justify-end">
<Button
disabled={false}
icon={ArrowRightIcon}
label="Next"
variant="secondary"
onClick={() => setStep(step + 1)}
/>
</div>
)}
{step === 1 && (
<div className="flex items-center justify-between">
<Button
icon={ArrowLeftIcon}
label="Previous"
variant="secondary"
onClick={() => setStep(step - 1)}
/>
<Button
href={getProfilePath(
offerProfileId as string,
token as string,
)}
icon={EyeIcon}
label="View your profile"
variant="primary"
/>
</div>
)}
</div>
</div>
</div>
)}
</>
);
}
Loading…
Cancel
Save