Files
E-Mail-Webseite-Marketing/app/page.tsx
2026-06-13 15:00:40 -05:00

212 lines
6.5 KiB
TypeScript

"use client";
import { FormEvent, MouseEvent, useEffect, useState } from "react";
import SiteHeader from "../components/SiteHeader";
import HeroSection from "../components/HeroSection";
import ProblemSection from "../components/ProblemSection";
import PricingSection from "../components/PricingSection";
import FaqSection from "../components/FaqSection";
import AssessmentSection from "../components/AssessmentSection";
import SiteFooter from "../components/SiteFooter";
type Theme = "dark" | "light";
export default function Page() {
const [menuOpen, setMenuOpen] = useState(false);
const [theme, setTheme] = useState<Theme>("dark");
const [mailboxes, setMailboxes] = useState(10);
const [formErrors, setFormErrors] = useState({ name: "", email: "" });
const [formStatus, setFormStatus] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const storedTheme = window.localStorage.getItem("bes-theme");
setTheme(storedTheme === "light" ? "light" : "dark");
}, []);
useEffect(() => {
document.documentElement.dataset.theme = theme;
window.localStorage.setItem("bes-theme", theme);
}, [theme]);
useEffect(() => {
document.body.classList.toggle("menu-open", menuOpen);
return () => document.body.classList.remove("menu-open");
}, [menuOpen]);
useEffect(() => {
const scrollProgress = document.querySelector<HTMLElement>(".scroll-progress");
if (!scrollProgress) {
return;
}
let ticking = false;
const updateScrollProgress = () => {
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const progress = maxScroll > 0 ? window.scrollY / maxScroll : 0;
scrollProgress.style.setProperty("--scroll-progress", Math.min(Math.max(progress, 0), 1).toFixed(4));
ticking = false;
};
const onScroll = () => {
if (ticking) {
return;
}
ticking = true;
requestAnimationFrame(updateScrollProgress);
};
updateScrollProgress();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
const revealItems = [
document.querySelector(".hero-copy"),
document.querySelector(".architecture-panel"),
...document.querySelectorAll(".module-card"),
document.querySelector(".local-strip"),
...document.querySelectorAll(".content-section > *"),
].filter((item): item is Element => item instanceof Element);
revealItems.forEach((item, index) => {
if (item instanceof HTMLElement) {
item.classList.add("reveal-item");
item.style.setProperty("--reveal-delay", `${Math.min(index % 4, 3) * 85}ms`);
}
});
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion || !("IntersectionObserver" in window)) {
revealItems.forEach((item) => item instanceof HTMLElement && item.classList.add("is-revealed"));
return;
}
document.body.classList.add("motion-ready");
const revealObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
entry.target.classList.add("is-revealed");
observer.unobserve(entry.target);
});
},
{
threshold: 0.18,
rootMargin: "0px 0px -8% 0px",
},
);
revealItems.forEach((item) => revealObserver.observe(item));
return () => {
revealObserver.disconnect();
document.body.classList.remove("motion-ready");
};
}, []);
const handleNavClick = (event: MouseEvent<HTMLElement>) => {
if (event.target instanceof HTMLAnchorElement) {
setMenuOpen(false);
}
};
const handleAssessmentSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const name = String(formData.get("name") || "").trim();
const email = String(formData.get("email") || "").trim();
const nextErrors = {
name: name ? "" : "Please enter your name.",
email: "",
};
if (!email) {
nextErrors.email = "Please enter your business email.";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
nextErrors.email = "Email address needs to include an @ symbol.";
}
setFormErrors(nextErrors);
setFormStatus("");
if (nextErrors.name || nextErrors.email) {
window.requestAnimationFrame(() => {
document.getElementById(nextErrors.name ? "name" : "email")?.focus();
});
return;
}
setIsSubmitting(true);
try {
const response = await fetch("/api/assessment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
email,
mailboxes: formData.get("mailboxes") ?? "",
provider: formData.get("provider") ?? "",
message: formData.get("message") ?? "",
}),
});
if (response.ok) {
setFormStatus("Thanks. We'll review your mailbox count and current provider before we reply.");
form.reset();
} else {
const data = await response.json().catch(() => null);
if (data?.fields) {
setFormErrors({
name: data.fields.name ?? "",
email: data.fields.email ?? "",
});
} else {
setFormStatus(data?.error ?? "Something went wrong. Please try again or call us.");
}
}
} catch {
setFormStatus("Network error. Please check your connection and try again.");
} finally {
setIsSubmitting(false);
}
};
return (
<>
<a className="skip-link" href="#main">Skip to content</a>
<div className="scroll-progress" aria-hidden="true"></div>
<SiteHeader
menuOpen={menuOpen}
theme={theme}
onMenuToggle={() => setMenuOpen((isOpen) => !isOpen)}
onThemeToggle={() => setTheme((currentTheme) => (currentTheme === "light" ? "dark" : "light"))}
onNavClick={handleNavClick}
/>
<main id="main">
<HeroSection />
<ProblemSection />
<PricingSection
mailboxes={mailboxes}
onMailboxesChange={setMailboxes}
/>
<FaqSection />
<AssessmentSection
formErrors={formErrors}
formStatus={formStatus}
isSubmitting={isSubmitting}
onAssessmentSubmit={handleAssessmentSubmit}
/>
</main>
<SiteFooter />
</>
);
}