Form now POSTs to Zoho
This commit is contained in:
parent
548e20e6f0
commit
033bdf6625
|
|
@ -1,49 +1,71 @@
|
|||
import SEO from '@/components/SEO'
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Textarea } from '@/components/ui/Textarea'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import RecaptchaPlaceholder from '@/components/RecaptchaPlaceholder'
|
||||
import { submitLead } from '@/lib/api'
|
||||
import { useDebounce } from '@/hooks/useDebounce'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
|
||||
const Contact = () => {
|
||||
const formRef = useRef(null)
|
||||
const [formState, setFormState] = useState({
|
||||
company: '',
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
zip: '',
|
||||
message: '',
|
||||
service_interest: '',
|
||||
recaptcha_token: '',
|
||||
company_website: '',
|
||||
'Last Name': '',
|
||||
Company: '',
|
||||
Email: '',
|
||||
Phone: '',
|
||||
'Zip Code': '',
|
||||
Description: '',
|
||||
})
|
||||
const [errors, setErrors] = useState({
|
||||
company: '',
|
||||
name: '',
|
||||
email: '',
|
||||
zip: '',
|
||||
message: '',
|
||||
'Last Name': '',
|
||||
Company: '',
|
||||
Email: '',
|
||||
'Zip Code': '',
|
||||
Description: '',
|
||||
recaptcha_token: '',
|
||||
})
|
||||
const debouncedErrors = useDebounce(errors, 300)
|
||||
const [debouncedErrors, setDebouncedErrors] = useState(errors)
|
||||
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 newErrors = { company: '', name: '', email: '', zip: '', message: '', recaptcha_token: '' }
|
||||
if (!formState.company.trim()) newErrors.company = 'Company name is required'
|
||||
if (!formState.name.trim()) newErrors.name = 'Name is required'
|
||||
if (!formState.zip.trim()) newErrors.zip = 'ZIP code is required'
|
||||
if (!formState.message.trim()) newErrors.message = 'Message is required'
|
||||
const newErrors = { 'Last Name': '', Company: '', Email: '', 'Zip Code': '', Description: '', recaptcha_token: '' }
|
||||
if (!formState.Company.trim()) newErrors.Company = 'Company name is required'
|
||||
if (!formState['Last Name'].trim()) newErrors['Last Name'] = 'Name is required'
|
||||
if (!formState['Zip Code'].trim()) newErrors['Zip Code'] = 'ZIP code is required'
|
||||
if (!formState.Description.trim()) newErrors.Description = 'Message is required'
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!formState.email.trim()) {
|
||||
newErrors.email = 'Email is required'
|
||||
} else if (!emailRegex.test(formState.email)) {
|
||||
newErrors.email = 'Please enter a valid email address'
|
||||
if (!formState.Email.trim()) {
|
||||
newErrors.Email = 'Email is required'
|
||||
} else if (!emailRegex.test(formState.Email)) {
|
||||
newErrors.Email = 'Please enter a valid email address'
|
||||
}
|
||||
const hasErrors = Object.values(newErrors).some(error => error !== '')
|
||||
setErrors(newErrors)
|
||||
|
|
@ -57,28 +79,8 @@ const Contact = () => {
|
|||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
if (!validateForm()) return
|
||||
handleSubmitForm()
|
||||
}
|
||||
|
||||
const handleSubmitForm = async () => {
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
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)
|
||||
}
|
||||
formRef.current.submit()
|
||||
}
|
||||
|
||||
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>
|
||||
<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 */}
|
||||
<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>
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
value={formState.company}
|
||||
id="Company"
|
||||
name="Company"
|
||||
value={formState.Company}
|
||||
onChange={handleChange}
|
||||
required
|
||||
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>
|
||||
|
||||
{/* Name + Email */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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>
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formState.name}
|
||||
id="Last_Name"
|
||||
name="Last Name"
|
||||
value={formState['Last Name']}
|
||||
onChange={handleChange}
|
||||
required
|
||||
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>
|
||||
<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>
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formState.email}
|
||||
id="Email"
|
||||
name="Email"
|
||||
value={formState.Email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
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>
|
||||
|
||||
{/* Phone + ZIP */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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>
|
||||
</label>
|
||||
<Input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formState.phone}
|
||||
id="Phone"
|
||||
name="Phone"
|
||||
value={formState.Phone}
|
||||
onChange={handleChange}
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
</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>
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="zip"
|
||||
name="zip"
|
||||
value={formState.zip}
|
||||
id="Zip_Code"
|
||||
name="Zip Code"
|
||||
value={formState['Zip Code']}
|
||||
onChange={handleChange}
|
||||
required
|
||||
autoComplete="postal-code"
|
||||
inputMode="numeric"
|
||||
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>
|
||||
|
||||
{/* 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 */}
|
||||
<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>
|
||||
</label>
|
||||
<Textarea
|
||||
id="message"
|
||||
name="message"
|
||||
value={formState.message}
|
||||
id="Description"
|
||||
name="Description"
|
||||
value={formState.Description}
|
||||
onChange={handleChange}
|
||||
required
|
||||
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}
|
||||
/>
|
||||
{debouncedErrors.message && <p className="text-xs text-red-500 mt-1">{debouncedErrors.message}</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}
|
||||
/>
|
||||
{debouncedErrors.Description && <p className="text-xs text-red-500 mt-1">{debouncedErrors.Description}</p>}
|
||||
</div>
|
||||
|
||||
<RecaptchaPlaceholder error={debouncedErrors.recaptcha_token} />
|
||||
|
|
@ -380,6 +368,13 @@ const Contact = () => {
|
|||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<iframe
|
||||
name="zoho_webform_iframe"
|
||||
id="zoho_webform_iframe"
|
||||
title="Zoho form submission"
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue