Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
302 changes: 302 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,302 @@
'use client'

import { useCallback, 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 COMBOBOX_REGIONS = [...DEMO_REQUEST_REGION_OPTIONS]
const COMBOBOX_USER_COUNTS = [...DEMO_REQUEST_USER_COUNT_OPTIONS]

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 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={COMBOBOX_REGIONS}
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={COMBOBOX_USER_COUNTS}
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>
)
}
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
25 changes: 13 additions & 12 deletions apps/sim/app/(home)/components/pricing/pricing.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from 'next/link'
import { Badge } from '@/components/emcn'
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'

interface PricingTier {
id: string
Expand All @@ -9,7 +10,7 @@ interface PricingTier {
billingPeriod?: string
color: string
features: string[]
cta: { label: string; href: string }
cta: { label: string; href?: string; action?: 'demo-request' }
Comment thread
cursor[bot] marked this conversation as resolved.
}

const PRICING_TIERS: PricingTier[] = [
Expand Down Expand Up @@ -78,7 +79,7 @@ const PRICING_TIERS: PricingTier[] = [
'SSO & SCIM · SOC2 & HIPAA',
'Self hosting · Dedicated support',
],
cta: { label: 'Book a demo', href: 'https://form.typeform.com/to/jqCO12pF' },
cta: { label: 'Book a demo', action: 'demo-request' },
},
]

Expand Down Expand Up @@ -125,24 +126,24 @@ function PricingCard({ tier }: PricingCardProps) {
</p>
<div className='mt-4'>
{isEnterprise ? (
<a
href={tier.cta.href}
target='_blank'
rel='noopener noreferrer'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
>
{tier.cta.label}
</a>
<DemoRequestModal theme='light'>
<button
type='button'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] bg-transparent px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
>
{tier.cta.label}
</button>
</DemoRequestModal>
) : isPro ? (
<Link
href={tier.cta.href}
href={tier.cta.href || '/signup'}
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
>
{tier.cta.label}
</Link>
) : (
<Link
href={tier.cta.href}
href={tier.cta.href || '/signup'}
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
>
{tier.cta.label}
Expand Down
Loading
Loading