# Collection page

This example combines collection stats, an item grid and recent activity. Adapt it to your application's loading, error and refresh behavior. GraveMarket's public reads do not require a credential.

> **TIP: Integration review**
With the [MCP server](https://docs.deads.io/tools/mcp.md) installed, ask your agent to *"review this file for
Solana Deads integration mistakes"*.

## Install

```bash
npm install @solanadeads/gravemarket
```

```ts
// lib/gravemarket.ts
import { GraveyardClient } from '@solanadeads/gravemarket';
export const gmk = new GraveyardClient();
```

The public client works without a key. See [optional API keys](https://docs.deads.io/sdk/gravemarket.md#api-keys) for authenticated use.

## The page

```tsx
import { useEffect, useState } from 'react';
import { gmk } from './lib/gravemarket';

// listing_price is NULLABLE and null means NOT LISTED — not free, not zero.
function priceLabel(item) {
  return item.listing_price == null
    ? 'Not listed'
    : `${item.listing_price} ${item.listing_currency}`;
}

// Include the chain and token ID for EVM collections.
const itemKey = (i) => `${i.chain}:${i.token_address}:${i.token_id ?? ''}`;

export function CollectionPage({ slug }: { slug: string }) {
  const [collection, setCollection] = useState(null);
  const [stats, setStats] = useState(null);
  const [items, setItems] = useState([]);
  const [activity, setActivity] = useState([]);
  const [cursor, setCursor] = useState(undefined);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    gmk.collections.get(slug).then(setCollection);
    gmk.collections.stats(slug).then(setStats);
    gmk.collections.activity(slug, { limit: 10 }).then((r) => setActivity(r.data));
    loadMore(true);
  }, [slug]);

  async function loadMore(reset = false) {
    setLoading(true);
    const page = await gmk.collections.items(slug, {
      limit: 24,
      ...(reset ? {} : { cursor }),
    });
    setItems((prev) => (reset ? page.data : [...prev, ...page.data]));
    setCursor(page.cursor);          // null/undefined when exhausted
    setLoading(false);
  }

  if (!collection) return <p>Loading…</p>;

  return (
    
      <header>
        <h1>{collection.name}</h1>
        {/* Floor currency varies BY CHAIN — always render it alongside the number. */}
        <p>
          Floor: {collection.floor_price ?? '—'} {collection.floor_currency ?? ''}
          {' · '}{collection.chain}
        </p>
        {stats?.wash_trade_count_7d > 0 && (
          <p>
            Volume figures include {stats.wash_trade_count_7d} suspected wash
            trade(s) in the last 7 days.
          </p>
        )}
      </header>

      <ul>
        {items.map((item) => (
          <li key={itemKey(item)}>
            <img src={item.thumbnail_url ?? item.image_url} alt={item.name ?? ''} />
            <span>{item.name ?? item.token_address.slice(0, 8)}</span>
            <span>{priceLabel(item)}</span>
            {item.rarity_rank && <span>Rank #{item.rarity_rank}</span>}
          </li>
        ))}
      </ul>

      {/* `cursor` present means more pages. Do not compute totals from what is loaded. */}
      {cursor && (
        <button onClick={() => loadMore()} disabled={loading}>
          {loading ? 'Loading…' : 'Load more'}
        </button>
      )}

      <section>
        <h2>Recent activity</h2>
        {activity.map((e, i) => (
          {e.event_type} — {e.price ?? '—'} {e.currency ?? ''}
        ))}
      </section>
    
  );
}
```

## Adapting the example {#what-this-deliberately-does-not-do}

Add request cancellation or stale-response guards when the selected collection changes, and display request failures with a retry action. This example fetches recent activity once; poll or refresh it if your UI needs continuing updates.

Use the collection record for total supply, preserve currency labels and show “Not listed” for a missing listing price. An item key must account for the chain and, on EVM, the token ID.

## Draining every page

For a full activity export, follow all pages. The SDK provides async generators:

```ts
const events = [];
for await (const batch of gmk.collections.activityAll(slug)) events.push(...batch);
```

For holders, follow the cursor explicitly:

```ts
const holders = [];
let cursor;
do {
  const page = await gmk.collections.holders(slug, { limit: 100, cursor });
  holders.push(...page.holders);
  cursor = page.cursor;
} while (cursor);
```

The holders response uses `holders` for its list field. Check each endpoint's response type when adding pagination.

## Next

- [The marketplace model](https://docs.deads.io/concepts/marketplace-model.md) — identifiers, prices and pagination
- [GraveMarket v1 API](https://docs.deads.io/api/gravemarket-v1.md) — request builders and response examples
- [React mint panel](https://docs.deads.io/recipes/react.md) — the GraveMint side, if you also render a mint

---
Source: https://docs.deads.io/recipes/collection-page
Markdown: https://docs.deads.io/recipes/collection-page.md
