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:
@@ -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...
|
||||
})
|
||||
```
|
||||
+120
@@ -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.
|
||||
Reference in New Issue
Block a user