Contact.jsx (line 23): added ZIP to validation/error state, rendered it as required, and shows ZIP code is required.
server/index.js (line 253): backend Zod schema now rejects missing or blank ZIP. api.js (line 3): preserves backend field errors for display.
This commit is contained in:
parent
529cce7ec0
commit
09926fed6d
|
|
@ -255,7 +255,7 @@ const leadSchema = z.object({
|
|||
name: z.string().min(1, 'Name is required').trim().max(100, 'Name must be 100 characters or less'),
|
||||
email: z.string().email('Valid email is required').trim().max(254, 'Email must be 254 characters or less'),
|
||||
phone: z.string().trim().max(50, 'Phone must be 50 characters or less').optional().or(z.literal('').transform(() => undefined)),
|
||||
zip: z.string().trim().max(10, 'ZIP code must be 10 characters or less').optional().or(z.literal('').transform(() => undefined)),
|
||||
zip: z.string({ required_error: 'ZIP code is required' }).trim().min(1, 'ZIP code is required').max(10, 'ZIP code must be 10 characters or less'),
|
||||
message: z.string().trim().max(5000, 'Message must be 5000 characters or less').optional().or(z.literal('').transform(() => undefined)),
|
||||
service_interest: z.string().trim().max(50, 'Service interest must be 50 characters or less').optional().or(z.literal('').transform(() => undefined)),
|
||||
company_website: z.string().optional(), // Honeypot field - bots fill this, humans don't see it
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export async function submitLead(data) {
|
|||
const errorData = await response.json().catch(() => ({}))
|
||||
const error = new Error(errorData.error || `API error: ${response.status}`)
|
||||
error.response = { status: response.status }
|
||||
error.fields = errorData.fields
|
||||
throw error
|
||||
}
|
||||
return response.json()
|
||||
|
|
|
|||
|
|
@ -23,15 +23,17 @@ const Contact = () => {
|
|||
company: '',
|
||||
name: '',
|
||||
email: '',
|
||||
zip: '',
|
||||
message: '',
|
||||
})
|
||||
const debouncedErrors = useDebounce(errors, 300)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = { company: '', name: '', email: '', message: '' }
|
||||
const newErrors = { company: '', name: '', email: '', zip: '', message: '' }
|
||||
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()) {
|
||||
|
|
@ -59,11 +61,14 @@ const Contact = () => {
|
|||
try {
|
||||
await submitLead(formState)
|
||||
toast.success("Thanks! We'll be in touch shortly.")
|
||||
setFormState({ company: '', name: '', email: '', phone: '', zip: '', message: '', service_interest: '' })
|
||||
setErrors({ company: '', name: '', email: '', message: '' })
|
||||
setFormState({ company: '', name: '', email: '', phone: '', zip: '', message: '', service_interest: '', company_website: '' })
|
||||
setErrors({ company: '', name: '', email: '', zip: '', message: '' })
|
||||
} 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.')
|
||||
}
|
||||
|
|
@ -247,7 +252,7 @@ const Contact = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone + Service Interest */}
|
||||
{/* 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">
|
||||
|
|
@ -263,27 +268,47 @@ const Contact = () => {
|
|||
/>
|
||||
</div>
|
||||
<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 htmlFor="zip" className="block text-sm font-medium text-text mb-1.5">
|
||||
ZIP Code <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="service_interest"
|
||||
name="service_interest"
|
||||
value={formState.service_interest}
|
||||
<Input
|
||||
type="text"
|
||||
id="zip"
|
||||
name="zip"
|
||||
value={formState.zip}
|
||||
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>
|
||||
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>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-text mb-1.5">
|
||||
|
|
|
|||
Loading…
Reference in New Issue