[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<{
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 (
<div className="flex space-x-1">
{stepLabels.map((label, index) => (
{steps.map(({ label, step }, index) => (
<div key={label} className="flex space-x-1">
{index === currentStep ? (
<p className="text-primary-700 text-sm">{label}</p>
) : (
<p className="text-sm text-slate-400">{label}</p>
)}
{index !== stepLabels.length - 1 && (
<p className="text-sm text-slate-400">{'>'}</p>
)}
{step === currentStep
? getPrimaryText(label)
: step !== undefined
? getTextWithLink(label, () => setStep(step))
: getSlateText(label)}
{index !== steps.length - 1 && getSlateText('>')}
</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 type { ProfileAnalysis } from '~/types/offers';
type Props = Readonly<{
analysis?: ProfileAnalysis | null;
isError: boolean;
isLoading: boolean;
profileId?: string;
token?: string;
}>;
export default function OffersSubmissionAnalysis({
analysis,
isError,
isLoading,
profileId = '',
token = '',
}: Props) {
const router = useRouter();
return (
<div>
<div className="mb-8">
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900">
Result
</h5>
@ -36,14 +23,6 @@ export default function OffersSubmissionAnalysis({
isError={isError}
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>
);
}

@ -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 { FormProvider, useForm } from 'react-hook-form';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { JobType } from '@prisma/client';
import { Button } from '@tih/ui';
import type { BreadcrumbStep } 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 OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
import type {
@ -23,10 +24,6 @@ import {
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
import OffersSubmissionAnalysis from './OffersSubmissionAnalysis';
import type { ProfileAnalysis } from '~/types/offers';
const defaultOfferValues = {
comments: '',
companyId: '',
@ -59,13 +56,6 @@ const defaultOfferProfileValues = {
offers: [defaultOfferValues],
};
type FormStep = {
component: JSX.Element;
hasNext: boolean;
hasPrevious: boolean;
label: string;
};
type Props = Readonly<{
initialOfferProfileValues?: OffersProfileFormData;
profileId?: string;
@ -77,11 +67,14 @@ export default function OffersSubmissionForm({
profileId: editProfileId = '',
token: editToken = '',
}: Props) {
const [formStep, setFormStep] = useState(0);
const [profileId, setProfileId] = useState(editProfileId);
const [token, setToken] = useState(editToken);
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
const [step, setStep] = useState(0);
const [params, setParams] = useState({
profileId: editProfileId,
token: editToken,
});
const [isSubmitted, setIsSubmitted] = useState(false);
const router = useRouter();
const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
@ -97,87 +90,61 @@ export default function OffersSubmissionForm({
onError(error) {
console.error(error.message);
},
onSuccess(data) {
setAnalysis(data);
onSuccess() {
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',
step: 0,
},
{
component: <BackgroundForm key={1} />,
hasNext: false,
hasPrevious: true,
label: 'Background',
step: 1,
},
{
component: (
<OffersProfileSave key={2} profileId={profileId} token={token} />
),
hasNext: true,
hasPrevious: false,
label: 'Save profile',
},
{
component: (
<OffersSubmissionAnalysis
analysis={analysis}
isError={generateAnalysisMutation.isError}
isLoading={generateAnalysisMutation.isLoading}
profileId={profileId}
token={token}
/>
),
hasNext: false,
hasPrevious: true,
label: 'Analysis',
},
];
const formStepsLabels = formSteps.map((step) => step.label);
const nextStep = async (currStep: number) => {
const goToNextStep = async (currStep: number) => {
if (currStep === 0) {
const result = await trigger('offers');
if (!result) {
return;
}
}
setFormStep(formStep + 1);
scrollToTop();
};
const previousStep = () => {
setFormStep(formStep - 1);
scrollToTop();
setStep(step + 1);
};
const mutationpath =
profileId && token ? 'offers.profile.update' : 'offers.profile.create';
editProfileId && editToken
? 'offers.profile.update'
: 'offers.profile.create';
const createOrUpdateMutation = trpc.useMutation([mutationpath], {
onError(error) {
console.error(error.message);
},
onSuccess(data) {
generateAnalysisMutation.mutate({
profileId: data?.id || '',
});
setProfileId(data.id);
setToken(data.token);
setFormStep(formStep + 1);
scrollToTop();
setParams({ profileId: data.id, token: data.token });
setIsSubmitted(true);
},
});
@ -206,47 +173,64 @@ export default function OffersSubmissionForm({
),
}));
if (profileId && token) {
if (params.profileId && params.token) {
createOrUpdateMutation.mutate({
background,
id: profileId,
id: params.profileId,
offers,
token,
token: params.token,
});
} else {
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 (
<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} />
<Breadcrumbs
currentStep={step}
setStep={setStep}
steps={breadcrumbSteps}
/>
</div>
<FormProvider {...formMethods}>
<form onSubmit={handleSubmit(onSubmit)}>
{formSteps[formStep].component}
{steps[step]}
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
{formSteps[formStep].hasNext && (
{step === 0 && (
<div className="flex justify-end">
<Button
disabled={false}
icon={ArrowRightIcon}
label="Next"
variant="secondary"
onClick={() => nextStep(formStep)}
onClick={() => goToNextStep(step)}
/>
</div>
)}
{formStep === 1 && (
{step === 1 && (
<div className="flex items-center justify-between">
<Button
icon={ArrowLeftIcon}
label="Previous"
variant="secondary"
onClick={previousStep}
onClick={() => setStep(step - 1)}
/>
<Button label="Submit" type="submit" variant="primary" />{' '}
</div>

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