@pradip1995/storefront-controllers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +56 -0
- package/src/account/load-account-data.ts +5 -0
- package/src/account/login-page.tsx +8 -0
- package/src/cart/cart-page.tsx +13 -0
- package/src/cart/load-cart-data.ts +18 -0
- package/src/checkout/checkout-page.tsx +39 -0
- package/src/checkout/load-checkout-data.ts +21 -0
- package/src/home/home-page.tsx +95 -0
- package/src/home/load-home-data.ts +118 -0
- package/src/layout/load-layout-data.ts +73 -0
- package/src/layout/main-layout.tsx +46 -0
- package/src/order/load-order-data.ts +8 -0
- package/src/order/order-details-controller.tsx +12 -0
- package/src/product/product-actions-wrapper.tsx +33 -0
- package/src/product/product-page.tsx +98 -0
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pradip1995/storefront-controllers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Medusa storefront page controllers — data loading and composition",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"registry": "https://registry.npmjs.org/"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/SmartByteLabs/medusa-storefront-kit.git",
|
|
13
|
+
"directory": "packages/storefront-controllers"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
"./account/login-page": "./src/account/login-page.tsx",
|
|
21
|
+
"./account/load-account-data": "./src/account/load-account-data.ts",
|
|
22
|
+
"./cart/cart-page": "./src/cart/cart-page.tsx",
|
|
23
|
+
"./cart/load-cart-data": "./src/cart/load-cart-data.ts",
|
|
24
|
+
"./checkout/checkout-page": "./src/checkout/checkout-page.tsx",
|
|
25
|
+
"./checkout/load-checkout-data": "./src/checkout/load-checkout-data.ts",
|
|
26
|
+
"./home/home-page": "./src/home/home-page.tsx",
|
|
27
|
+
"./home/load-home-data": "./src/home/load-home-data.ts",
|
|
28
|
+
"./layout/main-layout": "./src/layout/main-layout.tsx",
|
|
29
|
+
"./layout/load-layout-data": "./src/layout/load-layout-data.ts",
|
|
30
|
+
"./order/order-details-controller": "./src/order/order-details-controller.tsx",
|
|
31
|
+
"./order/load-order-data": "./src/order/load-order-data.ts",
|
|
32
|
+
"./product/product-page": "./src/product/product-page.tsx",
|
|
33
|
+
"./product/product-actions-wrapper": "./src/product/product-actions-wrapper.tsx"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"next": ">=15",
|
|
37
|
+
"react": ">=19",
|
|
38
|
+
"@pradip1995/commerce-core": "1.0.0",
|
|
39
|
+
"@pradip1995/storefront-registry": "1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@medusajs/types": "latest",
|
|
43
|
+
"@types/react": "^19",
|
|
44
|
+
"eslint": "^8.57.0",
|
|
45
|
+
"next": "15.3.8",
|
|
46
|
+
"react": "19.0.3",
|
|
47
|
+
"typescript": "^5.7.2",
|
|
48
|
+
"@pradip1995/commerce-core": "1.0.0",
|
|
49
|
+
"@pradip1995/storefront-registry": "1.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
53
|
+
"typecheck:full": "tsc --noEmit",
|
|
54
|
+
"lint": "tsc --noEmit -p tsconfig.typecheck.json"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { resolveComponent } from "@registry/resolve-component"
|
|
2
|
+
import type { AccountPageData } from "@core/types/account"
|
|
3
|
+
|
|
4
|
+
export async function LoginPage({ data }: { data: AccountPageData }) {
|
|
5
|
+
const LoginTemplate = (await resolveComponent("slots/account/LoginTemplate")).default
|
|
6
|
+
|
|
7
|
+
return <LoginTemplate countryCode={data.countryCode} />
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import CartTemplate from "@modules/cart/templates"
|
|
2
|
+
import type { CartPageData } from "@core/types/cart"
|
|
3
|
+
|
|
4
|
+
export function CartPage({ data }: { data: CartPageData }) {
|
|
5
|
+
return (
|
|
6
|
+
<CartTemplate
|
|
7
|
+
cart={data.cart}
|
|
8
|
+
customer={data.customer}
|
|
9
|
+
countryCode={data.countryCode}
|
|
10
|
+
abandonedCarts={data.abandonedCarts}
|
|
11
|
+
/>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { retrieveCart, getAbandonedCarts } from "@core/data/cart"
|
|
2
|
+
import { retrieveCustomer } from "@core/data/customer"
|
|
3
|
+
import type { CartPageData } from "@core/types/cart"
|
|
4
|
+
|
|
5
|
+
export async function loadCartData(countryCode: string): Promise<CartPageData> {
|
|
6
|
+
const [cart, customer, abandonedCarts] = await Promise.all([
|
|
7
|
+
retrieveCart(),
|
|
8
|
+
retrieveCustomer(),
|
|
9
|
+
getAbandonedCarts(),
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
cart,
|
|
14
|
+
customer,
|
|
15
|
+
countryCode,
|
|
16
|
+
abandonedCarts,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import PaymentWrapper from "@modules/checkout/components/payment-wrapper"
|
|
2
|
+
import CheckoutBeginTracker from "@modules/checkout/components/checkout-begin-tracker"
|
|
3
|
+
import CheckoutForm from "@modules/checkout/templates/checkout-form"
|
|
4
|
+
import CheckoutSummary from "@modules/checkout/templates/checkout-summary"
|
|
5
|
+
import type { CheckoutPageData } from "./load-checkout-data"
|
|
6
|
+
|
|
7
|
+
type CheckoutPageProps = {
|
|
8
|
+
data: CheckoutPageData
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CheckoutPage({ data }: CheckoutPageProps) {
|
|
12
|
+
const { cart, customer } = data
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="w-full max-w-[1360px] mx-auto px-3 min-[550px]:px-4 sm:px-6 md:px-8 min-[1023px]:px-4 min-[1150px]:px-6 min-[1360px]:px-8 py-4 min-[550px]:py-6 sm:py-8 md:py-12 min-[1023px]:py-10 min-[1150px]:py-12 min-[1360px]:py-12">
|
|
16
|
+
<CheckoutBeginTracker cart={cart} />
|
|
17
|
+
<div className="grid grid-cols-1 min-[1024px]:grid-cols-[1fr_416px] gap-4 min-[550px]:gap-5 sm:gap-6 md:gap-8 min-[768px]:gap-6 min-[1023px]:gap-4 min-[1150px]:gap-6 min-[1200px]:gap-10 min-[1360px]:gap-12 w-full items-start">
|
|
18
|
+
<div className="w-full min-[1024px]:sticky min-[1024px]:top-20">
|
|
19
|
+
<style
|
|
20
|
+
dangerouslySetInnerHTML={{
|
|
21
|
+
__html: `
|
|
22
|
+
.checkout-form-scroll::-webkit-scrollbar { display: none; }
|
|
23
|
+
.checkout-form-scroll { -ms-overflow-style: none; scrollbar-width: none; }
|
|
24
|
+
`,
|
|
25
|
+
}}
|
|
26
|
+
/>
|
|
27
|
+
<div className="w-full min-[1024px]:max-h-[calc(100vh-140px)] min-[1024px]:overflow-y-auto min-[1024px]:pr-4 checkout-form-scroll">
|
|
28
|
+
<PaymentWrapper cart={cart}>
|
|
29
|
+
<CheckoutForm cart={cart} customer={customer} />
|
|
30
|
+
</PaymentWrapper>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="w-full min-[1024px]:sticky min-[1024px]:top-20">
|
|
34
|
+
<CheckoutSummary cart={cart} customer={customer} />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { retrieveCart } from "@core/data/cart"
|
|
2
|
+
import { retrieveCustomer } from "@core/data/customer"
|
|
3
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
4
|
+
|
|
5
|
+
export type CheckoutPageData = {
|
|
6
|
+
cart: HttpTypes.StoreCart
|
|
7
|
+
customer: HttpTypes.StoreCustomer | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadCheckoutData(
|
|
11
|
+
cartIdFromUrl?: string
|
|
12
|
+
): Promise<CheckoutPageData | null> {
|
|
13
|
+
const cart = await retrieveCart(cartIdFromUrl)
|
|
14
|
+
if (!cart) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const customer = await retrieveCustomer()
|
|
19
|
+
|
|
20
|
+
return { cart, customer }
|
|
21
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { storefrontConfig } from "@registry/storefront.config"
|
|
2
|
+
import { resolveComponent } from "@registry/resolve-component"
|
|
3
|
+
import type { HomePageData } from "@core/types/home"
|
|
4
|
+
import type { HomeBlockType } from "@registry/storefront.config"
|
|
5
|
+
import { colorClasses } from "@theme/tokens"
|
|
6
|
+
|
|
7
|
+
function getBlockProps(
|
|
8
|
+
blockType: HomeBlockType,
|
|
9
|
+
data: HomePageData
|
|
10
|
+
): Record<string, unknown> {
|
|
11
|
+
switch (blockType) {
|
|
12
|
+
case "hero":
|
|
13
|
+
return data.hero
|
|
14
|
+
case "shopByAge":
|
|
15
|
+
return data.shopByAge
|
|
16
|
+
case "shopByCategory":
|
|
17
|
+
return data.shopByCategory
|
|
18
|
+
case "whyChooseUs":
|
|
19
|
+
return data.whyChooseUs
|
|
20
|
+
case "newArrivals":
|
|
21
|
+
return data.newArrivals
|
|
22
|
+
case "lovedByMoms":
|
|
23
|
+
return data.lovedByMoms
|
|
24
|
+
case "testimonials":
|
|
25
|
+
return data.testimonials
|
|
26
|
+
case "features":
|
|
27
|
+
return data.features
|
|
28
|
+
default:
|
|
29
|
+
return {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function HomePage({ data }: { data: HomePageData }) {
|
|
34
|
+
const blocks = storefrontConfig.blocks.home
|
|
35
|
+
|
|
36
|
+
const resolvedBlocks = await Promise.all(
|
|
37
|
+
blocks.map(async (block) => {
|
|
38
|
+
const mod = await resolveComponent(block.component)
|
|
39
|
+
return {
|
|
40
|
+
block,
|
|
41
|
+
Component: mod.default,
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const jsonLd = {
|
|
47
|
+
"@context": "https://schema.org",
|
|
48
|
+
"@type": "WebSite",
|
|
49
|
+
name: "Chocomelon",
|
|
50
|
+
alternateName: "Chocomelon Kids Wear",
|
|
51
|
+
url: "https://chocomelon.in",
|
|
52
|
+
potentialAction: {
|
|
53
|
+
"@type": "SearchAction",
|
|
54
|
+
target: "https://chocomelon.in/store?search={search_term_string}",
|
|
55
|
+
"query-input": "required name=search_term_string",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const organizationJsonLd = {
|
|
60
|
+
"@context": "https://schema.org",
|
|
61
|
+
"@type": "Organization",
|
|
62
|
+
name: "Chocomelon",
|
|
63
|
+
url: "https://chocomelon.in",
|
|
64
|
+
logo: "https://chocomelon.in/Logo.png",
|
|
65
|
+
sameAs: [
|
|
66
|
+
"https://www.instagram.com/chocomelon_kids_wear/",
|
|
67
|
+
"https://www.facebook.com/chocomelon.kids.wear",
|
|
68
|
+
],
|
|
69
|
+
contactPoint: {
|
|
70
|
+
"@type": "ContactPoint",
|
|
71
|
+
telephone: "+91 82382 57652",
|
|
72
|
+
contactType: "customer service",
|
|
73
|
+
areaServed: "IN",
|
|
74
|
+
availableLanguage: ["en", "gu", "hi"],
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={`${colorClasses.pageBg} w-full`}>
|
|
80
|
+
<script
|
|
81
|
+
type="application/ld+json"
|
|
82
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
83
|
+
/>
|
|
84
|
+
<script
|
|
85
|
+
type="application/ld+json"
|
|
86
|
+
dangerouslySetInnerHTML={{
|
|
87
|
+
__html: JSON.stringify(organizationJsonLd),
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
{resolvedBlocks.map(({ block, Component }) => (
|
|
91
|
+
<Component key={block.blockType} {...getBlockProps(block.blockType, data)} />
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { listCollections } from "@core/data/collections"
|
|
2
|
+
import { listCategories } from "@core/data/categories"
|
|
3
|
+
import { listProductsWithSort, getProductsByTag } from "@core/data/products"
|
|
4
|
+
import { getRegion } from "@core/data/regions"
|
|
5
|
+
import {
|
|
6
|
+
getTestimonialsFromConfig,
|
|
7
|
+
getHomeBannersFromConfig,
|
|
8
|
+
getAppBannersFromConfig,
|
|
9
|
+
getFeaturesFromConfig,
|
|
10
|
+
} from "@core/data/dynamic-config"
|
|
11
|
+
import { fetchRatings } from "@core/data/reviews"
|
|
12
|
+
import type { HomePageData, BannerData } from "@core/types/home"
|
|
13
|
+
|
|
14
|
+
const defaultBanner: BannerData = {
|
|
15
|
+
image: "/newbanner.png",
|
|
16
|
+
title: "Premium Kids Shop",
|
|
17
|
+
subtitle: "Little Trends, Quality Kids Wear",
|
|
18
|
+
description:
|
|
19
|
+
"Shop stylish kids wear for little ones and children of all years—from newborn up to 15 years. Crafted for play, absolute comfort, and everyday magic.",
|
|
20
|
+
buttonName: "Shop Kids Wear",
|
|
21
|
+
buttonLink: "/store",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildBannerData(
|
|
25
|
+
homeBanners: BannerData[] | null | undefined,
|
|
26
|
+
appBanners: BannerData[] | null | undefined
|
|
27
|
+
): HomePageData["hero"] {
|
|
28
|
+
const homeBanner =
|
|
29
|
+
homeBanners && homeBanners.length > 0 ? homeBanners[0] : defaultBanner
|
|
30
|
+
const homeBannerImage = homeBanner.image || "/newbanner.png"
|
|
31
|
+
const firstAppBanner = appBanners && appBanners.length > 0 ? appBanners[0] : null
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
homeBanner: { ...homeBanner, image: homeBannerImage },
|
|
35
|
+
appBanner: {
|
|
36
|
+
image: firstAppBanner?.image || "/appbanner.png",
|
|
37
|
+
title: firstAppBanner?.title || homeBanner.title,
|
|
38
|
+
subtitle: firstAppBanner?.subtitle || homeBanner.subtitle,
|
|
39
|
+
description: firstAppBanner?.description || homeBanner.description,
|
|
40
|
+
buttonName: firstAppBanner?.buttonName || homeBanner.buttonName,
|
|
41
|
+
buttonLink: firstAppBanner?.buttonLink || homeBanner.buttonLink,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function loadHomeData(countryCode: string): Promise<HomePageData | null> {
|
|
47
|
+
const region = await getRegion(countryCode)
|
|
48
|
+
if (!region) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [
|
|
53
|
+
collectionsResult,
|
|
54
|
+
categoriesResult,
|
|
55
|
+
newArrivalsResult,
|
|
56
|
+
bestsellersResult,
|
|
57
|
+
testimonialsData,
|
|
58
|
+
allRatings,
|
|
59
|
+
homeBanners,
|
|
60
|
+
appBanners,
|
|
61
|
+
featuresConfig,
|
|
62
|
+
] = await Promise.all([
|
|
63
|
+
listCollections({ fields: "id, handle, title, metadata" }).catch(() => ({
|
|
64
|
+
collections: [],
|
|
65
|
+
})),
|
|
66
|
+
listCategories({ limit: 50 }).catch(() => []),
|
|
67
|
+
listProductsWithSort({
|
|
68
|
+
page: 1,
|
|
69
|
+
queryParams: { limit: 8 },
|
|
70
|
+
sortBy: "created_at",
|
|
71
|
+
countryCode,
|
|
72
|
+
}).catch(() => ({ response: { products: [] } })),
|
|
73
|
+
getProductsByTag({
|
|
74
|
+
tagValue: "bestseller",
|
|
75
|
+
limit: 10,
|
|
76
|
+
countryCode,
|
|
77
|
+
}).catch(() => []),
|
|
78
|
+
getTestimonialsFromConfig().catch(() => null),
|
|
79
|
+
fetchRatings(),
|
|
80
|
+
getHomeBannersFromConfig().catch(() => null),
|
|
81
|
+
getAppBannersFromConfig().catch(() => null),
|
|
82
|
+
getFeaturesFromConfig().catch(() => null),
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
const collections =
|
|
86
|
+
(collectionsResult as { collections?: unknown[] }).collections || []
|
|
87
|
+
const categories = categoriesResult || []
|
|
88
|
+
const newArrivals =
|
|
89
|
+
(newArrivalsResult as { response: { products: unknown[] } }).response.products || []
|
|
90
|
+
const bestsellers = bestsellersResult || []
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
hero: buildBannerData(
|
|
94
|
+
homeBanners as BannerData[] | null,
|
|
95
|
+
appBanners as BannerData[] | null
|
|
96
|
+
),
|
|
97
|
+
shopByAge: { collections: collections as HomePageData["shopByAge"]["collections"] },
|
|
98
|
+
shopByCategory: {
|
|
99
|
+
categories: categories as HomePageData["shopByCategory"]["categories"],
|
|
100
|
+
},
|
|
101
|
+
whyChooseUs: {
|
|
102
|
+
title: featuresConfig?.title || "Why Choose Chocomelon?",
|
|
103
|
+
features: featuresConfig?.features || [],
|
|
104
|
+
},
|
|
105
|
+
newArrivals: {
|
|
106
|
+
products: newArrivals as HomePageData["newArrivals"]["products"],
|
|
107
|
+
region,
|
|
108
|
+
ratings: allRatings,
|
|
109
|
+
},
|
|
110
|
+
lovedByMoms: {
|
|
111
|
+
products: bestsellers as HomePageData["lovedByMoms"]["products"],
|
|
112
|
+
region,
|
|
113
|
+
ratings: allRatings,
|
|
114
|
+
},
|
|
115
|
+
testimonials: { initialData: testimonialsData },
|
|
116
|
+
features: {},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { listCartOptions, retrieveCart } from "@core/data/cart"
|
|
2
|
+
import { retrieveCustomer } from "@core/data/customer"
|
|
3
|
+
import { listCategories } from "@core/data/categories"
|
|
4
|
+
import { listRegions } from "@core/data/regions"
|
|
5
|
+
import { getLocale } from "@core/data/locale-actions"
|
|
6
|
+
import { getPromoBarConfig, getSocialLinksFromConfig } from "@core/data/dynamic-config"
|
|
7
|
+
import type { MainLayoutData } from "@core/types/layout"
|
|
8
|
+
|
|
9
|
+
export async function loadLayoutData(): Promise<MainLayoutData> {
|
|
10
|
+
const [
|
|
11
|
+
customer,
|
|
12
|
+
cart,
|
|
13
|
+
regions,
|
|
14
|
+
currentLocale,
|
|
15
|
+
promoBarConfig,
|
|
16
|
+
allCategories,
|
|
17
|
+
fetchedSocialLinks,
|
|
18
|
+
] = await Promise.all([
|
|
19
|
+
retrieveCustomer(),
|
|
20
|
+
retrieveCart(),
|
|
21
|
+
listRegions(),
|
|
22
|
+
getLocale(),
|
|
23
|
+
getPromoBarConfig(),
|
|
24
|
+
listCategories({ limit: 20 }),
|
|
25
|
+
getSocialLinksFromConfig(),
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
let shippingOptions: MainLayoutData["shippingOptions"] = []
|
|
29
|
+
if (cart) {
|
|
30
|
+
const { shipping_options } = await listCartOptions()
|
|
31
|
+
shippingOptions = shipping_options
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const categories = allCategories.filter((c) => !c.parent_category_id).slice(0, 6)
|
|
35
|
+
|
|
36
|
+
const defaultSocialLinks = [
|
|
37
|
+
{ name: "LinkedIn", url: "#", icon: "/LINK.svg" },
|
|
38
|
+
{ name: "Facebook", url: "#", icon: "/FACE.svg" },
|
|
39
|
+
{ name: "Instagram", url: "#", icon: "/INSTA.svg" },
|
|
40
|
+
{ name: "YouTube", url: "#", icon: "/YOU.svg" },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const getFallbackIcon = (name: string) => {
|
|
44
|
+
const n = name.toLowerCase()
|
|
45
|
+
if (n.includes("facebook")) return "/FACE.svg"
|
|
46
|
+
if (n.includes("linkedin")) return "/LINK.svg"
|
|
47
|
+
if (n.includes("instagram")) return "/INSTA.svg"
|
|
48
|
+
if (n.includes("youtube")) return "/YOU.svg"
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const socialLinks = (
|
|
53
|
+
fetchedSocialLinks && fetchedSocialLinks.length > 0
|
|
54
|
+
? fetchedSocialLinks
|
|
55
|
+
: defaultSocialLinks
|
|
56
|
+
).map((link) => ({
|
|
57
|
+
...link,
|
|
58
|
+
icon: link.icon || getFallbackIcon(link.name),
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
customer,
|
|
63
|
+
cart,
|
|
64
|
+
shippingOptions,
|
|
65
|
+
nav: {
|
|
66
|
+
regions,
|
|
67
|
+
currentLocale: currentLocale || "in",
|
|
68
|
+
customer,
|
|
69
|
+
},
|
|
70
|
+
promoBar: promoBarConfig,
|
|
71
|
+
footer: { categories, socialLinks },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WishlistProvider } from "@core/context/wishlist-context"
|
|
2
|
+
import { resolveComponent } from "@registry/resolve-component"
|
|
3
|
+
import type { MainLayoutData } from "@core/types/layout"
|
|
4
|
+
import { MainLayoutShell } from "@theme/layouts/MainLayoutShell"
|
|
5
|
+
import CartMismatchBanner from "@modules/layout/components/cart-mismatch-banner"
|
|
6
|
+
import FreeShippingPriceNudge from "@modules/shipping/components/free-shipping-price-nudge"
|
|
7
|
+
import PushNotificationManager from "@modules/layout/components/push-notification-manager"
|
|
8
|
+
import DeletionPendingModal from "@modules/account/components/deletion-pending-modal"
|
|
9
|
+
import VerificationBanner from "@modules/layout/components/verification-banner"
|
|
10
|
+
|
|
11
|
+
type MainLayoutProps = {
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
data: MainLayoutData
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function MainLayout({ children, data }: MainLayoutProps) {
|
|
17
|
+
const [PromoBar, Nav, Footer] = await Promise.all([
|
|
18
|
+
resolveComponent("slots/layout/PromoBar").then((m) => m.default),
|
|
19
|
+
resolveComponent("slots/layout/Nav").then((m) => m.default),
|
|
20
|
+
resolveComponent("slots/layout/Footer").then((m) => m.default),
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const { customer, cart, shippingOptions } = data
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<WishlistProvider>
|
|
27
|
+
<PromoBar {...data.promoBar} />
|
|
28
|
+
<Nav {...data.nav} />
|
|
29
|
+
{customer && <VerificationBanner customer={customer} />}
|
|
30
|
+
{customer && cart && <CartMismatchBanner customer={customer} cart={cart} />}
|
|
31
|
+
{cart && (
|
|
32
|
+
<FreeShippingPriceNudge
|
|
33
|
+
variant="popup"
|
|
34
|
+
cart={cart}
|
|
35
|
+
shippingOptions={shippingOptions}
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
<MainLayoutShell>{children}</MainLayoutShell>
|
|
39
|
+
<PushNotificationManager customerId={customer?.id} />
|
|
40
|
+
<Footer {...data.footer} />
|
|
41
|
+
{customer?.id === "pending_deletion" && (
|
|
42
|
+
<DeletionPendingModal isOpen={true} email={customer.email} />
|
|
43
|
+
)}
|
|
44
|
+
</WishlistProvider>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolveComponent } from "@registry/resolve-component"
|
|
2
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
3
|
+
|
|
4
|
+
type OrderDetailsControllerProps = {
|
|
5
|
+
order: HttpTypes.StoreOrder
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function OrderDetailsController({ order }: OrderDetailsControllerProps) {
|
|
9
|
+
const OrderDetails = (await resolveComponent("slots/order/OrderDetails")).default
|
|
10
|
+
|
|
11
|
+
return <OrderDetails order={order} />
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { listProducts } from "@core/data/products"
|
|
2
|
+
import { retrieveCart } from "@core/data/cart"
|
|
3
|
+
import { HttpTypes } from "@medusajs/types"
|
|
4
|
+
import { resolveComponent } from "@registry/resolve-component"
|
|
5
|
+
import { storefrontConfig } from "@registry/storefront.config"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fetches real-time pricing for a product and renders the theme ProductActions slot.
|
|
9
|
+
*/
|
|
10
|
+
export default async function ProductActionsWrapper({
|
|
11
|
+
id,
|
|
12
|
+
region,
|
|
13
|
+
}: {
|
|
14
|
+
id: string
|
|
15
|
+
region: HttpTypes.StoreRegion
|
|
16
|
+
}) {
|
|
17
|
+
const [product, cart] = await Promise.all([
|
|
18
|
+
listProducts({
|
|
19
|
+
queryParams: { id: [id] },
|
|
20
|
+
regionId: region.id,
|
|
21
|
+
}).then(({ response }) => response.products[0]),
|
|
22
|
+
retrieveCart().catch(() => null),
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
if (!product) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const mod = await resolveComponent(storefrontConfig.slots.product.ProductActions)
|
|
30
|
+
const ProductActions = mod.default
|
|
31
|
+
|
|
32
|
+
return <ProductActions product={product} region={region} cart={cart} />
|
|
33
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { Suspense } from "react"
|
|
2
|
+
import { notFound } from "next/navigation"
|
|
3
|
+
import { HttpTypes } from "@medusajs/types"
|
|
4
|
+
import ImageGallery from "@modules/products/components/image-gallery"
|
|
5
|
+
import RelatedProducts from "@modules/products/components/related-products"
|
|
6
|
+
import SkeletonRelatedProducts from "@modules/skeletons/templates/skeleton-related-products"
|
|
7
|
+
import Breadcrumb from "@modules/common/components/breadcrumb"
|
|
8
|
+
import { ProductProvider } from "@modules/products/context/product-context"
|
|
9
|
+
import ProductActionsWrapper from "@controllers/product/product-actions-wrapper"
|
|
10
|
+
import { resolveComponent } from "@registry/resolve-component"
|
|
11
|
+
import { storefrontConfig } from "@registry/storefront.config"
|
|
12
|
+
|
|
13
|
+
type ProductPageProps = {
|
|
14
|
+
product: HttpTypes.StoreProduct
|
|
15
|
+
region: HttpTypes.StoreRegion
|
|
16
|
+
countryCode: string
|
|
17
|
+
images: HttpTypes.StoreProductImage[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function ProductActionsFallback({
|
|
21
|
+
product,
|
|
22
|
+
region,
|
|
23
|
+
}: {
|
|
24
|
+
product: HttpTypes.StoreProduct
|
|
25
|
+
region: HttpTypes.StoreRegion
|
|
26
|
+
}) {
|
|
27
|
+
const mod = await resolveComponent(storefrontConfig.slots.product.ProductActions)
|
|
28
|
+
const ProductActions = mod.default
|
|
29
|
+
|
|
30
|
+
return <ProductActions disabled product={product} region={region} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function ProductPage({
|
|
34
|
+
product,
|
|
35
|
+
region,
|
|
36
|
+
countryCode,
|
|
37
|
+
images,
|
|
38
|
+
}: ProductPageProps) {
|
|
39
|
+
if (!product?.id) {
|
|
40
|
+
return notFound()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ProductInfo = (
|
|
44
|
+
await resolveComponent(storefrontConfig.slots.product.ProductInfo)
|
|
45
|
+
).default
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<div className="bg-page-bg min-h-screen">
|
|
50
|
+
<ProductProvider>
|
|
51
|
+
<div
|
|
52
|
+
className="mx-auto px-4 sm:px-6 py-6 lg:py-10"
|
|
53
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
54
|
+
>
|
|
55
|
+
<Breadcrumb product={product} />
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
className="flex flex-col lg:flex-row gap-8 lg:gap-14 mt-6 lg:mt-8"
|
|
59
|
+
data-testid="product-container"
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
id="product-gallery-container"
|
|
63
|
+
className="w-full lg:w-[55%] lg:sticky lg:top-28 lg:self-start"
|
|
64
|
+
>
|
|
65
|
+
<ImageGallery images={images} product={product} />
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="w-full lg:w-[45%] flex flex-col gap-6 lg:pt-2">
|
|
69
|
+
<ProductInfo product={product} region={region} />
|
|
70
|
+
<Suspense
|
|
71
|
+
fallback={
|
|
72
|
+
<ProductActionsFallback product={product} region={region} />
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
<ProductActionsWrapper id={product.id} region={region} />
|
|
76
|
+
</Suspense>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</ProductProvider>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
id="related-products"
|
|
85
|
+
className="mx-auto px-4 sm:px-6 py-12 lg:py-20 border-t border-[var(--color-header-border)]"
|
|
86
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
87
|
+
data-testid="related-products-container"
|
|
88
|
+
>
|
|
89
|
+
<h2 className="font-display text-2xl md:text-3xl text-heading text-center mb-10">
|
|
90
|
+
You may also like
|
|
91
|
+
</h2>
|
|
92
|
+
<Suspense fallback={<SkeletonRelatedProducts />}>
|
|
93
|
+
<RelatedProducts product={product} countryCode={countryCode} />
|
|
94
|
+
</Suspense>
|
|
95
|
+
</div>
|
|
96
|
+
</>
|
|
97
|
+
)
|
|
98
|
+
}
|