Skip to content

Commit 01d32b2

Browse files
committed
feat: add subcategory support
1 parent 67e8ae7 commit 01d32b2

File tree

9 files changed

+210
-21
lines changed

9 files changed

+210
-21
lines changed
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} collection`,
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)/categories/[handle]/page.tsx renamed to src/app/(main)/[category]/page.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,30 @@ import { Metadata } from "next"
44
import { notFound } from "next/navigation"
55

66
type Props = {
7-
params: { handle: string }
7+
params: { category: string }
88
}
99

1010
export async function generateMetadata({ params }: Props): Promise<Metadata> {
11-
const { product_categories } = await getCategoryByHandle(params.handle)
11+
const { product_categories } = await getCategoryByHandle(
12+
params.category
13+
).catch((err) => {
14+
notFound()
15+
})
1216

1317
const category = product_categories[0]
1418

15-
if (!category) {
16-
notFound()
17-
}
18-
1919
return {
20-
title: `${category.title} | Acme Store`,
21-
description: `${category.title} collection`,
20+
title: `${category.name} | Acme Store`,
21+
description: `${category.name} category`,
2222
}
2323
}
2424

2525
export default async function CategoryPage({ params }: Props) {
26-
const { product_categories } = await getCategoryByHandle(params.handle)
26+
const { product_categories } = await getCategoryByHandle(
27+
params.category
28+
).catch((err) => {
29+
notFound()
30+
})
2731

2832
const category = product_categories[0]
2933

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+
}

src/app/api/categories/[handle]/route.ts renamed to src/app/api/categories/[category]/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export async function GET(
2020
) {
2121
const productService = await initializeProductModule()
2222

23-
const { handle } = params
23+
const { category } = params
2424

2525
const searchParams = Object.fromEntries(request.nextUrl.searchParams)
2626
const { offset, limit, cart_id } = searchParams
@@ -30,7 +30,7 @@ export async function GET(
3030
} = (await productService
3131
.listCategories(
3232
{
33-
handle,
33+
handle: category,
3434
},
3535
{
3636
relations: [
@@ -40,6 +40,7 @@ export async function GET(
4040
"products.tags",
4141
"products.options",
4242
"products.status",
43+
"category_children",
4344
],
4445
select: ["id", "handle", "name", "description"],
4546
take: 1,

src/app/api/categories/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export async function GET(request: NextRequest) {
1616
.listAndCountCategories(
1717
{},
1818
{
19-
select: ["id", "handle", "name", "description"],
19+
select: ["id", "handle", "name", "description", "parent_category"],
20+
relations: ["category_children"],
2021
skip: parseInt(offset) || 0,
2122
take: 100,
2223
}

src/modules/categories/templates/index.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,30 @@ import { useInfiniteQuery } from "@tanstack/react-query"
1010
import { useCart } from "medusa-react"
1111
import React, { useEffect } from "react"
1212
import { useInView } from "react-intersection-observer"
13+
import Link from "next/link"
14+
import UnderlineLink from "@modules/common/components/underline-link"
1315

1416
type CategoryTemplateProps = {
1517
category: {
1618
handle: string
1719
name: string
1820
id: string
21+
category_children?: {
22+
name: string
23+
handle: string
24+
id: string
25+
}[]
26+
}
27+
parent?: {
28+
handle: string
29+
name: string
1930
}
2031
}
2132

22-
const CategoryTemplate: React.FC<CategoryTemplateProps> = ({ category }) => {
33+
const CategoryTemplate: React.FC<CategoryTemplateProps> = ({
34+
category,
35+
parent,
36+
}) => {
2337
const { cart } = useCart()
2438
const { ref, inView } = useInView()
2539

@@ -62,9 +76,33 @@ const CategoryTemplate: React.FC<CategoryTemplateProps> = ({ category }) => {
6276

6377
return (
6478
<div className="content-container py-6">
65-
<div className="mb-8 text-2xl-semi">
79+
<div className="flex flex-row mb-8 text-2xl-semi gap-4">
80+
{parent && (
81+
<>
82+
<span className="text-gray-500">
83+
<Link
84+
className="mr-4 hover:text-black"
85+
href={`/${parent.handle}`}
86+
>
87+
{parent.name}
88+
</Link>
89+
/
90+
</span>
91+
</>
92+
)}
6693
<h1>{category.name}</h1>
6794
</div>
95+
{category.category_children && (
96+
<div className="mb-8 text-base-large">
97+
<ul className="grid grid-cols-1 gap-2">
98+
{category.category_children?.map((c) => (
99+
<li key={c.id}>
100+
<UnderlineLink href={`/${c.handle}`}>{c.name}</UnderlineLink>
101+
</li>
102+
))}
103+
</ul>
104+
</div>
105+
)}
68106
<ul className="grid grid-cols-2 small:grid-cols-3 medium:grid-cols-4 gap-x-4 gap-y-8">
69107
{previews.map((p) => (
70108
<li key={p.id}>

src/modules/layout/components/footer-nav/index.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,39 @@ const FooterNav = () => {
2121
{product_categories && (
2222
<div className="flex flex-col gap-y-2">
2323
<span className="text-base-semi">Categories</span>
24-
<ul
25-
className={clsx("grid grid-cols-1 gap-2", {
26-
"grid-cols-2": (product_categories?.length || 0) > 3,
27-
})}
28-
>
24+
<ul className="grid grid-cols-1 gap-2">
2925
{product_categories?.slice(0, 6).map((c) => {
26+
if (c.parent_category) {
27+
return
28+
}
29+
30+
const children =
31+
c.category_children?.map((child) => ({
32+
name: child.name,
33+
handle: child.handle,
34+
id: child.id,
35+
})) || null
36+
3037
return (
31-
<li key={c.id}>
32-
<Link href={`/categories/${c.handle}`}>{c.name}</Link>
38+
<li className="flex flex-col gap-2" key={c.id}>
39+
<Link
40+
className={clsx(children && "text-small-semi")}
41+
href={`/${c.handle}`}
42+
>
43+
{c.name}
44+
</Link>
45+
{children && (
46+
<ul className="grid grid-cols-1 ml-3 gap-2">
47+
{children &&
48+
children.map((child) => (
49+
<li key={child.id}>
50+
<Link href={`/${child.handle}`}>
51+
{child.name}
52+
</Link>
53+
</li>
54+
))}
55+
</ul>
56+
)}
3357
</li>
3458
)
3559
})}

0 commit comments

Comments
 (0)