medusa-storefront-data 2.1.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/dynamic-config.d.ts +78 -1
- package/dist/server/dynamic-config.d.ts.map +1 -1
- package/dist/server/dynamic-config.js +410 -24
- 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/package.json +1 -1
- package/src/server/dynamic-config.ts +513 -35
- package/src/server/home.ts +155 -12
- package/src/server/homepage-section-defaults.ts +160 -0
|
@@ -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
|
}
|
|
@@ -253,32 +260,43 @@ 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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const title = homepageConfig["why-choose-us-title"] || "Why Choose Us?"
|
|
264
|
-
const featuresRaw = homepageConfig["why-choose-us-features"] || []
|
|
265
|
-
|
|
266
|
-
const features = featuresRaw.map((item: any) => {
|
|
267
|
-
const feature = item?.feature || item
|
|
268
|
-
return {
|
|
269
|
-
name: feature?.["feature-name"] || "",
|
|
270
|
-
icon: feature?.["feature-icon"] || "",
|
|
271
|
-
}
|
|
272
|
-
}).filter((f: any) => f.name && f.icon)
|
|
273
|
-
|
|
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)
|
|
274
274
|
|
|
275
275
|
return { title, features }
|
|
276
|
-
} catch
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
}
|
|
279
283
|
}
|
|
280
284
|
}
|
|
281
285
|
|
|
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
|
+
}
|
|
299
|
+
|
|
282
300
|
const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
|
283
301
|
|
|
284
302
|
/** Fix common UTF-8 mojibake from CMS copy-paste (e.g. em dash, apostrophe). */
|
|
@@ -292,6 +310,197 @@ function normalizeCmsText(value: unknown): string | undefined {
|
|
|
292
310
|
.replace(/≡ƒÿÄ≡ƒæòΓ£¿/g, " 👕✨")
|
|
293
311
|
}
|
|
294
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() : ""
|
|
450
|
+
}
|
|
451
|
+
return ""
|
|
452
|
+
})
|
|
453
|
+
.filter(Boolean)
|
|
454
|
+
}
|
|
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
|
+
}
|
|
475
|
+
|
|
476
|
+
export type TrustFeaturesConfig = {
|
|
477
|
+
title: string | null
|
|
478
|
+
description: string | null
|
|
479
|
+
features: Array<{ name: string; icon: string }>
|
|
480
|
+
}
|
|
481
|
+
|
|
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
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
295
504
|
function normalizeBannerButtonName(value: unknown): string | undefined {
|
|
296
505
|
const text = normalizeCmsText(value)
|
|
297
506
|
if (!text) return undefined
|
|
@@ -328,6 +537,65 @@ export const getContactInfoFromConfig = async (): Promise<{ phone?: string; emai
|
|
|
328
537
|
}
|
|
329
538
|
}
|
|
330
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
|
+
|
|
331
599
|
export const getTestimonialsFromConfig = async (): Promise<{ title: string; testimonials: Array<{ id: string; text: string; name: string; rating: number; avatar?: string }> } | null> => {
|
|
332
600
|
try {
|
|
333
601
|
const config = await getDynamicConfig()
|
|
@@ -335,26 +603,30 @@ export const getTestimonialsFromConfig = async (): Promise<{ title: string; test
|
|
|
335
603
|
|
|
336
604
|
if (!homepageConfig) return null
|
|
337
605
|
|
|
338
|
-
const
|
|
606
|
+
const block = await getMergedSectionBlock("testimonials")
|
|
607
|
+
const copy = parseSectionCopy(block)
|
|
608
|
+
const title = copy.title ?? copy.name ?? ""
|
|
339
609
|
const ratings = homepageConfig["ratings"] || []
|
|
340
610
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
622
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
623
|
+
).filter((t) => t.text && t.name)
|
|
353
624
|
|
|
354
625
|
return { title, testimonials }
|
|
355
|
-
} catch
|
|
356
|
-
|
|
357
|
-
|
|
626
|
+
} catch {
|
|
627
|
+
const block = DEFAULT_HOMEPAGE_SECTIONS.testimonials
|
|
628
|
+
const copy = parseSectionCopy(block)
|
|
629
|
+
return { title: copy.title ?? copy.name ?? "", testimonials: [] }
|
|
358
630
|
}
|
|
359
631
|
}
|
|
360
632
|
|
|
@@ -412,6 +684,212 @@ export const getFaqsFromConfig = async (): Promise<Array<{ category: string; ite
|
|
|
412
684
|
return null
|
|
413
685
|
}
|
|
414
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
|
+
|
|
415
893
|
export const getPromoBarConfig = async (): Promise<{ text: string | null; code: string | null; value: string | null; active: boolean }> => {
|
|
416
894
|
try {
|
|
417
895
|
const config = await getDynamicConfig()
|