All use cases
Build this app

A real estate listing portal

Search, map, listing detail and enquiry capture — with the agent-facing side built in.

The problem

Property search is a map and a grid showing the same result set, and they have to agree. Filters change both. Clicking a pin scrolls the list; hovering a card highlights the pin. Every portal rebuilds that from scratch, usually around a map library that owns its own state and disagrees with React about who is in charge.

Then the agent side arrives: your sales team wants to hand a buyer a link and walk them through listings live, and an AI assistant should be able to shortlist properties against a brief without anyone screen-scraping the site.

What you are building

Live surfaces, not screenshots — every one composed from the same components you would install. React + Inertia on Laravel. The map is engine-agnostic, so OpenStreetMap in development and Google in production is a one-line swap.

Listing grid

Cards, badges and typography straight from react-fancy — no bespoke CSS.

Listings128 results
New

$745,000

1420 Larkspur Lane

Boulder, CO

4 bd3 ba2,840 sqft
Open Sat

$612,000

88 Halyard Court

Annapolis, MD

3 bd2 ba2,110 sqft

$529,000

3007 Mesa Verde Dr

Santa Fe, NM

3 bd2 ba1,960 sqft

Map search

fancy-map over OpenStreetMap; the same component swaps to Google without touching your code.

Search area3 shown

Viewing request

Controlled fields with stable handles, so an agent can fill this without scraping the DOM.

Request a viewing

Agent replies within 1 business hour

The code

Map and list share one statetsx
// `view`, `markers` and `selectedId` are all CONTROLLED, so the map never holds
// state the list disagrees with. Selecting in either updates the same value.
const [selectedId, setSelectedId] = useState<string | null>(null);
const [view, setView] = useState({ center: { lat: 40.015, lng: -105.27 }, zoom: 12 });

<Map
  provider={leafletProvider()}          // googleProvider({ apiKey }) in production
  view={view}
  onViewChange={setView}
  markers={listings.map((l) => ({
    id: l.id,
    position: { lat: l.lat, lng: l.lng },
    label: formatPrice(l.price),
  }))}
  selectedId={selectedId}
  onSelect={setSelectedId}
/>
Filters run on the serverphp
// Inertia hands the filtered set to the page; the map and the grid render from
// the same array, so they cannot drift apart.
public function index(Request $request): Response
{
    $filters = $request->validate([
        'min_price' => ['nullable', 'integer'],
        'beds'      => ['nullable', 'integer', 'min:0'],
        'bounds'    => ['nullable', 'array'],
    ]);

    return Inertia::render('Listings/Index', [
        'filters'  => $filters,
        'listings' => Listing::query()
            ->when($filters['min_price'] ?? null, fn ($q, $v) => $q->where('price', '>=', $v))
            ->when($filters['beds'] ?? null, fn ($q, $v) => $q->where('beds', '>=', $v))
            ->withinBounds($filters['bounds'] ?? null)
            ->limit(200)
            ->get(),
    ]);
}
Let an agent shortlist against a briefts
// The map bridge exposes pan / fit / select as MCP tools, so an assistant works
// the same controlled state a human does -- no DOM scraping, no Playwright.
import { registerMapBridge } from "@particle-academy/agent-integrations";

registerMapBridge(server, {
  adapter: {
    getView: () => view,
    setView,
    listMarkers: () => markers,
    select: (id) => setSelectedId(id),
  },
});

How to solve it

  1. Install the map and the kit

    The map core carries no engine, so you choose Leaflet or Google per environment without changing component code.

    Run thisbash
    npm i @particle-academy/react-fancy @particle-academy/fancy-map
  2. Render the grid and the map from ONE array

    Both read the same server-provided listings. This is the decision that stops the two views disagreeing — everything else follows from it.

  3. Make selection controlled

    Hold selectedId in the page, not in the map. A pin click and a card click become the same state change, which is what makes hover-to-highlight fall out for free.

  4. Capture the enquiry

    Controlled fields with stable handles. That is the Human+ requirement, and it is also what makes the form testable without selectors.

  5. Make it findable

    Listings are the crawlable part of the business. fancy-seo server-renders the head and emits per-listing JSON-LD, so a property page arrives complete in the first byte.