Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix(ui): add request a demo modal
  • Loading branch information
Theodore Li committed Mar 25, 2026
commit aefdc21f0d959de34edc50737ae3c7ffa593f29b
303 changes: 303 additions & 0 deletions apps/sim/app/(home)/components/demo-request/demo-request-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
'use client'

import { useCallback, useMemo, useState } from 'react'
import {
Button,
Combobox,
FormField,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalTitle,
ModalTrigger,
Textarea,
} from '@/components/emcn'
import { Check } from '@/components/emcn/icons'
import {
DEMO_REQUEST_REGION_OPTIONS,
DEMO_REQUEST_USER_COUNT_OPTIONS,
type DemoRequestPayload,
demoRequestSchema,
} from '@/lib/marketing/demo-request'

interface DemoRequestModalProps {
children: React.ReactNode
theme?: 'dark' | 'light'
}

type DemoRequestField = keyof DemoRequestPayload
type DemoRequestErrors = Partial<Record<DemoRequestField, string>>

interface DemoRequestFormState {
firstName: string
lastName: string
companyEmail: string
phoneNumber: string
region: DemoRequestPayload['region'] | ''
userCount: DemoRequestPayload['userCount'] | ''
details: string
}

const SUBMIT_SUCCESS_MESSAGE = "We'll be in touch soon!"

const INITIAL_FORM_STATE: DemoRequestFormState = {
firstName: '',
lastName: '',
companyEmail: '',
phoneNumber: '',
region: '',
userCount: '',
details: '',
}

export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalProps) {
const [open, setOpen] = useState(false)
const [form, setForm] = useState<DemoRequestFormState>(INITIAL_FORM_STATE)
const [errors, setErrors] = useState<DemoRequestErrors>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitError, setSubmitError] = useState<string | null>(null)
const [submitSuccess, setSubmitSuccess] = useState(false)

const comboboxRegions = useMemo(() => [...DEMO_REQUEST_REGION_OPTIONS], [])
const comboboxUserCounts = useMemo(() => [...DEMO_REQUEST_USER_COUNT_OPTIONS], [])
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

const resetForm = useCallback(() => {
setForm(INITIAL_FORM_STATE)
setErrors({})
setIsSubmitting(false)
setSubmitError(null)
setSubmitSuccess(false)
}, [])

const handleOpenChange = useCallback(
(nextOpen: boolean) => {
setOpen(nextOpen)
if (!nextOpen) {
resetForm()
}
},
[resetForm]
)
Comment thread
waleedlatif1 marked this conversation as resolved.

const updateField = useCallback(
<TField extends keyof DemoRequestFormState>(
field: TField,
value: DemoRequestFormState[TField]
) => {
setForm((prev) => ({ ...prev, [field]: value }))
setErrors((prev) => {
if (!prev[field]) {
return prev
}

const nextErrors = { ...prev }
delete nextErrors[field]
return nextErrors
})
setSubmitError(null)
setSubmitSuccess(false)
},
[]
)

const handleSubmit = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setSubmitError(null)
setSubmitSuccess(false)

const parsed = demoRequestSchema.safeParse({
...form,
phoneNumber: form.phoneNumber || undefined,
})

if (!parsed.success) {
const fieldErrors = parsed.error.flatten().fieldErrors
setErrors({
firstName: fieldErrors.firstName?.[0],
lastName: fieldErrors.lastName?.[0],
companyEmail: fieldErrors.companyEmail?.[0],
phoneNumber: fieldErrors.phoneNumber?.[0],
region: fieldErrors.region?.[0],
userCount: fieldErrors.userCount?.[0],
details: fieldErrors.details?.[0],
})
return
}

setIsSubmitting(true)

try {
const response = await fetch('/api/demo-request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsed.data),
})

const result = (await response.json().catch(() => null)) as {
error?: string
message?: string
} | null

if (!response.ok) {
throw new Error(result?.error || 'Failed to submit demo request')
}

resetForm()
setSubmitSuccess(true)
Comment thread
TheodoreSpeaks marked this conversation as resolved.
Outdated
} catch (error) {
setSubmitError(
error instanceof Error
? error.message
: 'Failed to submit demo request. Please try again.'
)
} finally {
setIsSubmitting(false)
}
},
[form, resetForm]
)

return (
<Modal open={open} onOpenChange={handleOpenChange}>
<ModalTrigger asChild>{children}</ModalTrigger>
<ModalContent size='lg' className={theme === 'dark' ? 'dark' : undefined}>
<ModalHeader>
<ModalTitle className={submitSuccess ? 'sr-only' : undefined}>
{submitSuccess ? 'Demo request submitted' : 'Nearly there!'}
</ModalTitle>
</ModalHeader>
Comment thread
waleedlatif1 marked this conversation as resolved.
<div className='relative flex-1'>
<form
onSubmit={handleSubmit}
aria-hidden={submitSuccess}
className={
submitSuccess
? 'pointer-events-none invisible flex h-full flex-col'
: 'flex h-full flex-col'
}
>
<ModalBody>
<div className='space-y-4'>
<div className='grid gap-4 sm:grid-cols-2'>
<FormField htmlFor='firstName' label='First name' error={errors.firstName}>
<Input
id='firstName'
value={form.firstName}
onChange={(event) => updateField('firstName', event.target.value)}
placeholder='First'
/>
</FormField>
<FormField htmlFor='lastName' label='Last name' error={errors.lastName}>
<Input
id='lastName'
value={form.lastName}
onChange={(event) => updateField('lastName', event.target.value)}
placeholder='Last'
/>
</FormField>
</div>

<FormField htmlFor='companyEmail' label='Company email' error={errors.companyEmail}>
<Input
id='companyEmail'
type='email'
value={form.companyEmail}
onChange={(event) => updateField('companyEmail', event.target.value)}
placeholder='Your work email'
/>
</FormField>

<FormField
htmlFor='phoneNumber'
label='Phone number'
optional
error={errors.phoneNumber}
>
<Input
id='phoneNumber'
type='tel'
value={form.phoneNumber}
onChange={(event) => updateField('phoneNumber', event.target.value)}
placeholder='Your phone number'
/>
</FormField>

<div className='grid gap-4 sm:grid-cols-2'>
<FormField htmlFor='region' label='Region' error={errors.region}>
<Combobox
options={comboboxRegions}
value={form.region}
selectedValue={form.region}
onChange={(value) =>
updateField('region', value as DemoRequestPayload['region'])
}
placeholder='Select'
editable={false}
filterOptions={false}
/>
</FormField>
<FormField htmlFor='userCount' label='Number of users' error={errors.userCount}>
<Combobox
options={comboboxUserCounts}
value={form.userCount}
selectedValue={form.userCount}
onChange={(value) =>
updateField('userCount', value as DemoRequestPayload['userCount'])
}
placeholder='Select'
editable={false}
filterOptions={false}
/>
</FormField>
</div>

<FormField htmlFor='details' label='Details' error={errors.details}>
<Textarea
id='details'
value={form.details}
onChange={(event) => updateField('details', event.target.value)}
placeholder='Tell us about your needs and questions'
/>
</FormField>
</div>
</ModalBody>

<ModalFooter className='flex-col items-stretch gap-3'>
{submitError && <p className='text-[13px] text-[var(--text-error)]'>{submitError}</p>}
<Button type='submit' variant='primary' disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
</ModalFooter>
</form>

{submitSuccess ? (
<div className='absolute inset-0 flex items-center justify-center px-8 pb-10 sm:px-12 sm:pb-14'>
<div className='flex max-w-md flex-col items-center justify-center text-center'>
<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)]'>
<Check className='h-10 w-10' />
</div>
<h2 className='mt-8 font-medium text-[34px] text-[var(--text-primary)] leading-[1.1] tracking-[-0.03em]'>
{SUBMIT_SUCCESS_MESSAGE}
</h2>
<p className='mt-4 text-[17px] text-[var(--text-secondary)] leading-7'>
Our team will be in touch soon. If you have any questions, please email us at{' '}
<a
href='mailto:enterprise@sim.ai'
className='text-[var(--text-primary)] underline underline-offset-2'
>
enterprise@sim.ai
</a>
.
</p>
</div>
</div>
) : null}
</div>
</ModalContent>
</Modal>
)
}
14 changes: 11 additions & 3 deletions apps/sim/app/(home)/components/footer/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Image from 'next/image'
import Link from 'next/link'
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
import { FooterCTA } from '@/app/(home)/components/footer/footer-cta'

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

const PRODUCT_LINKS: FooterItem[] = [
{ label: 'Pricing', href: '/#pricing' },
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
{ label: 'Enterprise', href: '#', action: 'demo-request' },
{ label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
{ label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
Expand Down Expand Up @@ -83,8 +85,14 @@ function FooterColumn({ title, items }: { title: string; items: FooterItem[] })
<div>
<h3 className='mb-[16px] font-medium text-[#ECECEC] text-[14px]'>{title}</h3>
<div className='flex flex-col gap-[10px]'>
{items.map(({ label, href, external }) =>
external ? (
{items.map(({ label, href, external, action }) =>
action === 'demo-request' ? (
<DemoRequestModal key={label}>
<button type='button' className={`${LINK_CLASS} bg-transparent text-left`}>
{label}
</button>
</DemoRequestModal>
) : external ? (
<a
key={label}
href={href}
Expand Down
19 changes: 10 additions & 9 deletions apps/sim/app/(home)/components/hero/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dynamic from 'next/dynamic'
import Image from 'next/image'
import Link from 'next/link'
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
import {
BlocksLeftAnimated,
BlocksRightAnimated,
Expand Down Expand Up @@ -70,15 +71,15 @@ export default function Hero() {
</p>

<div className='mt-[12px] flex items-center gap-[8px]'>
<a
href='https://form.typeform.com/to/jqCO12pF'
target='_blank'
rel='noopener noreferrer'
className={`${CTA_BASE} border-[#3d3d3d] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
aria-label='Get a demo'
>
Get a demo
</a>
<DemoRequestModal>
<button
type='button'
className={`${CTA_BASE} border-[#3d3d3d] bg-transparent text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
aria-label='Get a demo'
>
Get a demo
</button>
</DemoRequestModal>
<Link
href='/signup'
className={`${CTA_BASE} gap-[8px] border-[#FFFFFF] bg-[#FFFFFF] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
Expand Down
Loading
Loading