+ );
+ }
+ ```
+
+
+
+
+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 (
+
+ );
+ }
+ ```
+
+
+
+
+{/* 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 (
+
+ );
+ }
+ ```
+
+
+
+
+{/* 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 (
+
+ );
+}
+```
+
+---
+
+## 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...
+})
+```
diff --git a/www/apps/resources/app/storefront-development/products/page.mdx b/www/apps/resources/app/storefront-development/products/page.mdx
new file mode 100644
index 0000000000..98df999245
--- /dev/null
+++ b/www/apps/resources/app/storefront-development/products/page.mdx
@@ -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.
+
+
\ No newline at end of file
diff --git a/www/apps/resources/app/storefront-development/products/price/page.mdx b/www/apps/resources/app/storefront-development/products/price/page.mdx
new file mode 100644
index 0000000000..9d96974391
--- /dev/null
+++ b/www/apps/resources/app/storefront-development/products/price/page.mdx
@@ -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`.
+
+
+
+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).
+
+
+
+---
+
+## 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>({})
+ 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 (
+
+ )
+}
+```
+
+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>({})
+ 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 (
+
+ )
+}
+```
+
+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.
diff --git a/www/apps/resources/app/storefront-development/products/retrieve/page.mdx b/www/apps/resources/app/storefront-development/products/retrieve/page.mdx
new file mode 100644
index 0000000000..78237aa82e
--- /dev/null
+++ b/www/apps/resources/app/storefront-development/products/retrieve/page.mdx
@@ -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):
+
+
+
+
+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)
+ })
+ ```
+
+
+
+
+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 (
+