# Listing Detail Page

> A React listing detail page for a classifieds marketplace: photo gallery, price card, seller, map, contact actions and similar listings.

- Type: React block (Blocks)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.2
- Install: `npx wingo-ui@latest add listing-detail-page`
- Docs: https://wingo-ui.com/components/listing-detail-page

## Interaction

Swipe or arrow through the photos (thumbnails under the stage or beside it, a counter on phones); hover zooms with a mouse, a click, tap or Enter opens the full screen viewer and Escape closes it. The heart on the photo and Save in the price card save the listing together, at once; a rejected onFavoriteChange rolls both back with a toast. The share button uses the system share sheet where there is one, otherwise it copies the link (Link copied). Send message opens a dialog (a bottom sheet on phones) with the listing, quick replies as tags that fill the text, and the message field: Send or Cmd/Ctrl+Enter sends, an empty message shows its error, a failed send shows an alert and keeps the text; success turns the dialog into Message sent with Go to the conversation. Show phone shows the masked number, fetches the full one through onRevealPhone and fades it in as a tel: link (announced); the sticky bar's Call reveals and dials in one tap. Read more expands the description (clamped at 8 lines); URLs in it stay plain text. The map is read only with the approximate area; on touch a tap opens it full height. Report this listing opens a dialog with the reasons as radio cards and optional details. Similar listings are a swipeable rail with arrows in its header. Below 1024px the title, price and contact buttons sit under the photos, the seller card follows the description, and once those buttons scroll away a bar with the price, Call and Message stays at the bottom. Reserved shows a warning banner; sold shows a neutral banner, disables the contact buttons and puts the similar listings first. Sections rise in once on mount; reduced motion skips it.

## Installation

```bash
npx wingo-ui@latest add listing-detail-page
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/listing-detail-page` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/blocks/listing-detail-page.tsx`
- `components/blocks/listing-detail-page-data.ts`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Alert](https://wingo-ui.com/components/alert) ([Markdown](https://wingo-ui.com/components/alert.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Breadcrumb](https://wingo-ui.com/components/breadcrumb) ([Markdown](https://wingo-ui.com/components/breadcrumb.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Carousel](https://wingo-ui.com/components/carousel) ([Markdown](https://wingo-ui.com/components/carousel.md))
- [Collapsible](https://wingo-ui.com/components/collapsible) ([Markdown](https://wingo-ui.com/components/collapsible.md))
- [Description List](https://wingo-ui.com/components/description-list) ([Markdown](https://wingo-ui.com/components/description-list.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Favorite button](https://wingo-ui.com/components/favorite-button) ([Markdown](https://wingo-ui.com/components/favorite-button.md))
- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Listing card](https://wingo-ui.com/components/listing-card) ([Markdown](https://wingo-ui.com/components/listing-card.md))
- [Location Map](https://wingo-ui.com/components/location-map) ([Markdown](https://wingo-ui.com/components/location-map.md))
- [Price](https://wingo-ui.com/components/price) ([Markdown](https://wingo-ui.com/components/price.md))
- [Product gallery](https://wingo-ui.com/components/product-gallery) ([Markdown](https://wingo-ui.com/components/product-gallery.md))
- [Radio Group](https://wingo-ui.com/components/radio-group) ([Markdown](https://wingo-ui.com/components/radio-group.md))
- [Seller card](https://wingo-ui.com/components/seller-card) ([Markdown](https://wingo-ui.com/components/seller-card.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Sticky CTA Bar](https://wingo-ui.com/components/sticky-cta-bar) ([Markdown](https://wingo-ui.com/components/sticky-cta-bar.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Toast](https://wingo-ui.com/components/toast) ([Markdown](https://wingo-ui.com/components/toast.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Romanian locations](https://wingo-ui.com/components/ro-locations) ([Markdown](https://wingo-ui.com/components/ro-locations.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
// app/(market)/listing/[slug]/page.tsx (the shell lives in app/(market)/layout.tsx; mount <Toaster /> once there)
"use client"

import { useRouter } from "next/navigation"
import { ListingDetailPage } from "@/components/blocks/listing-detail-page"

export default function ListingPage() {
  const router = useRouter()
  return (
    <ListingDetailPage
      onMessage={(text) => fetch("/api/messages", { method: "POST", body: JSON.stringify({ listing: "l01", text }) })}
      onRevealPhone={() => fetch("/api/listings/l01/phone").then((response) => response.text())}
      onFavoriteChange={(favorite) => fetch("/api/favorites/l01", { method: favorite ? "PUT" : "DELETE" })}
      onOpenConversation={() => router.push("/messages?listing=l01")}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `listing?` | `ListingDetail` |  | The listing (LISTING_DETAIL_SAMPLE.listing: l01, Trek FX 2 city bike, $280 negotiable, Portland Pearl District). ListingData without attributes plus images (ProductGalleryImage[]), description (plain text, blank lines split paragraphs), attributes (DescriptionListItem[]), views, favorites, adId, place { city, area, point, radiusKm }, courier { name, price } and the breadcrumb path. |
| `seller?` | `SellerData` |  | The seller card (LISTING_DETAIL_SAMPLE.seller: Daniel Reed, 4.8 from 126 reviews, replies within an hour). Its name labels the contact buttons. |
| `sellerListings?` | `ListingData[]` |  | The seller's other listings in the seller card, up to three (LISTING_DETAIL_SAMPLE.sellerListings). |
| `similar?` | `ListingData[]` |  | The similar listings rail (LISTING_DETAIL_SAMPLE.similar, 8 listings around Portland). An empty array hides the section. |
| `favorite?` | `boolean` |  | Saved to favorites, controlled; pair with onFavoriteChange. |
| `defaultFavorite?` | `boolean` | `false` | Saved to favorites at first, uncontrolled. |
| `onFavoriteChange?` | `(favorite: boolean) => (void \| Promise<unknown>)` |  | A heart was toggled. The change shows at once; a rejected promise rolls it back and shows a toast. |
| `onMessage?` | `(text: string) => (void \| Promise<unknown>)` |  | Sends the dialog's message. A promise drives the Send spinner; a rejection shows its message in the dialog. Default: a pretend 900 ms request. |
| `onRevealPhone?` | `() => (string \| Promise<string>)` |  | Fetches the full number when Show phone or Call is pressed. Default: resolves "(503) 555-0142" after 500 ms. |
| `onCall?` | `(phone: string) => void` |  | Replaces the tel: link once the number is shown (the demo shows a toast). |
| `onShare?` | `() => void` |  | The share buttons. Default: navigator.share where available, else copies the listing link and shows Link copied. |
| `onReport?` | `(report: ListingReport) => (void \| Promise<unknown>)` |  | Sends the report dialog (ListingReport = { reason, details }). A rejection shows an alert in the dialog. Default: a pretend 900 ms request, then a thank you toast. |
| `onOpenConversation?` | `() => void` |  | Go to the conversation after a sent message. Default: navigates to /messages?listing=<id> through the link provider. |
| `status?` | `"active" \| "reserved" \| "sold"` | `"active"` | reserved: a warning banner, contact still allowed. sold: a neutral banner, contact disabled, grey photos and similar ads first. |
| `galleryThumbnails?` | `"start" \| "bottom" \| "none"` | `"bottom"` | Where the gallery's thumbnails sit on wide screens; phones swipe with a counter. |
| `showMap?` | `boolean` | `true` | The Location section with the map. |
| `showSafety?` | `boolean` | `true` | The Stay safe note under the seller. |
| `stickyBar?` | `boolean` | `true` | Below 1024px, the bottom bar with the price, Call and Message once the contact buttons scroll away. |
| `defaultMessageOpen?` | `boolean` | `false` | Opens the message dialog on mount. |
| `labels?` | `Partial<ListingDetailPageLabels>` |  | Every visible string (DEFAULT_LISTING_DETAIL_PAGE_LABELS, English), counted ones as functions; nested gallery, lightbox, price, favorite, listingCard, carousel, sellerCard, map, stickyBar and breadcrumbLabels for the composed items. LISTING_DETAIL_PAGE_LABELS_EN is the same set under its older name. |
| `locale?` | `string` | `"en-US"` | Intl locale for prices, counts and dates. |
| `currency?` | `string` | `"USD"` | The currency of amounts without their own (the demo converts the sample to EUR). |
| `now?` | `Date \| number` |  | The clock for Today at 2:32 PM. Default: useNow() (the date while hydrating); the demo passes Sep 24, 2026, 4:40 PM. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Send message, the sticky bar, the map pin, the quick replies and the hearts (neutral hearts are red). |
| `radius?` | `"sm" \| "md" \| "lg"` | `"md"` | The surfaces: gallery, price card, seller card, map and listing cards 12 / 16 / 24px. |
| `loading?` | `boolean` | `false` | The gallery, the texts, the price card, the seller and the similar ads as skeletons of their real shape. |
| `animateIn?` | `boolean` | `true` | Sections rise in once when the page mounts on the client (never over server HTML, never with reduced motion). |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The router link for the breadcrumb, the seller and the listing cards. Default: the provider's (the shell's), then <a>. |
| `className?` | `string` |  | Classes for the root. |
| `classNames?` | `Partial<Record<"breadcrumb" \| "banner" \| "gallery" \| "summary" \| "attributes" \| "description" \| "location" \| "report" \| "aside" \| "priceCard" \| "seller" \| "safety" \| "similar" \| "stickyBar", string>>` |  | Classes for the sections. |

## Parts

### DEFAULT_LISTING_DETAIL_PAGE_LABELS (constant)

The English strings for every visible label, the default of labels. Type: ListingDetailPageLabels.

### LISTING_DETAIL_PAGE_LABELS_EN (constant)

The same English strings under their older name, kept for existing imports.

### LISTING_DETAIL_SAMPLE (constant)

The demo data in USD: the Trek FX 2 city bike listing, its seller, three seller listings and eight similar ones.

### LISTING_DETAIL_SAMPLE_EN (constant)

The same sample priced in EUR, converted at LISTING_DETAIL_USD_PER_EUR (the currency example).

### LISTING_DETAIL_APARTMENT_SAMPLE (constant)

A second sample: a 2-bedroom apartment in Portland listed by a real estate agency.

### createListingDetailSample (function)

Builds the bike sample: ({ currency?: "USD" | "EUR" }) => ListingDetailSample.

### createListingDetailApartmentSample (function)

Builds the apartment sample in USD or EUR, same options as createListingDetailSample.

### LISTING_DETAIL_NOW (constant)

The demo clock (Sep 24, 2026, 4:40 PM) as a timestamp, passed as now so relative dates stay stable.

### LISTING_DETAIL_USD_PER_EUR (constant)

The rounded dollars per euro rate (1.09) used to convert the EUR sample.

### ListingDetailPageProps (type)

Props of ListingDetailPage, including the div attributes of the root.

### ListingDetail (type)

The listing shape: ListingData plus images, description, attributes, views, place, courier and breadcrumb.

### ListingReport (type)

What onReport receives: { reason: string; details: string }.

## Examples

- **Message dialog**: Quick replies fill the message; send it to see the Message sent view.
- **Reserved**: A warning banner; messaging and calling still work.
- **Sold**: A neutral banner, contact disabled and similar listings first.
- **Apartment from an agency**: Greenleaf Realty as a company seller, the price per sq ft, a side thumbnail rail and brand actions.
- **Loading**: Gallery, text, price card and seller as skeletons of their real shape.
- **Euro prices**: en-GB formatting and the same listing priced in euro.

## See also

- [Marketplace Shell](https://wingo-ui.com/components/marketplace-shell) ([Markdown](https://wingo-ui.com/components/marketplace-shell.md))
- [Marketplace Home Page](https://wingo-ui.com/components/marketplace-home-page) ([Markdown](https://wingo-ui.com/components/marketplace-home-page.md))
- [Marketplace Search Page](https://wingo-ui.com/components/marketplace-search-page) ([Markdown](https://wingo-ui.com/components/marketplace-search-page.md))
- [Product gallery](https://wingo-ui.com/components/product-gallery) ([Markdown](https://wingo-ui.com/components/product-gallery.md))
- [Seller card](https://wingo-ui.com/components/seller-card) ([Markdown](https://wingo-ui.com/components/seller-card.md))
- [Listing card](https://wingo-ui.com/components/listing-card) ([Markdown](https://wingo-ui.com/components/listing-card.md))

---

Canonical: https://wingo-ui.com/components/listing-detail-page
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
