New Developments
Pre-construction directory (Panda /new-developments): country, city, status, price and name filters, landing rows per area, project cards (developer, units, asking range) and the map.
Anatomy
Every piece of New Developments 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<NewDevelopments />complete widgetPre-construction directory (Panda /new-developments): country, city, status, price and name filters, landing rows per area, project cards…
<PandaDirectoryRoot>PandaDirectoryRoot({ data, syncUrl?: true, open?, detailHref?, children }): the websites new developments directory with no layout (same…<DirectoryFilterBar />The whole websites filter bar (sticky under the header, data-condo-filter-bar): name pill, country, areas and price.
<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…
<DirectoryFilterCountry />The country popover (nothing when the tenant's projects are in one country).
<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 delivery year popover (Min / Max, Apply): a project's newDevelopmentInfo.year.
<DirectoryFilterUnits />The units popover (Min / Max, Apply): a project's total units.
<DirectoryFilterFloors />The floors popover (Min / Max, Apply): a project's floors.
<DirectoryFilterSize />The size popover (Min / Max square feet, Apply): a project's area range.
<DirectoryFilterDeveloper />The developer popover: every developer with its number of projects (a search box narrows the list), one tap filters by…
<DirectoryResults />The results: the infinite grid of projects.
<DirectoryMap />The websites directory map (theme pin look, popups, fits the results, searches the viewport on move and on draw, hover…
<NewDevelopmentsMap />NewDevelopmentsMap({ developments, hoveredId?, onHover?, routes?, className }): one price pill per project with a…
</PandaDirectoryRoot></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/new-developments/page.tsximport { NewDevelopments } from "@pandaidx/widgets/client"import { fetchNewDevelopments } from "@pandaidx/widgets/server"export default async function NewDevelopmentsPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {const data = await fetchNewDevelopments({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, searchParams: await searchParams })return <NewDevelopments data={data} title="New Developments" map="side" />}
Examples
The websites /new-developments desktop layout from parts
// app/new-developments/page.tsx (server): const data = await fetchPandaNewDevelopments({ 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 NewDevelopmentsView = ({ 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 project cards (DirectoryResults renderCard)
"use client"import Image from "next/image"import Link from "next/link"import { DirectoryFilterBar, DirectoryMap, DirectoryResults, PandaDirectoryRoot, type PandaDirectoryData, type SiteNewDevelopment } from "@pandaidx/widgets/client"export const NewDevelopmentsView = ({ data }: { data: PandaDirectoryData }) => (<PandaDirectoryRoot data={data} detailHref="/new-development/{slug}"><DirectoryFilterBar /><div className="flex"><div className="w-[60%]"><DirectoryResultsrenderCard={(card: SiteNewDevelopment, ctx) => {const body = (<><div className="relative aspect-video overflow-hidden rounded-card bg-muted">{card.images.cover ? <Image src={card.images.cover} alt="" fill sizes="(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.developer].filter(Boolean).join(" · ")}</p></>)return (<article onMouseEnter={() => ctx.onHover(true)} onMouseLeave={() => ctx.onHover(false)}>{ctx.href ? <Link href={ctx.href}>{body}</Link> : <button type="button" onClick={ctx.open}>{body}</button>}</article>)}}/></div><div className="sticky top-[var(--site-header-height,104px)] h-[calc(100dvh-var(--site-header-height,104px))] w-[40%]"><DirectoryMap /></div></div></PandaDirectoryRoot>)
Developer list with the projects each developer has under the other filters (usePandaDirectory facets)
"use client"import { PandaDirectoryRoot, usePandaDirectory, type PandaDirectoryData } from "@pandaidx/widgets/client"// Developer list with the projects each one has under the other filters (price, beds, delivery year...).const DeveloperList = () => {const { developers, facets, handleSubmit } = usePandaDirectory()// facets is null on the landing and before the first search: `developers` (every developer, A to Z) fills in.const list = facets ? facets.developers : developersreturn (<ul>{list.map((developer) => (<li key={developer.name}><button type="button" onClick={() => handleSubmit({ developer: developer.name })}>{developer.name} <span>{developer.count}</span></button></li>))}</ul>)}export const NewDevelopmentsView = ({ data }: { data: PandaDirectoryData }) => (<PandaDirectoryRoot data={data} detailHref="/new-development/{slug}"><DeveloperList /></PandaDirectoryRoot>)
A new developments directory in the site's own cards
// app/new-developments/page.tsximport { fetchNewDevelopments } from "@pandaidx/widgets/server"import { NewDevelopmentsView } from "@/components/new-developments-view"export default async function Page({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {const data = await fetchNewDevelopments({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, searchParams: await searchParams })return <NewDevelopmentsView data={data} />}// components/new-developments-view.tsx"use client"import Image from "next/image"import Link from "next/link"import { NewDevelopmentsMap, useNewDevelopmentsDirectory, type SiteNewDevelopmentsDirectoryResponse } from "@pandaidx/widgets/client"import { resolveLibraryHref } from "@pandaidx/widgets/search"export const NewDevelopmentsView = ({ data }: { data: SiteNewDevelopmentsDirectoryResponse }) => {const directory = useNewDevelopmentsDirectory({ data })return (<section className="mx-auto max-w-7xl px-6 py-16"><input value={directory.searchText} onChange={(event) => directory.search(event.target.value)} placeholder="Search projects" aria-label="Search projects" className="mb-8" /><ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3" aria-busy={directory.loading}>{directory.newDevelopments.map((development) => {const href = resolveLibraryHref(development, "default", { newDevelopmentDetail: "/new-development/{slug}" })const card = (<><div className="relative aspect-[4/3] overflow-hidden bg-muted">{development.images.cover ? <Image src={development.images.cover} alt="" fill sizes="(min-width: 1024px) 33vw, 50vw" className="object-cover" /> : null}</div><h3 className="mt-3 text-lg">{development.name}</h3><p className="text-sm opacity-60">{development.city}</p></>)return <li key={development.id}>{href ? <Link href={href}>{card}</Link> : card}</li>})}</ul><NewDevelopmentsMap developments={directory.newDevelopments} routes={{ newDevelopmentDetail: "/new-development/{slug}" }} className="mt-10 h-96" /></section>)}
A new developments directory with your own filters row, header, grid and cards (parts + layout)
"use client"import Image from "next/image"import Link from "next/link"import {DirectoryCount, DirectoryDisplayToggle, DirectoryFilterAreas, DirectoryFilterCountry, DirectoryFilterDeveloper, DirectoryFilterPrice, DirectoryFilterUnits, DirectoryFilterYear, DirectoryLoadMore, DirectoryMap,DirectoryResults, DirectorySearchName, DirectorySort, PandaDirectoryRoot, type PandaDirectoryData, type SiteNewDevelopment,} from "@pandaidx/widgets/client"// Every piece is yours to place: your filter row from the websites popovers, your header (count, sort, grid / list), your grid// (columns per breakpoint, gap, empty state, where "Load more" goes) and your cards. The map is a part too: put it above,// beside, inside a dialog, or leave it out. The root keeps the filters, URL, infinite loading and the hover sync.export const NewDevelopmentsView = ({ data }: { data: PandaDirectoryData }) => (<PandaDirectoryRoot data={data} detailHref="/new-development/{slug}"><div className="flex flex-wrap items-center gap-3"><DirectorySearchName className="w-72" /><DirectoryFilterCountry /><DirectoryFilterAreas /><DirectoryFilterPrice /><DirectoryFilterYear /><DirectoryFilterUnits /><DirectoryFilterDeveloper /><div className="ml-auto flex items-center gap-4"><DirectoryCount /><DirectorySort /><DirectoryDisplayToggle /></div></div><DirectoryResultslayout={{ columns: { base: 1, md: 2, xl: 3 }, gap: "3rem 2rem", loadMore: false, empty: <p>No projects match these filters.</p> }}renderCard={(card: SiteNewDevelopment, ctx) => {const body = (<><div className="relative aspect-[4/5] overflow-hidden">{card.images.cover ? <Image src={card.images.cover} alt="" fill sizes="(min-width: 1280px) 30vw, 100vw" className="object-cover" /> : null}</div><h3 className="mt-3 text-xl">{card.name}</h3><p className="text-sm opacity-70">{[card.city, card.developer].filter(Boolean).join(" · ")}</p>{card.headline ? <p className="mt-1 text-sm">{card.headline}</p> : null}</>)return (<article onMouseEnter={() => ctx.onHover(true)} onMouseLeave={() => ctx.onHover(false)}>{ctx.href ? <Link href={ctx.href}>{body}</Link> : <button type="button" onClick={ctx.open}>{body}</button>}</article>)}}/><DirectoryLoadMore /><div className="mt-10 h-[480px] overflow-hidden rounded-card"><DirectoryMap /></div></PandaDirectoryRoot>)
API reference
NewDevelopments
Pre-construction directory (Panda /new-developments): country, city, status, price and name filters, landing rows per area, project cards (developer, units, asking range) and the map. Opens with the filters of the URL (country, city or the websites area, status, minprice, maxprice, building; API names accepted) over initialFilters and writes every change back to the URL, so area links such as /new-developments?city=Miami land filtered.
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| data* | object | — | First page + filters from the server loader. |
| initialFilters | object | — | Filters preset by the page ({ country, city, status, minPrice, maxPrice, q }); the URL overrides them. Pass the same object to fetchNewDevelopments({ 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 (default 2 with the map beside, else 3). |
| cardVariant | "classic" | "overlay" | "minimal" | "horizontal" | "classic" | Project card design. |
| sections | "filters" | "groups" | "results" | ["filters","groups","results"] | Blocks and their order; groups = landing rows per area (unfiltered page 1). |
| developmentHref | string | "{href}" | Host route of a project ({href} = its Panda path /new-development/<slug>, {slug}). |
| 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). |
| 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). |
| preset | "default" | "template1" | — | Preset for this widget only (defaults to the provider preset). |
| variant | "default" | "template1" | "panda" | — | panda: the Panda website new developments directory (pill filters: name, country with flags, areas, price with histogram, show map; count + Sort + grid ⇄ list; gallery cards with logo, share and price; the map with its markers; load more; the mobile Map ⇄ List layout) with the website components (pass fetchPandaNewDevelopments() 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). |
| open | object | — | panda only: how a new development card opens (merged over PandaProvider open.development): page (default: navigates to href, /new-development/{slug}; the route must exist), dialog / sheet (the new development page in a dialog or side panel, kept in ?project=<slug>) or none. Example: { mode: "dialog" }. |
Parts
<PandaDirectoryRoot />PandaDirectoryRoot({ data, syncUrl?: true, open?, detailHref?, children }): the websites new developments directory with no layout (same state, /v1/condos/directory/* data, URL sync, infinite loading and building dialog as variant="panda"). data from fetchPandaNewDevelopments (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, country, areas and price. 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.
<DirectoryFilterCountry />The country popover (nothing when the tenant's projects are in one country).
<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 delivery year popover (Min / Max, Apply): a project's newDevelopmentInfo.year. Query key yearBuiltRange.
<DirectoryFilterUnits />The units popover (Min / Max, Apply): a project's total units. Query key unitsRange.
<DirectoryFilterFloors />The floors popover (Min / Max, Apply): a project's floors. Query key storiesRange.
<DirectoryFilterSize />The size popover (Min / Max square feet, Apply): a project's area range. Query key sqftRange. DirectoryFilterBedsBaths also filters projects: a project matches when the bedrooms / bathrooms it offers (its listed units, or the range entered in Admin) overlap the ones asked for. DirectoryFilterType and DirectoryMoreFilters read the listed units of the building. For any other filter build your own control with usePandaDirectory().handleSubmit({ unitsRange: { min: "100", max: "" } }).
<DirectoryFilterDeveloper />The developer popover: every developer with its number of projects (a search box narrows the list), one tap filters by it. The list is usePandaDirectory().developers ({ name, count }[], A to Z, it follows the projects entered in Admin): draw your own developer list with it and handleSubmit({ developer: name }). Query key developer (exact name, case-insensitive).
<DirectoryResults />The results: the infinite grid of projects. renderCard?: (card, ctx) => ReactNode draws each project in the site's own design: card is the typed SiteNewDevelopment (the same fields as useNewDevelopmentsDirectory; annotate it (card: SiteNewDevelopment, ctx)), ctx = { index, layout: "grid" | "list", hovered, onHover(hovered), open(), href } (href = resolveLibraryHref on the root's detailHref, default /new-development/{slug}; null = no link). renderAreaRow?: (row, ctx) => ReactNode draws the landing rows per area (row = { area, slug, totalCount, cards: SiteNewDevelopment[] }, ctx = { showAll(), renderCard(card, index) }): pass it and, with no filter set, the results are those rows instead of the infinite grid. The root keeps the filters, infinite loading, skeletons, empty state and map hover sync. 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>.
<NewDevelopmentsMap />NewDevelopmentsMap({ developments, hoveredId?, onHover?, routes?, className }): one price pill per project with a location (the theme's map pin look), a popup with name, city and price linking to the project (routes: { newDevelopmentDetail: "/new-development/{slug}" }), zoom controls at the kit's small control size. developments takes useNewDevelopmentsDirectory().newDevelopments; give it a height with className.
Hooks
useNewDevelopmentsDirectory()inside PandaProvideruseNewDevelopmentsDirectory({ data, initialFilters?, syncUrl?: true }) → { newDevelopments: { id, slug, type: "newDevelopments", href, query, name, path, links, city, developer, units, yearBuilt, price, area, images: { cover, gallery }, activeListings… }[] (current page), groups: { area, slug, totalCount, newDevelopments }[] (landing rows; only page 1 with no filters), countries: { code, name, count }[], total, page, pageSize, totalPages, query: { country, city, status: "forSale" | "forRent" | null, minPrice, maxPrice, q }, filtered, loading, searchText, search(text) (name box, debounced), update(change) (merges filters, back to page 1), setPage(page), reset() } — data comes from fetchNewDevelopments. Same live data, URL names and syncing as <NewDevelopments>. Your own filters, cards (link each with resolveLibraryHref(development, "default", { newDevelopmentDetail: "/new-development/{slug}" }) from @pandaidx/widgets/search; never development.path), pager and map (NewDevelopmentsMap) in the site's style. There is no sort: the API lists projects by name. Client component only; never call /v1/new-developments 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.
fetchNewDevelopments()widget loaderFirst page of GET /v1/new-developments/directory, filtered by the page URL (country, city / area, status, minprice / minPrice, maxprice / maxPrice, building / q, page) over initialFilters. Returns SiteNewDevelopmentsDirectoryResponse → data
fetchPandaNewDevelopments()loader of variant="panda"Data of variant="panda" (GET /v1/condos/directory?mode=newdev): areas, countries, the unfiltered first paint when the URL has no filters, the public application and the request-information form. Returns PandaDirectoryData → data