← back to Homesonspec
apps/web/src/app/page.tsx
208 lines
import Link from "next/link";
import { prisma } from "@homesonspec/database";
import HeroMap from "./HeroMap";
export const dynamic = "force-dynamic";
export default async function HomePage() {
const markets = await prisma.community.groupBy({
by: ["metro", "state"],
where: { isDemo: false, homes: { some: { status: "PUBLISHED", isDemo: false } } },
_count: { _all: true },
});
const homeCount = await prisma.inventoryHome.count({ where: { status: "PUBLISHED", isDemo: false } });
// Count only builders that actually have live inventory — never claim coverage
// we don't have. Builders seeded without published homes aren't counted here.
const builderCount = await prisma.builder.count({
where: { isDemo: false, homes: { some: { status: "PUBLISHED", isDemo: false } } },
});
// Community markers for the hero map — one point per community with live
// inventory (capped for a fast first paint). Convert Decimal lat/lon to plain
// numbers before crossing the server→client boundary.
const mapCommunities = await prisma.community.findMany({
where: {
isDemo: false,
lat: { not: null },
lon: { not: null },
homes: { some: { status: "PUBLISHED", isDemo: false } },
},
select: {
id: true,
name: true,
slug: true,
lat: true,
lon: true,
_count: { select: { homes: { where: { status: "PUBLISHED", isDemo: false } } } },
},
orderBy: { homes: { _count: "desc" } },
take: 800,
});
const heroMarkers = mapCommunities.map((c) => ({
id: c.id,
lat: Number(c.lat),
lon: Number(c.lon),
label: c.name,
sublabel: `${c._count.homes} home${c._count.homes === 1 ? "" : "s"}`,
href: `/communities/${c.slug}`,
}));
// Three featured homes on load — one value, one mid-range, one luxury — each a
// real published listing, preferring ones with builder photos.
const featuredSelect = {
id: true, street: true, city: true, state: true, price: true,
beds: true, bathsTotal: true, sqft: true, images: true,
community: { select: { name: true } }, builder: { select: { name: true } },
} as const;
const featBase = {
status: "PUBLISHED" as const,
isDemo: false,
freshness: { not: "INACTIVE" as const },
images: { isEmpty: false },
};
const [lowHome, midHome, highHome] = await Promise.all([
// Genuinely low (bottom of the value band) / true mid / top-of-market luxury,
// so the three cards read as distinct price levels, not boundary neighbors.
prisma.inventoryHome.findFirst({ where: { ...featBase, price: { gte: 250000, lt: 380000 } }, orderBy: { price: "asc" }, select: featuredSelect }),
prisma.inventoryHome.findFirst({ where: { ...featBase, price: { gte: 480000, lt: 650000 } }, orderBy: { price: "asc" }, select: featuredSelect }),
prisma.inventoryHome.findFirst({ where: { ...featBase, price: { gte: 750000 } }, orderBy: { price: "desc" }, select: featuredSelect }),
]);
const featured = [
{ tier: "Value", home: lowHome },
{ tier: "Mid-range", home: midHome },
{ tier: "Luxury", home: highHome },
].filter((f): f is { tier: string; home: NonNullable<typeof f.home> } => f.home !== null);
// Media-rights safe default: builder photos are OFF unless BUILDER_IMAGES_ENABLED=1
// is explicitly set (pm2 env), pending registry-recorded rights per docs/data-rights/POLICY.md.
// Facts-only is the binding default; opt IN to images only once rights are confirmed.
const showImages = process.env.BUILDER_IMAGES_ENABLED === "1";
return (
<div>
<section className="relative overflow-hidden bg-gradient-to-b from-brand-950 via-brand-900 to-brand-800 px-4 py-20 text-white">
{/* subtle radial glow behind the headline */}
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(60%_50%_at_50%_0%,rgba(244,169,52,0.14),transparent_70%)]" />
<div className="relative mx-auto max-w-3xl text-center">
<p className="eyebrow text-accent-300">Verified from the source</p>
<h1 className="mt-3 font-display text-4xl font-semibold leading-[1.05] tracking-tight sm:text-6xl">
Every new home.<br className="hidden sm:block" /> Every builder. One search.
</h1>
<p className="mt-5 text-lg text-brand-100">
Move-in-ready and under-construction homes, verified from the source.
</p>
<p className="mx-auto mt-3 max-w-2xl text-sm text-brand-200/90">
Search “spec home listings” today and you get scattered builder sites, generic portals, and
blog posts — there is no one place to see them all. Builders list inventory on dozens of
separate websites that never meet. HomesOnSpec brings builders' available inventory into
one verified search — and we add builders continuously.
</p>
<form action="/search" method="get" className="mx-auto mt-8 flex max-w-xl gap-2">
<input
type="text"
name="q"
placeholder="City, ZIP, or community — try “Leander” or “85212”"
className="field flex-1 shadow-lg ring-0"
/>
<button type="submit" className="btn-accent px-6 py-3 text-base shadow-lg">
Search
</button>
</form>
{/* Lead with the real scale signals (homes + metros); show the builder count
only once it reads as breadth (>1), otherwise state the honest forward-looking
posture rather than a "1 builder" that undercuts the page. */}
<p className="mt-5 text-sm text-brand-200">
<span className="font-semibold text-white">{homeCount.toLocaleString()}</span> verified homes
{" · "}<span className="font-semibold text-white">{markets.length}</span> metros
{" · "}
{builderCount > 1 ? (
<><span className="font-semibold text-white">{builderCount}</span> builders</>
) : (
<span className="font-semibold text-white">more builders coming</span>
)}
</p>
</div>
{/* Live national map — loads on page load, one marker per community with
available inventory; click a marker to open that community. */}
<div className="relative mx-auto mt-12 max-w-6xl">
<div
className="h-[440px] w-full overflow-hidden rounded-2xl border border-brand-600/40 shadow-[var(--shadow-lift)]"
data-testid="hero-map"
>
<HeroMap markers={heroMarkers} />
</div>
<p className="mt-3 text-center text-xs text-brand-200/80">
Every marker is a community with live, verified inventory.{" "}
<Link href="/map" className="font-semibold text-accent-300 underline underline-offset-2 hover:text-accent-200">
Open the full map explorer →
</Link>
</p>
</div>
</section>
{featured.length > 0 && (
<section className="mx-auto max-w-7xl px-4 py-14" data-testid="featured-homes">
<p className="eyebrow">Featured now</p>
<h2 className="mt-1 font-display text-2xl font-semibold text-brand-900">Homes at every price</h2>
<div className="mt-6 grid gap-5 sm:grid-cols-3">
{featured.map(({ tier, home }) => (
<Link key={home.id} href={`/homes/${home.id}`} className="card-interactive overflow-hidden">
<div className="relative h-44 bg-gradient-to-br from-brand-800 via-brand-700 to-brand-500">
{showImages && home.images[0]?.startsWith("http") ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={home.images[0]} alt={`${home.community.name} home`} loading="lazy" className="absolute inset-0 h-full w-full object-cover" />
) : null}
<span className="absolute left-3 top-3 inline-flex items-center rounded-full bg-white/95 px-2.5 py-0.5 text-xs font-semibold text-brand-800 shadow-sm">
{tier}
</span>
</div>
<div className="p-4">
<div className="font-display text-2xl font-semibold text-brand-900">
{home.price === null ? "Price on request" : `$${Number(home.price).toLocaleString()}`}
</div>
<div className="mt-0.5 truncate text-sm text-neutral-600">
{home.street ?? "Address from builder"} · {home.city}, {home.state}
</div>
<div className="mt-1 text-sm text-neutral-500">
{home.beds ?? "—"} bd · {home.bathsTotal?.toString() ?? "—"} ba · {home.sqft?.toLocaleString() ?? "—"} sqft
</div>
<div className="mt-2 text-xs text-neutral-400">{home.builder.name} · {home.community.name}</div>
</div>
</Link>
))}
</div>
</section>
)}
<section className="mx-auto max-w-7xl px-4 py-14">
<div className="flex items-end justify-between">
<div>
<p className="eyebrow">Where inventory is live</p>
<h2 className="mt-1 font-display text-2xl font-semibold text-brand-900">Browse by market</h2>
</div>
<Link href="/map" className="hidden text-sm font-semibold text-brand-700 hover:text-brand-800 sm:block">
See all on the map →
</Link>
</div>
<div className="mt-6 grid gap-4 sm:grid-cols-3">
{markets.map((market) => (
<Link
key={`${market.metro}-${market.state}`}
href={`/search?q=${encodeURIComponent(market.metro?.split("-")[0] ?? "")}`}
className="card-interactive group p-6"
>
<div className="font-display text-lg font-semibold text-brand-900 group-hover:text-brand-700">
{market.metro}
</div>
<div className="mt-1 text-sm text-neutral-500">
{market.state} · {market._count._all} communities
</div>
</Link>
))}
</div>
</section>
</div>
);
}