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,14 @@
export default function CTASection({ title, description, phone }: { title: string; description: string; phone: string }) {
return (
<div className="bg-brand-navy text-white">
<div className="mx-auto max-w-7xl px-4 py-12 md:py-16">
<h2 className="text-2xl md:text-3xl font-bold">{title}</h2>
<p className="mt-2 text-white/80">{description}</p>
<div className="mt-4 flex gap-3">
<a className="rounded bg-brand-green px-4 py-2 font-semibold" href={`tel:${phone}`}>Call {phone}</a>
<a className="rounded bg-brand-orange px-4 py-2 font-semibold" href="/contact">Request a Quote</a>
</div>
</div>
</div>
);
}

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

52
web/components/FAQ.tsx Normal file
View File

@@ -0,0 +1,52 @@
'use client';
import { useState } from 'react';
import { track } from '@/lib/analytics';
export type QA = {
q: string;
a: string;
};
export default function FAQ({ items }: { items: QA[] }) {
const [open, setOpen] = useState<number | null>(0);
const toggleItem = (index: number) => {
if (open === index) {
setOpen(null);
} else {
setOpen(index);
track('accordion_open', { question: items[index].q });
}
};
return (
<section aria-labelledby="faq-heading" className="py-24 bg-white">
<div className="container-custom">
<h2 id="faq-heading" className="font-heading font-bold text-3xl text-center mb-12">
Frequently Asked Questions
</h2>
<div className="max-w-3xl mx-auto space-y-4">
{items.map((it, i) => (
<div key={i} className="card">
<button
className="w-full text-left flex items-center justify-between hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-brand-green focus:ring-inset rounded-card"
aria-expanded={open === i}
onClick={() => toggleItem(i)}
>
<span className="font-heading font-semibold text-lg text-brand-dark">{it.q}</span>
<span className="text-brand-green text-xl font-bold">
{open === i ? '' : '+'}
</span>
</button>
{open === i && (
<div className="mt-4 text-gray-600 leading-relaxed font-body">
{it.a}
</div>
)}
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,24 @@
export default function FeatureGrid() {
const items = [
{ title: 'No Power?', desc: 'Quick diagnosis and repair', foot: 'Back online fast' },
{ title: 'Tripping Breaker?', desc: 'Circuit analysis and upgrade', foot: 'End the frustration' },
{ title: 'New Lighting?', desc: 'Professional installation', foot: 'Brighter, safer spaces' },
{ title: 'Panel Upgrade?', desc: 'Safe, code-compliant upgrade', foot: 'Modern, reliable power' },
{ title: 'Hot/Sparking Outlet?', desc: 'Emergency safety repair', foot: 'Prevent electrical fires' },
{ title: 'EV Charger Install?', desc: 'Dedicated EV-ready circuits', foot: 'Charge at home safely' },
];
return (
<div>
<h2 className="text-2xl md:text-3xl font-bold mb-6">Electrical Problems We Solve</h2>
<ul className="grid md:grid-cols-3 gap-4">
{items.map((it) => (
<li key={it.title} className="rounded-lg border p-4">
<div className="font-semibold">{it.title}</div>
<div className="text-sm text-slate-600">{it.desc}</div>
<div className="text-amber-600 text-sm font-semibold mt-2">{it.foot}</div>
</li>
))}
</ul>
</div>
);
}

64
web/components/Footer.tsx Normal file
View File

@@ -0,0 +1,64 @@
import site from '@/content/site.json';
export default function Footer() {
const s = site.business;
return (
<footer className="bg-brand-dark text-white" role="contentinfo">
<div className="container-custom py-16">
<div className="grid md:grid-cols-4 gap-8">
{/* Company */}
<div>
<h3 className="font-heading font-semibold text-lg mb-4">Company</h3>
<ul className="space-y-2 text-sm">
<li><a href="/about" className="hover:text-brand-green transition-colors">About</a></li>
<li><a href="/reviews" className="hover:text-brand-green transition-colors">Reviews</a></li>
<li><a href="/contact" className="hover:text-brand-green transition-colors">Contact</a></li>
</ul>
</div>
{/* Our Services */}
<div>
<h3 className="font-heading font-semibold text-lg mb-4">Our Services</h3>
<ul className="space-y-2 text-sm">
<li><a href="/corpus-christi/emergency-electrician" className="hover:text-brand-green transition-colors">Emergency Repair</a></li>
<li><a href="/corpus-christi/panel-upgrades" className="hover:text-brand-green transition-colors">Panel Upgrades</a></li>
<li><a href="/corpus-christi/ev-charger-install" className="hover:text-brand-green transition-colors">EV Charging</a></li>
<li><a href="/residential" className="hover:text-brand-green transition-colors">Residential</a></li>
<li><a href="/commercial" className="hover:text-brand-green transition-colors">Commercial</a></li>
<li><a href="/corpus-christi/lighting-retrofits" className="hover:text-brand-green transition-colors">Lighting & Fixtures</a></li>
</ul>
</div>
{/* Service Areas */}
<div>
<h3 className="font-heading font-semibold text-lg mb-4">Service Areas</h3>
<ul className="space-y-2 text-sm">
<li>Corpus Christi</li>
<li>Flour Bluff</li>
<li>Portland</li>
<li>Aransas Pass</li>
<li>Rockport</li>
<li>Calallen</li>
</ul>
</div>
{/* Contact */}
<div>
<h3 className="font-heading font-semibold text-lg mb-4">Contact</h3>
<div className="space-y-2 text-sm">
<p>{s.address}</p>
<p><a href={`tel:${s.phoneRaw}`} className="hover:text-brand-green transition-colors">{s.phone}</a></p>
<p><a href={`mailto:${s.email}`} className="hover:text-brand-green transition-colors">{s.email}</a></p>
<p>Mon-Fri 7AM-5PM<br/>24/7 Emergency</p>
</div>
</div>
</div>
{/* Bottom line */}
<div className="border-t border-gray-700 mt-12 pt-8 text-center text-sm text-gray-400">
© 2024 C & I Electrical Contractors · Licensed & Insured · TX License TECL ####
</div>
</div>
</footer>
);
}

58
web/components/Header.tsx Normal file
View File

@@ -0,0 +1,58 @@
'use client';
import { track } from '@/lib/analytics';
export default function Header() {
return (
<header className="bg-white sticky top-0 z-50 shadow-lg border-b border-gray-200">
<nav aria-label="primary" className="container-custom py-4 flex items-center justify-between">
{/* Left: Logo */}
<a href="/" className="flex items-center gap-3 group">
<div className="w-12 h-12 bg-brand-green rounded-full flex items-center justify-center shadow-md group-hover:shadow-lg transition-shadow">
<img src="/images/favicon.png" alt="C & I Electrical Contractors" className="w-6 h-6" />
</div>
<div>
<div className="font-heading font-semibold text-lg text-brand-dark">C & I Electrical Contractors</div>
<div className="text-xs text-brand-green font-medium">Licensed & Insured</div>
</div>
</a>
{/* Middle: Navigation */}
<div className="hidden md:flex items-center gap-8">
<a href="/residential" className="font-body text-gray-700 hover:text-brand-green font-medium transition-colors">
Residential
</a>
<a href="/commercial" className="font-body text-gray-700 hover:text-brand-green font-medium transition-colors">
Commercial
</a>
<a href="/reviews" className="font-body text-gray-700 hover:text-brand-green font-medium transition-colors">
Reviews
</a>
<a href="/about" className="font-body text-gray-700 hover:text-brand-green font-medium transition-colors">
About
</a>
<a href="/contact" className="font-body text-gray-700 hover:text-brand-green font-medium transition-colors">
Contact
</a>
</div>
{/* Right: CTAs */}
<div className="flex items-center gap-3">
<a
href="tel:+13618850315"
onClick={() => track('phone_click', { placement: 'header' })}
className="btn-primary"
aria-label="Call Now, 24/7"
>
Call Now (24/7)
</a>
<a
href="#quote-form"
className="hidden md:inline-flex btn-secondary"
>
Free Quote
</a>
</div>
</nav>
</header>
);
}

35
web/components/Hero.tsx Normal file
View File

@@ -0,0 +1,35 @@
import Button from './ui/Button';
export default function Hero(props: {
title: string;
subtitle: string;
image: string;
ribbon?: string;
primaryCta?: { label: string; href: string };
secondaryCta?: { label: string; href: string };
}) {
return (
<div className="bg-brand-navy text-white">
{props.ribbon && <div className="bg-brand-red text-center text-sm py-1">{props.ribbon}</div>}
<div className="mx-auto max-w-7xl px-4 py-12 md:py-16 grid md:grid-cols-2 gap-8 items-center">
<div>
<h1 className="text-3xl md:text-5xl font-bold">{props.title}</h1>
<p className="mt-4 text-white/85">{props.subtitle}</p>
<div className="mt-6 flex gap-3">
<Button variant="success" href={props.primaryCta?.href ?? '/contact'}>{props.primaryCta?.label ?? 'Call Now — 24/7 Emergency Help'}</Button>
<Button href={props.secondaryCta?.href ?? '/contact'}>{props.secondaryCta?.label ?? 'Get My Free Quote'}</Button>
</div>
<div className="mt-6 flex gap-3 text-xs text-white/75">
<span className="rounded bg-white/10 px-2 py-1">Licensed</span>
<span className="rounded bg-white/10 px-2 py-1">Insured</span>
<span className="rounded bg-white/10 px-2 py-1">Local since 2005</span>
<span className="rounded bg-white/10 px-2 py-1">24/7 Emergency</span>
</div>
</div>
<div className="rounded-xl overflow-hidden bg-white/5">
<img src={props.image} alt="Electrician at work" className="w-full h-auto" />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,164 @@
'use client';
import { useState } from 'react';
interface Location {
name: string;
coordinates: [number, number];
description: string;
services: string[];
responseTime: string;
}
const locations: Location[] = [
{
name: 'Corpus Christi',
coordinates: [27.8006, -97.3964],
description: 'Main service area - 24/7 emergency response',
services: ['Emergency Repair', 'Panel Upgrades', 'EV Chargers', 'Commercial'],
responseTime: '< 30 minutes'
},
{
name: 'Flour Bluff',
coordinates: [27.6831, -97.2201],
description: 'Residential and commercial electrical services',
services: ['Emergency Repair', 'Panel Upgrades', 'Lighting'],
responseTime: '< 45 minutes'
},
{
name: 'Portland',
coordinates: [27.8772, -97.3239],
description: 'Full electrical contractor services',
services: ['Emergency Repair', 'Panel Upgrades', 'Commercial'],
responseTime: '< 60 minutes'
},
{
name: 'Aransas Pass',
coordinates: [27.9095, -97.1499],
description: 'Coastal electrical services',
services: ['Emergency Repair', 'Panel Upgrades', 'Marine Electrical'],
responseTime: '< 60 minutes'
},
{
name: 'Rockport',
coordinates: [28.0206, -97.0544],
description: 'Coastal and residential electrical',
services: ['Emergency Repair', 'Panel Upgrades', 'Outdoor Lighting'],
responseTime: '< 75 minutes'
}
];
export default function InteractiveMap() {
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null);
const [hoveredLocation, setHoveredLocation] = useState<string | null>(null);
return (
<div className="relative">
{/* Map Container */}
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-3xl h-96 relative overflow-hidden shadow-xl">
{/* Background Pattern */}
<div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23000000" fill-opacity="0.03"%3E%3Ccircle cx="30" cy="30" r="2"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')]"></div>
{/* Service Area Circle */}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-gradient-to-br from-brand-green/20 to-green-600/20 rounded-full border-2 border-brand-green/30 animate-pulse-slow"></div>
{/* Location Markers */}
{locations.map((location, index) => (
<div
key={location.name}
className={`absolute transform -translate-x-1/2 -translate-y-1/2 cursor-pointer transition-all duration-300 ${
hoveredLocation === location.name ? 'scale-125' : 'scale-100'
}`}
style={{
left: `${20 + (index * 15)}%`,
top: `${30 + (index * 10)}%`
}}
onMouseEnter={() => setHoveredLocation(location.name)}
onMouseLeave={() => setHoveredLocation(null)}
onClick={() => setSelectedLocation(location)}
>
{/* Marker */}
<div className={`w-4 h-4 bg-gradient-to-r from-brand-green to-green-600 rounded-full border-2 border-white shadow-lg ${
selectedLocation?.name === location.name ? 'ring-4 ring-brand-green/50' : ''
}`}></div>
{/* Location Name */}
<div className={`absolute top-6 left-1/2 transform -translate-x-1/2 whitespace-nowrap bg-white px-3 py-1 rounded-full text-xs font-semibold shadow-md transition-all duration-300 ${
hoveredLocation === location.name ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
}`}>
{location.name}
</div>
</div>
))}
{/* Center Point */}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-6 h-6 bg-gradient-to-r from-brand-green to-green-600 rounded-full border-4 border-white shadow-xl animate-bounce-gentle"></div>
{/* Map Title */}
<div className="absolute top-6 left-6 bg-white/90 backdrop-blur-sm px-4 py-2 rounded-full shadow-md">
<h3 className="font-semibold text-gray-800">Service Coverage Area</h3>
</div>
</div>
{/* Location Info Panel */}
{selectedLocation && (
<div className="absolute -bottom-4 left-1/2 transform -translate-x-1/2 w-full max-w-md animate-slide-up">
<div className="bg-white rounded-2xl shadow-2xl p-6 border border-gray-100">
<div className="flex items-start justify-between mb-4">
<h3 className="font-heading font-bold text-xl text-gradient">
{selectedLocation.name}
</h3>
<button
onClick={() => setSelectedLocation(null)}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p className="text-gray-600 mb-4">{selectedLocation.description}</p>
<div className="mb-4">
<h4 className="font-semibold text-gray-800 mb-2">Services Available:</h4>
<div className="flex flex-wrap gap-2">
{selectedLocation.services.map((service) => (
<span key={service} className="bg-brand-green/10 text-brand-green px-2 py-1 rounded-full text-xs font-medium">
{service}
</span>
))}
</div>
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-gray-500">Response Time:</span>
<p className="font-semibold text-brand-green">{selectedLocation.responseTime}</p>
</div>
<a
href="tel:+13618850315"
className="btn-primary btn-sm"
>
Call Now
</a>
</div>
</div>
</div>
)}
{/* Legend */}
<div className="absolute bottom-4 right-4 bg-white/90 backdrop-blur-sm px-4 py-3 rounded-xl shadow-md">
<div className="flex items-center gap-3 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gradient-to-r from-brand-green to-green-600 rounded-full"></div>
<span className="text-gray-700">Service Area</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gradient-to-r from-brand-green to-green-600 rounded-full border-2 border-white"></div>
<span className="text-gray-700">Locations</span>
</div>
</div>
</div>
</div>
);
}

51
web/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,51 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import site from '@/content/site.json';
import Button from './ui/Button';
import { useState } from 'react';
export default function Navbar() {
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-40 bg-white/90 backdrop-blur">
<nav className="mx-auto max-w-7xl px-4 h-16 flex items-center justify-between" aria-label="Main">
<Link href="/" className="flex items-center gap-2" aria-label="Go to home">
<div className="flex items-center gap-2">
<Image
src="/images/favicon.png"
alt="C & I Electrical Contractors"
width={32}
height={32}
/>
<div>
<div className="font-semibold text-gray-800">C & I Electrical Contractors</div>
<div className="text-xs text-green-600">Licensed & Insured</div>
</div>
</div>
</Link>
<button className="md:hidden p-2" aria-expanded={open} aria-controls="menu" onClick={()=>setOpen(!open)}></button>
<ul id="menu" className={`md:flex gap-6 items-center ${open ? 'block' : 'hidden'} md:block`}>
<li><Link href="/residential">Residential</Link></li>
<li><Link href="/commercial">Commercial</Link></li>
<li><Link href="/reviews">Reviews</Link></li>
<li><Link href="/about">About</Link></li>
<li><Link href="/contact">Contact</Link></li>
<li className="md:ml-4"><Button variant="success" href={`tel:${site.business.phoneRaw}`}>Call Now 24/7</Button></li>
<li><Button href="/contact">Get My Free Quote</Button></li>
</ul>
</nav>
<div className="bg-brand-red text-white text-center text-sm py-1" role="status">
<div className="flex items-center justify-center gap-2">
<Image
src="/images/favicon.png"
alt="C & I Electrical Contractors"
width={16}
height={16}
/>
24/7 Emergency Electrician Average response under 60 minutes in Corpus Christi
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,22 @@
export default function ReviewsGrid() {
const items = [
{ name: 'Maria S.', area: 'Ocean Drive', text: 'Henry and his team upgraded our electrical panel quickly and professionally. No more tripping breakers!' },
{ name: 'David R.', area: 'Downtown', text: 'Excellent work on our office build-out. They finished on time and everything works perfectly. Very professional and easy to work with.' },
{ name: 'Jennifer L.', area: 'Flour Bluff', text: 'Called for an emergency repair and they were here within 2 hours on a Sunday. Fixed our power issue quickly. Very reliable service.' },
{ name: 'Robert M.', area: 'Calallen', text: 'Great experience with our kitchen remodel electrical work. GFCI outlets and under-cabinet lighting. Beautiful work and fair pricing.' },
{ name: 'Lisa K.', area: 'Port Area', text: 'C&I installed LED lighting in our warehouse. Much brighter now and the energy savings are noticeable. Highly recommend.' },
{ name: 'Sarah P.', area: 'Robstown', text: 'Installed an EV charger outlet in our garage and handled the permit process. Very happy with the service.' },
];
return (
<ul className="grid md:grid-cols-3 gap-4">
{items.map((r, i) => (
<li key={i} className="rounded-lg border p-4">
<div className="text-amber-500"></div>
<p className="mt-2 text-sm text-slate-700">&ldquo;{r.text}&rdquo;</p>
<div className="mt-2 text-sm font-semibold">{r.name}</div>
<div className="text-xs text-slate-500">{r.area}</div>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,148 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
const items = [
{
title: 'Emergency Repair',
desc: 'Outages, hot outlets, breakers tripping.',
href: '/corpus-christi/emergency-electrician',
icon: '🚨',
image: '/images/emergency_repair.png',
bgColor: 'bg-gradient-to-br from-red-500 to-red-600',
textColor: 'text-red-600',
hoverColor: 'hover:from-red-600 hover:to-red-700'
},
{
title: 'Panel Upgrades',
desc: '100A→200A, AFCI/GFCI, permits & inspections.',
href: '/corpus-christi/panel-upgrades',
icon: '⚡',
image: '/images/panel_upgrade.png',
bgColor: 'bg-gradient-to-br from-blue-500 to-blue-600',
textColor: 'text-blue-600',
hoverColor: 'hover:from-blue-600 hover:to-blue-700'
},
{
title: 'Residential Services',
desc: 'Complete home electrical solutions.',
href: '/corpus-christi/residential',
icon: '🏠',
image: '/images/residential.png',
bgColor: 'bg-gradient-to-br from-yellow-500 to-orange-500',
textColor: 'text-orange-600',
hoverColor: 'hover:from-yellow-600 hover:to-orange-600'
},
{
title: 'EV-Ready Circuits',
desc: 'Dedicated 240V outlet, load calc.',
href: '/corpus-christi/ev-charger-install',
icon: '🔋',
image: '/images/ev_ready.png',
bgColor: 'bg-gradient-to-br from-green-500 to-green-600',
textColor: 'text-green-600',
hoverColor: 'hover:from-green-600 hover:to-green-700'
},
{
title: 'Commercial Build-Outs',
desc: 'Data, emergency lighting, distribution.',
href: '/commercial',
icon: '🏢',
image: '/images/comnercial_buildout.png',
bgColor: 'bg-gradient-to-br from-purple-500 to-purple-600',
textColor: 'text-purple-600',
hoverColor: 'hover:from-purple-600 hover:to-purple-700'
},
{
title: 'Commercial Services',
desc: 'Professional commercial electrical solutions.',
href: '/commercial',
icon: '🏢',
image: '/images/commercial.png',
bgColor: 'bg-gradient-to-br from-indigo-500 to-indigo-600',
textColor: 'text-indigo-600',
hoverColor: 'hover:from-indigo-600 hover:to-indigo-700'
},
{
title: 'Electrical Diagnostics',
desc: 'Advanced troubleshooting and system analysis.',
href: '/corpus-christi/diagnostics',
icon: '🔍',
image: '/images/diagnostics.png',
bgColor: 'bg-gradient-to-br from-teal-500 to-teal-600',
textColor: 'text-teal-600',
hoverColor: 'hover:from-teal-600 hover:to-teal-700'
}
];
export default function ServiceCards() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return (
<section className="py-32 bg-gradient-to-br from-gray-50 to-white">
<div className="container-custom">
<div className="text-center mb-16">
<h2 className="font-heading font-bold text-4xl md:text-5xl mb-6 text-gradient">
Services We Offer
</h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Professional electrical solutions for residential and commercial properties in Corpus Christi
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{!mounted
? Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="card-elevated overflow-hidden animate-pulse">
<div className="h-48 bg-gray-200" />
<div className="p-6 space-y-3">
<div className="h-6 bg-gray-200 w-1/2 rounded" />
<div className="h-4 bg-gray-200 w-3/4 rounded" />
<div className="h-4 bg-gray-200 w-2/3 rounded" />
</div>
</div>
))
: items.map((it, index) => (
<Link
key={it.title}
href={it.href}
className="group card-elevated animate-fade-in overflow-hidden"
style={{ animationDelay: `${index * 0.1}s` }}
>
{/* Service Image */}
<div className="relative h-48 overflow-hidden rounded-t-2xl">
<Image
src={it.image}
alt={`${it.title} services in Corpus Christi`}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
<div className={`absolute top-4 left-4 w-12 h-12 ${it.bgColor} rounded-xl flex items-center justify-center shadow-lg`}>
<span className="text-xl">{it.icon}</span>
</div>
</div>
{/* Service Content */}
<div className="p-6">
<h3 className="font-heading font-bold text-xl mb-3 group-hover:text-gradient transition-all duration-300">
{it.title}
</h3>
<p className="text-gray-600 text-base mb-4 leading-relaxed">{it.desc}</p>
<span className={`inline-flex items-center gap-2 ${it.textColor} font-semibold text-sm group-hover:gap-3 transition-all duration-300`}>
Learn more
<svg className="w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</span>
</div>
</Link>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,22 @@
export default function ServicesGrid({ variant }: { variant?: 'residential' }) {
const services = [
{ title: 'Panel Upgrades', bullets: ['100A to 200A service', 'AFCI/GFCI breakers', 'Dedicated high-amp circuits', 'Permits & inspections'] },
{ title: 'Lighting & Fixtures', bullets: ['LED lighting conversion', 'Under-cabinet task lighting', 'Outdoor security lighting', 'Ceiling fan installation'] },
{ title: 'Diagnostics & Repair', bullets: ['Power outage troubleshooting', 'Hot outlet emergency repair', 'Circuit breaker replacement', 'Wire fault location'] },
{ title: 'Kitchen/Bath GFCI', bullets: ['GFCI outlets', 'Appliance circuits', 'Bath exhaust fans', 'Code updates'] },
{ title: 'EV-Ready Conduit', bullets: ['240V outlet installation', 'Dedicated EV circuits', 'Conduit for future upgrade', 'Load calculation analysis'] },
{ title: 'Whole-Home Electrical', bullets: ['New construction wiring', 'Complete rewiring', 'Smart home prep', 'Code-compliant installation'] },
];
return (
<ul className="grid md:grid-cols-3 gap-4">
{services.map((s) => (
<li key={s.title} className="rounded-lg border p-4">
<h3 className="font-semibold mb-2">{s.title}</h3>
<ul className="list-disc pl-5 text-sm text-slate-700 space-y-1">
{s.bullets.map((b) => <li key={b}>{b}</li>)}
</ul>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,10 @@
export default function SkipLink() {
return (
<a
href="#main"
className="sr-only focus:not-sr-only fixed top-2 left-2 z-[100] bg-white text-black px-3 py-2 rounded"
>
Skip to content
</a>
);
}

View File

@@ -0,0 +1,26 @@
'use client';
import { track } from '@/lib/analytics';
export default function StickyCallButton() {
return (
<div className="fixed md:hidden bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 p-4">
<div className="flex gap-3">
<a
href="tel:+13618850315"
onClick={() => track('phone_click', { placement: 'sticky_mobile' })}
className="flex-1 btn-primary text-center"
aria-label="Call Now 24/7"
>
📞 Call Now
</a>
<a
href="#quote-form"
className="flex-1 btn-secondary text-center"
aria-label="Get Free Quote"
>
📝 Free Quote
</a>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
export default function StickyEmergencyRibbon() {
return (
<div
aria-label="24/7 emergency notice"
className="w-full bg-brand-danger text-white text-sm text-center py-3"
>
<div className="flex items-center justify-center gap-2">
<span>🚨</span>
<span className="font-semibold">
24/7 Emergency Electrician Average response under 60 minutes in Corpus Christi
</span>
<span></span>
</div>
</div>
);
}

View File

@@ -0,0 +1,135 @@
type Props = {
license?: string;
years?: string;
rating?: string;
className?: string;
};
export default function TrustStrip({
license = 'TECL 12345',
years = '19+ Years',
rating = '4.9★ (200+ reviews)',
className = ''
}: Props) {
const trustBadges = [
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
),
text: 'Licensed & Insured',
subtext: license,
color: 'text-green-600'
},
{
icon: (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
),
text: 'Top Rated',
subtext: rating,
color: 'text-yellow-500'
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"/>
<polyline points="12,6 12,12 16,14"/>
</svg>
),
text: '24/7 Emergency',
subtext: '< 60 min response',
color: 'text-red-600'
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
),
text: 'BBB A+ Rating',
subtext: 'Accredited Business',
color: 'text-blue-600'
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
),
text: 'Free Estimates',
subtext: 'No hidden fees',
color: 'text-emerald-600'
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
),
text: 'Local Experts',
subtext: years,
color: 'text-purple-600'
}
];
return (
<div className={`bg-gradient-to-r from-gray-50 to-gray-100 border-t border-b border-gray-200 py-4 ${className}`} aria-label="trust badges and credentials">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 items-center">
{trustBadges.map((badge, index) => (
<div
key={index}
className="flex flex-col items-center text-center group hover:scale-105 transition-transform duration-200"
>
<div className={`${badge.color} mb-2 group-hover:scale-110 transition-transform duration-200`}>
{badge.icon}
</div>
<div className="text-xs font-semibold text-gray-900 leading-tight">
{badge.text}
</div>
<div className="text-xs text-gray-600 leading-tight">
{badge.subtext}
</div>
</div>
))}
</div>
{/* Additional trust indicators */}
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex flex-wrap items-center justify-center gap-6 text-xs text-gray-600">
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span>Corpus Christi Local Business</span>
</span>
<span className="hidden sm:inline"></span>
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span>Workers Compensation Insured</span>
</span>
<span className="hidden sm:inline"></span>
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
<span>Angie's List Super Service Award</span>
</span>
<span className="hidden sm:inline"></span>
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
<span>Google 5-Star Rated</span>
</span>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export default function Badge({ children }: { children: React.ReactNode }) { return <span className="inline-block rounded bg-slate-100 px-2 py-1 text-xs font-medium text-slate-700">{children}</span>; }

View File

@@ -0,0 +1,27 @@
import Link from 'next/link';
type Props = ({ href: string } | { onClick?: () => void }) & {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger' | 'success';
};
const base = 'inline-flex items-center justify-center rounded px-4 py-2 font-semibold focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2';
export default function Button(props: Props) {
const variantClass = {
primary: 'bg-brand-orange text-white hover:opacity-90',
secondary: 'bg-white text-slate-900 border',
danger: 'bg-brand-red text-white',
success: 'bg-brand-green text-white'
}[props.variant ?? 'primary'];
const className = `${base} ${variantClass}`;
if ('href' in props) {
const href = (props as any).href;
if (href.startsWith('http') || href.startsWith('tel:') || href.startsWith('mailto:')) {
return <a href={href} className={className}>{props.children}</a>;
}
return <Link href={href} className={className}>{props.children}</Link>;
}
return <button className={className} onClick={(props as any).onClick}>{props.children}</button>;
}

View File

@@ -0,0 +1 @@
export default function Card({ children }: { children: React.ReactNode }) { return <div className="rounded-lg border bg-white p-6 shadow-sm">{children}</div>; }