feat: add package booking intake flow
This commit is contained in:
parent
94bd7fcb30
commit
2757e36bdd
@ -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');
|
||||
},
|
||||
};
|
||||
@ -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 },
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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]'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user