@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.
- package/README.md +29 -0
- package/assets/hero-desktop.svg +10 -0
- package/assets/hero-mobile.svg +9 -0
- package/assets/logo.svg +3 -0
- package/package.json +60 -0
- package/src/blocks/home/Features/index.tsx +87 -0
- package/src/blocks/home/Hero/index.tsx +98 -0
- package/src/blocks/home/LovedByMoms/bestsellers-carousel.tsx +1 -0
- package/src/blocks/home/LovedByMoms/index.tsx +43 -0
- package/src/blocks/home/LovedByMoms/loved-by-moms-section.tsx +46 -0
- package/src/blocks/home/NewArrivals/index.tsx +91 -0
- package/src/blocks/home/PromotionalBanners/index.tsx +81 -0
- package/src/blocks/home/ShopByAge/collections-showcase-client.tsx +131 -0
- package/src/blocks/home/ShopByAge/collections-showcase-types.ts +4 -0
- package/src/blocks/home/ShopByAge/index.tsx +168 -0
- package/src/blocks/home/ShopByCategory/index.tsx +111 -0
- package/src/blocks/home/Testimonials/index.tsx +25 -0
- package/src/blocks/home/Testimonials/reviews-scroll.tsx +122 -0
- package/src/blocks/home/Testimonials/testimonials-client.tsx +127 -0
- package/src/blocks/home/WhyChooseUs/index.tsx +39 -0
- package/src/components/product-carousel.tsx +79 -0
- package/src/layouts/MainLayoutShell.tsx +14 -0
- package/src/primitives/Button.tsx +31 -0
- package/src/primitives/Card.tsx +32 -0
- package/src/primitives/index.ts +2 -0
- package/src/slots/account/ForgotPassword/index.tsx +1 -0
- package/src/slots/account/GoogleLogin/index.tsx +28 -0
- package/src/slots/account/Login/index.tsx +1 -0
- package/src/slots/account/LoginTemplate/index.tsx +12 -0
- package/src/slots/account/LoginTemplate/login-template-client.tsx +83 -0
- package/src/slots/account/Register/index.tsx +1 -0
- package/src/slots/cart/CartItem/index.tsx +11 -0
- package/src/slots/cart/CartSummary/index.tsx +8 -0
- package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
- package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
- package/src/slots/layout/Footer/index.tsx +95 -0
- package/src/slots/layout/Nav/index.tsx +50 -0
- package/src/slots/layout/Nav/nav-categories-dropdown.tsx +74 -0
- package/src/slots/layout/Nav/nav-collections-dropdown.tsx +106 -0
- package/src/slots/layout/Nav/nav-header-content.tsx +165 -0
- package/src/slots/layout/Nav/nav-header-shell.tsx +47 -0
- package/src/slots/layout/Nav/nav-link-luxury.tsx +15 -0
- package/src/slots/layout/PromoBar/index.tsx +9 -0
- package/src/slots/layout/PromoBar/promo-bar-content.tsx +118 -0
- package/src/slots/order/OrderDetails/index.tsx +12 -0
- package/src/slots/product/ProductActions/ProductCTASection.tsx +232 -0
- package/src/slots/product/ProductActions/ProductDetailsSection.tsx +200 -0
- package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +150 -0
- package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +112 -0
- package/src/slots/product/ProductActions/ProductOptionsSection.tsx +215 -0
- package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
- package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
- package/src/slots/product/ProductActions/SizeChartPanel.tsx +93 -0
- package/src/slots/product/ProductActions/index.tsx +156 -0
- package/src/slots/product/ProductActions/product-metadata-fields.ts +503 -0
- package/src/slots/product/ProductActions/size-chart-data.ts +108 -0
- package/src/slots/product/ProductCard/index.tsx +258 -0
- package/src/slots/product/ProductInfo/index.tsx +35 -0
- package/src/templates/CollectionsPage/index.tsx +72 -0
- package/src/templates/StorePage/index.tsx +134 -0
- package/src/tokens/colors.ts +21 -0
- package/src/tokens/fonts.ts +16 -0
- package/src/tokens/index.ts +3 -0
- package/src/tokens/spacing.ts +9 -0
- 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
|
+
}
|