Skip to main content

ProductInfo

Displays core product identification -- the product name and SKU. It can either render a pre-fetched Product object or fetch one internally via the Propeller SDK when given a productId and graphqlClient. The onProductLoaded callback lets sibling components (gallery, price, descriptions) hydrate from the same product data, making ProductInfo the ideal orchestrator for a product detail page.

Usage

The component fetches the product internally and notifies siblings via onProductLoaded:

import ProductInfo from '@/components/propeller/ProductInfo';
import { graphqlClient } from '@/lib/api';
import { config } from '@/data/config';
import { imageSearchFilters, imageVariantFiltersLarge } from '@/data/defaults';

const [product, setProduct] = useState<Product | null>(null);

<ProductInfo
user={authState.user}
productId={42}
graphqlClient={graphqlClient}
language="EN"
imageSearchFilters={imageSearchFilters}
imageVariantFilters={imageVariantFiltersLarge}
onProductLoaded={setProduct}
configuration={config}
/>

{/* Siblings consume the loaded product */}
<ProductPrice price={product?.price} />
<ProductGallery images={product?.media?.images?.items} />

Pre-fetched product (no SDK call)

When you already have the product data, pass it directly. The component renders immediately and still fires onProductLoaded:

<ProductInfo
user={authState.user}
product={existingProduct}
language="EN"
onProductLoaded={setProduct}
/>

Title only (hide SKU)

<ProductInfo
user={authState.user}
product={product}
showSku={false}
language="NL"
/>

SKU only (hide title)

<ProductInfo
user={authState.user}
product={product}
showTitle={false}
language="NL"
/>

With attribute-based labels

Display custom product attributes as image badges or text rows:

<ProductInfo
user={authState.user}
productId={42}
graphqlClient={graphqlClient}
configuration={config}
imageLabels={['new', 'sale']}
textLabels={['brand', 'color']}
/>

Full product detail page integration

This is the pattern used on the actual product page, where ProductInfo orchestrates data loading for the entire page:

const [product, setProduct] = useState<Product | null>(null);

<ProductInfo
user={state.user}
productId={productId}
graphqlClient={graphqlClient}
language={language}
imageSearchFilters={imageSearchFilters}
imageVariantFilters={imageVariantFiltersLarge}
onProductLoaded={setProduct}
configuration={config}
/>

<ProductPrice price={product?.price} includeTax={includeTax} />
<ProductBulkPrices bulkPrices={product?.bulkPrices || []} />
<ProductShortDescription product={product} language={language} />
<ItemStock inventory={product?.inventory} />
<AddToCart product={product} />

Configuration

Data Source

PropTypeDefaultDescription
userContact | Customer | null(required)The authenticated user. Used to build price calculation inputs (companyId, contactId, customerId).
productProductundefinedPre-fetched product object. When provided, the component skips internal fetching.
productIdnumberundefinedProduct ID to fetch when no product prop is given. Requires graphqlClient.
graphqlClientGraphQLClientundefinedInitialized Propeller SDK GraphQL client. Required when using productId.
configurationanyundefinedApp config object (from @/data/config). Provides fallback imageSearchFilters, imageVariantFiltersLarge, and productTrackAttributes.

Image & Price Filters

PropTypeDefaultDescription
imageSearchFiltersanyFalls back to configuration.imageSearchFiltersControls how many image items are returned. Example: { page: 1, offset: 20 }.
imageVariantFiltersanyFalls back to configuration.imageVariantFiltersLargeControls image size/format variants. Use imageVariantFiltersLarge from @/data/defaults.
taxZonestring'NL'Tax zone for price calculation.

Display Toggles

PropTypeDefaultDescription
showTitlebooleantrueShow the product name as an <h1>.
showSkubooleantrueShow the product SKU in a monospace label.

Locale & Styling

PropTypeDefaultDescription
languagestring'NL'Language code used to resolve localized product names from the names array.
classNamestring''Extra CSS class applied to the root <div>.

Labels

PropTypeDefaultDescription
imageLabelsstring[]undefinedAttribute codes to display as badge overlays on the product image. Resolved against product.attributes.items[].attributeDescription.code (or .name). Unmatched codes are silently omitted.
textLabelsstring[]undefinedAttribute codes to display as extra text rows below the product name. Resolved the same way as imageLabels.

Callbacks

PropTypeDescription
onProductLoaded(product: Product) => voidFired once product data is available -- immediately when product prop is supplied, or after the internal fetch completes. Use this to hydrate sibling components.

Behavior

Two data modes

  1. Pre-fetched (product prop provided): No SDK call is made. The component renders immediately and fires onProductLoaded with the supplied product.
  2. Self-contained (productId + graphqlClient provided): The component fetches the product via ProductService.getProduct(), shows a skeleton loader during the request, then renders and fires onProductLoaded.

Loading skeleton

While fetching (and no product prop is present), the component renders an animated pulse skeleton with two placeholder bars -- a narrow one for the SKU and a wider one for the title.

Language resolution

The product name is resolved by matching language against the product.names array (each entry is a LocalizedString with language and value). If no match is found, the first entry in the array is used as a fallback.

Re-fetch triggers

The internal fetch re-runs when any of these props change:

  • productId
  • product
  • language

User-aware pricing

The fetch includes priceCalculateProductInput and userBulkPriceProductInput derived from the user prop. For B2B users (Contact), companyId and contactId are included. For B2C users (Customer), customerId is included. This ensures server-side price calculation respects customer-specific pricing tiers.

Error handling

Fetch errors are caught silently -- the loading state is cleared but no error UI is shown. The component simply remains empty until valid data is available.


SDK Services

ProductInfo uses ProductService.getProduct() from propeller-sdk-v2 when fetching internally.

Product fields read by the component

FieldUsage
product.namesArray of LocalizedString. The component finds the entry matching language and falls back to the first entry. Rendered as the <h1> title.
product.skuRendered as the SKU label.

Product fields passed through via onProductLoaded

The full Product object is forwarded to the callback, enabling sibling components to access:

FieldConsuming component
product.priceProductPrice
product.bulkPricesProductBulkPrices
product.media.images.itemsProductGallery
product.inventoryItemStock
product.categoryPathBreadcrumbs
product.slugsURL slug resolution
product.attributesLabel resolution (imageLabels, textLabels)

Query variables built internally

When fetching by productId, the component constructs the following query variables:

  • productId -- from props
  • language -- from props, defaults to 'NL'
  • imageSearchFilters -- from props or configuration.imageSearchFilters
  • imageVariantFilters -- from props or configuration.imageVariantFiltersLarge
  • priceCalculateProductInput -- built from user (companyId, contactId, customerId) and taxZone
  • userBulkPriceProductInput -- same structure as price input
  • attributeResultSearchInput -- conditionally included when configuration.productTrackAttributes is a non-empty array

GraphQL Query Example

The internal fetch calls ProductService.getProduct(), which executes a query equivalent to:

query GetProduct(
$productId: Int!
$language: String
$imageSearchFilters: ImageSearchInput
$imageVariantFilters: ImageVariantFilterInput
$priceCalculateProductInput: PriceCalculateProductInput
$userBulkPriceProductInput: UserBulkPriceProductInput
$attributeResultSearchInput: AttributeResultSearchInput
) {
product(productId: $productId, language: $language) {
productId
sku
names {
language
value
}
slugs {
language
value
}
price(input: $priceCalculateProductInput) {
net
gross
}
bulkPrices(input: $userBulkPriceProductInput) {
from
price {
net
gross
}
}
media {
images(input: $imageSearchFilters) {
items {
imageVariants(input: $imageVariantFilters) {
url
}
}
}
}
inventory {
totalQuantity
}
categoryPath {
categoryId
names {
language
value
}
slugs {
language
value
}
}
attributes(input: $attributeResultSearchInput) {
items {
attributeDescription {
code
name
}
textValue
}
}
}
}

Variables for a B2B (Contact) user:

{
"productId": 42,
"language": "EN",
"imageSearchFilters": { "page": 1, "offset": 20 },
"imageVariantFilters": { "transformations": [] },
"priceCalculateProductInput": {
"taxZone": "NL",
"companyId": 100,
"contactId": 200
},
"userBulkPriceProductInput": {
"taxZone": "NL",
"companyId": 100,
"contactId": 200
}
}