← 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 &ldquo;spec home listings&rdquo; 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&apos; 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 &ldquo;Leander&rdquo; or &ldquo;85212&rdquo;"
              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>
  );
}