feat(website): book an intro call after partner application (#21343)

## What

After a partner submits the application wizard, the success screen now
offers an inline Cal.com booking widget so they can book an intro call
on the spot — keeping the partner process high-touch.

- Inline Cal.com embed on the application success step, prefilled with
the applicant's name/email (company in the notes)
- Wide `month_view` layout; the success modal widens to ~960px (the
4-step form and mobile are unchanged)
- Event-type-details panel hidden via `cal('ui', { hideEventTypeDetails:
true })` so it's just calendar + times, on its own `partner-intro` Cal
namespace (isolated from the ContactCal embed)
- "I'll book later" escape hatch; backend / submission path untouched

## Why

The warmest moment is right after someone opts in. Today the success
screen only shows a Close button — this turns that moment into a
scheduled conversation.

## How

- `PartnerIntroCalEmbed` — thin wrapper over `@calcom/embed-react`
(already a dependency), reusing the existing `ContactCal` embed pattern
- `buildPartnerIntroPrefill` — pure mapping of applicant fields → Cal
prefill
- `PartnerApplicationSuccess` — presentational success view (heading +
subtitle + embed + dismiss)
- The wizard reports submitted-state up (`onSubmittedChange`) so the
modal widens only on the booking step
- New Lingui copy + regenerated catalogs (en/es/fr)

## Test plan

- `npx jest PartnerApplication` — green (prefill mapping, embed
link/layout/prefill, success view)
- `npx nx typecheck twenty-website` — clean
- `npx oxlint -c .oxlintrc.json` / `npx oxfmt --check` — clean
- Manual: submit the wizard → success step shows the wide booking
calendar, prefilled, dark theme, no event-details panel; "I'll book
later" closes the modal

## Notes

- Frontend only — no backend, schema, or submission-path change
- Cal link `rashad-twenty/partner-intro` lives as a constant in
`config.ts`
- Branch is currently behind `main`; happy to rebase before review
This commit is contained in:
Rashad Karanouh
2026-06-09 18:00:48 +04:00
committed by GitHub
parent ba4ac6b70e
commit bda6fcfec9
17 changed files with 423 additions and 88 deletions

View File

@@ -40,3 +40,7 @@ ENTERPRISE_JWT_PUBLIC_KEY=
# (server-side only) via the /s/partners REST endpoint.
TWENTY_PARTNERS_API_URL=
TWENTY_PARTNERS_API_KEY=
# Cal.com path (no host) the partner application success screen books the intro
# call onto, e.g. acme/partner-intro. Falls back to a built-in default when unset.
NEXT_PUBLIC_PARTNER_INTRO_CAL_LINK=

View File

@@ -1153,11 +1153,6 @@ msgstr "Classic never dies. It just gets extended one more time."
msgid "Clear filters"
msgstr "Clear filters"
#. js-lingui-id: yz7wBu
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Close"
msgstr "Close"
#. js-lingui-id: L/LPQZ
#: src/sections/Faq/faq.data.ts
msgid "Cloud Pro is $9/user/month (yearly). Organization is $19/user/month and unlocks SSO and row-level permissions for teams that need finer access control."
@@ -2138,6 +2133,11 @@ msgstr "Ghana 🇬🇭"
msgid "Good choice!"
msgstr "Good choice!"
#. js-lingui-id: Zh0acB
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Grab 30 minutes so we can get to know your team."
msgstr "Grab 30 minutes so we can get to know your team."
#. js-lingui-id: +tDN2S
#: src/sections/PartnerApplication/wizard/partner-fields.data.ts
msgid "Greece 🇬🇷"
@@ -2320,6 +2320,11 @@ msgstr "How W3villa Technologies shipped W3Grads, an AI mock interview platform
msgid "Hungary 🇭🇺"
msgstr "Hungary 🇭🇺"
#. js-lingui-id: T6wyby
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "I'll book later →"
msgstr "I'll book later →"
#. js-lingui-id: RYDa0v
#: src/sections/PartnerApplication/wizard/partner-fields.data.ts
msgid "Iceland 🇮🇸"
@@ -3084,6 +3089,11 @@ msgstr "Norwegian"
msgid "Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you."
msgstr "Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you."
#. js-lingui-id: WFvpMn
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Now book your intro call."
msgstr "Now book your intro call."
#. js-lingui-id: CzeIij
#: src/app/[locale]/pricing/plan-table.data.ts
msgid "Number of dashboards"
@@ -4128,10 +4138,10 @@ msgstr "Thai"
msgid "Thailand 🇹🇭"
msgstr "Thailand 🇹🇭"
#. js-lingui-id: pYwj0k
#. js-lingui-id: eRjhUK
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Thanks,"
msgstr "Thanks,"
msgid "Thanks, you're in."
msgstr "Thanks, you're in."
#. js-lingui-id: FjkPYg
#: src/app/[locale]/customers/elevate-consulting/page.tsx
@@ -4770,11 +4780,6 @@ msgstr "We could not submit your application. Please try again in a moment."
msgid "We didn't want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind."
msgstr "We didn't want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind."
#. js-lingui-id: u7hKUm
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "we'll be in touch!"
msgstr "we'll be in touch!"
#. js-lingui-id: d5CWI6
#: src/app/[locale]/partners/components/PartnerHero.tsx
msgid "We're building the #1 Open Source CRM, but we can't do it alone. Join our partner ecosystem and grow with us."

View File

@@ -1158,11 +1158,6 @@ msgstr "Lo clásico nunca muere. Solo se extiende una vez más."
msgid "Clear filters"
msgstr ""
#. js-lingui-id: yz7wBu
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Close"
msgstr ""
#. js-lingui-id: L/LPQZ
#: src/sections/Faq/faq.data.ts
msgid "Cloud Pro is $9/user/month (yearly). Organization is $19/user/month and unlocks SSO and row-level permissions for teams that need finer access control."
@@ -2143,6 +2138,11 @@ msgstr ""
msgid "Good choice!"
msgstr "¡Buena elección!"
#. js-lingui-id: Zh0acB
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Grab 30 minutes so we can get to know your team."
msgstr ""
#. js-lingui-id: +tDN2S
#: src/sections/PartnerApplication/wizard/partner-fields.data.ts
msgid "Greece 🇬🇷"
@@ -2325,6 +2325,11 @@ msgstr "Cómo W3villa Technologies lanzó W3Grads, una plataforma de entrevistas
msgid "Hungary 🇭🇺"
msgstr ""
#. js-lingui-id: T6wyby
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "I'll book later →"
msgstr ""
#. js-lingui-id: RYDa0v
#: src/sections/PartnerApplication/wizard/partner-fields.data.ts
msgid "Iceland 🇮🇸"
@@ -3089,6 +3094,11 @@ msgstr ""
msgid "Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you."
msgstr "Ahora un desarrollador puede describir lo que quiere a Claude Code y tener una app funcionando en una tarde. Un objeto personalizado, un flujo de puntuación, una vista nueva, una integración. El cuello de botella ya no es construir. Es si tu plataforma te lo permite."
#. js-lingui-id: WFvpMn
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Now book your intro call."
msgstr ""
#. js-lingui-id: CzeIij
#: src/app/[locale]/pricing/plan-table.data.ts
msgid "Number of dashboards"
@@ -4133,9 +4143,9 @@ msgstr ""
msgid "Thailand 🇹🇭"
msgstr ""
#. js-lingui-id: pYwj0k
#. js-lingui-id: eRjhUK
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Thanks,"
msgid "Thanks, you're in."
msgstr ""
#. js-lingui-id: FjkPYg
@@ -4775,11 +4785,6 @@ msgstr "No pudimos enviar tu solicitud. Inténtalo de nuevo en un momento."
msgid "We didn't want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind."
msgstr "No queríamos parchear el problema. Queríamos crear algo en lo que las instituciones pudieran confiar a escala, y eso significaba partir de una base lo suficientemente sólida como para soportar toda la complejidad de lo que teníamos en mente."
#. js-lingui-id: u7hKUm
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "we'll be in touch!"
msgstr ""
#. js-lingui-id: d5CWI6
#: src/app/[locale]/partners/components/PartnerHero.tsx
msgid "We're building the #1 Open Source CRM, but we can't do it alone. Join our partner ecosystem and grow with us."

View File

@@ -1158,11 +1158,6 @@ msgstr "Le classique ne meurt jamais. Il est simplement étendu une fois de plus
msgid "Clear filters"
msgstr ""
#. js-lingui-id: yz7wBu
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Close"
msgstr ""
#. js-lingui-id: L/LPQZ
#: src/sections/Faq/faq.data.ts
msgid "Cloud Pro is $9/user/month (yearly). Organization is $19/user/month and unlocks SSO and row-level permissions for teams that need finer access control."
@@ -2143,6 +2138,11 @@ msgstr ""
msgid "Good choice!"
msgstr "Bon choix !"
#. js-lingui-id: Zh0acB
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Grab 30 minutes so we can get to know your team."
msgstr ""
#. js-lingui-id: +tDN2S
#: src/sections/PartnerApplication/wizard/partner-fields.data.ts
msgid "Greece 🇬🇷"
@@ -2325,6 +2325,11 @@ msgstr "Comment W3villa Technologies a livré W3Grads, une plateforme dentret
msgid "Hungary 🇭🇺"
msgstr ""
#. js-lingui-id: T6wyby
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "I'll book later →"
msgstr ""
#. js-lingui-id: RYDa0v
#: src/sections/PartnerApplication/wizard/partner-fields.data.ts
msgid "Iceland 🇮🇸"
@@ -3089,6 +3094,11 @@ msgstr ""
msgid "Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you."
msgstr "Aujourdhui, un développeur peut décrire ce quil veut à Claude Code et avoir une application fonctionnelle dans laprès-midi. Un objet personnalisé, un workflow de scoring, une nouvelle vue, une intégration. Le goulot détranglement nest plus la construction. Cest de savoir si votre plateforme vous le permet."
#. js-lingui-id: WFvpMn
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Now book your intro call."
msgstr ""
#. js-lingui-id: CzeIij
#: src/app/[locale]/pricing/plan-table.data.ts
msgid "Number of dashboards"
@@ -4133,9 +4143,9 @@ msgstr ""
msgid "Thailand 🇹🇭"
msgstr ""
#. js-lingui-id: pYwj0k
#. js-lingui-id: eRjhUK
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "Thanks,"
msgid "Thanks, you're in."
msgstr ""
#. js-lingui-id: FjkPYg
@@ -4775,11 +4785,6 @@ msgstr "Nous n'avons pas pu soumettre votre candidature. Veuillez réessayer dan
msgid "We didn't want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind."
msgstr "Nous ne voulions pas simplement masquer le problème. Nous voulions construire quelque chose sur lequel les établissements pourraient s'appuyer à grande échelle, ce qui signifiait partir d'une base suffisamment solide pour supporter toute la complexité de ce que nous avions en tête."
#. js-lingui-id: u7hKUm
#: src/sections/PartnerApplication/partner-application-modal-data.ts
msgid "we'll be in touch!"
msgstr ""
#. js-lingui-id: d5CWI6
#: src/app/[locale]/partners/components/PartnerHero.tsx
msgid "We're building the #1 Open Source CRM, but we can't do it alone. Join our partner ecosystem and grow with us."

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@ import {
PartnerApplicationWizard,
partnerWizardPanelClass,
} from '@/sections/PartnerApplication/wizard/PartnerApplicationWizard';
import { useEffect, useState } from 'react';
import { useLayoutEffect, useState } from 'react';
type PartnerApplicationModalProps = {
open: boolean;
@@ -17,9 +17,15 @@ export function PartnerApplicationModal({
onClose,
}: PartnerApplicationModalProps) {
const [resetSignal, setResetSignal] = useState(0);
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (open) setResetSignal((n) => n + 1);
// Reset before paint (not useEffect) so a stale submitted=true from a prior
// session can't flash the wide booking panel for a frame on reopen.
useLayoutEffect(() => {
if (open) {
setResetSignal((n) => n + 1);
setSubmitted(false);
}
}, [open]);
return (
@@ -29,8 +35,13 @@ export function PartnerApplicationModal({
if (!next) onClose();
}}
className={partnerWizardPanelClass}
panelWidth={submitted ? 'min(960px, 100%)' : undefined}
>
<PartnerApplicationWizard resetSignal={resetSignal} onSuccess={onClose} />
<PartnerApplicationWizard
resetSignal={resetSignal}
onSuccess={onClose}
onSubmitted={() => setSubmitted(true)}
/>
</Modal.Root>
);
}

View File

@@ -0,0 +1,7 @@
// Cal.com path (no host) the partner application success screen books the intro
// call onto. Overridable via env so the org / self-hosters can point it at a
// team link without a code change; defaults to the canonical handle.
// Full link: https://cal.com/<this-value>
export const PARTNER_INTRO_CAL_LINK =
process.env.NEXT_PUBLIC_PARTNER_INTRO_CAL_LINK ||
'rashad-twenty/partner-intro';

View File

@@ -12,9 +12,10 @@ export const PARTNER_APPLICATION_MODAL_COPY = {
next: msg`Next →`,
submit: msg`Submit application`,
submitInFlight: msg`Submitting…`,
successTitleSerif: msg`Thanks,`,
successTitleSans: msg`we'll be in touch!`,
successClose: msg`Close`,
successTitleSerif: msg`Thanks, you're in.`,
successTitleSans: msg`Now book your intro call.`,
bookIntroSubtitle: msg`Grab 30 minutes so we can get to know your team.`,
bookLater: msg`I'll book later →`,
validation: {
incompleteForm: msg`Please complete all required fields before continuing.`,
invalidEmail: msg`Enter a valid email address.`,

View File

@@ -0,0 +1,110 @@
'use client';
import { Body, Heading, HeadingPart } from '@/design-system/components';
import { PartnerIntroCalEmbed } from '@/sections/PartnerApplication/wizard/PartnerIntroCalEmbed';
import { buildPartnerIntroPrefill } from '@/sections/PartnerApplication/wizard/partner-intro-prefill';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
type DialogPrimitive = React.ComponentType<{ render?: React.ReactElement }>;
type PartnerApplicationSuccessProps = {
Title: DialogPrimitive;
Description: DialogPrimitive;
titleSerif: string;
titleSans: string;
subtitle: string;
bookLaterLabel: string;
name?: string;
email?: string;
company?: string;
onDismiss: () => void;
};
const TitleHeadingWrapper = styled.div`
color: ${theme.colors.secondary.text[100]};
`;
const SuccessView = styled.div`
align-items: stretch;
display: flex;
flex-direction: column;
gap: clamp(16px, 4vh, 32px);
margin-top: clamp(16px, 4vh, 32px);
`;
const Subtitle = styled.div`
color: ${theme.colors.secondary.text[60]};
`;
const EmbedFrame = styled.div`
border: 1px solid ${theme.colors.secondary.border[20]};
border-radius: ${theme.radius(2)};
overflow: hidden;
`;
const BookLaterButton = styled.button`
align-self: flex-end;
background: transparent;
border: none;
color: ${theme.colors.secondary.text[60]};
cursor: pointer;
font-family: ${theme.font.family.mono};
font-size: ${theme.font.size(3)};
padding: 0;
text-transform: uppercase;
`;
export function PartnerApplicationSuccess({
Title,
Description,
titleSerif,
titleSans,
subtitle,
bookLaterLabel,
name,
email,
company,
onDismiss,
}: PartnerApplicationSuccessProps) {
const prefill = buildPartnerIntroPrefill({ name, email, company });
return (
<>
<Title
render={
<TitleHeadingWrapper>
<Heading as="h2" size="lg" weight="light">
<HeadingPart fontFamily="serif" fontWeight="light">
{titleSerif}
</HeadingPart>
<br />
<HeadingPart fontFamily="sans" fontWeight="light">
{titleSans}
</HeadingPart>
</Heading>
</TitleHeadingWrapper>
}
/>
<SuccessView>
<Description
render={
<Subtitle>
<Body size="md">{subtitle}</Body>
</Subtitle>
}
/>
<EmbedFrame>
<PartnerIntroCalEmbed
name={prefill.name}
email={prefill.email}
notes={prefill.notes}
/>
</EmbedFrame>
<BookLaterButton type="button" onClick={onDismiss}>
{bookLaterLabel}
</BookLaterButton>
</SuccessView>
</>
);
}

View File

@@ -16,6 +16,7 @@ import { IdentityStep } from '@/sections/PartnerApplication/wizard/steps/Identit
import { ProfileStep } from '@/sections/PartnerApplication/wizard/steps/ProfileStep';
import { ExpertiseStep } from '@/sections/PartnerApplication/wizard/steps/ExpertiseStep';
import { CommercialsStep } from '@/sections/PartnerApplication/wizard/steps/CommercialsStep';
import { PartnerApplicationSuccess } from '@/sections/PartnerApplication/wizard/PartnerApplicationSuccess';
import {
buildPartnerApplicationRequestBody,
getCurrentStepId,
@@ -124,14 +125,6 @@ const FieldErrorBanner = styled.p`
margin: 0;
`;
const SuccessView = styled.div`
align-items: stretch;
display: flex;
flex-direction: column;
gap: clamp(16px, 4vh, 32px);
margin-top: clamp(16px, 4vh, 32px);
`;
function StepRenderer({
controller,
}: {
@@ -160,12 +153,14 @@ type WizardSlots = {
type WizardProps = {
resetSignal: number;
onSuccess: () => void;
onSubmitted?: () => void;
slots?: WizardSlots;
};
export function PartnerApplicationWizard({
resetSignal,
onSuccess,
onSubmitted,
slots,
}: WizardProps) {
const { i18n } = useLingui();
@@ -237,6 +232,7 @@ export function PartnerApplicationWizard({
return;
}
setSubmitted();
onSubmitted?.();
} catch {
setSubmitError(
i18n._(PARTNER_APPLICATION_MODAL_COPY.validation.submitFailed),
@@ -252,45 +248,25 @@ export function PartnerApplicationWizard({
setSubmitError,
setSubmitting,
setSubmitted,
onSubmitted,
i18n,
],
);
if (state.isSubmitted) {
return (
<>
<TitleBlock>
<Title
render={
<TitleHeadingWrapper>
<Heading as="h2" size="lg" weight="light">
<HeadingPart fontFamily="serif" fontWeight="light">
{i18n._(PARTNER_APPLICATION_MODAL_COPY.successTitleSerif)}
</HeadingPart>
<br />
<HeadingPart fontFamily="sans" fontWeight="light">
{i18n._(PARTNER_APPLICATION_MODAL_COPY.successTitleSans)}
</HeadingPart>
</Heading>
</TitleHeadingWrapper>
}
/>
</TitleBlock>
<SuccessView>
<Modal.Footer>
<PrimaryButton type="button" onClick={onSuccess}>
<ButtonShape
fillColor={theme.colors.primary.background[100]}
height={BUTTON_HEIGHTS_PX.regular}
strokeColor="none"
/>
<PrimaryLabel>
{i18n._(PARTNER_APPLICATION_MODAL_COPY.successClose)}
</PrimaryLabel>
</PrimaryButton>
</Modal.Footer>
</SuccessView>
</>
<PartnerApplicationSuccess
Title={Title}
Description={Description}
titleSerif={i18n._(PARTNER_APPLICATION_MODAL_COPY.successTitleSerif)}
titleSans={i18n._(PARTNER_APPLICATION_MODAL_COPY.successTitleSans)}
subtitle={i18n._(PARTNER_APPLICATION_MODAL_COPY.bookIntroSubtitle)}
bookLaterLabel={i18n._(PARTNER_APPLICATION_MODAL_COPY.bookLater)}
name={state.name}
email={state.email}
company={state.company}
onDismiss={onSuccess}
/>
);
}

View File

@@ -0,0 +1,52 @@
'use client';
import Cal, { getCalApi } from '@calcom/embed-react';
import { useEffect } from 'react';
import { PARTNER_INTRO_CAL_LINK } from '@/sections/PartnerApplication/config';
// Own namespace so the ui() config (hiding event-type details) applies only to
// this embed, not the ContactCal "talk to us" embed elsewhere on the site.
const PARTNER_INTRO_CAL_NAMESPACE = 'partner-intro';
type PartnerIntroCalEmbedProps = {
name?: string;
email?: string;
notes?: string;
};
// Renders Cal.com's own booking UI in an iframe. We control the link, theme,
// month_view layout, and prefill via config; hideEventTypeDetails must go
// through ui() (it is not part of the config prop type). Cal owns the rest.
export function PartnerIntroCalEmbed({
name,
email,
notes,
}: PartnerIntroCalEmbedProps) {
useEffect(() => {
let cancelled = false;
void (async () => {
const cal = await getCalApi({ namespace: PARTNER_INTRO_CAL_NAMESPACE });
if (cancelled) return;
cal('ui', { hideEventTypeDetails: true });
})();
return () => {
cancelled = true;
};
}, []);
return (
<Cal
namespace={PARTNER_INTRO_CAL_NAMESPACE}
calLink={PARTNER_INTRO_CAL_LINK}
config={{
theme: 'dark',
layout: 'month_view',
...(name !== undefined ? { name } : {}),
...(email !== undefined ? { email } : {}),
...(notes !== undefined ? { notes } : {}),
}}
style={{ minHeight: 520, overflow: 'auto', width: '100%' }}
/>
);
}

View File

@@ -0,0 +1,48 @@
// __tests__/PartnerApplicationSuccess.test.tsx
import { renderToStaticMarkup } from 'react-dom/server';
jest.mock('@calcom/embed-react', () => ({
__esModule: true,
default: (props: {
calLink: string;
config?: { name?: string; email?: string; notes?: string };
}) => (
<div
data-testid="cal-embed"
data-callink={props.calLink}
data-name={props.config?.name}
data-notes={props.config?.notes}
/>
),
}));
import { PartnerApplicationSuccess } from '@/sections/PartnerApplication/wizard/PartnerApplicationSuccess';
const Passthrough = ({ render }: { render?: React.ReactElement }) => (
<>{render}</>
);
describe('PartnerApplicationSuccess', () => {
it('renders the booking embed prefilled from the applicant plus an escape hatch', () => {
const html = renderToStaticMarkup(
<PartnerApplicationSuccess
Title={Passthrough}
Description={Passthrough}
titleSerif="Thanks, you are in."
titleSans="Now book your intro call."
subtitle="Grab 30 minutes so we can get to know your team."
bookLaterLabel="book later"
name="Ada Lovelace"
email="ada@example.com"
company="Analytical Engines"
onDismiss={() => undefined}
/>,
);
expect(html).toContain('data-callink="rashad-twenty/partner-intro"');
expect(html).toContain('data-name="Ada Lovelace"');
expect(html).toContain('data-notes="Company: Analytical Engines"');
expect(html).toContain('Now book your intro call.');
expect(html).toContain('book later');
});
});

View File

@@ -0,0 +1,48 @@
// __tests__/PartnerIntroCalEmbed.test.tsx
import { renderToStaticMarkup } from 'react-dom/server';
jest.mock('@calcom/embed-react', () => ({
__esModule: true,
default: (props: {
calLink: string;
config?: {
theme?: string;
layout?: string;
name?: string;
email?: string;
notes?: string;
};
}) => (
<div
data-testid="cal-embed"
data-callink={props.calLink}
data-theme={props.config?.theme}
data-layout={props.config?.layout}
data-name={props.config?.name}
data-email={props.config?.email}
data-notes={props.config?.notes}
/>
),
getCalApi: jest.fn(async () => jest.fn()),
}));
import { PartnerIntroCalEmbed } from '@/sections/PartnerApplication/wizard/PartnerIntroCalEmbed';
describe('PartnerIntroCalEmbed', () => {
it('points at the partner-intro Cal link and forwards prefill + theme', () => {
const html = renderToStaticMarkup(
<PartnerIntroCalEmbed
name="Ada Lovelace"
email="ada@example.com"
notes="Company: Acme"
/>,
);
expect(html).toContain('data-callink="rashad-twenty/partner-intro"');
expect(html).toContain('data-theme="dark"');
expect(html).toContain('data-layout="month_view"');
expect(html).toContain('data-name="Ada Lovelace"');
expect(html).toContain('data-email="ada@example.com"');
expect(html).toContain('data-notes="Company: Acme"');
});
});

View File

@@ -0,0 +1,37 @@
import { buildPartnerIntroPrefill } from '../partner-intro-prefill';
describe('buildPartnerIntroPrefill', () => {
it('maps name, email and company into Cal prefill fields', () => {
expect(
buildPartnerIntroPrefill({
name: 'Ada Lovelace',
email: 'ada@example.com',
company: 'Analytical Engines',
}),
).toEqual({
name: 'Ada Lovelace',
email: 'ada@example.com',
notes: 'Company: Analytical Engines',
});
});
it('trims values before mapping', () => {
expect(
buildPartnerIntroPrefill({
name: ' Ada ',
email: ' ada@example.com ',
company: ' Acme ',
}),
).toEqual({
name: 'Ada',
email: 'ada@example.com',
notes: 'Company: Acme',
});
});
it('omits empty or whitespace-only fields', () => {
expect(
buildPartnerIntroPrefill({ name: '', email: ' ', company: undefined }),
).toEqual({});
});
});

View File

@@ -0,0 +1,26 @@
export type PartnerIntroPrefillInput = {
name?: string;
email?: string;
company?: string;
};
// Cal.com prefill keys. `notes` surfaces the applicant's company on the booking
// so the call arrives pre-qualified.
export type PartnerIntroPrefill = {
name?: string;
email?: string;
notes?: string;
};
export function buildPartnerIntroPrefill(
input: PartnerIntroPrefillInput,
): PartnerIntroPrefill {
const prefill: PartnerIntroPrefill = {};
const name = input.name?.trim();
const email = input.email?.trim();
const company = input.company?.trim();
if (name) prefill.name = name;
if (email) prefill.email = email;
if (company) prefill.notes = `Company: ${company}`;
return prefill;
}