@pradip1995/theme-sahsha 3.1.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 (65) hide show
  1. package/README.md +29 -0
  2. package/assets/hero-desktop.svg +10 -0
  3. package/assets/hero-mobile.svg +9 -0
  4. package/assets/logo.svg +3 -0
  5. package/package.json +60 -0
  6. package/src/blocks/home/Features/index.tsx +87 -0
  7. package/src/blocks/home/Hero/index.tsx +98 -0
  8. package/src/blocks/home/LovedByMoms/bestsellers-carousel.tsx +1 -0
  9. package/src/blocks/home/LovedByMoms/index.tsx +43 -0
  10. package/src/blocks/home/LovedByMoms/loved-by-moms-section.tsx +46 -0
  11. package/src/blocks/home/NewArrivals/index.tsx +91 -0
  12. package/src/blocks/home/PromotionalBanners/index.tsx +81 -0
  13. package/src/blocks/home/ShopByAge/collections-showcase-client.tsx +131 -0
  14. package/src/blocks/home/ShopByAge/collections-showcase-types.ts +4 -0
  15. package/src/blocks/home/ShopByAge/index.tsx +168 -0
  16. package/src/blocks/home/ShopByCategory/index.tsx +111 -0
  17. package/src/blocks/home/Testimonials/index.tsx +25 -0
  18. package/src/blocks/home/Testimonials/reviews-scroll.tsx +122 -0
  19. package/src/blocks/home/Testimonials/testimonials-client.tsx +127 -0
  20. package/src/blocks/home/WhyChooseUs/index.tsx +39 -0
  21. package/src/components/product-carousel.tsx +79 -0
  22. package/src/layouts/MainLayoutShell.tsx +14 -0
  23. package/src/primitives/Button.tsx +31 -0
  24. package/src/primitives/Card.tsx +32 -0
  25. package/src/primitives/index.ts +2 -0
  26. package/src/slots/account/ForgotPassword/index.tsx +1 -0
  27. package/src/slots/account/GoogleLogin/index.tsx +28 -0
  28. package/src/slots/account/Login/index.tsx +1 -0
  29. package/src/slots/account/LoginTemplate/index.tsx +12 -0
  30. package/src/slots/account/LoginTemplate/login-template-client.tsx +83 -0
  31. package/src/slots/account/Register/index.tsx +1 -0
  32. package/src/slots/cart/CartItem/index.tsx +11 -0
  33. package/src/slots/cart/CartSummary/index.tsx +8 -0
  34. package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
  35. package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
  36. package/src/slots/layout/Footer/index.tsx +95 -0
  37. package/src/slots/layout/Nav/index.tsx +50 -0
  38. package/src/slots/layout/Nav/nav-categories-dropdown.tsx +74 -0
  39. package/src/slots/layout/Nav/nav-collections-dropdown.tsx +106 -0
  40. package/src/slots/layout/Nav/nav-header-content.tsx +165 -0
  41. package/src/slots/layout/Nav/nav-header-shell.tsx +47 -0
  42. package/src/slots/layout/Nav/nav-link-luxury.tsx +15 -0
  43. package/src/slots/layout/PromoBar/index.tsx +9 -0
  44. package/src/slots/layout/PromoBar/promo-bar-content.tsx +118 -0
  45. package/src/slots/order/OrderDetails/index.tsx +12 -0
  46. package/src/slots/product/ProductActions/ProductCTASection.tsx +232 -0
  47. package/src/slots/product/ProductActions/ProductDetailsSection.tsx +200 -0
  48. package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +150 -0
  49. package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +112 -0
  50. package/src/slots/product/ProductActions/ProductOptionsSection.tsx +215 -0
  51. package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
  52. package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
  53. package/src/slots/product/ProductActions/SizeChartPanel.tsx +93 -0
  54. package/src/slots/product/ProductActions/index.tsx +156 -0
  55. package/src/slots/product/ProductActions/product-metadata-fields.ts +503 -0
  56. package/src/slots/product/ProductActions/size-chart-data.ts +108 -0
  57. package/src/slots/product/ProductCard/index.tsx +258 -0
  58. package/src/slots/product/ProductInfo/index.tsx +35 -0
  59. package/src/templates/CollectionsPage/index.tsx +72 -0
  60. package/src/templates/StorePage/index.tsx +134 -0
  61. package/src/tokens/colors.ts +21 -0
  62. package/src/tokens/fonts.ts +16 -0
  63. package/src/tokens/index.ts +3 -0
  64. package/src/tokens/spacing.ts +9 -0
  65. package/src/tokens/theme.css +12754 -0
@@ -0,0 +1,156 @@
1
+ "use client"
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react"
4
+ import { useParams } from "next/navigation"
5
+ import { HttpTypes } from "@medusajs/types"
6
+ import { useProductVariant } from "@core/hooks/use-product-variant"
7
+ import { useProductActions } from "@core/hooks/use-product-actions"
8
+ import { getDisplayPrice } from "@core/domain/product/pricing"
9
+ import LoginPopup from "@modules/common/components/login-popup"
10
+ import { ProductPriceSection } from "./ProductPriceSection"
11
+ import { ProductOptionsSection } from "./ProductOptionsSection"
12
+ import { ProductCTASection } from "./ProductCTASection"
13
+ import { ProductTrustSection } from "./ProductTrustSection"
14
+ import { ProductFeaturePanel } from "./ProductFeaturePanel"
15
+ import { ProductDetailsSection } from "./ProductDetailsSection"
16
+
17
+ type ProductActionsProps = {
18
+ product: HttpTypes.StoreProduct
19
+ region?: HttpTypes.StoreRegion
20
+ disabled?: boolean
21
+ cart?: HttpTypes.StoreCart | null
22
+ }
23
+
24
+ export default function ProductActions({
25
+ product,
26
+ region,
27
+ disabled,
28
+ cart,
29
+ }: ProductActionsProps) {
30
+ const countryCode = useParams().countryCode as string
31
+ const actionsRef = useRef<HTMLDivElement>(null)
32
+
33
+ const {
34
+ options,
35
+ setOptionValue,
36
+ setColorValue,
37
+ selectedVariant,
38
+ isValidVariant,
39
+ validationErrors,
40
+ validateOptions,
41
+ colorOption,
42
+ sizeOption,
43
+ otherOptions,
44
+ colorVariants,
45
+ selectedColorValue,
46
+ } = useProductVariant({ product })
47
+
48
+ const displayPrice = useMemo(
49
+ () => getDisplayPrice(product, selectedVariant),
50
+ [product, selectedVariant]
51
+ )
52
+
53
+ const {
54
+ quantity,
55
+ setQuantity,
56
+ isAdding,
57
+ isBuyingNow,
58
+ variantInCart,
59
+ quantityInCart,
60
+ inStock,
61
+ inventoryLimit,
62
+ handleAddToCart,
63
+ handleBuyNow,
64
+ handleIncreaseQuantity,
65
+ handleDecreaseQuantity,
66
+ } = useProductActions({
67
+ product,
68
+ region,
69
+ cart,
70
+ selectedVariant,
71
+ options,
72
+ isValidVariant,
73
+ displayPrice,
74
+ validateOptions,
75
+ })
76
+
77
+ const [activeFeature, setActiveFeature] = useState<string | null>(null)
78
+ const [showLoginPopup, setShowLoginPopup] = useState(false)
79
+ const [showNotifyMessage, setShowNotifyMessage] = useState(false)
80
+
81
+ useEffect(() => {
82
+ setShowNotifyMessage(false)
83
+ }, [options])
84
+
85
+ return (
86
+ <>
87
+ <div
88
+ className="flex flex-col gap-4 sm:gap-5 w-full"
89
+ ref={actionsRef}
90
+ >
91
+ {displayPrice && (
92
+ <ProductPriceSection
93
+ displayPrice={displayPrice}
94
+ quantityInCart={quantityInCart}
95
+ />
96
+ )}
97
+
98
+ <ProductOptionsSection
99
+ product={product}
100
+ disabled={disabled}
101
+ isAdding={isAdding}
102
+ options={options}
103
+ setOptionValue={setOptionValue}
104
+ setColorValue={setColorValue}
105
+ validationErrors={validationErrors}
106
+ colorOption={colorOption}
107
+ sizeOption={sizeOption}
108
+ otherOptions={otherOptions}
109
+ colorVariants={colorVariants}
110
+ selectedColorValue={selectedColorValue}
111
+ onOpenSizeChart={() => setActiveFeature("size_chart")}
112
+ />
113
+
114
+ <ProductCTASection
115
+ product={product}
116
+ selectedVariant={selectedVariant}
117
+ options={options}
118
+ isValidVariant={isValidVariant}
119
+ disabled={disabled}
120
+ inStock={inStock}
121
+ inventoryLimit={inventoryLimit}
122
+ quantity={quantity}
123
+ setQuantity={setQuantity}
124
+ quantityInCart={quantityInCart}
125
+ variantInCart={variantInCart}
126
+ isAdding={isAdding}
127
+ isBuyingNow={isBuyingNow}
128
+ showNotifyMessage={showNotifyMessage}
129
+ setShowNotifyMessage={setShowNotifyMessage}
130
+ handleAddToCart={handleAddToCart}
131
+ handleBuyNow={handleBuyNow}
132
+ handleIncreaseQuantity={handleIncreaseQuantity}
133
+ handleDecreaseQuantity={handleDecreaseQuantity}
134
+ />
135
+
136
+ <ProductTrustSection
137
+ selectedVariantId={selectedVariant?.id}
138
+ onOpenFeature={setActiveFeature}
139
+ />
140
+
141
+ <ProductFeaturePanel
142
+ activeFeature={activeFeature}
143
+ onClose={() => setActiveFeature(null)}
144
+ />
145
+
146
+ <ProductDetailsSection product={product} />
147
+ </div>
148
+
149
+ <LoginPopup
150
+ isOpen={showLoginPopup}
151
+ onClose={() => setShowLoginPopup(false)}
152
+ countryCode={countryCode}
153
+ />
154
+ </>
155
+ )
156
+ }
@@ -0,0 +1,503 @@
1
+ type ProductMetadataField = {
2
+ key: string
3
+ label: string
4
+ options?: Record<string, string>
5
+ }
6
+
7
+ /** Sahsha product styles */
8
+ export const SAHSHA_STYLE_OPTIONS: Record<string, string> = {
9
+ coords: "Coords",
10
+ straight_kurti_suit_pant: "Straight Kurti Suit Pant",
11
+ straight_kurti_suit_plazzo: "Straight Kurti Suit PLAZZO",
12
+ a_line_cut: "A line Cut",
13
+ anarkali_suit_with_duppatta: "Anarkali Suit with Duppatta",
14
+ indo_western: "Indo Western",
15
+ gown: "Gown",
16
+ drape_sarees: "Drape Saares",
17
+ pakistani_suits: "Pakistani Suits",
18
+ }
19
+
20
+ const STYLE_NARRATIVE_LABELS: Record<
21
+ string,
22
+ { top: string; bottom: string | null }
23
+ > = {
24
+ coords: { top: "Top design", bottom: "Bottom design" },
25
+ straight_kurti_suit_pant: { top: "Kurti design", bottom: "Pant design" },
26
+ straight_kurti_suit_plazzo: { top: "Kurti design", bottom: "Plazzo design" },
27
+ a_line_cut: { top: "Kurti design", bottom: "Bottom design" },
28
+ anarkali_suit_with_duppatta: {
29
+ top: "Anarkali design",
30
+ bottom: "Dupatta & bottom design",
31
+ },
32
+ indo_western: { top: "Indo Western design", bottom: null },
33
+ gown: { top: "Gown design", bottom: null },
34
+ drape_sarees: { top: "Saree design", bottom: null },
35
+ pakistani_suits: { top: "Suit design", bottom: "Bottom design" },
36
+ }
37
+
38
+ export const PRODUCT_METADATA_FIELDS: ProductMetadataField[] = [
39
+ { key: "style", label: "Style", options: SAHSHA_STYLE_OPTIONS },
40
+ {
41
+ key: "sleeve_length",
42
+ label: "Sleeve Length",
43
+ options: {
44
+ sleeveless: "Sleeveless",
45
+ cap_sleeve: "Cap Sleeve",
46
+ short_sleeve: "Short Sleeve",
47
+ half_sleeve: "Half Sleeve",
48
+ three_quarter_sleeves: "Three-Quarter Sleeves",
49
+ full_sleeve: "Full Sleeve",
50
+ },
51
+ },
52
+ {
53
+ key: "top_type",
54
+ label: "Top Type",
55
+ options: {
56
+ kurta: "Kurta",
57
+ kurti: "Kurti",
58
+ coord_top: "Coord Top",
59
+ gown: "Gown",
60
+ anarkali: "Anarkali",
61
+ indo_western: "Indo Western",
62
+ saree: "Saree",
63
+ dupatta: "Dupatta",
64
+ },
65
+ },
66
+ {
67
+ key: "top_pattern",
68
+ label: "Top Pattern",
69
+ options: {
70
+ solid: "Solid",
71
+ printed: "Printed",
72
+ embroidered: "Embroidered",
73
+ woven_design: "Woven Design",
74
+ floral: "Floral",
75
+ geometric: "Geometric",
76
+ },
77
+ },
78
+ {
79
+ key: "top_hemline",
80
+ label: "Top Hemline",
81
+ options: {
82
+ straight: "Straight",
83
+ flared: "Flared",
84
+ asymmetric: "Asymmetric",
85
+ high_low: "High-Low",
86
+ },
87
+ },
88
+ {
89
+ key: "neck",
90
+ label: "Neck",
91
+ options: {
92
+ round_neck: "Round Neck",
93
+ v_neck: "V Neck",
94
+ boat_neck: "Boat Neck",
95
+ square_neck: "Square Neck",
96
+ mandarin_collar: "Mandarin Collar",
97
+ off_shoulder: "Off Shoulder",
98
+ },
99
+ },
100
+ {
101
+ key: "bottom_closure",
102
+ label: "Bottom Closure",
103
+ options: {
104
+ slip_on: "Slip-On",
105
+ zip: "Zip",
106
+ button: "Button",
107
+ drawstring: "Drawstring",
108
+ },
109
+ },
110
+ {
111
+ key: "ornamentation",
112
+ label: "Ornamentation",
113
+ options: {
114
+ thread_work: "Thread Work",
115
+ zari_work: "Zari Work",
116
+ sequins: "Sequins",
117
+ mirror_work: "Mirror Work",
118
+ stone_work: "Stone Work",
119
+ plain: "Plain",
120
+ printed: "Printed",
121
+ },
122
+ },
123
+ {
124
+ key: "weave_type",
125
+ label: "Weave Type",
126
+ options: {
127
+ machine_weave: "Machine Weave",
128
+ handloom: "Handloom",
129
+ power_loom: "Power Loom",
130
+ },
131
+ },
132
+ {
133
+ key: "top_shape",
134
+ label: "Top Shape",
135
+ options: {
136
+ straight: "Straight",
137
+ a_line: "A-Line",
138
+ anarkali: "Anarkali",
139
+ flared: "Flared",
140
+ empire: "Empire",
141
+ },
142
+ },
143
+ {
144
+ key: "bottom_type",
145
+ label: "Bottom Type",
146
+ options: {
147
+ trousers: "Trousers",
148
+ palazzo: "Palazzo",
149
+ pants: "Pants",
150
+ sharara: "Sharara",
151
+ lehenga: "Lehenga",
152
+ skirt: "Skirt",
153
+ },
154
+ },
155
+ {
156
+ key: "top_design_styling",
157
+ label: "Top Design Styling",
158
+ options: {
159
+ regular: "Regular",
160
+ layered: "Layered",
161
+ peplum: "Peplum",
162
+ panelled: "Panelled",
163
+ },
164
+ },
165
+ {
166
+ key: "top_length",
167
+ label: "Top Length",
168
+ options: {
169
+ above_knee: "Above Knee",
170
+ knee_length: "Knee Length",
171
+ calf_length: "Calf Length",
172
+ ankle_length: "Ankle Length",
173
+ floor_length: "Floor Length",
174
+ },
175
+ },
176
+ {
177
+ key: "bottom_pattern",
178
+ label: "Bottom Pattern",
179
+ options: {
180
+ solid: "Solid",
181
+ printed: "Printed",
182
+ embroidered: "Embroidered",
183
+ floral: "Floral",
184
+ geometric: "Geometric",
185
+ },
186
+ },
187
+ {
188
+ key: "waistband",
189
+ label: "Waistband",
190
+ options: {
191
+ elasticated: "Elasticated",
192
+ drawstring: "Drawstring",
193
+ fixed: "Fixed",
194
+ },
195
+ },
196
+ {
197
+ key: "weave_pattern",
198
+ label: "Weave Pattern",
199
+ options: {
200
+ regular: "Regular",
201
+ jacquard: "Jacquard",
202
+ dobby: "Dobby",
203
+ plain: "Plain",
204
+ },
205
+ },
206
+ {
207
+ key: "pattern_coverage",
208
+ label: "Pattern Coverage",
209
+ options: {
210
+ large: "Large",
211
+ all_over: "All Over",
212
+ border: "Border",
213
+ minimal: "Minimal",
214
+ yoke: "Yoke",
215
+ },
216
+ },
217
+ { key: "fabric", label: "Fabric" },
218
+ ]
219
+
220
+ export type ProductMetadataEntry = {
221
+ key: string
222
+ label: string
223
+ value: string
224
+ }
225
+
226
+ export function formatMetadataValue(
227
+ raw: unknown,
228
+ field: ProductMetadataField
229
+ ): string | null {
230
+ if (raw === null || raw === undefined || raw === "") {
231
+ return null
232
+ }
233
+
234
+ const str = String(raw).trim()
235
+ if (!str) {
236
+ return null
237
+ }
238
+
239
+ if (field.options?.[str]) {
240
+ return field.options[str]
241
+ }
242
+
243
+ return str
244
+ .replace(/_/g, " ")
245
+ .replace(/\b\w/g, (char) => char.toUpperCase())
246
+ }
247
+
248
+ export const PRODUCT_DETAILS_FIELD_OPTIONS: { key: string; label: string }[] =
249
+ PRODUCT_METADATA_FIELDS.map((field) => ({
250
+ key: field.key,
251
+ label: field.label,
252
+ }))
253
+
254
+ export type ProductDetailsNarrative = {
255
+ key: string
256
+ title: string
257
+ items: string[]
258
+ }
259
+
260
+ function getNarrativeTitles(metadata: Record<string, unknown>) {
261
+ const styleKey = String(metadata.style || "").trim()
262
+ const labels =
263
+ STYLE_NARRATIVE_LABELS[styleKey] ||
264
+ STYLE_NARRATIVE_LABELS.coords
265
+
266
+ return {
267
+ top: labels.top,
268
+ bottom: labels.bottom,
269
+ }
270
+ }
271
+
272
+ function formatKeyAsLabel(key: string): string {
273
+ return key
274
+ .replace(/^custom_spec_/, "")
275
+ .replace(/_/g, " ")
276
+ .replace(/\b\w/g, (char) => char.toUpperCase())
277
+ }
278
+
279
+ function parseMetadataFieldKeys(raw: unknown): string[] | null {
280
+ if (raw == null || raw === "") return null
281
+
282
+ if (Array.isArray(raw)) {
283
+ const keys = raw.map((item) => String(item).trim()).filter(Boolean)
284
+ return keys.length > 0 ? keys : null
285
+ }
286
+
287
+ if (typeof raw === "string") {
288
+ const trimmed = raw.trim()
289
+ if (!trimmed) return null
290
+
291
+ try {
292
+ const parsed = JSON.parse(trimmed) as unknown
293
+ if (Array.isArray(parsed)) {
294
+ const keys = parsed.map((item) => String(item).trim()).filter(Boolean)
295
+ return keys.length > 0 ? keys : null
296
+ }
297
+ } catch {
298
+ // fall through
299
+ }
300
+
301
+ const keys = trimmed.split(",").map((item) => item.trim()).filter(Boolean)
302
+ return keys.length > 0 ? keys : null
303
+ }
304
+
305
+ return null
306
+ }
307
+
308
+ export function parseMetadataBulletItems(raw: unknown): string[] {
309
+ if (raw === null || raw === undefined || raw === "") {
310
+ return []
311
+ }
312
+
313
+ if (Array.isArray(raw)) {
314
+ return raw.map((item) => String(item).trim()).filter(Boolean)
315
+ }
316
+
317
+ const str = String(raw).trim()
318
+ if (!str) {
319
+ return []
320
+ }
321
+
322
+ if (str.includes("\n")) {
323
+ return str.split("\n").map((item) => item.trim()).filter(Boolean)
324
+ }
325
+
326
+ if (str.includes(",")) {
327
+ return str.split(",").map((item) => item.trim()).filter(Boolean)
328
+ }
329
+
330
+ return [str]
331
+ }
332
+
333
+ function getDetailsFieldLabel(key: string): string {
334
+ return (
335
+ PRODUCT_DETAILS_FIELD_OPTIONS.find((item) => item.key === key)?.label ||
336
+ PRODUCT_METADATA_FIELDS.find((item) => item.key === key)?.label ||
337
+ formatKeyAsLabel(key)
338
+ )
339
+ }
340
+
341
+ function buildMetadataEntry(
342
+ key: string,
343
+ metadata: Record<string, unknown>
344
+ ): ProductMetadataEntry | null {
345
+ const field = PRODUCT_METADATA_FIELDS.find((item) => item.key === key)
346
+ if (!field) {
347
+ return null
348
+ }
349
+
350
+ const value = formatMetadataValue(metadata[key], field)
351
+ if (!value) {
352
+ return null
353
+ }
354
+
355
+ return {
356
+ key,
357
+ label: getDetailsFieldLabel(key),
358
+ value,
359
+ }
360
+ }
361
+
362
+ export function getProductMetadataEntries(
363
+ metadata: Record<string, unknown> | null | undefined
364
+ ): ProductMetadataEntry[] {
365
+ if (!metadata) {
366
+ return []
367
+ }
368
+
369
+ const adminSelectedKeys = parseMetadataFieldKeys(metadata.details_fields)
370
+ const entries: ProductMetadataEntry[] = []
371
+
372
+ const keysToShow = adminSelectedKeys ?? PRODUCT_METADATA_FIELDS.map((field) => field.key)
373
+
374
+ for (const key of keysToShow) {
375
+ const entry = buildMetadataEntry(key, metadata)
376
+ if (entry) {
377
+ entries.push(entry)
378
+ }
379
+ }
380
+
381
+ return entries
382
+ }
383
+
384
+ export function getProductDetailsNarratives(
385
+ metadata: Record<string, unknown> | null | undefined
386
+ ): ProductDetailsNarrative[] {
387
+ if (!metadata) {
388
+ return []
389
+ }
390
+
391
+ const titles = getNarrativeTitles(metadata)
392
+ const sections: ProductDetailsNarrative[] = []
393
+
394
+ const topItems = parseMetadataBulletItems(metadata.top_design)
395
+ if (topItems.length > 0) {
396
+ sections.push({
397
+ key: "top_design",
398
+ title: titles.top,
399
+ items: topItems,
400
+ })
401
+ }
402
+
403
+ const bottomItems = parseMetadataBulletItems(metadata.bottom_design)
404
+ if (bottomItems.length > 0 && titles.bottom) {
405
+ sections.push({
406
+ key: "bottom_design",
407
+ title: titles.bottom,
408
+ items: bottomItems,
409
+ })
410
+ }
411
+
412
+ const sizeFitItems = parseMetadataBulletItems(metadata.size_fit)
413
+ if (sizeFitItems.length > 0) {
414
+ sections.push({
415
+ key: "size_fit",
416
+ title: "Size & Fit",
417
+ items: sizeFitItems,
418
+ })
419
+ }
420
+
421
+ const materialCareItems = parseMetadataBulletItems(metadata.material_care)
422
+ if (materialCareItems.length > 0) {
423
+ sections.push({
424
+ key: "material_care",
425
+ title: "Material & Care",
426
+ items: materialCareItems,
427
+ })
428
+ }
429
+
430
+ return sections
431
+ }
432
+
433
+ export function hasProductDetailsContent(product: {
434
+ title?: string | null
435
+ metadata?: Record<string, unknown> | null
436
+ }): boolean {
437
+ const metadata = product.metadata
438
+ return (
439
+ getProductMetadataEntries(metadata).length > 0 ||
440
+ getProductDetailsNarratives(metadata).length > 0 ||
441
+ Boolean(product.title?.trim())
442
+ )
443
+ }
444
+
445
+ const DEFAULT_STYLE_SPOTLIGHT_FIELDS = [
446
+ { key: "style", label: "Style" },
447
+ { key: "top_pattern", label: "Pattern" },
448
+ { key: "neck", label: "Neck" },
449
+ { key: "sleeve_length", label: "Sleeve Length" },
450
+ ]
451
+
452
+ export const STYLE_SPOTLIGHT_FIELD_OPTIONS: { key: string; label: string }[] = [
453
+ ...PRODUCT_DETAILS_FIELD_OPTIONS,
454
+ { key: "color", label: "Color" },
455
+ ]
456
+
457
+ export type StyleSpotlightEntry = {
458
+ label: string
459
+ value: string
460
+ }
461
+
462
+ function getSpotlightFieldLabel(key: string): string {
463
+ return (
464
+ STYLE_SPOTLIGHT_FIELD_OPTIONS.find((item) => item.key === key)?.label ||
465
+ getDetailsFieldLabel(key)
466
+ )
467
+ }
468
+
469
+ function parseSpotlightFieldKeys(raw: unknown): string[] | null {
470
+ return parseMetadataFieldKeys(raw)
471
+ }
472
+
473
+ export function getStyleSpotlightEntries(
474
+ metadata: Record<string, unknown> | null | undefined,
475
+ colorLabel?: string | null
476
+ ): StyleSpotlightEntry[] {
477
+ const adminSelectedKeys = parseSpotlightFieldKeys(metadata?.spotlight_fields)
478
+ const selectedKeys =
479
+ adminSelectedKeys ??
480
+ [...DEFAULT_STYLE_SPOTLIGHT_FIELDS.map((field) => field.key), "color"]
481
+
482
+ const entries: StyleSpotlightEntry[] = []
483
+
484
+ for (const key of selectedKeys) {
485
+ if (key === "color") {
486
+ const trimmedColor = colorLabel?.trim()
487
+ if (trimmedColor) {
488
+ entries.push({ label: getSpotlightFieldLabel("color"), value: trimmedColor })
489
+ }
490
+ continue
491
+ }
492
+
493
+ const field = PRODUCT_METADATA_FIELDS.find((item) => item.key === key)
494
+ if (!field) continue
495
+
496
+ const value = formatMetadataValue(metadata?.[key], field)
497
+ if (value) {
498
+ entries.push({ label: getSpotlightFieldLabel(key), value })
499
+ }
500
+ }
501
+
502
+ return entries
503
+ }