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 pozycyjnaaddToCart(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).
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, or backorder allowed by the merchant).
priceMoneyCurrent price of the variant in the buyer preferred currency (auto-converted from the shop base currency when multi-currency is enabled). 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