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:
null 2026-05-25 18:17:16 -05:00
parent 529cce7ec0
commit 09926fed6d
3 changed files with 47 additions and 21 deletions

View File

@ -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

View File

@ -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()

View File

@ -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">