Add coaching intake lead flow
This commit is contained in:
parent
677724e9f6
commit
2a10483480
@ -6,7 +6,6 @@ const passport = require('passport');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const db = require('./db/models');
|
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const swaggerUI = require('swagger-ui-express');
|
const swaggerUI = require('swagger-ui-express');
|
||||||
const swaggerJsDoc = require('swagger-jsdoc');
|
const swaggerJsDoc = require('swagger-jsdoc');
|
||||||
@ -18,6 +17,7 @@ const pexelsRoutes = require('./routes/pexels');
|
|||||||
|
|
||||||
const openaiRoutes = require('./routes/openai');
|
const openaiRoutes = require('./routes/openai');
|
||||||
const coachingRoutes = require('./routes/coaching');
|
const coachingRoutes = require('./routes/coaching');
|
||||||
|
const coachingPublicRoutes = require('./routes/coachingPublic');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -81,6 +81,7 @@ app.use(bodyParser.json());
|
|||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/file', fileRoutes);
|
app.use('/api/file', fileRoutes);
|
||||||
app.use('/api/pexels', pexelsRoutes);
|
app.use('/api/pexels', pexelsRoutes);
|
||||||
|
app.use('/api/coaching-public', coachingPublicRoutes);
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,75 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/intake-leads",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const leads = await db.intake_leads.findAll({
|
||||||
|
order: [["createdAt", "DESC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(leads);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/intake-leads/:id/status",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const lead = await db.intake_leads.findByPk(req.params.id);
|
||||||
|
|
||||||
|
if (!lead) {
|
||||||
|
res.status(404).send({ error: "lead_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await lead.update({
|
||||||
|
status: req.body.status,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(lead);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/intake-leads/:id/convert",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const lead = await db.intake_leads.findByPk(req.params.id);
|
||||||
|
|
||||||
|
if (!lead) {
|
||||||
|
res.status(404).send({ error: "lead_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await db.clients.create({
|
||||||
|
name: lead.name,
|
||||||
|
email: lead.email,
|
||||||
|
company: lead.company,
|
||||||
|
role_title: lead.role_title,
|
||||||
|
status: "active",
|
||||||
|
goals: lead.goal,
|
||||||
|
notes: [
|
||||||
|
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.",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
|
tags: "intake",
|
||||||
|
ownerId: req.currentUser.id,
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await lead.update({
|
||||||
|
status: "converted",
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send({ lead, client });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/clients",
|
"/clients",
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
|
|||||||
41
backend/src/routes/coachingPublic.js
Normal file
41
backend/src/routes/coachingPublic.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const db = require("../db/models");
|
||||||
|
const wrapAsync = require("../helpers").wrapAsync;
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/intake",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const data = req.body || {};
|
||||||
|
const name = String(data.name || "").trim();
|
||||||
|
const email = String(data.email || "").trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
res.status(400).send({ error: "name_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
res.status(400).send({ error: "email_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lead = await db.intake_leads.create({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
company: data.company,
|
||||||
|
role_title: data.role_title,
|
||||||
|
goal: data.goal,
|
||||||
|
challenge: data.challenge,
|
||||||
|
desired_outcome: data.desired_outcome,
|
||||||
|
source: data.source || "website",
|
||||||
|
consent_ai_notes: Boolean(data.consent_ai_notes),
|
||||||
|
status: "new",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(lead);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -4,6 +4,7 @@ import {
|
|||||||
mdiBookOpenVariant,
|
mdiBookOpenVariant,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
mdiFileDocumentEditOutline,
|
mdiFileDocumentEditOutline,
|
||||||
|
mdiFormTextbox,
|
||||||
mdiLogout,
|
mdiLogout,
|
||||||
mdiMenu,
|
mdiMenu,
|
||||||
mdiShieldAccountOutline,
|
mdiShieldAccountOutline,
|
||||||
@ -39,6 +40,11 @@ const navItems = [
|
|||||||
icon: mdiFileDocumentEditOutline,
|
icon: mdiFileDocumentEditOutline,
|
||||||
label: 'Session Memory',
|
label: 'Session Memory',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/intake-leads',
|
||||||
|
icon: mdiFormTextbox,
|
||||||
|
label: 'Intake Leads',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/client-portal',
|
href: '/client-portal',
|
||||||
icon: mdiBookOpenVariant,
|
icon: mdiBookOpenVariant,
|
||||||
|
|||||||
@ -149,7 +149,7 @@ function Nav() {
|
|||||||
<a href='#control'>Trust</a>
|
<a href='#control'>Trust</a>
|
||||||
</nav>
|
</nav>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`rounded-full px-5 py-2.5 text-sm font-semibold ${ui.button}`}
|
className={`rounded-full px-5 py-2.5 text-sm font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start free
|
Start free
|
||||||
@ -440,7 +440,7 @@ export default function HowItWorks() {
|
|||||||
</p>
|
</p>
|
||||||
<div className='mt-9 flex flex-wrap justify-center gap-4'>
|
<div className='mt-9 flex flex-wrap justify-center gap-4'>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`rounded-full px-8 py-4 font-semibold ${ui.button}`}
|
className={`rounded-full px-8 py-4 font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start free
|
Start free
|
||||||
@ -624,7 +624,7 @@ export default function HowItWorks() {
|
|||||||
client portal, and approved follow-up prompts.
|
client portal, and approved follow-up prompts.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`mt-8 inline-flex rounded-full px-7 py-4 font-semibold ${ui.button}`}
|
className={`mt-8 inline-flex rounded-full px-7 py-4 font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start free
|
Start free
|
||||||
@ -663,7 +663,7 @@ export default function HowItWorks() {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className='mt-8 flex justify-center'>
|
<div className='mt-8 flex justify-center'>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`rounded-full px-8 py-4 font-semibold ${ui.button}`}
|
className={`rounded-full px-8 py-4 font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start free
|
Start free
|
||||||
|
|||||||
@ -179,7 +179,7 @@ export default function Starter() {
|
|||||||
<a href='#pricing'>Packages</a>
|
<a href='#pricing'>Packages</a>
|
||||||
</nav>
|
</nav>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`rounded-full px-5 py-2.5 text-sm font-semibold ${ui.button}`}
|
className={`rounded-full px-5 py-2.5 text-sm font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start free
|
Start free
|
||||||
@ -207,7 +207,7 @@ export default function Starter() {
|
|||||||
</p>
|
</p>
|
||||||
<div className='mt-9 flex justify-center'>
|
<div className='mt-9 flex justify-center'>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`rounded-full px-10 py-5 text-lg font-semibold ${ui.button}`}
|
className={`rounded-full px-10 py-5 text-lg font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start free
|
Start free
|
||||||
@ -503,7 +503,7 @@ export default function Starter() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`rounded-full px-6 py-3 text-center font-semibold ${ui.button}`}
|
className={`rounded-full px-6 py-3 text-center font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start your workspace
|
Start your workspace
|
||||||
@ -559,7 +559,7 @@ export default function Starter() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`mt-8 inline-flex rounded-full px-7 py-4 font-semibold ${ui.button}`}
|
className={`mt-8 inline-flex rounded-full px-7 py-4 font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Create workspace
|
Create workspace
|
||||||
|
|||||||
205
frontend/src/pages/intake-leads.tsx
Normal file
205
frontend/src/pages/intake-leads.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import {
|
||||||
|
mdiAccountConvertOutline,
|
||||||
|
mdiArchiveOutline,
|
||||||
|
mdiEmailOutline,
|
||||||
|
mdiFormTextbox,
|
||||||
|
} from '@mdi/js';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import React from 'react';
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
|
||||||
|
type IntakeLead = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
company?: string;
|
||||||
|
role_title?: string;
|
||||||
|
goal?: string;
|
||||||
|
challenge?: string;
|
||||||
|
desired_outcome?: string;
|
||||||
|
status: string;
|
||||||
|
consent_ai_notes?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Panel({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`rounded-lg border border-[#19192d]/10 bg-white ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IntakeLeads() {
|
||||||
|
const [leads, setLeads] = React.useState<IntakeLead[]>([]);
|
||||||
|
const [updatingLeadId, setUpdatingLeadId] = React.useState('');
|
||||||
|
|
||||||
|
async function loadLeads() {
|
||||||
|
const response = await axios.get('/coaching/intake-leads');
|
||||||
|
setLeads(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadLeads();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function convertLead(leadId: string) {
|
||||||
|
setUpdatingLeadId(leadId);
|
||||||
|
await axios.post(`/coaching/intake-leads/${leadId}/convert`);
|
||||||
|
await loadLeads();
|
||||||
|
setUpdatingLeadId('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function archiveLead(leadId: string) {
|
||||||
|
setUpdatingLeadId(leadId);
|
||||||
|
const response = await axios.patch(`/coaching/intake-leads/${leadId}/status`, {
|
||||||
|
status: 'archived',
|
||||||
|
});
|
||||||
|
setLeads((current) =>
|
||||||
|
current.map((lead) => {
|
||||||
|
if (lead.id === leadId) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lead;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setUpdatingLeadId('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Intake Leads')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<div className='mx-auto max-w-7xl'>
|
||||||
|
<div className='mb-4 rounded-lg bg-[#19192d] p-5 text-white'>
|
||||||
|
<div className='flex items-center gap-3 text-[#b17a1e]'>
|
||||||
|
<BaseIcon path={mdiFormTextbox} size={18} />
|
||||||
|
<span className='text-xs font-semibold uppercase tracking-[0.22em]'>
|
||||||
|
Intake
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className='mt-3 text-xl font-semibold'>Intake leads</h1>
|
||||||
|
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
|
||||||
|
Review website submissions and convert good fits into client
|
||||||
|
records.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-4'>
|
||||||
|
{leads.map((lead) => (
|
||||||
|
<Panel key={lead.id} className='p-5'>
|
||||||
|
<div className='flex flex-col justify-between gap-4 lg:flex-row lg:items-start'>
|
||||||
|
<div>
|
||||||
|
<div className='flex flex-wrap items-center gap-3'>
|
||||||
|
<h2 className='text-lg font-semibold text-[#19192d]'>
|
||||||
|
{lead.name}
|
||||||
|
</h2>
|
||||||
|
<span className='rounded-full bg-[#fbf8f1] px-3 py-1 text-xs font-semibold text-[#b17a1e]'>
|
||||||
|
{lead.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className='mt-1 text-sm text-[#72798a]'>
|
||||||
|
{lead.role_title || 'Role not provided'} ·{' '}
|
||||||
|
{lead.company || 'Company not provided'}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={`mailto:${lead.email}`}
|
||||||
|
className='mt-3 inline-flex items-center gap-2 text-sm font-semibold text-[#35b7a5]'
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiEmailOutline} size={18} />
|
||||||
|
{lead.email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{lead.status !== 'converted' && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
disabled={updatingLeadId === lead.id}
|
||||||
|
onClick={() => convertLead(lead.id)}
|
||||||
|
className='inline-flex items-center gap-2 rounded-full bg-[#35b7a5] px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiAccountConvertOutline} size={18} />
|
||||||
|
Convert to client
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{lead.status !== 'archived' && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
disabled={updatingLeadId === lead.id}
|
||||||
|
onClick={() => archiveLead(lead.id)}
|
||||||
|
className='inline-flex items-center gap-2 rounded-full border border-[#19192d]/10 bg-white px-3 py-1.5 text-sm font-semibold text-[#19192d] disabled:opacity-50'
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiArchiveOutline} size={18} />
|
||||||
|
Archive
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-5 grid gap-4 lg:grid-cols-3'>
|
||||||
|
<div className='rounded-lg bg-[#fffdf9] p-4'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#b17a1e]'>
|
||||||
|
Goal
|
||||||
|
</p>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-[#72798a]'>
|
||||||
|
{lead.goal || 'No goal provided.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-lg bg-[#fffdf9] p-4'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#b17a1e]'>
|
||||||
|
Challenge
|
||||||
|
</p>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-[#72798a]'>
|
||||||
|
{lead.challenge || 'No challenge provided.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-lg bg-[#fffdf9] p-4'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#b17a1e]'>
|
||||||
|
Desired outcome
|
||||||
|
</p>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-[#72798a]'>
|
||||||
|
{lead.desired_outcome || 'No outcome provided.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{leads.length === 0 && (
|
||||||
|
<Panel className='p-5'>
|
||||||
|
<p className='text-sm text-[#72798a]'>
|
||||||
|
No intake leads yet. Share the public intake page from{' '}
|
||||||
|
<Link href='/intake' className='font-semibold text-[#35b7a5]'>
|
||||||
|
/intake
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IntakeLeads.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
231
frontend/src/pages/intake.tsx
Normal file
231
frontend/src/pages/intake.tsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import React from 'react';
|
||||||
|
import LayoutGuest from '../layouts/Guest';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
|
const fieldClass =
|
||||||
|
'mt-2 w-full rounded-lg border border-[#19192d]/10 bg-white px-3 py-2 text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15';
|
||||||
|
|
||||||
|
type IntakeValues = {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
company: string;
|
||||||
|
role_title: string;
|
||||||
|
goal: string;
|
||||||
|
challenge: string;
|
||||||
|
desired_outcome: string;
|
||||||
|
consent_ai_notes: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyValues: IntakeValues = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
company: '',
|
||||||
|
role_title: '',
|
||||||
|
goal: '',
|
||||||
|
challenge: '',
|
||||||
|
desired_outcome: '',
|
||||||
|
consent_ai_notes: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function FieldLabel({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className='block'>
|
||||||
|
<span className='text-sm font-semibold text-[#72798a]'>{label}</span>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Intake() {
|
||||||
|
const [values, setValues] = React.useState<IntakeValues>(emptyValues);
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
|
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
||||||
|
|
||||||
|
function updateValue(field: keyof IntakeValues, value: string | boolean) {
|
||||||
|
setValues((current) => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitIntake(event: React.FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
await axios.post('/coaching-public/intake', {
|
||||||
|
...values,
|
||||||
|
source: 'website',
|
||||||
|
});
|
||||||
|
setIsSubmitted(true);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Start coaching')}</title>
|
||||||
|
</Head>
|
||||||
|
<main className='min-h-screen bg-[#fffdf9] text-[#19192d]'>
|
||||||
|
<header className='px-5 py-5'>
|
||||||
|
<div className='mx-auto flex max-w-6xl items-center justify-between'>
|
||||||
|
<Link href='/' className='font-semibold'>
|
||||||
|
Coaching SaaS Workspace
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href='/login/'
|
||||||
|
className='rounded-full border border-[#19192d]/10 bg-white px-4 py-2 text-sm font-semibold'
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className='mx-auto grid max-w-6xl gap-8 px-5 py-10 lg:grid-cols-[0.85fr_1.15fr]'>
|
||||||
|
<div>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
||||||
|
Coaching intake
|
||||||
|
</p>
|
||||||
|
<h1 className='mt-4 font-serif text-5xl font-semibold leading-tight md:text-6xl'>
|
||||||
|
Start with the coaching context that matters.
|
||||||
|
</h1>
|
||||||
|
<p className='mt-5 max-w-xl text-lg leading-8 text-[#72798a]'>
|
||||||
|
Share what you want to work on. Your coach can review this,
|
||||||
|
create a client record, and prepare the first session around your
|
||||||
|
goals.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rounded-lg border border-[#19192d]/10 bg-white p-5'>
|
||||||
|
{isSubmitted ? (
|
||||||
|
<div className='rounded-lg bg-[#f3fbf8] p-5'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
||||||
|
Received
|
||||||
|
</p>
|
||||||
|
<h2 className='mt-3 text-2xl font-semibold'>
|
||||||
|
Thanks, we have your intake.
|
||||||
|
</h2>
|
||||||
|
<p className='mt-3 leading-7 text-[#72798a]'>
|
||||||
|
Your coach can now review it and create your client workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form className='space-y-4' onSubmit={submitIntake}>
|
||||||
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
|
<FieldLabel label='Name'>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
value={values.name}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateValue('name', event.target.value)
|
||||||
|
}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel label='Email'>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type='email'
|
||||||
|
value={values.email}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateValue('email', event.target.value)
|
||||||
|
}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
|
<FieldLabel label='Company'>
|
||||||
|
<input
|
||||||
|
value={values.company}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateValue('company', event.target.value)
|
||||||
|
}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel label='Role'>
|
||||||
|
<input
|
||||||
|
value={values.role_title}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateValue('role_title', event.target.value)
|
||||||
|
}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldLabel label='What do you want coaching to help with?'>
|
||||||
|
<textarea
|
||||||
|
required
|
||||||
|
rows={4}
|
||||||
|
value={values.goal}
|
||||||
|
onChange={(event) => updateValue('goal', event.target.value)}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
</FieldLabel>
|
||||||
|
|
||||||
|
<FieldLabel label='What feels stuck right now?'>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
value={values.challenge}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateValue('challenge', event.target.value)
|
||||||
|
}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
</FieldLabel>
|
||||||
|
|
||||||
|
<FieldLabel label='What would a useful first outcome look like?'>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
value={values.desired_outcome}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateValue('desired_outcome', event.target.value)
|
||||||
|
}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
</FieldLabel>
|
||||||
|
|
||||||
|
<label className='flex gap-3 rounded-lg bg-[#fbf8f1] p-4 text-sm leading-6 text-[#72798a]'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={values.consent_ai_notes}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateValue('consent_ai_notes', event.target.checked)
|
||||||
|
}
|
||||||
|
className='mt-1'
|
||||||
|
/>
|
||||||
|
I am comfortable with AI-assisted summaries and prep notes
|
||||||
|
being used by the coach inside this workspace.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='submit'
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className='rounded-full bg-[#35b7a5] px-5 py-2.5 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Send intake'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Intake.getLayout = function getLayout(page: React.ReactElement) {
|
||||||
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
|
};
|
||||||
@ -84,7 +84,7 @@ function Nav() {
|
|||||||
<Link href='/#trust'>Trust</Link>
|
<Link href='/#trust'>Trust</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<Link
|
<Link
|
||||||
href='/register/'
|
href='/intake/'
|
||||||
className={`rounded-full px-5 py-2.5 text-sm font-semibold ${ui.button}`}
|
className={`rounded-full px-5 py-2.5 text-sm font-semibold ${ui.button}`}
|
||||||
>
|
>
|
||||||
Start free
|
Start free
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user