remarks
This commit is contained in:
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