> ## Documentation Index
> Fetch the complete documentation index at: https://docs.komerza.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Produits

> Récupérer et afficher les produits, variantes et avis

## Aperçu

Accédez aux données des produits via deux méthodes :

* **`getStore()`** - Retourne tous les produits de la boutique (inclus dans la réponse de la boutique)
* **`getProduct()`** - Récupère un seul produit par ID ou slug
* **`getProductReviews()`** - Obtient les avis paginés pour un produit

## Obtenir tous les produits

Les produits sont inclus dans la réponse de la boutique :

```javascript theme={null}
const response = await komerza.getStore();

if (response.success) {
  const products = response.data.products;

  products.forEach((product) => {
    console.log(product.name, product.variants.length, "variantes");
  });
}
```

## Obtenir un produit individuel

Récupérer un produit par ID ou slug :

```javascript theme={null}
const response = await komerza.getProduct("product-id-or-slug");
```

### Paramètres

<ParamField path="idOrSlug" type="string" required>
  L'ID unique du produit (UUID) ou le slug URL
</ParamField>

### Exemple

```javascript theme={null}
// Par ID
const byId = await komerza.getProduct("550e8400-e29b-41d4-a716-446655440000");

// Par slug
const bySlug = await komerza.getProduct("premium-license");

if (bySlug.success) {
  const product = bySlug.data;
  console.log(product.name);
  console.log(product.description);
  console.log(product.variants);
}
```

## Objet Product

```typescript theme={null}
interface Product {
  id: string; // Identifiant unique
  name: string; // Nom du produit
  description: string; // Description (supporte le markdown)
  slug?: string; // Slug adapté à l'URL
  imageNames: string[]; // Noms de fichiers des images du produit (voir page Images)
  rating: number; // Note moyenne des avis
  order: number; // Ordre d'affichage
  visibility: number; // 0=Public, 1=Non répertorié, 2=Privé
  storeId: string; // ID de la boutique parente
  variants: Variant[]; // Variantes du produit
  dateCreated: string; // Horodatage ISO 8601
}
```

## Variantes

Chaque produit possède une ou plusieurs variantes. Chaque variante a son propre prix, stock et paramètres :

```typescript theme={null}
interface Variant {
  id: string; // ID de la variante
  name: string; // Nom de la variante
  productId: string; // ID du produit parent
  storeId: string; // ID de la boutique
  cost: number; // Prix dans la devise de la boutique
  stock: number; // Stock disponible
  stockMode: number; // Mode de calcul du stock (voir ci-dessous)
  minimumQuantity: number; // Quantité minimale d'achat (par défaut : 1)
  maximumQuantity: number; // Quantité maximale d'achat (-1 = illimité)
  order: number; // Ordre d'affichage
  imageNames: string[]; // Noms de fichiers des images spécifiques à la variante
  type: number; // 0=Unique, 1=Abonnement
  subscriptionPeriod?: string; // Durée de l'abonnement si applicable
  requireDiscordAuthorization: boolean;
  dateCreated: string;
}
```

### Mode de calcul du stock

Le champ `stockMode` détermine comment la disponibilité du stock est calculée :

| Valeur | Mode    | Description                                                            |
| ------ | ------- | ---------------------------------------------------------------------- |
| 0      | Calculé | Le stock est automatiquement calculé à partir des articles disponibles |
| 1      | Ignoré  | Le stock n'est pas suivi — toujours disponible                         |
| 2      | Fixe    | Le stock est défini manuellement et décrémenté à l'achat               |

Voir l'[enum Stock Calculation Mode](/api-reference/enums#stock-calculation-mode) pour les détails complets.

### Affichage des images

Les `imageNames` des produits et variantes contiennent des noms de fichiers, pas des URLs complètes. Consultez la page [Images](/storefront/images) pour savoir comment construire les URLs complètes.

### Afficher les variantes

```javascript theme={null}
async function afficherProduit(productId) {
  const [productResponse, formatter] = await Promise.all([
    komerza.getProduct(productId),
    komerza.createFormatter(),
  ]);

  if (!productResponse.success) return;

  const product = productResponse.data;

  // Trier les variantes par ordre
  const variants = product.variants.sort((a, b) => a.order - b.order);

  variants.forEach((variant) => {
    const price = formatter.format(variant.cost);
    const inStock = variant.stockMode === 1 || variant.stock > 0;

    console.log(`${variant.name}: ${price}`);
    console.log(`  Stock: ${inStock ? "Disponible" : "Rupture de stock"}`);
    console.log(`  Qté min: ${variant.minimumQuantity}`);
    console.log(
      `  Qté max: ${
        variant.maximumQuantity === -1 ? "Illimité" : variant.maximumQuantity
      }`,
    );
  });
}
```

### Sélection de variante

```html theme={null}
<select id="variant-select" onchange="updatePrice()">
  <!-- Rempli dynamiquement -->
</select>
<span id="price"></span>
<button onclick="addToCart()">Ajouter au panier</button>

<script>
  let currentProduct;
  let formatter;

  async function loadProduct(productId) {
    [{ data: currentProduct }, formatter] = await Promise.all([
      komerza.getProduct(productId),
      komerza.createFormatter(),
    ]);

    const select = document.getElementById("variant-select");
    select.innerHTML = currentProduct.variants
      .map(
        (v) =>
          `<option value="${v.id}">${v.name} - ${formatter.format(
            v.cost,
          )}</option>`,
      )
      .join("");

    updatePrice();
  }

  function updatePrice() {
    const variantId = document.getElementById("variant-select").value;
    const variant = currentProduct.variants.find((v) => v.id === variantId);
    document.getElementById("price").textContent = formatter.format(
      variant.cost,
    );
  }

  function addToCart() {
    const variantId = document.getElementById("variant-select").value;
    komerza.addToBasket(currentProduct.id, variantId);
  }
</script>
```

## Avis produit

Obtenez les avis paginés pour un produit :

```javascript theme={null}
const response = await komerza.getProductReviews(productId, page);
```

### Paramètres

<ParamField path="productId" type="string" required>
  L'ID unique du produit
</ParamField>

<ParamField path="page" type="number" required>
  Numéro de page (indexé à partir de 1)
</ParamField>

### Retours

Retourne `PaginatedApiResponse<Review>` :

```typescript theme={null}
interface PaginatedApiResponse<Review> {
  success: boolean;
  pages: number; // Nombre total de pages
  data?: Review[]; // Tableau d'avis
  message?: string;
}

interface Review {
  id: string;
  rating: number; // 1-5 étoiles
  reason: string; // Texte de l'avis
  productId: string;
  reply?: string; // Réponse du marchand si disponible
  dateCreated: string;
}
```

### Afficher les avis

```javascript theme={null}
async function afficherAvis(productId, page = 1) {
  const response = await komerza.getProductReviews(productId, page);

  if (!response.success) return;

  const container = document.getElementById("reviews");

  container.innerHTML = response.data
    .map(
      (review) => `
    <div class="review">
      <div class="rating">${"★".repeat(review.rating)}${"☆".repeat(
        5 - review.rating,
      )}</div>
      <p>${review.reason}</p>
      ${
        review.reply
          ? `<p class="reply"><strong>Réponse :</strong> ${review.reply}</p>`
          : ""
      }
      <time>${new Date(review.dateCreated).toLocaleDateString()}</time>
    </div>
  `,
    )
    .join("");

  // Pagination
  if (response.pages > 1) {
    container.innerHTML += `
      <div class="pagination">
        ${
          page > 1
            ? `<button onclick="afficherAvis('${productId}', ${
                page - 1
              })">Précédent</button>`
            : ""
        }
        <span>Page ${page} sur ${response.pages}</span>
        ${
          page < response.pages
            ? `<button onclick="afficherAvis('${productId}', ${
                page + 1
              })">Suivant</button>`
            : ""
        }
      </div>
    `;
  }
}
```

## Images produit

Les images produit sont référencées par nom de fichier. Construisez l'URL complète :

```javascript theme={null}
function getProductImageUrl(storeId, productId, imageName) {
  return `https://cdn.komerza.com/stores/${storeId}/products/${productId}/${imageName}`;
}

// Utilisation
const product = response.data;
const imageUrl = getProductImageUrl(
  product.storeId,
  product.id,
  product.imageNames[0],
);
```

### Galerie d'images

```javascript theme={null}
function afficherImagesProduct(product) {
  const gallery = document.getElementById("gallery");

  // Utiliser les images de variante si disponibles, sinon les images produit
  const images =
    product.variants[0].imageNames.length > 0
      ? product.variants[0].imageNames
      : product.imageNames;

  gallery.innerHTML = images
    .map(
      (img) => `
    <img 
      src="https://cdn.komerza.com/stores/${product.storeId}/products/${product.id}/${img}"
      alt="${product.name}"
      loading="lazy"
    />
  `,
    )
    .join("");
}
```

## Gestion du stock

Vérifiez la disponibilité du stock avant d'ajouter au panier :

```javascript theme={null}
function peutAjouterAuPanier(variant, quantity) {
  // stockMode 1 = Ignoré (illimité)
  if (variant.stockMode === 1) return true;

  // Vérifier le stock disponible
  if (variant.stock < quantity) return false;

  // Vérifier les limites de quantité
  if (quantity < variant.minimumQuantity) return false;
  if (variant.maximumQuantity !== -1 && quantity > variant.maximumQuantity)
    return false;

  return true;
}

function getStatutStock(variant) {
  if (variant.stockMode === 1) return "En stock";
  if (variant.stock === 0) return "Rupture de stock";
  if (variant.stock < 10) return `Plus que ${variant.stock}`;
  return "En stock";
}
```

## Exemple complet

```html theme={null}
<!DOCTYPE html>
<html>
  <head>
    <title>Page produit</title>
    <script src="https://cdn.komerza.com/komerza.min.js"></script>
    <style>
      .product {
        max-width: 600px;
        margin: 0 auto;
      }
      .gallery img {
        max-width: 100%;
      }
      .variants {
        margin: 20px 0;
      }
      .reviews {
        margin-top: 40px;
      }
      .review {
        border-bottom: 1px solid #eee;
        padding: 15px 0;
      }
      .rating {
        color: gold;
      }
    </style>
  </head>
  <body>
    <div class="product">
      <div id="gallery"></div>
      <h1 id="name"></h1>
      <p id="description"></p>
      <p id="rating"></p>

      <div class="variants">
        <select id="variant-select"></select>
        <span id="price"></span>
        <span id="stock"></span>
      </div>

      <div>
        <label
          >Quantité : <input type="number" id="quantity" value="1" min="1"
        /></label>
        <button id="add-btn" onclick="addToCart()">Ajouter au panier</button>
      </div>

      <div class="reviews">
        <h2>Avis</h2>
        <div id="reviews"></div>
      </div>
    </div>

    <script>
      komerza.init("your-store-id");

      let product;
      let formatter;

      async function loadProduct(idOrSlug) {
        const [productResponse, fmt] = await Promise.all([
          komerza.getProduct(idOrSlug),
          komerza.createFormatter(),
        ]);

        if (!productResponse.success) {
          alert("Produit introuvable");
          return;
        }

        product = productResponse.data;
        formatter = fmt;

        // Afficher les informations du produit
        document.getElementById("name").textContent = product.name;
        document.getElementById("description").textContent =
          product.description;
        document.getElementById("rating").textContent =
          `Note : ${product.rating}★`;

        // Afficher les images
        const images = product.imageNames;
        document.getElementById("gallery").innerHTML = images
          .map(
            (img) =>
              `<img src="https://cdn.komerza.com/stores/${product.storeId}/products/${product.id}/${img}" />`,
          )
          .join("");

        // Remplir les variantes
        const select = document.getElementById("variant-select");
        select.innerHTML = product.variants
          .map((v) => `<option value="${v.id}">${v.name}</option>`)
          .join("");
        select.onchange = updateVariantDisplay;

        updateVariantDisplay();
        loadReviews(1);
      }

      function updateVariantDisplay() {
        const variantId = document.getElementById("variant-select").value;
        const variant = product.variants.find((v) => v.id === variantId);

        document.getElementById("price").textContent = formatter.format(
          variant.cost,
        );

        // Statut du stock
        let stockText = "En stock";
        if (variant.stockMode !== 1) {
          if (variant.stock === 0) stockText = "Rupture de stock";
          else if (variant.stock < 10) stockText = `Plus que ${variant.stock}`;
        }
        document.getElementById("stock").textContent = stockText;

        // Mettre à jour les limites de quantité
        const qtyInput = document.getElementById("quantity");
        qtyInput.min = variant.minimumQuantity;
        qtyInput.max =
          variant.maximumQuantity === -1 ? 999 : variant.maximumQuantity;
        qtyInput.value = variant.minimumQuantity;

        // Désactiver le bouton si rupture de stock
        document.getElementById("add-btn").disabled =
          variant.stockMode !== 1 && variant.stock === 0;
      }

      function addToCart() {
        const variantId = document.getElementById("variant-select").value;
        const quantity = parseInt(document.getElementById("quantity").value);

        komerza.addToBasket(product.id, variantId, quantity);
        alert("Ajouté au panier !");
      }

      async function loadReviews(page) {
        const response = await komerza.getProductReviews(product.id, page);

        if (!response.success || response.data.length === 0) {
          document.getElementById("reviews").innerHTML =
            "<p>Aucun avis pour l'instant</p>";
          return;
        }

        document.getElementById("reviews").innerHTML = response.data
          .map(
            (review) => `
        <div class="review">
          <div class="rating">${"★".repeat(review.rating)}${"☆".repeat(
            5 - review.rating,
          )}</div>
          <p>${review.reason}</p>
          ${review.reply ? `<p><em>Réponse : ${review.reply}</em></p>` : ""}
        </div>
      `,
          )
          .join("");
      }

      // Charger le produit depuis l'URL ou par défaut
      const urlParams = new URLSearchParams(window.location.search);
      const productId = urlParams.get("product") || "your-default-product-slug";
      loadProduct(productId);
    </script>
  </body>
</html>
```

## Étapes suivantes

<CardGroup cols={2}>
  <Card title="Formatage des devises" icon="dollar-sign" href="/storefront/formatting">
    Formater les prix dans la devise de votre boutique
  </Card>

  <Card title="Gestion du panier" icon="cart-shopping" href="/storefront/basket">
    Ajouter des produits au panier
  </Card>
</CardGroup>
