initial
This commit is contained in:
14
web/components/CTASection.tsx
Normal file
14
web/components/CTASection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
52
web/components/FAQ.tsx
Normal file
52
web/components/FAQ.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
web/components/FeatureGrid.tsx
Normal file
24
web/components/FeatureGrid.tsx
Normal 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
64
web/components/Footer.tsx
Normal 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
58
web/components/Header.tsx
Normal 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
35
web/components/Hero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
164
web/components/InteractiveMap.tsx
Normal file
164
web/components/InteractiveMap.tsx
Normal 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
51
web/components/Navbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
web/components/ReviewsGrid.tsx
Normal file
22
web/components/ReviewsGrid.tsx
Normal 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">“{r.text}”</p>
|
||||
<div className="mt-2 text-sm font-semibold">{r.name}</div>
|
||||
<div className="text-xs text-slate-500">{r.area}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
148
web/components/ServiceCards.tsx
Normal file
148
web/components/ServiceCards.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
web/components/ServicesGrid.tsx
Normal file
22
web/components/ServicesGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
web/components/SkipLink.tsx
Normal file
10
web/components/SkipLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
26
web/components/StickyCallButton.tsx
Normal file
26
web/components/StickyCallButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
web/components/StickyEmergencyRibbon.tsx
Normal file
16
web/components/StickyEmergencyRibbon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
135
web/components/TrustStrip.tsx
Normal file
135
web/components/TrustStrip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
web/components/ui/Badge.tsx
Normal file
1
web/components/ui/Badge.tsx
Normal 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>; }
|
||||
27
web/components/ui/Button.tsx
Normal file
27
web/components/ui/Button.tsx
Normal 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>;
|
||||
}
|
||||
1
web/components/ui/Card.tsx
Normal file
1
web/components/ui/Card.tsx
Normal 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>; }
|
||||
Reference in New Issue
Block a user