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.
@@ -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
- export const getFeaturesFromConfig = async (): Promise<{ title: string; features: Array<{ name: string; icon: string }> } | null> => {
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 config = await getDynamicConfig()
259
- const homepageConfig = config?.["homepage-config"]
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
- if (!homepageConfig) return null
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
- const title = homepageConfig["why-choose-us-title"] || "Why Choose Us?"
264
- const featuresRaw = homepageConfig["why-choose-us-features"] || []
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
- const features = featuresRaw.map((item: any) => {
267
- const feature = item?.feature || item
268
- return {
269
- name: feature?.["feature-name"] || "",
270
- icon: feature?.["feature-icon"] || "",
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
- }).filter((f: any) => f.name && f.icon)
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
- return { title, features }
276
- } catch (error) {
476
+ export type TrustFeaturesConfig = {
477
+ title: string | null
478
+ description: string | null
479
+ features: Array<{ name: string; icon: string }>
480
+ }
277
481
 
278
- return null
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 title = homepageConfig["rating-title"] || "Happy Parents Say About Us"
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
- const testimonials = ratings.map((item: any, index: number) => {
320
- const review = item?.review || item
321
- return {
322
- id: `review-${index}`,
323
- text: review?.["review-description"] || "",
324
- name: review?.["review-user-name"] || "",
325
- rating: parseFloat(review?.["review-rating"] || "5"),
326
- avatar: review?.["review-profile-image"] || undefined,
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
- }).filter((t: any) => t.text && t.name)
329
-
330
-
623
+ ).filter((t) => t.text && t.name)
331
624
 
332
625
  return { title, testimonials }
333
- } catch (error) {
334
-
335
- return null
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 }