267 lines
9.7 KiB
TypeScript
267 lines
9.7 KiB
TypeScript
'use client';
|
||
import { useState, useEffect } from 'react';
|
||
import { track } from '@/lib/analytics';
|
||
import Image from 'next/image';
|
||
|
||
export default function ContactForm({
|
||
compact = false,
|
||
variant = 'light'
|
||
}: {
|
||
compact?: boolean;
|
||
variant?: 'light' | 'dark';
|
||
}) {
|
||
const [ok, setOk] = useState(false);
|
||
const [loading, setLoading] = useState(false);
|
||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
const [mounted, setMounted] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
const labelColor = variant === 'dark' ? 'text-white' : 'text-gray-700';
|
||
|
||
const submit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
setLoading(true);
|
||
setErrors({});
|
||
|
||
const formData = new FormData(e.currentTarget);
|
||
const data: Record<string, string> = {};
|
||
for (const [key, value] of formData) {
|
||
data[key] = value.toString();
|
||
}
|
||
|
||
// Basic validation
|
||
const newErrors: Record<string, string> = {};
|
||
if (!data.name) newErrors.name = 'Name is required';
|
||
if (!data.phone) newErrors.phone = 'Phone is required';
|
||
if (!data.email) newErrors.email = 'Email is required';
|
||
if (!data.serviceType) newErrors.serviceType = 'Service type is required';
|
||
if (!data.issue) newErrors.issue = 'Brief issue description is required';
|
||
if (!data.preferredTime) newErrors.preferredTime = 'Preferred time is required';
|
||
|
||
if (Object.keys(newErrors).length > 0) {
|
||
setErrors(newErrors);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// TODO: wire to API or form service
|
||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||
track('form_submit', { source: 'quote_form', compact });
|
||
setOk(true);
|
||
} catch (error) {
|
||
console.error('Form submission failed:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (!mounted) {
|
||
// Render a stable skeleton on server and on initial client render to avoid hydration mismatches
|
||
return (
|
||
<div className={`bg-white rounded-lg shadow-lg p-6 ${compact ? 'max-w-sm' : 'max-w-lg'} mx-auto`}>
|
||
<div className="h-6 w-32 bg-gray-200 rounded mb-6 mx-auto" />
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="h-10 bg-gray-200 rounded" />
|
||
<div className="h-10 bg-gray-200 rounded" />
|
||
</div>
|
||
<div className="h-10 bg-gray-200 rounded" />
|
||
<div className="h-10 bg-gray-200 rounded" />
|
||
<div className="h-24 bg-gray-200 rounded" />
|
||
<div className="h-10 bg-gray-200 rounded" />
|
||
<div className="h-12 bg-gray-300 rounded" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (ok) {
|
||
return (
|
||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 text-center">
|
||
<div className="text-4xl mb-4">✅</div>
|
||
<h3 className="font-bold text-xl text-green-600 mb-2">Thank You!</h3>
|
||
<p className="text-gray-700">
|
||
We'll call you within <strong>15–30 minutes</strong> during business hours.
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`bg-white rounded-lg shadow-lg p-6 ${compact ? 'max-w-sm' : 'max-w-lg'} mx-auto`}>
|
||
<div className="flex items-center justify-center mb-6">
|
||
<Image
|
||
src="/images/favicon.png"
|
||
alt="C & I Electrical Contractors"
|
||
width={32}
|
||
height={32}
|
||
className="mr-2"
|
||
/>
|
||
<span className="text-lg font-semibold text-gray-800">Get Free Quote</span>
|
||
</div>
|
||
|
||
<form onSubmit={submit} noValidate className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label htmlFor="name" className={`block text-sm font-medium ${labelColor} mb-1`}>
|
||
Name *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="name"
|
||
name="name"
|
||
required
|
||
className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-1 focus:ring-green-500 text-sm ${
|
||
errors.name ? 'border-red-500' : 'border-gray-300'
|
||
}`}
|
||
aria-invalid={!!errors.name}
|
||
aria-describedby={errors.name ? 'name-error' : undefined}
|
||
/>
|
||
{errors.name && (
|
||
<p id="name-error" className="mt-1 text-xs text-red-500">
|
||
{errors.name}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="phone" className={`block text-sm font-medium ${labelColor} mb-1`}>
|
||
Phone *
|
||
</label>
|
||
<input
|
||
type="tel"
|
||
id="phone"
|
||
name="phone"
|
||
required
|
||
className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-1 focus:ring-green-500 text-sm ${
|
||
errors.phone ? 'border-red-500' : 'border-gray-300'
|
||
}`}
|
||
aria-invalid={!!errors.phone}
|
||
aria-describedby={errors.phone ? 'phone-error' : undefined}
|
||
/>
|
||
{errors.phone && (
|
||
<p id="phone-error" className="mt-1 text-xs text-red-500">
|
||
{errors.phone}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="email" className={`block text-sm font-medium ${labelColor} mb-1`}>
|
||
Email *
|
||
</label>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
name="email"
|
||
required
|
||
className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-1 focus:ring-green-500 text-sm ${
|
||
errors.email ? 'border-red-500' : 'border-gray-300'
|
||
}`}
|
||
aria-invalid={!!errors.email}
|
||
aria-describedby={errors.email ? 'email-error' : undefined}
|
||
/>
|
||
{errors.email && (
|
||
<p id="email-error" className="mt-1 text-xs text-red-500">
|
||
{errors.email}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="serviceType" className={`block text-sm font-medium ${labelColor} mb-1`}>
|
||
Service Type *
|
||
</label>
|
||
<select
|
||
id="serviceType"
|
||
name="serviceType"
|
||
required
|
||
className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-1 focus:ring-green-500 text-sm ${
|
||
errors.serviceType ? 'border-red-500' : 'border-gray-300'
|
||
}`}
|
||
aria-invalid={!!errors.serviceType}
|
||
aria-describedby={errors.serviceType ? 'serviceType-error' : undefined}
|
||
>
|
||
<option value="">Select service type</option>
|
||
<option value="emergency">Emergency Repair</option>
|
||
<option value="panel-upgrade">Panel Upgrade</option>
|
||
<option value="lighting">Lighting & Fixtures</option>
|
||
<option value="ev-charging">EV Charging Station</option>
|
||
<option value="commercial">Commercial Work</option>
|
||
<option value="other">Other</option>
|
||
</select>
|
||
{errors.serviceType && (
|
||
<p id="serviceType-error" className="mt-1 text-xs text-red-500">
|
||
{errors.serviceType}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="issue" className={`block text-sm font-medium ${labelColor} mb-1`}>
|
||
Brief Issue Description *
|
||
</label>
|
||
<textarea
|
||
id="issue"
|
||
name="issue"
|
||
rows={3}
|
||
required
|
||
className={`w-full px-3 py-2 border-2 border-green-500 rounded focus:outline-none focus:ring-1 focus:ring-green-500 text-sm ${
|
||
errors.issue ? 'border-red-500' : 'border-green-500'
|
||
}`}
|
||
placeholder="Describe your electrical problem or project..."
|
||
aria-invalid={!!errors.issue}
|
||
aria-describedby={errors.issue ? 'issue-error' : undefined}
|
||
/>
|
||
{errors.issue && (
|
||
<p id="issue-error" className="mt-1 text-xs text-red-500">
|
||
{errors.issue}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="preferredTime" className={`block text-sm font-medium ${labelColor} mb-1`}>
|
||
Preferred Time *
|
||
</label>
|
||
<select
|
||
id="preferredTime"
|
||
name="preferredTime"
|
||
required
|
||
className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-1 focus:ring-green-500 text-sm ${
|
||
errors.preferredTime ? 'border-red-500' : 'border-gray-300'
|
||
}`}
|
||
aria-invalid={!!errors.preferredTime}
|
||
aria-describedby={errors.preferredTime ? 'preferredTime-error' : undefined}
|
||
>
|
||
<option value="">Select preferred time</option>
|
||
<option value="emergency">Emergency (same day)</option>
|
||
<option value="morning">Morning (7AM-12PM)</option>
|
||
<option value="afternoon">Afternoon (12PM-5PM)</option>
|
||
<option value="evening">Evening (5PM-8PM)</option>
|
||
<option value="flexible">Flexible</option>
|
||
</select>
|
||
{errors.preferredTime && (
|
||
<p id="preferredTime-error" className="mt-1 text-xs text-red-500">
|
||
{errors.preferredTime}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="w-full bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-4 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{loading ? 'Sending...' : 'Get My Free Quote'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|