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
This commit is contained in:
Shahed Nasser
2023-04-13 21:18:34 +03:00
committed by GitHub
parent 95d338262b
commit b9e0af5470
6 changed files with 631 additions and 11 deletions
@@ -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)
+1 -1
View File
@@ -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)
+2 -3
View File
@@ -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'],
@@ -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, youll 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 Medusas storefronts. If you dont 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 Medusas JS Client, among other methods.
If you follow the JS Client code blocks, its assumed you already have [Medusas 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):
<Tabs groupId="request-type" wrapperClassName="code-tabs">
<TabItem value="client" label="Medusa JS Client" default>
```ts
medusa.products.list()
.then(({ products, limit, offset, count }) => {
console.log(products.length)
})
```
</TabItem>
<TabItem value="medusa-react" label="Medusa React">
```tsx
import { useProducts } from "medusa-react"
import { Product } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts()
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && <span>No Products</span>}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
)
}
export default Products
```
</TabItem>
<TabItem value="fetch" label="Fetch API">
```ts
fetch(`<BACKEND_URL>/store/products`, {
credentials: "include",
})
.then((response) => response.json())
.then(({ products, limit, offset, count }) => {
console.log(products.length)
})
```
</TabItem>
<TabItem value="curl" label="cURL">
```bash
curl -L -X GET '<BACKEND_URL>/store/products'
```
</TabItem>
</Tabs>
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:
<Tabs groupId="request-type" wrapperClassName="code-tabs">
<TabItem value="client" label="Medusa JS Client" default>
```ts
medusa.products.list({
category_id: ["cat_123"],
})
.then(({ products, limit, offset, count }) => {
console.log(products.length)
})
```
</TabItem>
<TabItem value="medusa-react" label="Medusa React">
```tsx
import { useProducts } from "medusa-react"
import { Product } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts({
category_id: ["cat_123"],
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && <span>No Products</span>}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
)
}
export default Products
```
</TabItem>
<TabItem value="fetch" label="Fetch API">
```ts
fetch(`<BACKEND_URL>/store/products?category_id[]=cat_123`, {
credentials: "include",
})
.then((response) => response.json())
.then(({ products, limit, offset, count }) => {
console.log(products.length)
})
```
</TabItem>
<TabItem value="curl" label="cURL">
```bash
curl -L -X GET '<BACKEND_URL>/store/products?category_id[]=cat_123'
```
</TabItem>
</Tabs>
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 customers context:
- `region_id`: The ID of the customers region.
- `cart_id`: The ID of the customers cart.
- `currency_code`: The code of the currency to retrieve prices for.
Its recommended to always include the cart and regions IDs when youre listing or retrieving a single products details, as itll show you the correct pricing fields as explained in the next section.
For example:
<Tabs groupId="request-type" wrapperClassName="code-tabs">
<TabItem value="client" label="Medusa JS Client" default>
```ts
medusa.products.list({
cart_id,
region_id,
})
.then(({ products, limit, offset, count }) => {
console.log(products.length)
})
```
</TabItem>
<TabItem value="medusa-react" label="Medusa React">
```tsx
import { useProducts } from "medusa-react"
import { Product } from "@medusajs/medusa"
const Products = () => {
const { products, isLoading } = useProducts({
cart_id,
region_id,
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && <span>No Products</span>}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
)
}
export default Products
```
</TabItem>
<TabItem value="fetch" label="Fetch API">
<!-- eslint-disable max-len -->
```ts
fetch(`<BACKEND_URL>/store/products?cart_id=${cartId}&region_id=${regionId}`, {
credentials: "include",
})
.then((response) => response.json())
.then(({ products, limit, offset, count }) => {
console.log(products.length)
})
```
</TabItem>
<TabItem value="curl" label="cURL">
```bash
curl -L -X GET '<BACKEND_URL>/store/products?cart_id=<CART_ID>&region_id=<REGION_ID>'
```
</TabItem>
</Tabs>
### 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 variants price, youll 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
}
```
<!-- vale docs.Spacing = NO -->
To display it along with a currency, its recommended to use JavaScripts [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). For example:
<!-- vale docs.Spacing = YES -->
```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 regions `currency_code` attribute.
Medusa React provides utility methods such as `formatVariantPrice` that handles this logic for you.
Heres an example of how you can calculate the price with and without Medusa React:
<Tabs groupId="client-preference" wrapperClassName="code-tabs">
<TabItem value="client" label="Without Medusa Client" default>
```tsx
import React, { useEffect, useState } from "react"
import Medusa from "@medusajs/medusa-js"
const medusa = new Medusa({
baseUrl: "<YOUR_BACKEND_URL>",
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 (
<ul>
{products.map((product) => (
<>
{product.variants.map((variant) => (
<li key={variant.id}>{
formatPrice(variant.calculated_price_incl_tax)
}</li>
))}
</>
))}
</ul>
)
}
export default Products
```
</TabItem>
<TabItem value="medusa-react" label="With Medusa React">
```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 (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && (
<span>No Products</span>
)}
{products && products.length > 0 && (
<ul>
{products.map((product: Product) => (
<>
{product.variants.map(
(variant: ProductVariant) => (
<li key={variant.id}>
{formatVariantPrice({
variant,
// assuming already defined somewhere
region,
})}
</li>
))}
</>
))}
</ul>
)}
</div>
)
}
export default Products
```
</TabItem>
</Tabs>
---
## 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):
<Tabs groupId="request-type" wrapperClassName="code-tabs">
<TabItem value="client" label="Medusa JS Client" default>
```ts
medusa.products.search({
q: "Shirt",
})
.then(({ hits }) => {
console.log(hits.length)
})
```
</TabItem>
<TabItem value="fetch" label="Fetch API">
```ts
fetch(`<BACKEND_URL>/store/products/search?q=Shirt`, {
credentials: "include",
method: "POST",
})
.then((response) => response.json())
.then(({ hits }) => {
console.log(hits.length)
})
```
</TabItem>
<TabItem value="curl" label="cURL">
```bash
curl -L -X POST '<BACKEND_URL>/store/products/search?q=Shirt'
```
</TabItem>
</Tabs>
This endpoint requires the query parameter `q` being the term to search products for. The search plugin or service youre 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 youre 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):
<Tabs groupId="request-type" wrapperClassName="code-tabs">
<TabItem value="client" label="Medusa JS Client" default>
```ts
medusa.products.retrieve(productId)
.then(({ product }) => {
console.log(product.id)
})
```
</TabItem>
<TabItem value="medusa-react" label="Medusa React">
```tsx
import { useProduct } from "medusa-react"
const Products = () => {
const { product, isLoading } = useProduct(productId)
return (
<div>
{isLoading && <span>Loading...</span>}
{product && <span>{product.title}</span>}
</div>
)
}
export default Products
```
</TabItem>
<TabItem value="fetch" label="Fetch API">
```ts
fetch(`<BACKEND_URL>/store/products/${productId}`, {
credentials: "include",
})
.then((response) => response.json())
.then(({ product }) => {
console.log(product.id)
})
```
</TabItem>
<TabItem value="curl" label="cURL">
```bash
curl -L -X GET '<BACKEND_URL>/store/products/<PRODUCT_ID>'
```
</TabItem>
</Tabs>
This endpoint requires the products 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 pages path. For example, instead of displaying the products 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:
<Tabs groupId="request-type" wrapperClassName="code-tabs">
<TabItem value="client" label="Medusa JS Client" default>
```ts
medusa.products.list({
handle,
})
.then(({ products }) => {
if (!products.length) {
// product does not exist
}
const product = products[0]
})
```
</TabItem>
<TabItem value="medusa-react" label="Medusa React">
```tsx
import { useProducts } from "medusa-react"
const Products = () => {
const { products, isLoading } = useProducts({
handle,
})
return (
<div>
{isLoading && <span>Loading...</span>}
{products && !products.length && (
<span>Product does not exist</span>
)}
{products && products.length > 0 && products[0].title}
</div>
)
}
export default Products
```
</TabItem>
<TabItem value="fetch" label="Fetch API">
```ts
fetch(`<BACKEND_URL>/store/products?handle=${handle}`, {
credentials: "include",
})
.then((response) => response.json())
.then(({ products }) => {
if (!products.length) {
// product does not exist
}
const product = products[0]
})
```
</TabItem>
<TabItem value="curl" label="cURL">
```bash
curl -L -X GET '<BACKEND_URL>/store/products?handle=<HANDLE>'
```
</TabItem>
</Tabs>
As the `handle` of each product is unique, when you pass the handle as a filter youll either:
- receive an empty `products` array, meaning the product doesnt exist;
- or youll receive a `products` array with one item being the product youre 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)