Fazekit

Code

"use client";
import * as React from "react";
import { MotionConfig, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { defaultAgencyData, type AgencyData, type Listing } from "./data";
import { Mortgage, Neighbourhoods, SoldStats } from "./sections/guides";
import { DEFAULT_FILTERS, Hero, type Filters } from "./sections/hero";
import { Listings } from "./sections/listings";
import { Navbar } from "./sections/navbar";
import { Agents, Footer, Testimonials, Valuation, type ValuationRequest } from "./sections/people";
import { HP_SERIF_STACK, scrollToId, useMoney } from "./sections/ui";

export type { AgencyData, Listing, Filters, ValuationRequest };
export { defaultAgencyData };

export interface RealEstateAgencyTemplateProps {
  /** Override any top-level content group (brand, listings, agents…). Missing groups fall back to the demo content. */
  data?: Partial<AgencyData>;
  /** Called when a visitor runs a property search. */
  onSearch?: (filters: Filters) => void;
  /** Called when a listing is saved (`true`) or unsaved (`false`). */
  onSaveListing?: (listing: Listing, saved: boolean) => void;
  /** Called when the valuation form is submitted. Return a promise to keep the loading state. */
  onValuationRequest?: (request: ValuationRequest) => void | Promise<void>;
  /** Called when someone subscribes to listing alerts. */
  onSubscribe?: (email: string) => void;
  className?: string;
}

/**
 * Harbor & Pine — a warm, editorial real-estate agency landing page.
 * Animated harbour hero with property search, listings with photo carousels, neighbourhood guides,
 * sold-stats counters, a mortgage calculator with amortization chart, agents, an instant-valuation form,
 * auto-advancing testimonials and a footer with listing alerts.
 */
export function RealEstateAgencyTemplate({ data, onSearch, onSaveListing, onValuationRequest, onSubscribe, className }: RealEstateAgencyTemplateProps) {
  const d: AgencyData = React.useMemo(() => ({ ...defaultAgencyData, ...data }), [data]);
  const reduce = useReducedMotion();
  const money = useMoney(d.brand.currency);
  const [filters, setFilters] = React.useState<Filters>(DEFAULT_FILTERS);
  const [saved, setSaved] = React.useState<Set<string>>(() => new Set(["l2"]));
  const [valuation, setValuation] = React.useState({ address: "", nonce: 0 });

  const search = () => {
    onSearch?.(filters);
    scrollToId("listings", reduce);
  };
  const toggleSave = (id: string) => {
    const on = !saved.has(id);
    setSaved((cur) => {
      const n = new Set(cur);
      if (on) n.add(id);
      else n.delete(id);
      return n;
    });
    const l = d.listings.find((x) => x.id === id);
    if (l) onSaveListing?.(l, on);
  };
  const toValuation = (address = "") => {
    setValuation((v) => ({ address, nonce: v.nonce + 1 }));
    scrollToId("sell", reduce);
  };

  return (
    <MotionConfig reducedMotion="user">
      <div className={cn("relative w-full bg-background text-foreground antialiased selection:bg-[#24543f]/20", className)} style={{ "--hp-serif": HP_SERIF_STACK } as React.CSSProperties}>
        <Navbar brand={d.brand} links={d.nav} onValuation={() => toValuation()} />
        <main>
          <Hero data={d} filters={filters} setFilters={setFilters} onSearch={search} onValuation={toValuation} />
          <Listings data={d} money={money} filters={filters} setFilters={setFilters} saved={saved} onSave={toggleSave} />
          <Neighbourhoods
            data={d}
            money={money}
            onBrowse={(area) => {
              setFilters({ ...DEFAULT_FILTERS, mode: filters.mode, area });
              scrollToId("listings", reduce);
            }}
          />
          <SoldStats stats={d.stats} />
          <Mortgage config={d.mortgage} money={money} />
          <Agents agents={d.agents} />
          <Valuation data={d} money={money} prefill={valuation} onSubmit={onValuationRequest} />
          <Testimonials items={d.testimonials} agents={d.agents} />
        </main>
        <Footer data={d} onSubscribe={onSubscribe} />
      </div>
    </MotionConfig>
  );
}

More in Landing Pages

View all →