01.software Docs

Product listing grid

Render a product listing grid from listing groups and SDK card helpers.

Product listing grid

Render a product listing grid using createQueryHooks(client).useProductListingGroupsQuery() and the buildProductListingCard() SDK helper.

'use client'

import { useMemo } from 'react'
import {
  buildProductListingCard,
  type ProductListingCard,
} from '@01.software/sdk'
import { createQueryHooks } from '@01.software/sdk/query'
import { useClient } from '@/lib/sdk'

function getMediaUrl(media: ProductListingCard['primaryImage']) {
  return media && typeof media === 'object' && 'url' in media
    ? (media.url ?? null)
    : null
}

export function ProductGrid({ productIds }: { productIds: string[] }) {
  const client = useClient()
  const query = createQueryHooks(client)
  const { data } = query.useProductListingGroupsQuery({
    options: {
      limit: productIds.length || 1,
      where: { id: { in: productIds } },
    },
  })

  const cards: ProductListingCard[] = useMemo(
    () => data?.docs.map((item) => buildProductListingCard(item)) ?? [],
    [data],
  )

  return (
    <ul>
      {cards.map((card) => {
        const imageUrl = getMediaUrl(card.primaryImage)

        return (
          <li key={card.id}>
            <a href={card.href}>
              {imageUrl ? <img src={imageUrl} alt={card.title} /> : null}
              <h3>{card.title}</h3>
              <p>
                {card.priceRange.isPriceRange
                  ? `${card.priceRange.minPrice}–${card.priceRange.maxPrice}`
                  : card.priceRange.minPrice}
              </p>
            </a>
            {card.swatches.length > 0 && (
              <ul aria-label="Available colors">
                {card.swatches.map((swatch) => (
                  <li key={swatch.optionValueId}>
                    <a
                      href={swatch.href}
                      aria-disabled={!swatch.availableForSale}
                      style={{ background: swatch.swatchColor ?? undefined }}
                    >
                      {swatch.label}
                    </a>
                  </li>
                ))}
              </ul>
            )}
          </li>
        )
      })}
    </ul>
  )
}

The card model is intentionally minimal - it derives only what every listing card needs. For richer data (brand, categories, tags) read item.product directly. Each swatch href is a hint-only URL (?opt.<optionId>=<valueId>); the detail page resolves it through resolveProductSelection(detail, { search }).

Single-group products emit swatches: []. Storefronts that want a chip even for a single colorway can fall back to item.groups themselves.

On this page