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

387 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SEO from '@/components/SEO'
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 RecaptchaPlaceholder from '@/components/RecaptchaPlaceholder'
import { ArrowRight } from 'lucide-react'
const Contact = () => {
const formRef = useRef(null)
const [formState, setFormState] = useState({
'Last Name': '',
Company: '',
Email: '',
Phone: '',
'Zip Code': '',
Description: '',
})
const [errors, setErrors] = useState({
'Last Name': '',
Company: '',
Email: '',
'Zip Code': '',
Description: '',
recaptcha_token: '',
})
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 = { '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'
}
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
}
const handleSubmit = (e) => {
e.preventDefault()
if (!validateForm()) return
setIsSubmitting(true)
formRef.current.submit()
}
const handleChange = (e) => {
const { name, value } = e.target
setFormState(prev => ({ ...prev, [name]: value }))
if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }))
}
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 = [
<div key="8x8"><span className="font-numeric">8x8</span> Certified Partner with proven expertise</div>,
'Cisco Certified Partner',
<div key="veteran"><span className="font-numeric">25+</span> years of experience</div>,
'SMB to Enterprise solutions',
'No vendor bias — we recommend what fits',
]
return (
<>
<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 */}
<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">
<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">
Contact
</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>
<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>
</div>
</section>
{/* Contact Body */}
<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>
<div className="border-t border-white/10" />
<div>
<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>
</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>
<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">
Company Name <span className="text-red-500">*</span>
</label>
<Input
type="text"
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' : ''}
/>
{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="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="Last_Name"
name="Last Name"
value={formState['Last Name']}
onChange={handleChange}
required
placeholder="Your full name"
className={debouncedErrors['Last Name'] ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{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">
Email <span className="text-red-500">*</span>
</label>
<Input
type="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' : ''}
/>
{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">
Phone <span className="text-soft-text font-normal">(optional)</span>
</label>
<Input
type="tel"
id="Phone"
name="Phone"
value={formState.Phone}
onChange={handleChange}
placeholder="(555) 123-4567"
/>
</div>
<div>
<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_Code"
name="Zip Code"
value={formState['Zip Code']}
onChange={handleChange}
required
autoComplete="postal-code"
inputMode="numeric"
placeholder="33702"
className={debouncedErrors['Zip Code'] ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{debouncedErrors['Zip Code'] && <p className="text-xs text-red-500 mt-1">{debouncedErrors['Zip Code']}</p>}
</div>
</div>
{/* Message */}
<div>
<label htmlFor="Description" className="block text-sm font-medium text-text mb-1.5">
Message <span className="text-red-500">*</span>
</label>
<Textarea
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.Description ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
rows={5}
/>
{debouncedErrors.Description && <p className="text-xs text-red-500 mt-1">{debouncedErrors.Description}</p>}
</div>
<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>
<iframe
name="zoho_webform_iframe"
id="zoho_webform_iframe"
title="Zoho form submission"
style={{ display: 'none' }}
/>
</div>
</div>
</div>
</section>
</>
)
}
export default Contact