Skip to content

Commit aefdc21

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

File tree

9 files changed

+558
-24
lines changed

9 files changed

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

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)