medusa-storefront-data 2.5.7 → 2.5.9
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/HOMEPAGE_CONFIG.md +23 -1
- package/dist/src/server/dynamic-config.d.ts +14 -0
- package/dist/src/server/dynamic-config.d.ts.map +1 -1
- package/dist/src/server/dynamic-config.js +95 -1
- package/dist/src/server/hero-banner-priority.d.ts +31 -0
- package/dist/src/server/hero-banner-priority.d.ts.map +1 -0
- package/dist/src/server/hero-banner-priority.js +26 -0
- package/dist/src/server/hero-banner-resolve.test.d.ts +2 -0
- package/dist/src/server/hero-banner-resolve.test.d.ts.map +1 -0
- package/dist/src/server/hero-banner-resolve.test.js +56 -0
- package/dist/src/server/hero-banners-config.test.d.ts +2 -0
- package/dist/src/server/hero-banners-config.test.d.ts.map +1 -0
- package/dist/src/server/hero-banners-config.test.js +44 -0
- package/dist/src/server/home-sections/customer-stories.d.ts +9 -0
- package/dist/src/server/home-sections/customer-stories.d.ts.map +1 -0
- package/dist/src/server/home-sections/customer-stories.js +9 -0
- package/dist/src/server/homepage-config.types.d.ts +16 -1
- package/dist/src/server/homepage-config.types.d.ts.map +1 -1
- package/dist/src/server/homepage-config.types.js +1 -0
- package/dist/src/server/homepage-section-defaults.d.ts +12 -0
- package/dist/src/server/homepage-section-defaults.d.ts.map +1 -1
- package/dist/src/server/homepage-section-defaults.js +2 -0
- package/dist/src/server/index.d.ts +1 -0
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +1 -0
- package/dist/src/server/page-input.d.ts +3 -1
- package/dist/src/server/page-input.d.ts.map +1 -1
- package/dist/src/server/page-input.js +88 -5
- package/dist/src/server/storefront-cms.d.ts +11 -0
- package/dist/src/server/storefront-cms.d.ts.map +1 -0
- package/dist/src/server/storefront-cms.js +24 -0
- package/package.json +29 -19
- package/src/server/dynamic-config.ts +141 -4
- package/src/server/hero-banner-priority.ts +63 -0
- package/src/server/home-sections/customer-stories.ts +23 -0
- package/src/server/homepage-config.types.ts +18 -0
- package/src/server/homepage-section-defaults.ts +2 -0
- package/src/server/page-input.ts +94 -5
|
@@ -5,6 +5,84 @@ import { normalizeHomepageConfig } from "./normalize-homepage-config";
|
|
|
5
5
|
function asObject(v) {
|
|
6
6
|
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
7
7
|
}
|
|
8
|
+
function asArray(v) {
|
|
9
|
+
return Array.isArray(v) ? v : [];
|
|
10
|
+
}
|
|
11
|
+
/** `hero-banners.homeBanners[].slide` from medusa-plugin-dynamic-config. */
|
|
12
|
+
function heroSlideRow(row) {
|
|
13
|
+
const rowObj = asObject(row);
|
|
14
|
+
if (!rowObj)
|
|
15
|
+
return null;
|
|
16
|
+
const slide = asObject(rowObj.slide) ?? rowObj;
|
|
17
|
+
const image = slide.image;
|
|
18
|
+
if (image == null || image === "")
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
image,
|
|
22
|
+
title: slide.title,
|
|
23
|
+
subtitle: slide.subtitle,
|
|
24
|
+
description: slide.description,
|
|
25
|
+
buttonText: slide.buttonText ?? slide.buttonName,
|
|
26
|
+
buttonLink: slide.buttonLink,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function simplifiedConfigFromHeroBanners(response) {
|
|
30
|
+
if (!response || typeof response !== "object")
|
|
31
|
+
return null;
|
|
32
|
+
const root = asObject(response["hero-banners"]);
|
|
33
|
+
if (!root)
|
|
34
|
+
return null;
|
|
35
|
+
const desktop = asArray(root.homeBanners)
|
|
36
|
+
.map(heroSlideRow)
|
|
37
|
+
.filter((row) => row !== null);
|
|
38
|
+
const mobile = asArray(root.appBanners)
|
|
39
|
+
.map(heroSlideRow)
|
|
40
|
+
.filter((row) => row !== null);
|
|
41
|
+
if (!desktop.length && !mobile.length)
|
|
42
|
+
return null;
|
|
43
|
+
const banners = {};
|
|
44
|
+
if (desktop.length)
|
|
45
|
+
banners.desktop = desktop;
|
|
46
|
+
if (mobile.length)
|
|
47
|
+
banners.mobile = mobile;
|
|
48
|
+
return { site: { banners } };
|
|
49
|
+
}
|
|
50
|
+
function mergeHomepageConfigLayers(...layers) {
|
|
51
|
+
const defined = layers.filter(Boolean);
|
|
52
|
+
if (!defined.length)
|
|
53
|
+
return null;
|
|
54
|
+
if (defined.length === 1)
|
|
55
|
+
return defined[0];
|
|
56
|
+
const merged = {};
|
|
57
|
+
for (const layer of defined) {
|
|
58
|
+
const site = asObject(layer.site) ?? {};
|
|
59
|
+
const mergedSite = asObject(merged.site) ?? {};
|
|
60
|
+
const layerBanners = asObject(site.banners);
|
|
61
|
+
if (layerBanners) {
|
|
62
|
+
mergedSite.banners = {
|
|
63
|
+
...(asObject(mergedSite.banners) ?? {}),
|
|
64
|
+
...layerBanners,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
for (const [key, value] of Object.entries(site)) {
|
|
68
|
+
if (key !== "banners" && value !== undefined)
|
|
69
|
+
mergedSite[key] = value;
|
|
70
|
+
}
|
|
71
|
+
if (Object.keys(mergedSite).length)
|
|
72
|
+
merged.site = mergedSite;
|
|
73
|
+
const sections = asObject(layer.sections);
|
|
74
|
+
if (sections) {
|
|
75
|
+
merged.sections = { ...(asObject(merged.sections) ?? {}), ...sections };
|
|
76
|
+
}
|
|
77
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
78
|
+
if (key === "site" || key === "sections")
|
|
79
|
+
continue;
|
|
80
|
+
if (value !== undefined)
|
|
81
|
+
merged[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return merged;
|
|
85
|
+
}
|
|
8
86
|
export const DEFAULT_PAGE_INPUT = defaultPageInputJson;
|
|
9
87
|
/**
|
|
10
88
|
* Merge CMS / main-project overrides onto package JSON defaults.
|
|
@@ -76,14 +154,19 @@ export function simplifiedHomepageConfigFromDynamicResponse(response) {
|
|
|
76
154
|
? simplified
|
|
77
155
|
: null;
|
|
78
156
|
}
|
|
157
|
+
/** CMS-only homepage config (no package JSON defaults). */
|
|
158
|
+
export function homepageConfigFromDynamicResponseNormalized(response) {
|
|
159
|
+
const legacy = response?.["homepage-config"] ?? null;
|
|
160
|
+
const fromSplit = simplifiedHomepageConfigFromDynamicResponse(response);
|
|
161
|
+
const fromHeroBanners = simplifiedConfigFromHeroBanners(response);
|
|
162
|
+
const raw = mergeHomepageConfigLayers(fromSplit, legacy, fromHeroBanners);
|
|
163
|
+
return normalizeHomepageConfig(raw);
|
|
164
|
+
}
|
|
79
165
|
/**
|
|
80
166
|
* Build `homepage-config` from Medusa `GET /store/dynamic-config` (main project only).
|
|
81
|
-
* Supports legacy `homepage-config
|
|
167
|
+
* Supports legacy `homepage-config`, split plugin keys, and `hero-banners`.
|
|
82
168
|
*/
|
|
83
169
|
export function homepageConfigFromDynamicResponse(response) {
|
|
84
|
-
const
|
|
85
|
-
const fromSplit = simplifiedHomepageConfigFromDynamicResponse(response);
|
|
86
|
-
const raw = fromSplit ?? legacy;
|
|
87
|
-
const normalized = normalizeHomepageConfig(raw);
|
|
170
|
+
const normalized = homepageConfigFromDynamicResponseNormalized(response);
|
|
88
171
|
return mergePageInputWithDefaults(normalized);
|
|
89
172
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import type { StorefrontPageInput } from "./page-input";
|
|
3
|
+
type Maybe<T> = T | null | undefined;
|
|
4
|
+
/** Cached CMS merge for the storefront (fetch once per request). */
|
|
5
|
+
export declare const loadStorefrontPageInput: () => Promise<StorefrontPageInput>;
|
|
6
|
+
export declare function resolveStorefrontPageInput(pageInput?: Maybe<StorefrontPageInput>): Promise<StorefrontPageInput>;
|
|
7
|
+
export declare function loadContactInfoFromStorefrontInput(pageInput?: Maybe<StorefrontPageInput>): Promise<import("./dynamic-config").ContactInfo | null>;
|
|
8
|
+
export declare function loadContactEmailFromStorefrontInput(pageInput?: Maybe<StorefrontPageInput>): Promise<string>;
|
|
9
|
+
export declare function loadHelpFaqFromStorefrontInput(pageInput?: Maybe<StorefrontPageInput>): Promise<import("./dynamic-config").HelpFaqConfig>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=storefront-cms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storefront-cms.d.ts","sourceRoot":"","sources":["../../../src/server/storefront-cms.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAGpB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAOvD,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,SAAS,CAAA;AAEpC,oEAAoE;AACpE,eAAO,MAAM,uBAAuB,QACxB,OAAO,CAAC,mBAAmB,CAItC,CAAA;AAED,wBAAsB,0BAA0B,CAC9C,SAAS,CAAC,EAAE,KAAK,CAAC,mBAAmB,CAAC,GACrC,OAAO,CAAC,mBAAmB,CAAC,CAE9B;AAED,wBAAsB,kCAAkC,CACtD,SAAS,CAAC,EAAE,KAAK,CAAC,mBAAmB,CAAC,0DAIvC;AAED,wBAAsB,mCAAmC,CACvD,SAAS,CAAC,EAAE,KAAK,CAAC,mBAAmB,CAAC,GACrC,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,wBAAsB,8BAA8B,CAClD,SAAS,CAAC,EAAE,KAAK,CAAC,mBAAmB,CAAC,qDAIvC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { cache } from "react";
|
|
3
|
+
import { getContactInfoFromPageInput, getHelpFaqFromPageInput, } from "./dynamic-config";
|
|
4
|
+
import { getDynamicConfig, homepageConfigFromDynamicResponse } from "./dynamic-config";
|
|
5
|
+
/** Cached CMS merge for the storefront (fetch once per request). */
|
|
6
|
+
export const loadStorefrontPageInput = cache(async () => {
|
|
7
|
+
const raw = await getDynamicConfig();
|
|
8
|
+
return homepageConfigFromDynamicResponse(raw);
|
|
9
|
+
});
|
|
10
|
+
export async function resolveStorefrontPageInput(pageInput) {
|
|
11
|
+
return (pageInput ?? (await loadStorefrontPageInput()));
|
|
12
|
+
}
|
|
13
|
+
export async function loadContactInfoFromStorefrontInput(pageInput) {
|
|
14
|
+
const resolved = await resolveStorefrontPageInput(pageInput);
|
|
15
|
+
return getContactInfoFromPageInput(resolved);
|
|
16
|
+
}
|
|
17
|
+
export async function loadContactEmailFromStorefrontInput(pageInput) {
|
|
18
|
+
const info = await loadContactInfoFromStorefrontInput(pageInput);
|
|
19
|
+
return info?.email ?? "";
|
|
20
|
+
}
|
|
21
|
+
export async function loadHelpFaqFromStorefrontInput(pageInput) {
|
|
22
|
+
const resolved = await resolveStorefrontPageInput(pageInput);
|
|
23
|
+
return getHelpFaqFromPageInput(resolved);
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "medusa-storefront-data",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Medusa storefront server data layer extracted from Next.js storefront",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,16 +17,17 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
|
-
"clean": "rm -rf dist"
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"test": "npm run build"
|
|
21
22
|
},
|
|
22
23
|
"peerDependencies": {
|
|
23
|
-
"next": ">=14",
|
|
24
|
-
"react": ">=18",
|
|
25
24
|
"@medusajs/js-sdk": ">=2",
|
|
26
25
|
"@medusajs/types": ">=2",
|
|
27
26
|
"medusa-reviews-logic": "*",
|
|
27
|
+
"medusa-services": "^1.0.0",
|
|
28
28
|
"medusa-wishlist-logic": "*",
|
|
29
|
-
"
|
|
29
|
+
"next": ">=14",
|
|
30
|
+
"react": ">=18"
|
|
30
31
|
},
|
|
31
32
|
"peerDependenciesMeta": {
|
|
32
33
|
"medusa-reviews-logic": {
|
|
@@ -38,13 +39,17 @@
|
|
|
38
39
|
"server-only": "^0.0.1"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"typescript": "^5.6.0",
|
|
42
|
-
"@types/react": "^18.3.0",
|
|
43
|
-
"next": "^15.0.0",
|
|
44
42
|
"@medusajs/js-sdk": "^2.0.0",
|
|
45
43
|
"@medusajs/types": "^2.0.0",
|
|
46
44
|
"@types/node": "^22.0.0",
|
|
47
|
-
"
|
|
45
|
+
"@types/react": "^19.0.0",
|
|
46
|
+
"@types/react-dom": "^19.0.0",
|
|
47
|
+
"medusa-services": "^1.2.1",
|
|
48
|
+
"medusa-ui-home-config": "^1.1.3",
|
|
49
|
+
"next": "^15.0.0",
|
|
50
|
+
"react": "^19.0.0",
|
|
51
|
+
"react-dom": "^19.0.0",
|
|
52
|
+
"typescript": "^5.6.0"
|
|
48
53
|
},
|
|
49
54
|
"exports": {
|
|
50
55
|
"./package.json": "./package.json",
|
|
@@ -53,6 +58,16 @@
|
|
|
53
58
|
"import": "./src/server/index.ts",
|
|
54
59
|
"default": "./src/server/index.ts"
|
|
55
60
|
},
|
|
61
|
+
"./help/sections/helpFaq": {
|
|
62
|
+
"types": "./dist/server/help-sections/help-faq.d.ts",
|
|
63
|
+
"import": "./src/server/help-sections/help-faq.ts",
|
|
64
|
+
"default": "./src/server/help-sections/help-faq.ts"
|
|
65
|
+
},
|
|
66
|
+
"./storefront-cms": {
|
|
67
|
+
"types": "./src/server/storefront-cms.ts",
|
|
68
|
+
"import": "./src/server/storefront-cms.ts",
|
|
69
|
+
"default": "./src/server/storefront-cms.ts"
|
|
70
|
+
},
|
|
56
71
|
"./home/sections/promoAnnouncements": {
|
|
57
72
|
"types": "./dist/server/home-sections/promo-announcements.d.ts",
|
|
58
73
|
"import": "./src/server/home-sections/promo-announcements.ts",
|
|
@@ -128,6 +143,11 @@
|
|
|
128
143
|
"import": "./src/server/home-sections/testimonials.ts",
|
|
129
144
|
"default": "./src/server/home-sections/testimonials.ts"
|
|
130
145
|
},
|
|
146
|
+
"./home/sections/customerStories": {
|
|
147
|
+
"types": "./dist/server/home-sections/customer-stories.d.ts",
|
|
148
|
+
"import": "./src/server/home-sections/customer-stories.ts",
|
|
149
|
+
"default": "./src/server/home-sections/customer-stories.ts"
|
|
150
|
+
},
|
|
131
151
|
"./home/sections/brandPillars": {
|
|
132
152
|
"types": "./dist/server/home-sections/brand-pillars.d.ts",
|
|
133
153
|
"import": "./src/server/home-sections/brand-pillars.ts",
|
|
@@ -167,16 +187,6 @@
|
|
|
167
187
|
"types": "./dist/server/home-sections/blog-posts.d.ts",
|
|
168
188
|
"import": "./src/server/home-sections/blog-posts.ts",
|
|
169
189
|
"default": "./src/server/home-sections/blog-posts.ts"
|
|
170
|
-
},
|
|
171
|
-
"./help/sections/helpFaq": {
|
|
172
|
-
"types": "./dist/server/help-sections/help-faq.d.ts",
|
|
173
|
-
"import": "./src/server/help-sections/help-faq.ts",
|
|
174
|
-
"default": "./src/server/help-sections/help-faq.ts"
|
|
175
|
-
},
|
|
176
|
-
"./storefront-cms": {
|
|
177
|
-
"types": "./src/server/storefront-cms.ts",
|
|
178
|
-
"import": "./src/server/storefront-cms.ts",
|
|
179
|
-
"default": "./src/server/storefront-cms.ts"
|
|
180
190
|
}
|
|
181
191
|
},
|
|
182
192
|
"main": "./dist/server/index.js",
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
} from "./homepage-section-defaults"
|
|
10
10
|
import type { DynamicConfigResponse, HomepageConfig } from "./homepage-config.types"
|
|
11
11
|
import { mergeSectionBlock } from "./config-merge"
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
mergePageInputWithDefaults,
|
|
14
|
+
resolvePageInput,
|
|
15
|
+
} from "./page-input"
|
|
13
16
|
|
|
14
17
|
export {
|
|
15
18
|
DEFAULT_PAGE_INPUT,
|
|
@@ -20,7 +23,6 @@ export {
|
|
|
20
23
|
type StorefrontPageInput,
|
|
21
24
|
} from "./page-input"
|
|
22
25
|
|
|
23
|
-
|
|
24
26
|
export const getDynamicConfig = cache(async (): Promise<DynamicConfigResponse | null> => {
|
|
25
27
|
try {
|
|
26
28
|
const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
|
|
@@ -80,6 +82,7 @@ export function getLogoFromPageInput(
|
|
|
80
82
|
|
|
81
83
|
|
|
82
84
|
|
|
85
|
+
/** Normalized hero slide used by storefront UI (`getHomeBannersFromPageInput`). */
|
|
83
86
|
export type HomepageBanner = {
|
|
84
87
|
image?: string
|
|
85
88
|
title?: string
|
|
@@ -131,8 +134,6 @@ export function getAppBannersFromPageInput(
|
|
|
131
134
|
return null
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
136
137
|
// Legacy function for backward compatibility
|
|
137
138
|
|
|
138
139
|
export function getWebsiteDescriptionFromPageInput(
|
|
@@ -504,6 +505,142 @@ export function getTestimonialsFromPageInput(
|
|
|
504
505
|
return { title, subtitle, testimonials }
|
|
505
506
|
}
|
|
506
507
|
|
|
508
|
+
export type CustomerStoryItem = {
|
|
509
|
+
id: string
|
|
510
|
+
image: string
|
|
511
|
+
text: string
|
|
512
|
+
name: string
|
|
513
|
+
rating?: number
|
|
514
|
+
location?: string
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function parseCustomerStoryRow(
|
|
518
|
+
item: unknown,
|
|
519
|
+
index: number
|
|
520
|
+
): CustomerStoryItem | null {
|
|
521
|
+
if (!item || typeof item !== "object") return null
|
|
522
|
+
const row =
|
|
523
|
+
(item as Record<string, unknown>).story ??
|
|
524
|
+
(item as Record<string, unknown>).review ??
|
|
525
|
+
item
|
|
526
|
+
const r = row as Record<string, unknown>
|
|
527
|
+
const text = String(
|
|
528
|
+
r.text ?? r["review-description"] ?? r.description ?? r.quote ?? ""
|
|
529
|
+
).trim()
|
|
530
|
+
const name = String(
|
|
531
|
+
r.name ??
|
|
532
|
+
r["review-user-name"] ??
|
|
533
|
+
r["user-name"] ??
|
|
534
|
+
r.user_name ??
|
|
535
|
+
r.author ??
|
|
536
|
+
""
|
|
537
|
+
).trim()
|
|
538
|
+
const image = String(
|
|
539
|
+
r.image ??
|
|
540
|
+
r["review-profile-image"] ??
|
|
541
|
+
r["profile-image"] ??
|
|
542
|
+
r.profile_image ??
|
|
543
|
+
""
|
|
544
|
+
).trim()
|
|
545
|
+
/** Quote + name required; image optional (admin Ratings often has no photo). */
|
|
546
|
+
if (!text || !name) return null
|
|
547
|
+
const ratingRaw = r.rating ?? r["review-rating"]
|
|
548
|
+
const rating =
|
|
549
|
+
ratingRaw != null && ratingRaw !== ""
|
|
550
|
+
? parseFloat(String(ratingRaw))
|
|
551
|
+
: undefined
|
|
552
|
+
const location = String(r.location ?? "").trim()
|
|
553
|
+
return {
|
|
554
|
+
id: String(r.id ?? `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${index + 1}`),
|
|
555
|
+
image: image || "/Logo.png",
|
|
556
|
+
text,
|
|
557
|
+
name,
|
|
558
|
+
rating: Number.isFinite(rating) ? Math.max(0, Math.min(5, rating!)) : undefined,
|
|
559
|
+
location: location || undefined,
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function parseCustomerStoriesFromBlock(
|
|
564
|
+
block: Record<string, unknown>
|
|
565
|
+
): CustomerStoryItem[] {
|
|
566
|
+
const raw =
|
|
567
|
+
block.items ??
|
|
568
|
+
block.stories ??
|
|
569
|
+
block.reviews ??
|
|
570
|
+
block["customer-stories"] ??
|
|
571
|
+
[]
|
|
572
|
+
if (!Array.isArray(raw)) return []
|
|
573
|
+
return raw
|
|
574
|
+
.map((item, index) => parseCustomerStoryRow(item, index))
|
|
575
|
+
.filter((row): row is CustomerStoryItem => row !== null)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function parseCustomerStoriesFromRatings(ratings: unknown): CustomerStoryItem[] {
|
|
579
|
+
if (!Array.isArray(ratings)) return []
|
|
580
|
+
return ratings
|
|
581
|
+
.map((item, index) => parseCustomerStoryRow(item, index))
|
|
582
|
+
.filter((row): row is CustomerStoryItem => row !== null)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function getHomepageRatingsArray(
|
|
586
|
+
pageInput?: HomepageConfig | null
|
|
587
|
+
): unknown[] {
|
|
588
|
+
const resolved = resolvePageInput(pageInput)
|
|
589
|
+
const fromResolved = resolved["ratings"]
|
|
590
|
+
if (Array.isArray(fromResolved) && fromResolved.length > 0) {
|
|
591
|
+
return fromResolved
|
|
592
|
+
}
|
|
593
|
+
const fromOverride = pageInput?.["ratings"]
|
|
594
|
+
if (Array.isArray(fromOverride) && fromOverride.length > 0) {
|
|
595
|
+
return fromOverride
|
|
596
|
+
}
|
|
597
|
+
return []
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export function getCustomerStoriesFromPageInput(
|
|
601
|
+
pageInput?: HomepageConfig | null
|
|
602
|
+
): {
|
|
603
|
+
title: string
|
|
604
|
+
eyebrow: string
|
|
605
|
+
items: CustomerStoryItem[]
|
|
606
|
+
} {
|
|
607
|
+
const homepageConfig = resolvePageInput(pageInput)
|
|
608
|
+
const block = getMergedSectionBlock("customerStories", pageInput)
|
|
609
|
+
const copy = parseSectionCopy(block)
|
|
610
|
+
const ratingTitle = String(homepageConfig["rating-title"] ?? "").trim()
|
|
611
|
+
const title = copy.title ?? copy.name ?? ratingTitle
|
|
612
|
+
const eyebrow = copy.eyebrow ?? ""
|
|
613
|
+
const defaults = DEFAULT_HOMEPAGE_SECTIONS.customerStories as Record<string, unknown>
|
|
614
|
+
|
|
615
|
+
/** Admin "Ratings" (`homepage-config.ratings[].review`) — always before merged section defaults. */
|
|
616
|
+
const ratingsItems = parseCustomerStoriesFromRatings(
|
|
617
|
+
getHomepageRatingsArray(pageInput)
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
/** CMS `sections.customerStories` only (no package default stories mixed in). */
|
|
621
|
+
const cmsSectionBlock =
|
|
622
|
+
findSectionBlock(getHomepageSectionsRoot(pageInput), "customerStories") ?? {}
|
|
623
|
+
const sectionItems = parseCustomerStoriesFromBlock(cmsSectionBlock)
|
|
624
|
+
|
|
625
|
+
let items =
|
|
626
|
+
ratingsItems.length > 0
|
|
627
|
+
? ratingsItems
|
|
628
|
+
: sectionItems.length > 0
|
|
629
|
+
? sectionItems
|
|
630
|
+
: parseCustomerStoriesFromBlock(block)
|
|
631
|
+
if (items.length === 0) {
|
|
632
|
+
items = parseCustomerStoriesFromBlock(
|
|
633
|
+
defaults as Record<string, unknown>
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
title: title || String(defaults.title ?? "Customer Stories"),
|
|
639
|
+
eyebrow: eyebrow || String(defaults.eyebrow ?? "WHAT PARENTS SAY"),
|
|
640
|
+
items,
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
507
644
|
|
|
508
645
|
export function getSocialLinksFromPageInput(
|
|
509
646
|
pageInput?: HomepageConfig | null
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Simplified banner shape used by hero sections (not legacy kebab-case CMS keys). */
|
|
2
|
+
export type HomepageBanner = {
|
|
3
|
+
image?: string
|
|
4
|
+
title?: string
|
|
5
|
+
subtitle?: string
|
|
6
|
+
description?: string
|
|
7
|
+
buttonName?: string
|
|
8
|
+
buttonLink?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ThemeHeroBannerFallbacks = {
|
|
12
|
+
home: HomepageBanner
|
|
13
|
+
app: HomepageBanner
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type HeroBannerSource = "dynamic-config" | "theme-copy" | "library-default"
|
|
17
|
+
|
|
18
|
+
export type ResolvedHeroBanners = {
|
|
19
|
+
homeBanners: HomepageBanner[]
|
|
20
|
+
appBanners: HomepageBanner[]
|
|
21
|
+
homeSource: HeroBannerSource
|
|
22
|
+
appSource: HeroBannerSource
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hasBannerData(
|
|
26
|
+
banner: HomepageBanner | null | undefined
|
|
27
|
+
): boolean {
|
|
28
|
+
if (!banner) return false
|
|
29
|
+
return Boolean(
|
|
30
|
+
banner.image?.trim() ||
|
|
31
|
+
banner.title?.trim() ||
|
|
32
|
+
banner.subtitle?.trim() ||
|
|
33
|
+
banner.description?.trim() ||
|
|
34
|
+
banner.buttonName?.trim() ||
|
|
35
|
+
banner.buttonLink?.trim()
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whole-banner priority (not per-field):
|
|
41
|
+
* 1. dynamic-config 2. theme copy.ts 3. package library defaults
|
|
42
|
+
*/
|
|
43
|
+
export function pickHeroBannerBySource(
|
|
44
|
+
cms: HomepageBanner | null | undefined,
|
|
45
|
+
theme: HomepageBanner,
|
|
46
|
+
library: HomepageBanner
|
|
47
|
+
): { banner: HomepageBanner; source: HeroBannerSource } {
|
|
48
|
+
if (cms && hasBannerData(cms)) {
|
|
49
|
+
return { banner: cms, source: "dynamic-config" }
|
|
50
|
+
}
|
|
51
|
+
if (hasBannerData(theme)) {
|
|
52
|
+
return { banner: theme, source: "theme-copy" }
|
|
53
|
+
}
|
|
54
|
+
return { banner: library, source: "library-default" }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveHeroBannerWithPriority(
|
|
58
|
+
cms: HomepageBanner | null | undefined,
|
|
59
|
+
theme: HomepageBanner,
|
|
60
|
+
library: HomepageBanner
|
|
61
|
+
): HomepageBanner {
|
|
62
|
+
return pickHeroBannerBySource(cms, theme, library).banner
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "server-only"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getCustomerStoriesFromPageInput,
|
|
5
|
+
getHomeSectionsCopyFromPageInput,
|
|
6
|
+
} from "../dynamic-config"
|
|
7
|
+
import type { SectionLoaderOptions } from "./shared"
|
|
8
|
+
|
|
9
|
+
export type CustomerStoriesSectionData = {
|
|
10
|
+
customerStories: ReturnType<typeof getCustomerStoriesFromPageInput>
|
|
11
|
+
copy: ReturnType<typeof getHomeSectionsCopyFromPageInput>["customerStories"]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadCustomerStoriesSectionData(
|
|
15
|
+
_countryCode: string,
|
|
16
|
+
options?: SectionLoaderOptions
|
|
17
|
+
): Promise<CustomerStoriesSectionData> {
|
|
18
|
+
const pageInput = options?.pageInput ?? null
|
|
19
|
+
return {
|
|
20
|
+
customerStories: getCustomerStoriesFromPageInput(pageInput),
|
|
21
|
+
copy: getHomeSectionsCopyFromPageInput(pageInput).customerStories,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -212,6 +212,22 @@ export type HomepageVideoStoriesSection = HomepageSectionCopyFields & {
|
|
|
212
212
|
"video-stories"?: HomepageVideoStoryEntry[]
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
export type HomepageCustomerStoryItem = {
|
|
216
|
+
id?: string
|
|
217
|
+
image?: string
|
|
218
|
+
text?: string
|
|
219
|
+
name?: string
|
|
220
|
+
rating?: number | string
|
|
221
|
+
location?: string
|
|
222
|
+
story?: HomepageCustomerStoryItem
|
|
223
|
+
review?: HomepageCustomerStoryItem
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type HomepageCustomerStoriesSection = HomepageSectionCopyFields & {
|
|
227
|
+
items?: HomepageCustomerStoryItem[]
|
|
228
|
+
stories?: HomepageCustomerStoryItem[]
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
/** Per-section blocks under `homepage-config.sections`. */
|
|
216
232
|
export type HomepageSectionsConfig = {
|
|
217
233
|
promoAnnouncements?: HomepagePromoAnnouncementsSection
|
|
@@ -228,6 +244,7 @@ export type HomepageSectionsConfig = {
|
|
|
228
244
|
themeDresses?: HomepageSectionCopyFields
|
|
229
245
|
brandMarquee?: HomepageSectionCopyFields
|
|
230
246
|
aboutBrand?: HomepageAboutBrandSection
|
|
247
|
+
customerStories?: HomepageCustomerStoriesSection
|
|
231
248
|
testimonials?: HomepageTestimonialsSection
|
|
232
249
|
brandPillars?: HomepageBrandPillarsSection
|
|
233
250
|
instagramPosts?: HomepageInstagramPostsSection
|
|
@@ -457,6 +474,7 @@ export const HOMEPAGE_SECTION_IDS = [
|
|
|
457
474
|
"themeDresses",
|
|
458
475
|
"brandMarquee",
|
|
459
476
|
"aboutBrand",
|
|
477
|
+
"customerStories",
|
|
460
478
|
"testimonials",
|
|
461
479
|
"brandPillars",
|
|
462
480
|
"instagramPosts",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import helpFaqDefaults from "./help-faq-section-defaults.json"
|
|
2
|
+
import { customerStoriesSectionDefaults } from "medusa-ui-home-config/sections/customerStories"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Default `homepage-config.sections` blocks.
|
|
@@ -71,6 +72,7 @@ export const DEFAULT_HOMEPAGE_SECTIONS = {
|
|
|
71
72
|
{ label: "Products", value: "3000+" },
|
|
72
73
|
],
|
|
73
74
|
},
|
|
75
|
+
customerStories: customerStoriesSectionDefaults,
|
|
74
76
|
testimonials: {
|
|
75
77
|
title: "What Clients Talk About Us",
|
|
76
78
|
description: "The Trust We've Earned",
|
package/src/server/page-input.ts
CHANGED
|
@@ -14,6 +14,87 @@ function asObject(v: unknown): Loose | null {
|
|
|
14
14
|
return v && typeof v === "object" && !Array.isArray(v) ? (v as Loose) : null
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function asArray(v: unknown): unknown[] {
|
|
18
|
+
return Array.isArray(v) ? v : []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `hero-banners.homeBanners[].slide` from medusa-plugin-dynamic-config. */
|
|
22
|
+
function heroSlideRow(row: unknown): Loose | null {
|
|
23
|
+
const rowObj = asObject(row)
|
|
24
|
+
if (!rowObj) return null
|
|
25
|
+
const slide = asObject(rowObj.slide) ?? rowObj
|
|
26
|
+
const image = slide.image
|
|
27
|
+
if (image == null || image === "") return null
|
|
28
|
+
return {
|
|
29
|
+
image,
|
|
30
|
+
title: slide.title,
|
|
31
|
+
subtitle: slide.subtitle,
|
|
32
|
+
description: slide.description,
|
|
33
|
+
buttonText: slide.buttonText ?? slide.buttonName,
|
|
34
|
+
buttonLink: slide.buttonLink,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function simplifiedConfigFromHeroBanners(
|
|
39
|
+
response: DynamicConfigResponse | null | undefined
|
|
40
|
+
): HomepageConfig | null {
|
|
41
|
+
if (!response || typeof response !== "object") return null
|
|
42
|
+
const root = asObject((response as Loose)["hero-banners"])
|
|
43
|
+
if (!root) return null
|
|
44
|
+
|
|
45
|
+
const desktop = asArray(root.homeBanners)
|
|
46
|
+
.map(heroSlideRow)
|
|
47
|
+
.filter((row): row is Loose => row !== null)
|
|
48
|
+
const mobile = asArray(root.appBanners)
|
|
49
|
+
.map(heroSlideRow)
|
|
50
|
+
.filter((row): row is Loose => row !== null)
|
|
51
|
+
|
|
52
|
+
if (!desktop.length && !mobile.length) return null
|
|
53
|
+
|
|
54
|
+
const banners: Loose = {}
|
|
55
|
+
if (desktop.length) banners.desktop = desktop
|
|
56
|
+
if (mobile.length) banners.mobile = mobile
|
|
57
|
+
|
|
58
|
+
return { site: { banners } } as HomepageConfig
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mergeHomepageConfigLayers(
|
|
62
|
+
...layers: Array<HomepageConfig | null | undefined>
|
|
63
|
+
): HomepageConfig | null {
|
|
64
|
+
const defined = layers.filter(Boolean) as HomepageConfig[]
|
|
65
|
+
if (!defined.length) return null
|
|
66
|
+
if (defined.length === 1) return defined[0]
|
|
67
|
+
|
|
68
|
+
const merged: Loose = {}
|
|
69
|
+
for (const layer of defined) {
|
|
70
|
+
const site = asObject((layer as Loose).site) ?? {}
|
|
71
|
+
const mergedSite = asObject(merged.site) ?? {}
|
|
72
|
+
const layerBanners = asObject(site.banners)
|
|
73
|
+
if (layerBanners) {
|
|
74
|
+
mergedSite.banners = {
|
|
75
|
+
...(asObject(mergedSite.banners) ?? {}),
|
|
76
|
+
...layerBanners,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const [key, value] of Object.entries(site)) {
|
|
80
|
+
if (key !== "banners" && value !== undefined) mergedSite[key] = value
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(mergedSite).length) merged.site = mergedSite
|
|
83
|
+
|
|
84
|
+
const sections = asObject((layer as Loose).sections)
|
|
85
|
+
if (sections) {
|
|
86
|
+
merged.sections = { ...(asObject(merged.sections) ?? {}), ...sections }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const [key, value] of Object.entries(layer as Loose)) {
|
|
90
|
+
if (key === "site" || key === "sections") continue
|
|
91
|
+
if (value !== undefined) merged[key] = value
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return merged as HomepageConfig
|
|
96
|
+
}
|
|
97
|
+
|
|
17
98
|
/** Full `homepage-config` blob passed from the main project into pages and layout. */
|
|
18
99
|
export type StorefrontPageInput = HomepageConfig
|
|
19
100
|
|
|
@@ -106,16 +187,24 @@ export function simplifiedHomepageConfigFromDynamicResponse(
|
|
|
106
187
|
: null
|
|
107
188
|
}
|
|
108
189
|
|
|
190
|
+
/** CMS-only homepage config (no package JSON defaults). */
|
|
191
|
+
export function homepageConfigFromDynamicResponseNormalized(
|
|
192
|
+
response: DynamicConfigResponse | null | undefined
|
|
193
|
+
): HomepageConfig {
|
|
194
|
+
const legacy = response?.["homepage-config"] ?? null
|
|
195
|
+
const fromSplit = simplifiedHomepageConfigFromDynamicResponse(response)
|
|
196
|
+
const fromHeroBanners = simplifiedConfigFromHeroBanners(response)
|
|
197
|
+
const raw = mergeHomepageConfigLayers(fromSplit, legacy, fromHeroBanners)
|
|
198
|
+
return normalizeHomepageConfig(raw)
|
|
199
|
+
}
|
|
200
|
+
|
|
109
201
|
/**
|
|
110
202
|
* Build `homepage-config` from Medusa `GET /store/dynamic-config` (main project only).
|
|
111
|
-
* Supports legacy `homepage-config
|
|
203
|
+
* Supports legacy `homepage-config`, split plugin keys, and `hero-banners`.
|
|
112
204
|
*/
|
|
113
205
|
export function homepageConfigFromDynamicResponse(
|
|
114
206
|
response: DynamicConfigResponse | null | undefined
|
|
115
207
|
): StorefrontPageInput {
|
|
116
|
-
const
|
|
117
|
-
const fromSplit = simplifiedHomepageConfigFromDynamicResponse(response)
|
|
118
|
-
const raw = fromSplit ?? legacy
|
|
119
|
-
const normalized = normalizeHomepageConfig(raw)
|
|
208
|
+
const normalized = homepageConfigFromDynamicResponseNormalized(response)
|
|
120
209
|
return mergePageInputWithDefaults(normalized)
|
|
121
210
|
}
|