Queue-North-Website/src/pages/Contact.jsx

387 lines
18 KiB
React
Raw Normal View History

2026-05-18 13:55:06 -05:00
import SEO from '@/components/SEO'
2026-05-27 20:57:55 -05:00
import { useState, useRef, useEffect } from 'react'
import { toast } from 'sonner'
2026-05-12 01:04:17 -05:00
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
2026-05-25 20:20:15 -05:00
import RecaptchaPlaceholder from '@/components/RecaptchaPlaceholder'
2026-05-26 12:22:11 -05:00
import { ArrowRight } from 'lucide-react'
2026-05-12 01:04:17 -05:00
const Contact = () => {
2026-05-27 20:57:55 -05:00
const formRef = useRef(null)
2026-05-12 01:04:17 -05:00
const [formState, setFormState] = useState({
2026-05-27 20:57:55 -05:00
'Last Name': '',
Company: '',
Email: '',
Phone: '',
'Zip Code': '',
Description: '',
2026-05-12 01:04:17 -05:00
})
const [errors, setErrors] = useState({
2026-05-27 20:57:55 -05:00
'Last Name': '',
Company: '',
Email: '',
'Zip Code': '',
Description: '',
2026-05-25 20:20:15 -05:00
recaptcha_token: '',
})
2026-05-27 20:57:55 -05:00
const [debouncedErrors, setDebouncedErrors] = useState(errors)
const [isSubmitting, setIsSubmitting] = useState(false)
2026-05-12 01:04:17 -05:00
2026-05-27 20:57:55 -05:00
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 = () => {
2026-05-27 20:57:55 -05:00
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@]+$/
2026-05-27 20:57:55 -05:00
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)
if (hasErrors) {
toast.error('Please fix the errors in the form')
return false
}
return true
}
2026-05-12 01:04:17 -05:00
const handleSubmit = (e) => {
e.preventDefault()
if (!validateForm()) return
setIsSubmitting(true)
2026-05-27 20:57:55 -05:00
formRef.current.submit()
2026-05-12 01:04:17 -05:00
}
const handleChange = (e) => {
const { name, value } = e.target
setFormState(prev => ({ ...prev, [name]: value }))
if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }))
2026-05-12 01:04:17 -05:00
}
const contactDetails = [
{
label: 'Phone',
icon: (
<svg className="h-5 w-5 text-primary-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
</svg>
),
content: (
<div>
<a href="tel:+13217308020" className="block text-white hover:text-primary-cyan transition-colors">(321) 730-8020</a>
<a href="tel:+18886562850" className="block text-white/70 text-sm hover:text-primary-cyan transition-colors mt-0.5">(888) 656-2850 Toll-Free</a>
</div>
),
},
{
label: 'Office',
icon: (
<svg className="h-5 w-5 text-primary-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
),
content: (
<a
href="https://maps.google.com/?q=7901+4th+St+N+St+Petersburg+FL+33702"
target="_blank"
rel="noopener noreferrer"
className="text-white hover:text-primary-cyan transition-colors leading-relaxed"
>
<span className="block">7901 4th St N</span>
<span className="block">St. Petersburg, FL 33702</span>
</a>
),
},
{
label: 'Hours',
icon: (
<svg className="h-5 w-5 text-primary-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
content: <p className="text-white/80 text-sm">Mon Fri: 8:00 AM 6:00 PM CT</p>,
},
]
const trustPoints = [
2026-05-25 17:56:32 -05:00
<div key="8x8"><span className="font-numeric">8x8</span> Certified Partner with proven expertise</div>,
2026-05-27 14:10:28 -05:00
'Cisco Certified Partner',
2026-05-25 17:56:32 -05:00
<div key="veteran"><span className="font-numeric">25+</span> years of experience</div>,
'SMB to Enterprise solutions',
'No vendor bias — we recommend what fits',
]
2026-05-12 01:04:17 -05:00
return (
<>
2026-05-18 13:55:06 -05:00
<SEO
title="Contact Queue North | Schedule a Free Consultation"
description="Contact Queue North Technologies to schedule a free consultation. Call (321) 730-8020 or toll-free (888) 656-2850 for business phone, UCaaS, IT support, and networking solutions."
url="https://queuenorth.com/contact"
/>
{/* Hero */}
2026-05-26 12:22:11 -05:00
<section className="relative isolate overflow-hidden bg-primary-navy py-16 lg:py-24">
<div className="absolute inset-0 -z-10">
<img
src="/assets/hero-tech.webp"
alt="Queue North communications infrastructure consultation"
className="h-full w-full object-cover object-center"
/>
<div className="absolute inset-0 bg-primary-navy/82" />
<div className="absolute inset-0 bg-gradient-to-r from-primary-navy via-primary-navy/92 to-primary-navy/45" />
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
2026-05-26 12:22:11 -05:00
<div className="inline-flex items-center gap-2 rounded-md border border-white/20 bg-white/10 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-primary-cyan mb-6">
2026-05-27 14:27:30 -05:00
Contact
2026-05-26 12:22:11 -05:00
</div>
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">Let's Talk</h1>
<p className="text-xl text-white/70 max-w-2xl">
Tell us about your business and we'll cut through the noise to find what actually works for you.
</p>
2026-05-26 12:22:11 -05:00
<a
href="#contact-form"
className="mt-8 inline-flex h-11 items-center justify-center gap-2 rounded-md bg-white px-5 text-sm font-semibold text-primary-navy hover:bg-section-alt transition-colors"
>
Send a Message
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</a>
2026-05-12 01:04:17 -05:00
</div>
</section>
2026-05-12 01:04:17 -05:00
{/* Contact Body */}
2026-05-26 12:22:11 -05:00
<section id="contact-form" className="bg-background py-16 lg:py-24">
<div className="max-w-7xl mx-auto px-0 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-5 rounded-none sm:rounded-md overflow-hidden shadow-none sm:shadow-xl border-y sm:border border-border">
{/* Left: Info panel — order 2 on mobile so form appears first */}
<div className="lg:col-span-2 order-2 lg:order-1 bg-primary-navy text-white p-8 lg:p-10 flex flex-col gap-10">
<div className="space-y-7">
{contactDetails.map((item) => (
<div key={item.label} className="flex items-start gap-4">
<div className="w-10 h-10 bg-white/10 rounded-md flex items-center justify-center flex-shrink-0">
{item.icon}
</div>
<div>
<p className="text-white/50 text-xs uppercase tracking-wider mb-1">{item.label}</p>
{item.content}
</div>
</div>
))}
</div>
2026-05-12 01:04:17 -05:00
<div className="border-t border-white/10" />
<div>
2026-05-27 14:10:28 -05:00
<p className="text-white/50 text-xs uppercase tracking-wider mb-5">Why Queue North Technologies</p>
<ul className="space-y-4">
{trustPoints.map((point, i) => (
<li key={i} className="flex items-start gap-3 text-white/80 text-sm leading-relaxed">
<svg className="h-4 w-4 text-primary-cyan flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
{point}
</li>
))}
</ul>
</div>
2026-05-12 01:04:17 -05:00
</div>
{/* Right: Form panel — order 1 on mobile so it appears first */}
<div className="lg:col-span-3 order-1 lg:order-2 bg-white p-6 sm:p-8 lg:p-10">
<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>
2026-05-27 20:57:55 -05:00
<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>
2026-05-27 20:57:55 -05:00
<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"
2026-05-27 20:57:55 -05:00
id="Company"
name="Company"
value={formState.Company}
onChange={handleChange}
required
placeholder="Your company name"
2026-05-27 20:57:55 -05:00
className={debouncedErrors.Company ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
2026-05-27 20:57:55 -05:00
{debouncedErrors.Company && <p className="text-xs text-red-500 mt-1">{debouncedErrors.Company}</p>}
</div>
2026-05-12 01:04:17 -05:00
{/* Name + Email */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
2026-05-27 20:57:55 -05:00
<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"
2026-05-27 20:57:55 -05:00
id="Last_Name"
name="Last Name"
value={formState['Last Name']}
onChange={handleChange}
required
placeholder="Your full name"
2026-05-27 20:57:55 -05:00
className={debouncedErrors['Last Name'] ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
2026-05-27 20:57:55 -05:00
{debouncedErrors['Last Name'] && <p className="text-xs text-red-500 mt-1">{debouncedErrors['Last Name']}</p>}
</div>
<div>
2026-05-27 20:57:55 -05:00
<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"
2026-05-27 20:57:55 -05:00
id="Email"
name="Email"
value={formState.Email}
onChange={handleChange}
required
placeholder="you@company.com"
2026-05-27 20:57:55 -05:00
className={debouncedErrors.Email ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
2026-05-27 20:57:55 -05:00
{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>
2026-05-27 20:57:55 -05:00
<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"
2026-05-27 20:57:55 -05:00
id="Phone"
name="Phone"
value={formState.Phone}
onChange={handleChange}
placeholder="(555) 123-4567"
/>
</div>
<div>
2026-05-27 20:57:55 -05:00
<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"
2026-05-27 20:57:55 -05:00
id="Zip_Code"
name="Zip Code"
value={formState['Zip Code']}
onChange={handleChange}
required
autoComplete="postal-code"
inputMode="numeric"
placeholder="33702"
2026-05-27 20:57:55 -05:00
className={debouncedErrors['Zip Code'] ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
2026-05-27 20:57:55 -05:00
{debouncedErrors['Zip Code'] && <p className="text-xs text-red-500 mt-1">{debouncedErrors['Zip Code']}</p>}
</div>
</div>
{/* Message */}
<div>
2026-05-27 20:57:55 -05:00
<label htmlFor="Description" className="block text-sm font-medium text-text mb-1.5">
Message <span className="text-red-500">*</span>
</label>
<Textarea
2026-05-27 20:57:55 -05:00
id="Description"
name="Description"
value={formState.Description}
onChange={handleChange}
required
placeholder="Tell us about your needs..."
2026-05-27 20:57:55 -05:00
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}
/>
2026-05-27 20:57:55 -05:00
{debouncedErrors.Description && <p className="text-xs text-red-500 mt-1">{debouncedErrors.Description}</p>}
</div>
2026-05-25 20:20:15 -05:00
<RecaptchaPlaceholder error={debouncedErrors.recaptcha_token} />
<Button type="submit" className="w-full h-11" disabled={isSubmitting}>
{isSubmitting ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Sending...
</>
) : (
'Send Message'
)}
</Button>
</form>
2026-05-27 20:57:55 -05:00
<iframe
name="zoho_webform_iframe"
id="zoho_webform_iframe"
title="Zoho form submission"
style={{ display: 'none' }}
/>
</div>
</div>
2026-05-12 01:04:17 -05:00
</div>
</section>
</>
2026-05-12 01:04:17 -05:00
)
}
export default Contact