From aefdc21f0d959de34edc50737ae3c7ffa593f29b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 25 Mar 2026 12:33:01 -0700 Subject: [PATCH 01/15] fix(ui): add request a demo modal --- .../demo-request/demo-request-modal.tsx | 303 ++++++++++++++++++ .../app/(home)/components/footer/footer.tsx | 14 +- apps/sim/app/(home)/components/hero/hero.tsx | 19 +- .../app/(home)/components/pricing/pricing.tsx | 25 +- apps/sim/app/api/demo-request/route.ts | 104 ++++++ .../emcn/components/form-field/form-field.tsx | 28 ++ apps/sim/components/emcn/components/index.ts | 2 + .../emcn/components/modal/modal.tsx | 14 + apps/sim/lib/marketing/demo-request.ts | 73 +++++ 9 files changed, 558 insertions(+), 24 deletions(-) create mode 100644 apps/sim/app/(home)/components/demo-request/demo-request-modal.tsx create mode 100644 apps/sim/app/api/demo-request/route.ts create mode 100644 apps/sim/components/emcn/components/form-field/form-field.tsx create mode 100644 apps/sim/lib/marketing/demo-request.ts diff --git a/apps/sim/app/(home)/components/demo-request/demo-request-modal.tsx b/apps/sim/app/(home)/components/demo-request/demo-request-modal.tsx new file mode 100644 index 00000000000..13deeb8fb1f --- /dev/null +++ b/apps/sim/app/(home)/components/demo-request/demo-request-modal.tsx @@ -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> + +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(INITIAL_FORM_STATE) + const [errors, setErrors] = useState({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [submitError, setSubmitError] = useState(null) + const [submitSuccess, setSubmitSuccess] = useState(false) + + const comboboxRegions = useMemo(() => [...DEMO_REQUEST_REGION_OPTIONS], []) + const comboboxUserCounts = useMemo(() => [...DEMO_REQUEST_USER_COUNT_OPTIONS], []) + + 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] + ) + + const updateField = useCallback( + ( + 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) => { + 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) + } catch (error) { + setSubmitError( + error instanceof Error + ? error.message + : 'Failed to submit demo request. Please try again.' + ) + } finally { + setIsSubmitting(false) + } + }, + [form, resetForm] + ) + + return ( + + {children} + + + + {submitSuccess ? 'Demo request submitted' : 'Nearly there!'} + + +
+
+ +
+
+ + updateField('firstName', event.target.value)} + placeholder='First' + /> + + + updateField('lastName', event.target.value)} + placeholder='Last' + /> + +
+ + + updateField('companyEmail', event.target.value)} + placeholder='Your work email' + /> + + + + updateField('phoneNumber', event.target.value)} + placeholder='Your phone number' + /> + + +
+ + + updateField('region', value as DemoRequestPayload['region']) + } + placeholder='Select' + editable={false} + filterOptions={false} + /> + + + + updateField('userCount', value as DemoRequestPayload['userCount']) + } + placeholder='Select' + editable={false} + filterOptions={false} + /> + +
+ + +