Account and saved searches

The visitor account area (collections, saved searches, likes and the visitor's own profile) in its own page (sections in a left sidebar), a dialog-sidebar or the drawer.

Root + partsClient component@pandaidx/widgets 1.14.1

Anatomy

Every piece of Account and saved searches 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
<PandaAccountRoot>Holds the data and state its parts and hooks read
<AccountTabs />

Sections nav with counts: orientation vertical (left sidebar) | horizontal, items order, labels, icons, counts.

<AccountCollections />

Collections grid (columns, card, renderCard, title, emptyLabel); a card opens the collection in place or on its page…

<AccountCollectionView />

Listings of the open collection with a back button; a card opens the listing detail (open.listing).

<AccountSavedSearches />

Saved searches grid; a card opens its results on the search page.

<AccountLikes />

Liked listings grid with a remove button per card.

<AccountProfile />

The visitor's profile editor (PandaAccountProfile) on the profile section.

<AccountCard />

The built-in card (item, variant default | compact | row) for your own grids; restyle with the account.card part.

</PandaAccountRoot>
<PandaAccountMenu>Holds the data and state its parts and hooks read
<AccountMenuItem />

One entry: id (collections | saved | likes | account | dashboard | logout), optional icon element and label; order =…

<AccountMenuHeader />

Avatar, name and email of the visitor (the websites account header in a dropdown); the avatar opens My Account.

<AccountMenuSeparator />

Line between groups of entries.

</PandaAccountMenu>
<PandaAccountProfile>Holds the data and state its parts and hooks read
<ProfilePhoto />

Avatar (initials without a photo) with Upload photo (JPEG / PNG / WebP, 5 MB; allowUpload={false} hides it; it hides…

<ProfileFields />

Name, email (read only) and phone numbers; fields sets which and their order.

<ProfileSave />

Save button (label) with the saving / saved / error state.

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

PandaAccountRoot
// app/account/page.tsx
import { PandaAccountRoot } from "@pandaidx/widgets/client"
export default async function AccountPage({ searchParams }: { searchParams: Promise<{ tab?: string }> }) {
const { tab } = await searchParams
return <PandaAccountRoot shell="page" defaultTab={tab} columns={3} />
}

Examples

Account page with a left sidebar and 3-column collections

PandaAccountRoot
// app/layout.tsx: <PandaProvider … open={{ account: { mode: "page", href: "/account" } }}>
// app/account/page.tsx (Server Component)
import { PandaAccountRoot, AccountTabs, AccountCollections, AccountCollectionView, AccountSavedSearches, AccountLikes, AccountProfile } from "@pandaidx/widgets/client"
export default async function AccountPage({ searchParams }: { searchParams: Promise<{ tab?: string }> }) {
const { tab } = await searchParams
return (
<main className="mx-auto max-w-7xl px-6 py-16">
<PandaAccountRoot shell="page" defaultTab={tab} columns={3}>
<div className="grid gap-10 md:grid-cols-[220px_1fr]">
<AccountTabs orientation="vertical" />
<div>
<AccountCollections />
<AccountCollectionView />
<AccountSavedSearches card="row" columns={1} />
<AccountLikes />
<AccountProfile />
</div>
</div>
</PandaAccountRoot>
</main>
)
}

Full websites area as a page (search, map, sort) with 3 columns

PandaAccountRoot
<PandaAccountRoot shell="page" defaultTab={tab} columns={3} />

Saved searches inside Your Collections (one grid)

PandaAccountRoot
<PandaAccountRoot shell="page" defaultTab={tab} columns={3} card="compact" mergeSaved />

Sidebar dialog from your own button

PandaAccountRoot
<PandaAccountRoot shell="dialog-sidebar" trigger={<button type="button">My account</button>} card="compact" columns={3} />

Inline account menu with icons (account sidebar or mobile menu)

PandaAccountMenu
<PandaAccountMenu mode="inline" icons header items={["likes", "collections", "saved", "account", "logout"]} labels={{ likes: "Favorites" }} />

Dropdown with your own icons and order

PandaAccountMenu
<PandaAccountMenu trigger={<button type="button">Account</button>}>
<AccountMenuHeader />
<AccountMenuItem id="likes" icon={<Star />} label="Favorites" />
<AccountMenuItem id="collections" icon={<Folder />} />
<AccountMenuSeparator />
<AccountMenuItem id="logout" />
</PandaAccountMenu>

Header button with icons and another order

PandaAccountMenu
<PandaAuthButton variant="avatar" icons items={["collections", "likes", "saved", "account", "logout"]} />

Two-column profile

PandaAccountProfile
<PandaAccountProfile>
<div className="grid gap-8 md:grid-cols-[160px_1fr]">
<ProfilePhoto size={128} />
<div className="grid gap-6"><ProfileFields fields={["name", "phones", "email"]} /><ProfileSave label="Update profile" /></div>
</div>
</PandaAccountProfile>

Custom collection picker in your own popover

CollectionPicker
<Popover>
<PopoverTrigger asChild><button type="button">Save to…</button></PopoverTrigger>
<PopoverContent className="w-80">
<CollectionPicker listing={listing} layout="grid" labels={{ title: "Save to a board", create: "New board" }} classNames={{ row: "rounded-none", image: "rounded-none" }} />
</PopoverContent>
</Popover>

API reference

PandaAccountRoot

The visitor account area (collections, saved searches, likes and the visitor's own profile) in its own page (sections in a left sidebar), a dialog-sidebar or the drawer. Without children it is the full websites area (search, sort, map, share, edit/delete) with columns and card; with children you lay out the parts (AccountTabs, AccountCollections, AccountCollectionView, AccountSavedSearches, AccountLikes, AccountProfile). AI-built pages are Server Components: use the serializable props (shell, defaultTab, columns, card); renderCard only from a client component. Signed out: a sign-in prompt. Point PandaProvider open.account at the page so every menu and openDrawer goes there.

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

PropTypeDefaultDescription
shell"page" | "dialog-sidebar" | "drawer""page"page: in the page with a left sidebar; dialog-sidebar: a dialog with the sections on the left; drawer: the side / bottom drawer.
defaultTab"collections" | "saved" | "likes" | "profile""collections"Section on load: pass the page's ?tab= (what open.account page mode sends).
openboolean—dialog-sidebar / drawer: controlled open state (client components).
onOpenChangefunction—dialog-sidebar / drawer: open state changes (client components only).
triggernode—dialog-sidebar / drawer: the element that opens it (signed out it opens sign-in).
columns1 | 2 | 3 | 4—Card columns (phones 1, tablets up to 2). Unset: the auto-fill grid (full area) or 3 (parts).
card"default" | "compact" | "row""default"Built-in card: default (the websites cards in the full area; photo on top in the parts), compact (smaller photo) or row (list row with a thumbnail; pair with columns={1}).
renderCardfunction—(item: { kind, title, subtitle, image, document, open(), remove? }) => ReactNode: your own card. Client components only; prefer card + columns.
profilebooleantrueAdds the My Account profile section for visitors (the site owner manages theirs in the dashboard).
mergeSavedbooleanfalseSaved searches inside "Your Collections": one grid (collections, then saved searches with a "Saved search" badge, part account.card-badge) and no separate Saved Searches section. Without children it lays out the built-in parts (sections, collections, likes, profile) instead of the drawer.
classNamestring—Classes of the area (page) or the dialog / drawer panel.
childrennode—Your layout of the account parts; default: the full area.

PandaAccountMenu

The signed-in visitor's account menu: My Collections, Saved Searches, Likes (opened the PandaProvider open.account way), My Account, Dashboard (site owner only) and Sign Out, translated by the widget dictionary. dropdown behind the avatar (or your trigger), or inline as a list (an account sidebar, a mobile menu, the footer). Reorder with items (serializable), add icons with icons, or compose AccountMenuHeader / AccountMenuItem / AccountMenuSeparator in any order with your own icons and labels. Signed out it renders nothing: pair it with PandaAuthButton (which already shows this menu signed in).

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

PropTypeDefaultDescription
mode"dropdown" | "inline""dropdown"dropdown: behind a trigger; inline: the entries as a vertical list in the page.
items"collections" | "saved" | "likes" | "account" | "dashboard" | "logout"["collections","saved","likes","account","logout"]Entries in order when there are no children; unknown ids are ignored; dashboard only shows to the site owner.
iconsbooleanfalseBuilt-in icon before each entry (folder, bookmark, heart, user, dashboard, log out).
labelsobject—Copy overrides by entry id: { likes: "Favorites", logout: "Log out" }.
headerbooleanfalseAvatar, name and email above the entries (no children).
triggernode—dropdown: the element that opens it (default: the visitor avatar).
align"start" | "center" | "end""end"dropdown: menu alignment to the trigger.
classNamestring—Classes of the menu panel (dropdown) or the list (inline).
itemClassNamestring—Classes appended to every entry.
onActionfunction—Runs after the visitor picks an entry (client components only).
childrennode—Your own entries (AccountMenuHeader, AccountMenuItem id, AccountMenuSeparator) in any order; replaces items.

PandaAccountProfile

The visitor's "My Account" on the site: photo (remove, or a photo URL with ProfilePhoto allowUrl), name and phone numbers, email read only, saved with the visitor session (PATCH /v1/account, the websites Edit Profile update: CRM activity and owner notification). Email and password never change here. Signed out it renders nothing. PandaProvider open.account opens it from My Account (dialog, the sidebar dialog's profile section, or ?tab=profile on the account page); the site owner always goes to the dashboard.

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

PropTypeDefaultDescription
labelsobject—Copy: name, email, phones, addPhone, removePhoto, photoUrl, save, saving, saved.
classNamestring—Classes of the form.
childrennode—Your layout of ProfilePhoto, ProfileFields, ProfileSave; default: the three stacked.

CollectionPicker

Add-to-collection picker for your own dialog, popover, sheet or page: the visitor's collections with a check on the ones holding the listing (click adds / removes), and Create Collection with the listing in it, over the same services as the websites dialog. layout list | grid, labels, classNames slots; renderRow from client components. Signed out it offers sign-up. The listing cards keep the websites Collections dialog.

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

PropTypeDefaultDescription
listing*object—The listing to add.
layout"list" | "grid""list"list: rows with a round photo; grid: photo tiles.
labelsobject—Copy: title, create, createPlaceholder, createSubmit, empty, error, signIn, countOne / countMany ({{n}}).
classNamesobject—Classes per slot: root, title, list, row, image, name, count, create.
renderRowfunction—({ collection: { _id, name, image, count, selected }, toggle }) => ReactNode: your own row (client components only).
onChangefunction—Runs after an add / remove / create (client components only).

Parts

<AccountTabs />

Sections nav with counts: orientation vertical (left sidebar) | horizontal, items order, labels, icons, counts.

<AccountCollections />

Collections grid (columns, card, renderCard, title, emptyLabel); a card opens the collection in place or on its page (open.collection).

<AccountCollectionView />

Listings of the open collection with a back button; a card opens the listing detail (open.listing).

<AccountSavedSearches />

Saved searches grid; a card opens its results on the search page.

<AccountLikes />

Liked listings grid with a remove button per card.

<AccountProfile />

The visitor's profile editor (PandaAccountProfile) on the profile section.

<AccountCard />

The built-in card (item, variant default | compact | row) for your own grids; restyle with the account.card part.

<AccountMenuItem />

One entry: id (collections | saved | likes | account | dashboard | logout), optional icon element and label; order = children order.

<AccountMenuHeader />

Avatar, name and email of the visitor (the websites account header in a dropdown); the avatar opens My Account.

<AccountMenuSeparator />

Line between groups of entries.

<ProfilePhoto />

Avatar (initials without a photo) with Upload photo (JPEG / PNG / WebP, 5 MB; allowUpload={false} hides it; it hides itself where the API has no upload storage) and Remove photo; allowUrl adds a photo URL field; size in px.

<ProfileFields />

Name, email (read only) and phone numbers; fields sets which and their order.

<ProfileSave />

Save button (label) with the saving / saved / error state.

Hooks

useVisitorLibrary()inside PandaAccountRoot

{ collections: { _id, name, slug, images, count }[], savedSearches: { _id, title, url, image, total }[], likes: { ListingId, address, image, ListPrice… }[], counts: { collections, saved, likes }, loading, signedIn, refresh(), deleteCollection(id), renameCollection(id, name), deleteSavedSearch(id), removeLike(listing), collectionListings(id) } — counters, cards or pages of your own (same cache as the drawer)

usePandaAuth()inside PandaAccountMenu

{ user, signedIn, openAuth(), openDrawer(tab?), logout() } — a menu fully of your own

useAccountProfile()inside PandaAccountProfile

{ profile: { name, phones, image }, email, setField(field, value), save() → { ok, message }, uploadPhoto(file) → { ok, message } (sets image; save after), uploading, reset(), saving, error, saved, dirty, signedIn } — a profile form of your own

useCollectionPicker()inside CollectionPicker

useCollectionPicker(listing, { enabled? }) → { collections: { _id, name, image, count, selected }[], loading, error, signedIn, toggle(collectionId), create(name), creating } — a picker of your own