medusa-storefront-data 2.1.0 → 2.4.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.
Files changed (269) hide show
  1. package/HOMEPAGE_CONFIG.md +99 -0
  2. package/dist/segment-data/default-page-input.json +191 -0
  3. package/dist/src/config.d.ts.map +1 -0
  4. package/dist/src/cookies.d.ts.map +1 -0
  5. package/dist/src/edge.d.ts.map +1 -0
  6. package/dist/src/middleware.d.ts.map +1 -0
  7. package/dist/src/server/cart.d.ts.map +1 -0
  8. package/dist/src/server/categories.d.ts.map +1 -0
  9. package/dist/src/server/collections.d.ts.map +1 -0
  10. package/dist/src/server/config-merge.d.ts +3 -0
  11. package/dist/src/server/config-merge.d.ts.map +1 -0
  12. package/dist/src/server/config-merge.js +31 -0
  13. package/dist/src/server/contact.d.ts.map +1 -0
  14. package/dist/src/server/customer-registration.d.ts.map +1 -0
  15. package/dist/src/server/customer.d.ts.map +1 -0
  16. package/dist/src/server/dynamic-config.d.ts +240 -0
  17. package/dist/src/server/dynamic-config.d.ts.map +1 -0
  18. package/dist/src/server/dynamic-config.js +703 -0
  19. package/dist/src/server/fulfillment.d.ts.map +1 -0
  20. package/dist/src/server/guest.d.ts.map +1 -0
  21. package/dist/src/server/home-sections/about-brand.d.ts +9 -0
  22. package/dist/src/server/home-sections/about-brand.d.ts.map +1 -0
  23. package/dist/src/server/home-sections/about-brand.js +9 -0
  24. package/dist/src/server/home-sections/baptism-picks.d.ts +10 -0
  25. package/dist/src/server/home-sections/baptism-picks.d.ts.map +1 -0
  26. package/dist/src/server/home-sections/baptism-picks.js +27 -0
  27. package/dist/src/server/home-sections/baptism.d.ts +13 -0
  28. package/dist/src/server/home-sections/baptism.d.ts.map +1 -0
  29. package/dist/src/server/home-sections/baptism.js +43 -0
  30. package/dist/src/server/home-sections/blog-posts.d.ts +9 -0
  31. package/dist/src/server/home-sections/blog-posts.d.ts.map +1 -0
  32. package/dist/src/server/home-sections/blog-posts.js +9 -0
  33. package/dist/src/server/home-sections/brand-marquee.d.ts +8 -0
  34. package/dist/src/server/home-sections/brand-marquee.d.ts.map +1 -0
  35. package/dist/src/server/home-sections/brand-marquee.js +7 -0
  36. package/dist/src/server/home-sections/brand-pillars.d.ts +9 -0
  37. package/dist/src/server/home-sections/brand-pillars.d.ts.map +1 -0
  38. package/dist/src/server/home-sections/brand-pillars.js +9 -0
  39. package/dist/src/server/home-sections/category-pills.d.ts +9 -0
  40. package/dist/src/server/home-sections/category-pills.d.ts.map +1 -0
  41. package/dist/src/server/home-sections/category-pills.js +19 -0
  42. package/dist/src/server/home-sections/celebrity-trust.d.ts +12 -0
  43. package/dist/src/server/home-sections/celebrity-trust.d.ts.map +1 -0
  44. package/dist/src/server/home-sections/celebrity-trust.js +27 -0
  45. package/dist/src/server/home-sections/features.d.ts +9 -0
  46. package/dist/src/server/home-sections/features.d.ts.map +1 -0
  47. package/dist/src/server/home-sections/features.js +9 -0
  48. package/dist/src/server/home-sections/hero.d.ts +9 -0
  49. package/dist/src/server/home-sections/hero.d.ts.map +1 -0
  50. package/dist/src/server/home-sections/hero.js +9 -0
  51. package/dist/src/server/home-sections/instagram-posts.d.ts +9 -0
  52. package/dist/src/server/home-sections/instagram-posts.d.ts.map +1 -0
  53. package/dist/src/server/home-sections/instagram-posts.js +9 -0
  54. package/dist/src/server/home-sections/loved-by-moms.d.ts +12 -0
  55. package/dist/src/server/home-sections/loved-by-moms.d.ts.map +1 -0
  56. package/dist/src/server/home-sections/loved-by-moms.js +20 -0
  57. package/dist/src/server/home-sections/luxe-favourites.d.ts +11 -0
  58. package/dist/src/server/home-sections/luxe-favourites.d.ts.map +1 -0
  59. package/dist/src/server/home-sections/luxe-favourites.js +19 -0
  60. package/dist/src/server/home-sections/new-arrivals-classic.d.ts +8 -0
  61. package/dist/src/server/home-sections/new-arrivals-classic.d.ts.map +1 -0
  62. package/dist/src/server/home-sections/new-arrivals-classic.js +10 -0
  63. package/dist/src/server/home-sections/new-arrivals.d.ts +12 -0
  64. package/dist/src/server/home-sections/new-arrivals.d.ts.map +1 -0
  65. package/dist/src/server/home-sections/new-arrivals.js +28 -0
  66. package/dist/src/server/home-sections/promo-announcements.d.ts +7 -0
  67. package/dist/src/server/home-sections/promo-announcements.d.ts.map +1 -0
  68. package/dist/src/server/home-sections/promo-announcements.js +7 -0
  69. package/dist/src/server/home-sections/promo-countdown.d.ts +6 -0
  70. package/dist/src/server/home-sections/promo-countdown.d.ts.map +1 -0
  71. package/dist/src/server/home-sections/promo-countdown.js +5 -0
  72. package/dist/src/server/home-sections/shared.d.ts +10 -0
  73. package/dist/src/server/home-sections/shared.d.ts.map +1 -0
  74. package/dist/src/server/home-sections/shared.js +29 -0
  75. package/dist/src/server/home-sections/shop-by-age.d.ts +9 -0
  76. package/dist/src/server/home-sections/shop-by-age.d.ts.map +1 -0
  77. package/dist/src/server/home-sections/shop-by-age.js +17 -0
  78. package/dist/src/server/home-sections/shop-by-category.d.ts +10 -0
  79. package/dist/src/server/home-sections/shop-by-category.d.ts.map +1 -0
  80. package/dist/src/server/home-sections/shop-by-category.js +17 -0
  81. package/dist/src/server/home-sections/testimonials.d.ts +9 -0
  82. package/dist/src/server/home-sections/testimonials.d.ts.map +1 -0
  83. package/dist/src/server/home-sections/testimonials.js +9 -0
  84. package/dist/src/server/home-sections/theme-dresses.d.ts +12 -0
  85. package/dist/src/server/home-sections/theme-dresses.d.ts.map +1 -0
  86. package/dist/src/server/home-sections/theme-dresses.js +27 -0
  87. package/dist/src/server/home-sections/video-stories.d.ts +9 -0
  88. package/dist/src/server/home-sections/video-stories.d.ts.map +1 -0
  89. package/dist/src/server/home-sections/video-stories.js +9 -0
  90. package/dist/src/server/home-sections/why-choose-us.d.ts +9 -0
  91. package/dist/src/server/home-sections/why-choose-us.d.ts.map +1 -0
  92. package/dist/src/server/home-sections/why-choose-us.js +9 -0
  93. package/dist/src/server/home.d.ts +62 -0
  94. package/dist/src/server/home.d.ts.map +1 -0
  95. package/dist/src/server/home.js +122 -0
  96. package/dist/src/server/homepage-config.types.d.ts +274 -0
  97. package/dist/src/server/homepage-config.types.d.ts.map +1 -0
  98. package/dist/src/server/homepage-config.types.js +33 -0
  99. package/dist/src/server/homepage-section-defaults.d.ts +188 -0
  100. package/dist/src/server/homepage-section-defaults.d.ts.map +1 -0
  101. package/dist/src/server/homepage-section-defaults.js +188 -0
  102. package/dist/{server → src/server}/index.d.ts +2 -0
  103. package/dist/src/server/index.d.ts.map +1 -0
  104. package/dist/{server → src/server}/index.js +2 -0
  105. package/dist/src/server/locale-actions.d.ts.map +1 -0
  106. package/dist/src/server/locales.d.ts.map +1 -0
  107. package/dist/src/server/notifications.d.ts.map +1 -0
  108. package/dist/src/server/onboarding.d.ts.map +1 -0
  109. package/dist/src/server/orders.d.ts.map +1 -0
  110. package/dist/src/server/page-input.d.ts +19 -0
  111. package/dist/src/server/page-input.d.ts.map +1 -0
  112. package/dist/src/server/page-input.js +40 -0
  113. package/dist/src/server/payment-details.d.ts.map +1 -0
  114. package/dist/src/server/payment.d.ts.map +1 -0
  115. package/dist/src/server/pincode.d.ts.map +1 -0
  116. package/dist/src/server/products.d.ts.map +1 -0
  117. package/dist/src/server/regions.d.ts.map +1 -0
  118. package/dist/src/server/resolve-home-segment-data.d.ts +25 -0
  119. package/dist/src/server/resolve-home-segment-data.d.ts.map +1 -0
  120. package/dist/src/server/resolve-home-segment-data.js +20 -0
  121. package/dist/src/server/returns.d.ts.map +1 -0
  122. package/dist/src/server/shoppable-looks.d.ts +17 -0
  123. package/dist/src/server/shoppable-looks.d.ts.map +1 -0
  124. package/dist/src/server/shoppable-looks.js +136 -0
  125. package/dist/src/server/swaps.d.ts.map +1 -0
  126. package/dist/src/server/variants.d.ts.map +1 -0
  127. package/dist/src/server/wishlist.d.ts.map +1 -0
  128. package/dist/src/util/get-locale-header.d.ts.map +1 -0
  129. package/dist/src/util/medusa-error.d.ts.map +1 -0
  130. package/dist/src/util/revalidate-cart.d.ts.map +1 -0
  131. package/dist/src/util/sort-products.d.ts.map +1 -0
  132. package/dist/src/util/store-client.d.ts.map +1 -0
  133. package/homepage-config.example.json +209 -0
  134. package/homepage-sections.example.json +55 -0
  135. package/package.json +121 -2
  136. package/segment-data/default-page-input.json +191 -0
  137. package/src/server/config-merge.ts +31 -0
  138. package/src/server/dynamic-config.ts +838 -248
  139. package/src/server/home-sections/about-brand.ts +24 -0
  140. package/src/server/home-sections/baptism-picks.ts +46 -0
  141. package/src/server/home-sections/baptism.ts +64 -0
  142. package/src/server/home-sections/blog-posts.ts +23 -0
  143. package/src/server/home-sections/brand-marquee.ts +17 -0
  144. package/src/server/home-sections/brand-pillars.ts +23 -0
  145. package/src/server/home-sections/category-pills.ts +32 -0
  146. package/src/server/home-sections/celebrity-trust.ts +42 -0
  147. package/src/server/home-sections/features.ts +23 -0
  148. package/src/server/home-sections/hero.ts +24 -0
  149. package/src/server/home-sections/instagram-posts.ts +23 -0
  150. package/src/server/home-sections/loved-by-moms.ts +34 -0
  151. package/src/server/home-sections/luxe-favourites.ts +30 -0
  152. package/src/server/home-sections/new-arrivals-classic.ts +21 -0
  153. package/src/server/home-sections/new-arrivals.ts +47 -0
  154. package/src/server/home-sections/promo-announcements.ts +17 -0
  155. package/src/server/home-sections/promo-countdown.ts +15 -0
  156. package/src/server/home-sections/shared.ts +44 -0
  157. package/src/server/home-sections/shop-by-age.ts +26 -0
  158. package/src/server/home-sections/shop-by-category.ts +28 -0
  159. package/src/server/home-sections/testimonials.ts +23 -0
  160. package/src/server/home-sections/theme-dresses.ts +49 -0
  161. package/src/server/home-sections/video-stories.ts +23 -0
  162. package/src/server/home-sections/why-choose-us.ts +23 -0
  163. package/src/server/home.ts +173 -16
  164. package/src/server/homepage-config.types.ts +335 -0
  165. package/src/server/homepage-section-defaults.ts +199 -0
  166. package/src/server/index.ts +2 -0
  167. package/src/server/page-input.ts +64 -0
  168. package/src/server/resolve-home-segment-data.ts +63 -0
  169. package/src/server/shoppable-looks.ts +178 -0
  170. package/dist/config.d.ts.map +0 -1
  171. package/dist/cookies.d.ts.map +0 -1
  172. package/dist/edge.d.ts.map +0 -1
  173. package/dist/middleware.d.ts.map +0 -1
  174. package/dist/server/cart.d.ts.map +0 -1
  175. package/dist/server/categories.d.ts.map +0 -1
  176. package/dist/server/collections.d.ts.map +0 -1
  177. package/dist/server/contact.d.ts.map +0 -1
  178. package/dist/server/customer-registration.d.ts.map +0 -1
  179. package/dist/server/customer.d.ts.map +0 -1
  180. package/dist/server/dynamic-config.d.ts +0 -125
  181. package/dist/server/dynamic-config.d.ts.map +0 -1
  182. package/dist/server/dynamic-config.js +0 -289
  183. package/dist/server/fulfillment.d.ts.map +0 -1
  184. package/dist/server/guest.d.ts.map +0 -1
  185. package/dist/server/home.d.ts +0 -15
  186. package/dist/server/home.d.ts.map +0 -1
  187. package/dist/server/home.js +0 -45
  188. package/dist/server/index.d.ts.map +0 -1
  189. package/dist/server/locale-actions.d.ts.map +0 -1
  190. package/dist/server/locales.d.ts.map +0 -1
  191. package/dist/server/notifications.d.ts.map +0 -1
  192. package/dist/server/onboarding.d.ts.map +0 -1
  193. package/dist/server/orders.d.ts.map +0 -1
  194. package/dist/server/payment-details.d.ts.map +0 -1
  195. package/dist/server/payment.d.ts.map +0 -1
  196. package/dist/server/pincode.d.ts.map +0 -1
  197. package/dist/server/products.d.ts.map +0 -1
  198. package/dist/server/regions.d.ts.map +0 -1
  199. package/dist/server/returns.d.ts.map +0 -1
  200. package/dist/server/swaps.d.ts.map +0 -1
  201. package/dist/server/variants.d.ts.map +0 -1
  202. package/dist/server/wishlist.d.ts.map +0 -1
  203. package/dist/util/get-locale-header.d.ts.map +0 -1
  204. package/dist/util/medusa-error.d.ts.map +0 -1
  205. package/dist/util/revalidate-cart.d.ts.map +0 -1
  206. package/dist/util/sort-products.d.ts.map +0 -1
  207. package/dist/util/store-client.d.ts.map +0 -1
  208. /package/dist/{config.d.ts → src/config.d.ts} +0 -0
  209. /package/dist/{config.js → src/config.js} +0 -0
  210. /package/dist/{cookies.d.ts → src/cookies.d.ts} +0 -0
  211. /package/dist/{cookies.js → src/cookies.js} +0 -0
  212. /package/dist/{edge.d.ts → src/edge.d.ts} +0 -0
  213. /package/dist/{edge.js → src/edge.js} +0 -0
  214. /package/dist/{middleware.d.ts → src/middleware.d.ts} +0 -0
  215. /package/dist/{middleware.js → src/middleware.js} +0 -0
  216. /package/dist/{server → src/server}/cart.d.ts +0 -0
  217. /package/dist/{server → src/server}/cart.js +0 -0
  218. /package/dist/{server → src/server}/categories.d.ts +0 -0
  219. /package/dist/{server → src/server}/categories.js +0 -0
  220. /package/dist/{server → src/server}/collections.d.ts +0 -0
  221. /package/dist/{server → src/server}/collections.js +0 -0
  222. /package/dist/{server → src/server}/contact.d.ts +0 -0
  223. /package/dist/{server → src/server}/contact.js +0 -0
  224. /package/dist/{server → src/server}/customer-registration.d.ts +0 -0
  225. /package/dist/{server → src/server}/customer-registration.js +0 -0
  226. /package/dist/{server → src/server}/customer.d.ts +0 -0
  227. /package/dist/{server → src/server}/customer.js +0 -0
  228. /package/dist/{server → src/server}/fulfillment.d.ts +0 -0
  229. /package/dist/{server → src/server}/fulfillment.js +0 -0
  230. /package/dist/{server → src/server}/guest.d.ts +0 -0
  231. /package/dist/{server → src/server}/guest.js +0 -0
  232. /package/dist/{server → src/server}/locale-actions.d.ts +0 -0
  233. /package/dist/{server → src/server}/locale-actions.js +0 -0
  234. /package/dist/{server → src/server}/locales.d.ts +0 -0
  235. /package/dist/{server → src/server}/locales.js +0 -0
  236. /package/dist/{server → src/server}/notifications.d.ts +0 -0
  237. /package/dist/{server → src/server}/notifications.js +0 -0
  238. /package/dist/{server → src/server}/onboarding.d.ts +0 -0
  239. /package/dist/{server → src/server}/onboarding.js +0 -0
  240. /package/dist/{server → src/server}/orders.d.ts +0 -0
  241. /package/dist/{server → src/server}/orders.js +0 -0
  242. /package/dist/{server → src/server}/payment-details.d.ts +0 -0
  243. /package/dist/{server → src/server}/payment-details.js +0 -0
  244. /package/dist/{server → src/server}/payment.d.ts +0 -0
  245. /package/dist/{server → src/server}/payment.js +0 -0
  246. /package/dist/{server → src/server}/pincode.d.ts +0 -0
  247. /package/dist/{server → src/server}/pincode.js +0 -0
  248. /package/dist/{server → src/server}/products.d.ts +0 -0
  249. /package/dist/{server → src/server}/products.js +0 -0
  250. /package/dist/{server → src/server}/regions.d.ts +0 -0
  251. /package/dist/{server → src/server}/regions.js +0 -0
  252. /package/dist/{server → src/server}/returns.d.ts +0 -0
  253. /package/dist/{server → src/server}/returns.js +0 -0
  254. /package/dist/{server → src/server}/swaps.d.ts +0 -0
  255. /package/dist/{server → src/server}/swaps.js +0 -0
  256. /package/dist/{server → src/server}/variants.d.ts +0 -0
  257. /package/dist/{server → src/server}/variants.js +0 -0
  258. /package/dist/{server → src/server}/wishlist.d.ts +0 -0
  259. /package/dist/{server → src/server}/wishlist.js +0 -0
  260. /package/dist/{util → src/util}/get-locale-header.d.ts +0 -0
  261. /package/dist/{util → src/util}/get-locale-header.js +0 -0
  262. /package/dist/{util → src/util}/medusa-error.d.ts +0 -0
  263. /package/dist/{util → src/util}/medusa-error.js +0 -0
  264. /package/dist/{util → src/util}/revalidate-cart.d.ts +0 -0
  265. /package/dist/{util → src/util}/revalidate-cart.js +0 -0
  266. /package/dist/{util → src/util}/sort-products.d.ts +0 -0
  267. /package/dist/{util → src/util}/sort-products.js +0 -0
  268. /package/dist/{util → src/util}/store-client.d.ts +0 -0
  269. /package/dist/{util → src/util}/store-client.js +0 -0
@@ -1,73 +1,28 @@
1
- "use server"
1
+ import "server-only"
2
2
 
3
3
  import { cache } from "react"
4
4
  import { getAuthHeaders } from "../cookies"
5
-
6
- // Define the structure based on the actual API response and client usage
7
- export interface DynamicConfig {
8
- "homepage-config"?: {
9
- logo?: string
10
- "homepage-banner-array"?: Array<{
11
- "homepage-banner"?: {
12
- "homepage-banner-image"?: string
13
- "homepage-banner-title"?: string
14
- "homepage-banner-subtitle"?: string
15
- "homepage-banner-description"?: string
16
- "homepage-banner-button-name"?: string
17
- "homepage-banner-button-link"?: string
18
- }
19
- }>
20
- "app-banner-array"?: Array<{
21
- "app-banner"?: {
22
- "app-banner-image"?: string
23
- "app-banner-title"?: string
24
- "app-banner-subtitle"?: string
25
- "app-banner-description"?: string
26
- "app-banner-button-name"?: string
27
- "app-banner-button-link"?: string
28
- "app-banner-link"?: string
29
- }
30
- }>
31
- "website-description"?: string
32
- "website_description"?: string
33
- "why-choose-us-title"?: string
34
- "why-choose-us-features"?: Array<{
35
- feature?: {
36
- "feature-name"?: string
37
- "feature-icon"?: string
38
- }
39
- // Handle potential direct structure if any
40
- "feature-name"?: string
41
- "feature-icon"?: string
42
- }>
43
- "contact-us"?: {
44
- "contact-phone"?: string
45
- "contact-email"?: string
46
- "contact-address"?: string
47
- }
48
- "social-links"?: Array<{
49
- "social-link"?: {
50
- "social-platform-name"?: string
51
- "social-platform-icon"?: string
52
- "social-platform-url"?: string
53
- }
54
- // Handle potential direct structure if any
55
- "social-platform-name"?: string
56
- "social-platform-icon"?: string
57
- "social-platform-url"?: string
58
- }>
59
- "promo-bar"?: {
60
- "promo-text"?: string
61
- "promo-code"?: string
62
- "promo-value"?: string
63
- "promo-active"?: boolean
64
- }
65
- // Allow for other properties
66
- [key: string]: any
67
- }
68
- }
69
-
70
- export const getDynamicConfig = cache(async (): Promise<DynamicConfig | null> => {
5
+ import {
6
+ DEFAULT_HOMEPAGE_SECTIONS,
7
+ HOME_SECTION_COPY_ID_LIST,
8
+ type DefaultHomeSectionId,
9
+ } from "./homepage-section-defaults"
10
+ import type { DynamicConfigResponse, HomepageConfig } from "./homepage-config.types"
11
+ import { mergeSectionBlock } from "./config-merge"
12
+ import { mergePageInputWithDefaults, resolvePageInput } from "./page-input"
13
+
14
+ export {
15
+ DEFAULT_PAGE_INPUT,
16
+ mergePageInputWithDefaults,
17
+ resolvePageInput,
18
+ homepageConfigFromDynamicResponse,
19
+ type StorefrontPageInput,
20
+ } from "./page-input"
21
+
22
+ /** @deprecated Use `DynamicConfigResponse` from `medusa-storefront-data/homepage-config-schema`. */
23
+ export type DynamicConfig = DynamicConfigResponse
24
+
25
+ export const getDynamicConfig = cache(async (): Promise<DynamicConfigResponse | null> => {
71
26
  try {
72
27
  const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
73
28
  const baseUrl = process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
@@ -116,16 +71,17 @@ export const getDynamicConfig = cache(async (): Promise<DynamicConfig | null> =>
116
71
  }
117
72
  })
118
73
 
119
- export const getLogoFromConfig = async (): Promise<string | null> => {
120
- try {
121
- const config = await getDynamicConfig()
122
- const logo = config?.["homepage-config"]?.logo
123
-
124
- return logo || null
125
- } catch (error) {
74
+ export function getLogoFromPageInput(
75
+ pageInput?: HomepageConfig | null
76
+ ): string | null {
77
+ const config = resolvePageInput(pageInput)
78
+ return config.logo || null
79
+ }
126
80
 
127
- return null
128
- }
81
+ /** @deprecated Fetch in main project; pass `pageInput` into layout/pages instead. */
82
+ export const getLogoFromConfig = async (): Promise<string | null> => {
83
+ const config = await getDynamicConfig()
84
+ return getLogoFromPageInput(config?.["homepage-config"] ?? null)
129
85
  }
130
86
 
131
87
  export const getBannerImageFromConfig = async (): Promise<string | null> => {
@@ -169,68 +125,67 @@ export const getMobileBannerFromConfig = async (): Promise<{ image: string | nul
169
125
  }
170
126
  }
171
127
 
172
- export const getHomeBannersFromConfig = async (): Promise<Array<{
173
- image?: string;
174
- title?: string;
175
- subtitle?: string;
176
- description?: string;
177
- buttonName?: string;
178
- buttonLink?: string;
179
- }> | null> => {
180
- try {
181
- const config = await getDynamicConfig()
182
- const bannerArray = config?.["homepage-config"]?.["homepage-banner-array"]
183
-
184
- if (bannerArray && bannerArray.length > 0) {
185
- return bannerArray.map(item => {
186
- const banner = item["homepage-banner"]
187
- return {
188
- image: banner?.["homepage-banner-image"],
189
- title: normalizeCmsText(banner?.["homepage-banner-title"]),
190
- subtitle: normalizeCmsText(banner?.["homepage-banner-subtitle"]),
191
- description: normalizeCmsText(banner?.["homepage-banner-description"]),
192
- buttonName: normalizeBannerButtonName(banner?.["homepage-banner-button-name"]),
193
- buttonLink: banner?.["homepage-banner-button-link"],
194
- }
195
- })
196
- }
197
- return null
198
- } catch (error) {
128
+ export type HomepageBanner = {
129
+ image?: string
130
+ title?: string
131
+ subtitle?: string
132
+ description?: string
133
+ buttonName?: string
134
+ buttonLink?: string
135
+ }
199
136
 
200
- return null
137
+ export function getHomeBannersFromPageInput(
138
+ pageInput?: HomepageConfig | null
139
+ ): HomepageBanner[] | null {
140
+ const config = resolvePageInput(pageInput)
141
+ const bannerArray = config["homepage-banner-array"]
142
+ if (bannerArray && bannerArray.length > 0) {
143
+ return bannerArray.map((item) => {
144
+ const banner = item["homepage-banner"]
145
+ return {
146
+ image: banner?.["homepage-banner-image"],
147
+ title: normalizeCmsText(banner?.["homepage-banner-title"]),
148
+ subtitle: normalizeCmsText(banner?.["homepage-banner-subtitle"]),
149
+ description: normalizeCmsText(banner?.["homepage-banner-description"]),
150
+ buttonName: normalizeBannerButtonName(banner?.["homepage-banner-button-name"]),
151
+ buttonLink: banner?.["homepage-banner-button-link"],
152
+ }
153
+ })
201
154
  }
155
+ return null
202
156
  }
203
157
 
204
- export const getAppBannersFromConfig = async (): Promise<Array<{
205
- image?: string;
206
- title?: string;
207
- subtitle?: string;
208
- description?: string;
209
- buttonName?: string;
210
- buttonLink?: string;
211
- }> | null> => {
212
- try {
213
- const config = await getDynamicConfig()
214
- const bannerArray = config?.["homepage-config"]?.["app-banner-array"]
158
+ export function getAppBannersFromPageInput(
159
+ pageInput?: HomepageConfig | null
160
+ ): HomepageBanner[] | null {
161
+ const config = resolvePageInput(pageInput)
162
+ const bannerArray = config["app-banner-array"]
163
+ if (bannerArray && bannerArray.length > 0) {
164
+ return bannerArray.map((item) => {
165
+ const banner = item["app-banner"]
166
+ return {
167
+ image: banner?.["app-banner-image"],
168
+ title: normalizeCmsText(banner?.["app-banner-title"]),
169
+ subtitle: normalizeCmsText(banner?.["app-banner-subtitle"]),
170
+ description: normalizeCmsText(banner?.["app-banner-description"]),
171
+ buttonName: normalizeBannerButtonName(banner?.["app-banner-button-name"]),
172
+ buttonLink: banner?.["app-banner-button-link"],
173
+ }
174
+ })
175
+ }
176
+ return null
177
+ }
215
178
 
216
- if (bannerArray && bannerArray.length > 0) {
217
- return bannerArray.map(item => {
218
- const banner = item["app-banner"]
219
- return {
220
- image: banner?.["app-banner-image"],
221
- title: normalizeCmsText(banner?.["app-banner-title"]),
222
- subtitle: normalizeCmsText(banner?.["app-banner-subtitle"]),
223
- description: normalizeCmsText(banner?.["app-banner-description"]),
224
- buttonName: normalizeBannerButtonName(banner?.["app-banner-button-name"]),
225
- buttonLink: banner?.["app-banner-button-link"],
226
- }
227
- })
228
- }
229
- return null
230
- } catch (error) {
179
+ /** @deprecated Pass `pageInput` from main project. */
180
+ export const getHomeBannersFromConfig = async (): Promise<HomepageBanner[] | null> => {
181
+ const config = await getDynamicConfig()
182
+ return getHomeBannersFromPageInput(config?.["homepage-config"] ?? null)
183
+ }
231
184
 
232
- return null
233
- }
185
+ /** @deprecated Pass `pageInput` from main project. */
186
+ export const getAppBannersFromConfig = async (): Promise<HomepageBanner[] | null> => {
187
+ const config = await getDynamicConfig()
188
+ return getAppBannersFromPageInput(config?.["homepage-config"] ?? null)
234
189
  }
235
190
 
236
191
  // Legacy function for backward compatibility
@@ -239,44 +194,56 @@ export const getMobileBannerImageFromConfig = async (): Promise<string | null> =
239
194
  return image
240
195
  }
241
196
 
242
- export const getWebsiteDescriptionFromConfig = async (): Promise<string | null> => {
243
- try {
244
- const config = await getDynamicConfig()
245
- const homepageConfig = config?.["homepage-config"]
246
- const desc = homepageConfig?.["website-description"] || homepageConfig?.["website_description"]
247
-
248
-
249
- return desc || null
250
- } catch (error) {
251
-
252
- return null
253
- }
197
+ export function getWebsiteDescriptionFromPageInput(
198
+ pageInput?: HomepageConfig | null
199
+ ): string | null {
200
+ const config = resolvePageInput(pageInput)
201
+ const desc = config["website-description"] || config["website_description"]
202
+ return desc || null
254
203
  }
255
204
 
256
- export const getFeaturesFromConfig = async (): Promise<{ title: string; features: Array<{ name: string; icon: string }> } | null> => {
257
- try {
258
- const config = await getDynamicConfig()
259
- const homepageConfig = config?.["homepage-config"]
260
-
261
- if (!homepageConfig) return null
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)
205
+ /** @deprecated Pass `pageInput` from main project. */
206
+ export const getWebsiteDescriptionFromConfig = async (): Promise<string | null> => {
207
+ const config = await getDynamicConfig()
208
+ return getWebsiteDescriptionFromPageInput(config?.["homepage-config"] ?? null)
209
+ }
273
210
 
211
+ /** Why choose us — defaults + `sections.whyChooseUs` override. */
212
+ export function getFeaturesFromPageInput(
213
+ pageInput?: HomepageConfig | null
214
+ ): {
215
+ title: string
216
+ features: Array<{ name: string; icon: string }>
217
+ } {
218
+ const block = getMergedSectionBlock("whyChooseUs", pageInput)
219
+ const copy = parseSectionCopy(block)
220
+ const title = copy.title ?? copy.name ?? ""
221
+ const featuresRaw = block.features ?? block.items ?? []
222
+ const features = parseFeatureIconList(featuresRaw)
223
+ return { title, features }
224
+ }
274
225
 
275
- return { title, features }
276
- } catch (error) {
226
+ /** @deprecated Pass `pageInput` from main project. */
227
+ export const getFeaturesFromConfig = async (): Promise<{
228
+ title: string
229
+ features: Array<{ name: string; icon: string }>
230
+ }> => {
231
+ const config = await getDynamicConfig()
232
+ return getFeaturesFromPageInput(config?.["homepage-config"] ?? null)
233
+ }
277
234
 
278
- return null
279
- }
235
+ function parseFeatureIconList(raw: unknown): Array<{ name: string; icon: string }> {
236
+ if (!Array.isArray(raw)) return []
237
+ return raw
238
+ .map((item: unknown) => {
239
+ if (!item || typeof item !== "object") return null
240
+ const feature = (item as Record<string, unknown>).feature ?? item
241
+ const row = feature as Record<string, unknown>
242
+ const name = normalizeCmsText(row["feature-name"] ?? row.name) ?? ""
243
+ const icon = normalizeCmsText(row["feature-icon"] ?? row.icon) ?? ""
244
+ return name && icon ? { name, icon } : null
245
+ })
246
+ .filter(Boolean) as Array<{ name: string; icon: string }>
280
247
  }
281
248
 
282
249
  const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
@@ -292,6 +259,176 @@ function normalizeCmsText(value: unknown): string | undefined {
292
259
  .replace(/≡ƒÿÄ≡ƒæòΓ£¿/g, " 👕✨")
293
260
  }
294
261
 
262
+ /** CMS copy for a homepage section (title, name, text, description, etc.). */
263
+ export type SectionCopy = {
264
+ title: string | null
265
+ name: string | null
266
+ text: string | null
267
+ description: string | null
268
+ subtitle: string | null
269
+ eyebrow: string | null
270
+ /** Layout variant, e.g. new arrivals `grid` | `classic`. */
271
+ variant: string | null
272
+ layout: string | null
273
+ }
274
+
275
+ const EMPTY_SECTION_COPY: SectionCopy = {
276
+ title: null,
277
+ name: null,
278
+ text: null,
279
+ description: null,
280
+ subtitle: null,
281
+ eyebrow: null,
282
+ variant: null,
283
+ layout: null,
284
+ }
285
+
286
+ export function parseSectionCopy(block: unknown): SectionCopy {
287
+ if (!block || typeof block !== "object") return { ...EMPTY_SECTION_COPY }
288
+ const row = block as Record<string, unknown>
289
+ return {
290
+ title: normalizeCmsText(row.title) ?? null,
291
+ name: normalizeCmsText(row.name) ?? null,
292
+ text: normalizeCmsText(row.text) ?? null,
293
+ description: normalizeCmsText(row.description) ?? null,
294
+ subtitle: normalizeCmsText(row.subtitle) ?? null,
295
+ eyebrow: normalizeCmsText(row.eyebrow) ?? null,
296
+ variant: normalizeCmsText(row.variant) ?? null,
297
+ layout: normalizeCmsText(row.layout) ?? null,
298
+ }
299
+ }
300
+
301
+ export type HomeSectionCopyId = DefaultHomeSectionId
302
+
303
+ export type HomeSectionsCopy = Record<HomeSectionCopyId, SectionCopy>
304
+
305
+ const HOME_SECTION_COPY_IDS = new Set<HomeSectionCopyId>(HOME_SECTION_COPY_ID_LIST)
306
+
307
+ function sectionIdFromConfigKey(key: string): HomeSectionCopyId | null {
308
+ const id = key.replace(/-([a-z])/g, (_, c: string) =>
309
+ c.toUpperCase()
310
+ ) as HomeSectionCopyId
311
+ return HOME_SECTION_COPY_IDS.has(id) ? id : null
312
+ }
313
+
314
+ function getHomepageSectionsRoot(
315
+ pageInput?: HomepageConfig | null
316
+ ): Record<string, unknown> {
317
+ const config = resolvePageInput(pageInput)
318
+ const root = config.sections
319
+ return root && typeof root === "object"
320
+ ? (root as Record<string, unknown>)
321
+ : {}
322
+ }
323
+
324
+ function findSectionBlock(
325
+ root: Record<string, unknown>,
326
+ id: HomeSectionCopyId
327
+ ): Record<string, unknown> | null {
328
+ for (const [key, value] of Object.entries(root)) {
329
+ if (sectionIdFromConfigKey(key) !== id) continue
330
+ if (value && typeof value === "object") {
331
+ return value as Record<string, unknown>
332
+ }
333
+ if (typeof value === "string") {
334
+ return { text: value }
335
+ }
336
+ }
337
+ return null
338
+ }
339
+
340
+ export { mergeSectionBlock } from "./config-merge"
341
+
342
+ export function getMergedSectionBlock(
343
+ id: HomeSectionCopyId,
344
+ pageInput?: HomepageConfig | null
345
+ ): Record<string, unknown> {
346
+ const root = getHomepageSectionsRoot(pageInput)
347
+ const defaults = DEFAULT_HOMEPAGE_SECTIONS[id] ?? {}
348
+ const override = findSectionBlock(root, id)
349
+ return mergeSectionBlock(
350
+ { ...defaults } as Record<string, unknown>,
351
+ override
352
+ )
353
+ }
354
+
355
+ function mergeSectionCopy(base: SectionCopy, patch: SectionCopy): SectionCopy {
356
+ const pick = (override: string | null, fallback: string | null) =>
357
+ override ?? fallback
358
+ return {
359
+ title: pick(patch.title ?? patch.name, base.title ?? base.name),
360
+ name: pick(patch.name, base.name),
361
+ text: pick(patch.text, base.text),
362
+ description: pick(patch.description, base.description),
363
+ subtitle: pick(patch.subtitle, base.subtitle),
364
+ eyebrow: pick(patch.eyebrow, base.eyebrow),
365
+ variant: pick(patch.variant ?? patch.layout, base.variant ?? base.layout),
366
+ layout: pick(patch.layout ?? patch.variant, base.layout ?? base.variant),
367
+ }
368
+ }
369
+
370
+ function parseAnnouncementMessages(block: Record<string, unknown> | null): string[] {
371
+ if (!block) return []
372
+ const raw = block.messages ?? block.items ?? block["announcement-messages"]
373
+ if (!Array.isArray(raw)) return []
374
+ return raw
375
+ .map((item: unknown) => {
376
+ if (typeof item === "string") return item.trim()
377
+ if (item && typeof item === "object") {
378
+ const row = item as Record<string, unknown>
379
+ const nested = (row.message ?? row.text) as unknown
380
+ return typeof nested === "string" ? nested.trim() : ""
381
+ }
382
+ return ""
383
+ })
384
+ .filter(Boolean)
385
+ }
386
+
387
+ /**
388
+ * Merged section copy: package defaults + `homepage-config.sections` overrides.
389
+ */
390
+ export function getHomeSectionsCopyFromPageInput(
391
+ pageInput?: HomepageConfig | null
392
+ ): HomeSectionsCopy {
393
+ const copy = {} as HomeSectionsCopy
394
+ for (const id of HOME_SECTION_COPY_ID_LIST) {
395
+ const block = getMergedSectionBlock(id, pageInput)
396
+ copy[id] = parseSectionCopy(block)
397
+ }
398
+ return copy
399
+ }
400
+
401
+ /** @deprecated Pass `pageInput` from main project instead of fetching here. */
402
+ export const getHomeSectionsCopyFromConfig = async (): Promise<HomeSectionsCopy> => {
403
+ const config = await getDynamicConfig()
404
+ return getHomeSectionsCopyFromPageInput(config?.["homepage-config"] ?? null)
405
+ }
406
+
407
+ export type TrustFeaturesConfig = {
408
+ title: string | null
409
+ description: string | null
410
+ features: Array<{ name: string; icon: string }>
411
+ }
412
+
413
+ /** Trust badge row — defaults + `sections.features` override. */
414
+ export function getTrustFeaturesFromPageInput(
415
+ pageInput?: HomepageConfig | null
416
+ ): TrustFeaturesConfig {
417
+ const block = getMergedSectionBlock("features", pageInput)
418
+ const copy = parseSectionCopy(block)
419
+ const title = copy.title ?? copy.name
420
+ const description = copy.description ?? copy.text
421
+ const rawList = block.features ?? block.items ?? []
422
+ const features = parseFeatureIconList(rawList)
423
+ return { title: title ?? null, description: description ?? null, features }
424
+ }
425
+
426
+ /** @deprecated Pass `pageInput` from main project. */
427
+ export const getTrustFeaturesFromConfig = async (): Promise<TrustFeaturesConfig> => {
428
+ const config = await getDynamicConfig()
429
+ return getTrustFeaturesFromPageInput(config?.["homepage-config"] ?? null)
430
+ }
431
+
295
432
  function normalizeBannerButtonName(value: unknown): string | undefined {
296
433
  const text = normalizeCmsText(value)
297
434
  if (!text) return undefined
@@ -305,125 +442,578 @@ function normalizeConfigEmail(value: unknown): string | undefined {
305
442
  return EMAIL_PATTERN.test(trimmed) ? trimmed : undefined
306
443
  }
307
444
 
308
- export const getContactInfoFromConfig = async (): Promise<{ phone?: string; email?: string; address?: string } | null> => {
309
- try {
310
- const config = await getDynamicConfig()
311
- const contactConfig = config?.["homepage-config"]?.["contact-us"]
445
+ export type ContactInfo = { phone?: string; email?: string; address?: string }
446
+
447
+ export function getContactInfoFromPageInput(
448
+ pageInput?: HomepageConfig | null
449
+ ): ContactInfo | null {
450
+ const config = resolvePageInput(pageInput)
451
+ const contactConfig = config["contact-us"]
452
+ if (!contactConfig) return null
453
+ return {
454
+ phone: contactConfig["contact-phone"],
455
+ email: normalizeConfigEmail(contactConfig["contact-email"]),
456
+ address: contactConfig["contact-address"],
457
+ }
458
+ }
312
459
 
313
- if (!contactConfig) {
460
+ /** @deprecated Pass `pageInput` from main project. */
461
+ export const getContactInfoFromConfig = async (): Promise<ContactInfo | null> => {
462
+ const config = await getDynamicConfig()
463
+ return getContactInfoFromPageInput(config?.["homepage-config"] ?? null)
464
+ }
314
465
 
315
- return null
316
- }
466
+ function normalizeVideoStoryUrl(entry: unknown): string | null {
467
+ if (typeof entry === "string" && entry.trim()) return entry.trim()
468
+ if (!entry || typeof entry !== "object") return null
469
+ const row = entry as Record<string, unknown>
470
+ const nested =
471
+ (row["video-story"] as Record<string, unknown> | undefined) ??
472
+ (row["video_stories"] as Record<string, unknown> | undefined) ??
473
+ row
474
+ const url =
475
+ nested["video-url"] ??
476
+ nested["video_url"] ??
477
+ nested.url ??
478
+ nested.src ??
479
+ nested.video
480
+ return typeof url === "string" && url.trim() ? url.trim() : null
481
+ }
317
482
 
483
+ /**
484
+ * Homepage video stories/reviews carousel (`homepage-config.video-stories`).
485
+ */
486
+ export function getVideoStoriesFromPageInput(
487
+ pageInput?: HomepageConfig | null
488
+ ): { title: string; videoUrls: string[] } {
489
+ const homepageConfig = resolvePageInput(pageInput)
490
+ const block = getMergedSectionBlock("videoStories", pageInput)
491
+ const copy = parseSectionCopy(block)
492
+ const title = copy.title ?? copy.name ?? ""
493
+ const raw =
494
+ block["video-urls"] ??
495
+ block.videoUrls ??
496
+ block.videos ??
497
+ block["video-stories"] ??
498
+ homepageConfig["video-stories"] ??
499
+ homepageConfig["video-stories-array"] ??
500
+ homepageConfig["video_stories"] ??
501
+ []
502
+ const list = Array.isArray(raw) ? raw : [raw]
503
+ const videoUrls = list
504
+ .map(normalizeVideoStoryUrl)
505
+ .filter((url): url is string => Boolean(url))
506
+ return { title, videoUrls }
507
+ }
318
508
 
509
+ /** @deprecated Pass `pageInput` from main project. */
510
+ export const getVideoStoriesFromConfig = async (): Promise<{
511
+ title: string
512
+ videoUrls: string[]
513
+ } | null> => {
514
+ const config = await getDynamicConfig()
515
+ return getVideoStoriesFromPageInput(config?.["homepage-config"] ?? null)
516
+ }
319
517
 
320
- return {
321
- phone: contactConfig["contact-phone"],
322
- email: normalizeConfigEmail(contactConfig["contact-email"]),
323
- address: contactConfig["contact-address"],
324
- }
325
- } catch (error) {
518
+ export type TestimonialItem = {
519
+ id: string
520
+ text: string
521
+ name: string
522
+ rating?: number
523
+ avatar?: string
524
+ }
326
525
 
327
- return null
526
+ function parseTestimonialRow(
527
+ item: unknown,
528
+ index: number
529
+ ): TestimonialItem | null {
530
+ if (!item || typeof item !== "object") return null
531
+ const row = (item as Record<string, unknown>).testimonial ??
532
+ (item as Record<string, unknown>).review ??
533
+ item
534
+ const r = row as Record<string, unknown>
535
+ const text = String(
536
+ r.text ?? r["review-description"] ?? r.description ?? r.quote ?? ""
537
+ ).trim()
538
+ const name = String(
539
+ r.name ?? r["review-user-name"] ?? r.author ?? r["user-name"] ?? ""
540
+ ).trim()
541
+ if (!text || !name) return null
542
+ const ratingRaw = r.rating ?? r["review-rating"]
543
+ const rating =
544
+ ratingRaw != null && ratingRaw !== ""
545
+ ? parseFloat(String(ratingRaw))
546
+ : undefined
547
+ return {
548
+ id: String(r.id ?? `review-${index}`),
549
+ text,
550
+ name,
551
+ rating: Number.isFinite(rating) ? rating : undefined,
552
+ avatar: (r.avatar ?? r["review-profile-image"] ?? r.image) as string | undefined,
328
553
  }
329
554
  }
330
555
 
331
- export const getTestimonialsFromConfig = async (): Promise<{ title: string; testimonials: Array<{ id: string; text: string; name: string; rating: number; avatar?: string }> } | null> => {
332
- try {
333
- const config = await getDynamicConfig()
334
- const homepageConfig = config?.["homepage-config"]
556
+ function parseTestimonialsFromBlock(block: Record<string, unknown>): TestimonialItem[] {
557
+ const raw =
558
+ block.items ??
559
+ block.testimonials ??
560
+ block.reviews ??
561
+ block["testimonial-items"] ??
562
+ []
563
+ if (!Array.isArray(raw)) return []
564
+ return raw
565
+ .map((item, index) => parseTestimonialRow(item, index))
566
+ .filter((t): t is TestimonialItem => t !== null)
567
+ }
335
568
 
336
- if (!homepageConfig) return null
569
+ function parseRatingsFromHomepage(
570
+ ratings: unknown
571
+ ): TestimonialItem[] {
572
+ if (!Array.isArray(ratings)) return []
573
+ return ratings
574
+ .map((item, index) => parseTestimonialRow(item, index))
575
+ .filter((t): t is TestimonialItem => t !== null)
576
+ }
577
+
578
+ export function getTestimonialsFromPageInput(
579
+ pageInput?: HomepageConfig | null
580
+ ): {
581
+ title: string
582
+ subtitle: string | null
583
+ testimonials: TestimonialItem[]
584
+ } {
585
+ const homepageConfig = resolvePageInput(pageInput)
586
+ const block = getMergedSectionBlock("testimonials", pageInput)
587
+ const copy = parseSectionCopy(block)
588
+ const title = copy.title ?? copy.name ?? ""
589
+ const subtitle = (copy.description ?? copy.text ?? null) as string | null
590
+ let testimonials = parseTestimonialsFromBlock(block)
591
+ if (testimonials.length === 0) {
592
+ testimonials = parseRatingsFromHomepage(homepageConfig["ratings"])
593
+ }
594
+ if (testimonials.length === 0) {
595
+ testimonials = parseTestimonialsFromBlock(
596
+ DEFAULT_HOMEPAGE_SECTIONS.testimonials as Record<string, unknown>
597
+ )
598
+ }
599
+ return { title, subtitle, testimonials }
600
+ }
337
601
 
338
- const title = homepageConfig["rating-title"] || "Happy Parents Say About Us"
339
- const ratings = homepageConfig["ratings"] || []
602
+ /** @deprecated Pass `pageInput` from main project. */
603
+ export const getTestimonialsFromConfig = async (): Promise<{
604
+ title: string
605
+ subtitle: string | null
606
+ testimonials: TestimonialItem[]
607
+ }> => {
608
+ const config = await getDynamicConfig()
609
+ return getTestimonialsFromPageInput(config?.["homepage-config"] ?? null)
610
+ }
340
611
 
341
- const testimonials = ratings.map((item: any, index: number) => {
342
- const review = item?.review || item
612
+ export function getSocialLinksFromPageInput(
613
+ pageInput?: HomepageConfig | null
614
+ ): Array<{ name: string; url: string; icon?: string }> | null {
615
+ const config = resolvePageInput(pageInput)
616
+ const socialLinksRaw = config["social-links"] || []
617
+ const socialLinks = socialLinksRaw
618
+ .map((item: Record<string, unknown>) => {
619
+ const link = (item?.["social-link"] || item) as Record<string, unknown>
343
620
  return {
344
- id: `review-${index}`,
345
- text: review?.["review-description"] || "",
346
- name: review?.["review-user-name"] || "",
347
- rating: parseFloat(review?.["review-rating"] || "5"),
348
- avatar: review?.["review-profile-image"] || undefined,
621
+ name: String(link?.["social-platform-name"] || ""),
622
+ url: String(link?.["social-platform-url"] || ""),
623
+ icon: link?.["social-platform-icon"] as string | undefined,
349
624
  }
350
- }).filter((t: any) => t.text && t.name)
625
+ })
626
+ .filter((l) => l.name && l.url)
627
+ return socialLinks.length > 0 ? socialLinks : null
628
+ }
351
629
 
630
+ /** @deprecated Pass `pageInput` from main project. */
631
+ export const getSocialLinksFromConfig = async (): Promise<Array<{
632
+ name: string
633
+ url: string
634
+ icon?: string
635
+ }> | null> => {
636
+ const config = await getDynamicConfig()
637
+ return getSocialLinksFromPageInput(config?.["homepage-config"] ?? null)
638
+ }
352
639
 
640
+ export function getFaqsFromPageInput(
641
+ pageInput?: HomepageConfig | null
642
+ ): Array<{ category: string; items: Array<{ q: string; a: string }> }> | null {
643
+ const config = resolvePageInput(pageInput)
644
+ const faqsRaw = config["faq-array"] || []
645
+ if (!Array.isArray(faqsRaw) || faqsRaw.length === 0) return null
646
+ const categoriesMap: Record<
647
+ string,
648
+ { displayCategory: string; items: Array<{ q: string; a: string }> }
649
+ > = {}
650
+ faqsRaw.forEach((item: Record<string, unknown>) => {
651
+ const faq = (item?.["faq"] || item) as Record<string, unknown>
652
+ const displayCategory = String(faq?.["faq-category"] || "General Questions").trim()
653
+ const categoryKey = displayCategory.toLowerCase()
654
+ const q = String(faq?.["faq-question"] || "").trim()
655
+ const a = String(faq?.["faq-answer"] || "").trim()
656
+ if (q && a) {
657
+ if (!categoriesMap[categoryKey]) {
658
+ categoriesMap[categoryKey] = { displayCategory, items: [] }
659
+ }
660
+ categoriesMap[categoryKey].items.push({ q, a })
661
+ }
662
+ })
663
+ const faqs = Object.values(categoriesMap).map((catData) => ({
664
+ category: catData.displayCategory,
665
+ items: catData.items,
666
+ }))
667
+ return faqs.length > 0 ? faqs : null
668
+ }
353
669
 
354
- return { title, testimonials }
355
- } catch (error) {
670
+ /** @deprecated Pass `pageInput` from main project. */
671
+ export const getFaqsFromConfig = async (): Promise<Array<{
672
+ category: string
673
+ items: Array<{ q: string; a: string }>
674
+ }> | null> => {
675
+ const config = await getDynamicConfig()
676
+ return getFaqsFromPageInput(config?.["homepage-config"] ?? null)
677
+ }
356
678
 
357
- return null
679
+ export function getAnnouncementMessagesFromPageInput(
680
+ pageInput?: HomepageConfig | null
681
+ ): string[] {
682
+ const block = getMergedSectionBlock("promoAnnouncements", pageInput)
683
+ return parseAnnouncementMessages(block)
684
+ }
685
+
686
+ /** @deprecated Pass `pageInput` from main project. */
687
+ export const getAnnouncementMessagesFromConfig = async (): Promise<string[]> => {
688
+ const config = await getDynamicConfig()
689
+ return getAnnouncementMessagesFromPageInput(config?.["homepage-config"] ?? null)
690
+ }
691
+
692
+ export type AboutBrandConfig = {
693
+ eyebrow: string
694
+ title: string
695
+ description: string
696
+ readMoreHref: string
697
+ stats: Array<{ label: string; value: string }>
698
+ }
699
+
700
+ export function getAboutBrandFromPageInput(
701
+ pageInput?: HomepageConfig | null
702
+ ): AboutBrandConfig {
703
+ const homepageConfig = resolvePageInput(pageInput)
704
+ const merged = getMergedSectionBlock("aboutBrand", pageInput)
705
+ const legacy =
706
+ homepageConfig["about-brand"] ?? homepageConfig["who-we-are"]
707
+ const b = mergeSectionBlock(
708
+ merged,
709
+ legacy && typeof legacy === "object"
710
+ ? (legacy as Record<string, unknown>)
711
+ : null
712
+ )
713
+ const statsRaw = (b.stats ?? b["about-stats"] ?? []) as unknown[]
714
+ const stats = Array.isArray(statsRaw)
715
+ ? statsRaw
716
+ .map((s: unknown) => {
717
+ if (!s || typeof s !== "object") return null
718
+ const row = s as Record<string, unknown>
719
+ const label = String(row.label ?? row.name ?? "").trim()
720
+ const value = String(row.value ?? "").trim()
721
+ return label && value ? { label, value } : null
722
+ })
723
+ .filter(Boolean) as Array<{ label: string; value: string }>
724
+ : []
725
+ const copy = parseSectionCopy(b)
726
+ return {
727
+ eyebrow: copy.eyebrow ?? "",
728
+ title: copy.title ?? copy.name ?? "",
729
+ description: copy.description ?? copy.text ?? "",
730
+ readMoreHref: String(b.readMoreHref ?? b["read-more-link"] ?? "/about"),
731
+ stats,
358
732
  }
359
733
  }
360
734
 
361
- export const getSocialLinksFromConfig = async (): Promise<Array<{ name: string; url: string; icon?: string }> | null> => {
362
- try {
363
- const config = await getDynamicConfig()
364
- const socialLinksRaw = config?.["homepage-config"]?.["social-links"] || []
735
+ /** @deprecated Pass `pageInput` from main project. */
736
+ export const getAboutBrandFromConfig = async (): Promise<AboutBrandConfig> => {
737
+ const config = await getDynamicConfig()
738
+ return getAboutBrandFromPageInput(config?.["homepage-config"] ?? null)
739
+ }
740
+
741
+ export type BrandPillarConfig = {
742
+ title: string
743
+ description: string
744
+ image?: string
745
+ }
746
+
747
+ export function getBrandPillarsFromPageInput(
748
+ pageInput?: HomepageConfig | null
749
+ ): { sectionTitle: string; pillars: BrandPillarConfig[] } {
750
+ const block = getMergedSectionBlock("brandPillars", pageInput)
751
+ const copy = parseSectionCopy(block)
752
+ const sectionTitle = copy.title ?? copy.name ?? ""
753
+ const raw = block.pillars ?? block["brand-pillars"] ?? []
754
+ const pillars = (Array.isArray(raw) ? raw : [])
755
+ .map((item: unknown) => {
756
+ if (!item || typeof item !== "object") return null
757
+ const row = (item as Record<string, unknown>)["pillar"] ?? item
758
+ const p = row as Record<string, unknown>
759
+ const title = String(p.title ?? p.name ?? "").trim()
760
+ const description = String(p.description ?? p.text ?? "").trim()
761
+ const image = (p.image ?? p.icon) as string | undefined
762
+ return title && description ? { title, description, image } : null
763
+ })
764
+ .filter(Boolean) as BrandPillarConfig[]
765
+ return { sectionTitle, pillars }
766
+ }
365
767
 
366
- const socialLinks = socialLinksRaw.map((item: any) => {
367
- const link = item?.["social-link"] || item
768
+ /** @deprecated Pass `pageInput` from main project. */
769
+ export const getBrandPillarsFromConfig = async (): Promise<{
770
+ sectionTitle: string
771
+ pillars: BrandPillarConfig[]
772
+ }> => {
773
+ const config = await getDynamicConfig()
774
+ return getBrandPillarsFromPageInput(config?.["homepage-config"] ?? null)
775
+ }
776
+
777
+ export type BlogPostConfig = {
778
+ title: string
779
+ excerpt?: string
780
+ category?: string
781
+ date?: string
782
+ href: string
783
+ image?: string
784
+ }
785
+
786
+ export function getBlogPostsFromPageInput(
787
+ pageInput?: HomepageConfig | null
788
+ ): BlogPostConfig[] {
789
+ const block = getMergedSectionBlock("blogPosts", pageInput)
790
+ const raw = block.posts ?? block["blog-posts"] ?? block["blog-array"] ?? []
791
+ return (Array.isArray(raw) ? raw : [])
792
+ .map((item: unknown) => {
793
+ if (!item || typeof item !== "object") return null
794
+ const row = (item as Record<string, unknown>)["blog"] ?? item
795
+ const b = row as Record<string, unknown>
796
+ const title = String(b.title ?? "").trim()
797
+ const href = String(b.href ?? b.link ?? b.url ?? "").trim()
798
+ if (!title || !href) return null
368
799
  return {
369
- name: link?.["social-platform-name"] || "",
370
- url: link?.["social-platform-url"] || "",
371
- icon: link?.["social-platform-icon"] || undefined,
800
+ title,
801
+ href,
802
+ excerpt: String(b.excerpt ?? b.description ?? "").trim() || undefined,
803
+ category: String(b.category ?? b.tag ?? "").trim() || undefined,
804
+ date: String(b.date ?? "").trim() || undefined,
805
+ image: (b.image ?? b.thumbnail) as string | undefined,
372
806
  }
373
- }).filter((l: any) => l.name && l.url)
374
-
807
+ })
808
+ .filter(Boolean) as BlogPostConfig[]
809
+ }
375
810
 
376
- return socialLinks.length > 0 ? socialLinks : null
377
- } catch (error) {
811
+ /** @deprecated Pass `pageInput` from main project. */
812
+ export const getBlogPostsFromConfig = async (): Promise<BlogPostConfig[]> => {
813
+ const config = await getDynamicConfig()
814
+ return getBlogPostsFromPageInput(config?.["homepage-config"] ?? null)
815
+ }
378
816
 
379
- return null
817
+ function readPromoCountdownBlock(block: unknown): {
818
+ code: string | null
819
+ message: string | null
820
+ endAt: string | null
821
+ active: boolean | undefined
822
+ ctaLabel: string | null
823
+ ctaHref: string | null
824
+ } {
825
+ if (!block || typeof block !== "object") {
826
+ return {
827
+ code: null,
828
+ message: null,
829
+ endAt: null,
830
+ active: undefined,
831
+ ctaLabel: null,
832
+ ctaHref: null,
833
+ }
380
834
  }
835
+ const row = block as Record<string, unknown>
836
+ const code =
837
+ normalizeCmsText(row["promo-code"] ?? row.code ?? row["coupon-code"]) ?? null
838
+ const message =
839
+ normalizeCmsText(row["promo-text"] ?? row.message ?? row.text) ?? null
840
+ const endAtRaw = row["end-at"] ?? row.endAt ?? row["ends-at"]
841
+ const endAt = typeof endAtRaw === "string" && endAtRaw.trim() ? endAtRaw.trim() : null
842
+ const activeRaw = row["promo-active"] ?? row.active
843
+ const active = typeof activeRaw === "boolean" ? activeRaw : undefined
844
+ const ctaLabel =
845
+ normalizeCmsText(row["cta-label"] ?? row.ctaLabel ?? row["button-label"]) ?? null
846
+ const ctaHref =
847
+ normalizeCmsText(row["cta-href"] ?? row.ctaHref ?? row["button-href"]) ?? null
848
+ return { code, message, endAt, active, ctaLabel, ctaHref }
381
849
  }
382
850
 
383
- export const getFaqsFromConfig = async (): Promise<Array<{ category: string; items: Array<{ q: string; a: string }> }> | null> => {
384
- try {
385
- const config = await getDynamicConfig()
386
- const faqsRaw = config?.["homepage-config"]?.["faq-array"] || []
851
+ /** Promo countdown defaults + `sections.promoCountdown` override. */
852
+ export function getPromoCountdownFromPageInput(
853
+ pageInput?: HomepageConfig | null
854
+ ): {
855
+ code: string | null
856
+ message: string | null
857
+ endAt: string | null
858
+ active: boolean
859
+ ctaLabel: string | null
860
+ ctaHref: string | null
861
+ } {
862
+ const block = getMergedSectionBlock("promoCountdown", pageInput)
863
+ const fromBlock = readPromoCountdownBlock(block)
864
+ const defaults = readPromoCountdownBlock(DEFAULT_HOMEPAGE_SECTIONS.promoCountdown)
865
+ return {
866
+ code: fromBlock.code ?? defaults.code,
867
+ message: fromBlock.message ?? defaults.message,
868
+ endAt: fromBlock.endAt ?? defaults.endAt,
869
+ active: fromBlock.active ?? defaults.active ?? false,
870
+ ctaLabel: fromBlock.ctaLabel ?? defaults.ctaLabel,
871
+ ctaHref: fromBlock.ctaHref ?? defaults.ctaHref,
872
+ }
873
+ }
387
874
 
388
- if (!Array.isArray(faqsRaw) || faqsRaw.length === 0) return null
875
+ /** @deprecated Pass `pageInput` from main project. */
876
+ export const getPromoCountdownFromConfig = async (): Promise<{
877
+ code: string | null
878
+ message: string | null
879
+ endAt: string | null
880
+ active: boolean
881
+ ctaLabel: string | null
882
+ ctaHref: string | null
883
+ }> => {
884
+ const config = await getDynamicConfig()
885
+ return getPromoCountdownFromPageInput(config?.["homepage-config"] ?? null)
886
+ }
389
887
 
390
- const categoriesMap: Record<string, { displayCategory: string, items: Array<{q: string; a: string}> }> = {}
888
+ export type InstagramPostConfig = {
889
+ id: string
890
+ image: string
891
+ url?: string
892
+ }
391
893
 
392
- faqsRaw.forEach((item: any) => {
393
- const faq = item?.["faq"] || item
394
- const displayCategory = (faq?.["faq-category"] || "General Questions").trim()
395
- const categoryKey = displayCategory.toLowerCase()
396
- const q = (faq?.["faq-question"] || "").trim()
397
- const a = (faq?.["faq-answer"] || "").trim()
894
+ export type InstagramPostsData = {
895
+ title: string
896
+ handle: string
897
+ profileUrl: string
898
+ profileImage?: string
899
+ posts: InstagramPostConfig[]
900
+ }
398
901
 
399
- if (q && a) {
400
- if (!categoriesMap[categoryKey]) categoriesMap[categoryKey] = { displayCategory, items: [] }
401
- categoriesMap[categoryKey].items.push({ q, a })
402
- }
403
- })
902
+ function parseInstagramPostRow(
903
+ item: unknown,
904
+ index: number
905
+ ): InstagramPostConfig | null {
906
+ if (!item || typeof item !== "object") return null
907
+ const row = (item as Record<string, unknown>).post ??
908
+ (item as Record<string, unknown>).instagram ??
909
+ item
910
+ const p = row as Record<string, unknown>
911
+ const image = String(p.image ?? p["post-image"] ?? p.thumbnail ?? p.src ?? "").trim()
912
+ if (!image) return null
913
+ const url = String(p.url ?? p.link ?? p.href ?? p["post-url"] ?? "").trim() || undefined
914
+ return {
915
+ id: String(p.id ?? `ig-${index}`),
916
+ image,
917
+ url,
918
+ }
919
+ }
404
920
 
405
- const faqs = Object.values(categoriesMap).map(catData => ({
406
- category: catData.displayCategory,
407
- items: catData.items
408
- }))
921
+ function parseInstagramPostsFromBlock(
922
+ block: Record<string, unknown>
923
+ ): InstagramPostConfig[] {
924
+ const raw =
925
+ block.posts ??
926
+ block.items ??
927
+ block["instagram-posts"] ??
928
+ block["posts-array"] ??
929
+ []
930
+ if (!Array.isArray(raw)) return []
931
+ return raw
932
+ .map((item, index) => parseInstagramPostRow(item, index))
933
+ .filter((p): p is InstagramPostConfig => p !== null)
934
+ }
409
935
 
410
- return faqs.length > 0 ? faqs : null
411
- } catch (error) {
412
- return null
936
+ /** Instagram feed — `sections.instagramPosts` + optional social-links fallback. */
937
+ export function getInstagramPostsFromPageInput(
938
+ pageInput?: HomepageConfig | null
939
+ ): InstagramPostsData {
940
+ const defaultsBlock = DEFAULT_HOMEPAGE_SECTIONS.instagramPosts as Record<
941
+ string,
942
+ unknown
943
+ >
944
+ const defaultCopy = parseSectionCopy(defaultsBlock)
945
+ const defaultPosts = parseInstagramPostsFromBlock(defaultsBlock)
946
+ const block = getMergedSectionBlock("instagramPosts", pageInput)
947
+ const copy = parseSectionCopy(block)
948
+ let posts = parseInstagramPostsFromBlock(block)
949
+ if (posts.length === 0) posts = defaultPosts
950
+ const handle =
951
+ normalizeCmsText(
952
+ block.handle ??
953
+ block.username ??
954
+ block["instagram-handle"] ??
955
+ defaultsBlock.handle
956
+ ) ?? "yourbrand"
957
+ const profileUrl =
958
+ normalizeCmsText(
959
+ block["profile-url"] ??
960
+ block.profileUrl ??
961
+ block["instagram-url"] ??
962
+ defaultsBlock["profile-url"]
963
+ ) ?? `https://www.instagram.com/${handle.replace(/^@/, "")}/`
964
+ const profileImage =
965
+ (block["profile-image"] ??
966
+ block.profileImage ??
967
+ defaultsBlock["profile-image"]) as string | undefined
968
+ return {
969
+ title:
970
+ copy.title ?? copy.name ?? defaultCopy.title ?? "Follow Us on Instagram",
971
+ handle,
972
+ profileUrl,
973
+ profileImage,
974
+ posts,
413
975
  }
414
976
  }
415
- export const getPromoBarConfig = async (): Promise<{ text: string | null; code: string | null; value: string | null; active: boolean }> => {
416
- try {
417
- const config = await getDynamicConfig()
418
- const promoConfig = config?.["homepage-config"]?.["promo-bar"]
419
977
 
420
- return {
421
- text: normalizeCmsText(promoConfig?.["promo-text"]) || null,
422
- code: promoConfig?.["promo-code"] || null,
423
- value: promoConfig?.["promo-value"] || null,
424
- active: promoConfig?.["promo-active"] ?? false,
425
- }
426
- } catch (error) {
427
- return { text: null, code: null, value: null, active: false }
428
- }
978
+ /** @deprecated Pass `pageInput` from main project. */
979
+ export const getInstagramPostsFromConfig = async (): Promise<InstagramPostsData> => {
980
+ const config = await getDynamicConfig()
981
+ return getInstagramPostsFromPageInput(config?.["homepage-config"] ?? null)
982
+ }
983
+
984
+ export {
985
+ DEFAULT_HOMEPAGE_SECTIONS,
986
+ type DefaultHomeSectionId,
987
+ } from "./homepage-section-defaults"
988
+ export type {
989
+ DynamicConfigResponse,
990
+ HomepageConfig,
991
+ HomepageSectionsConfig,
992
+ HomepageSectionId,
993
+ HomepageSectionCopyFields,
994
+ HomepageNewArrivalsSection,
995
+ } from "./homepage-config.types"
996
+ export { HOMEPAGE_SECTION_IDS } from "./homepage-config.types"
997
+
998
+ export function getPromoBarFromPageInput(
999
+ pageInput?: HomepageConfig | null
1000
+ ): { text: string | null; code: string | null; value: string | null; active: boolean } {
1001
+ const config = resolvePageInput(pageInput)
1002
+ const promoConfig = config["promo-bar"]
1003
+ const text = normalizeCmsText(promoConfig?.["promo-text"]) ?? null
1004
+ const code = normalizeCmsText(promoConfig?.["promo-code"]) ?? null
1005
+ const value = normalizeCmsText(promoConfig?.["promo-value"]) ?? null
1006
+ const active = Boolean(promoConfig?.["promo-active"])
1007
+ return { text, code, value, active }
1008
+ }
1009
+
1010
+ /** @deprecated Pass `pageInput` from main project. */
1011
+ export const getPromoBarConfig = async (): Promise<{
1012
+ text: string | null
1013
+ code: string | null
1014
+ value: string | null
1015
+ active: boolean
1016
+ }> => {
1017
+ const config = await getDynamicConfig()
1018
+ return getPromoBarFromPageInput(config?.["homepage-config"] ?? null)
429
1019
  }