From b9e0af5470a2e815d8743326842bf479441742c8 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 13 Apr 2023 21:18:34 +0300 Subject: [PATCH] docs: added how-to guide for show products (#3828) * docs: added how-to guide for show products * small fixes * fix links * renamed store directory * fixing links after renaming directory * fix links after renaming directory * fix link * fixed sidebar link * fix tab --- .../products/admin/manage-categories.mdx | 2 +- docs/content/modules/products/categories.md | 2 +- docs/content/modules/products/overview.mdx | 5 +- .../products/storefront/show-products.mdx | 624 ++++++++++++++++++ .../{store => storefront}/use-categories.mdx | 0 www/docs/sidebars.js | 9 +- 6 files changed, 631 insertions(+), 11 deletions(-) create mode 100644 docs/content/modules/products/storefront/show-products.mdx rename docs/content/modules/products/{store => storefront}/use-categories.mdx (100%) diff --git a/docs/content/modules/products/admin/manage-categories.mdx b/docs/content/modules/products/admin/manage-categories.mdx index a07321ab64..af01bf2035 100644 --- a/docs/content/modules/products/admin/manage-categories.mdx +++ b/docs/content/modules/products/admin/manage-categories.mdx @@ -681,4 +681,4 @@ The request returns the following fields: ## See Also -- [How to use product categories in a storefront](../store/use-categories.mdx) +- [How to use product categories in a storefront](../storefront/use-categories.mdx) diff --git a/docs/content/modules/products/categories.md b/docs/content/modules/products/categories.md index 03dab5afd9..8b97532208 100644 --- a/docs/content/modules/products/categories.md +++ b/docs/content/modules/products/categories.md @@ -93,4 +93,4 @@ Aside from these relations, the `mpath` attribute, which is a [Materialized Path ## See Also - [How to manage product categories using the admin APIs](./admin/manage-categories.mdx) -- [How to use product categories in a storefront](./store/use-categories.mdx) +- [How to use product categories in a storefront](./storefront/use-categories.mdx) diff --git a/docs/content/modules/products/overview.mdx b/docs/content/modules/products/overview.mdx index e156478cb8..a118db171f 100644 --- a/docs/content/modules/products/overview.mdx +++ b/docs/content/modules/products/overview.mdx @@ -42,12 +42,11 @@ Admins can manage unlimited amount of products and their variants. They can mana }, { type: 'link', - href: '#', + href: '/modules/products/storefront/show-products', label: 'Storefront: Show Products', customProps: { icon: Icons['academic-cap-solid'], description: 'Learn how to show products in a storefront.', - isSoon: true, } }, ]} /> @@ -79,7 +78,7 @@ Customers can use this organization to filter products while browsing them. }, { type: 'link', - href: '/modules/products/store/use-categories', + href: '/modules/products/storefront/use-categories', label: 'Storefront: Use Categories', customProps: { icon: Icons['server-solid'], diff --git a/docs/content/modules/products/storefront/show-products.mdx b/docs/content/modules/products/storefront/show-products.mdx new file mode 100644 index 0000000000..29817b1874 --- /dev/null +++ b/docs/content/modules/products/storefront/show-products.mdx @@ -0,0 +1,624 @@ +--- +description: 'Learn how to show products in your storefront using the Store REST APIs. This includes listing products, showing the price of a product, and more.' +addHowToData: true +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# How to Show Products on the Storefront + +In this document, you’ll learn how to show products in your storefront using the Store REST APIs. + +## Overview + +Using the products store REST APIs, you can display products on your storefront along with their different details. + +### Scenario + +You want to add or use the following storefront functionalities: + +- List products with filters. +- Display product prices. +- Search products. +- Retrieve details of a single product by ID or by handle. + +--- + +## Prerequisites + +### Medusa Components + +It's assumed that you already have a Medusa backend installed and set up. If not, you can follow the [quickstart guide](../../../development/backend/install.mdx) to get started. + +It's also assumed you already have a storefront set up. It can be a custom storefront or one of Medusa’s storefronts. If you don’t have a storefront set up, you can install the [Next.js starter storefront](../../../starters/nextjs-medusa-starter.mdx). + +### JS Client + +This guide includes code snippets to send requests to your Medusa backend using Medusa’s JS Client, among other methods. + +If you follow the JS Client code blocks, it’s assumed you already have [Medusa’s JS Client installed](../../../js-client/overview.md) and have [created an instance of the client](../../../js-client/overview.md#configuration). + +### Medusa React + +This guide also includes code snippets to send requests to your Medusa backend using Medusa React, among other methods. + +If you follow the Medusa React code blocks, it's assumed you already have [Medusa React installed](../../../medusa-react/overview.md) and have [used MedusaProvider higher in your component tree](../../../medusa-react/overview.md#usage). + +--- + +## List Products + +You can list available products using the [List Products endpoint](/api/store#tag/Products/operation/GetProducts): + + + + +```ts +medusa.products.list() +.then(({ products, limit, offset, count }) => { + console.log(products.length) +}) +``` + + + + +```tsx +import { useProducts } from "medusa-react" +import { Product } from "@medusajs/medusa" + +const Products = () => { + const { products, isLoading } = useProducts() + + return ( +
+ {isLoading && Loading...} + {products && !products.length && No Products} + {products && products.length > 0 && ( +
    + {products.map((product: Product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} + +export default Products +``` + +
+ + +```ts +fetch(`/store/products`, { + credentials: "include", +}) +.then((response) => response.json()) +.then(({ products, limit, offset, count }) => { + console.log(products.length) +}) +``` + + + + +```bash +curl -L -X GET '/store/products' +``` + + +
+ +This endpoint does not require any parameters. You can pass it parameters related to pagination, filtering, and more as explained in the [API reference](/api/store#tag/Products/operation/GetProducts). + +The request returns an array of product objects along with [pagination parameters](/api/store#section/Pagination). + +### Filtering Retrieved Products + +The List Products endpoint accepts different query parameters that allow you to filter through retrieved results. + +For example, you can filter products by a category ID: + + + + +```ts +medusa.products.list({ + category_id: ["cat_123"], +}) +.then(({ products, limit, offset, count }) => { + console.log(products.length) +}) +``` + + + + +```tsx +import { useProducts } from "medusa-react" +import { Product } from "@medusajs/medusa" + +const Products = () => { + const { products, isLoading } = useProducts({ + category_id: ["cat_123"], + }) + + return ( +
+ {isLoading && Loading...} + {products && !products.length && No Products} + {products && products.length > 0 && ( +
    + {products.map((product: Product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} + +export default Products +``` + +
+ + +```ts +fetch(`/store/products?category_id[]=cat_123`, { + credentials: "include", +}) +.then((response) => response.json()) +.then(({ products, limit, offset, count }) => { + console.log(products.length) +}) +``` + + + + +```bash +curl -L -X GET '/store/products?category_id[]=cat_123' +``` + + +
+ +This will retrieve only products that belong to that category. + +### Product Pricing Parameters + +By default, the prices are retrieved based on the default currency associated with a store. You can use the following query parameters to ensure you are retrieving correct pricing based on the customer’s context: + +- `region_id`: The ID of the customer’s region. +- `cart_id`: The ID of the customer’s cart. +- `currency_code`: The code of the currency to retrieve prices for. + +It’s recommended to always include the cart and region’s IDs when you’re listing or retrieving a single product’s details, as it’ll show you the correct pricing fields as explained in the next section. + +For example: + + + + +```ts +medusa.products.list({ + cart_id, + region_id, +}) +.then(({ products, limit, offset, count }) => { + console.log(products.length) +}) +``` + + + + +```tsx +import { useProducts } from "medusa-react" +import { Product } from "@medusajs/medusa" + +const Products = () => { + const { products, isLoading } = useProducts({ + cart_id, + region_id, + }) + + return ( +
+ {isLoading && Loading...} + {products && !products.length && No Products} + {products && products.length > 0 && ( +
    + {products.map((product: Product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} + +export default Products +``` + +
+ + + + +```ts +fetch(`/store/products?cart_id=${cartId}®ion_id=${regionId}`, { + credentials: "include", +}) +.then((response) => response.json()) +.then(({ products, limit, offset, count }) => { + console.log(products.length) +}) +``` + + + + +```bash +curl -L -X GET '/store/products?cart_id=®ion_id=' +``` + + +
+ +### Display Product Price + +Each product object in the retrieved array has a `variants` array. Each item in the `variants` array is a product variant object. + +Product prices are available for each variant in the product. Each variant has a `prices` array with all the available prices in the context. However, when displaying the variant’s price, you’ll use the following properties inside a variant object: + +- `original_price`: The original price of the product variant. +- `calculated_price`: The calculated price, which can be based on prices defined in a price list. +- `original_tax`: The tax amount applied to the original price, if any. +- `calculated_tax`: The tax amount applied to the calculated price, if any. +- `original_price_incl_tax`: The price after applying the tax amount on the original price. +- `calculated_price_incl_tax`: The price after applying the tax amount on the calculated price + +Typically, you would display the `calculated_price_incl_tax` as the price of the product variant. + +:::note + +You must pass one of the [pricing parameters](#product-pricing-parameters) to the request to retrieve these values. Otherwise, their value will be `null`. + +::: + +Prices in Medusa are stored as the price entered by the admin multiplied by a 100. So, to show the correct price, you would need to convert it to a decimal with a method like this: + +```ts +const convertToDecimal = (amount: number) => { + return Math.floor(amount) / 100 +} +``` + + + +To display it along with a currency, it’s recommended to use JavaScript’s [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). For example: + + + +```ts +new Intl.NumberFormat("en-US", { + style: "currency", + currency: "eur", +}).format(convertToDecimal(amount)) +``` + +Ideally, you would retrieve the value of the `currency` property from the selected region’s `currency_code` attribute. + +Medusa React provides utility methods such as `formatVariantPrice` that handles this logic for you. + +Here’s an example of how you can calculate the price with and without Medusa React: + + + + +```tsx +import React, { useEffect, useState } from "react" +import Medusa from "@medusajs/medusa-js" + +const medusa = new Medusa({ + baseUrl: "", + maxRetries: 3, +}) + +function Products() { + const [products, setProducts] = useState([]) + + useEffect(() => { + medusa.products.list({ + // TODO assuming region is already defined somewhere + region_id: region.id, + }) + .then(({ products, limit, offset, count }) => { + // ignore pagination for sake of example + setProducts(products) + }) + }) + + const convertToDecimal = (amount) => { + return Math.floor(amount) / 100 + } + + const formatPrice = (amount) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + // TODO assuming region is already defined somewhere + currency: region.currency_code, + }).format(convertToDecimal(amount)) + } + + return ( +
    + {products.map((product) => ( + <> + {product.variants.map((variant) => ( +
  • { + formatPrice(variant.calculated_price_incl_tax) + }
  • + ))} + + ))} +
+ ) +} + +export default Products +``` + +
+ + +```tsx +import { formatVariantPrice, useProducts } from "medusa-react" +import { Product, ProductVariant } from "@medusajs/medusa" + +const Products = () => { + const { products, isLoading } = useProducts({ + region_id: region.id, // assuming already defined somewhere + }) + + return ( +
+ {isLoading && Loading...} + {products && !products.length && ( + No Products + )} + {products && products.length > 0 && ( +
    + {products.map((product: Product) => ( + <> + {product.variants.map( + (variant: ProductVariant) => ( +
  • + {formatVariantPrice({ + variant, + // assuming already defined somewhere + region, + })} +
  • + ))} + + ))} +
+ )} +
+ ) +} + +export default Products +``` + +
+
+ +--- + +## Search Products + +:::note + +The Search functionality requires either installing a [search plugin](../../../plugins/search/index.mdx) or creating a search service. + +::: + +You can search products using the [Search Products endpoint](/api/store#tag/Products/operation/PostProductsSearch): + + + + +```ts +medusa.products.search({ + q: "Shirt", +}) +.then(({ hits }) => { + console.log(hits.length) +}) +``` + + + + +```ts +fetch(`/store/products/search?q=Shirt`, { + credentials: "include", + method: "POST", +}) +.then((response) => response.json()) +.then(({ hits }) => { + console.log(hits.length) +}) +``` + + + + +```bash +curl -L -X POST '/store/products/search?q=Shirt' +``` + + + + +This endpoint requires the query parameter `q` being the term to search products for. The search plugin or service you’re using determine how `q` will be used to search the products. It also accepts pagination parameters as explained in the [API reference](/api/store#tag/Products/operation/PostProductsSearch). + +The request returns a `hits` array holding the result items. The structure of the items depends on the plugin you’re using. + +--- + +## Retrieve a Product by ID + +You can retrieve the details of a single product by its ID using the [Get a Product endpoint](/api/store#tag/Products/operation/GetProductsProduct): + + + + +```ts +medusa.products.retrieve(productId) +.then(({ product }) => { + console.log(product.id) +}) +``` + + + + +```tsx +import { useProduct } from "medusa-react" + +const Products = () => { + const { product, isLoading } = useProduct(productId) + + return ( +
+ {isLoading && Loading...} + {product && {product.title}} +
+ ) +} + +export default Products +``` + +
+ + +```ts +fetch(`/store/products/${productId}`, { + credentials: "include", +}) +.then((response) => response.json()) +.then(({ product }) => { + console.log(product.id) +}) +``` + + + + +```bash +curl -L -X GET '/store/products/' +``` + + +
+ +This endpoint requires the product’s ID to be passed as a path parameter. You can also pass query parameters such as `cart_id` and `region_id` which are relevant for pricing as explained in the [Product Pricing Parameters section](#product-pricing-parameters). You can check the full list of accepted parameters in the [API reference](/api/store#tag/Products/operation/GetProductsProduct). + +The request returns a product object. You can display its price as explained in the [Display Product Price](#display-product-price) section. + +--- + +## Retrieve Product by Handle + +On the storefront, you may use the handle of a product as its page’s path. For example, instead of displaying the product’s details on the path `/products/prod_123`, you can display it on the path `/products/shirt`, where `shirt` is the handle of the product. This type of URL is human-readable and is good for Search Engine Optimization (SEO) + +You can retrieve the details of a product by its handle by sending a request to the List Products endpoint, passing the `handle` as a filter: + + + + +```ts +medusa.products.list({ + handle, +}) +.then(({ products }) => { + if (!products.length) { + // product does not exist + } + const product = products[0] +}) +``` + + + + +```tsx +import { useProducts } from "medusa-react" + +const Products = () => { + const { products, isLoading } = useProducts({ + handle, + }) + + return ( +
+ {isLoading && Loading...} + {products && !products.length && ( + Product does not exist + )} + {products && products.length > 0 && products[0].title} +
+ ) +} + +export default Products +``` + +
+ + +```ts +fetch(`/store/products?handle=${handle}`, { + credentials: "include", +}) +.then((response) => response.json()) +.then(({ products }) => { + if (!products.length) { + // product does not exist + } + const product = products[0] +}) +``` + + + + +```bash +curl -L -X GET '/store/products?handle=' +``` + + +
+ +As the `handle` of each product is unique, when you pass the handle as a filter you’ll either: + +- receive an empty `products` array, meaning the product doesn’t exist; +- or you’ll receive a `products` array with one item being the product you’re looking for. In this case, you can access the product at index `0`. + +As explained earlier, make sure to pass the [product pricing parameters](#product-pricing-parameters) to [display the product's price](#display-product-price) + +--- + +## See Also + +- [How to use regions in a storefront](../../regions-and-currencies/storefront/use-regions.mdx) +- [How to implement cart functionality](../../carts-and-checkout/storefront/implement-cart.mdx) diff --git a/docs/content/modules/products/store/use-categories.mdx b/docs/content/modules/products/storefront/use-categories.mdx similarity index 100% rename from docs/content/modules/products/store/use-categories.mdx rename to docs/content/modules/products/storefront/use-categories.mdx diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index f599fbd234..ee49544a15 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -633,16 +633,13 @@ module.exports = { label: "Admin: Import Products", }, { - type: "link", - href: "#", + type: "doc", + id: "modules/products/storefront/show-products", label: "Storefront: Show Products", - customProps: { - sidebar_is_soon: true, - }, }, { type: "doc", - id: "modules/products/store/use-categories", + id: "modules/products/storefront/use-categories", label: "Storefront: Use Categories", }, {