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

470 lines
20 KiB
React
Raw Normal View History

2026-05-18 13:55:06 -05:00
import SEO from '@/components/SEO'
2026-05-12 01:04:17 -05:00
import { useState } 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'
import { Select } from '@/components/ui/Select'
2026-05-25 20:20:15 -05:00
import RecaptchaPlaceholder from '@/components/RecaptchaPlaceholder'
import { submitSupport } from '@/lib/api'
import { useDebounce } from '@/hooks/useDebounce'
import { AlertCircle, ArrowRight, CheckCircle2, Clock3, ExternalLink, Headphones, LifeBuoy, ShieldCheck, TicketCheck, Wrench } from 'lucide-react'
const portalLinks = [
{
label: 'Sign in',
href: 'https://queuenorthtechnologiesllc.zohodesk.com/portal/en/signin',
},
{
label: 'Create account',
href: 'https://queuenorthtechnologiesllc.zohodesk.com/portal/en/signup',
},
]
const responseTargets = [
{ priority: 'Low', context: 'General request', target: '24 hours' },
{ priority: 'Medium', context: 'Standard issue', target: '4 hours' },
{ priority: 'High', context: 'Critical outage', target: '1 hour' },
]
const supportedSystems = [
'8x8 Communications Platform',
2026-05-26 12:31:26 -05:00
'Cisco Webex',
'VoIP Phone Systems',
'Contact Center Solutions',
'Network Infrastructure',
'Cloud Migration Support',
]
2026-05-12 01:04:17 -05:00
const Support = () => {
const [formState, setFormState] = useState({
name: '',
company: '',
email: '',
phone: '',
issue: '',
priority: 'medium',
2026-05-25 20:20:15 -05:00
recaptcha_token: '',
company_website: '', // Honeypot field - hidden from humans, bots will fill it
2026-05-12 01:04:17 -05:00
})
const [errors, setErrors] = useState({
name: '',
company: '',
email: '',
issue: '',
2026-05-25 20:20:15 -05:00
recaptcha_token: '',
})
// Debounce validation errors so they don't flash on every keystroke
const debouncedErrors = useDebounce(errors, 300)
const [isSubmitting, setIsSubmitting] = useState(false)
2026-05-12 01:04:17 -05:00
const validateForm = () => {
const newErrors = {
name: '',
company: '',
email: '',
issue: '',
2026-05-25 20:20:15 -05:00
recaptcha_token: '',
}
// Validate required fields
if (!formState.name.trim()) newErrors.name = 'Name is required'
if (!formState.company.trim()) newErrors.company = 'Company name is required'
if (!formState.issue.trim()) newErrors.issue = 'Please describe your issue'
// 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'
}
// Validate issue minimum length (10 chars matches server-side Zod rule)
if (formState.issue.trim() && formState.issue.trim().length < 10) {
newErrors.issue = 'Issue description must be at least 10 characters'
}
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
handleSubmitForm()
}
const handleSubmitForm = async () => {
setIsSubmitting(true)
try {
await submitSupport(formState)
toast.success("Thanks! We\'ll get back to you soon.")
setFormState({
name: '',
company: '',
email: '',
phone: '',
issue: '',
priority: 'medium',
2026-05-25 20:20:15 -05:00
recaptcha_token: '',
company_website: '',
})
setErrors({
name: '',
company: '',
email: '',
issue: '',
2026-05-25 20:20:15 -05:00
recaptcha_token: '',
})
} catch (error) {
2026-05-25 20:20:15 -05:00
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)
}
2026-05-12 01:04:17 -05:00
}
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-18 13:55:06 -05:00
<SEO
title="IT Support & Help Desk | Queue North Technologies"
description="Get IT support and help desk services from Queue North Technologies. 24/7 monitoring, rapid response SLAs, and dedicated support engineers for your business."
url="https://queuenorth.com/support"
/>
2026-05-12 01:04:17 -05:00
{/* Page Hero */}
2026-05-26 12:22:11 -05:00
<section className="relative isolate overflow-hidden bg-primary-navy py-16 lg:py-24 text-white">
<div className="absolute inset-0 -z-10">
<img
src="/assets/modern-call-center.webp"
alt="Support team managing communications requests"
className="h-full w-full object-cover object-center"
/>
<div className="absolute inset-0 bg-primary-navy/84" />
<div className="absolute inset-0 bg-gradient-to-r from-primary-navy via-primary-navy/92 to-primary-navy/50" />
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-[1fr_0.72fr] gap-10 lg:gap-14 items-center">
<div>
<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">
<LifeBuoy className="h-4 w-4" aria-hidden="true" />
Queue North Support
</div>
<h1 className="mt-6 text-4xl md:text-5xl lg:text-6xl font-bold leading-tight">
Get help without getting handed off.
</h1>
<p className="mt-6 text-lg md:text-xl text-white/75 max-w-2xl leading-relaxed">
Open a support request, access the client portal, or escalate a service-impacting issue through one clear path.
</p>
<div className="mt-8 flex flex-col sm:flex-row gap-3">
<a href="#support-request" className="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">
Submit Request
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</a>
<a
href={portalLinks[0].href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-11 items-center justify-center gap-2 rounded-md border border-white/40 px-5 text-sm font-semibold text-white hover:bg-white/10 transition-colors"
>
Open Portal
<ExternalLink className="h-4 w-4" aria-hidden="true" />
</a>
</div>
2026-05-12 01:04:17 -05:00
</div>
<div className="rounded-md border border-white/15 bg-white/10 p-6 shadow-xl backdrop-blur">
<h2 className="text-lg font-semibold mb-5">Choose the right path</h2>
<div className="space-y-4">
<div className="flex gap-4">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-white/10 text-primary-cyan">
<TicketCheck className="h-5 w-5" aria-hidden="true" />
</span>
<div>
<h3 className="font-semibold">Existing client</h3>
<p className="text-sm text-white/70">Submit the form or use the portal to track requests.</p>
</div>
</div>
<div className="flex gap-4">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-white/10 text-primary-cyan">
<AlertCircle className="h-5 w-5" aria-hidden="true" />
</span>
<div>
<h3 className="font-semibold">Active outage</h3>
<a href="tel:+13217308020" className="text-sm font-semibold text-primary-cyan hover:text-white transition-colors" aria-label="Call Queue North support">
Call (321) 730-8020
</a>
</div>
</div>
<div className="flex gap-4">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-white/10 text-primary-cyan">
<Wrench className="h-5 w-5" aria-hidden="true" />
</span>
<div>
<h3 className="font-semibold">Planned work</h3>
<p className="text-sm text-white/70">Use the request form for moves, adds, changes, and deployments.</p>
</div>
</div>
</div>
</div>
2026-05-12 01:04:17 -05:00
</div>
</div>
</section>
2026-05-12 01:04:17 -05:00
{/* Support Form */}
2026-05-26 12:22:11 -05:00
<section id="support-request" className="bg-background py-16 lg:py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-[0.9fr_1.1fr] gap-8 lg:gap-10 items-start">
2026-05-18 13:45:39 -05:00
{/* Left - Info — order 2 on mobile so form appears first */}
<div className="order-2 lg:order-1 space-y-6">
2026-05-26 12:22:11 -05:00
<div className="rounded-md border border-border border-t-[3px] border-t-primary-blue bg-white p-6 shadow-sm">
<div className="flex items-start gap-4">
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-md bg-primary-navy text-white">
<Headphones className="h-5 w-5" aria-hidden="true" />
</span>
<div>
<h2 className="text-xl font-bold text-primary-navy">Support Center</h2>
<p className="mt-2 text-sm leading-relaxed text-soft-text">
Sign in to manage existing tickets, create an account, or submit the form on this page for a new request.
</p>
</div>
</div>
<div className="mt-6 flex flex-col sm:flex-row gap-3">
{portalLinks.map((link, index) => (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className={`inline-flex h-10 items-center justify-center gap-2 rounded-md px-4 text-sm font-semibold transition-colors ${index === 0 ? 'bg-primary-navy text-white hover:bg-primary-navy-dark' : 'border border-border bg-white text-primary-navy hover:bg-section-alt'}`}
>
{link.label}
<ExternalLink className="h-4 w-4" aria-hidden="true" />
</a>
))}
</div>
</div>
2026-05-12 01:04:17 -05:00
2026-05-26 12:22:11 -05:00
<div className="rounded-md border border-border border-t-[3px] border-t-teal-500 bg-white p-6 shadow-sm">
<div className="flex items-center gap-3 mb-5">
<span className="flex h-10 w-10 items-center justify-center rounded-md bg-section-alt text-primary-navy">
<Clock3 className="h-5 w-5" aria-hidden="true" />
</span>
<h2 className="text-xl font-bold text-primary-navy">Response Targets</h2>
</div>
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
{responseTargets.map((item) => (
<div key={item.priority} className="rounded-md border border-border bg-background p-4">
<div className="flex items-center justify-between gap-3">
<p className="font-semibold text-text">{item.priority}</p>
<p className="font-numeric text-xl text-primary-navy">{item.target}</p>
</div>
<p className="mt-1 text-sm text-soft-text">{item.context}</p>
</div>
))}
</div>
</div>
2026-05-26 12:22:11 -05:00
<div className="rounded-md border border-border border-t-[3px] border-t-cyan-400 bg-section-alt p-6">
<div className="flex items-center gap-3 mb-5">
<span className="flex h-10 w-10 items-center justify-center rounded-md bg-white text-primary-navy">
<ShieldCheck className="h-5 w-5" aria-hidden="true" />
</span>
<h2 className="text-xl font-bold text-primary-navy">Covered Systems</h2>
</div>
<ul className="space-y-3">
{supportedSystems.map((item) => (
<li key={item} className="flex items-center gap-3 text-sm text-text">
<CheckCircle2 className="h-5 w-5 shrink-0 text-primary-blue" aria-hidden="true" />
{item}
</li>
))}
</ul>
</div>
2026-05-12 01:04:17 -05:00
</div>
2026-05-18 13:45:39 -05:00
{/* Right - Form — order 1 on mobile so it appears first */}
<div className="order-1 lg:order-2">
2026-05-26 12:22:11 -05:00
<form onSubmit={handleSubmit} noValidate className={`rounded-md border border-border border-t-[3px] border-t-accent-gold bg-white p-6 shadow-sm lg:p-8 space-y-5 ${isSubmitting ? 'opacity-70 pointer-events-none' : ''}`}>
<div className="border-b border-border pb-5">
<h2 className="text-2xl font-bold text-primary-navy">Submit a Request</h2>
<p className="mt-2 text-sm text-soft-text">
Include who is affected, what changed, and how urgent the issue is.
</p>
</div>
2026-05-12 01:04:17 -05:00
<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-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="company" className="block text-sm font-medium text-text mb-2">
Company <span className="text-red-600">*</span>
</label>
<Input
type="text"
id="company"
name="company"
value={formState.company}
onChange={handleChange}
required
placeholder="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>
</div>
2026-05-12 01:04:17 -05:00
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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="you@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>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-text mb-2">
Phone <span className="text-soft-text">(optional)</span>
</label>
<Input
type="tel"
id="phone"
name="phone"
value={formState.phone}
onChange={handleChange}
placeholder="(555) 123-4567"
/>
</div>
</div>
<div>
<label htmlFor="priority" className="block text-sm font-medium text-text mb-2">
Priority <span className="text-red-600">*</span>
</label>
<Select
id="priority"
name="priority"
value={formState.priority}
onChange={handleChange}
>
<option value="low">Low - General inquiries (24 hours)</option>
<option value="medium">Medium - Standard issues (4 hours)</option>
<option value="high">High - Critical issues (1 hour)</option>
</Select>
</div>
<div>
<label htmlFor="issue" className="block text-sm font-medium text-text mb-2">
Issue Details <span className="text-red-600">*</span>
</label>
<Textarea
id="issue"
name="issue"
value={formState.issue}
onChange={handleChange}
required
placeholder="What changed, who is affected, and what have you already tried?"
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.issue ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
rows={5}
/>
{debouncedErrors.issue && (
<p className="text-xs text-red-600 mt-1">{debouncedErrors.issue}</p>
)}
</div>
2026-05-12 01:04:17 -05:00
<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>
2026-05-25 20:20:15 -05:00
<RecaptchaPlaceholder error={debouncedErrors.recaptcha_token} />
<Button
type="submit"
className="w-full gap-2"
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>
Submitting...
</>
) : (
<>
Submit Request
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</>
)}
</Button>
</form>
</div>
</div>
2026-05-12 01:04:17 -05:00
</div>
</section>
</>
2026-05-12 01:04:17 -05:00
)
}
export default Support