diff --git a/apps/sim/app/(landing)/components/contact/contact-form.tsx b/apps/sim/app/(landing)/components/contact/contact-form.tsx index 4cde2b3da7..11030ac760 100644 --- a/apps/sim/app/(landing)/components/contact/contact-form.tsx +++ b/apps/sim/app/(landing)/components/contact/contact-form.tsx @@ -1,10 +1,13 @@ 'use client' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' +import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile' +import { toError } from '@sim/utils/errors' import { useMutation } from '@tanstack/react-query' -import { Combobox, Input, Textarea } from '@/components/emcn' +import Link from 'next/link' +import { Combobox, type ComboboxOption, Input, Textarea } from '@/components/emcn' import { Check } from '@/components/emcn/icons' -import { cn } from '@/lib/core/utils/cn' +import { getEnv } from '@/lib/core/config/env' import { captureClientEvent } from '@/lib/posthog/client' import { CONTACT_TOPIC_OPTIONS, @@ -34,12 +37,28 @@ const INITIAL_FORM_STATE: ContactFormState = { message: '', } -const COMBOBOX_TOPICS = [...CONTACT_TOPIC_OPTIONS] - const LANDING_INPUT = - 'h-[36px] rounded-[5px] border border-[var(--border-1)] bg-[var(--surface-5)] px-3 font-[430] font-season text-[14px] text-[var(--text-primary)] outline-none transition-colors placeholder:text-[var(--text-muted)]' + 'h-[40px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg-surface)] px-3 font-[430] font-season text-[14px] text-[var(--landing-text)] outline-none transition-colors placeholder:text-[var(--landing-text-muted)] focus:border-[var(--landing-border-strong)]' + +const LANDING_TEXTAREA = + 'min-h-[140px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg-surface)] px-3 py-2.5 font-[430] font-season text-[14px] text-[var(--landing-text)] outline-none transition-colors placeholder:text-[var(--landing-text-muted)] focus:border-[var(--landing-border-strong)]' + +const LANDING_COMBOBOX = + 'h-[40px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg-surface)] px-3 font-[430] font-season text-[14px] text-[var(--landing-text)] hover:bg-[var(--landing-bg-surface)] focus-within:border-[var(--landing-border-strong)]' + +const LANDING_SUBMIT = + 'flex h-[40px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-text-subtle)] bg-[var(--landing-text-subtle)] font-[430] font-season text-[14px] text-[var(--landing-text-dark)] transition-colors hover:border-[var(--landing-bg-hover)] hover:bg-[var(--landing-bg-hover)] disabled:cursor-not-allowed disabled:opacity-60' + +const LANDING_LABEL = + 'font-[500] font-season text-[13px] text-[var(--landing-text)] tracking-[0.02em]' -async function submitContactRequest(payload: ContactRequestPayload) { +interface SubmitContactRequestInput extends ContactRequestPayload { + website: string + captchaToken?: string + captchaUnavailable?: boolean +} + +async function submitContactRequest(payload: SubmitContactRequestInput) { const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -59,9 +78,7 @@ async function submitContactRequest(payload: ContactRequestPayload) { } export function ContactForm() { - const [form, setForm] = useState(INITIAL_FORM_STATE) - const [errors, setErrors] = useState({}) - const [submitSuccess, setSubmitSuccess] = useState(false) + const turnstileRef = useRef(null) const contactMutation = useMutation({ mutationFn: submitContactRequest, @@ -71,8 +88,23 @@ export function ContactForm() { setErrors({}) setSubmitSuccess(true) }, + onError: () => { + turnstileRef.current?.reset() + }, }) + const [form, setForm] = useState(INITIAL_FORM_STATE) + const [errors, setErrors] = useState({}) + const [submitSuccess, setSubmitSuccess] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [website, setWebsite] = useState('') + const [widgetReady, setWidgetReady] = useState(false) + const [turnstileSiteKey, setTurnstileSiteKey] = useState() + + useEffect(() => { + setTurnstileSiteKey(getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY')) + }, []) + function updateField( field: TField, value: ContactFormState[TField] @@ -91,9 +123,10 @@ export function ContactForm() { } } - function handleSubmit(event: React.FormEvent) { + async function handleSubmit(event: React.FormEvent) { event.preventDefault() - if (contactMutation.isPending) return + if (contactMutation.isPending || isSubmitting) return + setIsSubmitting(true) const parsed = contactRequestSchema.safeParse({ ...form, @@ -110,35 +143,55 @@ export function ContactForm() { subject: fieldErrors.subject?.[0], message: fieldErrors.message?.[0], }) + setIsSubmitting(false) return } - contactMutation.mutate(parsed.data) + let captchaToken: string | undefined + let captchaUnavailable: boolean | undefined + const widget = turnstileRef.current + + if (turnstileSiteKey) { + if (widgetReady && widget) { + try { + widget.reset() + widget.execute() + captchaToken = await widget.getResponsePromise(30_000) + } catch { + captchaUnavailable = true + } + } else { + captchaUnavailable = true + } + } + + contactMutation.mutate({ ...parsed.data, website, captchaToken, captchaUnavailable }) + setIsSubmitting(false) } + const isBusy = contactMutation.isPending || isSubmitting + const submitError = contactMutation.isError - ? contactMutation.error instanceof Error - ? contactMutation.error.message - : 'Failed to send message. Please try again.' + ? toError(contactMutation.error).message || 'Failed to send message. Please try again.' : null if (submitSuccess) { return ( -
-
+
+
-

+

Message received

-

+

Thanks for reaching out. We've sent a confirmation to your inbox and will get back to you shortly.

@@ -147,12 +200,33 @@ export function ContactForm() { } return ( -
-
- + + {/* Honeypot */} + + +
+ - +
-
- +
+ - + updateField('topic', value as ContactRequestPayload['topic'])} placeholder='Select a topic' editable={false} filterOptions={false} - className='h-[36px] rounded-[5px] px-3 font-[430] font-season text-[14px]' + className={LANDING_COMBOBOX} />
- + - +