From ee6bdd0ab08808d060561062d528809cb8119b7c Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 11 Jun 2024 20:55:56 +0300 Subject: [PATCH] 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 --- .../products/categories/list/page.mdx | 211 ++++++++++ .../categories/nested-categories/page.mdx | 120 ++++++ .../products/categories/page.mdx | 13 + .../products/categories/products/page.mdx | 143 +++++++ .../products/categories/retrieve/page.mdx | 194 +++++++++ .../products/collections/list/page.mdx | 213 ++++++++++ .../products/collections/page.mdx | 13 + .../products/collections/products/page.mdx | 145 +++++++ .../products/collections/retrieve/page.mdx | 195 +++++++++ .../products/list/page.mdx | 222 ++++++++++ .../storefront-development/products/page.mdx | 16 + .../products/price/page.mdx | 389 ++++++++++++++++++ .../products/retrieve/page.mdx | 238 +++++++++++ .../products/variants/page.mdx | 132 ++++++ www/apps/resources/generated/files-map.mjs | 56 +++ www/apps/resources/generated/sidebar.mjs | 101 +++++ www/apps/resources/sidebar.mjs | 62 +++ 17 files changed, 2463 insertions(+) create mode 100644 www/apps/resources/app/storefront-development/products/categories/list/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/categories/nested-categories/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/categories/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/categories/products/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/categories/retrieve/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/collections/list/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/collections/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/collections/products/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/collections/retrieve/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/list/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/price/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/retrieve/page.mdx create mode 100644 www/apps/resources/app/storefront-development/products/variants/page.mdx diff --git a/www/apps/resources/app/storefront-development/products/categories/list/page.mdx b/www/apps/resources/app/storefront-development/products/categories/list/page.mdx new file mode 100644 index 0000000000..a0c2e1aa16 --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/categories/list/page.mdx @@ -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): + + + + + ```ts + fetch(`http://localhost:9000/store/product-categories`, { + credentials: "include", + }) + .then((res) => res.json()) + .then(({ product_categories }) => { + // use categories... + console.log(product_categories) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && categories.length === 0 && ( + No product categories found. + )} + {!loading && categories.length > 0 && ( +
    + {categories.map((category) => ( +
  • {category.name}
  • + ))} +
+ )} +
+ ); + } + ``` + +
+
+ +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 ( +
+ {loading && Loading...} + {!loading && categories.length === 0 && ( + No product categories found. + )} + {!loading && categories.length > 0 && ( + + )} + {!loading && hasMorePages && ( + + )} +
+ ); +} +``` + +--- + +## 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... +}) +``` diff --git a/www/apps/resources/app/storefront-development/products/categories/nested-categories/page.mdx b/www/apps/resources/app/storefront-development/products/categories/nested-categories/page.mdx new file mode 100644 index 0000000000..5c2dcd6473 --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/categories/nested-categories/page.mdx @@ -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: + + + + +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) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {category && ( + <> +

{category.name}

+

{category.description}

+ {(category.category_children?.length || 0) > 0 && ( + <> + Child Categories +
    + {category.category_children!.map( + (childCategory) => ( +
  • + {childCategory.name} +
  • + ) + )} +
+ + )} + + )} +
+ ) + } + ``` + +
+
+ +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). diff --git a/www/apps/resources/app/storefront-development/products/categories/page.mdx b/www/apps/resources/app/storefront-development/products/categories/page.mdx new file mode 100644 index 0000000000..979e9f9e2e --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/categories/page.mdx @@ -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. + + \ No newline at end of file diff --git a/www/apps/resources/app/storefront-development/products/categories/products/page.mdx b/www/apps/resources/app/storefront-development/products/categories/products/page.mdx new file mode 100644 index 0000000000..095d3de8e6 --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/categories/products/page.mdx @@ -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: + + + + +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) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && products.length === 0 && ( + No products found for category. + )} + {!loading && products.length > 0 && ( +
    + {products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} + {!loading && hasMorePages && ( + + )} +
+ ); + } + ``` + +
+
+ +{/* TODO add a link to product object in API reference (once available). */} + +The response has a `products` field, which is an array of products. diff --git a/www/apps/resources/app/storefront-development/products/categories/retrieve/page.mdx b/www/apps/resources/app/storefront-development/products/categories/retrieve/page.mdx new file mode 100644 index 0000000000..dbd8e718c7 --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/categories/retrieve/page.mdx @@ -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): + + + + +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) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {category && ( + <> +

{category.name}

+

{category.description}

+ + )} +
+ ) + } + ``` + +
+
+ +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: + + + + +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]) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && !category && ( + Product category not found + )} + {category && ( + <> +

{category.name}

+

{category.description}

+ + )} +
+ ) + } + ``` + +
+
diff --git a/www/apps/resources/app/storefront-development/products/collections/list/page.mdx b/www/apps/resources/app/storefront-development/products/collections/list/page.mdx new file mode 100644 index 0000000000..745446d48a --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/collections/list/page.mdx @@ -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): + + + + + ```ts + fetch(`http://localhost:9000/store/collections`, { + credentials: "include", + }) + .then((res) => res.json()) + .then(({ collections }) => { + // use collections... + console.log(collections) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && collections.length === 0 && ( + No product collections found. + )} + {!loading && collections.length > 0 && ( +
    + {collections.map((collection) => ( +
  • {collection.title}
  • + ))} +
+ )} +
+ ); + } + ``` + +
+
+ +{/* 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 ( +
+ {loading && Loading...} + {!loading && collections.length === 0 && ( + No product collections found. + )} + {!loading && collections.length > 0 && ( +
    + {collections.map((collection) => ( +
  • {collection.title}
  • + ))} +
+ )} + {!loading && hasMorePages && ( + + )} +
+ ); +} +``` + +--- + +## 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... +}) +``` diff --git a/www/apps/resources/app/storefront-development/products/collections/page.mdx b/www/apps/resources/app/storefront-development/products/collections/page.mdx new file mode 100644 index 0000000000..419b20128b --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/collections/page.mdx @@ -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. + + \ No newline at end of file diff --git a/www/apps/resources/app/storefront-development/products/collections/products/page.mdx b/www/apps/resources/app/storefront-development/products/collections/products/page.mdx new file mode 100644 index 0000000000..7463662ac8 --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/collections/products/page.mdx @@ -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: + + + + +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) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && products.length === 0 && ( + No products found for collection. + )} + {!loading && products.length > 0 && ( +
    + {products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} + {!loading && hasMorePages && ( + + )} +
+ ); + } + ``` + +
+
+ +{/* TODO add a link to product object in API reference (once available). */} + +The response has a `products` field, which is an array of products. diff --git a/www/apps/resources/app/storefront-development/products/collections/retrieve/page.mdx b/www/apps/resources/app/storefront-development/products/collections/retrieve/page.mdx new file mode 100644 index 0000000000..1aad9a48da --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/collections/retrieve/page.mdx @@ -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): + + + + +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) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {collection && ( + <> +

{collection.title}

+ + )} +
+ ) + } + ``` + +
+
+ +{/* 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: + + + + +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]) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && !collection && ( + Product collection not found + )} + {collection && ( + <> +

{collection.title}

+ + )} +
+ ) + } + ``` + +
+
diff --git a/www/apps/resources/app/storefront-development/products/list/page.mdx b/www/apps/resources/app/storefront-development/products/list/page.mdx new file mode 100644 index 0000000000..6d08844d51 --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/list/page.mdx @@ -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): + + + + +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) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && products.length === 0 && No products found.} + {!loading && products.length > 0 && ( +
    + {products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ); + } + ``` + +
+
+ +{/* 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 ( +
+ {loading && Loading...} + {!loading && products.length === 0 && No products found.} + {!loading && products.length > 0 && ( +
    + {products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} + {!loading && hasMorePages && ( + + )} +
+ ); +} +``` + +--- + +## 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 ( +
+ {loading && Loading...} + {product && ( + <> +

{product.title}

+ {(product.options?.length || 0) > 0 && ( +
    + {product.options!.map((option) => ( +
  • + {option.title} + {option.values?.map((optionValue) => ( + + ))} +
  • + ))} +
+ )} + {selectedVariant && ( + Selected Variant: {selectedVariant.id} + )} + {price && ( + + {!selectedVariant && "From: "} + {price} + + )} + {product.images?.map((image) => ( + + ))} + + )} +
+ ) +} +``` + +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 ( +
+ {loading && Loading...} + {product && ( + <> +

{product.title}

+ {(product.options?.length || 0) > 0 && ( +
    + {product.options!.map((option) => ( +
  • + {option.title} + {option.values?.map((optionValue) => ( + + ))} +
  • + ))} +
+ )} + {selectedVariant && ( + Selected Variant: {selectedVariant.id} + )} + {price && ( + + {!selectedVariant && "From: "} + {price} + {isSale && `SALE - Original Price: ${originalPrice}`} + + )} + {product.images?.map((image) => ( + + ))} + + )} +
+ ) +} +``` + +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 ( +
+ {loading && Loading...} + {product && ( + <> +

{product.title}

+ {(product.options?.length || 0) > 0 && ( +
    + {product.options!.map((option) => ( +
  • + {option.title} +
      + {option.values?.map((optionValue) => ( +
    • {optionValue.value}
    • + ))} +
    +
  • + ))} +
+ )} + {product.images?.map((image) => ( + + ))} + + )} +
+ ) + } + ``` + +
+
+ +{/* 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: + + + + +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]) + }) + ``` + + + + +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 ( +
+ {loading && Loading...} + {!loading && !product && Product not found} + {product && ( + <> +

{product.title}

+ {(product.options?.length || 0) > 0 && ( +
    + {product.options!.map((option) => ( +
  • + {option.title} +
      + {option.values?.map((optionValue) => ( +
    • {optionValue.value}
    • + ))} +
    +
  • + ))} +
+ )} + {product.images?.map((image) => ( + + ))} + + )} +
+ ) + } + ``` + +
+
+ diff --git a/www/apps/resources/app/storefront-development/products/variants/page.mdx b/www/apps/resources/app/storefront-development/products/variants/page.mdx new file mode 100644 index 0000000000..faaa03c20a --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/variants/page.mdx @@ -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. + + + +{/* TODO add the link once available. */} + +The add-to-cart functionality is explained in the Cart's guides. + + + +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>({}) + + 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 ( +
+ {loading && Loading...} + {product && ( + <> +

{product.title}

+ {(product.options?.length || 0) > 0 && ( +
    + {product.options!.map((option) => ( +
  • + {option.title} + {option.values?.map((optionValue) => ( + + ))} +
  • + ))} +
+ )} + {selectedVariant && ( + Selected Variant: {selectedVariant.id} + )} + {product.images?.map((image) => ( + + ))} + + )} +
+ ) +} +``` + +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. diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index bc1f22fc43..864acea129 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -899,6 +899,62 @@ export const filesMap = [ "filePath": "/www/apps/resources/app/storefront-development/page.mdx", "pathname": "/storefront-development" }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/categories/list/page.mdx", + "pathname": "/storefront-development/products/categories/list" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/categories/nested-categories/page.mdx", + "pathname": "/storefront-development/products/categories/nested-categories" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/categories/page.mdx", + "pathname": "/storefront-development/products/categories" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/categories/products/page.mdx", + "pathname": "/storefront-development/products/categories/products" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/categories/retrieve/page.mdx", + "pathname": "/storefront-development/products/categories/retrieve" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/collections/list/page.mdx", + "pathname": "/storefront-development/products/collections/list" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/collections/page.mdx", + "pathname": "/storefront-development/products/collections" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/collections/products/page.mdx", + "pathname": "/storefront-development/products/collections/products" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/collections/retrieve/page.mdx", + "pathname": "/storefront-development/products/collections/retrieve" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/list/page.mdx", + "pathname": "/storefront-development/products/list" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/page.mdx", + "pathname": "/storefront-development/products" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/price/page.mdx", + "pathname": "/storefront-development/products/price" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/retrieve/page.mdx", + "pathname": "/storefront-development/products/retrieve" + }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/variants/page.mdx", + "pathname": "/storefront-development/products/variants" + }, { "filePath": "/www/apps/resources/app/storefront-development/regions/context/page.mdx", "pathname": "/storefront-development/regions/context" diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index b7c2732296..879f96daf9 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -7182,6 +7182,107 @@ export const generatedSidebar = [ "children": [] } ] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products", + "title": "Products", + "children": [ + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/list", + "title": "List Products", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/retrieve", + "title": "Retrieve a Product", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/variants", + "title": "Select a Variant", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/price", + "title": "Retrieve Variant Prices", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/categories", + "title": "Categories", + "children": [ + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/categories/list", + "title": "List Categories", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/categories/retrieve", + "title": "Retrieve a Category", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/categories/products", + "title": "Retrieve a Category's Products", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/categories/nested-categories", + "title": "Retrieve Nested Categories", + "children": [] + } + ] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/collections", + "title": "Collections", + "children": [ + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/collections/list", + "title": "List Collections", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/collections/retrieve", + "title": "Retrieve a Collection", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/collections/products", + "title": "Retrieve a Collection's Products", + "children": [] + } + ] + } + ] } ] }, diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 149410f6ca..2743e812a3 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -1835,6 +1835,68 @@ export const sidebar = sidebarAttachHrefCommonOptions([ }, ], }, + { + path: "/storefront-development/products", + title: "Products", + children: [ + { + path: "/storefront-development/products/list", + title: "List Products", + }, + { + path: "/storefront-development/products/retrieve", + title: "Retrieve a Product", + }, + { + path: "/storefront-development/products/variants", + title: "Select a Variant", + }, + { + path: "/storefront-development/products/price", + title: "Retrieve Variant Prices", + }, + { + path: "/storefront-development/products/categories", + title: "Categories", + children: [ + { + path: "/storefront-development/products/categories/list", + title: "List Categories", + }, + { + path: "/storefront-development/products/categories/retrieve", + title: "Retrieve a Category", + }, + { + path: "/storefront-development/products/categories/products", + title: "Retrieve a Category's Products", + }, + { + path: "/storefront-development/products/categories/nested-categories", + title: "Retrieve Nested Categories", + }, + ], + }, + { + path: "/storefront-development/products/collections", + title: "Collections", + children: [ + { + path: "/storefront-development/products/collections/list", + title: "List Collections", + }, + { + path: "/storefront-development/products/collections/retrieve", + title: "Retrieve a Collection", + }, + { + path: "/storefront-development/products/collections/products", + title: "Retrieve a Collection's Products", + }, + ], + }, + ], + }, ], }, {