remove submit ticket
This commit is contained in:
parent
85d7ae4bb1
commit
335601b00e
|
|
@ -1,13 +1,4 @@
|
||||||
import SEO from '@/components/SEO'
|
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 { submitSupport } from '@/lib/api'
|
|
||||||
import { useDebounce } from '@/hooks/useDebounce'
|
|
||||||
import { AlertCircle, ArrowRight, CheckCircle2, Clock3, ExternalLink, Headphones, LifeBuoy, ShieldCheck, TicketCheck, Wrench } from 'lucide-react'
|
import { AlertCircle, ArrowRight, CheckCircle2, Clock3, ExternalLink, Headphones, LifeBuoy, ShieldCheck, TicketCheck, Wrench } from 'lucide-react'
|
||||||
|
|
||||||
const portalLinks = [
|
const portalLinks = [
|
||||||
|
|
@ -37,114 +28,6 @@ const supportedSystems = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const Support = () => {
|
const Support = () => {
|
||||||
const [formState, setFormState] = useState({
|
|
||||||
name: '',
|
|
||||||
company: '',
|
|
||||||
email: '',
|
|
||||||
phone: '',
|
|
||||||
issue: '',
|
|
||||||
priority: 'medium',
|
|
||||||
recaptcha_token: '',
|
|
||||||
company_website: '', // Honeypot field - hidden from humans, bots will fill it
|
|
||||||
})
|
|
||||||
const [errors, setErrors] = useState({
|
|
||||||
name: '',
|
|
||||||
company: '',
|
|
||||||
email: '',
|
|
||||||
issue: '',
|
|
||||||
recaptcha_token: '',
|
|
||||||
})
|
|
||||||
// Debounce validation errors so they don't flash on every keystroke
|
|
||||||
const debouncedErrors = useDebounce(errors, 300)
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
|
|
||||||
const validateForm = () => {
|
|
||||||
const newErrors = {
|
|
||||||
name: '',
|
|
||||||
company: '',
|
|
||||||
email: '',
|
|
||||||
issue: '',
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
recaptcha_token: '',
|
|
||||||
company_website: '',
|
|
||||||
})
|
|
||||||
setErrors({
|
|
||||||
name: '',
|
|
||||||
company: '',
|
|
||||||
email: '',
|
|
||||||
issue: '',
|
|
||||||
recaptcha_token: '',
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
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 }))
|
|
||||||
// Clear error for this field as user types
|
|
||||||
if (errors[name]) {
|
|
||||||
setErrors(prev => ({ ...prev, [name]: '' }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO
|
<SEO
|
||||||
|
|
@ -174,20 +57,25 @@ const Support = () => {
|
||||||
Get help without getting handed off.
|
Get help without getting handed off.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-6 text-lg md:text-xl text-white/75 max-w-2xl leading-relaxed">
|
<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.
|
Sign in to the client portal to manage tickets, create a new support request, or escalate a service-impacting issue through one clear path.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-col sm:flex-row gap-3">
|
<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
|
<a
|
||||||
href={portalLinks[0].href}
|
href={portalLinks[0].href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={portalLinks[1].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"
|
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
|
Create Account
|
||||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -202,7 +90,7 @@ const Support = () => {
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold">Existing client</h3>
|
<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>
|
<p className="text-sm text-white/70">Sign in to create, update, and track support tickets.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
|
|
@ -222,7 +110,7 @@ const Support = () => {
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold">Planned work</h3>
|
<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>
|
<p className="text-sm text-white/70">Use the portal for moves, adds, changes, and deployments.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -231,12 +119,11 @@ const Support = () => {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Support Form */}
|
{/* Support Portal */}
|
||||||
<section id="support-request" className="bg-background py-16 lg:py-24">
|
<section id="support-portal" 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="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">
|
<div className="grid grid-cols-1 lg:grid-cols-[0.95fr_1.05fr] gap-8 lg:gap-10 items-start">
|
||||||
{/* Left - Info — order 2 on mobile so form appears first */}
|
<div className="space-y-6">
|
||||||
<div className="order-2 lg:order-1 space-y-6">
|
|
||||||
<div className="rounded-md border border-border border-t-[3px] border-t-primary-blue bg-white p-6 shadow-sm">
|
<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">
|
<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">
|
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-md bg-primary-navy text-white">
|
||||||
|
|
@ -245,7 +132,7 @@ const Support = () => {
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-primary-navy">Support Center</h2>
|
<h2 className="text-xl font-bold text-primary-navy">Support Center</h2>
|
||||||
<p className="mt-2 text-sm leading-relaxed text-soft-text">
|
<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.
|
Use the client portal to create requests, manage existing tickets, add updates, and keep support history in one place.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -304,160 +191,65 @@ const Support = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right - Form — order 1 on mobile so it appears first */}
|
<div>
|
||||||
<div className="order-1 lg:order-2">
|
<div className="rounded-md border border-border border-t-[3px] border-t-accent-gold bg-white p-6 shadow-sm lg:p-8">
|
||||||
<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="flex items-start gap-4 border-b border-border pb-6">
|
||||||
<div className="border-b border-border pb-5">
|
<span className="flex h-12 w-12 shrink-0 items-center justify-center rounded-md bg-primary-navy text-white">
|
||||||
<h2 className="text-2xl font-bold text-primary-navy">Submit a Request</h2>
|
<TicketCheck className="h-6 w-6" aria-hidden="true" />
|
||||||
<p className="mt-2 text-sm text-soft-text">
|
</span>
|
||||||
Include who is affected, what changed, and how urgent the issue is.
|
<div>
|
||||||
|
<p className="text-sm font-semibold uppercase tracking-wide text-primary-blue">Client Portal</p>
|
||||||
|
<h2 className="mt-2 text-3xl font-bold text-primary-navy">Manage support in Zoho Desk.</h2>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-soft-text">
|
||||||
|
Existing clients can sign in to submit new tickets, review open items, and keep communications tied to the right account.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
<label htmlFor="company" className="block text-sm font-medium text-text mb-2">
|
<a
|
||||||
Company <span className="text-red-600">*</span>
|
href={portalLinks[0].href}
|
||||||
</label>
|
target="_blank"
|
||||||
<Input
|
rel="noopener noreferrer"
|
||||||
type="text"
|
className="inline-flex h-11 items-center justify-center gap-2 rounded-md bg-primary-navy px-5 text-sm font-semibold text-white hover:bg-primary-navy-dark transition-colors"
|
||||||
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>
|
|
||||||
|
|
||||||
<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>
|
Sign In
|
||||||
<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>
|
|
||||||
|
|
||||||
<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 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" />
|
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||||
</>
|
</a>
|
||||||
)}
|
<a
|
||||||
</Button>
|
href={portalLinks[1].href}
|
||||||
</form>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex h-11 items-center justify-center gap-2 rounded-md border border-border bg-white px-5 text-sm font-semibold text-primary-navy hover:bg-section-alt transition-colors"
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 grid gap-3">
|
||||||
|
{[
|
||||||
|
'Submit and track support tickets',
|
||||||
|
'Add updates without starting a new thread',
|
||||||
|
'Keep request history tied to your account',
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item} className="flex items-center gap-3 rounded-md border border-border bg-background p-4 text-sm font-medium text-text">
|
||||||
|
<CheckCircle2 className="h-5 w-5 shrink-0 text-primary-blue" aria-hidden="true" />
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 rounded-md border border-border bg-section-alt p-4">
|
||||||
|
<p className="text-sm font-semibold text-primary-navy">Service-impacting issue?</p>
|
||||||
|
<p className="mt-1 text-sm text-soft-text">
|
||||||
|
Call Queue North directly for urgent outages or time-sensitive escalations.
|
||||||
|
</p>
|
||||||
|
<a href="tel:+13217308020" className="mt-3 inline-flex text-sm font-semibold text-primary-blue hover:text-primary-navy transition-colors">
|
||||||
|
(321) 730-8020
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue