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

328 lines
13 KiB
React
Raw Normal View History

2026-05-12 01:04:17 -05:00
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
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'
import { Select } from '@/components/ui/Select'
import { Link } from 'react-router-dom'
2026-05-12 01:04:17 -05:00
import { api } from '@/lib/api'
import { useDebounce } from '@/hooks/useDebounce'
2026-05-12 01:04:17 -05:00
const Contact = () => {
const [formState, setFormState] = useState({
company: '',
name: '',
email: '',
phone: '',
zip: '',
message: '',
service_interest: '',
})
const [errors, setErrors] = useState({
company: '',
name: '',
email: '',
message: '',
})
// Debounce validation errors so they don't flash on every keystroke
const debouncedErrors = useDebounce(errors, 300)
2026-05-12 01:04:17 -05:00
const mutation = useMutation({
mutationFn: (data) => api.post('/leads', data),
onSuccess: () => {
toast.success('Thanks! We\'ll be in touch shortly.')
setFormState({
company: '',
name: '',
email: '',
phone: '',
zip: '',
message: '',
service_interest: '',
})
setErrors({
company: '',
name: '',
email: '',
message: '',
})
2026-05-12 01:04:17 -05:00
},
onError: (error) => {
toast.error(error.message || 'Failed to submit form. Please try again.')
},
})
const validateForm = () => {
const newErrors = {
company: '',
name: '',
email: '',
message: '',
}
// Validate required fields
if (!formState.company.trim()) newErrors.company = 'Company name is required'
if (!formState.name.trim()) newErrors.name = 'Name is required'
if (!formState.message.trim()) newErrors.message = 'Message is required'
// Validate email format
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
}
2026-05-12 01:04:17 -05:00
const handleSubmit = (e) => {
e.preventDefault()
if (!validateForm()) return
2026-05-12 01:04:17 -05:00
mutation.mutate(formState)
}
const handleChange = (e) => {
const { name, value } = e.target
setFormState(prev => ({ ...prev, [name]: value }))
// Clear error for this field as user types
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }))
}
2026-05-12 01:04:17 -05:00
}
return (
<>
2026-05-12 01:04:17 -05:00
{/* Page Hero */}
<section className="bg-background py-16 lg:py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div>
<h1 className="text-4xl md:text-5xl font-bold text-primary-navy mb-6">Contact Us</h1>
<p className="text-xl text-soft-text max-w-3xl mb-8">
Have questions about our services? We're here to help. Fill out the form and we'll get back to you shortly.
2026-05-12 01:04:17 -05:00
</p>
<div className="flex flex-wrap gap-4 mb-8">
<div className="flex items-center gap-2 px-4 py-2 bg-primary-navy/10 rounded-lg text-primary-navy">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 5.13a1 1 0 01-1.31.42a2 2 0 00-2.63-.54l-2.238 5.03a2 2 0 01-2.71.319L2.663 12H4a2 2 0 012 2v2a2 2 0 01-2 2H2a2 2 0 01-2-2V5z" />
</svg>
<a href="tel:+19064826616" className="text-sm font-medium hover:underline">(906) 482-6616</a>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-primary-navy/10 rounded-lg text-primary-navy">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<a href="mailto:info@queuenorth.com" className="text-sm font-medium hover:underline">info@queuenorth.com</a>
2026-05-12 01:04:17 -05:00
</div>
</div>
<div className="mb-8">
<Link to="/contact" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-6 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors">
Request Consultation
</Link>
</div>
2026-05-12 01:04:17 -05:00
</div>
</div>
</section>
2026-05-12 01:04:17 -05:00
{/* Contact Form */}
<section className="bg-background py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Left - Info */}
2026-05-12 01:04:17 -05:00
<div>
<div className="mb-8">
<h2 className="text-2xl font-bold text-primary-navy mb-4">Get in Touch</h2>
<p className="text-soft-text mb-6">
Our team of communications and infrastructure experts is ready to help you find the right solution for your business needs.
</p>
<div className="space-y-4">
<div>
<h3 className="font-semibold text-text mb-2">Hours of Operation</h3>
<p className="text-soft-text">Monday - Friday: 8:00 AM - 6:00 PM CT</p>
</div>
<div>
<h3 className="font-semibold text-text mb-2">Phone</h3>
<p className="text-soft-text"><a href="tel:+19064826616" className="hover:underline">(906) 482-6616</a></p>
</div>
<div>
<h3 className="font-semibold text-text mb-2">Contact Us</h3>
<p className="text-soft-text">Use the form on the right to get in touch with our team.</p>
</div>
</div>
</div>
2026-05-12 01:04:17 -05:00
<div className="bg-section-alt rounded-lg p-6">
<h3 className="font-semibold text-primary-navy mb-4">Why Choose Queue North?</h3>
<ul className="space-y-3">
{[
'8x8 Certified Partner with proven expertise',
'25+ years of industry experience',
'SMB to Enterprise solutions',
'Focus on your business outcomes',
].map((item, index) => (
<li key={index} className="flex items-center gap-3 text-text">
<svg className="h-5 w-5 text-primary-navy" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
{item}
</li>
))}
</ul>
</div>
2026-05-12 01:04:17 -05:00
</div>
{/* Right - Form */}
2026-05-12 01:04:17 -05:00
<div>
<form onSubmit={handleSubmit} noValidate className={`space-y-6 ${mutation.isPending ? 'opacity-70 pointer-events-none' : ''}`}>
<div>
<label htmlFor="company" className="block text-sm font-medium text-text mb-2">
Company Name <span className="text-red-600">*</span>
</label>
<Input
type="text"
id="company"
name="company"
value={formState.company}
onChange={handleChange}
required
placeholder="Your company name"
className={errors.company ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{debouncedErrors.company && (
<p className="text-xs text-red-600 mt-1">{debouncedErrors.company}</p>
)}
</div>
2026-05-12 01:04:17 -05:00
<div>
<label htmlFor="name" className="block text-sm font-medium text-text mb-2">
Name <span className="text-red-600">*</span>
</label>
<Input
type="text"
id="name"
name="name"
value={formState.name}
onChange={handleChange}
required
placeholder="Your full name"
className={errors.name ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{debouncedErrors.name && (
<p className="text-xs text-red-600 mt-1">{debouncedErrors.name}</p>
)}
</div>
2026-05-12 01:04:17 -05:00
<div>
<label htmlFor="email" className="block text-sm font-medium text-text mb-2">
Email <span className="text-red-600">*</span>
</label>
<Input
type="email"
id="email"
name="email"
value={formState.email}
onChange={handleChange}
required
placeholder="your.email@example.com"
className={errors.email ? 'border-red-500 focus-visible:ring-red-500' : ''}
/>
{debouncedErrors.email && (
<p className="text-xs text-red-600 mt-1">{debouncedErrors.email}</p>
)}
</div>
2026-05-12 01:04:17 -05:00
<div>
<label htmlFor="phone" className="block text-sm font-medium text-text mb-2">
Phone (Optional)
</label>
<Input
type="tel"
id="phone"
name="phone"
value={formState.phone}
onChange={handleChange}
placeholder="(555) 123-4567"
/>
</div>
2026-05-12 01:04:17 -05:00
<div>
<label htmlFor="zip" className="block text-sm font-medium text-text mb-2">
ZIP Code (Optional)
</label>
<Input
type="text"
id="zip"
name="zip"
value={formState.zip}
onChange={handleChange}
placeholder="12345"
/>
</div>
<div>
<label htmlFor="service_interest" className="block text-sm font-medium text-text mb-2">
Service Interest (Optional)
</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>
</Select>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-text mb-2">
Message <span className="text-red-600">*</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 ${errors.message ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
rows={5}
/>
{debouncedErrors.message && (
<p className="text-xs text-red-600 mt-1">{debouncedErrors.message}</p>
)}
</div>
2026-05-12 01:04:17 -05:00
<Button
type="submit"
className="w-full"
disabled={mutation.isPending}
>
{mutation.isPending ? 'Submitting...' : 'Request Consultation'}
</Button>
</form>
</div>
</div>
2026-05-12 01:04:17 -05:00
</div>
</section>
</>
2026-05-12 01:04:17 -05:00
)
}
export default Contact