Condo Directory
The condo directory (Panda /condos): area picker, building name search, status and sort, landing rows per area, building cards (live price ranges and counters) and the buildings map.
Anatomy
Every piece of Condo Directory as it nests in code. A root holds the data and state; its parts and hooks only work inside it.
<PandaProvider>Mounted once in app/layout.tsx<CondoDirectory />complete widgetThe condo directory (Panda /condos): area picker, building name search, status and sort, landing rows per area, building cards (live price…
<PandaDirectoryRoot>PandaDirectoryRoot({ data, syncUrl?: true, open?, detailHref?, children }): the websites condo directory with no layout (same state,…<DirectoryFilterBar />The whole websites filter bar (sticky under the header, data-condo-filter-bar): name pill, areas, status, price, beds /…
<DirectorySearchName />The "Search by name" pill (debounced), 40px with a kit theme.
<DirectoryFilterAreas />The areas popover (search box, list with counts, Apply).
<DirectoryFilterPrice />The price popover ("Price Range" title, Min / Max Price selects, histogram slider, Apply); the projects' asking price…
<DirectoryFilterStatus />For Sale / For Rent popover with Apply.
<DirectoryFilterBedsBaths />Beds / Baths popover (BedsBaths filters) with Apply.
<DirectoryFilterType />Property type popover with Apply.
<DirectoryMoreFilters />"All Filters" button and its dialog (size, types, amenities, views, year built, stories, units).
<DirectoryResultsHeader />The count ("271 Buildings", kit body size), the websites sort menu and the grid / list / map switches.
<DirectoryCount />The results count alone ("271 Buildings"), so the header is yours to lay out.
<DirectorySort />The sort menu alone (websites sort options; new developments default to newest, condos to name).
<DirectoryDisplayToggle />The grid / list switch alone.
<DirectoryLoadMore />The Load more button and the infinite-scroll sentinel (nothing without a next page).
<DirectoryFilterYear />The year built popover (Min / Max, Apply).
<DirectoryFilterUnits />The total units popover (Min / Max, Apply).
<DirectoryFilterFloors />The stories popover (Min / Max, Apply).
<DirectoryFilterSize />The size popover (Min / Max square feet, Apply).
<DirectoryResults />The results: the area rows (carousels with Show all) with no filter, the infinite grid / list / table once a filter is…
<DirectoryMap />The websites directory map (theme pin look, popups, fits the results, searches the viewport on move and on draw, hover…
<CondoAreaMap />The buildings map for your own directory: buildings takes useCondoDirectory().buildings (the page) or…
</PandaDirectoryRoot><DirectoryHeader />complete widgetHeaders of the Panda website directories.
</PandaProvider>Installation
- 1npm install <tarball or @pandaidx/widgets>Terminalnpm install https://github.com/pandaidx/panda-widgets/releases/download/v1.14.1/pandaidx-widgets-1.14.1.tgz
- 2import "@pandaidx/widgets/styles.css" in app/layout.tsx
- 3Wrap the site in <PandaProvider publicKey apiUrl preset theme> (app/layout.tsx)
- 4In each page (Server Component) call the widget's server loader, then render the widget with its data
- 5Allow the images: next.config images.remotePatterns [{ protocol: "https", hostname: "**" }] (listing and blog photos come from several CDNs)
Usage
// app/condos/page.tsximport { CondoDirectory } from "@pandaidx/widgets/client"import { fetchCondoDirectory } from "@pandaidx/widgets/server"export default async function CondosPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {const data = await fetchCondoDirectory({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, searchParams: await searchParams })return <CondoDirectory data={data} title="Condo Directory" map="side" columns={2} areaHref="/condos/areas/{slug}" />// Panda website design: <CondoDirectory variant="panda" data={await fetchPandaCondoDirectory({ publicKey, apiUrl, searchParams })} mobileHeader={isPhone} />}
Examples
The websites /condos desktop layout from parts (Panda filters, area rows, infinite grid, sticky map)
// app/condos/page.tsx (server): const data = await fetchPandaCondoDirectory({ publicKey, apiUrl, searchParams: await searchParams })"use client"import { DirectoryFilterBar, DirectoryMap, DirectoryResults, DirectoryResultsHeader, PandaDirectoryRoot, type PandaDirectoryData } from "@pandaidx/widgets/client"// The websites desktop layout: sticky filter bar, results 60% on the left, the map 40% sticky to the right edge.export const CondosView = ({ data }: { data: PandaDirectoryData }) => (<PandaDirectoryRoot data={data}><DirectoryFilterBar /><div className="flex"><div className="w-[60%]"><DirectoryResultsHeader /><DirectoryResults /></div><div className="sticky top-[var(--site-header-height,104px)] h-[calc(100dvh-var(--site-header-height,104px))] w-[40%] pb-4 pr-3"><div className="size-full overflow-hidden rounded-card"><DirectoryMap /></div></div></div></PandaDirectoryRoot>)
Websites filters and map, the site's own cards and area rows (DirectoryResults renderCard / renderAreaRow)
// app/condos/page.tsx (server): const data = await fetchPandaCondoDirectory({ publicKey, apiUrl, searchParams: await searchParams })"use client"import Image from "next/image"import Link from "next/link"import { DirectoryFilterBar, DirectoryMap, DirectoryResults, DirectoryResultsHeader, PandaDirectoryRoot, type DirectoryCardContext, type PandaDirectoryData, type SiteBuildingCard } from "@pandaidx/widgets/client"// The websites filters (same popovers, URL and query) and map; the cards and the area rows are the site's design.const BuildingCard = ({ card, ctx }: { card: SiteBuildingCard; ctx: DirectoryCardContext }) => {const body = (<><div className="relative aspect-[4/5] overflow-hidden rounded-card bg-muted">{card.image ? <Image src={card.image} alt="" fill sizes={ctx.layout === "row" ? "(min-width: 1024px) 25vw, 75vw" : "(min-width: 1024px) 30vw, 100vw"} className="object-cover" /> : null}</div><h3 className="mt-3 font-heading text-xl">{card.name}</h3><p className="text-sm text-muted-foreground">{card.city} · {card.activeListings.forSale} for sale</p></>)return (<article data-hovered={ctx.hovered || undefined} onMouseEnter={() => ctx.onHover(true)} onMouseLeave={() => ctx.onHover(false)} className="group">{/* ctx.href: the card's detail on your building route (root detailHref); ctx.open(): the building dialog. */}{ctx.href ? <Link href={ctx.href} onClick={(event) => { event.preventDefault(); ctx.open() }}>{body}</Link> : <button type="button" onClick={ctx.open} className="text-left">{body}</button>}</article>)}export const CondosView = ({ data }: { data: PandaDirectoryData }) => (<PandaDirectoryRoot data={data} detailHref="/condos/{slug}"><DirectoryFilterBar /><div className="flex"><div className="w-[60%]"><DirectoryResultsHeader /><DirectoryResultsrenderCard={(card: SiteBuildingCard, ctx) => <BuildingCard card={card} ctx={ctx} />}renderAreaRow={(row, ctx) => (<section className="px-6"><header className="mb-4 flex items-baseline justify-between"><h2 className="font-heading text-2xl">{row.area}</h2><button type="button" onClick={ctx.showAll}>Show all {row.totalCount}</button></header><div className="grid grid-cols-2 gap-6 xl:grid-cols-3">{row.cards.slice(0, 6).map((card, index) => <div key={card.id}>{ctx.renderCard(card, index)}</div>)}</div></section>)}/></div><div className="sticky top-[var(--site-header-height,104px)] h-[calc(100dvh-var(--site-header-height,104px))] w-[40%] pb-4 pr-3"><div className="size-full overflow-hidden rounded-card"><DirectoryMap /></div></div></div></PandaDirectoryRoot>)
Websites filters and map, results drawn 100% by the site (usePandaDirectory cards)
"use client"import Link from "next/link"import { DirectoryFilterBar, DirectoryMap, PandaDirectoryRoot, usePandaDirectory, type PandaDirectoryData, type SiteBuildingCard } from "@pandaidx/widgets/client"import { resolveLibraryHref } from "@pandaidx/widgets/search"const Results = () => {const { cards, areaRows, landing, total, hasMore, loadMore, isLoading, setHoveredId, open } = usePandaDirectory<SiteBuildingCard>()const list = landing ? areaRows.flatMap((row) => row.cards) : cardsreturn (<section aria-busy={isLoading}><p>{total} buildings</p><ul className="grid gap-6 sm:grid-cols-2">{list.map((card) => {const href = resolveLibraryHref(card, "default", { buildingDetail: "/condos/{slug}" })return (<li key={card.id} onMouseEnter={() => setHoveredId(card.id)} onMouseLeave={() => setHoveredId(null)}>{href ? <Link href={href}>{card.name}</Link> : <button type="button" onClick={() => open(card)}>{card.name}</button>}</li>)})}</ul>{!landing && hasMore ? <button type="button" onClick={loadMore}>Load more</button> : null}</section>)}export const CondosView = ({ data }: { data: PandaDirectoryData }) => (<PandaDirectoryRoot data={data} detailHref="/condos/{slug}"><DirectoryFilterBar /><div className="grid grid-cols-[3fr_2fr]"><Results /><div className="sticky top-0 h-dvh"><DirectoryMap /></div></div></PandaDirectoryRoot>)
Your own filter bar with the websites filter popovers
"use client"import { DirectoryFilterAreas, DirectoryFilterBedsBaths, DirectoryFilterPrice, DirectoryFilterStatus, DirectoryMoreFilters, DirectorySearchName, usePandaDirectory } from "@pandaidx/widgets/client"// Your own bar with the websites filters (their popovers, Apply buttons and query): pick and order them.export const CondoFilters = () => {const { setViewMode, viewMode } = usePandaDirectory()return (<div className="sticky top-[var(--site-header-height,104px)] z-20 flex h-14 items-center gap-2 bg-background px-6"><DirectorySearchName /><DirectoryFilterAreas /><DirectoryFilterStatus /><DirectoryFilterPrice /><DirectoryFilterBedsBaths /><DirectoryMoreFilters />{viewMode === "fullList" ? <button type="button" className="ml-auto" onClick={() => setViewMode("split")}>Show map</button> : null}</div>)}
Area pills with the total each area lists under the other filters (usePandaDirectory facets)
"use client"import { PandaDirectoryRoot, usePandaDirectory, type PandaDirectoryData } from "@pandaidx/widgets/client"// Area pills with the total each one lists under the other filters (price, beds, delivery year...).const AreaPills = ({ areas }: { areas: PandaDirectoryData["allAreas"] }) => {const { facets, query, handleSubmit } = usePandaDirectory()return (<ul className="flex flex-wrap gap-2">{areas.map((area) => {// facets is null on the landing (no filter) and before the first search: fall back to buildingCount.const count = facets ? (facets.areas[area.query] ?? 0) : Number(area.buildingCount ?? 0)if (facets && count === 0) return nullconst selected = query.selectedAreas.includes(area.query)return (<li key={area._id}><button type="button" aria-pressed={selected} onClick={() => handleSubmit({ selectedAreas: selected ? [] : [area.query] })}><span className="capitalize">{area.query.toLowerCase()}</span> <span>{count}</span></button></li>)})}</ul>)}export const CondosView = ({ data }: { data: PandaDirectoryData }) => (<PandaDirectoryRoot data={data} detailHref="/condos/{slug}"><AreaPills areas={data.allAreas} /></PandaDirectoryRoot>)
A condo directory in the site's own filters, cards and pager
// app/condos/page.tsximport { fetchCondoDirectory } from "@pandaidx/widgets/server"import { CondoDirectoryView } from "@/components/condo-directory-view"export default async function CondosPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {const data = await fetchCondoDirectory({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, searchParams: await searchParams })return <CondoDirectoryView data={data} />}// components/condo-directory-view.tsx"use client"import Image from "next/image"import Link from "next/link"import { CondoAreaMap, useCondoDirectory, type SiteCondoDirectoryResponse } from "@pandaidx/widgets/client"import { resolveLibraryHref } from "@pandaidx/widgets/search"export const CondoDirectoryView = ({ data }: { data: SiteCondoDirectoryResponse }) => {const directory = useCondoDirectory({ data })return (<section className="mx-auto max-w-7xl px-6 py-16"><div className="mb-8 flex flex-wrap gap-4"><select value={directory.query.area ?? ""} onChange={(event) => directory.update({ area: event.target.value || null })} aria-label="Area"><option value="">All areas</option>{directory.areas.map((area) => <option key={area.slug} value={area.slug}>{area.name} ({area.buildingCount})</option>)}</select><input value={directory.searchText} onChange={(event) => directory.search(event.target.value)} placeholder="Search by name" aria-label="Search by name" />{directory.filtered ? <button type="button" onClick={directory.reset}>Clear</button> : null}</div><ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3" aria-busy={directory.loading}>{directory.buildings.map((building) => {// The card is the API record: resolveLibraryHref mounts its detail URL on your building route.const href = resolveLibraryHref(building, "default", { buildingDetail: "/condos/{slug}" })const card = (<><div className="relative aspect-[4/3] overflow-hidden bg-muted">{building.image ? <Image src={building.image} alt="" fill sizes="(min-width: 1024px) 33vw, 50vw" className="object-cover" /> : null}</div><h3 className="mt-3 text-lg">{building.name}</h3><p className="text-sm opacity-60">{building.city}</p></>)return <li key={building.id}>{href ? <Link href={href} className="group block">{card}</Link> : card}</li>})}</ul><CondoAreaMap buildings={directory.buildings} routes={{ buildingDetail: "/condos/{slug}" }} className="mt-10 h-96" />{directory.totalPages > 1 ? (<nav className="mt-10 flex justify-between"><button type="button" disabled={directory.page <= 1} onClick={() => directory.setPage(directory.page - 1)}>Previous</button><button type="button" disabled={directory.page >= directory.totalPages} onClick={() => directory.setPage(directory.page + 1)}>Next</button></nav>) : null}</section>)}
API reference
CondoDirectory
The condo directory (Panda /condos): area picker, building name search, status and sort, landing rows per area, building cards (live price ranges and counters) and the buildings map. Opens with the filters of the URL (websites names: area, building, status, minprice, maxprice, minbeds, minbaths, minarea, types, features, minyear, minstories, minunits, sort, bbox / points; API names accepted too) over initialFilters, and writes every change back to the URL, so library links such as /condos?area=aventura land filtered.
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| data* | object | — | First page + filters from the server loader (fetchPandaCondoDirectory for variant="panda"). |
| mobileHeader | boolean | false | panda only: render the directory pill search header above it (websites shows it instead of the site header on phones: pass true when the visitor's user agent is a phone). |
| buildingClick | "dialog" | "navigate" | "dialog" | Deprecated: use open={{ mode: "page" }}. panda only: a building card opens the building dialog (websites) or navigates to /condos/<slug>. |
| open | object | — | panda only: how a building card opens (merged over PandaProvider open.building): dialog (default, the websites building dialog), sheet, page (navigates to href, /condos/{slug} by default; the route must exist) or none. Dialog / sheet keep the building in ?condo=<slug> (shareable, reopens on load). A new development card in this directory follows open.development. Example: { mode: "sheet", size: "lg" }. |
| initialFilters | object | — | Filters preset by the page ({ area, city, q, status, minPrice, maxPrice, minBeds, …, types, features, bbox, sort }); the URL overrides them. Pass the same object to fetchCondoDirectory({ initialFilters }). |
| title | string | — | Heading above the directory. |
| subtitle | string | — | Copy under the heading. |
| map | "side" | "top" | "off" | "side" | Map beside the cards, above them, or none. |
| columns | object | 2 | Card columns: a number 1-4 or { base, sm, md, lg, xl }. Default 2 with the map beside, else 3. |
| cardVariant | "classic" | "overlay" | "minimal" | "horizontal" | "classic" | Building card design. |
| sections | "filters" | "groups" | "results" | ["filters","groups","results"] | Blocks and their order; groups = landing rows per area (unfiltered page 1). |
| buildingHref | string | "{href}" | Host route of a building ({href} = its Panda path /condos/<slug>, {slug} = <name>--<city>). |
| areaHref | string | — | Host route of an area page ({slug}, e.g. /condos/areas/{slug} with <CondoArea>); unset = "View all" filters in place. |
| syncUrl | boolean | true | Mirrors filters and page into the URL with the websites names (the loader reads them). |
| classNames | object | — | Host classes appended to any slot (see slots). |
| preset | "default" | "template1" | — | Preset for this widget only (defaults to the provider preset). |
| variant | "default" | "template1" | "panda" | — | panda: the Panda website condo directory (pill filter bar with status / price / beds & baths / all filters and name search, landing rows per area with carousels, grid ⇄ list ⇄ table, the map with its markers and draw / drag search, infinite loading, building dialog, the mobile Map ⇄ List layout) with the website components (pass fetchPandaCondoDirectory() data; every other design prop is ignored). default / template1: deprecated alias of preset. |
| theme | object | — | Token overrides for this widget only, merged over the provider theme (same shape as PandaProvider.theme). |
DirectoryHeader
Headers of the Panda website directories. mobile: the pill search header phones get on /condos, /new-developments, building pages and area pages (building / area / city search dialog, filters, share). search: the desktop header of condo area pages (logo, area search pill, Condo Directory / New Developments tabs, account, side menu).
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| layout | "mobile" | "search" | "mobile" | Which header. |
| directory | "condos" | "new-developments" | "condos" | Directory of the mobile header (condos also covers building and area pages). |
| application | object | — | Public application (share URLs). |
| header | object | — | search layout: { logo: { src, alt }, menu } (same as PropertySearch header). |
| image | string | — | search layout: area image shown in the search pill (data.area.image2). |
| preset | "default" | "template1" | — | Preset for this widget only (defaults to the provider preset). |
| variant | "default" | "template1" | — | Deprecated alias of preset. |
| theme | object | — | Token overrides for this widget only, merged over the provider theme (same shape as PandaProvider.theme). |
Parts
<PandaDirectoryRoot />PandaDirectoryRoot({ data, syncUrl?: true, open?, detailHref?, children }): the websites condo directory with no layout (same state, /v1/condos/directory/* data, URL sync, infinite loading and building dialog as variant="panda"). data from fetchPandaCondoDirectory (server). Every Directory* part and usePandaDirectory() render inside it.
<DirectoryFilterBar />The whole websites filter bar (sticky under the header, data-condo-filter-bar): name pill, areas, status, price, beds / baths and All Filters. Kit: 40px pill triggers (--panda-button-height-sm), the name field a pill at the same height.
<DirectorySearchName />The "Search by name" pill (debounced), 40px with a kit theme. className sets its width (default w-64).
<DirectoryFilterAreas />The areas popover (search box, list with counts, Apply).
<DirectoryFilterPrice />The price popover ("Price Range" title, Min / Max Price selects, histogram slider, Apply); the projects' asking price on new developments.
<DirectoryFilterStatus />For Sale / For Rent popover with Apply.
<DirectoryFilterBedsBaths />Beds / Baths popover (BedsBaths filters) with Apply.
<DirectoryFilterType />Property type popover with Apply.
<DirectoryMoreFilters />"All Filters" button and its dialog (size, types, amenities, views, year built, stories, units).
<DirectoryResultsHeader />The count ("271 Buildings", kit body size), the websites sort menu and the grid / list / map switches.
<DirectoryCount />The results count alone ("271 Buildings"), so the header is yours to lay out. className styles the text.
<DirectorySort />The sort menu alone (websites sort options; new developments default to newest, condos to name). className styles the trigger. The options are also in usePandaDirectory().sortOptions.
<DirectoryDisplayToggle />The grid / list switch alone.
<DirectoryLoadMore />The Load more button and the infinite-scroll sentinel (nothing without a next page). Place it under your results when DirectoryResults layout={{ loadMore: false }} or when you draw the results from usePandaDirectory().
<DirectoryFilterYear />The year built popover (Min / Max, Apply). Query key yearBuiltRange.
<DirectoryFilterUnits />The total units popover (Min / Max, Apply). Query key unitsRange.
<DirectoryFilterFloors />The stories popover (Min / Max, Apply). Query key storiesRange.
<DirectoryFilterSize />The size popover (Min / Max square feet, Apply). Query key sqftRange.
<DirectoryResults />The results: the area rows (carousels with Show all) with no filter, the infinite grid / list / table once a filter is set. renderCard?: (card, ctx) => ReactNode draws each result in the site's own design: card is the typed record (SiteBuildingCard here, the same fields as useCondoDirectory; annotate it (card: SiteBuildingCard, ctx)), ctx = { index, layout: "grid" | "list" | "row", hovered, onHover(hovered), open(), href } (href = resolveLibraryHref on the root's detailHref, default /condos/{slug}; null = no link: render ctx.href ? <Link href={ctx.href}> : <button onClick={ctx.open}>; call ctx.onHover from onMouseEnter / onMouseLeave for the map). renderAreaRow?: (row, ctx) => ReactNode draws each landing row: row = { area, slug, totalCount, cards: SiteBuildingCard[] }, ctx = { showAll(), renderCard(card, index) }. The root keeps the filters, URL, infinite loading, skeletons, empty state, map hover sync and the building dialog; without them it is the websites look. layout?: { columns, gap, className, empty, loadMore } sets the grid yourself: columns is one number or per breakpoint ({ base: 1, md: 2, xl: 3 }, mobile first: base, sm 640, md 768, lg 1024, xl 1280, 2xl 1536), gap a CSS gap ("3rem 1.5rem"), className goes on the grid, empty replaces the empty state, and loadMore: false drops the built-in Load more button so you place <DirectoryLoadMore /> yourself.
<DirectoryMap />The websites directory map (theme pin look, popups, fits the results, searches the viewport on move and on draw, hover sync with the cards, kit-sized controls). Fills its parent: give the parent a height. Props for the site's layout: avoid (CSS selector of the site's panels floating over the map, e.g. a results list on the left or a card strip at the bottom: the camera frames and centers the results in the visible part), expand={false} (hide the list / full-map toggle when the site has its own), chromeCorner / zoomCorner ("top-left" | "top-right" | "bottom-left" | "bottom-right"), chromeClassName / zoomClassName (move a slot, e.g. top-16 under a floating search), showSchools, showDraw, showZoom, pinLabel(card) (each pin's text from the same typed card as the results, e.g. the asking price; null = icon dot) and renderPopup(card) (the card a pin opens over the map, e.g. the site's own project card; popupMaxWidth; popupAutoPan eases the map so the card is fully visible). Example, a full-screen map with the list floating over it: <div className="fixed inset-0"><DirectoryMap avoid="[data-map-panel]" expand={false} /><aside data-map-panel className="absolute inset-y-4 left-4 w-[36rem]">…</aside></div>.
<CondoAreaMap />The buildings map for your own directory: buildings takes useCondoDirectory().buildings (the page) or useCondoDirectory({ pins: true }).pins (every match, max 500, when fetchCondoDirectory({ pins: true }) loaded the page); hoveredId / onHover link it to your cards (building.id); routes ({ buildingDetail: "/condos/{slug}" }) points the popup links at your building pages. Give it a height with className.
Hooks
useCondoDirectory()inside PandaProvideruseCondoDirectory({ data, initialFilters?, syncUrl?: true, pins?: false }) → { buildings: { id, slug, type: "buildings", href, query, name, path, links, headline, city, address, image, location, price, rentPrice, beds, baths, sqft, activeListings… }[] (current page, live; each card is a data-link record), pins: { id, slug, kind, name, path, location, image, price }[] | null (with pins: true, also on fetchCondoDirectory({ pins: true }): every matching building with coordinates, max 500, for <CondoAreaMap buildings={pins}>), groups: { area, slug, totalCount, buildings }[] (landing rows; only page 1 with no filters), areas: { slug, name, buildingCount }[], total, page, pageSize, totalPages, query: { area, city, q, status: "forSale" | "forRent" | null, minPrice, maxPrice, minBeds, minBaths, types, features, sort… }, filtered, loading, searchText, search(text) (name box, debounced), update(change) (merges filters, back to page 1), setPage(page), reset() } — data comes from fetchCondoDirectory. Same live data, URL names and syncing as <CondoDirectory>: library links such as /condos?area=aventura land filtered. Your own filter bar, cards (link each with resolveLibraryHref(building, "default", { buildingDetail: "/condos/{slug}" }) from @pandaidx/widgets/search: the card is the unchanged API record; never building.path or a rebuilt URL), pager and map (CondoAreaMap) in the site's style. Client component only; never call /v1/condos yourself.
usePandaDirectory()inside PandaProviderusePandaDirectory<Card = SiteBuildingCard | SiteNewDevelopment>() (inside PandaDirectoryRoot) → the websites directory state: { query (websites names: status, selectedAreas, searchName, priceRange, bedsBaths, sqftRange, propertySubTypes, amenities, views, features, yearBuiltRange, storiesRange, unitsRange, country, sort), handleSubmit(change), total, isLoading, isLoadingMore, hasMore, loadMore(), areasHasMore, loadMoreAreas(), viewMode (split | fullList | fullMap), setViewMode, displayMode, setDisplayMode… } plus the results as typed records: cards: Card[] (SiteBuildingCard on condos, SiteNewDevelopment on new developments, the same fields as useCondoDirectory / useNewDevelopmentsDirectory; link each with resolveLibraryHref(card, "default", { buildingDetail: "/condos/{slug}" })), areaRows: { area, slug, totalCount, cards }[] (the landing rows per area, condos and new developments: the first projects of each area, from the first paint), landing (true = no filter is set: draw areaRows, else cards), hoveredId / setHoveredId(id) (map hover sync with DirectoryMap, card.id), developers (new developments: { name, count }[], A to Z; filter with handleSubmit({ developer: name })), open(card) (the root's dialog / sheet / page), mapCards (every map pin as a typed card with location, price, photos and developer: draw your own map with any engine) and searchMapArea(points, { drawn? }) (search a viewport box or a drawn ring, like DirectoryMap) and facets ({ areas: Record<string, number>, developers: { name, count }[] } | null: what picking each area or developer lists under the OTHER filters on screen, from the same search as the list, so facets.areas["AVENTURA"] (keys are the upper-case area.query) is exactly the total you get after choosing Aventura, also with price, beds or delivery year set; developers only on new developments. Null before the first search and on the landing with no filter: fall back to allAreas[].buildingCount and developers[].count then). Enough to draw the results, and even the map, 100% yourself.
Server data
Loaders run in a Server Component and come from @pandaidx/widgets/server. Typed data is for a page you design yourself, rendered inside the root.
fetchCondoDirectory()widget loaderFirst page of GET /v1/condos, filtered by the page URL (websites or API names) over initialFilters. Returns SiteCondoDirectoryResponse → data
fetchPandaCondoDirectory()loader of variant="panda"Data of variant="panda" (GET /v1/condos/directory?mode=condo): every area, the unfiltered first paint (landing rows + map pins) when the URL has no filters, the public application and the request-information form. The widget then loads pages from /v1/condos/directory/buildings with the website params. Returns PandaDirectoryData → data