Files
twenty/packages/twenty-website/src/partner-application/wizard/PartnerApplicationWizard.tsx
T
Abdullah. 569d887d1e [Website] Cut over to the rebuilt site (#21825)
Renaming the package so any further PRs directed to the website are
targeted to the reworked code instead of diverging. Once merged, I will
start preparing this for deployment to dev to test before releasing to
prod. Any improvements will also be applied to this package.

I avoided making significant changes to API routes so nothing breaks,
but will test it thoroughly today to confirm. That said, everything is
ported - double checked.

Big diff PR, impossible to review, but last one! No more rebuilds.
2026-06-19 10:22:46 +02:00

296 lines
7.3 KiB
TypeScript

'use client';
import { useLingui } from '@lingui/react';
import { styled } from '@linaria/react';
import { type FormEvent, useCallback, useEffect } from 'react';
import {
color,
fontFamily,
fontSize,
radius,
semanticColor,
spacing,
} from '@/tokens';
import { Body, Button, Heading, StepIndicator } from '@/ui';
import { buildPartnerApplicationRequestBody } from '../build-partner-application-request-body';
import { PARTNER_APPLICATION_STEP_IDS } from '../data/partner-application-step-ids';
import { getCurrentStepId } from '../get-current-step-id';
import { PARTNER_APPLICATION_COPY } from '../partner-application-copy';
import {
type PartnerApplicationController,
usePartnerApplicationState,
} from '../use-partner-application-state';
import { validatePartnerApplicationStep } from '../validate-partner-application-step';
import { PartnerApplicationSuccess } from './PartnerApplicationSuccess';
import { CommercialsStep } from './steps/CommercialsStep';
import { ExpertiseStep } from './steps/ExpertiseStep';
import { IdentityStep } from './steps/IdentityStep';
import { ProfileStep } from './steps/ProfileStep';
const COPY = PARTNER_APPLICATION_COPY;
const STEPS = PARTNER_APPLICATION_STEP_IDS;
const WizardRoot = styled.div`
display: flex;
flex-direction: column;
& > * + * {
margin-top: ${spacing(4)};
}
`;
const TitleBlock = styled.div`
display: flex;
flex-direction: column;
& > * + * {
margin-top: ${spacing(6)};
}
`;
const IntroGroup = styled.div`
display: flex;
flex-direction: column;
& > * + * {
margin-top: ${spacing(3)};
}
`;
const HeaderStrip = styled.div`
align-items: center;
display: flex;
gap: ${spacing(3)};
justify-content: space-between;
`;
const HeaderLabel = styled.span`
color: ${semanticColor.inkMuted};
font-family: ${fontFamily('mono')};
font-size: ${fontSize(3)};
text-transform: uppercase;
`;
const FieldsStack = styled.div`
display: flex;
flex-direction: column;
& > * + * {
margin-top: ${spacing(4)};
}
`;
const Footer = styled.div`
display: flex;
flex-direction: column;
& > * + * {
margin-top: ${spacing(2)};
}
`;
const SecondaryButton = styled.button`
background: none;
border: 1px solid ${semanticColor.lineStrong};
border-radius: ${radius(2)};
color: ${semanticColor.ink};
cursor: pointer;
font-family: ${fontFamily('mono')};
font-size: ${fontSize(3)};
height: ${spacing(10)};
padding: 0 ${spacing(4)};
text-transform: uppercase;
`;
const FooterControls = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
width: 100%;
`;
const ErrorBanner = styled.p`
color: ${color('error')};
font-family: ${fontFamily('sans')};
font-size: ${fontSize(3)};
`;
function StepRenderer({
controller,
}: {
controller: PartnerApplicationController;
}) {
switch (getCurrentStepId(controller.state)) {
case 'identity':
return <IdentityStep controller={controller} />;
case 'profile':
return <ProfileStep controller={controller} />;
case 'expertise':
return <ExpertiseStep controller={controller} />;
case 'commercials':
return <CommercialsStep controller={controller} />;
}
}
export function PartnerApplicationWizard({
onSubmitted,
onSuccess,
resetSignal,
}: {
onSubmitted?: () => void;
onSuccess: () => void;
resetSignal: number;
}) {
const { i18n } = useLingui();
const controller = usePartnerApplicationState();
const {
goBack,
goNext,
reset,
setSubmitError,
setSubmitted,
setSubmitting,
state,
} = controller;
useEffect(() => {
reset();
}, [resetSignal, reset]);
const stepId = getCurrentStepId(state);
const stepIndex = state.stepIndex;
const isLastStep = stepIndex === STEPS.length - 1;
const errorValues = Object.values(state.fieldErrors);
const hasFieldErrors = errorValues.length > 0;
const fieldErrorMessage = errorValues.includes('invalid_email')
? COPY.validation.invalidEmail
: errorValues.includes('invalid_url')
? COPY.validation.invalidUrl
: errorValues.includes('invalid_amount')
? COPY.validation.invalidAmount
: COPY.validation.incompleteForm;
const handleSubmit = useCallback(
async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!isLastStep) {
goNext();
return;
}
if (state.isSubmitting) return;
setSubmitError(null);
if (Object.keys(validatePartnerApplicationStep(state)).length > 0) {
goNext();
return;
}
const payload = buildPartnerApplicationRequestBody(state);
setSubmitting(true);
try {
const response = await fetch('/api/partner-application', {
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
});
if (!response.ok) {
setSubmitError(i18n._(COPY.validation.submitFailed));
return;
}
setSubmitted();
onSubmitted?.();
} catch {
setSubmitError(i18n._(COPY.validation.submitFailed));
} finally {
setSubmitting(false);
}
},
[
goNext,
i18n,
isLastStep,
onSubmitted,
setSubmitError,
setSubmitted,
setSubmitting,
state,
],
);
if (state.isSubmitted) {
return (
<PartnerApplicationSuccess
company={state.company}
email={state.email}
name={state.name}
onDismiss={onSuccess}
/>
);
}
const stepLabel = `${i18n._(
COPY.stepProgressLabel(stepIndex + 1, STEPS.length),
)} · ${i18n._(COPY.stepHeaders[stepId])}`;
return (
<WizardRoot>
<TitleBlock>
{stepIndex === 0 ? (
<IntroGroup>
<Heading as="h2" size="lg" weight="light">
{i18n._(COPY.title)}
</Heading>
<Body muted size="md">
{i18n._(COPY.subtitle)}
</Body>
</IntroGroup>
) : null}
<HeaderStrip>
<HeaderLabel>{stepLabel}</HeaderLabel>
<StepIndicator activeStepIndex={stepIndex} stepCount={STEPS.length} />
</HeaderStrip>
</TitleBlock>
<form autoComplete="off" noValidate onSubmit={handleSubmit}>
<FieldsStack>
<StepRenderer controller={controller} />
<Footer>
{state.submitError !== null ? (
<ErrorBanner role="alert">{state.submitError}</ErrorBanner>
) : null}
{hasFieldErrors ? (
<ErrorBanner role="alert">
{i18n._(fieldErrorMessage)}
</ErrorBanner>
) : null}
<FooterControls>
{stepIndex > 0 ? (
<SecondaryButton onClick={goBack} type="button">
{i18n._(COPY.back)}
</SecondaryButton>
) : (
<span />
)}
<Button
disabled={state.isSubmitting}
label={
isLastStep
? state.isSubmitting
? i18n._(COPY.submitInFlight)
: i18n._(COPY.submit)
: i18n._(COPY.next)
}
type="submit"
variant="filled"
/>
</FooterControls>
</Footer>
</FieldsStack>
</form>
</WizardRoot>
);
}