Skip to content

Commit e37f9fe

Browse files
author
Theodore Li
committed
feat(ui): add request a demo modal
1 parent 668b948 commit e37f9fe

File tree

11 files changed

+565
-45
lines changed

11 files changed

+565
-45
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
'use client'
2+
3+
import { useCallback, useMemo, useState } from 'react'
4+
import {
5+
Button,
6+
Combobox,
7+
FormField,
8+
Input,
9+
Modal,
10+
ModalBody,
11+
ModalContent,
12+
ModalFooter,
13+
ModalHeader,
14+
ModalTitle,
15+
ModalTrigger,
16+
Textarea,
17+
} from '@/components/emcn'
18+
import {
19+
DEMO_REQUEST_REGION_OPTIONS,
20+
DEMO_REQUEST_USER_COUNT_OPTIONS,
21+
type DemoRequestPayload,
22+
demoRequestSchema,
23+
} from '@/lib/marketing/demo-request'
24+
25+
interface DemoRequestModalProps {
26+
children: React.ReactNode
27+
theme?: 'dark' | 'light'
28+
}
29+
30+
type DemoRequestField = keyof DemoRequestPayload
31+
type DemoRequestErrors = Partial<Record<DemoRequestField, string>>
32+
33+
interface DemoRequestFormState {
34+
firstName: string
35+
lastName: string
36+
companyEmail: string
37+
phoneNumber: string
38+
region: DemoRequestPayload['region'] | ''
39+
userCount: DemoRequestPayload['userCount'] | ''
40+
details: string
41+
}
42+
43+
const INITIAL_FORM_STATE: DemoRequestFormState = {
44+
firstName: '',
45+
lastName: '',
46+
companyEmail: '',
47+
phoneNumber: '',
48+
region: '',
49+
userCount: '',
50+
details: '',
51+
}
52+
53+
export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalProps) {
54+
const [open, setOpen] = useState(false)
55+
const [form, setForm] = useState<DemoRequestFormState>(INITIAL_FORM_STATE)
56+
const [errors, setErrors] = useState<DemoRequestErrors>({})
57+
const [isSubmitting, setIsSubmitting] = useState(false)
58+
const [submitError, setSubmitError] = useState<string | null>(null)
59+
const [submitSuccess, setSubmitSuccess] = useState<string | null>(null)
60+
61+
const comboboxRegions = useMemo(() => [...DEMO_REQUEST_REGION_OPTIONS], [])
62+
const comboboxUserCounts = useMemo(() => [...DEMO_REQUEST_USER_COUNT_OPTIONS], [])
63+
64+
const resetForm = useCallback(() => {
65+
setForm(INITIAL_FORM_STATE)
66+
setErrors({})
67+
setIsSubmitting(false)
68+
setSubmitError(null)
69+
setSubmitSuccess(null)
70+
}, [])
71+
72+
const handleOpenChange = useCallback(
73+
(nextOpen: boolean) => {
74+
setOpen(nextOpen)
75+
if (!nextOpen) {
76+
resetForm()
77+
}
78+
},
79+
[resetForm]
80+
)
81+
82+
const updateField = useCallback(
83+
<TField extends keyof DemoRequestFormState>(
84+
field: TField,
85+
value: DemoRequestFormState[TField]
86+
) => {
87+
setForm((prev) => ({ ...prev, [field]: value }))
88+
setErrors((prev) => {
89+
if (!prev[field]) {
90+
return prev
91+
}
92+
93+
const nextErrors = { ...prev }
94+
delete nextErrors[field]
95+
return nextErrors
96+
})
97+
setSubmitError(null)
98+
setSubmitSuccess(null)
99+
},
100+
[]
101+
)
102+
103+
const handleSubmit = useCallback(
104+
async (event: React.FormEvent<HTMLFormElement>) => {
105+
event.preventDefault()
106+
setSubmitError(null)
107+
setSubmitSuccess(null)
108+
109+
const parsed = demoRequestSchema.safeParse({
110+
...form,
111+
phoneNumber: form.phoneNumber || undefined,
112+
})
113+
114+
if (!parsed.success) {
115+
const fieldErrors = parsed.error.flatten().fieldErrors
116+
setErrors({
117+
firstName: fieldErrors.firstName?.[0],
118+
lastName: fieldErrors.lastName?.[0],
119+
companyEmail: fieldErrors.companyEmail?.[0],
120+
phoneNumber: fieldErrors.phoneNumber?.[0],
121+
region: fieldErrors.region?.[0],
122+
userCount: fieldErrors.userCount?.[0],
123+
details: fieldErrors.details?.[0],
124+
})
125+
return
126+
}
127+
128+
setIsSubmitting(true)
129+
130+
try {
131+
const response = await fetch('/api/demo-request', {
132+
method: 'POST',
133+
headers: { 'Content-Type': 'application/json' },
134+
body: JSON.stringify(parsed.data),
135+
})
136+
137+
const result = (await response.json().catch(() => null)) as {
138+
error?: string
139+
message?: string
140+
} | null
141+
142+
if (!response.ok) {
143+
throw new Error(result?.error || 'Failed to submit demo request')
144+
}
145+
146+
resetForm()
147+
setSubmitSuccess(result?.message || 'Request received. We will be in touch shortly.')
148+
} catch (error) {
149+
setSubmitError(
150+
error instanceof Error
151+
? error.message
152+
: 'Failed to submit demo request. Please try again.'
153+
)
154+
} finally {
155+
setIsSubmitting(false)
156+
}
157+
},
158+
[form, resetForm]
159+
)
160+
161+
return (
162+
<Modal open={open} onOpenChange={handleOpenChange}>
163+
<ModalTrigger asChild>{children}</ModalTrigger>
164+
<ModalContent size='lg' className={theme === 'dark' ? 'dark' : undefined}>
165+
<ModalHeader>
166+
<ModalTitle>Nearly there!</ModalTitle>
167+
</ModalHeader>
168+
<form onSubmit={handleSubmit}>
169+
<ModalBody>
170+
<div className='space-y-4'>
171+
<div className='grid gap-4 sm:grid-cols-2'>
172+
<FormField htmlFor='firstName' label='First name' error={errors.firstName}>
173+
<Input
174+
id='firstName'
175+
value={form.firstName}
176+
onChange={(event) => updateField('firstName', event.target.value)}
177+
placeholder='First'
178+
/>
179+
</FormField>
180+
<FormField htmlFor='lastName' label='Last name' error={errors.lastName}>
181+
<Input
182+
id='lastName'
183+
value={form.lastName}
184+
onChange={(event) => updateField('lastName', event.target.value)}
185+
placeholder='Last'
186+
/>
187+
</FormField>
188+
</div>
189+
190+
<FormField htmlFor='companyEmail' label='Company email' error={errors.companyEmail}>
191+
<Input
192+
id='companyEmail'
193+
type='email'
194+
value={form.companyEmail}
195+
onChange={(event) => updateField('companyEmail', event.target.value)}
196+
placeholder='Your work email'
197+
/>
198+
</FormField>
199+
200+
<FormField
201+
htmlFor='phoneNumber'
202+
label='Phone number'
203+
optional
204+
error={errors.phoneNumber}
205+
>
206+
<Input
207+
id='phoneNumber'
208+
type='tel'
209+
value={form.phoneNumber}
210+
onChange={(event) => updateField('phoneNumber', event.target.value)}
211+
placeholder='Your phone number'
212+
/>
213+
</FormField>
214+
215+
<div className='grid gap-4 sm:grid-cols-2'>
216+
<FormField htmlFor='region' label='Region' error={errors.region}>
217+
<Combobox
218+
options={comboboxRegions}
219+
value={form.region}
220+
selectedValue={form.region}
221+
onChange={(value) =>
222+
updateField('region', value as DemoRequestPayload['region'])
223+
}
224+
placeholder='Select'
225+
editable={false}
226+
filterOptions={false}
227+
/>
228+
</FormField>
229+
<FormField htmlFor='userCount' label='Number of users' error={errors.userCount}>
230+
<Combobox
231+
options={comboboxUserCounts}
232+
value={form.userCount}
233+
selectedValue={form.userCount}
234+
onChange={(value) =>
235+
updateField('userCount', value as DemoRequestPayload['userCount'])
236+
}
237+
placeholder='Select'
238+
editable={false}
239+
filterOptions={false}
240+
/>
241+
</FormField>
242+
</div>
243+
244+
<FormField htmlFor='details' label='Details' error={errors.details}>
245+
<Textarea
246+
id='details'
247+
value={form.details}
248+
onChange={(event) => updateField('details', event.target.value)}
249+
placeholder='Tell us about your needs and questions'
250+
/>
251+
</FormField>
252+
</div>
253+
</ModalBody>
254+
255+
<ModalFooter className='flex-col items-stretch gap-3'>
256+
{submitError && <p className='text-[13px] text-[var(--text-error)]'>{submitError}</p>}
257+
{submitSuccess && <p className='text-[#1E8E3E] text-[13px]'>{submitSuccess}</p>}
258+
<Button type='submit' variant='primary' disabled={isSubmitting}>
259+
{isSubmitting ? 'Submitting...' : 'Submit'}
260+
</Button>
261+
</ModalFooter>
262+
</form>
263+
</ModalContent>
264+
</Modal>
265+
)
266+
}

apps/sim/app/(home)/components/enterprise/enterprise.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -210,24 +210,24 @@ export default function Enterprise() {
210210
rel='noopener noreferrer'
211211
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-white bg-white px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
212212
>
213-
Book a demo
214-
<span className='relative h-[10px] w-[10px] shrink-0'>
215-
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
216-
<svg
217-
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
218-
viewBox='0 0 10 10'
219-
fill='none'
220-
>
221-
<path
222-
d='M1 5H8M5.5 2L8.5 5L5.5 8'
223-
stroke='currentColor'
224-
strokeWidth='1.33'
225-
strokeLinecap='square'
226-
strokeLinejoin='miter'
213+
Book a demo
214+
<span className='relative h-[10px] w-[10px] shrink-0'>
215+
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
216+
<svg
217+
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
218+
viewBox='0 0 10 10'
227219
fill='none'
228-
/>
229-
</svg>
230-
</span>
220+
>
221+
<path
222+
d='M1 5H8M5.5 2L8.5 5L5.5 8'
223+
stroke='currentColor'
224+
strokeWidth='1.33'
225+
strokeLinecap='square'
226+
strokeLinejoin='miter'
227+
fill='none'
228+
/>
229+
</svg>
230+
</span>
231231
</Link>
232232
</div>
233233
</div>

apps/sim/app/(home)/components/footer/footer.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Image from 'next/image'
22
import Link from 'next/link'
3+
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
34
import { FooterCTA } from '@/app/(home)/components/footer/footer-cta'
45

56
const LINK_CLASS = 'text-[14px] text-[#999] transition-colors hover:text-[#ECECEC]'
@@ -8,11 +9,12 @@ interface FooterItem {
89
label: string
910
href: string
1011
external?: boolean
12+
action?: 'demo-request'
1113
}
1214

1315
const PRODUCT_LINKS: FooterItem[] = [
1416
{ label: 'Pricing', href: '/#pricing' },
15-
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
17+
{ label: 'Enterprise', href: '#', action: 'demo-request' },
1618
{ label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
1719
{ label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
1820
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
@@ -83,8 +85,14 @@ function FooterColumn({ title, items }: { title: string; items: FooterItem[] })
8385
<div>
8486
<h3 className='mb-[16px] font-medium text-[#ECECEC] text-[14px]'>{title}</h3>
8587
<div className='flex flex-col gap-[10px]'>
86-
{items.map(({ label, href, external }) =>
87-
external ? (
88+
{items.map(({ label, href, external, action }) =>
89+
action === 'demo-request' ? (
90+
<DemoRequestModal key={label}>
91+
<button type='button' className={`${LINK_CLASS} bg-transparent text-left`}>
92+
{label}
93+
</button>
94+
</DemoRequestModal>
95+
) : external ? (
8896
<a
8997
key={label}
9098
href={href}

apps/sim/app/(home)/components/hero/hero.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dynamic from 'next/dynamic'
44
import Image from 'next/image'
55
import Link from 'next/link'
6+
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
67
import {
78
BlocksLeftAnimated,
89
BlocksRightAnimated,
@@ -70,15 +71,15 @@ export default function Hero() {
7071
</p>
7172

7273
<div className='mt-[12px] flex items-center gap-[8px]'>
73-
<a
74-
href='https://form.typeform.com/to/jqCO12pF'
75-
target='_blank'
76-
rel='noopener noreferrer'
77-
className={`${CTA_BASE} border-[#3d3d3d] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
78-
aria-label='Get a demo'
79-
>
80-
Get a demo
81-
</a>
74+
<DemoRequestModal>
75+
<button
76+
type='button'
77+
className={`${CTA_BASE} border-[#3d3d3d] bg-transparent text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
78+
aria-label='Get a demo'
79+
>
80+
Get a demo
81+
</button>
82+
</DemoRequestModal>
8283
<Link
8384
href='/signup'
8485
className={`${CTA_BASE} gap-[8px] border-[#FFFFFF] bg-[#FFFFFF] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}

0 commit comments

Comments
 (0)