feat: add package booking intake flow

This commit is contained in:
Flatlogic Bot 2026-06-11 14:50:28 +00:00
parent 94bd7fcb30
commit 2757e36bdd
11 changed files with 184 additions and 41 deletions

View File

@ -0,0 +1,20 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('intake_leads', 'package_name', {
type: Sequelize.TEXT,
allowNull: true,
});
await queryInterface.addColumn('intake_leads', 'preferred_time', {
type: Sequelize.TEXT,
allowNull: true,
});
},
async down(queryInterface) {
await queryInterface.removeColumn('intake_leads', 'preferred_time');
await queryInterface.removeColumn('intake_leads', 'package_name');
},
};

View File

@ -7,6 +7,8 @@ module.exports = function(sequelize, DataTypes) {
email: { type: DataTypes.TEXT }, email: { type: DataTypes.TEXT },
company: { type: DataTypes.TEXT }, company: { type: DataTypes.TEXT },
role_title: { type: DataTypes.TEXT }, role_title: { type: DataTypes.TEXT },
package_name: { type: DataTypes.TEXT },
preferred_time: { type: DataTypes.TEXT },
goal: { type: DataTypes.TEXT }, goal: { type: DataTypes.TEXT },
challenge: { type: DataTypes.TEXT }, challenge: { type: DataTypes.TEXT },
desired_outcome: { type: DataTypes.TEXT }, desired_outcome: { type: DataTypes.TEXT },

View File

@ -691,6 +691,8 @@ router.post(
status: "active", status: "active",
goals: lead.goal, goals: lead.goal,
notes: [ notes: [
lead.package_name && `Requested package: ${lead.package_name}`,
lead.preferred_time && `Preferred first-call time: ${lead.preferred_time}`,
lead.challenge && `Challenge: ${lead.challenge}`, lead.challenge && `Challenge: ${lead.challenge}`,
lead.desired_outcome && `Desired outcome: ${lead.desired_outcome}`, lead.desired_outcome && `Desired outcome: ${lead.desired_outcome}`,
lead.consent_ai_notes ? "Consented to AI-assisted session notes." : "AI notes consent not granted yet.", lead.consent_ai_notes ? "Consented to AI-assisted session notes." : "AI notes consent not granted yet.",

View File

@ -26,6 +26,8 @@ router.post(
email, email,
company: data.company, company: data.company,
role_title: data.role_title, role_title: data.role_title,
package_name: data.package_name,
preferred_time: data.preferred_time,
goal: data.goal, goal: data.goal,
challenge: data.challenge, challenge: data.challenge,
desired_outcome: data.desired_outcome, desired_outcome: data.desired_outcome,

View File

@ -1,6 +1,17 @@
export const publicCoachSite = { export const publicCoachSite = {
workspaceName: 'Coaching SaaS Workspace', workspaceName: 'Coaching SaaS Workspace',
footerLine: 'Coaching beyond the session.', footerLine: 'Coaching beyond the session.',
assessmentPath: '/intake/?package=leadership-assessment',
socialLinks: [
{
label: 'LinkedIn',
href: 'https://www.linkedin.com/',
},
{
label: 'Interview series',
href: 'https://www.youtube.com/',
},
],
coach: { coach: {
name: 'Alex Morgan', name: 'Alex Morgan',
title: 'Executive coach for founders, operators, and leadership teams.', title: 'Executive coach for founders, operators, and leadership teams.',
@ -9,8 +20,8 @@ export const publicCoachSite = {
aboutIntro: aboutIntro:
'Use this page to introduce the coach, their niche, credentials, and point of view. The template is built for practices where trust, confidentiality, and continuity matter as much as booking the first call.', 'Use this page to introduce the coach, their niche, credentials, and point of view. The template is built for practices where trust, confidentiality, and continuity matter as much as booking the first call.',
credentials: [ credentials: [
'Founder and executive coaching', 'ICF-style executive coaching profile',
'Leadership transitions', 'Founder and senior leadership transitions',
'Decision systems and operating rhythm', 'Decision systems and operating rhythm',
'Confidential client workspace', 'Confidential client workspace',
], ],
@ -31,8 +42,11 @@ export const publicCoachSite = {
], ],
packages: [ packages: [
{ {
slug: 'leadership-assessment',
name: 'Leadership Assessment', name: 'Leadership Assessment',
price: 'Intro', price: 'Intro',
bookingLabel: 'Book assessment',
bookingPath: '/intake/?package=leadership-assessment',
copy: 'A focused first step for founders and senior leaders who want to clarify the coaching agenda.', copy: 'A focused first step for founders and senior leaders who want to clarify the coaching agenda.',
items: [ items: [
'intake review', 'intake review',
@ -41,8 +55,11 @@ export const publicCoachSite = {
], ],
}, },
{ {
slug: 'founder-coaching',
name: 'Founder Coaching', name: 'Founder Coaching',
price: 'Monthly', price: 'Monthly',
bookingLabel: 'Book monthly coaching',
bookingPath: '/intake/?package=founder-coaching',
copy: 'Ongoing 1:1 coaching with session memory, commitments, resources, and between-session accountability.', copy: 'Ongoing 1:1 coaching with session memory, commitments, resources, and between-session accountability.',
items: [ items: [
'two sessions per month', 'two sessions per month',
@ -51,8 +68,11 @@ export const publicCoachSite = {
], ],
}, },
{ {
slug: 'executive-operating-rhythm',
name: 'Executive Operating Rhythm', name: 'Executive Operating Rhythm',
price: 'Custom', price: 'Custom',
bookingLabel: 'Book package consult',
bookingPath: '/intake/?package=executive-operating-rhythm',
copy: 'A deeper engagement for leaders navigating delegation, decision rights, and team operating cadence.', copy: 'A deeper engagement for leaders navigating delegation, decision rights, and team operating cadence.',
items: [ items: [
'leadership themes', 'leadership themes',
@ -69,4 +89,24 @@ export const publicCoachSite = {
'A private place for client resources', 'A private place for client resources',
'Less admin after every session', 'Less admin after every session',
], ],
testimonials: [
{
quote:
'The session recap caught the exact leadership pattern I wanted to revisit. I edited one sentence and sent it.',
name: 'Ari Morgan',
role: 'Executive Coach',
},
{
quote:
'My clients finally have one place for notes, commitments, and the resources I share after each call.',
name: 'Leah Stone',
role: 'Founder Coach',
},
{
quote:
'Prep used to mean hunting through old docs. Now I start every session with the thread already visible.',
name: 'Daniel Reyes',
role: 'Leadership Advisor',
},
],
} as const; } as const;

View File

@ -1,4 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { publicCoachSite } from '../coachingSite';
const links = [ const links = [
{ href: '/how-it-works/', label: 'How it works' }, { href: '/how-it-works/', label: 'How it works' },
@ -30,7 +31,7 @@ export default function PublicSiteNav() {
</nav> </nav>
<Link <Link
href='/intake/' href={publicCoachSite.assessmentPath}
className='rounded-none bg-[#35b7a5] px-5 py-2.5 text-sm font-semibold text-white transition hover:brightness-105' className='rounded-none bg-[#35b7a5] px-5 py-2.5 text-sm font-semibold text-white transition hover:brightness-105'
> >
Start assessment Start assessment

View File

@ -55,8 +55,8 @@ export default function AboutCoach() {
{publicCoachSite.coach.aboutIntro} {publicCoachSite.coach.aboutIntro}
</p> </p>
<div className='mt-9 flex flex-wrap gap-3'> <div className='mt-9 flex flex-wrap gap-3'>
<Link href='/intake/' className={`rounded-none px-7 py-4 font-semibold ${ui.button}`}> <Link href={publicCoachSite.assessmentPath} className={`rounded-none px-7 py-4 font-semibold ${ui.button}`}>
Start assessment Book assessment
</Link> </Link>
<Link <Link
href='/services/' href='/services/'
@ -97,6 +97,24 @@ export default function AboutCoach() {
</div> </div>
))} ))}
</div> </div>
<div className='mt-6 border-t border-[#19192d]/10 pt-5'>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
Connect
</p>
<div className='mt-4 flex flex-wrap gap-3'>
{publicCoachSite.socialLinks.map((item) => (
<a
key={item.label}
href={item.href}
className='rounded-none border border-[#19192d]/10 bg-white px-4 py-2 text-sm font-semibold text-[#19192d] transition hover:border-[#35b7a5]'
target='_blank'
rel='noreferrer'
>
{item.label}
</a>
))}
</div>
</div>
</div> </div>
</section> </section>
@ -122,8 +140,8 @@ export default function AboutCoach() {
Start with an assessment and bring context into the first call. Start with an assessment and bring context into the first call.
</h2> </h2>
<div className='mt-8 flex justify-center'> <div className='mt-8 flex justify-center'>
<Link href='/intake/' className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}> <Link href={publicCoachSite.assessmentPath} className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}>
Start assessment Book assessment
</Link> </Link>
</div> </div>
</section> </section>

View File

@ -4,6 +4,7 @@ import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import PublicSiteNav from '../components/PublicSiteNav'; import PublicSiteNav from '../components/PublicSiteNav';
import { publicCoachSite } from '../coachingSite';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
@ -104,24 +105,6 @@ const trustCards = [
], ],
]; ];
const testimonials = [
[
'The session recap caught the exact leadership pattern I wanted to revisit. I edited one sentence and sent it.',
'Ari Morgan',
'Executive Coach',
],
[
'My clients finally have one place for notes, commitments, and the resources I share after each call.',
'Leah Stone',
'Founder Coach',
],
[
'Prep used to mean hunting through old docs. Now I start every session with the thread already visible.',
'Daniel Reyes',
'Leadership Advisor',
],
];
function WaveDivider({ function WaveDivider({
from = '#fffdf9', from = '#fffdf9',
to = '#19192d', to = '#19192d',
@ -199,10 +182,10 @@ export default function Starter() {
</p> </p>
<div className='mt-9 flex justify-center'> <div className='mt-9 flex justify-center'>
<Link <Link
href='/intake/' href={publicCoachSite.assessmentPath}
className={`rounded-none px-10 py-5 text-lg font-semibold ${ui.button}`} className={`rounded-none px-10 py-5 text-lg font-semibold ${ui.button}`}
> >
Start assessment Book assessment
</Link> </Link>
</div> </div>
<p className={`mt-5 text-lg ${ui.muted}`}> <p className={`mt-5 text-lg ${ui.muted}`}>
@ -543,21 +526,21 @@ export default function Starter() {
</h2> </h2>
</div> </div>
<Link <Link
href='/intake/' href={publicCoachSite.assessmentPath}
className={`rounded-none px-6 py-3 text-center font-semibold ${ui.button}`} className={`rounded-none px-6 py-3 text-center font-semibold ${ui.button}`}
> >
Start your workspace Book assessment
</Link> </Link>
</div> </div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-3'> <div className='grid grid-cols-1 gap-4 md:grid-cols-3'>
{testimonials.map(([quote, name, role]) => ( {publicCoachSite.testimonials.map((item) => (
<figure key={name} className={`p-6 ${ui.card}`}> <figure key={item.name} className={`p-6 ${ui.card}`}>
<blockquote className={`leading-8 ${ui.ink}`}> <blockquote className={`leading-8 ${ui.ink}`}>
{quote} {item.quote}
</blockquote> </blockquote>
<figcaption className='mt-6'> <figcaption className='mt-6'>
<p className='font-semibold'>{name}</p> <p className='font-semibold'>{item.name}</p>
<p className={`text-sm ${ui.muted}`}>{role}</p> <p className={`text-sm ${ui.muted}`}>{item.role}</p>
</figcaption> </figcaption>
</figure> </figure>
))} ))}
@ -599,10 +582,10 @@ export default function Starter() {
))} ))}
</div> </div>
<Link <Link
href='/intake/' href={publicCoachSite.assessmentPath}
className={`mt-8 inline-flex rounded-none px-7 py-4 font-semibold ${ui.button}`} className={`mt-8 inline-flex rounded-none px-7 py-4 font-semibold ${ui.button}`}
> >
Create workspace Book assessment
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -20,6 +20,8 @@ type IntakeLead = {
email: string; email: string;
company?: string; company?: string;
role_title?: string; role_title?: string;
package_name?: string;
preferred_time?: string;
goal?: string; goal?: string;
challenge?: string; challenge?: string;
desired_outcome?: string; desired_outcome?: string;
@ -142,6 +144,14 @@ export default function IntakeLeads() {
{lead.role_title || 'Role not provided'} ·{' '} {lead.role_title || 'Role not provided'} ·{' '}
{lead.company || 'Company not provided'} {lead.company || 'Company not provided'}
</p> </p>
<div className='mt-3 flex flex-wrap gap-2'>
<span className='rounded-none border border-[#19192d]/10 bg-white px-3 py-1 text-xs font-semibold text-[#19192d]'>
{lead.package_name || 'Package not selected'}
</span>
<span className='rounded-none border border-[#19192d]/10 bg-white px-3 py-1 text-xs font-semibold text-[#19192d]'>
{lead.preferred_time || 'Preferred time not provided'}
</span>
</div>
<a <a
href={`mailto:${lead.email}`} href={`mailto:${lead.email}`}
className='mt-3 inline-flex items-center gap-2 text-sm font-semibold text-[#35b7a5]' className='mt-3 inline-flex items-center gap-2 text-sm font-semibold text-[#35b7a5]'

View File

@ -2,7 +2,9 @@ import axios from 'axios';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { publicCoachSite } from '../coachingSite';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
@ -14,6 +16,8 @@ type IntakeValues = {
email: string; email: string;
company: string; company: string;
role_title: string; role_title: string;
package_name: string;
preferred_time: string;
goal: string; goal: string;
challenge: string; challenge: string;
desired_outcome: string; desired_outcome: string;
@ -25,12 +29,21 @@ const emptyValues: IntakeValues = {
email: '', email: '',
company: '', company: '',
role_title: '', role_title: '',
package_name: publicCoachSite.packages[0].name,
preferred_time: '',
goal: '', goal: '',
challenge: '', challenge: '',
desired_outcome: '', desired_outcome: '',
consent_ai_notes: false, consent_ai_notes: false,
}; };
const packageOptions = publicCoachSite.packages.map((item) => {
return {
slug: item.slug,
name: item.name,
};
});
function FieldLabel({ function FieldLabel({
label, label,
children, children,
@ -47,10 +60,33 @@ function FieldLabel({
} }
export default function Intake() { export default function Intake() {
const router = useRouter();
const [values, setValues] = React.useState<IntakeValues>(emptyValues); const [values, setValues] = React.useState<IntakeValues>(emptyValues);
const [isSubmitting, setIsSubmitting] = React.useState(false); const [isSubmitting, setIsSubmitting] = React.useState(false);
const [isSubmitted, setIsSubmitted] = React.useState(false); const [isSubmitted, setIsSubmitted] = React.useState(false);
React.useEffect(() => {
if (!router.isReady) {
return;
}
const packageSlug = String(router.query.package || '');
const selectedPackage = packageOptions.find((item) => {
return item.slug === packageSlug;
});
if (!selectedPackage) {
return;
}
setValues((current) => {
return {
...current,
package_name: selectedPackage.name,
};
});
}, [router.isReady, router.query.package]);
function updateValue(field: keyof IntakeValues, value: string | boolean) { function updateValue(field: keyof IntakeValues, value: string | boolean) {
setValues((current) => { setValues((current) => {
return { return {
@ -176,6 +212,35 @@ export default function Intake() {
</FieldLabel> </FieldLabel>
</div> </div>
<div className='grid gap-6 md:grid-cols-2'>
<FieldLabel label='Package / session type'>
<select
required
value={values.package_name}
onChange={(event) =>
updateValue('package_name', event.target.value)
}
className={fieldClass}
>
{packageOptions.map((item) => (
<option key={item.slug} value={item.name}>
{item.name}
</option>
))}
</select>
</FieldLabel>
<FieldLabel label='Preferred time for the first call'>
<input
value={values.preferred_time}
onChange={(event) =>
updateValue('preferred_time', event.target.value)
}
className={fieldClass}
placeholder='Example: Tue mornings, EST'
/>
</FieldLabel>
</div>
<FieldLabel label='What do you want coaching to help with?'> <FieldLabel label='What do you want coaching to help with?'>
<textarea <textarea
required required

View File

@ -51,7 +51,7 @@ export default function Services() {
</h1> </h1>
<p className={`mx-auto mt-7 max-w-3xl text-xl leading-8 ${ui.muted}`}> <p className={`mx-auto mt-7 max-w-3xl text-xl leading-8 ${ui.muted}`}>
Use these cards as starter offers. A coach can rename packages, Use these cards as starter offers. A coach can rename packages,
change pricing, and route every CTA into the intake flow. change pricing, and route every CTA into the booking intake flow.
</p> </p>
</section> </section>
@ -76,8 +76,8 @@ export default function Services() {
</div> </div>
))} ))}
</div> </div>
<Link href='/intake/' className={`mt-7 inline-flex justify-center rounded-none px-6 py-4 font-semibold ${ui.button}`}> <Link href={item.bookingPath} className={`mt-7 inline-flex justify-center rounded-none px-6 py-4 font-semibold ${ui.button}`}>
Start intake {item.bookingLabel}
</Link> </Link>
</div> </div>
))} ))}
@ -125,8 +125,8 @@ export default function Services() {
Start with intake, then convert the lead into a client workspace. Start with intake, then convert the lead into a client workspace.
</h2> </h2>
<div className='mt-8 flex justify-center'> <div className='mt-8 flex justify-center'>
<Link href='/intake/' className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}> <Link href={publicCoachSite.assessmentPath} className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}>
Start assessment Book assessment
</Link> </Link>
</div> </div>
</section> </section>