remarks
This commit is contained in:
@@ -7,6 +7,12 @@ services:
|
||||
- "3040:3000" # Kannst du später sogar löschen, wenn es nur noch über Caddy laufen soll
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- SES_SMTP_HOST=${SES_SMTP_HOST:-email-smtp.us-east-2.amazonaws.com}
|
||||
- SES_SMTP_PORT=${SES_SMTP_PORT:-587}
|
||||
- SES_SMTP_USER=${SES_SMTP_USER}
|
||||
- SES_SMTP_PASS=${SES_SMTP_PASS}
|
||||
- SES_FROM_EMAIL=${SES_FROM_EMAIL:-website@buddelectric.net}
|
||||
- SES_TO_EMAIL=${SES_TO_EMAIL:-buddelectric@grandecom.net}
|
||||
networks:
|
||||
- bizmatch # <-- NEU: Container ins selbe Netz wie Caddy hängen
|
||||
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -10,12 +10,14 @@
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"nodemailer": "^9.0.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
@@ -1556,6 +1558,16 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz",
|
||||
"integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
|
||||
@@ -5046,6 +5058,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
|
||||
"integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"nodemailer": "^9.0.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
|
||||
BIN
public/images/newspaper.webp
Normal file
BIN
public/images/newspaper.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
@@ -1,7 +1,8 @@
|
||||
import { Shield, CheckCircle2, Award, Clock } from "lucide-react";
|
||||
import { Shield, Award, Clock } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Metadata } from "next";
|
||||
import NewspaperClipping from "@/components/NewspaperClipping";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "About Budd Electric Co. | Family-Owned Since 1969",
|
||||
@@ -37,17 +38,7 @@ export default function AboutUs() {
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div className="relative">
|
||||
<div className="aspect-[4/3] rounded-lg overflow-hidden bg-gray-100 shadow-xl border-l-8 border-[color:var(--brand)]">
|
||||
<Image
|
||||
src="/images/residential.png"
|
||||
alt="Budd Electric professional electrician team working on residential electrical services"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
className="object-cover"
|
||||
loading="lazy"
|
||||
quality={65}
|
||||
/>
|
||||
</div>
|
||||
<NewspaperClipping />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-bold tracking-[0.2em] text-[color:var(--brand)] uppercase mb-3">Who We Are</h2>
|
||||
|
||||
79
src/app/api/contact/route.ts
Normal file
79
src/app/api/contact/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { firstName, lastName, email, phone, serviceType, message } = body;
|
||||
|
||||
if (!firstName || typeof firstName !== "string" || firstName.trim().length === 0) {
|
||||
return NextResponse.json({ error: "First name is required." }, { status: 400 });
|
||||
}
|
||||
if (!lastName || typeof lastName !== "string" || lastName.trim().length === 0) {
|
||||
return NextResponse.json({ error: "Last name is required." }, { status: 400 });
|
||||
}
|
||||
if (!email || typeof email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return NextResponse.json({ error: "A valid email address is required." }, { status: 400 });
|
||||
}
|
||||
if (!phone || typeof phone !== "string" || phone.trim().length === 0) {
|
||||
return NextResponse.json({ error: "Phone number is required." }, { status: 400 });
|
||||
}
|
||||
if (!serviceType || typeof serviceType !== "string" || serviceType.trim().length === 0) {
|
||||
return NextResponse.json({ error: "Please select a service type." }, { status: 400 });
|
||||
}
|
||||
if (!message || typeof message !== "string" || message.trim().length === 0) {
|
||||
return NextResponse.json({ error: "Message is required." }, { status: 400 });
|
||||
}
|
||||
|
||||
const smtpHost = process.env.SES_SMTP_HOST || "email-smtp.us-east-2.amazonaws.com";
|
||||
const smtpPort = parseInt(process.env.SES_SMTP_PORT || "587", 10);
|
||||
const smtpUser = process.env.SES_SMTP_USER;
|
||||
const smtpPass = process.env.SES_SMTP_PASS;
|
||||
const fromEmail = process.env.SES_FROM_EMAIL || "website@buddelectric.net";
|
||||
const toEmail = process.env.SES_TO_EMAIL || "buddelectric@grandecom.net";
|
||||
|
||||
if (!smtpUser || !smtpPass) {
|
||||
return NextResponse.json({ error: "Email service is not configured." }, { status: 500 });
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
});
|
||||
|
||||
const serviceTypeLabels: Record<string, string> = {
|
||||
residential: "Residential Service",
|
||||
commercial: "Commercial Project",
|
||||
generator: "Generator Quote",
|
||||
other: "Other",
|
||||
};
|
||||
|
||||
const htmlBody = `
|
||||
<h2 style="color:#1e293b;">New Contact Form Submission</h2>
|
||||
<table style="border-collapse:collapse;width:100%;max-width:600px;">
|
||||
<tr><td style="padding:8px;border-bottom:1px solid #e2e8f0;font-weight:bold;">Name</td><td style="padding:8px;border-bottom:1px solid #e2e8f0;">${firstName} ${lastName}</td></tr>
|
||||
<tr><td style="padding:8px;border-bottom:1px solid #e2e8f0;font-weight:bold;">Email</td><td style="padding:8px;border-bottom:1px solid #e2e8f0;">${email}</td></tr>
|
||||
<tr><td style="padding:8px;border-bottom:1px solid #e2e8f0;font-weight:bold;">Phone</td><td style="padding:8px;border-bottom:1px solid #e2e8f0;">${phone}</td></tr>
|
||||
<tr><td style="padding:8px;border-bottom:1px solid #e2e8f0;font-weight:bold;">Service Type</td><td style="padding:8px;border-bottom:1px solid #e2e8f0;">${serviceTypeLabels[serviceType] || serviceType}</td></tr>
|
||||
<tr><td style="padding:8px;font-weight:bold;">Message</td><td style="padding:8px;">${message}</td></tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
await transporter.sendMail({
|
||||
from: fromEmail,
|
||||
to: toEmail,
|
||||
replyTo: email,
|
||||
subject: `New Contact Form Submission from ${firstName} ${lastName}`,
|
||||
html: htmlBody,
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Failed to send your message. Please try again or call us directly." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MapPin, Phone, Mail, Clock } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import ContactForm from "@/components/ContactForm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact Budd Electric Co. | Corpus Christi Electrician",
|
||||
@@ -99,41 +100,7 @@ export default function Contact() {
|
||||
{/* FORM */}
|
||||
<div className="bg-white p-10 rounded shadow-2xl border-t-8 border-[color:var(--brand)]">
|
||||
<h3 className="text-2xl font-bold font-display text-[color:var(--ink)] mb-6">Send us a Message</h3>
|
||||
<form className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="firstName" className="text-sm font-bold text-gray-700 uppercase tracking-wide">First Name</label>
|
||||
<input id="firstName" name="firstName" type="text" required className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="lastName" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Last Name</label>
|
||||
<input id="lastName" name="lastName" type="text" required className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Email</label>
|
||||
<input id="email" name="email" type="email" required className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="phone" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Phone</label>
|
||||
<input id="phone" name="phone" type="tel" required className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="serviceType" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Service Type</label>
|
||||
<select id="serviceType" name="serviceType" required className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]">
|
||||
<option value="">Select a service type</option>
|
||||
<option value="residential">Residential Service</option>
|
||||
<option value="commercial">Commercial Project</option>
|
||||
<option value="generator">Generator Quote</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Message</label>
|
||||
<textarea id="message" name="message" rows={4} required className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]"></textarea>
|
||||
</div>
|
||||
<button type="submit" className="w-full bg-[color:var(--brand)] text-white font-bold uppercase tracking-widest py-4 rounded hover:bg-[color:var(--brand-strong)] transition shadow-lg">Send Message</button>
|
||||
</form>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Home, Phone, Zap, Factory } from "lucide-react";
|
||||
import { Home, Zap, Building2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Electrical Services Corpus Christi | Residential & Commercial",
|
||||
description: "Professional residential, commercial, and industrial electrical services in Corpus Christi, TX. Licensed electricians for all your electrical needs. Call (361) 855-2255.",
|
||||
title: "Electrical Services Corpus Christi | Commercial & Residential",
|
||||
description: "Professional commercial and residential electrical services in Corpus Christi, TX. Licensed electricians for all your electrical needs. Call (361) 855-2255.",
|
||||
};
|
||||
|
||||
export default function Services() {
|
||||
@@ -28,7 +28,7 @@ export default function Services() {
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10 text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white font-display mb-6">Electrical Services</h1>
|
||||
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
|
||||
Comprehensive solutions for every need. Residential, Commercial, and Industrial.
|
||||
Comprehensive solutions for every need. Commercial and Residential electrical services.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -36,15 +36,79 @@ export default function Services() {
|
||||
{/* SERVICE LIST */}
|
||||
<section className="py-24">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8 space-y-24">
|
||||
{/* COMMERCIAL */}
|
||||
<div id="commercial" className="grid lg:grid-cols-2 gap-16 items-center scroll-mt-36">
|
||||
<div className="order-2 lg:order-1">
|
||||
<div className="w-14 h-14 bg-red-50 text-[color:var(--brand)] rounded flex items-center justify-center mb-6">
|
||||
<Building2 size={32} />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[color:var(--ink)] font-display mb-6">Commercial & Residential Electrical Remodels</h2>
|
||||
<p className="text-[color:var(--muted)] text-lg mb-8 leading-relaxed">
|
||||
We are your wiring, lighting and electrical service upgrade specialists in Corpus Christi and all of South Texas. We have encountered all the standard common electrical problems and have the experience, knowledge, skill, and expertise to solve them all. Our electrical repair expertise includes faulty outlets, panel reinstallation, meter replacement, new lighting fixtures, and adding circuits to your fuse box. Call Budd Electric Co. for a complimentary electrical repair consultation or free quote at (361) 855-2255.
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{["Commercial & Residential", "Complete Electrical Service Upgrades", "Electrical Repairs"].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-gray-700 font-medium">
|
||||
<span className="w-2 h-2 bg-[color:var(--brand)] rounded-full"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2 rounded-lg overflow-hidden shadow-xl aspect-[4/3] bg-gray-100 relative">
|
||||
<Image
|
||||
src="/images/commercial.png"
|
||||
alt="Commercial electrical wiring and service upgrades"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
quality={65}
|
||||
loading="lazy"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERATORS */}
|
||||
<div id="generator" className="grid lg:grid-cols-2 gap-16 items-center scroll-mt-36">
|
||||
<div className="rounded-lg overflow-hidden shadow-xl aspect-[4/3] bg-gray-100 relative">
|
||||
<Image
|
||||
src="/images/generator.png"
|
||||
alt="Kohler whole-home standby generator installation and service"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
quality={65}
|
||||
loading="lazy"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="w-14 h-14 bg-red-50 text-[color:var(--brand)] rounded flex items-center justify-center mb-6">
|
||||
<Zap size={32} />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[color:var(--ink)] font-display mb-6">Kohler Generators</h2>
|
||||
<p className="text-[color:var(--muted)] text-lg mb-8 leading-relaxed">
|
||||
Don't be left in the dark during a storm. As an authorized Kohler dealer, we sell, install, and service standby generators to keep your power on when the grid goes down.
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{["Sales & Consultation", "Professional Installation", "Maintenance Plans", "Warranty Service", "Transfer Switch Install"].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-gray-700 font-medium">
|
||||
<span className="w-2 h-2 bg-[color:var(--brand)] rounded-full"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RESIDENTIAL */}
|
||||
<div id="residential" className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div id="residential" className="grid lg:grid-cols-2 gap-16 items-center scroll-mt-36">
|
||||
<div className="order-2 lg:order-1">
|
||||
<div className="w-14 h-14 bg-red-50 text-[color:var(--brand)] rounded flex items-center justify-center mb-6">
|
||||
<Home size={32} />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[color:var(--ink)] font-display mb-6">Residential Services</h2>
|
||||
<p className="text-[color:var(--muted)] text-lg mb-8 leading-relaxed">
|
||||
Your home is your sanctuary. We ensure it's powered safely and efficiently. From simple outlet repairs to full-home rewiring, no job is too small or too complex.
|
||||
Your home is your sanctuary. We ensure it's powered safely and efficiently. From simple outlet repairs to full-home rewiring, no job is too small or too complex.
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{["Service Upgrades", "Safety Inspections", "Lighting Installation", "Ceiling Fans", "Outlet Repair", "Panel Replacement"].map((item, i) => (
|
||||
@@ -67,70 +131,6 @@ export default function Services() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COMMERCIAL */}
|
||||
<div id="commercial" className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div className="rounded-lg overflow-hidden shadow-xl aspect-[4/3] bg-gray-100 relative">
|
||||
<Image
|
||||
src="/images/commercial.png"
|
||||
alt="Commercial and industrial electrical wiring and services"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
quality={65}
|
||||
loading="lazy"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="w-14 h-14 bg-red-50 text-[color:var(--brand)] rounded flex items-center justify-center mb-6">
|
||||
<Factory size={32} />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[color:var(--ink)] font-display mb-6">Commercial & Industrial</h2>
|
||||
<p className="text-[color:var(--muted)] text-lg mb-8 leading-relaxed">
|
||||
Keep your business running smoothly with our expert commercial services. We handle everything from new construction build-outs to ongoing maintenance contracts.
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{["New Construction Wiring", "Tenant Build-outs", "Parking Lot Lighting", "Emergency Lighting", "Data Cabling", "Troubleshooting"].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-gray-700 font-medium">
|
||||
<span className="w-2 h-2 bg-[color:var(--brand)] rounded-full"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERATORS */}
|
||||
<div id="generator" className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div className="order-2 lg:order-1">
|
||||
<div className="w-14 h-14 bg-red-50 text-[color:var(--brand)] rounded flex items-center justify-center mb-6">
|
||||
<Zap size={32} />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[color:var(--ink)] font-display mb-6">Kohler Generators</h2>
|
||||
<p className="text-[color:var(--muted)] text-lg mb-8 leading-relaxed">
|
||||
Don't be left in the dark during a storm. As an authorized Kohler dealer, we sell, install, and service standby generators to keep your power on when the grid goes down.
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{["Sales & Consultation", "Professional Installation", "Maintenance Plans", "Warranty Service", "Transfer Switch Install"].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-gray-700 font-medium">
|
||||
<span className="w-2 h-2 bg-[color:var(--brand)] rounded-full"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2 rounded-lg overflow-hidden shadow-xl aspect-[4/3] bg-gray-100 relative">
|
||||
<Image
|
||||
src="/images/generator.png"
|
||||
alt="Kohler whole-home standby generator installation and service"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
quality={65}
|
||||
loading="lazy"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ArrowRight, Zap, Shield, Home as HomeIcon, Factory, HardHat, CheckCircle2, Star, Clock, MapPin, Phone } from "lucide-react";
|
||||
import { ArrowRight, Zap, Shield, Home as HomeIcon, Building2, CheckCircle2, Star, Clock, MapPin, Phone } from "lucide-react";
|
||||
import ReviewCarousel from "@/components/ReviewCarousel";
|
||||
import HeroCarousel from "@/components/HeroCarousel";
|
||||
import FAQSection from "@/components/FAQSection";
|
||||
@@ -56,16 +56,12 @@ export default function Home() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 mb-16 flex flex-wrap items-center gap-4 md:gap-8 text-sm font-medium text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="text-[color:var(--brand)]" size={18} />
|
||||
<span>Licensed & Insured</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="text-[color:var(--brand)]" size={18} />
|
||||
<span>24/7 Emergency Support</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 mb-16 flex flex-wrap items-center gap-4 md:gap-8 text-sm font-medium text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="text-[color:var(--brand)]" size={18} />
|
||||
<span>Licensed & Insured</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative hidden lg:block">
|
||||
@@ -94,31 +90,31 @@ export default function Home() {
|
||||
<div className="w-14 h-14 bg-white text-[color:var(--brand)] rounded flex items-center justify-center mb-6 shadow-sm border border-gray-100 group-hover:scale-110 transition">
|
||||
<HomeIcon size={28} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-[color:var(--ink)] mb-3 font-display">Residential</h3>
|
||||
<h3 className="text-xl font-bold text-[color:var(--ink)] mb-3 font-display">Residential Services</h3>
|
||||
<p className="text-gray-500 mb-6 line-clamp-3">
|
||||
Complete home electrical services including safety inspections, repairs, lighting, and fan installations.
|
||||
</p>
|
||||
<Link href="/electrical-services" className="text-[color:var(--brand)] font-bold uppercase text-xs tracking-widest hover:underline" aria-label="Learn more about Residential services">Residential Services</Link>
|
||||
<Link href="/electrical-services#residential" className="text-[color:var(--brand)] font-bold uppercase text-xs tracking-widest hover:underline" aria-label="Learn more about Residential services">Residential Services</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2 */}
|
||||
<div className="group bg-[color:var(--bg-dark)] text-white rounded-lg overflow-hidden shadow-2xl hover:shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5)] transition duration-300 relative transform md:-translate-y-4">
|
||||
<div className="h-2 bg-[color:var(--brand)] w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10">
|
||||
<Zap size={100} />
|
||||
</div>
|
||||
<div className="p-8 relative z-10">
|
||||
<div className="w-14 h-14 bg-[color:var(--brand)] text-white rounded flex items-center justify-center mb-6 shadow-lg group-hover:scale-110 transition">
|
||||
<Factory size={28} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mb-3 font-display">Commercial</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Reliable power for your business. New construction, maintenance, and industrial-grade solutions.
|
||||
</p>
|
||||
<Link href="/electrical-services" className="text-white font-bold uppercase text-xs tracking-widest hover:text-[color:var(--brand)] transition" aria-label="Learn more about Commercial services">Commercial Services</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Card 2 */}
|
||||
<div className="group bg-[color:var(--bg-dark)] text-white rounded-lg overflow-hidden shadow-2xl hover:shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5)] transition duration-300 relative transform md:-translate-y-4">
|
||||
<div className="h-2 bg-[color:var(--brand)] w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10">
|
||||
<Zap size={100} />
|
||||
</div>
|
||||
<div className="p-8 relative z-10">
|
||||
<div className="w-14 h-14 bg-[color:var(--brand)] text-white rounded flex items-center justify-center mb-6 shadow-lg group-hover:scale-110 transition">
|
||||
<Building2 size={28} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mb-3 font-display">Commercial & Residential Electrical Remodels</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Commercial & residential electrical remodels, wiring, lighting, and service upgrades for Corpus Christi and South Texas.
|
||||
</p>
|
||||
<Link href="/electrical-services#commercial" className="text-white font-bold uppercase text-xs tracking-widest hover:text-[color:var(--brand)] transition" aria-label="Learn more about Commercial services">Commercial Services</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 3 */}
|
||||
<div className="group bg-gray-50 border border-gray-100 rounded-lg overflow-hidden hover:shadow-2xl transition duration-300 relative">
|
||||
@@ -127,11 +123,11 @@ export default function Home() {
|
||||
<div className="w-14 h-14 bg-white text-[color:var(--brand)] rounded flex items-center justify-center mb-6 shadow-sm border border-gray-100 group-hover:scale-110 transition">
|
||||
<Zap size={28} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-[color:var(--ink)] mb-3 font-display">Generators</h3>
|
||||
<h3 className="text-xl font-bold text-[color:var(--ink)] mb-3 font-display">Kohler Generators</h3>
|
||||
<p className="text-gray-500 mb-6 line-clamp-3">
|
||||
Authorized Kohler dealer. Keep your lights on when the grid goes down with standby power.
|
||||
</p>
|
||||
<Link href="/electrical-services" className="text-[color:var(--brand)] font-bold uppercase text-xs tracking-widest hover:underline" aria-label="Learn more about Generator services">Generator Services</Link>
|
||||
<Link href="/electrical-services#generator" className="text-[color:var(--brand)] font-bold uppercase text-xs tracking-widest hover:underline" aria-label="Learn more about Kohler Generator services">Kohler Generator Services</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
95
src/components/ContactForm.tsx
Normal file
95
src/components/ContactForm.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ContactForm() {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
serviceType: "",
|
||||
message: "",
|
||||
});
|
||||
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
const [statusMessage, setStatusMessage] = useState("");
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
setStatusMessage("");
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
setStatus("success");
|
||||
setStatusMessage("Thank you! Your message has been sent. We'll get back to you shortly.");
|
||||
setFormData({ firstName: "", lastName: "", email: "", phone: "", serviceType: "", message: "" });
|
||||
} else {
|
||||
setStatus("error");
|
||||
setStatusMessage(data.error || "Something went wrong. Please try again.");
|
||||
}
|
||||
} catch {
|
||||
setStatus("error");
|
||||
setStatusMessage("Network error. Please check your connection and try again.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="firstName" className="text-sm font-bold text-gray-700 uppercase tracking-wide">First Name</label>
|
||||
<input id="firstName" name="firstName" type="text" required value={formData.firstName} onChange={handleChange} className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="lastName" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Last Name</label>
|
||||
<input id="lastName" name="lastName" type="text" required value={formData.lastName} onChange={handleChange} className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Email</label>
|
||||
<input id="email" name="email" type="email" required value={formData.email} onChange={handleChange} className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="phone" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Phone</label>
|
||||
<input id="phone" name="phone" type="tel" required value={formData.phone} onChange={handleChange} className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="serviceType" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Service Type</label>
|
||||
<select id="serviceType" name="serviceType" required value={formData.serviceType} onChange={handleChange} className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]">
|
||||
<option value="">Select a service type</option>
|
||||
<option value="commercial">Commercial Project</option>
|
||||
<option value="residential">Residential Service</option>
|
||||
<option value="generator">Generator Quote</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="text-sm font-bold text-gray-700 uppercase tracking-wide">Message</label>
|
||||
<textarea id="message" name="message" rows={4} required value={formData.message} onChange={handleChange} className="w-full bg-gray-50 border border-gray-200 p-3 rounded focus:outline-none focus:border-[color:var(--brand)] focus:ring-1 focus:ring-[color:var(--brand)]"></textarea>
|
||||
</div>
|
||||
|
||||
{status === "success" && (
|
||||
<div className="p-3 bg-green-50 border border-green-200 rounded text-green-700 text-sm">{statusMessage}</div>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">{statusMessage}</div>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={status === "loading"} className="w-full bg-[color:var(--brand)] text-white font-bold uppercase tracking-widest py-4 rounded hover:bg-[color:var(--brand-strong)] transition shadow-lg disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{status === "loading" ? "Sending..." : "Send Message"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default function Header() {
|
||||
href="/contact-us"
|
||||
className="hidden lg:flex items-center gap-2 rounded bg-[color:var(--brand)] px-5 py-2.5 text-sm font-bold text-white shadow-lg shadow-red-900/20 transition hover:bg-[color:var(--brand-strong)] uppercase tracking-wider"
|
||||
>
|
||||
Free Quote
|
||||
Contact Us
|
||||
<ArrowRight size={14} strokeWidth={3} />
|
||||
</Link>
|
||||
<button
|
||||
@@ -117,7 +117,7 @@ export default function Header() {
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center justify-center gap-2 rounded bg-[color:var(--brand)] px-7 py-4 text-sm font-bold text-white shadow-lg shadow-red-900/20 transition hover:bg-[color:var(--brand-strong)] hover:shadow-red-900/40 uppercase tracking-wider w-full"
|
||||
>
|
||||
Get a Quote
|
||||
Contact Us
|
||||
<ArrowRight size={16} strokeWidth={3} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
74
src/components/NewspaperClipping.tsx
Normal file
74
src/components/NewspaperClipping.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import Image from "next/image";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export default function NewspaperClipping() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const close = useCallback(() => setOpen(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") close();
|
||||
};
|
||||
document.addEventListener("keydown", handleKey);
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKey);
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [open, close]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="aspect-[4/3] rounded-lg overflow-hidden bg-gray-100 shadow-xl border-l-8 border-[color:var(--brand)] cursor-pointer group relative"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Image
|
||||
src="/images/newspaper.webp"
|
||||
alt="Budd Electric Co. featured in the local newspaper"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
className="object-contain p-3 group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors flex items-center justify-center">
|
||||
<span className="text-white text-sm font-bold opacity-0 group-hover:opacity-100 transition-opacity bg-black/60 px-4 py-2 rounded">Click to enlarge</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-center text-sm text-gray-400 italic">Featured in the local press</p>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/85 flex items-center justify-center p-4 md:p-8"
|
||||
onClick={close}
|
||||
>
|
||||
<button
|
||||
onClick={close}
|
||||
className="absolute top-4 right-4 text-white/80 hover:text-white transition-colors z-10"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={36} strokeWidth={2} />
|
||||
</button>
|
||||
<div
|
||||
className="relative max-w-5xl w-full max-h-[90vh]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Image
|
||||
src="/images/newspaper.webp"
|
||||
alt="Budd Electric Co. featured in the local newspaper"
|
||||
width={1600}
|
||||
height={1200}
|
||||
className="object-contain w-full h-full max-h-[90vh]"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user