Skip to content

Commit 29e8b3f

Browse files
authored
Merge pull request #170 from medusajs/add-product-categories
feat: add product categories support
2 parents 55eef7b + d75c2b5 commit 29e8b3f

File tree

12 files changed

+565
-23
lines changed

12 files changed

+565
-23
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import SkeletonCollectionPage from "@modules/skeletons/templates/skeleton-collection-page"
2+
3+
export default function Loading() {
4+
return <SkeletonCollectionPage />
5+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getCategoryByHandle } from "@lib/data"
2+
import CategoryTemplate from "@modules/categories/templates"
3+
import { Metadata } from "next"
4+
import { notFound } from "next/navigation"
5+
6+
type Props = {
7+
params: {
8+
category: string
9+
subcategory: string
10+
}
11+
}
12+
13+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
14+
const { product_categories } = await getCategoryByHandle(
15+
`${params.category}/${params.subcategory}`
16+
).catch((err) => {
17+
notFound()
18+
})
19+
20+
const category = product_categories[0]
21+
22+
return {
23+
title: `${category.name} | Acme Store`,
24+
description: `${category.name} category`,
25+
}
26+
}
27+
28+
export default async function CategoryPage({ params }: Props) {
29+
const { product_categories, parent } = await getCategoryByHandle(
30+
`${params.category}/${params.subcategory}`
31+
).catch((err) => {
32+
notFound()
33+
})
34+
35+
const category = product_categories[0]
36+
37+
return <CategoryTemplate category={category} parent={parent} />
38+
}

src/app/(main)/[category]/loading.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import SkeletonCollectionPage from "@modules/skeletons/templates/skeleton-collection-page"
2+
3+
export default function Loading() {
4+
return <SkeletonCollectionPage />
5+
}

src/app/(main)/[category]/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { getCategoryByHandle } from "@lib/data"
2+
import CategoryTemplate from "@modules/categories/templates"
3+
import { Metadata } from "next"
4+
import { notFound } from "next/navigation"
5+
6+
type Props = {
7+
params: { category: string }
8+
}
9+
10+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
11+
const { product_categories } = await getCategoryByHandle(
12+
params.category
13+
).catch((err) => {
14+
notFound()
15+
})
16+
17+
const category = product_categories[0]
18+
19+
return {
20+
title: `${category.name} | Acme Store`,
21+
description: `${category.name} category`,
22+
}
23+
}
24+
25+
export default async function CategoryPage({ params }: Props) {
26+
const { product_categories } = await getCategoryByHandle(
27+
params.category
28+
).catch((err) => {
29+
notFound()
30+
})
31+
32+
const category = product_categories[0]
33+
34+
return <CategoryTemplate category={category} />
35+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { initialize as initializeProductModule } from "@medusajs/product"
3+
import { ProductCategoryDTO, ProductDTO } from "@medusajs/types/dist/product"
4+
import { notFound } from "next/navigation"
5+
import getPrices from "@lib/util/get-product-prices"
6+
import filterProductsByStatus from "@lib/util/filter-products-by-status"
7+
8+
type ProductCategoryResponse = ProductCategoryDTO & {
9+
products: ProductDTO[]
10+
}
11+
12+
/**
13+
* This endpoint uses the serverless Product Module to retrieve a category and its products by handle.
14+
* The module connects directly to you Medusa database to retrieve and manipulate data, without the need for a dedicated server.
15+
* Read more about the Product Module here: https://docs.medusajs.com/modules/products/serverless-module
16+
*/
17+
export async function GET(
18+
request: NextRequest,
19+
{ params }: { params: Record<string, any> }
20+
) {
21+
const productService = await initializeProductModule()
22+
23+
const { category, subcategory } = params
24+
25+
const searchParams = Object.fromEntries(request.nextUrl.searchParams)
26+
const { offset, limit, cart_id } = searchParams
27+
28+
const {
29+
[0]: { name: parent_name, handle: parent_handle },
30+
[1]: { products, ...categoryMeta },
31+
} = (await productService
32+
.listCategories(
33+
{
34+
handle: [`${category}/${subcategory}`, category],
35+
},
36+
{
37+
relations: [
38+
"products",
39+
"products.variants",
40+
"products.variants.options",
41+
"products.tags",
42+
"products.options",
43+
"products.status",
44+
],
45+
select: ["id", "handle", "name", "description"],
46+
take: 2,
47+
}
48+
)
49+
.catch((e) => {
50+
return notFound()
51+
})) as ProductCategoryResponse[]
52+
53+
const publishedProducts = filterProductsByStatus(products, "published")
54+
55+
const count = publishedProducts.length || 0
56+
57+
const offsetInt = parseInt(offset) || 0
58+
const limitInt = parseFloat(limit) || 12
59+
60+
const productsSlice = publishedProducts.slice(offsetInt, offsetInt + limitInt)
61+
62+
const productsWithPrices = await getPrices(productsSlice, cart_id)
63+
64+
const nextPage = offsetInt + limitInt
65+
66+
return NextResponse.json({
67+
parent: {
68+
name: parent_name,
69+
handle: parent_handle,
70+
},
71+
product_categories: [categoryMeta],
72+
response: {
73+
products: productsWithPrices,
74+
count,
75+
},
76+
nextPage: count > nextPage ? nextPage : null,
77+
})
78+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { initialize as initializeProductModule } from "@medusajs/product"
3+
import { ProductCategoryDTO, ProductDTO } from "@medusajs/types/dist/product"
4+
import { notFound } from "next/navigation"
5+
import getPrices from "@lib/util/get-product-prices"
6+
import filterProductsByStatus from "@lib/util/filter-products-by-status"
7+
8+
type ProductCategoryResponse = ProductCategoryDTO & {
9+
products: ProductDTO[]
10+
}
11+
12+
/**
13+
* This endpoint uses the serverless Product Module to retrieve a category and its products by handle.
14+
* The module connects directly to you Medusa database to retrieve and manipulate data, without the need for a dedicated server.
15+
* Read more about the Product Module here: https://docs.medusajs.com/modules/products/serverless-module
16+
*/
17+
export async function GET(
18+
request: NextRequest,
19+
{ params }: { params: Record<string, any> }
20+
) {
21+
const productService = await initializeProductModule()
22+
23+
const { category } = params
24+
25+
const searchParams = Object.fromEntries(request.nextUrl.searchParams)
26+
const { offset, limit, cart_id } = searchParams
27+
28+
const {
29+
[0]: { products, ...categoryMeta },
30+
} = (await productService
31+
.listCategories(
32+
{
33+
handle: category,
34+
},
35+
{
36+
relations: [
37+
"products",
38+
"products.variants",
39+
"products.variants.options",
40+
"products.tags",
41+
"products.options",
42+
"products.status",
43+
"category_children",
44+
],
45+
select: ["id", "handle", "name", "description"],
46+
take: 1,
47+
}
48+
)
49+
.catch((e) => {
50+
return notFound()
51+
})) as ProductCategoryResponse[]
52+
53+
const publishedProducts = filterProductsByStatus(products, "published")
54+
55+
const count = publishedProducts.length || 0
56+
57+
const offsetInt = parseInt(offset) || 0
58+
const limitInt = parseFloat(limit) || 12
59+
60+
const productsSlice = publishedProducts.slice(offsetInt, offsetInt + limitInt)
61+
62+
const productsWithPrices = await getPrices(productsSlice, cart_id)
63+
64+
const nextPage = offsetInt + limitInt
65+
66+
return NextResponse.json({
67+
product_categories: [categoryMeta],
68+
response: {
69+
products: productsWithPrices,
70+
count,
71+
},
72+
nextPage: count > nextPage ? nextPage : null,
73+
})
74+
}

src/app/api/categories/route.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { initialize as initializeProductModule } from "@medusajs/product"
3+
import { notFound } from "next/navigation"
4+
5+
/**
6+
* This endpoint uses the serverless Product Module to list and count all product categories.
7+
* The module connects directly to you Medusa database to retrieve and manipulate data, without the need for a dedicated server.
8+
* Read more about the Product Module here: https://docs.medusajs.com/modules/products/serverless-module
9+
*/
10+
export async function GET(request: NextRequest) {
11+
const productService = await initializeProductModule()
12+
13+
const { offset } = Object.fromEntries(request.nextUrl.searchParams)
14+
15+
const [product_categories, count] = await productService
16+
.listAndCountCategories(
17+
{},
18+
{
19+
select: ["id", "handle", "name", "description", "parent_category"],
20+
relations: ["category_children"],
21+
skip: parseInt(offset) || 0,
22+
take: 100,
23+
}
24+
)
25+
.catch((e) => {
26+
return notFound()
27+
})
28+
29+
return NextResponse.json({
30+
product_categories,
31+
count,
32+
})
33+
}

src/app/api/products/route.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse, NextRequest } from "next/server"
22
import getPrices from "@lib/util/get-product-prices"
3+
import filterProductsByStatus from "@lib/util/filter-products-by-status"
34

45
import { initialize as initializeProductModule } from "@medusajs/product"
56
import {
@@ -59,7 +60,7 @@ async function getProductsByCollectionId(queryParams: Record<string, any>) {
5960

6061
const products = data.map((c) => c.products).flat() as ProductDTO[]
6162

62-
const publishedProducts = filterPublishedProducts(products)
63+
const publishedProducts = filterProductsByStatus(products, "published")
6364

6465
const count = publishedProducts.length
6566

@@ -97,7 +98,7 @@ async function getProducts(params: Record<string, any>) {
9798
withDeleted: false,
9899
})
99100

100-
const publishedProducts = filterPublishedProducts(data)
101+
const publishedProducts = filterProductsByStatus(data, "published")
101102

102103
const productsWithPrices = await getPrices(publishedProducts, cart_id)
103104

@@ -109,7 +110,3 @@ async function getProducts(params: Record<string, any>) {
109110
nextPage: count > nextPage ? nextPage : null,
110111
}
111112
}
112-
113-
function filterPublishedProducts(products: ProductDTO[]) {
114-
return products.filter((product) => product.status === "published")
115-
}

0 commit comments

Comments
 (0)