@akinon/pz-theme 2.0.37-rc.0 → 2.0.38-beta.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/CHANGELOG.md +15 -31
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
- package/src/__tests__/custom-css.test.ts +181 -0
- package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
- package/src/__tests__/facet-filters-block.test.tsx +707 -0
- package/src/__tests__/item-template.test.tsx +184 -0
- package/src/__tests__/listing-controls.test.tsx +961 -0
- package/src/__tests__/part-style-scope.test.tsx +229 -0
- package/src/__tests__/part-styles.test.ts +311 -0
- package/src/__tests__/registry-parity.test.ts +42 -0
- package/src/__tests__/row-visibility.test.ts +672 -0
- package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
- package/src/__tests__/theme-block-html-id.test.tsx +167 -0
- package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
- package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
- package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
- package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
- package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
- package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
- package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
- package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
- package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
- package/src/blocks/__tests__/share-block.test.tsx +392 -0
- package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
- package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
- package/src/blocks/__tests__/variant-selector-golden.json +20 -0
- package/src/blocks/account-menu-block.tsx +264 -0
- package/src/blocks/add-to-cart-block.tsx +417 -0
- package/src/blocks/address-list-block.tsx +348 -0
- package/src/blocks/applied-filters-block.tsx +288 -0
- package/src/blocks/auth-form-block.tsx +295 -0
- package/src/blocks/basket-items-block.tsx +457 -0
- package/src/blocks/basket-summary-block.tsx +404 -0
- package/src/blocks/block-renderer-registry.tsx +94 -0
- package/src/blocks/change-password-form-block.tsx +240 -0
- package/src/blocks/checkout-steps-block.tsx +288 -0
- package/src/blocks/checkout-summary-block.tsx +449 -0
- package/src/blocks/countdown-block.tsx +182 -0
- package/src/blocks/coupon-list-block.tsx +292 -0
- package/src/blocks/facet-filters-block.tsx +1686 -0
- package/src/blocks/faq-accordion-block.tsx +358 -0
- package/src/blocks/favorite-button-block.tsx +252 -0
- package/src/blocks/favourites-grid-block.tsx +510 -0
- package/src/blocks/forgot-password-block.tsx +155 -0
- package/src/blocks/item-template-utils.ts +131 -0
- package/src/blocks/item-template.tsx +164 -0
- package/src/blocks/listing-breadcrumb-block.tsx +175 -0
- package/src/blocks/listing-context.ts +850 -0
- package/src/blocks/listing-pagination-block.tsx +923 -0
- package/src/blocks/listing-result-count-block.tsx +126 -0
- package/src/blocks/listing-title-block.tsx +115 -0
- package/src/blocks/oauth-buttons-block.tsx +205 -0
- package/src/blocks/order-detail-block.tsx +474 -0
- package/src/blocks/order-list-block.tsx +274 -0
- package/src/blocks/order-success-block.tsx +641 -0
- package/src/blocks/order-tracking-block.tsx +533 -0
- package/src/blocks/product-attributes-block.tsx +307 -0
- package/src/blocks/product-block-utils.tsx +639 -0
- package/src/blocks/product-breadcrumb-block.tsx +215 -0
- package/src/blocks/product-description-block.tsx +169 -0
- package/src/blocks/product-gallery-block.tsx +1523 -0
- package/src/blocks/product-grid-block.tsx +966 -0
- package/src/blocks/product-price-block.tsx +373 -0
- package/src/blocks/product-recommendations-block.tsx +1433 -0
- package/src/blocks/product-title-block.tsx +129 -0
- package/src/blocks/profile-form-block.tsx +297 -0
- package/src/blocks/recently-viewed-store.ts +201 -0
- package/src/blocks/register-form-block.tsx +362 -0
- package/src/blocks/share-block.tsx +667 -0
- package/src/blocks/size-guide-block.tsx +313 -0
- package/src/blocks/sort-select-block.tsx +636 -0
- package/src/blocks/stock-status-block.tsx +168 -0
- package/src/blocks/store-locator-block.tsx +1184 -0
- package/src/blocks/variant-selector-block.tsx +1110 -0
- package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
|
+
import {
|
|
8
|
+
MoneyCurrencyDisplay,
|
|
9
|
+
MoneyCurrencyPosition,
|
|
10
|
+
MoneyDecimals,
|
|
11
|
+
MoneyStyleOptions
|
|
12
|
+
} from './commerce-format';
|
|
13
|
+
import {
|
|
14
|
+
DesignerPlaceholder,
|
|
15
|
+
formatPrice,
|
|
16
|
+
getPreviousPrice,
|
|
17
|
+
getVariantPriceRange,
|
|
18
|
+
parsePriceValue,
|
|
19
|
+
pickProductField,
|
|
20
|
+
readToggle,
|
|
21
|
+
useThemeProduct
|
|
22
|
+
} from './product-block-utils';
|
|
23
|
+
|
|
24
|
+
const ALLOWED_ORDERS = ['retail-first', 'price-first'];
|
|
25
|
+
const ALLOWED_DISCOUNT_MODES = ['percent', 'amount', 'both'];
|
|
26
|
+
const ALLOWED_ALIGNMENTS = ['flex-start', 'center', 'flex-end'];
|
|
27
|
+
const ALLOWED_DECIMALS = ['always', 'auto', 'never'];
|
|
28
|
+
const ALLOWED_CURRENCY_DISPLAYS = ['', 'symbol', 'code', 'custom', 'none'];
|
|
29
|
+
const ALLOWED_CURRENCY_POSITIONS = ['', 'before', 'after'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Functional PDP block: current price with the retail price struck through
|
|
33
|
+
* when it is actually higher, and an optional saving badge. Values stay
|
|
34
|
+
* commerce-driven; the editor owns placement, order, currency visibility,
|
|
35
|
+
* the money format, the badge wording and — through named parts — the styling.
|
|
36
|
+
*
|
|
37
|
+
* Three additions are switched off by default, so a published composition that
|
|
38
|
+
* never touched them renders exactly the markup it always did: the price
|
|
39
|
+
* history caption (showPreviousPrice), the variant price range
|
|
40
|
+
* (showPriceRange) and the percentage-plus-amount badge (discountMode: both).
|
|
41
|
+
*
|
|
42
|
+
* Parts: row, price, price-min, range-separator, price-max, retail-price,
|
|
43
|
+
* discount, discount-percent, discount-amount, history, history-caption,
|
|
44
|
+
* previous-price.
|
|
45
|
+
* Properties: showRetail, showCurrency, decimals, currencyDisplay,
|
|
46
|
+
* currencyText, currencyPosition, gap, retailColor, order, align,
|
|
47
|
+
* showDiscountBadge, discountMode, discountTemplate, discountBothTemplate,
|
|
48
|
+
* showPreviousPrice, previousPriceTemplate, showPriceRange,
|
|
49
|
+
* priceRangeSeparator.
|
|
50
|
+
*/
|
|
51
|
+
const ProductPriceBlock = ({
|
|
52
|
+
block,
|
|
53
|
+
currentBreakpoint = 'desktop',
|
|
54
|
+
isDesigner = false
|
|
55
|
+
}: BlockRendererProps) => {
|
|
56
|
+
const product = useThemeProduct();
|
|
57
|
+
const { locale, currency: activeCurrency } = useLocalization();
|
|
58
|
+
|
|
59
|
+
const prop = (key: string, fallback: string) =>
|
|
60
|
+
String(
|
|
61
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const showRetail = readToggle(
|
|
65
|
+
block.properties?.showRetail,
|
|
66
|
+
currentBreakpoint,
|
|
67
|
+
true
|
|
68
|
+
);
|
|
69
|
+
const showCurrency = readToggle(
|
|
70
|
+
block.properties?.showCurrency,
|
|
71
|
+
currentBreakpoint,
|
|
72
|
+
true
|
|
73
|
+
);
|
|
74
|
+
// Designs are written in the brand's own money format ("1.599 TL"), which no
|
|
75
|
+
// locale produces. Every value below is validated against the list the editor
|
|
76
|
+
// offers, so a hand-edited composition cannot reach Intl with a bad option.
|
|
77
|
+
const decimalsValue = prop('decimals', 'always');
|
|
78
|
+
const currencyDisplayValue = prop('currencyDisplay', '');
|
|
79
|
+
const currencyPositionValue = prop('currencyPosition', '');
|
|
80
|
+
const money: MoneyStyleOptions = {
|
|
81
|
+
decimals: (ALLOWED_DECIMALS.includes(decimalsValue)
|
|
82
|
+
? decimalsValue
|
|
83
|
+
: 'always') as MoneyDecimals,
|
|
84
|
+
currencyDisplay: (ALLOWED_CURRENCY_DISPLAYS.includes(currencyDisplayValue)
|
|
85
|
+
? currencyDisplayValue
|
|
86
|
+
: '') as MoneyCurrencyDisplay,
|
|
87
|
+
currencyText: prop('currencyText', ''),
|
|
88
|
+
currencyPosition: (ALLOWED_CURRENCY_POSITIONS.includes(
|
|
89
|
+
currencyPositionValue
|
|
90
|
+
)
|
|
91
|
+
? currencyPositionValue
|
|
92
|
+
: '') as MoneyCurrencyPosition
|
|
93
|
+
};
|
|
94
|
+
const gap = Number(
|
|
95
|
+
getResponsiveValue(block.properties?.gap, currentBreakpoint, 8)
|
|
96
|
+
);
|
|
97
|
+
const retailColor = String(
|
|
98
|
+
getResponsiveValue(block.properties?.retailColor, currentBreakpoint, '')
|
|
99
|
+
);
|
|
100
|
+
const orderValue = prop('order', 'retail-first');
|
|
101
|
+
const order = ALLOWED_ORDERS.includes(orderValue)
|
|
102
|
+
? orderValue
|
|
103
|
+
: 'retail-first';
|
|
104
|
+
const alignValue = prop('align', 'flex-start');
|
|
105
|
+
const align = ALLOWED_ALIGNMENTS.includes(alignValue)
|
|
106
|
+
? alignValue
|
|
107
|
+
: 'flex-start';
|
|
108
|
+
const showDiscountBadge = readToggle(
|
|
109
|
+
block.properties?.showDiscountBadge,
|
|
110
|
+
currentBreakpoint,
|
|
111
|
+
false
|
|
112
|
+
);
|
|
113
|
+
const discountModeValue = prop('discountMode', 'percent');
|
|
114
|
+
const discountMode = ALLOWED_DISCOUNT_MODES.includes(discountModeValue)
|
|
115
|
+
? discountModeValue
|
|
116
|
+
: 'percent';
|
|
117
|
+
const discountTemplate = prop('discountTemplate', '-{value}');
|
|
118
|
+
const discountBothTemplate = prop(
|
|
119
|
+
'discountBothTemplate',
|
|
120
|
+
'-{percent} ({amount})'
|
|
121
|
+
);
|
|
122
|
+
const showPreviousPrice = readToggle(
|
|
123
|
+
block.properties?.showPreviousPrice,
|
|
124
|
+
currentBreakpoint,
|
|
125
|
+
false
|
|
126
|
+
);
|
|
127
|
+
const previousPriceTemplate = prop(
|
|
128
|
+
'previousPriceTemplate',
|
|
129
|
+
'Lowest price in the last 30 days: {value}'
|
|
130
|
+
);
|
|
131
|
+
const showPriceRange = readToggle(
|
|
132
|
+
block.properties?.showPriceRange,
|
|
133
|
+
currentBreakpoint,
|
|
134
|
+
false
|
|
135
|
+
);
|
|
136
|
+
const priceRangeSeparator = prop('priceRangeSeparator', ' – ');
|
|
137
|
+
|
|
138
|
+
const priceValue = pickProductField(product, 'price');
|
|
139
|
+
const retailValue =
|
|
140
|
+
pickProductField(product, 'retailPrice') ??
|
|
141
|
+
pickProductField(product, 'retail_price');
|
|
142
|
+
// Commerce sends the currency on the product; the active currency is only
|
|
143
|
+
// the fallback, and the grid block resolves it the same way so both render
|
|
144
|
+
// the same money string for the same value.
|
|
145
|
+
const currency =
|
|
146
|
+
pickProductField<string>(product, 'currency') ??
|
|
147
|
+
pickProductField<string>(product, 'currency_type') ??
|
|
148
|
+
activeCurrency ??
|
|
149
|
+
'';
|
|
150
|
+
|
|
151
|
+
const priceText = formatPrice(
|
|
152
|
+
priceValue,
|
|
153
|
+
currency,
|
|
154
|
+
showCurrency,
|
|
155
|
+
locale,
|
|
156
|
+
money
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!priceText) {
|
|
160
|
+
return isDesigner ? (
|
|
161
|
+
<DesignerPlaceholder
|
|
162
|
+
label="Product price (no price)"
|
|
163
|
+
attribute="data-product-price"
|
|
164
|
+
/>
|
|
165
|
+
) : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const retailText = formatPrice(
|
|
169
|
+
retailValue,
|
|
170
|
+
currency,
|
|
171
|
+
showCurrency,
|
|
172
|
+
locale,
|
|
173
|
+
money
|
|
174
|
+
);
|
|
175
|
+
const priceNumber = parsePriceValue(priceValue);
|
|
176
|
+
const retailNumber = parsePriceValue(retailValue);
|
|
177
|
+
// A retail price equal to (or below) the active price is not a discount and
|
|
178
|
+
// must not be struck through — commerce sends both fields unconditionally.
|
|
179
|
+
const hasDiscount =
|
|
180
|
+
showRetail &&
|
|
181
|
+
Boolean(retailText) &&
|
|
182
|
+
priceNumber !== null &&
|
|
183
|
+
retailNumber !== null &&
|
|
184
|
+
retailNumber > priceNumber;
|
|
185
|
+
|
|
186
|
+
const safeGap = Number.isFinite(gap) && gap >= 0 ? gap : 8;
|
|
187
|
+
|
|
188
|
+
const saving =
|
|
189
|
+
showDiscountBadge && hasDiscount && priceNumber !== null && retailNumber !== null
|
|
190
|
+
? {
|
|
191
|
+
amount: retailNumber - priceNumber,
|
|
192
|
+
percent: Math.round(
|
|
193
|
+
((retailNumber - priceNumber) / retailNumber) * 100
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
: null;
|
|
197
|
+
// The saving is money, so it goes through the shared formatter with the
|
|
198
|
+
// explicit locale rather than being pasted together from raw numbers.
|
|
199
|
+
const savedAmountText = saving
|
|
200
|
+
? formatPrice(saving.amount, currency, showCurrency, locale, money)
|
|
201
|
+
: null;
|
|
202
|
+
const savedPercentText = saving ? `${saving.percent}%` : null;
|
|
203
|
+
const discountText =
|
|
204
|
+
discountMode === 'amount' ? savedAmountText : savedPercentText;
|
|
205
|
+
|
|
206
|
+
const priceRange = showPriceRange ? getVariantPriceRange(product) : null;
|
|
207
|
+
const rangeMinText = priceRange
|
|
208
|
+
? formatPrice(priceRange.min, currency, showCurrency, locale, money)
|
|
209
|
+
: null;
|
|
210
|
+
const rangeMaxText = priceRange
|
|
211
|
+
? formatPrice(priceRange.max, currency, showCurrency, locale, money)
|
|
212
|
+
: null;
|
|
213
|
+
const hasRange = Boolean(rangeMinText && rangeMaxText);
|
|
214
|
+
|
|
215
|
+
const previousPriceText = showPreviousPrice
|
|
216
|
+
? formatPrice(
|
|
217
|
+
getPreviousPrice(product),
|
|
218
|
+
currency,
|
|
219
|
+
showCurrency,
|
|
220
|
+
locale,
|
|
221
|
+
money
|
|
222
|
+
)
|
|
223
|
+
: null;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Prints a template with its placeholders swapped for their own parts, so a
|
|
227
|
+
* brand can colour the saved amount without touching the words around it.
|
|
228
|
+
* The delimiters are kept by the capture group; a template that names no
|
|
229
|
+
* placeholder simply renders as text. `textPart` names the literal words
|
|
230
|
+
* when they are stylable in their own right, as the legal caption is.
|
|
231
|
+
*/
|
|
232
|
+
const renderTemplate = (
|
|
233
|
+
template: string,
|
|
234
|
+
tokens: Record<string, { text: string; part: string }>,
|
|
235
|
+
textPart?: string
|
|
236
|
+
) =>
|
|
237
|
+
template.split(/(\{[a-z]+\})/gi).map((segment, index) => {
|
|
238
|
+
const token = tokens[segment];
|
|
239
|
+
|
|
240
|
+
if (token) {
|
|
241
|
+
return (
|
|
242
|
+
<span key={index} {...partAttrs(token.part)}>
|
|
243
|
+
{token.text}
|
|
244
|
+
</span>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!segment) return null;
|
|
249
|
+
|
|
250
|
+
return textPart ? (
|
|
251
|
+
<span key={index} {...partAttrs(textPart)}>
|
|
252
|
+
{segment}
|
|
253
|
+
</span>
|
|
254
|
+
) : (
|
|
255
|
+
<React.Fragment key={index}>{segment}</React.Fragment>
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 'both' is the only mode that needs two values in one badge; 'percent' and
|
|
260
|
+
// 'amount' keep printing a single string child, which is the markup every
|
|
261
|
+
// published badge has today.
|
|
262
|
+
const badgeContent =
|
|
263
|
+
discountMode === 'both'
|
|
264
|
+
? savedAmountText && savedPercentText
|
|
265
|
+
? renderTemplate(discountBothTemplate, {
|
|
266
|
+
'{percent}': { text: savedPercentText, part: 'discount-percent' },
|
|
267
|
+
'{amount}': { text: savedAmountText, part: 'discount-amount' }
|
|
268
|
+
})
|
|
269
|
+
: null
|
|
270
|
+
: discountText
|
|
271
|
+
? discountTemplate.replace(/\{value\}/g, discountText)
|
|
272
|
+
: null;
|
|
273
|
+
|
|
274
|
+
// A cleared template would otherwise drop the price out of a line whose
|
|
275
|
+
// whole purpose is to state it, so the placeholder is appended instead.
|
|
276
|
+
const historyTemplate = previousPriceTemplate.includes('{value}')
|
|
277
|
+
? previousPriceTemplate
|
|
278
|
+
: `${previousPriceTemplate} {value}`.trim();
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The caption joins the existing wrapping row as a full-width child rather
|
|
282
|
+
* than arriving in a new wrapper: the row already wraps, so a 100% basis
|
|
283
|
+
* puts the caption on its own line under the price — and a composition with
|
|
284
|
+
* the caption switched off keeps byte-for-byte the DOM it renders today.
|
|
285
|
+
*/
|
|
286
|
+
const historyNode = previousPriceText ? (
|
|
287
|
+
<span
|
|
288
|
+
data-product-previous-price=""
|
|
289
|
+
{...partAttrs('history')}
|
|
290
|
+
style={{
|
|
291
|
+
flexBasis: '100%',
|
|
292
|
+
fontSize: '0.75em',
|
|
293
|
+
color: 'var(--product-previous-price-color, #6b7280)'
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{renderTemplate(
|
|
297
|
+
historyTemplate,
|
|
298
|
+
{
|
|
299
|
+
'{value}': { text: previousPriceText, part: 'previous-price' }
|
|
300
|
+
},
|
|
301
|
+
'history-caption'
|
|
302
|
+
)}
|
|
303
|
+
</span>
|
|
304
|
+
) : showPreviousPrice && isDesigner ? (
|
|
305
|
+
// Designer-only: the toggle is on but commerce sent no price history, and
|
|
306
|
+
// an author needs to see that the caption is missing for a reason.
|
|
307
|
+
<DesignerPlaceholder
|
|
308
|
+
label="Lowest price (no price history)"
|
|
309
|
+
attribute="data-product-previous-price"
|
|
310
|
+
/>
|
|
311
|
+
) : null;
|
|
312
|
+
|
|
313
|
+
const retailNode = hasDiscount ? (
|
|
314
|
+
<span
|
|
315
|
+
data-product-retail-price=""
|
|
316
|
+
{...partAttrs('retail-price')}
|
|
317
|
+
style={{
|
|
318
|
+
textDecoration: 'line-through',
|
|
319
|
+
fontSize: '0.85em',
|
|
320
|
+
color: retailColor || 'var(--product-retail-price-color, #9ca3af)'
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
{retailText}
|
|
324
|
+
</span>
|
|
325
|
+
) : null;
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<div
|
|
329
|
+
data-product-price=""
|
|
330
|
+
data-product-price-discounted={hasDiscount ? 'true' : 'false'}
|
|
331
|
+
{...partAttrs('row')}
|
|
332
|
+
style={{
|
|
333
|
+
display: 'flex',
|
|
334
|
+
alignItems: 'baseline',
|
|
335
|
+
justifyContent: align,
|
|
336
|
+
flexWrap: 'wrap',
|
|
337
|
+
gap: `${safeGap}px`
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
{order === 'retail-first' && retailNode}
|
|
341
|
+
<span data-product-current-price="" {...partAttrs('price')}>
|
|
342
|
+
{hasRange ? (
|
|
343
|
+
<>
|
|
344
|
+
<span {...partAttrs('price-min')}>{rangeMinText}</span>
|
|
345
|
+
<span {...partAttrs('range-separator')}>{priceRangeSeparator}</span>
|
|
346
|
+
<span {...partAttrs('price-max')}>{rangeMaxText}</span>
|
|
347
|
+
</>
|
|
348
|
+
) : (
|
|
349
|
+
priceText
|
|
350
|
+
)}
|
|
351
|
+
</span>
|
|
352
|
+
{order === 'price-first' && retailNode}
|
|
353
|
+
{badgeContent && (
|
|
354
|
+
<span
|
|
355
|
+
data-product-discount-badge=""
|
|
356
|
+
{...partAttrs('discount')}
|
|
357
|
+
style={{
|
|
358
|
+
fontSize: '0.75em',
|
|
359
|
+
padding: '2px 6px',
|
|
360
|
+
borderRadius: '2px',
|
|
361
|
+
background: 'var(--product-discount-badge-bg, #dc2626)',
|
|
362
|
+
color: 'var(--product-discount-badge-color, #ffffff)'
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
{badgeContent}
|
|
366
|
+
</span>
|
|
367
|
+
)}
|
|
368
|
+
{historyNode}
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
export default ProductPriceBlock;
|