Agent Profile

The Panda websites profile page (/profile, /about) or a team member page (/team/<slug>): the About Moderna hero (languages eyebrow, uppercase name with a side rule, title, email, phone, social links, full bio beside the…

Headless hookClient component@pandaidx/widgets 1.14.1

Anatomy

Every piece of Agent Profile 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
<AgentProfile />complete widget

The Panda websites profile page (/profile, /about) or a team member page (/team/<slug>): the About Moderna hero (languages eyebrow,…

<AgentAbout />complete widget

The Panda websites "About Moderna" block on its own (homepage About section): languages eyebrow, name, title, contacts, social links and…

</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

AgentProfile
// app/profile/page.tsx
import { notFound } from "next/navigation"
import { AgentProfile } from "@pandaidx/widgets/client"
import { fetchAgentProfile } from "@pandaidx/widgets/server"
export default async function ProfilePage({ searchParams }: { searchParams: Promise<{ listings?: string; inventory?: string; status?: string; page?: string }> }) {
const data = await fetchAgentProfile({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, searchParams: await searchParams })
if (!data) notFound()
return <AgentProfile data={data} />
}
// app/team/[slug]/page.tsx: fetchAgentProfile({ ..., member: slug, searchParams }) → <AgentProfile data={data} />

Examples

A profile with its own hero, inventory tabs and reviews

AgentProfile
// app/profile/page.tsx
import { notFound } from "next/navigation"
import { fetchAgentProfile } from "@pandaidx/widgets/server"
import { ProfileView } from "@/components/profile-view"
export default async function ProfilePage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const data = await fetchAgentProfile({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, searchParams: await searchParams })
if (!data) notFound()
return <ProfileView data={data} />
}
// components/profile-view.tsx
"use client"
import Link from "next/link"
import { ListingGridCard, ListingGridRoot, MlsDisclaimers, useAgentProfile, useListingGrid, type AgentProfileData } from "@pandaidx/widgets/client"
const Cards = () => {
const grid = useListingGrid()
return (
<>
<div className="grid gap-6 md:grid-cols-3">{grid.listings.map((listing) => <ListingGridCard key={listing._id} listing={listing} />)}</div>
<MlsDisclaimers className="mt-10" />
</>
)
}
export const ProfileView = ({ data }: { data: AgentProfileData }) => {
const profile = useAgentProfile({ data })
return (
<main className="mx-auto max-w-6xl px-4 py-16">
<h1 className="text-4xl">{profile.name}</h1>
{profile.biography.map((text) => <p key={text} className="mt-4">{text}</p>)}
{profile.emailHref ? <a href={profile.emailHref}>{profile.email}</a> : null}
{profile.listings && profile.inventory ? (
<section className="mt-12">
{profile.hasOffice ? <div id="office-listings" /> : null}
<nav id="listings" className="mb-6 flex gap-4">
{profile.inventory.tabs.map((tab) => <Link key={tab.href} href={tab.href} aria-current={tab.selected}>{tab.label}</Link>)}
</nav>
<ListingGridRoot data={profile.listings}><Cards /></ListingGridRoot>
</section>
) : null}
</main>
)
}

API reference

AgentProfile

The Panda websites profile page (/profile, /about) or a team member page (/team/<slug>): the About Moderna hero (languages eyebrow, uppercase name with a side rule, title, email, phone, social links, full bio beside the portrait or a YouTube play overlay, on the agent's background color with contrasting text), the MLS inventory (ListingGrid: Personal/Office menu, Active/Sold tabs, pages of 20; a member's Active/Sold listings), the videos band (VideoGallery), the black quote band with a Contact button and the Client Reviews grid (five stars).

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

PropTypeDefaultDescription
data*object—Owner profile or team member page (null → notFound()).
sections"about" | "listings" | "videos" | "quote" | "reviews"["about","listings","videos","quote","reviews"]Blocks to render (websites order kept). A member page only has about + listings.
profileTheme"moderna""moderna"About hero design (the websites profile theme).
contactHrefstring"/contact"Link of the quote band's Contact button.
contactLabelstring"Contact"Text of the quote band button.
videosTitlestring"Videos"Heading of the videos band.
reviewsTitlestring"Client Reviews"Heading of the reviews band.
reviewsIdstring"reviews"Id of the reviews band (target of the About "Client Reviews" buttons).
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).

AgentAbout

The Panda websites "About Moderna" block on its own (homepage About section): languages eyebrow, name, title, contacts, social links and the bio (cut at 800 characters with View Profile / Client Reviews buttons in section, full in hero) beside the portrait or a YouTube play overlay; background from the dashboard with contrasting text.

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

PropTypeDefaultDescription
person*object—Name, title, email, phones, links, biography, portraits, spoken languages.
sectionobject—Background color, image (removed → text full width), fit, side and YouTube video.
layout"section" | "hero""section"Homepage section (bio cut + buttons) or profile hero (full bio).
profileHrefstring"/profile"View Profile link.
reviewsHrefstring"/profile#reviews"Client Reviews link.
bioLengthnumber800section: characters of the bio before "...".
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).

Hooks

useAgentProfile()inside PandaProvider

useAgentProfile({ data }) → { kind: "owner" | "member", member, name, title, biography (paragraphs), headshot, image, spokenLanguages, email, emailHref, phones, phoneHref, socials: { network, href }[] (facebook, instagram, twitter, linkedin, youtube, tiktok, whatsapp, website; ready links), quote (owner only), section (About hero background / image / video settings), listings (the MLS grid data: draw it with <ListingGridRoot data={listings}>), status ("active" | "sold"), inventory: { sourceMenu?: { label, options: { href, label, selected }[] }, tabs: { href, label, selected }[], pagination?: { prevHref, nextHref, prevLabel, nextLabel } } | null (owner: the Personal / Office menu, Active / Sold tabs and Prev / Next links of the website, in the site's language; null on a member page), available, hasOffice (render <div id="office-listings" /> before the grid: the Office tabs jump to it), videos, testimonials, teammates } — data comes from fetchAgentProfile (member: <slug> for /team/<slug>; null → notFound()). Never rebuild the inventory links or read ?listings= yourself: the inventory links already follow the website's rules. AgentProfile stays the ready-made page. 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.

fetchAgentProfile()widget loader

The owner's /profile (/v1/site profile + About settings, the MLS inventory page picked by searchParams, /v1/videos, /v1/testimonials) or, with member, a team member (/v1/team/:slug + up to 500 listings of the ?listings= status, person.slug / person.group, inventory.available = which of Active / Sold have listings so a page can render its own tabs, and teammates = the other members from /v1/team). Null when the site / member is missing or the team page is off. Returns AgentProfileData | null → data