docs: add products storefront guides (#7645)

* add tips + regions pages

* docs: added storefront regions guide

* removed storefront tips chapter from book

* added product guides

* finished price guide

* add product category pages

* more categories pages

* finished category pages

* add collections guides

* add missing next.js comment

* use useRegion hook

* added missing link
This commit is contained in:
Shahed Nasser
2024-06-11 19:55:56 +02:00
committed by GitHub
parent 48d8b2c623
commit ee6bdd0ab0
17 changed files with 2463 additions and 0 deletions
@@ -0,0 +1,211 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `List Product Categories in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to list product categories in the storefront, including paginating and filtering them.
## List Product Categories
To retrieve the list of product categories, send a request to the [List Product Categories API route](!api!/store#product-categories_getproductcategories):
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(`http://localhost:9000/store/product-categories`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_categories }) => {
// use categories...
console.log(product_categories)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["17"], ["18"], ["19"], ["20"],
["21"], ["22"], ["23"], ["24"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react";
import { HttpTypes } from "@medusajs/types"
export default function Categories() {
const [loading, setLoading] = useState(true)
const [categories, setCategories] = useState<
HttpTypes.StoreProductCategory[]
>([])
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/product-categories`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_categories }) => {
setCategories(product_categories)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && categories.length === 0 && (
<span>No product categories found.</span>
)}
{!loading && categories.length > 0 && (
<ul>
{categories.map((category) => (
<li key={category.id}>{category.name}</li>
))}
</ul>
)}
</div>
);
}
```
</CodeTab>
</CodeTabs>
The response has a `product_categories` field, which is an array of [product categories](!api!/store/#product-categories_productcategory_schema).
---
## Paginate Product Categories
To paginate product categories, pass the following query parameters:
- `limit`: The number of product categories to return in the request.
- `offset`: The number of product categories to skip before the returned product categories. You can calculate this by multiplying the current page with the limit.
The response object returns a `count` field, which is the total count of product categories. Use it to determine whether there are more product categories that can be loaded.
For example:
export const paginateHighlights = [
["20", "offset", "Calculate the number of product categories to skip based on the current page and limit."],
["28", "searchParams.toString()", "Pass the pagination parameters in the query."],
["33", "count", "The total number of product categories in the Medusa application."],
["45", "setHasMorePages", "Set whether there are more pages based on the total count."],
["64", "button", "Show a button to load more product categories if there are more pages."]
]
```tsx highlights={paginateHighlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react";
import { HttpTypes } from "@medusajs/types"
export default function Categories() {
const [loading, setLoading] = useState(true)
const [categories, setCategories] = useState<
HttpTypes.StoreProductCategory[]
>([])
const limit = 20
const [currentPage, setCurrentPage] = useState(1)
const [hasMorePages, setHasMorePages] = useState(false)
useEffect(() => {
if (!loading) {
return
}
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
})
fetch(`http://localhost:9000/store/product-categories?${
searchParams.toString()
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_categories, count }) => {
setCategories((prev) => {
if (prev.length > offset) {
// product categories already added because
// the same request has already been sent
return prev
}
return [
...prev,
...product_categories
]
})
setHasMorePages(count > limit * currentPage)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && categories.length === 0 && (
<span>No product categories found.</span>
)}
{!loading && categories.length > 0 && (
<ul>
{categories.map((category) => (
<li key={category.id}>{category.name}</li>
))}
</ul>
)}
{!loading && hasMorePages && (
<button
onClick={() => {
setCurrentPage((prev) => prev + 1)
setLoading(true)
}}
disabled={loading}
>
Load More
</button>
)}
</div>
);
}
```
---
## Filter Categories
The List Product Categories API route accepts query parameters to filter the categories by description, handle, and more.
Refer to the [API reference](!api!/store#product-categories_getproductcategories) for the list of accepted query parameters.
For example, to run a query on the product categories:
```ts
const searchParams = new URLSearchParams({
q: "Shirt"
})
fetch(`http://localhost:9000/store/product-categories?${
searchParams.toString()
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_categories, count }) => {
// TODO set categories...
})
```
@@ -0,0 +1,120 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve Nested Categories in Storefront`,
}
# {metadata.title}
A product category has parent and child categories.
To retrieve the child or nested categories of a category in your storefront, pass `*category_children` to the `fields` query parameter of the [Get a Category API Route](!api!/store#product-categories_getproductcategoriesid).
For example:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["2", `"*category_children"`, "Select the `category_children` relation."],
]
```ts highlights={fetchHighlights}
const searchParams = new URLSearchParams({
fields: "*category_children"
})
fetch(`http://localhost:9000/store/product-categories/${id}?${
searchParams.toString()
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_category }) => {
// use the product category's children...
console.log(product_category.category_children)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["12", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["24", `"*category_children"`, "Select the `category_children` relation."],
["27"], ["28"], ["29"],
["30"], ["31"], ["32"], ["33"], ["34"], ["35"], ["36"],
["50", "", "Show the nested categories."],
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
id: string
}
}
export default function Category({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [category, setCategory] = useState<
HttpTypes.StoreProductCategory | undefined
>()
useEffect(() => {
if (!loading) {
return
}
const searchParams = new URLSearchParams({
fields: "*category_children"
})
fetch(`http://localhost:9000/store/product-categories/${id}?${
searchParams.toString()
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_category }) => {
setCategory(product_category)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{category && (
<>
<h1>{category.name}</h1>
<p>{category.description}</p>
{(category.category_children?.length || 0) > 0 && (
<>
<span>Child Categories</span>
<ul>
{category.category_children!.map(
(childCategory) => (
<li key={childCategory.id}>
{childCategory.name}
</li>
)
)}
</ul>
</>
)}
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
The `product_category` field in the response has a `category_children` field. It's an array of [product category objects](!api!/store/#product-categories_productcategory_schema).
@@ -0,0 +1,13 @@
import { ChildDocs } from "docs-ui"
export const metadata = {
title: `Product Categories in Storefront`,
}
# {metadata.title}
Products can be categorized into categories, such as Shirts or Shoes.
Customers can browse those categories to narrow down and find the products they're looking for.
<ChildDocs type="item" onlyTopLevel={true} />
@@ -0,0 +1,143 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve a Category's Products in Storefront`,
}
# {metadata.title}
To retrieve a category's products in the storefront, send a request to the [List Products API route](!api!/store#products_getproducts) passing it the `category_id` query parameter:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["3", "", "Pass the category ID as a query parameter."],
["9", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
]
```ts highlights={fetchHighlights}
const searchParams = new URLSearchParams({
// other query params...
"category_id[]": categoryId
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products, count }) => {
// use products...
console.log(products)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["13", "params: { categoryId }", "This is based on Next.js which passes the path parameters as a prop."],
["33", "", "Pass the category ID as a query parameter."],
["36"], ["37"], ["38"],
["39", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["40"], ["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"], ["48"], ["49"], ["50"], ["51"], ["52"], ["53"], ["54"],
["55"], ["56"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
categoryId: string
}
}
export default function CategoryProducts({
params: { categoryId }
}: Params) {
const [loading, setLoading] = useState(true)
const [products, setProducts] = useState<
HttpTypes.StoreProduct[]
>([])
const limit = 20
const [currentPage, setCurrentPage] = useState(1)
const [hasMorePages, setHasMorePages] = useState(false)
useEffect(() => {
if (!loading) {
return
}
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
"category_id[]": categoryId
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products: dataProducts, count }) => {
setProducts((prev) => {
if (prev.length > offset) {
// products already added because the same request has already been sent
return prev
}
return [
...prev,
...dataProducts
]
})
setHasMorePages(count > limit * currentPage)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && products.length === 0 && (
<span>No products found for category.</span>
)}
{!loading && products.length > 0 && (
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
{!loading && hasMorePages && (
<button
onClick={() => {
setCurrentPage((prev) => prev + 1)
setLoading(true)
}}
disabled={loading}
>
Load More
</button>
)}
</div>
);
}
```
</CodeTab>
</CodeTabs>
{/* TODO add a link to product object in API reference (once available). */}
The response has a `products` field, which is an array of products.
@@ -0,0 +1,194 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve a Category in Storefront`,
}
# {metadata.title}
In this document, learn how to retrieve a product category and its details in the storefront.
There are two ways to retrieve a product category:
- Retrieve a category by its ID.
- Retrieve a category by its `handle` field. This is useful if you're creating human-readable URLs in your storefront.
## Retrieve a Product Category by ID
To retrieve a product category by its ID, send a request to the [Get a Product Category API route](!api!/store#product-categories_getproductcategoriesid):
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["1", "id", "The product category's ID."],
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/product-categories/${id}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_category }) => {
// use the product...
console.log(product)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["12", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["23"], ["24"], ["25"], ["26"],
["27"], ["28"], ["29"], ["30"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
id: string
}
}
export default function Category({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [category, setCategory] = useState<
HttpTypes.StoreProductCategory | undefined
>()
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/product-categories/${id}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_category }) => {
setCategory(product_category)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{category && (
<>
<h1>{category.name}</h1>
<p>{category.description}</p>
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
The response has a `product_category` field, which is a [product category object](!api!/store/#product-categories_productcategory_schema).
---
## Retrieve a Product Category by Handle
To retrieve a product by its handle, send a request to the [List Product Categories API route](!api!/store#product-categories_getproductcategories) passing it the `handle` query parameter:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const handleFetchHighlights = [
["1", "handle", "The product category's handle."],
]
```ts highlights={handleFetchHighlights}
fetch(`http://localhost:9000/store/product-categories?handle=${
handle
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_categories }) => {
if (!product_categories.length) {
// product categories with the specified handle doesn't exist
return
}
// use the product category...
console.log(product_categories[0])
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const handleHighlights = [
["12", "{ params: { handle } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["23"], ["24"], ["25"], ["26"],
["27"], ["28"], ["29"], ["30"],
["31"], ["32"], ["33"], ["34"]
]
```tsx highlights={handleHighlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
handle: string
}
}
export default function Category({ params: { handle } }: Params) {
const [loading, setLoading] = useState(true)
const [category, setCategory] = useState<
HttpTypes.StoreProductCategory | undefined
>()
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/product-categories?handle=${
handle
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ product_categories }) => {
if (product_categories.length) {
setCategory(product_categories[0])
}
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && !category && (
<span>Product category not found</span>
)}
{category && (
<>
<h1>{category.name}</h1>
<p>{category.description}</p>
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
@@ -0,0 +1,213 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `List Product Collections in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to list product collections in the storefront, including paginating and filtering them.
## List Product Collections
To retrieve the list of product collections, send a request to the [List Product Collections API route](!api!/store#collections_getcollections):
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(`http://localhost:9000/store/collections`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collections }) => {
// use collections...
console.log(collections)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["17"], ["18"], ["19"], ["20"],
["21"], ["22"], ["23"], ["24"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react";
import { HttpTypes } from "@medusajs/types"
export default function Collections() {
const [loading, setLoading] = useState(true)
const [collections, setCollections] = useState<
HttpTypes.StoreCollection[]
>([])
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/collections`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collections: dataCollections }) => {
setCollections(dataCollections)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && collections.length === 0 && (
<span>No product collections found.</span>
)}
{!loading && collections.length > 0 && (
<ul>
{collections.map((collection) => (
<li key={collection.id}>{collection.title}</li>
))}
</ul>
)}
</div>
);
}
```
</CodeTab>
</CodeTabs>
{/* TODO add link to product colleciton object */}
The response has a `collections` field, which is an array of product collections.
---
## Paginate Product Collections
To paginate product collections, pass the following query parameters:
- `limit`: The number of product collections to return in the request.
- `offset`: The number of product collections to skip before the returned product collections. You can calculate this by multiplying the current page with the limit.
The response object returns a `count` field, which is the total count of product collections. Use it to determine whether there are more product collections that can be loaded.
For example:
export const paginateHighlights = [
["20", "offset", "Calculate the number of product collections to skip based on the current page and limit."],
["28", "searchParams.toString()", "Pass the pagination parameters in the query."],
["33", "count", "The total number of product collections in the Medusa application."],
["45", "setHasMorePages", "Set whether there are more pages based on the total count."],
["64", "button", "Show a button to load more product collections if there are more pages."]
]
```tsx highlights={paginateHighlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react";
import { HttpTypes } from "@medusajs/types"
export default function Collections() {
const [loading, setLoading] = useState(true)
const [collections, setCollections] = useState<
HttpTypes.StoreCollection[]
>([])
const limit = 20
const [currentPage, setCurrentPage] = useState(1)
const [hasMorePages, setHasMorePages] = useState(false)
useEffect(() => {
if (!loading) {
return
}
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
})
fetch(`http://localhost:9000/store/collections?${
searchParams.toString()
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collections: dataCollections, count }) => {
setCollections((prev) => {
if (prev.length > offset) {
// product collections already added because
// the same request has already been sent
return prev
}
return [
...prev,
...dataCollections
]
})
setHasMorePages(count > limit * currentPage)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && collections.length === 0 && (
<span>No product collections found.</span>
)}
{!loading && collections.length > 0 && (
<ul>
{collections.map((collection) => (
<li key={collection.id}>{collection.title}</li>
))}
</ul>
)}
{!loading && hasMorePages && (
<button
onClick={() => {
setCurrentPage((prev) => prev + 1)
setLoading(true)
}}
disabled={loading}
>
Load More
</button>
)}
</div>
);
}
```
---
## Filter Collections
The List Product Collections API route accepts query parameters to filter the collections by title, handle, and more.
Refer to the [API reference](!api!/store#collections_getcollections) for the list of accepted query parameters.
For example:
```ts
const searchParams = new URLSearchParams({
title: "test"
})
fetch(`http://localhost:9000/store/collections?${
searchParams.toString()
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collections, count }) => {
// TODO set collections...
})
```
@@ -0,0 +1,13 @@
import { ChildDocs } from "docs-ui"
export const metadata = {
title: `Product Collections in Storefront`,
}
# {metadata.title}
Products can be organized into collections, such as Summer Collection.
Customers can browse those collections and the products in them.
<ChildDocs type="item" onlyTopLevel={true} />
@@ -0,0 +1,145 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve a Collection's Products in Storefront`,
}
# {metadata.title}
To retrieve a collection's products in the storefront, send a request to the [List Products API route](!api!/store#products_getproducts) passing it the `collection_id` query parameter:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["3", "", "Pass the collection ID as a query parameter."],
["9", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
]
```ts highlights={fetchHighlights}
const searchParams = new URLSearchParams({
// other query params...
"collection_id[]": collectionId
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products, count }) => {
// use products...
console.log(products)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["13", "params: { collectionId }", "This is based on Next.js which passes the path parameters as a prop."],
["33", "", "Pass the collection ID as a query parameter."],
["36"], ["37"], ["38"],
["39", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["40"], ["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"], ["48"], ["49"], ["50"], ["51"], ["52"], ["53"], ["54"],
["55"], ["56"], ["57"], ["58"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
collectionId: string
}
}
export default function CollectionProducts({
params: { collectionId }
}: Params) {
const [loading, setLoading] = useState(true)
const [products, setProducts] = useState<
HttpTypes.StoreProduct[]
>([])
const limit = 20
const [currentPage, setCurrentPage] = useState(1)
const [hasMorePages, setHasMorePages] = useState(false)
useEffect(() => {
if (!loading) {
return
}
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
"collection_id[]": collectionId
})
fetch(`http://localhost:9000/store/products?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products: dataProducts, count }) => {
setProducts((prev) => {
if (prev.length > offset) {
// products already added because the same request has already been sent
return prev
}
return [
...prev,
...dataProducts
]
})
setHasMorePages(count > limit * currentPage)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && products.length === 0 && (
<span>No products found for collection.</span>
)}
{!loading && products.length > 0 && (
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
{!loading && hasMorePages && (
<button
onClick={() => {
setCurrentPage((prev) => prev + 1)
setLoading(true)
}}
disabled={loading}
>
Load More
</button>
)}
</div>
);
}
```
</CodeTab>
</CodeTabs>
{/* TODO add a link to product object in API reference (once available). */}
The response has a `products` field, which is an array of products.
@@ -0,0 +1,195 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve a Collection in Storefront`,
}
# {metadata.title}
In this document, learn how to retrieve a product collection and its details in the storefront.
There are two ways to retrieve a product collection:
- Retrieve a collection by its ID.
- Retrieve a collection by its `handle` field. This is useful if you're creating human-readable URLs in your storefront.
## Retrieve a Product Collection by ID
To retrieve a product collection by its ID, send a request to the [Get a Collection API route](!api!/store#collections_getcollectionsid):
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["1", "id", "The product collection's ID."],
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/collections/${id}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collection }) => {
// use the collection...
console.log(collection)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["12", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["23"], ["24"], ["25"], ["26"],
["27"], ["28"], ["29"], ["30"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
id: string
}
}
export default function Collection({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [collection, setCollection] = useState<
HttpTypes.StoreCollection | undefined
>()
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/collections/${id}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collection: dataCollection }) => {
setCollection(dataCollection)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{collection && (
<>
<h1>{collection.title}</h1>
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
{/* TODO add link to product colleciton object */}
The response has a `collection` field, which is a product collection object.
---
## Retrieve a Product Collection by Handle
To retrieve a product by its handle, send a request to the [List Product Collections API route](!api!/store#collections_getcollections) passing it the `handle` query parameter:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const handleFetchHighlights = [
["2", "handle", "The collection's handle."],
]
```ts highlights={handleFetchHighlights}
fetch(`http://localhost:9000/store/collections?handle=${
handle
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collections }) => {
if (!collections.length) {
// collections with the specified handle doesn't exist
return
}
// use the collection...
console.log(collections[0])
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const handleHighlights = [
["13", "{ params: { handle } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["25"], ["26"], ["27"], ["28"],
["29"], ["30"], ["31"], ["32"], ["33"], ["34"], ["35"], ["36"]
]
```tsx highlights={handleHighlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
handle: string
}
}
export default function Collection(
{ params: { handle } }: Params
) {
const [loading, setLoading] = useState(true)
const [collection, setCollection] = useState<
HttpTypes.StoreCollection | undefined
>()
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/collections?handle=${
handle
}`, {
credentials: "include",
})
.then((res) => res.json())
.then(({ collections }) => {
if (collections.length) {
setCollection(collections[0])
}
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && !collection && (
<span>Product collection not found</span>
)}
{collection && (
<>
<h1>{collection.title}</h1>
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
@@ -0,0 +1,222 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `List Products in Storefront`,
}
# {metadata.title}
In this document, learn how to list, paginate, and filter products in the storefront.
## List Products
To list products, send a request to the [List Products API route](!api!/store#products_getproducts):
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["4", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/products`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then((data) => {
// use products...
console.log(data.products)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["17"], ["18"], ["19"],
["20", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["21"], ["22"], ["23"], ["24"], ["25"], ["26"], ["27"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react";
import { HttpTypes } from "@medusajs/types"
export default function Products() {
const [loading, setLoading] = useState(true)
const [products, setProducts] = useState<
HttpTypes.StoreProduct[]
>([])
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/products`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then((data) => {
setProducts(data.products)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && products.length === 0 && <span>No products found.</span>}
{!loading && products.length > 0 && (
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
</div>
);
}
```
</CodeTab>
</CodeTabs>
{/* TODO add a link to product object in API reference (once available). */}
The response has a `products` field, which is an array of products.
---
## Paginate Products
To paginate products, pass the following query parameters:
- `limit`: The number of products to return in the request.
- `offset`: The number of products to skip before the returned products. You can calculate this by multiplying the current page with the limit.
The response object returns a `count` field, which is the total count of products. Use it to determine whether there are more products that can be loaded.
For example:
export const paginateHighlights = [
["20", "offset", "Calculate the number of products to skip based on the current page and limit."],
["27", "searchParams.toString()", "Pass the pagination parameters in the query."],
["34", "count", "The total number of products in the Medusa application."],
["45", "setHasMorePages", "Set whether there are more pages based on the total count."],
["62", "button", "Show a button to load more products if there are more pages."]
]
```tsx highlights={paginateHighlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react";
import { HttpTypes } from "@medusajs/types"
export default function Products() {
const [loading, setLoading] = useState(true)
const [products, setProducts] = useState<
HttpTypes.StoreProduct[]
>([])
const limit = 20
const [currentPage, setCurrentPage] = useState(1)
const [hasMorePages, setHasMorePages] = useState(false)
useEffect(() => {
if (!loading) {
return
}
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products: dataProducts, count }) => {
setProducts((prev) => {
if (prev.length > offset) {
// products already added because the same request has already been sent
return prev
}
return [
...prev,
...dataProducts
]
})
setHasMorePages(count > limit * currentPage)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && products.length === 0 && <span>No products found.</span>}
{!loading && products.length > 0 && (
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)}
{!loading && hasMorePages && (
<button
onClick={() => {
setCurrentPage((prev) => prev + 1)
setLoading(true)
}}
disabled={loading}
>
Load More
</button>
)}
</div>
);
}
```
---
## Filter Products
The List Products API route accepts query parameters to filter products by title, category, whether they're a gift card, and more.
Refer to the [API reference](!api!/store#products_getproducts) for the list of accepted query parameters.
For example, to run a query on the products:
```ts
const searchParams = new URLSearchParams({
// other params...
q: "Shirt"
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products: dataProducts, count }) => {
// TODO set products...
})
```
@@ -0,0 +1,16 @@
import { ChildDocs } from "docs-ui"
export const metadata = {
title: `Products in Storefront`,
}
# {metadata.title}
Customers browse products in the storefront before purchasing.
Some features essential to implement in your storefront are:
- Show customers products and allow them to filter these products.
- Show products organized by category or collection.
<ChildDocs type="item" onlyTopLevel={true} />
@@ -0,0 +1,389 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve Product Variant's Prices in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to show a product's variants' prices in the storefront, including handling sale prices.
## Pricing Parameters
When you retrieve products either with the [List Products](!api!/store#products_getproducts) or [Retrieve Products](!api!/store#products_getproductsid) API routes, you must pass at least one of the following query parameters to retrieve the correct product variant price:
- `region_id`: The ID of the customer's region.
- `currency_code`: The currency code the customer is viewing prices in.
- `customer_id`: The ID of the customer viewing the prices. This is useful when you have a promotion or price list overriding a product's price for specific customer groups.
- `customer_group_id`: The ID of the group of the customer viewing the prices. This is useful when you have a promotion or price list overriding a product's price for specific customer groups.
Also, you must include in the beginning of the `fields` query parameter the value `*variants.calculated_price`.
<Note type="check">
The examples in this guide only pass the `region_id` query parameter. Learn how to store and retrieve the customer's region in the [Regions guides](../../regions/page.mdx).
</Note>
---
## Show Product Variant's Price
The following React-based storefront example retrieves the product's price based on the selected variant:
export const priceHighlights = [
["5", "useRegion", "The `useRegion` hook is implemented in the Region React Context guide."],
["13", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["19", "region", "Access the region using the `useRegion` hook."],
["26", "queryParams", "Build the pricing query parameters."],
["58", "formatPrice", "A utility function to format an amount with its currency."],
["59", `"en-US"`, "If you use a different locale change it here."],
["66", "variantPrice", "Assign the variant to compute its price, which is either the selected or cheapest variant."],
["68", "selectedVariant", "Use the selected variant for pricing."],
["71", "", "If there isn't a selected variant, retrieve the variant with the cheapest price."],
["79", "price", "Compute the price of the selected or cheapest variant."],
["123", "", "If there's a computed price but no selected variant, show a `From` prefix to the price."],
["124", "price", "Display the computed price."]
]
```tsx highlights={priceHighlights}
"use client" // include with Next.js 13+
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})
const { region } = useRegion
useEffect(() => {
if (!loading) {
return
}
const queryParams = new URLSearchParams({
fields: `*variants.calculated_price`,
region_id: region.id
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
const selectedVariant = useMemo(() => {
if (
!product?.variants ||
!product.options ||
Object.keys(selectedOptions).length !== product.options?.length
) {
return
}
return product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]
))
}, [selectedOptions, product])
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
const variantPrice = useMemo(() => {
if (selectedVariant) {
return selectedVariant
}
return product?.variants?.sort((a: any, b: any) => {
return (
a.calculated_price.calculated_amount -
b.calculated_price.calculated_amount
)
})[0]
}, [selectedVariant, product])
const price = useMemo(() => {
if (!variantPrice) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.calculated_amount)
}, [variantPrice])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
{option.values?.map((optionValue) => (
<button
key={optionValue.id}
onClick={() => {
setSelectedOptions((prev) => {
return {
...prev,
[option.id!]: optionValue.value!
}
})
}}
>
{optionValue.value}
</button>
))}
</li>
))}
</ul>
)}
{selectedVariant && (
<span>Selected Variant: {selectedVariant.id}</span>
)}
{price && (
<span>
{!selectedVariant && "From: "}
{price}
</span>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
In the example above, you:
- Use the `useRegion` hook defined in the previous [Region React Context guide](../../regions/context/page.mdx).
- Pass the pricing query parameters to the request retrieving the product. This retrieves for every variant a new `calculated_price` field holding details about the variant's price.
- Choose the variant to show its price:
- If there's a selected variant, choose it.
- If there isn't a selected variant, retrieve and choose the variant with the cheapest price.
- Format the price based on the chosen variant in the previous step. The variant's `calculated_price.calculated_amount` field is used.
- Display the formatted price to the customer. If there isn't a select variant, show a `From` label to indicate that the price shown is the cheapest.
### Price Formatting
To format the price, use JavaScript's [NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) utility. You pass it the amount and the currency code (which you retrieve from the selected region):
```ts
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
```
---
## Show Product Variant's Sale Price
To check if a product variant's price is a sale price, check whether the variant's `calculated_price.calculated_price.price_list_type` field is equal to `sale`.
In that case, the original price is in the variant's `calculated_price.original_amount` field.
For example, in a React-based storefront:
export const saleHighlights = [
["5", "useRegion", "The `useRegion` hook is implemented in the Region React Context guide."],
["13", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["19", "region", "Access the region using the `useRegion` hook."],
["88", "isSale", "Determine whether the price is a sale price based on the value of the variant's `calculated_price.calculated_price.price_list_type` field."],
["97", "originalPrice", "Retrieve the original price from the variant's `calculated_price.original_amount` field if the price is a sale price."],
["143", "", "If the price is a sale price, show that to the customer along with the original price."]
]
```tsx highlights={saleHighlights}
"use client" // include with Next.js 13+
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})
const { region } = useRegion()
useEffect(() => {
if (!loading) {
return
}
const queryParams = new URLSearchParams({
fields: `*variants.calculated_price`,
region_id: region.id
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
const selectedVariant = useMemo(() => {
if (
!product?.variants ||
!product.options ||
Object.keys(selectedOptions).length !== product.options?.length
) {
return
}
return product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]
))
}, [selectedOptions, product])
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
const variantPrice = useMemo(() => {
if (selectedVariant) {
return selectedVariant
}
return product?.variants?.sort((a: any, b: any) => {
return (
a.calculated_price.calculated_amount -
b.calculated_price.calculated_amount
)
})[0]
}, [selectedVariant, product])
const price = useMemo(() => {
if (!variantPrice) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.calculated_amount)
}, [variantPrice])
const isSale = useMemo(() => {
if (!variantPrice) {
return false
}
// @ts-ignore
return variantPrice.calculated_price.calculated_price.price_list_type === "sale"
}, [variantPrice])
const originalPrice = useMemo(() => {
if (!isSale) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.original_amount)
}, [isSale, variantPrice])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
{option.values?.map((optionValue) => (
<button
key={optionValue.id}
onClick={() => {
setSelectedOptions((prev) => {
return {
...prev,
[option.id!]: optionValue.value!
}
})
}}
>
{optionValue.value}
</button>
))}
</li>
))}
</ul>
)}
{selectedVariant && (
<span>Selected Variant: {selectedVariant.id}</span>
)}
{price && (
<span>
{!selectedVariant && "From: "}
{price}
{isSale && `SALE - Original Price: ${originalPrice}`}
</span>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
In this example, you:
- Define an `isSale` memo variable that determines whether the chosen variant's price is a sale price. You do that by checking if the value of the variant's `calculated_price.calculated_price.price_list_type` field is `sale`.
- Define an `originalPrice` memo variable that, if `isSale` is enabled, has the formatted original price of the chosen variant. The variant's original price is in the `calculated_price.original_amount` field.
- If `isSale` is enabled, show a message to the customer indicating that this product is on sale along with the original price.
@@ -0,0 +1,238 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve a Product in Storefront`,
}
# {metadata.title}
In this document, learn how to retrieve a product and its details in the storefront.
There are two ways to retrieve a product:
- Retrieve a product by its ID.
- Retrieve a product by its `handle` field. This is useful if you're creating human-readable URLs in your storefront.
## Retrieve a Product by ID
To retrieve a product by its ID, send a request to the [Retrieve Product API route](!api!/store#products_getproductsid):
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["1", "id", "The product's ID."],
["4", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/products/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ product }) => {
// use the product...
console.log(product)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["12", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."]
["23"], ["24"], ["25"],
["26", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["27"], ["28"], ["29"], ["30"], ["31"], ["32"], ["33"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/products/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
<ul>
{option.values?.map((optionValue) => (
<li key={optionValue.id}>{optionValue.value}</li>
))}
</ul>
</li>
))}
</ul>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
{/* TODO add a link to product object in API reference (once available). */}
The response has a `product` field, which is a product object.
---
## Retrieve a Product by Handle
To retrieve a product by its handle, send a request to the [List Products API route](!api!/store#products_getproducts) passing it the `handle` query parameter:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const handleFetchHighlights = [
["1", "id", "The product's ID."],
["4", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
]
```ts highlights={handleFetchHighlights}
fetch(`http://localhost:9000/store/products?handle=${handle}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products }) => {
if (!products.length) {
// product with the specified handle doesn't exist
return
}
// use the product...
console.log(products[0])
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const handleHighlights = [
["12", "{ params: { handle } }: Params", "This is based on Next.js which passes the path parameters as a prop."]
["23"], ["24"], ["25"],
["26", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["27"], ["28"], ["29"], ["30"], ["31"], ["32"], ["33"], ["34"], ["35"]
]
```tsx highlights={handleHighlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
handle: string
}
}
export default function Product({ params: { handle } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/products?handle=${handle}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ products }) => {
if (products.length) {
setProduct(products[0])
}
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{!loading && !product && <span>Product not found</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
<ul>
{option.values?.map((optionValue) => (
<li key={optionValue.id}>{optionValue.value}</li>
))}
</ul>
</li>
))}
</ul>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
@@ -0,0 +1,132 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Select Product Variants in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to select a product variant to be added to the cart in the storefront.
<Note>
{/* TODO add the link once available. */}
The add-to-cart functionality is explained in the Cart's guides.
</Note>
If a product has different options and variants for those options, the customer has to choose the options when adding the product to the cart.
Since a variant is a combination of the product options' values (for example, size `S` and color `Blue`), you find the variant based on the chosen option values.
For example, in a React-based storefront:
export const highlights = [
["12", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["17", "selectedOptions", "Store the options the customer selects."],
["37", "selectedVariant", "Compute the selected variant based on the chosen option values combinations."],
["66", "setSelectedOptions", "When an option's value is selected, add it to the selected options, which re-computes the selected variant."],
["81", "selectedVariant", "Show the selected variant's ID, if computed."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})
useEffect(() => {
if (!loading) {
return
}
fetch(`http://localhost:9000/store/products/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp"
}
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
const selectedVariant = useMemo(() => {
if (
!product?.variants ||
!product.options ||
Object.keys(selectedOptions).length !== product.options?.length
) {
return
}
return product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]
))
}, [selectedOptions, product])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
{option.values?.map((optionValue) => (
<button
key={optionValue.id}
onClick={() => {
setSelectedOptions((prev) => {
return {
...prev,
[option.id!]: optionValue.value!
}
})
}}
>
{optionValue.value}
</button>
))}
</li>
))}
</ul>
)}
{selectedVariant && (
<span>Selected Variant: {selectedVariant.id}</span>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
In this example, you:
- Store the selected options in the `selectedOptions` state variable. It's an object whose keys are options' ID, and values are the selected value of that option.
- Compute the selected variable whenever the selected option is changed. When the customer chooses a value for all options, you find a product variant that has the same chosen option-value combinations.
- Change the `selectedOptions`'s value whenever the customer clicks on an option value.
- Show the ID of the selected variant when it's found.