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

392 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 } 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