medusa-storefront-data 2.0.0 → 2.3.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/dist/server/customer.d.ts +1 -1
- package/dist/server/customer.d.ts.map +1 -1
- package/dist/server/customer.js +1 -0
- package/dist/server/dynamic-config.d.ts +78 -1
- package/dist/server/dynamic-config.d.ts.map +1 -1
- package/dist/server/dynamic-config.js +447 -38
- package/dist/server/fulfillment.d.ts +1 -1
- package/dist/server/fulfillment.d.ts.map +1 -1
- package/dist/server/home.d.ts +31 -4
- package/dist/server/home.d.ts.map +1 -1
- package/dist/server/home.js +89 -10
- package/dist/server/homepage-section-defaults.d.ts +155 -0
- package/dist/server/homepage-section-defaults.d.ts.map +1 -0
- package/dist/server/homepage-section-defaults.js +149 -0
- package/dist/server/swaps.d.ts +6 -5
- package/dist/server/swaps.d.ts.map +1 -1
- package/dist/server/swaps.js +1 -1
- package/dist/server/wishlist.js +1 -1
- package/package.json +2 -2
- package/src/server/customer.ts +6 -1
- package/src/server/dynamic-config.ts +545 -45
- package/src/server/home.ts +155 -12
- package/src/server/homepage-section-defaults.ts +160 -0
- package/src/server/swaps.ts +8 -6
- package/src/server/wishlist.ts +1 -1
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { cache } from "react"
|
|
4
4
|
import { getAuthHeaders } from "../cookies"
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_HOMEPAGE_SECTIONS,
|
|
7
|
+
HOME_SECTION_COPY_ID_LIST,
|
|
8
|
+
type DefaultHomeSectionId,
|
|
9
|
+
} from "./homepage-section-defaults"
|
|
5
10
|
|
|
6
11
|
// Define the structure based on the actual API response and client usage
|
|
7
12
|
export interface DynamicConfig {
|
|
@@ -62,6 +67,8 @@ export interface DynamicConfig {
|
|
|
62
67
|
"promo-value"?: string
|
|
63
68
|
"promo-active"?: boolean
|
|
64
69
|
}
|
|
70
|
+
/** Central homepage section copy (Option 1). Keys: camelCase or kebab-case section ids. */
|
|
71
|
+
sections?: Record<string, unknown>
|
|
65
72
|
// Allow for other properties
|
|
66
73
|
[key: string]: any
|
|
67
74
|
}
|
|
@@ -186,10 +193,10 @@ export const getHomeBannersFromConfig = async (): Promise<Array<{
|
|
|
186
193
|
const banner = item["homepage-banner"]
|
|
187
194
|
return {
|
|
188
195
|
image: banner?.["homepage-banner-image"],
|
|
189
|
-
title: banner?.["homepage-banner-title"],
|
|
190
|
-
subtitle: banner?.["homepage-banner-subtitle"],
|
|
191
|
-
description: banner?.["homepage-banner-description"],
|
|
192
|
-
buttonName: banner?.["homepage-banner-button-name"],
|
|
196
|
+
title: normalizeCmsText(banner?.["homepage-banner-title"]),
|
|
197
|
+
subtitle: normalizeCmsText(banner?.["homepage-banner-subtitle"]),
|
|
198
|
+
description: normalizeCmsText(banner?.["homepage-banner-description"]),
|
|
199
|
+
buttonName: normalizeBannerButtonName(banner?.["homepage-banner-button-name"]),
|
|
193
200
|
buttonLink: banner?.["homepage-banner-button-link"],
|
|
194
201
|
}
|
|
195
202
|
})
|
|
@@ -218,10 +225,10 @@ export const getAppBannersFromConfig = async (): Promise<Array<{
|
|
|
218
225
|
const banner = item["app-banner"]
|
|
219
226
|
return {
|
|
220
227
|
image: banner?.["app-banner-image"],
|
|
221
|
-
title: banner?.["app-banner-title"],
|
|
222
|
-
subtitle: banner?.["app-banner-subtitle"],
|
|
223
|
-
description: banner?.["app-banner-description"],
|
|
224
|
-
buttonName: banner?.["app-banner-button-name"],
|
|
228
|
+
title: normalizeCmsText(banner?.["app-banner-title"]),
|
|
229
|
+
subtitle: normalizeCmsText(banner?.["app-banner-subtitle"]),
|
|
230
|
+
description: normalizeCmsText(banner?.["app-banner-description"]),
|
|
231
|
+
buttonName: normalizeBannerButtonName(banner?.["app-banner-button-name"]),
|
|
225
232
|
buttonLink: banner?.["app-banner-button-link"],
|
|
226
233
|
}
|
|
227
234
|
})
|
|
@@ -253,32 +260,260 @@ export const getWebsiteDescriptionFromConfig = async (): Promise<string | null>
|
|
|
253
260
|
}
|
|
254
261
|
}
|
|
255
262
|
|
|
256
|
-
|
|
263
|
+
/** Why choose us — defaults + `sections.whyChooseUs` override. */
|
|
264
|
+
export const getFeaturesFromConfig = async (): Promise<{
|
|
265
|
+
title: string
|
|
266
|
+
features: Array<{ name: string; icon: string }>
|
|
267
|
+
}> => {
|
|
257
268
|
try {
|
|
258
|
-
const
|
|
259
|
-
const
|
|
269
|
+
const block = await getMergedSectionBlock("whyChooseUs")
|
|
270
|
+
const copy = parseSectionCopy(block)
|
|
271
|
+
const title = copy.title ?? copy.name ?? ""
|
|
272
|
+
const featuresRaw = block.features ?? block.items ?? []
|
|
273
|
+
const features = parseFeatureIconList(featuresRaw)
|
|
260
274
|
|
|
261
|
-
|
|
275
|
+
return { title, features }
|
|
276
|
+
} catch {
|
|
277
|
+
const block = DEFAULT_HOMEPAGE_SECTIONS.whyChooseUs
|
|
278
|
+
const copy = parseSectionCopy(block)
|
|
279
|
+
return {
|
|
280
|
+
title: copy.title ?? copy.name ?? "",
|
|
281
|
+
features: parseFeatureIconList(block.features ?? []),
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
262
285
|
|
|
263
|
-
|
|
264
|
-
|
|
286
|
+
function parseFeatureIconList(raw: unknown): Array<{ name: string; icon: string }> {
|
|
287
|
+
if (!Array.isArray(raw)) return []
|
|
288
|
+
return raw
|
|
289
|
+
.map((item: unknown) => {
|
|
290
|
+
if (!item || typeof item !== "object") return null
|
|
291
|
+
const feature = (item as Record<string, unknown>).feature ?? item
|
|
292
|
+
const row = feature as Record<string, unknown>
|
|
293
|
+
const name = normalizeCmsText(row["feature-name"] ?? row.name) ?? ""
|
|
294
|
+
const icon = normalizeCmsText(row["feature-icon"] ?? row.icon) ?? ""
|
|
295
|
+
return name && icon ? { name, icon } : null
|
|
296
|
+
})
|
|
297
|
+
.filter(Boolean) as Array<{ name: string; icon: string }>
|
|
298
|
+
}
|
|
265
299
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
300
|
+
const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
|
301
|
+
|
|
302
|
+
/** Fix common UTF-8 mojibake from CMS copy-paste (e.g. em dash, apostrophe). */
|
|
303
|
+
function normalizeCmsText(value: unknown): string | undefined {
|
|
304
|
+
if (typeof value !== "string" || !value.trim()) return undefined
|
|
305
|
+
return value
|
|
306
|
+
.trim()
|
|
307
|
+
.replace(/ΓÇö/g, "—")
|
|
308
|
+
.replace(/ΓÇÖ/g, "'")
|
|
309
|
+
.replace(/ΓÇ£|ΓÇ¥/g, '"')
|
|
310
|
+
.replace(/≡ƒÿÄ≡ƒæòΓ£¿/g, " 👕✨")
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** CMS copy for a homepage section (title, name, text, description, etc.). */
|
|
314
|
+
export type SectionCopy = {
|
|
315
|
+
title: string | null
|
|
316
|
+
name: string | null
|
|
317
|
+
text: string | null
|
|
318
|
+
description: string | null
|
|
319
|
+
subtitle: string | null
|
|
320
|
+
eyebrow: string | null
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const EMPTY_SECTION_COPY: SectionCopy = {
|
|
324
|
+
title: null,
|
|
325
|
+
name: null,
|
|
326
|
+
text: null,
|
|
327
|
+
description: null,
|
|
328
|
+
subtitle: null,
|
|
329
|
+
eyebrow: null,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function parseSectionCopy(block: unknown): SectionCopy {
|
|
333
|
+
if (!block || typeof block !== "object") return { ...EMPTY_SECTION_COPY }
|
|
334
|
+
const row = block as Record<string, unknown>
|
|
335
|
+
return {
|
|
336
|
+
title: normalizeCmsText(row.title) ?? null,
|
|
337
|
+
name: normalizeCmsText(row.name) ?? null,
|
|
338
|
+
text: normalizeCmsText(row.text) ?? null,
|
|
339
|
+
description: normalizeCmsText(row.description) ?? null,
|
|
340
|
+
subtitle: normalizeCmsText(row.subtitle) ?? null,
|
|
341
|
+
eyebrow: normalizeCmsText(row.eyebrow) ?? null,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export type HomeSectionCopyId = DefaultHomeSectionId
|
|
346
|
+
|
|
347
|
+
export type HomeSectionsCopy = Record<HomeSectionCopyId, SectionCopy>
|
|
348
|
+
|
|
349
|
+
const HOME_SECTION_COPY_IDS = new Set<HomeSectionCopyId>(HOME_SECTION_COPY_ID_LIST)
|
|
350
|
+
|
|
351
|
+
function sectionIdFromConfigKey(key: string): HomeSectionCopyId | null {
|
|
352
|
+
const id = key.replace(/-([a-z])/g, (_, c: string) =>
|
|
353
|
+
c.toUpperCase()
|
|
354
|
+
) as HomeSectionCopyId
|
|
355
|
+
return HOME_SECTION_COPY_IDS.has(id) ? id : null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function getHomepageSectionsRoot(): Promise<Record<string, unknown>> {
|
|
359
|
+
const config = await getDynamicConfig()
|
|
360
|
+
const root = config?.["homepage-config"]?.sections
|
|
361
|
+
return root && typeof root === "object"
|
|
362
|
+
? (root as Record<string, unknown>)
|
|
363
|
+
: {}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function findSectionBlock(
|
|
367
|
+
root: Record<string, unknown>,
|
|
368
|
+
id: HomeSectionCopyId
|
|
369
|
+
): Record<string, unknown> | null {
|
|
370
|
+
for (const [key, value] of Object.entries(root)) {
|
|
371
|
+
if (sectionIdFromConfigKey(key) !== id) continue
|
|
372
|
+
if (value && typeof value === "object") {
|
|
373
|
+
return value as Record<string, unknown>
|
|
374
|
+
}
|
|
375
|
+
if (typeof value === "string") {
|
|
376
|
+
return { text: value }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Merge CMS section block onto package defaults (override wins when set). */
|
|
383
|
+
export function mergeSectionBlock(
|
|
384
|
+
defaults: Record<string, unknown>,
|
|
385
|
+
override: Record<string, unknown> | null
|
|
386
|
+
): Record<string, unknown> {
|
|
387
|
+
if (!override) return { ...defaults }
|
|
388
|
+
const merged = { ...defaults }
|
|
389
|
+
for (const [key, value] of Object.entries(override)) {
|
|
390
|
+
if (value === undefined || value === null) continue
|
|
391
|
+
if (Array.isArray(value)) {
|
|
392
|
+
if (value.length > 0) merged[key] = value
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
if (typeof value === "string") {
|
|
396
|
+
if (value.trim()) merged[key] = value.trim()
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
400
|
+
merged[key] = value
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
if (typeof value === "object") {
|
|
404
|
+
const base =
|
|
405
|
+
merged[key] && typeof merged[key] === "object" && !Array.isArray(merged[key])
|
|
406
|
+
? (merged[key] as Record<string, unknown>)
|
|
407
|
+
: {}
|
|
408
|
+
merged[key] = mergeSectionBlock(base, value as Record<string, unknown>)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return merged
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function getMergedSectionBlock(
|
|
415
|
+
id: HomeSectionCopyId
|
|
416
|
+
): Promise<Record<string, unknown>> {
|
|
417
|
+
const root = await getHomepageSectionsRoot()
|
|
418
|
+
const defaults = DEFAULT_HOMEPAGE_SECTIONS[id] ?? {}
|
|
419
|
+
const override = findSectionBlock(root, id)
|
|
420
|
+
return mergeSectionBlock(
|
|
421
|
+
{ ...defaults } as Record<string, unknown>,
|
|
422
|
+
override
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function mergeSectionCopy(base: SectionCopy, patch: SectionCopy): SectionCopy {
|
|
427
|
+
const pick = (override: string | null, fallback: string | null) =>
|
|
428
|
+
override ?? fallback
|
|
429
|
+
return {
|
|
430
|
+
title: pick(patch.title ?? patch.name, base.title ?? base.name),
|
|
431
|
+
name: pick(patch.name, base.name),
|
|
432
|
+
text: pick(patch.text, base.text),
|
|
433
|
+
description: pick(patch.description, base.description),
|
|
434
|
+
subtitle: pick(patch.subtitle, base.subtitle),
|
|
435
|
+
eyebrow: pick(patch.eyebrow, base.eyebrow),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function parseAnnouncementMessages(block: Record<string, unknown> | null): string[] {
|
|
440
|
+
if (!block) return []
|
|
441
|
+
const raw = block.messages ?? block.items ?? block["announcement-messages"]
|
|
442
|
+
if (!Array.isArray(raw)) return []
|
|
443
|
+
return raw
|
|
444
|
+
.map((item: unknown) => {
|
|
445
|
+
if (typeof item === "string") return item.trim()
|
|
446
|
+
if (item && typeof item === "object") {
|
|
447
|
+
const row = item as Record<string, unknown>
|
|
448
|
+
const nested = (row.message ?? row.text) as unknown
|
|
449
|
+
return typeof nested === "string" ? nested.trim() : ""
|
|
271
450
|
}
|
|
272
|
-
|
|
451
|
+
return ""
|
|
452
|
+
})
|
|
453
|
+
.filter(Boolean)
|
|
454
|
+
}
|
|
273
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Merged section copy: package defaults + `homepage-config.sections` overrides.
|
|
458
|
+
*/
|
|
459
|
+
export const getHomeSectionsCopyFromConfig = async (): Promise<HomeSectionsCopy> => {
|
|
460
|
+
try {
|
|
461
|
+
const copy = {} as HomeSectionsCopy
|
|
462
|
+
for (const id of HOME_SECTION_COPY_ID_LIST) {
|
|
463
|
+
const block = await getMergedSectionBlock(id)
|
|
464
|
+
copy[id] = parseSectionCopy(block)
|
|
465
|
+
}
|
|
466
|
+
return copy
|
|
467
|
+
} catch {
|
|
468
|
+
const fallback = {} as HomeSectionsCopy
|
|
469
|
+
for (const id of HOME_SECTION_COPY_ID_LIST) {
|
|
470
|
+
fallback[id] = parseSectionCopy(DEFAULT_HOMEPAGE_SECTIONS[id])
|
|
471
|
+
}
|
|
472
|
+
return fallback
|
|
473
|
+
}
|
|
474
|
+
}
|
|
274
475
|
|
|
275
|
-
|
|
276
|
-
|
|
476
|
+
export type TrustFeaturesConfig = {
|
|
477
|
+
title: string | null
|
|
478
|
+
description: string | null
|
|
479
|
+
features: Array<{ name: string; icon: string }>
|
|
480
|
+
}
|
|
277
481
|
|
|
278
|
-
|
|
482
|
+
/** Trust badge row — defaults + `sections.features` override. */
|
|
483
|
+
export const getTrustFeaturesFromConfig = async (): Promise<TrustFeaturesConfig> => {
|
|
484
|
+
try {
|
|
485
|
+
const block = await getMergedSectionBlock("features")
|
|
486
|
+
const copy = parseSectionCopy(block)
|
|
487
|
+
const title = copy.title ?? copy.name
|
|
488
|
+
const description = copy.description ?? copy.text
|
|
489
|
+
const rawList = block.features ?? block.items ?? []
|
|
490
|
+
const features = parseFeatureIconList(rawList)
|
|
491
|
+
|
|
492
|
+
return { title: title ?? null, description: description ?? null, features }
|
|
493
|
+
} catch {
|
|
494
|
+
const block = DEFAULT_HOMEPAGE_SECTIONS.features
|
|
495
|
+
const copy = parseSectionCopy(block)
|
|
496
|
+
return {
|
|
497
|
+
title: copy.title,
|
|
498
|
+
description: copy.description,
|
|
499
|
+
features: parseFeatureIconList(block.features ?? []),
|
|
500
|
+
}
|
|
279
501
|
}
|
|
280
502
|
}
|
|
281
503
|
|
|
504
|
+
function normalizeBannerButtonName(value: unknown): string | undefined {
|
|
505
|
+
const text = normalizeCmsText(value)
|
|
506
|
+
if (!text) return undefined
|
|
507
|
+
if (/^shop\s+now\s*1$/i.test(text)) return "Shop Now"
|
|
508
|
+
return text
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function normalizeConfigEmail(value: unknown): string | undefined {
|
|
512
|
+
if (typeof value !== "string") return undefined
|
|
513
|
+
const trimmed = value.trim()
|
|
514
|
+
return EMAIL_PATTERN.test(trimmed) ? trimmed : undefined
|
|
515
|
+
}
|
|
516
|
+
|
|
282
517
|
export const getContactInfoFromConfig = async (): Promise<{ phone?: string; email?: string; address?: string } | null> => {
|
|
283
518
|
try {
|
|
284
519
|
const config = await getDynamicConfig()
|
|
@@ -293,11 +528,7 @@ export const getContactInfoFromConfig = async (): Promise<{ phone?: string; emai
|
|
|
293
528
|
|
|
294
529
|
return {
|
|
295
530
|
phone: contactConfig["contact-phone"],
|
|
296
|
-
email:
|
|
297
|
-
typeof contactConfig["contact-email"] === "string" &&
|
|
298
|
-
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contactConfig["contact-email"].trim())
|
|
299
|
-
? contactConfig["contact-email"].trim()
|
|
300
|
-
: undefined,
|
|
531
|
+
email: normalizeConfigEmail(contactConfig["contact-email"]),
|
|
301
532
|
address: contactConfig["contact-address"],
|
|
302
533
|
}
|
|
303
534
|
} catch (error) {
|
|
@@ -306,6 +537,65 @@ export const getContactInfoFromConfig = async (): Promise<{ phone?: string; emai
|
|
|
306
537
|
}
|
|
307
538
|
}
|
|
308
539
|
|
|
540
|
+
function normalizeVideoStoryUrl(entry: unknown): string | null {
|
|
541
|
+
if (typeof entry === "string" && entry.trim()) return entry.trim()
|
|
542
|
+
if (!entry || typeof entry !== "object") return null
|
|
543
|
+
const row = entry as Record<string, unknown>
|
|
544
|
+
const nested =
|
|
545
|
+
(row["video-story"] as Record<string, unknown> | undefined) ??
|
|
546
|
+
(row["video_stories"] as Record<string, unknown> | undefined) ??
|
|
547
|
+
row
|
|
548
|
+
const url =
|
|
549
|
+
nested["video-url"] ??
|
|
550
|
+
nested["video_url"] ??
|
|
551
|
+
nested.url ??
|
|
552
|
+
nested.src ??
|
|
553
|
+
nested.video
|
|
554
|
+
return typeof url === "string" && url.trim() ? url.trim() : null
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Homepage video stories/reviews carousel (`homepage-config.video-stories`).
|
|
559
|
+
*/
|
|
560
|
+
export const getVideoStoriesFromConfig = async (): Promise<{
|
|
561
|
+
title: string
|
|
562
|
+
videoUrls: string[]
|
|
563
|
+
} | null> => {
|
|
564
|
+
try {
|
|
565
|
+
const config = await getDynamicConfig()
|
|
566
|
+
const homepageConfig = config?.["homepage-config"]
|
|
567
|
+
if (!homepageConfig) return null
|
|
568
|
+
|
|
569
|
+
const block = await getMergedSectionBlock("videoStories")
|
|
570
|
+
const copy = parseSectionCopy(block)
|
|
571
|
+
const title = copy.title ?? copy.name ?? ""
|
|
572
|
+
|
|
573
|
+
const raw =
|
|
574
|
+
block["video-urls"] ??
|
|
575
|
+
block.videoUrls ??
|
|
576
|
+
block.videos ??
|
|
577
|
+
block["video-stories"] ??
|
|
578
|
+
homepageConfig["video-stories"] ??
|
|
579
|
+
homepageConfig["video-stories-array"] ??
|
|
580
|
+
homepageConfig["video_stories"] ??
|
|
581
|
+
[]
|
|
582
|
+
|
|
583
|
+
const list = Array.isArray(raw) ? raw : [raw]
|
|
584
|
+
const videoUrls = list
|
|
585
|
+
.map(normalizeVideoStoryUrl)
|
|
586
|
+
.filter((url): url is string => Boolean(url))
|
|
587
|
+
|
|
588
|
+
return { title, videoUrls }
|
|
589
|
+
} catch {
|
|
590
|
+
const block = DEFAULT_HOMEPAGE_SECTIONS.videoStories
|
|
591
|
+
const copy = parseSectionCopy(block)
|
|
592
|
+
return {
|
|
593
|
+
title: copy.title ?? copy.name ?? "",
|
|
594
|
+
videoUrls: [],
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
309
599
|
export const getTestimonialsFromConfig = async (): Promise<{ title: string; testimonials: Array<{ id: string; text: string; name: string; rating: number; avatar?: string }> } | null> => {
|
|
310
600
|
try {
|
|
311
601
|
const config = await getDynamicConfig()
|
|
@@ -313,26 +603,30 @@ export const getTestimonialsFromConfig = async (): Promise<{ title: string; test
|
|
|
313
603
|
|
|
314
604
|
if (!homepageConfig) return null
|
|
315
605
|
|
|
316
|
-
const
|
|
606
|
+
const block = await getMergedSectionBlock("testimonials")
|
|
607
|
+
const copy = parseSectionCopy(block)
|
|
608
|
+
const title = copy.title ?? copy.name ?? ""
|
|
317
609
|
const ratings = homepageConfig["ratings"] || []
|
|
318
610
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
611
|
+
let testimonials = (Array.isArray(ratings) ? ratings : []).map(
|
|
612
|
+
(item: unknown, index: number) => {
|
|
613
|
+
const row = item as Record<string, unknown>
|
|
614
|
+
const review = (row?.review ?? row) as Record<string, unknown>
|
|
615
|
+
return {
|
|
616
|
+
id: `review-${index}`,
|
|
617
|
+
text: String(review?.["review-description"] ?? "").trim(),
|
|
618
|
+
name: String(review?.["review-user-name"] ?? "").trim(),
|
|
619
|
+
rating: parseFloat(String(review?.["review-rating"] ?? "5")),
|
|
620
|
+
avatar: review?.["review-profile-image"] as string | undefined,
|
|
621
|
+
}
|
|
327
622
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
623
|
+
).filter((t) => t.text && t.name)
|
|
331
624
|
|
|
332
625
|
return { title, testimonials }
|
|
333
|
-
} catch
|
|
334
|
-
|
|
335
|
-
|
|
626
|
+
} catch {
|
|
627
|
+
const block = DEFAULT_HOMEPAGE_SECTIONS.testimonials
|
|
628
|
+
const copy = parseSectionCopy(block)
|
|
629
|
+
return { title: copy.title ?? copy.name ?? "", testimonials: [] }
|
|
336
630
|
}
|
|
337
631
|
}
|
|
338
632
|
|
|
@@ -390,16 +684,222 @@ export const getFaqsFromConfig = async (): Promise<Array<{ category: string; ite
|
|
|
390
684
|
return null
|
|
391
685
|
}
|
|
392
686
|
}
|
|
687
|
+
export const getAnnouncementMessagesFromConfig = async (): Promise<string[]> => {
|
|
688
|
+
try {
|
|
689
|
+
const block = await getMergedSectionBlock("promoAnnouncements")
|
|
690
|
+
return parseAnnouncementMessages(block)
|
|
691
|
+
} catch {
|
|
692
|
+
return parseAnnouncementMessages(
|
|
693
|
+
DEFAULT_HOMEPAGE_SECTIONS.promoAnnouncements as Record<string, unknown>
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export type AboutBrandConfig = {
|
|
699
|
+
eyebrow: string
|
|
700
|
+
title: string
|
|
701
|
+
description: string
|
|
702
|
+
readMoreHref: string
|
|
703
|
+
stats: Array<{ label: string; value: string }>
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export const getAboutBrandFromConfig = async (): Promise<AboutBrandConfig> => {
|
|
707
|
+
try {
|
|
708
|
+
const config = await getDynamicConfig()
|
|
709
|
+
const merged = await getMergedSectionBlock("aboutBrand")
|
|
710
|
+
const legacy =
|
|
711
|
+
config?.["homepage-config"]?.["about-brand"] ??
|
|
712
|
+
config?.["homepage-config"]?.["who-we-are"]
|
|
713
|
+
const b = mergeSectionBlock(
|
|
714
|
+
merged,
|
|
715
|
+
legacy && typeof legacy === "object"
|
|
716
|
+
? (legacy as Record<string, unknown>)
|
|
717
|
+
: null
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
const statsRaw = (b.stats ?? b["about-stats"] ?? []) as unknown[]
|
|
721
|
+
const stats = Array.isArray(statsRaw)
|
|
722
|
+
? statsRaw
|
|
723
|
+
.map((s: unknown) => {
|
|
724
|
+
if (!s || typeof s !== "object") return null
|
|
725
|
+
const row = s as Record<string, unknown>
|
|
726
|
+
const label = String(row.label ?? row.name ?? "").trim()
|
|
727
|
+
const value = String(row.value ?? "").trim()
|
|
728
|
+
return label && value ? { label, value } : null
|
|
729
|
+
})
|
|
730
|
+
.filter(Boolean) as Array<{ label: string; value: string }>
|
|
731
|
+
: []
|
|
732
|
+
|
|
733
|
+
const copy = parseSectionCopy(b)
|
|
734
|
+
return {
|
|
735
|
+
eyebrow: copy.eyebrow ?? "",
|
|
736
|
+
title: copy.title ?? copy.name ?? "",
|
|
737
|
+
description: copy.description ?? copy.text ?? "",
|
|
738
|
+
readMoreHref: String(b.readMoreHref ?? b["read-more-link"] ?? "/about"),
|
|
739
|
+
stats,
|
|
740
|
+
}
|
|
741
|
+
} catch {
|
|
742
|
+
const b = DEFAULT_HOMEPAGE_SECTIONS.aboutBrand
|
|
743
|
+
const copy = parseSectionCopy(b)
|
|
744
|
+
return {
|
|
745
|
+
eyebrow: copy.eyebrow ?? "",
|
|
746
|
+
title: copy.title ?? copy.name ?? "",
|
|
747
|
+
description: copy.description ?? copy.text ?? "",
|
|
748
|
+
readMoreHref: String(b.readMoreHref ?? "/about"),
|
|
749
|
+
stats: (b.stats ?? []) as unknown as Array<{ label: string; value: string }>,
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export type BrandPillarConfig = {
|
|
755
|
+
title: string
|
|
756
|
+
description: string
|
|
757
|
+
image?: string
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export const getBrandPillarsFromConfig = async (): Promise<{
|
|
761
|
+
sectionTitle: string
|
|
762
|
+
pillars: BrandPillarConfig[]
|
|
763
|
+
}> => {
|
|
764
|
+
try {
|
|
765
|
+
const block = await getMergedSectionBlock("brandPillars")
|
|
766
|
+
const copy = parseSectionCopy(block)
|
|
767
|
+
const sectionTitle = copy.title ?? copy.name ?? ""
|
|
768
|
+
const raw = block.pillars ?? block["brand-pillars"] ?? []
|
|
769
|
+
const pillars = (Array.isArray(raw) ? raw : [])
|
|
770
|
+
.map((item: unknown) => {
|
|
771
|
+
if (!item || typeof item !== "object") return null
|
|
772
|
+
const row = (item as Record<string, unknown>)["pillar"] ?? item
|
|
773
|
+
const p = row as Record<string, unknown>
|
|
774
|
+
const title = String(p.title ?? p.name ?? "").trim()
|
|
775
|
+
const description = String(p.description ?? p.text ?? "").trim()
|
|
776
|
+
const image = (p.image ?? p.icon) as string | undefined
|
|
777
|
+
return title && description ? { title, description, image } : null
|
|
778
|
+
})
|
|
779
|
+
.filter(Boolean) as BrandPillarConfig[]
|
|
780
|
+
|
|
781
|
+
return { sectionTitle, pillars }
|
|
782
|
+
} catch {
|
|
783
|
+
const block = DEFAULT_HOMEPAGE_SECTIONS.brandPillars
|
|
784
|
+
const copy = parseSectionCopy(block)
|
|
785
|
+
const raw = block.pillars ?? []
|
|
786
|
+
return {
|
|
787
|
+
sectionTitle: copy.title ?? copy.name ?? "",
|
|
788
|
+
pillars: [...(Array.isArray(raw) ? raw : [])] as BrandPillarConfig[],
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export type BlogPostConfig = {
|
|
794
|
+
title: string
|
|
795
|
+
excerpt?: string
|
|
796
|
+
category?: string
|
|
797
|
+
date?: string
|
|
798
|
+
href: string
|
|
799
|
+
image?: string
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export const getBlogPostsFromConfig = async (): Promise<BlogPostConfig[]> => {
|
|
803
|
+
try {
|
|
804
|
+
const block = await getMergedSectionBlock("blogPosts")
|
|
805
|
+
const raw = block.posts ?? block["blog-posts"] ?? block["blog-array"] ?? []
|
|
806
|
+
const posts = (Array.isArray(raw) ? raw : [])
|
|
807
|
+
.map((item: unknown) => {
|
|
808
|
+
if (!item || typeof item !== "object") return null
|
|
809
|
+
const row = (item as Record<string, unknown>)["blog"] ?? item
|
|
810
|
+
const b = row as Record<string, unknown>
|
|
811
|
+
const title = String(b.title ?? "").trim()
|
|
812
|
+
const href = String(b.href ?? b.link ?? b.url ?? "").trim()
|
|
813
|
+
if (!title || !href) return null
|
|
814
|
+
return {
|
|
815
|
+
title,
|
|
816
|
+
href,
|
|
817
|
+
excerpt: String(b.excerpt ?? b.description ?? "").trim() || undefined,
|
|
818
|
+
category: String(b.category ?? b.tag ?? "").trim() || undefined,
|
|
819
|
+
date: String(b.date ?? "").trim() || undefined,
|
|
820
|
+
image: (b.image ?? b.thumbnail) as string | undefined,
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
.filter(Boolean) as BlogPostConfig[]
|
|
824
|
+
return posts
|
|
825
|
+
} catch {
|
|
826
|
+
const block = DEFAULT_HOMEPAGE_SECTIONS.blogPosts
|
|
827
|
+
const raw = block.posts ?? []
|
|
828
|
+
return (Array.isArray(raw) ? raw : [])
|
|
829
|
+
.map((item: unknown) => {
|
|
830
|
+
if (!item || typeof item !== "object") return null
|
|
831
|
+
const b = item as Record<string, unknown>
|
|
832
|
+
const title = String(b.title ?? "").trim()
|
|
833
|
+
const href = String(b.href ?? b.link ?? "").trim()
|
|
834
|
+
if (!title || !href) return null
|
|
835
|
+
return { title, href } as BlogPostConfig
|
|
836
|
+
})
|
|
837
|
+
.filter(Boolean) as BlogPostConfig[]
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function readPromoCountdownBlock(block: unknown): {
|
|
842
|
+
code: string | null
|
|
843
|
+
message: string | null
|
|
844
|
+
endAt: string | null
|
|
845
|
+
active: boolean | undefined
|
|
846
|
+
} {
|
|
847
|
+
if (!block || typeof block !== "object") {
|
|
848
|
+
return { code: null, message: null, endAt: null, active: undefined }
|
|
849
|
+
}
|
|
850
|
+
const row = block as Record<string, unknown>
|
|
851
|
+
const code =
|
|
852
|
+
normalizeCmsText(row["promo-code"] ?? row.code ?? row["coupon-code"]) ?? null
|
|
853
|
+
const message =
|
|
854
|
+
normalizeCmsText(row["promo-text"] ?? row.message ?? row.text) ?? null
|
|
855
|
+
const endAtRaw = row["end-at"] ?? row.endAt ?? row["ends-at"]
|
|
856
|
+
const endAt = typeof endAtRaw === "string" && endAtRaw.trim() ? endAtRaw.trim() : null
|
|
857
|
+
const activeRaw = row["promo-active"] ?? row.active
|
|
858
|
+
const active = typeof activeRaw === "boolean" ? activeRaw : undefined
|
|
859
|
+
return { code, message, endAt, active }
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Promo countdown — defaults + `sections.promoCountdown` override. */
|
|
863
|
+
export const getPromoCountdownFromConfig = async (): Promise<{
|
|
864
|
+
code: string | null
|
|
865
|
+
message: string | null
|
|
866
|
+
endAt: string | null
|
|
867
|
+
active: boolean
|
|
868
|
+
}> => {
|
|
869
|
+
try {
|
|
870
|
+
const block = await getMergedSectionBlock("promoCountdown")
|
|
871
|
+
const fromBlock = readPromoCountdownBlock(block)
|
|
872
|
+
const defaults = readPromoCountdownBlock(DEFAULT_HOMEPAGE_SECTIONS.promoCountdown)
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
code: fromBlock.code ?? defaults.code,
|
|
876
|
+
message: fromBlock.message ?? defaults.message,
|
|
877
|
+
endAt: fromBlock.endAt ?? defaults.endAt,
|
|
878
|
+
active: fromBlock.active ?? defaults.active ?? false,
|
|
879
|
+
}
|
|
880
|
+
} catch {
|
|
881
|
+
const defaults = readPromoCountdownBlock(DEFAULT_HOMEPAGE_SECTIONS.promoCountdown)
|
|
882
|
+
return {
|
|
883
|
+
code: defaults.code,
|
|
884
|
+
message: defaults.message,
|
|
885
|
+
endAt: defaults.endAt,
|
|
886
|
+
active: defaults.active ?? false,
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export { DEFAULT_HOMEPAGE_SECTIONS } from "./homepage-section-defaults"
|
|
892
|
+
|
|
393
893
|
export const getPromoBarConfig = async (): Promise<{ text: string | null; code: string | null; value: string | null; active: boolean }> => {
|
|
394
894
|
try {
|
|
395
895
|
const config = await getDynamicConfig()
|
|
396
896
|
const promoConfig = config?.["homepage-config"]?.["promo-bar"]
|
|
397
897
|
|
|
398
898
|
return {
|
|
399
|
-
text: promoConfig?.["promo-text"] || null,
|
|
899
|
+
text: normalizeCmsText(promoConfig?.["promo-text"]) || null,
|
|
400
900
|
code: promoConfig?.["promo-code"] || null,
|
|
401
901
|
value: promoConfig?.["promo-value"] || null,
|
|
402
|
-
active: promoConfig?.["promo-active"] ?? false
|
|
902
|
+
active: promoConfig?.["promo-active"] ?? false,
|
|
403
903
|
}
|
|
404
904
|
} catch (error) {
|
|
405
905
|
return { text: null, code: null, value: null, active: false }
|