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 },
company: { type: DataTypes.TEXT },
role_title: { type: DataTypes.TEXT },
package_name: { type: DataTypes.TEXT },
preferred_time: { type: DataTypes.TEXT },
goal: { type: DataTypes.TEXT },
challenge: { type: DataTypes.TEXT },
desired_outcome: { type: DataTypes.TEXT },

View File

@ -691,6 +691,8 @@ router.post(
status: "active",
goals: lead.goal,
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.desired_outcome && `Desired outcome: ${lead.desired_outcome}`,
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,
company: data.company,
role_title: data.role_title,
package_name: data.package_name,
preferred_time: data.preferred_time,
goal: data.goal,
challenge: data.challenge,
desired_outcome: data.desired_outcome,

View File

@ -1,6 +1,17 @@
export const publicCoachSite = {
workspaceName: 'Coaching SaaS Workspace',
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: {
name: 'Alex Morgan',
title: 'Executive coach for founders, operators, and leadership teams.',
@ -9,8 +20,8 @@ export const publicCoachSite = {
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.',
credentials: [
'Founder and executive coaching',
'Leadership transitions',
'ICF-style executive coaching profile',
'Founder and senior leadership transitions',
'Decision systems and operating rhythm',
'Confidential client workspace',
],
@ -31,8 +42,11 @@ export const publicCoachSite = {
],
packages: [
{
slug: 'leadership-assessment',
name: 'Leadership Assessment',
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.',
items: [
'intake review',
@ -41,8 +55,11 @@ export const publicCoachSite = {
],
},
{
slug: 'founder-coaching',
name: 'Founder Coaching',
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.',
items: [
'two sessions per month',
@ -51,8 +68,11 @@ export const publicCoachSite = {
],
},
{
slug: 'executive-operating-rhythm',
name: 'Executive Operating Rhythm',
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.',
items: [
'leadership themes',
@ -69,4 +89,24 @@ export const publicCoachSite = {
'A private place for client resources',
'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;

View File

@ -1,4 +1,5 @@
import Link from 'next/link';
import { publicCoachSite } from '../coachingSite';
const links = [
{ href: '/how-it-works/', label: 'How it works' },
@ -30,7 +31,7 @@ export default function PublicSiteNav() {
</nav>
<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'
>
Start assessment

View File

@ -55,8 +55,8 @@ export default function AboutCoach() {
{publicCoachSite.coach.aboutIntro}
</p>
<div className='mt-9 flex flex-wrap gap-3'>
<Link href='/intake/' className={`rounded-none px-7 py-4 font-semibold ${ui.button}`}>
Start assessment
<Link href={publicCoachSite.assessmentPath} className={`rounded-none px-7 py-4 font-semibold ${ui.button}`}>
Book assessment
</Link>
<Link
href='/services/'
@ -97,6 +97,24 @@ export default function AboutCoach() {
</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>
</section>
@ -122,8 +140,8 @@ export default function AboutCoach() {
Start with an assessment and bring context into the first call.
</h2>
<div className='mt-8 flex justify-center'>
<Link href='/intake/' className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}>
Start assessment
<Link href={publicCoachSite.assessmentPath} className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}>
Book assessment
</Link>
</div>
</section>

View File

@ -4,6 +4,7 @@ import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import PublicSiteNav from '../components/PublicSiteNav';
import { publicCoachSite } from '../coachingSite';
import LayoutGuest from '../layouts/Guest';
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({
from = '#fffdf9',
to = '#19192d',
@ -199,10 +182,10 @@ export default function Starter() {
</p>
<div className='mt-9 flex justify-center'>
<Link
href='/intake/'
href={publicCoachSite.assessmentPath}
className={`rounded-none px-10 py-5 text-lg font-semibold ${ui.button}`}
>
Start assessment
Book assessment
</Link>
</div>
<p className={`mt-5 text-lg ${ui.muted}`}>
@ -543,21 +526,21 @@ export default function Starter() {
</h2>
</div>
<Link
href='/intake/'
href={publicCoachSite.assessmentPath}
className={`rounded-none px-6 py-3 text-center font-semibold ${ui.button}`}
>
Start your workspace
Book assessment
</Link>
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-3'>
{testimonials.map(([quote, name, role]) => (
<figure key={name} className={`p-6 ${ui.card}`}>
{publicCoachSite.testimonials.map((item) => (
<figure key={item.name} className={`p-6 ${ui.card}`}>
<blockquote className={`leading-8 ${ui.ink}`}>
{quote}
{item.quote}
</blockquote>
<figcaption className='mt-6'>
<p className='font-semibold'>{name}</p>
<p className={`text-sm ${ui.muted}`}>{role}</p>
<p className='font-semibold'>{item.name}</p>
<p className={`text-sm ${ui.muted}`}>{item.role}</p>
</figcaption>
</figure>
))}
@ -599,10 +582,10 @@ export default function Starter() {
))}
</div>
<Link
href='/intake/'
href={publicCoachSite.assessmentPath}
className={`mt-8 inline-flex rounded-none px-7 py-4 font-semibold ${ui.button}`}
>
Create workspace
Book assessment
</Link>
</div>
</div>

View File

@ -20,6 +20,8 @@ type IntakeLead = {
email: string;
company?: string;
role_title?: string;
package_name?: string;
preferred_time?: string;
goal?: string;
challenge?: string;
desired_outcome?: string;
@ -142,6 +144,14 @@ export default function IntakeLeads() {
{lead.role_title || 'Role not provided'} ·{' '}
{lead.company || 'Company not provided'}
</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
href={`mailto:${lead.email}`}
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 Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { publicCoachSite } from '../coachingSite';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
@ -14,6 +16,8 @@ type IntakeValues = {
email: string;
company: string;
role_title: string;
package_name: string;
preferred_time: string;
goal: string;
challenge: string;
desired_outcome: string;
@ -25,12 +29,21 @@ const emptyValues: IntakeValues = {
email: '',
company: '',
role_title: '',
package_name: publicCoachSite.packages[0].name,
preferred_time: '',
goal: '',
challenge: '',
desired_outcome: '',
consent_ai_notes: false,
};
const packageOptions = publicCoachSite.packages.map((item) => {
return {
slug: item.slug,
name: item.name,
};
});
function FieldLabel({
label,
children,
@ -47,10 +60,33 @@ function FieldLabel({
}
export default function Intake() {
const router = useRouter();
const [values, setValues] = React.useState<IntakeValues>(emptyValues);
const [isSubmitting, setIsSubmitting] = 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) {
setValues((current) => {
return {
@ -176,6 +212,35 @@ export default function Intake() {
</FieldLabel>
</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?'>
<textarea
required

View File

@ -51,7 +51,7 @@ export default function Services() {
</h1>
<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,
change pricing, and route every CTA into the intake flow.
change pricing, and route every CTA into the booking intake flow.
</p>
</section>
@ -76,8 +76,8 @@ export default function Services() {
</div>
))}
</div>
<Link href='/intake/' className={`mt-7 inline-flex justify-center rounded-none px-6 py-4 font-semibold ${ui.button}`}>
Start intake
<Link href={item.bookingPath} className={`mt-7 inline-flex justify-center rounded-none px-6 py-4 font-semibold ${ui.button}`}>
{item.bookingLabel}
</Link>
</div>
))}
@ -125,8 +125,8 @@ export default function Services() {
Start with intake, then convert the lead into a client workspace.
</h2>
<div className='mt-8 flex justify-center'>
<Link href='/intake/' className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}>
Start assessment
<Link href={publicCoachSite.assessmentPath} className={`rounded-none px-8 py-4 font-semibold ${ui.button}`}>
Book assessment
</Link>
</div>
</section>