initial
This commit is contained in:
266
web/components/ContactForm.tsx
Normal file
266
web/components/ContactForm.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user