Skip to content

Commit a218c5f

Browse files
authored
Merge pull request #173 from medusajs/add-pricing-module
feat: pricing module + category improvements
2 parents 4945449 + 10df152 commit a218c5f

File tree

25 files changed

+2044
-1773
lines changed

25 files changed

+2044
-1773
lines changed

.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000
55
NEXT_PUBLIC_BASE_URL=http://localhost:8000
66

77
# Posgres URL for your Medusa DB for the Product Module. See - https://docs.medusajs.com/modules/products/serverless-module
8-
PRODUCT_POSTGRES_URL=postgres://postgres:postgres@localhost:5432/medusa
8+
POSTGRES_URL=postgres://postgres:postgres@localhost:5432/medusa
99

1010
# Your Stripe public key. See – https://docs.medusajs.com/add-plugins/stripe
1111
NEXT_PUBLIC_STRIPE_KEY=

next.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ const store = require("./store.config.json")
44
module.exports = withStoreConfig({
55
experimental: {
66
serverActions: true,
7-
serverComponentsExternalPackages: ["@medusajs/product"],
7+
serverComponentsExternalPackages: [
8+
"@medusajs/product",
9+
"@medusajs/modules-sdk",
10+
],
811
},
912
features: store.features,
1013
reactStrictMode: true,

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
"dependencies": {
2323
"@headlessui/react": "^1.6.1",
2424
"@hookform/error-message": "^2.0.0",
25+
"@medusajs/link-modules": "^0.1.1",
2526
"@medusajs/medusa-js": "^6.0.3",
26-
"@medusajs/product": "^0.1.7",
27+
"@medusajs/modules-sdk": "^1.11.0",
28+
"@medusajs/pricing": "^0.0.3",
29+
"@medusajs/product": "^0.2.0",
2730
"@meilisearch/instant-meilisearch": "^0.7.1",
2831
"@paypal/paypal-js": "^5.0.6",
2932
"@paypal/react-paypal-js": "^7.8.1",
@@ -40,7 +43,8 @@
4043
"react-hook-form": "^7.30.0",
4144
"react-instantsearch-hooks-web": "^6.29.0",
4245
"react-intersection-observer": "^9.3.4",
43-
"sharp": "^0.30.7"
46+
"sharp": "^0.30.7",
47+
"webpack": "^5"
4448
},
4549
"devDependencies": {
4650
"@babel/core": "^7.17.5",

src/app/(checkout)/not-found.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Metadata } from "next"
2+
import Link from "next/link"
3+
4+
export const metadata: Metadata = {
5+
title: "404",
6+
description: "Something went wrong",
7+
}
8+
9+
export default function NotFound() {
10+
return (
11+
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-64px)]">
12+
<h1 className="text-2xl-semi text-gry-900">Page not found</h1>
13+
<p className="text-small-regular text-gray-700">
14+
The page you tried to access does not exist.
15+
</p>
16+
<Link href="/" className="mt-4 underline text-base-regular text-gray-900">
17+
Go to frontpage
18+
</Link>
19+
</div>
20+
)
21+
}

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

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

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

1010
export async function generateMetadata({ params }: Props): Promise<Metadata> {
@@ -29,7 +29,5 @@ export default async function CategoryPage({ params }: Props) {
2929
notFound()
3030
})
3131

32-
const category = product_categories[0]
33-
34-
return <CategoryTemplate category={category} />
32+
return <CategoryTemplate categories={product_categories} />
3533
}

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

Lines changed: 0 additions & 38 deletions
This file was deleted.

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

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/app/(main)/not-found.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Metadata } from "next"
2+
import Link from "next/link"
3+
4+
export const metadata: Metadata = {
5+
title: "404",
6+
description: "Something went wrong",
7+
}
8+
9+
export default function NotFound() {
10+
return (
11+
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-64px)]">
12+
<h1 className="text-2xl-semi text-gry-900">Page not found</h1>
13+
<p className="text-small-regular text-gray-700">
14+
The page you tried to access does not exist.
15+
</p>
16+
<Link href="/" className="mt-4 underline text-base-regular text-gray-900">
17+
Go to frontpage
18+
</Link>
19+
</div>
20+
)
21+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { initialize as initializeProductModule } from "@medusajs/product"
3+
import { ProductDTO } from "@medusajs/types/dist/product"
4+
import { IPricingModuleService } from "@medusajs/types"
5+
import { notFound } from "next/navigation"
6+
import { MedusaApp, Modules } from "@medusajs/modules-sdk"
7+
import { getPricesByPriceSetId } from "@lib/util/get-prices-by-price-set-id"
8+
9+
/**
10+
* This endpoint uses the serverless Product and Pricing Modules to retrieve a category and its products by handle.
11+
* The module connects directly to you Medusa database to retrieve and manipulate data, without the need for a dedicated server.
12+
* Read more about the Product Module here: https://docs.medusajs.com/modules/products/serverless-module
13+
*/
14+
export async function GET(
15+
request: NextRequest,
16+
{ params }: { params: Record<string, any> }
17+
) {
18+
// Initialize the Product Module
19+
const productService = await initializeProductModule()
20+
21+
// Extract the query parameters
22+
const searchParams = Object.fromEntries(request.nextUrl.searchParams)
23+
const { page, limit } = searchParams
24+
25+
let { handle: categoryHandle } = params
26+
27+
const handle = categoryHandle.map((handle: string, index: number) =>
28+
categoryHandle.slice(0, index + 1).join("/")
29+
)
30+
31+
// Fetch the category by handle
32+
const product_categories = await productService
33+
.listCategories(
34+
{
35+
handle,
36+
},
37+
{
38+
select: ["id", "handle", "name", "description"],
39+
relations: ["category_children"],
40+
take: handle.length,
41+
}
42+
)
43+
.catch((e) => {
44+
return notFound()
45+
})
46+
47+
const category = product_categories[0]
48+
49+
if (!category) {
50+
return notFound()
51+
}
52+
53+
// Fetch the products by category id
54+
const {
55+
rows: products,
56+
metadata: { count },
57+
} = await getProductsByCategoryId(category.id, searchParams)
58+
59+
// Filter out unpublished products
60+
const publishedProducts: ProductDTO[] = products.filter(
61+
(product) => product.status === "published"
62+
)
63+
64+
// Calculate the next page
65+
const nextPage = parseInt(page) + parseInt(limit)
66+
67+
// Return the response
68+
return NextResponse.json({
69+
product_categories: Object.values(product_categories),
70+
response: {
71+
products: publishedProducts,
72+
count,
73+
},
74+
nextPage: count > nextPage ? nextPage : null,
75+
})
76+
}
77+
78+
/**
79+
* This function uses the serverless Product and Pricing Modules to retrieve products by category id.
80+
* @param category_id The category id
81+
* @param params The query parameters
82+
* @returns The products and metadata
83+
*/
84+
async function getProductsByCategoryId(
85+
category_id: string,
86+
params: Record<string, any>
87+
): Promise<{ rows: ProductDTO[]; metadata: Record<string, any> }> {
88+
// Extract the query parameters
89+
let { currency_code } = params
90+
91+
currency_code = currency_code && currency_code.toUpperCase()
92+
93+
// Initialize Remote Query with the Product and Pricing Modules
94+
const { query, modules } = await MedusaApp({
95+
modulesConfig: {
96+
[Modules.PRODUCT]: true,
97+
[Modules.PRICING]: true,
98+
},
99+
sharedResourcesConfig: {
100+
database: { clientUrl: process.env.POSTGRES_URL },
101+
},
102+
})
103+
104+
// Set the filters for the query
105+
const filters = {
106+
take: parseInt(params.limit) || 100,
107+
skip: parseInt(params.offset) || 0,
108+
filters: {
109+
category_id: [category_id],
110+
},
111+
currency_code,
112+
}
113+
114+
// Set the GraphQL query
115+
const productsQuery = `#graphql
116+
query($filters: Record, $take: Int, $skip: Int) {
117+
products(filters: $filters, take: $take, skip: $skip) {
118+
id
119+
title
120+
handle
121+
tags
122+
status
123+
collection
124+
collection_id
125+
thumbnail
126+
images {
127+
url
128+
alt_text
129+
id
130+
}
131+
options {
132+
id
133+
value
134+
title
135+
}
136+
variants {
137+
id
138+
title
139+
created_at
140+
updated_at
141+
thumbnail
142+
inventory_quantity
143+
material
144+
weight
145+
length
146+
height
147+
width
148+
options {
149+
id
150+
value
151+
title
152+
}
153+
price {
154+
price_set {
155+
id
156+
}
157+
}
158+
}
159+
}
160+
}`
161+
162+
// Run the query
163+
const { rows, metadata } = await query(productsQuery, filters)
164+
165+
// Calculate prices
166+
const productsWithPrices = await getPricesByPriceSetId({
167+
products: rows,
168+
currency_code,
169+
pricingService: modules.pricingService as unknown as IPricingModuleService,
170+
})
171+
172+
// Return the response
173+
return {
174+
rows: productsWithPrices,
175+
metadata,
176+
}
177+
}

0 commit comments

Comments
 (0)