Listing Grid

Listing grids of the Panda websites outside the search.

Root + partsClient component@pandaidx/widgets 1.14.1

Anatomy

Every piece of Listing Grid as it nests in code. A root holds the data and state; its parts and hooks only work inside it. MlsDisclaimers must render in every layout (MLS and consent rules).

<PandaProvider>Mounted once in app/layout.tsx
<ListingGrid />complete widget

Listing grids of the Panda websites outside the search.

<ListingGridRoot>Holds the data and state its parts and hooks read
<ListingGridCard />

The Panda websites listing card (photo gallery, price, actions, detail dialog) for one of useListingGrid().listings,…

<ListingGridMap />

The websites listings map (price pills, clusters, popup card) for the grid's listings (listings narrows them).

<ListingRow />

Horizontal listing row: photo left, price, title, address, beds · baths · size, courtesy line and the ⋯ actions menu.

<ListingActionsMenu />

One ⋯ dropdown with every action of a listing (like, collection, share, mortgage, video, MLS sheet) for your own cards…

<ListingMlsBadge />

The MLS number pill of the Panda cards for your own card (id = the listing's ListingId): a click (or Enter / Space)…

<MlsDisclaimers />required

MLS disclaimer of each feed of the site + Panda's (required on every page that lists MLS listings).

</ListingGridRoot>
</PandaProvider>

Installation

  1. 1
    npm install <tarball or @pandaidx/widgets>
    Terminal
    npm install https://github.com/pandaidx/panda-widgets/releases/download/v1.14.1/pandaidx-widgets-1.14.1.tgz
  2. 2
    import "@pandaidx/widgets/styles.css" in app/layout.tsx
  3. 3
    Wrap the site in <PandaProvider publicKey apiUrl preset theme> (app/layout.tsx)
  4. 4
    In each page (Server Component) call the widget's server loader, then render the widget with its data
  5. 5
    Allow the images: next.config images.remotePatterns [{ protocol: "https", hostname: "**" }] (listing and blog photos come from several CDNs)

Usage

ListingGrid
// app/profile/page.tsx (listings section of the profile)
import { ListingGrid } from "@pandaidx/widgets/client"
import { fetchListingGrid } from "@pandaidx/widgets/server"
export default async function ProfilePage({ searchParams }: { searchParams: Promise<{ listings?: string; page?: string }> }) {
const { listings, page } = await searchParams
const status = listings === "sold" ? "sold" : "active"
const data = await fetchListingGrid({
publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!,
apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL,
source: "agent",
status,
page: Number(page) || 1,
})
if (!data) return null
return <ListingGrid data={data} pagination={data.hasMore ? { nextHref: `?listings=${status}&page=${data.page + 1}#listings`, nextLabel: "Next" } : undefined} />
}
// app/collection/[slug]/page.tsx: const data = await fetchListingGrid({ ..., source: "collection", slug }); <ListingGrid data={data} />

Examples

A profile listings section in the site's own grid (tabs, cards, pages)

ListingGrid
// app/profile/page.tsx: Panda loads the listings, the site draws them
import { ListingGridRoot } from "@pandaidx/widgets/client"
import { fetchListingGrid } from "@pandaidx/widgets/server"
import { ListingsSection } from "@/components/listings-section"
export default async function ProfilePage({ searchParams }: { searchParams: Promise<{ listings?: string; page?: string }> }) {
const { listings, page } = await searchParams
const data = await fetchListingGrid({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, source: "agent", status: listings === "sold" ? "sold" : "active", page: Number(page) || 1 })
if (!data) return null
return (
<ListingGridRoot data={data}>
<ListingsSection />
</ListingGridRoot>
)
}
// components/listings-section.tsx
"use client"
import Image from "next/image"
import { ListingActionsMenu, MlsDisclaimers, useListingActions, useListingGrid, type SiteListingCard } from "@pandaidx/widgets/client"
const Card = ({ listing }: { listing: SiteListingCard }) => {
const actions = useListingActions(listing)
return (
<article className="group">
<button type="button" onClick={actions.openDetail} className="relative block aspect-[4/3] w-full overflow-hidden bg-muted">
{listing.image ? <Image src={listing.image} alt="" fill sizes="(min-width: 1024px) 33vw, 50vw" className="object-cover" /> : null}
</button>
<div className="mt-3 flex items-start justify-between">
<p className="text-lg">{listing.ListPrice ? `$${listing.ListPrice.toLocaleString()}` : null}</p>
<ListingActionsMenu listing={listing} />
</div>
{listing.ListOfficeName ? <p className="mt-1 text-xs text-muted-foreground">Listing by {listing.ListOfficeName}</p> : null}
</article>
)
}
export const ListingsSection = () => {
const { listings, status, hasMore, page } = useListingGrid()
return (
<section className="mx-auto max-w-7xl px-6 py-16">
<nav className="mb-8 flex gap-6">
<a href="?listings=active#listings" aria-current={status === "active"}>Active</a>
<a href="?listings=sold#listings" aria-current={status === "sold"}>Sold</a>
</nav>
<div id="listings" className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{listings.map((listing) => <Card key={listing._id} listing={listing} />)}
</div>
{hasMore ? <a href={`?listings=${status}&page=${page + 1}#listings`}>Next</a> : null}
<MlsDisclaimers className="mt-12" />
</section>
)
}

Panda's cards and map in your own layout

ListingGrid
// Inside <ListingGridRoot data={data}> (a client component)
import { ListingGridCard, ListingGridMap, MlsDisclaimers, useListingGrid } from "@pandaidx/widgets/client"
export const CollectionBody = () => {
const { listings, collection } = useListingGrid()
return (
<div className="grid gap-8 lg:grid-cols-[1fr_28rem]">
<div>
<h1 className="text-3xl">{collection?.name}</h1>
<div className="mt-6 grid gap-6 sm:grid-cols-2">{listings.map((listing) => <ListingGridCard key={listing._id} listing={listing} />)}</div>
<MlsDisclaimers className="mt-10" />
</div>
<ListingGridMap className="sticky top-24 h-[32rem] rounded-2xl" />
</div>
)
}

API reference

ListingGrid

Listing grids of the Panda websites outside the search. inventory: the owner's or office's MLS listings (/profile, Personal/Office dropdown + Active/Sold square tabs, 320-400px cards, Prev/Next page links) or a team member's (/team/<slug>). collection: the /collection/<slug> body (title, count, filter, Share dialog, "N places" + sort, cards and a sticky rounded map). Cards are the websites listing cards and open the websites detail dialog. The MLS disclaimers show under the listings.

Import from @pandaidx/widgets/client · renders inside PandaProvider

PropTypeDefaultDescription
data*object—Listings, collection header data and the public application from the loader.
layout"inventory" | "collection"—Section design; default collection for collection data, inventory otherwise.
titlestring—Inventory title above the tabs (team page: "Listings").
tabsarray—Status tabs [{ href, label, selected }] (links). Default Active / Sold → ?listings=active|sold#<sectionId> (read listings from the page searchParams and pass status to the loader). For any other status (Pending, Under Contract) or filter, pass your own tabs: each links to the page with the status / filters it loads with fetchListingGrid({ status, filters }). [] (or a single tab) hides the row, for a page that renders its own tabs.
activeLabelstring"Active"Label of the default Active tab.
soldLabelstring"Sold"Label of the default Sold tab.
sourceMenuobject—{ label, options: [{ href, label, selected }] } Personal / Office dropdown (shown with 2+ options), like the websites profile.
paginationobject—{ prevHref, nextHref, prevLabel, nextLabel } page links (websites pages the inventory by 20; use data.hasMore).
emptyMessagestring—Empty state title when there are no listings.
sectionIdstring"listings"Id of the section (the tabs' hash target, e.g. office-listings).
showMapboolean—Listings map (sticky beside the cards on desktop, 280px on top on phones). Default on for collections, off for inventories.
showHeaderbooleantrueCollection header (title, count, filter, Share).
cardVariant"panda" | "collection"—Card of the collection layout / map split: collection (collection page card, default for collections) or panda (search result card).
cardobject—Design of the panda cards: { actions: { items, placement: price|image|footer, align, floating, floatingPosition, style: icon|button }, gallery: arrows|autoplay|none, galleryInterval, radius, badges: { mls, age, ageTone, style: mixed|uniform }, feedback: { copy: toast|check|none, hints: hover|popover|none } } (same as PropertySearch.card).
disclaimersbooleantrueThe feeds' MLS disclaimers under the listings (required by the IDX rules wherever MLS listings show). false only when the page renders MlsDisclaimers itself.
preset"default" | "template1"—Preset for this widget only (defaults to the provider preset).
variant"default" | "template1"—Deprecated alias of preset.
themeobject—Token overrides for this widget only, merged over the provider theme (same shape as PandaProvider.theme).

Parts

<ListingGridCard />

The Panda websites listing card (photo gallery, price, actions, detail dialog) for one of useListingGrid().listings, shaped by the root's card. Use it when only the grid around the cards is yours.

<ListingGridMap />

The websites listings map (price pills, clusters, popup card) for the grid's listings (listings narrows them). It fills its parent: give it a height with className. hoveredListingId / onHoverListing link it to your cards.

<ListingRow />

Horizontal listing row: photo left, price, title, address, beds · baths · size, courtesy line and the ⋯ actions menu. Opens the listing detail.

<ListingActionsMenu />

One ⋯ dropdown with every action of a listing (like, collection, share, mortgage, video, MLS sheet) for your own cards and rows; listing takes a grid listing as is.

<ListingMlsBadge />

The MLS number pill of the Panda cards for your own card (id = the listing's ListingId): a click (or Enter / Space) copies the number and the pill swaps to a check + copiedLabel (default "Copied") for 1.5s, then back, at a stable width; no toast (feedback: check | toast | none; onCopy after the copy). Reads the card badge tokens (--panda-listing-card-badge-*); place it over your photo (absolute top-3 left-3).

<MlsDisclaimers />required

MLS disclaimer of each feed of the site + Panda's (required on every page that lists MLS listings). Inside ListingGridRoot it needs no props; elsewhere pass application from any Panda loader (fetchListingGrid, fetchAgentProfile, getSearchResults). The text and logo come from the Panda API. layout="inline" puts each logo on the left of its text.

Hooks

useListingGrid()inside ListingGridRoot

{ source: "agent" | "office" | "team" | "collection", status: "active" | "pending" | "active-under-contract" | "sold" | null, listings, total, page, pageSize, hasMore, collection: { name, description, path, totalCount } | null, application, isLiked(listing) } — the data of the nearest ListingGridRoot: your own title, status tabs (links to ?status=active|pending|sold), count, empty state and page links (hasMore). Client component only.

useListingActions()inside ListingGridRoot

{ signedIn, liked, likePending, toggleLike(), openCollections(), share(), openMortgage(), videoUrl, openVideo(), mlsSheetUrl, detailHref, openDetail(), available(action) } for one grid listing (as is: _id, ListingId, OriginatingSystemKey, ListPrice, image…). The detail, collections, share, mortgage and video dialogs open from the root's ListingActionsHost. Client component only.

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.

fetchListingGrid()widget loader

Listings of an inventory or a collection + the public application and request-showing form; null when the team member or collection does not exist (call notFound()). Returns ListingGridData → data