@akinon/pz-theme 2.0.66-beta.0 → 2.0.66
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 +4 -4
- package/package.json +4 -4
- package/src/sections/posts-slider-section.tsx +170 -1
- package/src/utils/iterator-utils.ts +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.66
|
|
3
|
+
## 2.0.66
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
- @akinon/
|
|
9
|
-
- @akinon/
|
|
7
|
+
- Updated dependencies [7891fab]
|
|
8
|
+
- @akinon/theme-contract@2.0.66
|
|
9
|
+
- @akinon/next@2.0.66
|
|
10
10
|
|
|
11
11
|
## 2.0.65
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.66
|
|
4
|
+
"version": "2.0.66",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.66
|
|
11
|
+
"@akinon/next": "2.0.66",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.66
|
|
16
|
+
"@akinon/theme-contract": "2.0.66",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.66
|
|
23
|
+
"@akinon/next": "2.0.66",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -7,6 +7,7 @@ import { twMerge } from 'tailwind-merge';
|
|
|
7
7
|
import ThemeBlock from '../theme-block';
|
|
8
8
|
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
9
9
|
import { safeDeepClone } from '../utils/safe-clone';
|
|
10
|
+
import { getPriceWithCurrency, isPriceBinding } from '../utils/iterator-utils';
|
|
10
11
|
import {
|
|
11
12
|
applyFirstSlideImageLoading,
|
|
12
13
|
getCSSStyles,
|
|
@@ -64,6 +65,23 @@ const cloneBlock = (block: any): any => ({
|
|
|
64
65
|
: undefined
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
const parseProductPrice = (value: unknown): number => {
|
|
69
|
+
const parsed = Number(value);
|
|
70
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const readProductPricePair = (productData: Record<string, unknown>) => {
|
|
74
|
+
const activePrice = (productData as any)?.active_price as
|
|
75
|
+
| Record<string, unknown>
|
|
76
|
+
| undefined;
|
|
77
|
+
return {
|
|
78
|
+
current: parseProductPrice(activePrice?.price ?? (productData as any)?.price),
|
|
79
|
+
retail: parseProductPrice(
|
|
80
|
+
activePrice?.retail_price ?? (productData as any)?.retail_price
|
|
81
|
+
)
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
67
85
|
const replaceBlockValues = (
|
|
68
86
|
blockToReplace: any,
|
|
69
87
|
productData: Record<string, unknown>,
|
|
@@ -78,6 +96,65 @@ const replaceBlockValues = (
|
|
|
78
96
|
'item.',
|
|
79
97
|
''
|
|
80
98
|
);
|
|
99
|
+
|
|
100
|
+
// No payload carries a discount percentage, but every one carries the two
|
|
101
|
+
// prices it derives from — so `item.discount_percentage` is computed here.
|
|
102
|
+
// A non-discounted product hides the block instead of printing "-%0".
|
|
103
|
+
if (bindingPath === 'discount_percentage') {
|
|
104
|
+
const { current, retail } = readProductPricePair(productData);
|
|
105
|
+
if (retail > 0 && current > 0 && retail > current) {
|
|
106
|
+
newBlock.value = `-%${Math.round((1 - current / retail) * 100)}`;
|
|
107
|
+
newBlock.hidden = false;
|
|
108
|
+
} else {
|
|
109
|
+
newBlock.hidden = true;
|
|
110
|
+
}
|
|
111
|
+
if (newBlock.blocks && newBlock.blocks.length > 0) {
|
|
112
|
+
newBlock.blocks = newBlock.blocks.map((childBlock: any) =>
|
|
113
|
+
replaceBlockValues(childBlock, productData, productIndex)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return newBlock;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Prices resolve through the pair reader rather than the literal path:
|
|
120
|
+
// v2 collection items publish them under `active_price`, PDP-shaped
|
|
121
|
+
// payloads at the top level, and a composition bound against the one the
|
|
122
|
+
// brand's endpoint doesn't ship would otherwise render an empty block
|
|
123
|
+
// (imports blank a bound block's placeholder value on purpose).
|
|
124
|
+
if (isPriceBinding(String(newBlock.properties.dataBinding))) {
|
|
125
|
+
const { current, retail } = readProductPricePair(productData);
|
|
126
|
+
const isRetail =
|
|
127
|
+
bindingPath === 'retail_price' ||
|
|
128
|
+
bindingPath === 'active_price.retail_price';
|
|
129
|
+
const activePrice = (productData as any)?.active_price as
|
|
130
|
+
| Record<string, unknown>
|
|
131
|
+
| undefined;
|
|
132
|
+
const currency =
|
|
133
|
+
activePrice?.currency_type ??
|
|
134
|
+
(productData as any)?.currency_type ??
|
|
135
|
+
(productData as any)?.currency;
|
|
136
|
+
|
|
137
|
+
if (isRetail) {
|
|
138
|
+
// A struck-through retail price only means something when it is
|
|
139
|
+
// higher than what the shopper pays; the same figure twice reads as
|
|
140
|
+
// a rendering bug, so non-discounted products hide it.
|
|
141
|
+
if (retail > 0 && current > 0 && retail > current) {
|
|
142
|
+
newBlock.value = getPriceWithCurrency(retail, currency);
|
|
143
|
+
} else {
|
|
144
|
+
newBlock.hidden = true;
|
|
145
|
+
}
|
|
146
|
+
} else if (current > 0) {
|
|
147
|
+
newBlock.value = getPriceWithCurrency(current, currency);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (newBlock.blocks && newBlock.blocks.length > 0) {
|
|
151
|
+
newBlock.blocks = newBlock.blocks.map((childBlock: any) =>
|
|
152
|
+
replaceBlockValues(childBlock, productData, productIndex)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return newBlock;
|
|
156
|
+
}
|
|
157
|
+
|
|
81
158
|
const resolvedValue = getNestedValue(productData, bindingPath);
|
|
82
159
|
|
|
83
160
|
if (resolvedValue !== undefined) {
|
|
@@ -92,6 +169,11 @@ const replaceBlockValues = (
|
|
|
92
169
|
? String(resolvedValue)
|
|
93
170
|
: resolvedValue;
|
|
94
171
|
}
|
|
172
|
+
} else if (newBlock.type === 'image') {
|
|
173
|
+
// Real data arrived but this binding has no value on it (e.g. a swatch
|
|
174
|
+
// field the collection endpoint doesn't ship). A broken/placeholder
|
|
175
|
+
// image is worse than no image, so the clone hides it.
|
|
176
|
+
newBlock.hidden = true;
|
|
95
177
|
}
|
|
96
178
|
} else if (isPlaceholderMode && newBlock.type === 'image') {
|
|
97
179
|
const placeholderImages = [
|
|
@@ -383,6 +465,49 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
383
465
|
const useInfinite = infinite && hasMultipleSlides;
|
|
384
466
|
const useAutoPlay = autoPlay && hasMultipleSlides;
|
|
385
467
|
|
|
468
|
+
// 'overlay' keeps the library's centred side arrows; 'top-right' moves
|
|
469
|
+
// navigation into a header row above the track (with an optional inline
|
|
470
|
+
// title), the pattern fashion sliders use. Composition-driven via section
|
|
471
|
+
// properties, so a brand opts in per section without a renderer fork.
|
|
472
|
+
const arrowPosition = getResponsiveValue(
|
|
473
|
+
getConfigEntry('arrow-position'),
|
|
474
|
+
currentBreakpoint,
|
|
475
|
+
'overlay'
|
|
476
|
+
);
|
|
477
|
+
const sliderTitle =
|
|
478
|
+
typeof section.properties?.['slider-title'] === 'string'
|
|
479
|
+
? section.properties['slider-title']
|
|
480
|
+
: '';
|
|
481
|
+
const useTopNav = showArrows && arrowPosition === 'top-right';
|
|
482
|
+
const carouselRef = React.useRef<Carousel | null>(null);
|
|
483
|
+
|
|
484
|
+
const topNavChevron = (direction: 'left' | 'right') => (
|
|
485
|
+
<svg
|
|
486
|
+
width="8"
|
|
487
|
+
height="12"
|
|
488
|
+
viewBox="0 0 8 12"
|
|
489
|
+
fill="none"
|
|
490
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
491
|
+
style={direction === 'right' ? { transform: 'scaleX(-1)' } : undefined}
|
|
492
|
+
>
|
|
493
|
+
<path d="M6.5 1L1.5 6l5 5" stroke="currentColor" strokeWidth="1.2" />
|
|
494
|
+
</svg>
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const topNavButtonStyle: React.CSSProperties = {
|
|
498
|
+
width: '32px',
|
|
499
|
+
height: '32px',
|
|
500
|
+
display: 'flex',
|
|
501
|
+
alignItems: 'center',
|
|
502
|
+
justifyContent: 'center',
|
|
503
|
+
background: 'transparent',
|
|
504
|
+
border: 'none',
|
|
505
|
+
borderRadius: '2px',
|
|
506
|
+
color: '#000000',
|
|
507
|
+
cursor: 'pointer',
|
|
508
|
+
padding: 0
|
|
509
|
+
};
|
|
510
|
+
|
|
386
511
|
const postBlockAction = (
|
|
387
512
|
type: PreviewToEditorMessageType,
|
|
388
513
|
blockId: string,
|
|
@@ -418,13 +543,57 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
418
543
|
)}
|
|
419
544
|
style={sectionStyles}
|
|
420
545
|
>
|
|
546
|
+
{useTopNav && (
|
|
547
|
+
<div
|
|
548
|
+
style={{
|
|
549
|
+
display: 'flex',
|
|
550
|
+
alignItems: 'center',
|
|
551
|
+
justifyContent: sliderTitle ? 'space-between' : 'flex-end',
|
|
552
|
+
padding: '16px 14px 16px 24px'
|
|
553
|
+
}}
|
|
554
|
+
>
|
|
555
|
+
{sliderTitle && (
|
|
556
|
+
<p
|
|
557
|
+
style={{
|
|
558
|
+
margin: 0,
|
|
559
|
+
fontSize: '14px',
|
|
560
|
+
fontWeight: 400,
|
|
561
|
+
lineHeight: 1.4,
|
|
562
|
+
color: '#000000',
|
|
563
|
+
textTransform: 'uppercase'
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
566
|
+
{sliderTitle}
|
|
567
|
+
</p>
|
|
568
|
+
)}
|
|
569
|
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
570
|
+
<button
|
|
571
|
+
type="button"
|
|
572
|
+
aria-label="Previous"
|
|
573
|
+
style={topNavButtonStyle}
|
|
574
|
+
onClick={() => carouselRef.current?.previous(currentSlidesToSlide)}
|
|
575
|
+
>
|
|
576
|
+
{topNavChevron('left')}
|
|
577
|
+
</button>
|
|
578
|
+
<button
|
|
579
|
+
type="button"
|
|
580
|
+
aria-label="Next"
|
|
581
|
+
style={topNavButtonStyle}
|
|
582
|
+
onClick={() => carouselRef.current?.next(currentSlidesToSlide)}
|
|
583
|
+
>
|
|
584
|
+
{topNavChevron('right')}
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
421
589
|
<Carousel
|
|
422
590
|
key={`${desktopItems}-${tabletItems}-${mobileItems}-${desktopSlides}-${tabletSlides}-${mobileSlides}-${visibleCount}-${currentBreakpoint}-${useInfinite}-${useAutoPlay}-${autoPlaySpeed}`}
|
|
591
|
+
ref={carouselRef}
|
|
423
592
|
responsive={carouselResponsive}
|
|
424
593
|
{...(isDesigner ? { slidesToSlide: currentSlidesToSlide } : {})}
|
|
425
594
|
swipeable={true}
|
|
426
595
|
draggable={true}
|
|
427
|
-
arrows={showArrows}
|
|
596
|
+
arrows={showArrows && !useTopNav}
|
|
428
597
|
showDots={showDots}
|
|
429
598
|
renderDotsOutside={showDots}
|
|
430
599
|
infinite={useInfinite}
|
|
@@ -457,7 +457,10 @@ const normalizeCurrencyLabel = (rawCurrency: unknown): string => {
|
|
|
457
457
|
return normalized;
|
|
458
458
|
};
|
|
459
459
|
|
|
460
|
-
const getPriceWithCurrency = (
|
|
460
|
+
export const getPriceWithCurrency = (
|
|
461
|
+
value: unknown,
|
|
462
|
+
currency: unknown
|
|
463
|
+
): string => {
|
|
461
464
|
const amount = formatDisplayPrice(value);
|
|
462
465
|
const currencyLabel = normalizeCurrencyLabel(currency);
|
|
463
466
|
|
|
@@ -499,10 +502,15 @@ const buildProductPriceHtml = (
|
|
|
499
502
|
return currentText;
|
|
500
503
|
};
|
|
501
504
|
|
|
502
|
-
const isPriceBinding = (dataBinding: string): boolean => {
|
|
505
|
+
export const isPriceBinding = (dataBinding: string): boolean => {
|
|
503
506
|
const path = dataBinding.replace('item.', '');
|
|
504
507
|
return (
|
|
505
|
-
path === 'active_price.price' ||
|
|
508
|
+
path === 'active_price.price' ||
|
|
509
|
+
path === 'price' ||
|
|
510
|
+
path === 'retail_price' ||
|
|
511
|
+
// Retail price travels under active_price on v2 payloads; leaving it out
|
|
512
|
+
// printed "3999.00" beside a formatted "2.999 TL" in the same row.
|
|
513
|
+
path === 'active_price.retail_price'
|
|
506
514
|
);
|
|
507
515
|
};
|
|
508
516
|
|