178 lines
5.5 KiB
TypeScript
178 lines
5.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("");
|
|
|
|
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 = (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;
|
|
}
|
|
|
|
setFormStatus("Thanks. We'll review your mailbox count and current provider before we reply.");
|
|
form.reset();
|
|
};
|
|
|
|
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}
|
|
onAssessmentSubmit={handleAssessmentSubmit}
|
|
/>
|
|
</main>
|
|
|
|
<SiteFooter />
|
|
</>
|
|
);
|
|
}
|