Przejdź do głównej zawartości

Strona produktu z wariantami

Framework: React / Next.js
Pobieranie danych jest przenośne — w blokach z przykładami zakładka Raw pokazuje czysty fetch działający w dowolnym frameworku.
UI (komponenty z hookami i JSX) jest specyficzne dla React / Next.js. W innym frameworku weź zapytanie z zakładki Raw i napisz własny widok.

Co zbudujesz​

Stronę produktu (/products/[handle]): wybór wariantu (rozmiar / kolor), cena z przekreśleniem ceny promocyjnej, status dostępności i przycisk „Dodaj do koszyka".

Trzy kroki:

const { data } = useProduct(handle);          // 1. pobierz produkt
const variants = data.product.variants.nodes; // 2. connection → tablica wariantów
await addToCart(variant.id, 1); // 3. dodaj wariant (backend resolwuje resztę)

Wymagania​

Krok 1 — Pobierz produkt po handle​

handle to slug z adresu URL (/products/niebieska-koszulka). Wybierz kontekst renderowania:

import { fetchProduct } from '@/lib/graphql/server';

const data = await fetchProduct({ id: '…', handle: '…' });

Krok 2 — Rozpakuj listy (Relay Connection, nie tablica)​

To najczęstsza pułapka: useProduct zwraca surowy wynik zapytania (bez normalizacji), a variants i images to Relay Connections, nie tablice. Wyciągnij listy przez .nodes (skrót schematu, równoważny edges.map(e => e.node)):

const variants = data.product.variants.nodes; // ProductVariant[]
const images = data.product.images.nodes; // Image[]
Connection ≠ tablica

data.product.variants nie jest tablicą — to Relay Connection, więc indeksowanie ([0]) zwróci undefined. Zawsze rozpakuj przez .nodes (lub .edges). Inaczej niż useProducts (lista), useProduct nie normalizuje odpowiedzi za Ciebie.

Krok 3 — Komponent wariantu (React)​

Ten komponent jest weryfikowany typami przeciw @doswiftly/storefront-sdk — błędny dostęp do pola wariantu lub zła sygnatura dodania do koszyka nie przejdą weryfikacji, dlatego ten przykład nie może zdryfować od API:

VariantPicker.tsx
'use client';

import { useState } from 'react';
import type { ProductVariant, CartLineInput } from '@doswiftly/storefront-sdk';

// Picker wariantu produktu: wybór wariantu, cena z przekreśleniem ceny promocyjnej,
// status dostępności i dodanie do koszyka.
export function VariantPicker({
variants,
onAddToCart,
}: {
variants: ProductVariant[];
onAddToCart: (line: CartLineInput) => void | Promise<void>;
}) {
const [selected, setSelected] = useState(0);
const variant = variants[selected];

return (
<div>
{/* Wybór wariantu — selectedOptions to [{ name, value }] */}
<div>
{variants.map((v, idx) => (
<button key={v.id} aria-pressed={idx === selected} onClick={() => setSelected(idx)}>
{v.selectedOptions.map((o) => o.value).join(' / ')}
</button>
))}
</div>

{/* Cena wariantu + przekreślona cena promocyjna (opcjonalna) */}
<div>
<strong>
{variant.price.amount} {variant.price.currencyCode}
</strong>
{variant.compareAtPrice ? (
<s>
{variant.compareAtPrice.amount} {variant.compareAtPrice.currencyCode}
</s>
) : null}
</div>

{/* Dostępność wariantu */}
<p>{variant.isAvailable ? 'Dostępny' : 'Niedostępny'}</p>

{/* Dodanie do koszyka — CartLineInput przyjmuje tylko variantId + quantity.
Tytuł, cenę i obraz backend resolwuje z wariantu po stronie serwera. */}
<button
disabled={!variant.isAvailable}
onClick={() => onAddToCart({ variantId: variant.id, quantity: 1 })}
>
Dodaj do koszyka
</button>
</div>
);
}

Zwróć uwagę: onAddToCart buduje CartLineInput z samym variantId i quantity. Tytuł, cenę i obraz backend resolwuje z wariantu po stronie serwera — front ich nie przekazuje.

Krok 4 — Podłącz koszyk​

Przekaż rozpakowane warianty do komponentu i podłącz onAddToCart do useCartActions. Sygnatura jest pozycyjna — addToCart(variantId, quantity?, attributeSelections?):

const { addToCart } = useCartActions();
const variants = data.product.variants.nodes;

<VariantPicker
variants={variants}
onAddToCart={(line) => addToCart(line.variantId, line.quantity)}
/>;

attributeSelections (trzeci, opcjonalny argument addToCart) służą do konfiguratora produktu — np. wybór komponentu z dopłatą. Orkiestracja koszyka (recovery, optymistyczne aktualizacje) — Koszyk.

Typy​

Renderowane ze schematu GraphQL — nigdy nie rozjeżdżają się z API:

Product​

Product - main catalog item

PoleTypOpis
attachments[ProductAttachment!]!Files publicly attached to the product — manuals, certificates, warranty cards. Material delivered after purchase does NOT appear here: it requires an entitlement and is reached through the order line item. Resolved lazily, so a product list that does not ask for this field pays no extra query.
attributes[EntityAttributeField!]!This product's stored custom-field values — merchant-managed metadata such as manufacturer, licence, material or EAN. Only fields the merchant marked visible are returned. Pass `namespace` to fetch a single group.
averageRatingFloatAverage rating (1-5)
brandBrandCanonical brand entity. Resolved via DataLoader (N+1 safe for list queries). Null when the product has no assigned brand.
categories[Category!]!All categories the product belongs to (M2M via the ProductCategory junction). Sorted by junction.sortOrder ASC. Empty list when the product is in no category.
compareAtPriceRangeProductPriceRangeCompare-at price range (Money pair). Null when no variant has a compareAtPrice.
compareAtPriceRangeWithConversionConvertedPriceRangeOpt-in: compare-at price range with conversion transparency.
configuratorFields[ConfiguratorField!]!Configurable fields shown on the product page for the shopper to fill in or choose (combines shared template fields and product-specific ones). Pass `{ filledBy: CUSTOMER }` to return only the fields a shopper can edit.
createdAtDateTime!Creation timestamp
descriptionStringProduct description. Returns ready-to-render HTML by default; pass `format: TEXT` for plain text or `format: JSON` for the structured document.
descriptionHtmlStringHTML product description (DEPRECATED — use `description(format: HTML)`).
featuredImageImageFeatured/primary image
handleString!URL-friendly handle (slug)
idID!Unique identifier
imagesImageConnection!Product images (Relay Connection)
isAvailableBoolean!Whether product is available for sale (any variant in stock)
isPurchasableBoolean!Whether the storefront should offer a direct purchase path (Add to cart / Buy now). Mirrors `visibility === PUBLIC`. BUNDLE_ONLY ("Komponent") products resolve to false — the PDP renders a Komponent banner explaining access is only via configurator.
options[ProductOption!]!Per-product option definitions (Color, Size, …) with their available values. Use these to build a variant picker without aggregating `selectedOptions` manually.
priceRangeProductPriceRangePrice range (Money pair). Default field — industry-standard schema. Null when the store restricts prices to signed-in customers — fetch via `variantPrices` after login.
priceRangeWithConversionConvertedPriceRangeOpt-in: price range with full conversion transparency (exchangeRate, baseCurrency, isConverted). Use for a currency-converter UI when the customer has a preferred currency different from the shop base.
priceReductionPriceReductionInfoStatutory reference of the price reduction shown on product cards — computed for the variant behind `priceRange.minVariantPrice`. Null when that variant is not reduced or the reference cannot be proven. Use it wherever a listing shows `compareAtPriceRange`.
primaryCategoryCategoryDefault category for breadcrumb/nav (= categories[0] by sortOrder). Null when the product is in no category.
recommendationsProductRecommendationsSimilar products recommendations
reviewCountIntNumber of reviews
seoSEOSEO metadata
stockTotalIntTotal stock across all variants
tags[String!]!Product tags
titleString!Product title
typeProductTypeEnum!Product type enum (PHYSICAL, DIGITAL, SERVICE, SUBSCRIPTION, GIFT_CARD)
updatedAtDateTime!When the product catalogue definition last changed. Covers the whole product, not just its main record: variants, images, options, categories, tags, attachments and configurator fields all move this timestamp. Stock movements caused by orders, cart reservations and page views deliberately do NOT — so this is a trustworthy `lastmod` for sitemaps and a reliable trigger for re-fetching cached product content.
variantsProductVariantConnection!Product variants (Relay Connection)
vendorStringLegacy vendor/brand name (free-text). Preferred: `brand` field (canonical Brand entity).
visibilityProductVisibility!Catalog visibility — PUBLIC | HIDDEN | BUNDLE_ONLY

ProductVariant​

ProductVariantobjectPełna referencja →

A purchasable unit of a product — one variant out of the product variants list, identified by its `id` and described by its options.

PoleTypOpis
attributes[EntityAttributeField!]!Variant custom field values (post-Opcja A unified custom fields).
availabilityVariantAvailabilityAvailability resolved by the platform: status, release window (pre-order), expected availability and handling time. Prefer this over `isAvailable` / `availableStock` when you need to say WHY and WHEN. Null only on the lightweight variant embedded in a cart line — read it from the product variant.
availableStockIntAvailable stock — what the buyer can buy now (on-hand minus active reservations, never below 0). Null when the merchant has disabled stock tracking for this variant (e.g. digital, made-to-order).
barcodeStringBarcode (EAN/UPC/etc.) as set by the merchant. Null when not configured.
compareAtPriceMoneyStrike-through compare-at price when the variant is on sale. Null when the variant is not discounted; show the strike-through only when this is present and greater than `price`.
compareAtPriceWithConversionPriceMoneyOptional opt-in: `compareAtPrice` with full conversion transparency.
idID!Stable identifier of the variant. Pass to `cartAddLines.variantId` to add the variant to a cart.
imageImageImage specific to this variant. Null when the variant inherits the parent product image.
isAvailableBoolean!True when the variant can be purchased right now (in stock, pre-order or backorder allowed by the merchant). Equivalent to `availability.status !== OUT_OF_STOCK`.
priceMoneyPublic catalog price of the variant in the shop base currency — the same for every visitor (`priceWithConversion` converts it to the visitor currency). A signed-in customer with their own prices (customer group) gets them from `variantPrices`. Null when the store restricts prices to signed-in customers — fetch via `variantPrices` after login.
priceReductionPriceReductionInfoStatutory reference of the current price reduction (EU price-indication rules). Null when the variant is not reduced, when the shop has not enabled price-history tracking, or when the reference cannot be proven — do not show a "lowest price" sentence then.
priceWithConversionPriceMoneyOptional opt-in: `price` with full conversion transparency (original shop-currency amount, exchange rate, conversion timestamp). Use when building a currency-converter UI.
selectedOptions[SelectedOption!]!Option values that define this variant (e.g. Size = Large, Color = Red).
skuStringStock-keeping unit code as set by the merchant. Null when not configured.
sortOrderIntDisplay position of this variant within the parent product list. Lower values come first.
storeAvailabilityStoreAvailabilityConnectionPer-location stock availability. Null for single-location shops. Resolved per-location with `near`, `locationType`, and `@inContext(preferredLocationId)` support.
titleString!Display title of the variant (e.g. "Large / Red"). Composed from the variant options.
weightWeightVariant weight with its unit. Stored in grams; pick a display unit on the storefront if needed. Null when the merchant has not set a weight.

Powiązane​