This commit is contained in:
2025-08-22 14:11:18 -05:00
commit 3e9ca1a146
88 changed files with 14387 additions and 0 deletions

View 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>1530 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>
);
}