Form now POSTs to Zoho

This commit is contained in:
null 2026-05-27 20:57:55 -05:00
parent 548e20e6f0
commit 033bdf6625
1 changed files with 114 additions and 119 deletions

View File

@ -1,49 +1,71 @@
import SEO from '@/components/SEO' import SEO from '@/components/SEO'
import { useState } from 'react' import { useState, useRef, useEffect } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input' import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea' import { Textarea } from '@/components/ui/Textarea'
import { Select } from '@/components/ui/Select'
import RecaptchaPlaceholder from '@/components/RecaptchaPlaceholder' import RecaptchaPlaceholder from '@/components/RecaptchaPlaceholder'
import { submitLead } from '@/lib/api'
import { useDebounce } from '@/hooks/useDebounce'
import { ArrowRight } from 'lucide-react' import { ArrowRight } from 'lucide-react'
const Contact = () => { const Contact = () => {
const formRef = useRef(null)
const [formState, setFormState] = useState({ const [formState, setFormState] = useState({
company: '', 'Last Name': '',
name: '', Company: '',
email: '', Email: '',
phone: '', Phone: '',
zip: '', 'Zip Code': '',
message: '', Description: '',
service_interest: '',
recaptcha_token: '',
company_website: '',
}) })
const [errors, setErrors] = useState({ const [errors, setErrors] = useState({
company: '', 'Last Name': '',
name: '', Company: '',
email: '', Email: '',
zip: '', 'Zip Code': '',
message: '', Description: '',
recaptcha_token: '', recaptcha_token: '',
}) })
const debouncedErrors = useDebounce(errors, 300) const [debouncedErrors, setDebouncedErrors] = useState(errors)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
const t = setTimeout(() => setDebouncedErrors(errors), 300)
return () => clearTimeout(t)
}, [errors])
useEffect(() => {
const iframe = document.getElementById('zoho_webform_iframe')
if (!iframe) return
const handleLoad = () => {
if (!isSubmitting) return
setIsSubmitting(false)
toast.success("Thanks! We'll be in touch shortly.")
setFormState({ 'Last Name': '', Company: '', Email: '', Phone: '', 'Zip Code': '', Description: '' })
setErrors({ 'Last Name': '', Company: '', Email: '', 'Zip Code': '', Description: '', recaptcha_token: '' })
}
iframe.addEventListener('load', handleLoad)
return () => iframe.removeEventListener('load', handleLoad)
}, [isSubmitting])
useEffect(() => {
const script = document.createElement('script')
script.id = 'wf_anal'
script.src = 'https://crm.zohopublic.com/crm/WebFormAnalyticsServeServlet?rid=e44e9662530fc5bd9cdd3c43501fc243f89ba03759e7946c4b5e5016795b606b59b54d0e73c68671b2140fac5c8e788agid3b907524e85f9cba94899d77d7200771ee5d0ea567c43ec341d7b2ce40324d40gid26922a9cd1e8191a5f58ecb2524e0d22b8dd027eb943658ee681ab6890436af2gidefa1b1002d15951a0a2ac36cb33cdb4b5c6aeb110e6f4ac68b764345b9429653&tw=e048253ca680b107993ed5922e00cc1ebab3de97e797fce56fc6ad6af0dfc0bc'
document.body.appendChild(script)
return () => { document.getElementById('wf_anal')?.remove() }
}, [])
const validateForm = () => { const validateForm = () => {
const newErrors = { company: '', name: '', email: '', zip: '', message: '', recaptcha_token: '' } const newErrors = { 'Last Name': '', Company: '', Email: '', 'Zip Code': '', Description: '', recaptcha_token: '' }
if (!formState.company.trim()) newErrors.company = 'Company name is required' if (!formState.Company.trim()) newErrors.Company = 'Company name is required'
if (!formState.name.trim()) newErrors.name = 'Name is required' if (!formState['Last Name'].trim()) newErrors['Last Name'] = 'Name is required'
if (!formState.zip.trim()) newErrors.zip = 'ZIP code is required' if (!formState['Zip Code'].trim()) newErrors['Zip Code'] = 'ZIP code is required'
if (!formState.message.trim()) newErrors.message = 'Message is required' if (!formState.Description.trim()) newErrors.Description = 'Message is required'
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!formState.email.trim()) { if (!formState.Email.trim()) {
newErrors.email = 'Email is required' newErrors.Email = 'Email is required'
} else if (!emailRegex.test(formState.email)) { } else if (!emailRegex.test(formState.Email)) {
newErrors.email = 'Please enter a valid email address' newErrors.Email = 'Please enter a valid email address'
} }
const hasErrors = Object.values(newErrors).some(error => error !== '') const hasErrors = Object.values(newErrors).some(error => error !== '')
setErrors(newErrors) setErrors(newErrors)
@ -57,28 +79,8 @@ const Contact = () => {
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault() e.preventDefault()
if (!validateForm()) return if (!validateForm()) return
handleSubmitForm()
}
const handleSubmitForm = async () => {
setIsSubmitting(true) setIsSubmitting(true)
try { formRef.current.submit()
await submitLead(formState)
toast.success("Thanks! We'll be in touch shortly.")
setFormState({ company: '', name: '', email: '', phone: '', zip: '', message: '', service_interest: '', recaptcha_token: '', company_website: '' })
setErrors({ company: '', name: '', email: '', zip: '', message: '', recaptcha_token: '' })
} catch (error) {
if (error.response?.status === 409) {
toast.success("We already have your submission! We'll be in touch.")
} else if (error.response?.status === 400 && error.fields) {
setErrors(prev => ({ ...prev, ...error.fields }))
toast.error('Please fix the errors in the form')
} else {
toast.error(error.message || 'Failed to submit form. Please try again.')
}
} finally {
setIsSubmitting(false)
}
} }
const handleChange = (e) => { const handleChange = (e) => {
@ -221,147 +223,133 @@ const Contact = () => {
<h2 className="text-2xl font-bold text-primary-navy mb-1">Send Us a Message</h2> <h2 className="text-2xl font-bold text-primary-navy mb-1">Send Us a Message</h2>
<p className="text-soft-text text-sm mb-8">We typically respond within one business day.</p> <p className="text-soft-text text-sm mb-8">We typically respond within one business day.</p>
<form onSubmit={handleSubmit} noValidate className={`space-y-5 ${isSubmitting ? 'opacity-70 pointer-events-none' : ''}`}> <form
ref={formRef}
id="contact-form"
name="WebToLeads7130861000000581796"
method="POST"
action="https://crm.zoho.com/crm/WebToLeadForm"
target="zoho_webform_iframe"
acceptCharset="UTF-8"
onSubmit={handleSubmit}
noValidate
className={`space-y-5 ${isSubmitting ? 'opacity-70 pointer-events-none' : ''}`}
>
{/* Zoho required hidden fields */}
<input type="hidden" name="xnQsjsdp" value="b78607b2ef073f134a736184c22aa442ba026b6b00cfdbcb8078d8dee0bb1bbd" />
<input type="hidden" name="zc_gad" id="zc_gad" value="" />
<input type="hidden" name="xmIwtLD" value="e1201f09c921b74ca7844fca8689433ad14277423595fe88de0e4cd6c58e43e743fb001043cb5229e129ff4ab8b2beea" />
<input type="hidden" name="actionType" value="TGVhZHM=" />
<input type="hidden" name="returnURL" value="null" />
{/* Honeypot */}
<input type="text" name="aG9uZXlwb3Q" defaultValue="" tabIndex={-1} autoComplete="off" aria-hidden="true" style={{ display: 'none' }} readOnly />
{/* Company */} {/* Company */}
<div> <div>
<label htmlFor="company" className="block text-sm font-medium text-text mb-1.5"> <label htmlFor="Company" className="block text-sm font-medium text-text mb-1.5">
Company Name <span className="text-red-500">*</span> Company Name <span className="text-red-500">*</span>
</label> </label>
<Input <Input
type="text" type="text"
id="company" id="Company"
name="company" name="Company"
value={formState.company} value={formState.Company}
onChange={handleChange} onChange={handleChange}
required required
placeholder="Your company name" placeholder="Your company name"
className={debouncedErrors.company ? 'border-red-500 focus-visible:ring-red-500' : ''} className={debouncedErrors.Company ? 'border-red-500 focus-visible:ring-red-500' : ''}
/> />
{debouncedErrors.company && <p className="text-xs text-red-500 mt-1">{debouncedErrors.company}</p>} {debouncedErrors.Company && <p className="text-xs text-red-500 mt-1">{debouncedErrors.Company}</p>}
</div> </div>
{/* Name + Email */} {/* Name + Email */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label htmlFor="name" className="block text-sm font-medium text-text mb-1.5"> <label htmlFor="Last_Name" className="block text-sm font-medium text-text mb-1.5">
Name <span className="text-red-500">*</span> Name <span className="text-red-500">*</span>
</label> </label>
<Input <Input
type="text" type="text"
id="name" id="Last_Name"
name="name" name="Last Name"
value={formState.name} value={formState['Last Name']}
onChange={handleChange} onChange={handleChange}
required required
placeholder="Your full name" placeholder="Your full name"
className={debouncedErrors.name ? 'border-red-500 focus-visible:ring-red-500' : ''} className={debouncedErrors['Last Name'] ? 'border-red-500 focus-visible:ring-red-500' : ''}
/> />
{debouncedErrors.name && <p className="text-xs text-red-500 mt-1">{debouncedErrors.name}</p>} {debouncedErrors['Last Name'] && <p className="text-xs text-red-500 mt-1">{debouncedErrors['Last Name']}</p>}
</div> </div>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-text mb-1.5"> <label htmlFor="Email" className="block text-sm font-medium text-text mb-1.5">
Email <span className="text-red-500">*</span> Email <span className="text-red-500">*</span>
</label> </label>
<Input <Input
type="email" type="email"
id="email" id="Email"
name="email" name="Email"
value={formState.email} value={formState.Email}
onChange={handleChange} onChange={handleChange}
required required
placeholder="you@company.com" placeholder="you@company.com"
className={debouncedErrors.email ? 'border-red-500 focus-visible:ring-red-500' : ''} className={debouncedErrors.Email ? 'border-red-500 focus-visible:ring-red-500' : ''}
/> />
{debouncedErrors.email && <p className="text-xs text-red-500 mt-1">{debouncedErrors.email}</p>} {debouncedErrors.Email && <p className="text-xs text-red-500 mt-1">{debouncedErrors.Email}</p>}
</div> </div>
</div> </div>
{/* Phone + ZIP */} {/* Phone + ZIP */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label htmlFor="phone" className="block text-sm font-medium text-text mb-1.5"> <label htmlFor="Phone" className="block text-sm font-medium text-text mb-1.5">
Phone <span className="text-soft-text font-normal">(optional)</span> Phone <span className="text-soft-text font-normal">(optional)</span>
</label> </label>
<Input <Input
type="tel" type="tel"
id="phone" id="Phone"
name="phone" name="Phone"
value={formState.phone} value={formState.Phone}
onChange={handleChange} onChange={handleChange}
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
/> />
</div> </div>
<div> <div>
<label htmlFor="zip" className="block text-sm font-medium text-text mb-1.5"> <label htmlFor="Zip_Code" className="block text-sm font-medium text-text mb-1.5">
ZIP Code <span className="text-red-500">*</span> ZIP Code <span className="text-red-500">*</span>
</label> </label>
<Input <Input
type="text" type="text"
id="zip" id="Zip_Code"
name="zip" name="Zip Code"
value={formState.zip} value={formState['Zip Code']}
onChange={handleChange} onChange={handleChange}
required required
autoComplete="postal-code" autoComplete="postal-code"
inputMode="numeric" inputMode="numeric"
placeholder="33702" placeholder="33702"
className={debouncedErrors.zip ? 'border-red-500 focus-visible:ring-red-500' : ''} className={debouncedErrors['Zip Code'] ? 'border-red-500 focus-visible:ring-red-500' : ''}
/> />
{debouncedErrors.zip && <p className="text-xs text-red-500 mt-1">{debouncedErrors.zip}</p>} {debouncedErrors['Zip Code'] && <p className="text-xs text-red-500 mt-1">{debouncedErrors['Zip Code']}</p>}
</div> </div>
</div> </div>
{/* Service Interest */}
<div>
<label htmlFor="service_interest" className="block text-sm font-medium text-text mb-1.5">
Service Interest <span className="text-soft-text font-normal">(optional)</span>
</label>
<Select
id="service_interest"
name="service_interest"
value={formState.service_interest}
onChange={handleChange}
>
<option value="">Select a service...</option>
<option value="unified-communications">Unified Communications</option>
<option value="contact-center">Contact Center</option>
<option value="managed-support">Managed Support</option>
<option value="consulting-training">Consulting & Training</option>
<option value="infrastructure-cabling">Infrastructure Cabling</option>
<option value="wireless-access">Wireless Access</option>
<option value="local-networking">Local Networking</option>
<option value="other">Other</option>
</Select>
</div>
{/* Message */} {/* Message */}
<div> <div>
<label htmlFor="message" className="block text-sm font-medium text-text mb-1.5"> <label htmlFor="Description" className="block text-sm font-medium text-text mb-1.5">
Message <span className="text-red-500">*</span> Message <span className="text-red-500">*</span>
</label> </label>
<Textarea <Textarea
id="message" id="Description"
name="message" name="Description"
value={formState.message} value={formState.Description}
onChange={handleChange} onChange={handleChange}
required required
placeholder="Tell us about your needs..." placeholder="Tell us about your needs..."
className={`w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-[#F8FAFC] placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-navy focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${debouncedErrors.message ? 'border-red-500 focus-visible:ring-red-500' : ''}`} className={`w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-[#F8FAFC] placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-navy focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${debouncedErrors.Description ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
rows={5} rows={5}
/> />
{debouncedErrors.message && <p className="text-xs text-red-500 mt-1">{debouncedErrors.message}</p>} {debouncedErrors.Description && <p className="text-xs text-red-500 mt-1">{debouncedErrors.Description}</p>}
</div>
{/* Honeypot */}
<div className="absolute opacity-0 h-0 overflow-hidden" aria-hidden="true">
<input
type="text"
name="company_website"
tabIndex="-1"
autoComplete="off"
value={formState.company_website}
onChange={handleChange}
/>
</div> </div>
<RecaptchaPlaceholder error={debouncedErrors.recaptcha_token} /> <RecaptchaPlaceholder error={debouncedErrors.recaptcha_token} />
@ -380,6 +368,13 @@ const Contact = () => {
)} )}
</Button> </Button>
</form> </form>
<iframe
name="zoho_webform_iframe"
id="zoho_webform_iframe"
title="Zoho form submission"
style={{ display: 'none' }}
/>
</div> </div>
</div> </div>
</div> </div>