import type { CSSProperties, ReactNode } from "react";
import Link from "next/link";
import GlobeCanvas from "@/components/globe-canvas";
import StatCounter from "@/components/stat-counter";
import RevealObserver from "@/components/reveal-observer";
import ContactForm from "./contact-form";
import type { Item, Page, Section } from "@/lib/pages";
import { SITE_URL, hrefFor } from "@/lib/routes";
type Vars = CSSProperties & Record<`--${string}`, string | number>;
const delay = (ms: number) => ({ "--d": `${ms}ms` }) as Vars;
const pad = (n: number) => String(n).padStart(2, "0");
export type Crumb = { name: string; href: string };
const Arrow = () => → ;
function Eyebrow({ index, children }: { index?: string; children: ReactNode }) {
return (
{index && {index} }
{children}
);
}
/** "> " marks a sentence that was bold in the copy deck. */
function Para({ text, className }: { text: string; className?: string }) {
if (text.startsWith("> ")) return {text.slice(2)}
;
return {text}
;
}
function Paras({ items, firstClass }: { items: string[]; firstClass?: string }) {
return (
<>
{items.map((t, i) => (
))}
>
);
}
function CtaLinks({ ctas, path }: { ctas: string[]; path: string }) {
if (!ctas.length) return null;
return (
{ctas.map((c) => (
{c}
))}
);
}
function SectionHead({ section, index }: { section: Section; index: number }) {
const isFaq = section.kind === "faq";
const eyebrow = isFaq && !section.title ? null : section.eyebrow;
const title = section.title ?? (isFaq ? "Frequently Asked Questions" : null);
return (
{eyebrow && {eyebrow} }
{title &&
{title} }
);
}
function Intro({ section }: { section: Section }) {
const { intro } = section;
if (!intro.length) return null;
if (intro.length === 1) {
return (
);
}
return (
);
}
function Outro({ section, path }: { section: Section; path: string }) {
if (!section.outro.length && !section.ctas.length) return null;
return (
{section.outro.length > 0 && (
)}
);
}
function ItemList({ list }: { list: string[] }) {
if (!list.length) return null;
return (
{list.map((l) => (
{l}
))}
);
}
function columnsFor(items: Item[]) {
const n = items.length;
const avg = items.reduce((a, it) => a + it.body.join(" ").length, 0) / Math.max(n, 1);
if (n === 2 || (n === 4 && avg > 240)) return 2;
if (n === 4) return 4;
return 3;
}
/* ───────────── Section variants ───────────── */
function TextSection({ section, path }: { section: Section; path: string }) {
return (
<>
{section.ctas.length > 0 && (
)}
>
);
}
function CardsSection({ section, path }: { section: Section; path: string }) {
const cols = columnsFor(section.items);
return (
<>
{section.items.map((it, i) => (
{pad(i + 1)}
{it.title}
{it.subtitle && {it.subtitle}
}
{it.body.length > 0 && (
)}
{it.cta && (
{it.cta}
)}
))}
>
);
}
function StepsSection({ section, path }: { section: Section; path: string }) {
return (
<>
{section.items.map((it, i) => (
{it.num ?? pad(i + 1)}
{it.title}
))}
>
);
}
function FaqSection({ section, path }: { section: Section; path: string }) {
return (
<>
{section.items.map((it, i) => (
{it.title}
))}
>
);
}
function LinksSection({ section, path }: { section: Section; path: string }) {
const cols = columnsFor(section.items);
return (
<>
{section.items.map((it, i) => (
{pad(i + 1)}
{it.title}
{it.cta}
))}
>
);
}
function StatsSection({ section, path }: { section: Section; path: string }) {
return (
<>
{section.items.map((it, i) => {
const m = it.title.match(/^([\d.,]+)(.*)$/);
const value = m ? Number(m[1].replace(/,/g, "")) : 0;
return (
);
})}
>
);
}
function AddressLines({ text }: { text: string }) {
return (
{text.split("\n").map((line) => {
const [label, ...rest] = line.split(":");
const value = rest.join(":").trim();
if (/^phone$/i.test(label.trim()) && value) {
return (
Phone
{value}
);
}
if (/^email$/i.test(label.trim()) && value) {
return (
Email
{value}
);
}
return {line} ;
})}
);
}
function OfficesSection({ section, path }: { section: Section; path: string }) {
return (
<>
{section.items.map((it, i) => {
const address = it.body.find((b) => b.includes("Phone:"));
const desc = it.body.filter((b) => b !== address);
return (
{it.title}
{it.subtitle && {it.subtitle} }
{address && }
);
})}
>
);
}
function ShowcaseSection({ section, path }: { section: Section; path: string }) {
return (
<>
{section.items.map((it, i) => {
const [name, tagline] = (it.kicker ?? "").split("|").map((s) => s.trim());
return (
{pad(i + 1)}
{name}
{tagline &&
{tagline}
}
{it.title}
{it.cta && (
{it.cta}
)}
);
})}
>
);
}
function FormSection({ section, path }: { section: Section; path: string }) {
return (
);
}
const variants: Record ReactNode> = {
text: TextSection,
cards: CardsSection,
steps: StepsSection,
faq: FaqSection,
links: LinksSection,
stats: StatsSection,
offices: OfficesSection,
showcase: ShowcaseSection,
};
/* ───────────── Hero & closing CTA ───────────── */
function PageHero({ page, crumbs }: { page: Page; crumbs: Crumb[] }) {
const { hero, path } = page;
const [name, tagline] = (hero.eyebrow ?? "").split("|").map((s) => s.trim());
return (
{crumbs.map((c, i) =>
i < crumbs.length - 1 ? (
{c.name}
/
) : (
{c.name}
),
)}
{tagline ? (
<>
{name} {tagline}
>
) : (
name
)}
{hero.title}
{hero.intro.length > 1 && (
)}
{hero.ctas.map((c, i) => (
{c}
))}
);
}
function ClosingCta({ section, path }: { section: Section; path: string }) {
const [lead, ...rest] = section.intro;
return (
{section.eyebrow && (
{section.eyebrow}
)}
{section.title}
{lead && (
)}
{section.ctas.length > 0 && (
{section.ctas.map((c) => (
{c}
))}
)}
);
}
/* ───────────── Structured data ───────────── */
function JsonLd({ page, crumbs }: { page: Page; crumbs: Crumb[] }) {
const faqs = page.sections.filter((s) => s.kind === "faq").flatMap((s) => s.items);
const graph: object[] = [
{
"@type": "BreadcrumbList",
itemListElement: crumbs.map((c, i) => ({
"@type": "ListItem",
position: i + 1,
name: c.name,
item: SITE_URL + (c.href === "/" ? "" : c.href),
})),
},
];
if (faqs.length) {
graph.push({
"@type": "FAQPage",
mainEntity: faqs.map((f) => ({
"@type": "Question",
name: f.title,
acceptedAnswer: { "@type": "Answer", text: f.body.join(" ") },
})),
});
}
const json = JSON.stringify({ "@context": "https://schema.org", "@graph": graph }).replace(/;
}
/* ───────────── Page ───────────── */
export default function ContentPage({ page, crumbs }: { page: Page; crumbs: Crumb[] }) {
const last = page.sections[page.sections.length - 1];
const closing = last && last.kind === "text" && last.ctas.length > 0 ? last : null;
const body = closing ? page.sections.slice(0, -1) : page.sections;
return (
<>
{body.map((section, i) => {
const Variant = variants[section.kind] ?? TextSection;
return (
);
})}
{closing && }
>
);
}