392 lines
18 KiB
JavaScript
392 lines
18 KiB
JavaScript
import SEO from '@/components/SEO'
|
||
import { useState } 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 [formState, setFormState] = useState({
|
||
company: '',
|
||
name: '',
|
||
email: '',
|
||
phone: '',
|
||
zip: '',
|
||
message: '',
|
||
service_interest: '',
|
||
recaptcha_token: '',
|
||
company_website: '',
|
||
})
|
||
const [errors, setErrors] = useState({
|
||
company: '',
|
||
name: '',
|
||
email: '',
|
||
zip: '',
|
||
message: '',
|
||
recaptcha_token: '',
|
||
})
|
||
const debouncedErrors = useDebounce(errors, 300)
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
|
||
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 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
|
||
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)
|
||
}
|
||
}
|
||
|
||
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 Queue North
|
||
</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 onSubmit={handleSubmit} noValidate className={`space-y-5 ${isSubmitting ? 'opacity-70 pointer-events-none' : ''}`}>
|
||
{/* 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="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}
|
||
onChange={handleChange}
|
||
required
|
||
placeholder="Your full name"
|
||
className={debouncedErrors.name ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||
/>
|
||
{debouncedErrors.name && <p className="text-xs text-red-500 mt-1">{debouncedErrors.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" 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}
|
||
onChange={handleChange}
|
||
required
|
||
autoComplete="postal-code"
|
||
inputMode="numeric"
|
||
placeholder="33702"
|
||
className={debouncedErrors.zip ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||
/>
|
||
{debouncedErrors.zip && <p className="text-xs text-red-500 mt-1">{debouncedErrors.zip}</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">
|
||
Message <span className="text-red-500">*</span>
|
||
</label>
|
||
<Textarea
|
||
id="message"
|
||
name="message"
|
||
value={formState.message}
|
||
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' : ''}`}
|
||
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}
|
||
/>
|
||
</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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default Contact
|