@akinon/pz-theme 2.0.81-rc.0 → 2.0.81
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 +7 -33
- package/package.json +4 -4
- package/src/__tests__/account-slot-coverage.test.ts +2 -1
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +87 -1
- package/src/blocks/product-recommendations-block.tsx +171 -11
- package/src/blocks/slider-block.tsx +27 -6
- package/src/chrome/placeholder-slugs.ts +1 -0
- package/src/chrome/route-slug.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.81
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Updated dependencies [51ea06888]
|
|
11
|
-
- Updated dependencies
|
|
12
|
-
- Updated dependencies [2fea41430]
|
|
13
|
-
- Updated dependencies [b55acb768]
|
|
14
|
-
- Updated dependencies [760258c1c]
|
|
15
|
-
- Updated dependencies [143be2b9d]
|
|
16
|
-
- Updated dependencies [7889b08fe]
|
|
17
|
-
- Updated dependencies [9f8cd3bc5]
|
|
18
|
-
- Updated dependencies [bfafa3f49]
|
|
19
|
-
- Updated dependencies [57d7eb305]
|
|
20
|
-
- Updated dependencies [d99a6a7d5]
|
|
21
|
-
- Updated dependencies [9db81a714]
|
|
22
|
-
- Updated dependencies [591e345e1]
|
|
23
|
-
- Updated dependencies [4de5303c5]
|
|
24
|
-
- Updated dependencies [95b139dc1]
|
|
25
|
-
- Updated dependencies [1d00f2d06]
|
|
26
|
-
- Updated dependencies [4ac7b2a1e]
|
|
27
|
-
- Updated dependencies [e9598c71d]
|
|
28
|
-
- Updated dependencies [4998a9631]
|
|
29
|
-
- Updated dependencies [804d2bd6c]
|
|
30
|
-
- Updated dependencies [3909d3224]
|
|
31
|
-
- Updated dependencies [6a3d8a631]
|
|
32
|
-
- Updated dependencies [e18836b20]
|
|
33
|
-
- Updated dependencies [b1111d7b]
|
|
34
|
-
- @akinon/next@2.0.81-rc.0
|
|
35
|
-
- @akinon/theme-contract@2.0.81-rc.0
|
|
3
|
+
## 2.0.81
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ab131f37: ZERO-4835: add loyalty slug, favourites source and slider peek
|
|
8
|
+
- @akinon/next@2.0.81
|
|
9
|
+
- @akinon/theme-contract@2.0.81
|
|
36
10
|
|
|
37
11
|
## 2.0.80
|
|
38
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.81
|
|
4
|
+
"version": "2.0.81",
|
|
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.81
|
|
11
|
+
"@akinon/next": "2.0.81",
|
|
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.81
|
|
16
|
+
"@akinon/theme-contract": "2.0.81",
|
|
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.81
|
|
23
|
+
"@akinon/next": "2.0.81",
|
|
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",
|
|
@@ -42,7 +42,8 @@ const EXPECTED_MOUNTS: Record<string, string> = {
|
|
|
42
42
|
// that is the cue to mount the slot and shrink this list.
|
|
43
43
|
const UNMOUNTED_ACCOUNT_KEYS: Record<string, string> = {
|
|
44
44
|
accountNotifications: 'notification-settings',
|
|
45
|
-
accountStockAlerts: 'stock-waiting-products'
|
|
45
|
+
accountStockAlerts: 'stock-waiting-products',
|
|
46
|
+
accountLoyalty: 'loyalty'
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
const getMountableAccountKeys = () =>
|
|
@@ -17,6 +17,7 @@ let mockWidget: Record<string, unknown> | undefined;
|
|
|
17
17
|
let mockDetail: Record<string, unknown> | undefined;
|
|
18
18
|
/** Every payload that reached the basket endpoint, in order. */
|
|
19
19
|
const mockAdds: Record<string, unknown>[] = [];
|
|
20
|
+
let mockFavourites: Record<string, unknown> | undefined;
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Every `@akinon/next/data/client/*` subpath is folded onto one mock module by
|
|
@@ -44,7 +45,12 @@ function mockDataClient() {
|
|
|
44
45
|
return { unwrap: () => Promise.resolve({}) };
|
|
45
46
|
},
|
|
46
47
|
{ isLoading: false }
|
|
47
|
-
]
|
|
48
|
+
],
|
|
49
|
+
useGetFavoritesQuery: (_args: unknown, options?: { skip?: boolean }) => ({
|
|
50
|
+
data: options?.skip ? undefined : mockFavourites,
|
|
51
|
+
isLoading: false,
|
|
52
|
+
isFetching: false
|
|
53
|
+
})
|
|
48
54
|
};
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -869,3 +875,83 @@ describe('section source', () => {
|
|
|
869
875
|
expect(container.textContent).toContain('Source panel');
|
|
870
876
|
});
|
|
871
877
|
});
|
|
878
|
+
|
|
879
|
+
describe('struck list price and discount badge', () => {
|
|
880
|
+
const DISCOUNTED = {
|
|
881
|
+
pk: 91,
|
|
882
|
+
name: 'Polo',
|
|
883
|
+
absolute_url: '/polo',
|
|
884
|
+
price: '2999',
|
|
885
|
+
retail_price: '3999',
|
|
886
|
+
discount_ratio: 25.01,
|
|
887
|
+
currency_type: 'TRY'
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
afterEach(() => {
|
|
891
|
+
mockWidget = undefined;
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('keeps the single price span while both toggles are off', () => {
|
|
895
|
+
mockWidget = { products: [DISCOUNTED] };
|
|
896
|
+
const { container } = renderBlock(
|
|
897
|
+
<ProductRecommendationsBlock {...props({ collectionId: 'c1' })} />
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
expect(container.querySelector('[data-product-card-price]')).toBeTruthy();
|
|
901
|
+
expect(container.querySelector('[data-product-card-price-row]')).toBeNull();
|
|
902
|
+
expect(
|
|
903
|
+
container.querySelector('[data-product-card-retail-price]')
|
|
904
|
+
).toBeNull();
|
|
905
|
+
expect(container.querySelector('[data-product-card-badge]')).toBeNull();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('draws the struck price, the discounted state and the -%N chip', () => {
|
|
909
|
+
mockWidget = { products: [DISCOUNTED] };
|
|
910
|
+
const { container } = renderBlock(
|
|
911
|
+
<ProductRecommendationsBlock
|
|
912
|
+
{...props({
|
|
913
|
+
collectionId: 'c1',
|
|
914
|
+
showRetailPrice: true,
|
|
915
|
+
showDiscountBadge: true
|
|
916
|
+
})}
|
|
917
|
+
/>
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
const retail = container.querySelector(
|
|
921
|
+
'[data-product-card-retail-price]'
|
|
922
|
+
) as HTMLElement;
|
|
923
|
+
const price = container.querySelector(
|
|
924
|
+
'[data-product-card-price]'
|
|
925
|
+
) as HTMLElement;
|
|
926
|
+
const badge = container.querySelector(
|
|
927
|
+
'[data-product-card-badge]'
|
|
928
|
+
) as HTMLElement;
|
|
929
|
+
|
|
930
|
+
expect(retail.tagName).toBe('S');
|
|
931
|
+
expect(retail.textContent).toContain('3,999');
|
|
932
|
+
expect(price.getAttribute('data-state')).toContain('discounted');
|
|
933
|
+
expect(badge.textContent).toBe('-%25');
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
it('hides the struck price and the chip for a product without a discount', () => {
|
|
937
|
+
mockWidget = { products: [CARD] };
|
|
938
|
+
const { container } = renderBlock(
|
|
939
|
+
<ProductRecommendationsBlock
|
|
940
|
+
{...props({
|
|
941
|
+
collectionId: 'c1',
|
|
942
|
+
showRetailPrice: true,
|
|
943
|
+
showDiscountBadge: true
|
|
944
|
+
})}
|
|
945
|
+
/>
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
expect(
|
|
949
|
+
container.querySelector('[data-product-card-retail-price]')
|
|
950
|
+
).toBeNull();
|
|
951
|
+
expect(container.querySelector('[data-product-card-badge]')).toBeNull();
|
|
952
|
+
const price = container.querySelector(
|
|
953
|
+
'[data-product-card-price]'
|
|
954
|
+
) as HTMLElement;
|
|
955
|
+
expect(price.getAttribute('data-state') || '').not.toContain('discounted');
|
|
956
|
+
});
|
|
957
|
+
});
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useRef } from 'react';
|
|
4
4
|
import NextLink from 'next/link';
|
|
5
|
+
import { useSession } from 'next-auth/react';
|
|
5
6
|
import { useGetWidgetQuery } from '@akinon/next/data/client/misc';
|
|
7
|
+
import { useGetFavoritesQuery } from '@akinon/next/data/client/wishlist';
|
|
6
8
|
import {
|
|
7
9
|
useAddProductMutation,
|
|
8
10
|
useGetProductByPkQuery
|
|
@@ -21,6 +23,7 @@ import { DESIGNER_SAMPLE_PRODUCT } from './item-template-utils';
|
|
|
21
23
|
import {
|
|
22
24
|
formatListingPrice,
|
|
23
25
|
listingPlaceholderStyle,
|
|
26
|
+
normalizeListingProduct,
|
|
24
27
|
readToggle
|
|
25
28
|
} from './listing-context';
|
|
26
29
|
import {
|
|
@@ -60,7 +63,7 @@ const ALLOWED_DECIMALS = ['always', 'auto', 'never'];
|
|
|
60
63
|
const ALLOWED_CURRENCY_DISPLAYS = ['', 'symbol', 'code', 'custom', 'none'];
|
|
61
64
|
const ALLOWED_CURRENCY_POSITIONS = ['', 'before', 'after'];
|
|
62
65
|
/** `collection` is the source the block has always had, so it is the default. */
|
|
63
|
-
const ALLOWED_SOURCES = ['collection', 'recently-viewed', 'section'];
|
|
66
|
+
const ALLOWED_SOURCES = ['collection', 'recently-viewed', 'section', 'favourites'];
|
|
64
67
|
const ALLOWED_QUICK_VIEW_ICONS = ['eye', 'plus', 'none'];
|
|
65
68
|
|
|
66
69
|
const DEFAULT_QUICK_VIEW_FEEDBACK = 2000;
|
|
@@ -207,6 +210,9 @@ interface RecommendationCard {
|
|
|
207
210
|
url: string;
|
|
208
211
|
image: string;
|
|
209
212
|
price: string;
|
|
213
|
+
retailPrice?: string;
|
|
214
|
+
hasDiscount?: boolean;
|
|
215
|
+
discountPercent?: number;
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
/**
|
|
@@ -312,6 +318,7 @@ const ProductRecommendationsBlock = ({
|
|
|
312
318
|
// collection picked in the editor's Source panel), so the strip needs no
|
|
313
319
|
// collection id of its own.
|
|
314
320
|
const isSectionSource = source === 'section';
|
|
321
|
+
const isFavourites = source === 'favourites';
|
|
315
322
|
const sectionData = useThemeSectionData();
|
|
316
323
|
|
|
317
324
|
const title = String(
|
|
@@ -362,6 +369,16 @@ const ProductRecommendationsBlock = ({
|
|
|
362
369
|
currentBreakpoint,
|
|
363
370
|
true
|
|
364
371
|
);
|
|
372
|
+
const showRetailPrice = readToggle(
|
|
373
|
+
block.properties?.showRetailPrice,
|
|
374
|
+
currentBreakpoint,
|
|
375
|
+
false
|
|
376
|
+
);
|
|
377
|
+
const showDiscountBadge = readToggle(
|
|
378
|
+
block.properties?.showDiscountBadge,
|
|
379
|
+
currentBreakpoint,
|
|
380
|
+
false
|
|
381
|
+
);
|
|
365
382
|
const imageRatioValue = prop('imageRatio', '');
|
|
366
383
|
const imageRatio = ALLOWED_IMAGE_RATIOS.includes(imageRatioValue)
|
|
367
384
|
? imageRatioValue
|
|
@@ -514,9 +531,15 @@ const ProductRecommendationsBlock = ({
|
|
|
514
531
|
// is what prevents a request before the author has named a collection — and
|
|
515
532
|
// now also a request for a collection the recently-viewed source never uses.
|
|
516
533
|
const { data, isFetching } = useGetWidgetQuery(collectionId, {
|
|
517
|
-
skip: !collectionId || isRecentlyViewed || isSectionSource
|
|
534
|
+
skip: !collectionId || isRecentlyViewed || isSectionSource || isFavourites
|
|
518
535
|
});
|
|
519
536
|
|
|
537
|
+
const { status: sessionStatus } = useSession();
|
|
538
|
+
const { data: favouritesData } = useGetFavoritesQuery(
|
|
539
|
+
{ limit: 100, page: 1 },
|
|
540
|
+
{ skip: !isFavourites || sessionStatus !== 'authenticated' }
|
|
541
|
+
);
|
|
542
|
+
|
|
520
543
|
// The arrows report where the track stands: the previous arrow is disabled
|
|
521
544
|
// at the start and the next one at the end, re-read on scroll and resize.
|
|
522
545
|
// In the designer both stay enabled so the author can style them.
|
|
@@ -539,7 +562,7 @@ const ProductRecommendationsBlock = ({
|
|
|
539
562
|
track.removeEventListener('scroll', update);
|
|
540
563
|
window.removeEventListener('resize', update);
|
|
541
564
|
};
|
|
542
|
-
}, [showArrows, isDesigner, data, recentEntries, sectionData]);
|
|
565
|
+
}, [showArrows, isDesigner, data, recentEntries, sectionData, favouritesData]);
|
|
543
566
|
|
|
544
567
|
/**
|
|
545
568
|
* The overlay's own request. A row carries a summary only, so the variant
|
|
@@ -674,6 +697,35 @@ const ProductRecommendationsBlock = ({
|
|
|
674
697
|
[]
|
|
675
698
|
);
|
|
676
699
|
|
|
700
|
+
const discountFields = (
|
|
701
|
+
priceRaw: unknown,
|
|
702
|
+
retailRaw: unknown,
|
|
703
|
+
ratioRaw: unknown,
|
|
704
|
+
currency: string
|
|
705
|
+
): Pick<
|
|
706
|
+
RecommendationCard,
|
|
707
|
+
'hasDiscount' | 'retailPrice' | 'discountPercent'
|
|
708
|
+
> => {
|
|
709
|
+
const priceValue = parsePriceValue(priceRaw);
|
|
710
|
+
const retailValue = parsePriceValue(retailRaw);
|
|
711
|
+
const hasDiscount =
|
|
712
|
+
priceValue !== null && retailValue !== null && retailValue > priceValue;
|
|
713
|
+
|
|
714
|
+
if (!hasDiscount) return { hasDiscount: false };
|
|
715
|
+
|
|
716
|
+
const ratio = Number(ratioRaw);
|
|
717
|
+
const discountPercent =
|
|
718
|
+
Number.isFinite(ratio) && ratio > 0
|
|
719
|
+
? Math.round(ratio)
|
|
720
|
+
: Math.round((1 - (priceValue as number) / (retailValue as number)) * 100);
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
hasDiscount: true,
|
|
724
|
+
retailPrice: formatListingPrice(retailRaw, currency, locale, money),
|
|
725
|
+
discountPercent
|
|
726
|
+
};
|
|
727
|
+
};
|
|
728
|
+
|
|
677
729
|
const collectionProducts: RecommendationCard[] = (
|
|
678
730
|
(isSectionSource
|
|
679
731
|
? sectionData?.items || []
|
|
@@ -714,6 +766,12 @@ const ProductRecommendationsBlock = ({
|
|
|
714
766
|
currency,
|
|
715
767
|
locale,
|
|
716
768
|
money
|
|
769
|
+
),
|
|
770
|
+
...discountFields(
|
|
771
|
+
entry.price ?? activePrice.price,
|
|
772
|
+
entry.retail_price ?? activePrice.retail_price,
|
|
773
|
+
entry.discount_ratio,
|
|
774
|
+
currency
|
|
717
775
|
)
|
|
718
776
|
};
|
|
719
777
|
})
|
|
@@ -744,8 +802,64 @@ const ProductRecommendationsBlock = ({
|
|
|
744
802
|
)
|
|
745
803
|
}));
|
|
746
804
|
|
|
805
|
+
const favouriteCards: RecommendationCard[] = (
|
|
806
|
+
(favouritesData as
|
|
807
|
+
| {
|
|
808
|
+
results?: Array<{
|
|
809
|
+
pk?: number;
|
|
810
|
+
product?: Record<string, unknown> | null;
|
|
811
|
+
}>;
|
|
812
|
+
}
|
|
813
|
+
| undefined)?.results ?? []
|
|
814
|
+
)
|
|
815
|
+
.filter((favourite) => Boolean(favourite.product))
|
|
816
|
+
.slice(0, limit)
|
|
817
|
+
.map((favourite, index) => {
|
|
818
|
+
const product = favourite.product as Record<string, unknown>;
|
|
819
|
+
const normalized = normalizeListingProduct(product, index, {
|
|
820
|
+
locale,
|
|
821
|
+
currency: localeCurrency
|
|
822
|
+
});
|
|
823
|
+
const activePrice = (product.active_price ?? {}) as Record<
|
|
824
|
+
string,
|
|
825
|
+
unknown
|
|
826
|
+
>;
|
|
827
|
+
const currency =
|
|
828
|
+
toText(product.currency_type) ||
|
|
829
|
+
toText(activePrice.currency_type) ||
|
|
830
|
+
localeCurrency ||
|
|
831
|
+
'';
|
|
832
|
+
const url =
|
|
833
|
+
normalized?.url && normalized.url !== '#'
|
|
834
|
+
? normalized.url
|
|
835
|
+
: toText(product.absolute_url);
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
...(normalized ?? {}),
|
|
839
|
+
key: toText(favourite.pk ?? product.pk) || `favourite-${index}`,
|
|
840
|
+
pk: toProductPk(product.pk),
|
|
841
|
+
name: normalized?.name || toText(product.name),
|
|
842
|
+
url: url || '#',
|
|
843
|
+
image: normalized?.image || normalized?.images?.[0] || '',
|
|
844
|
+
price: formatListingPrice(
|
|
845
|
+
product.price ?? activePrice.price,
|
|
846
|
+
currency,
|
|
847
|
+
locale,
|
|
848
|
+
money
|
|
849
|
+
),
|
|
850
|
+
...discountFields(
|
|
851
|
+
product.price ?? activePrice.price,
|
|
852
|
+
product.retail_price ?? activePrice.retail_price,
|
|
853
|
+
product.discount_ratio,
|
|
854
|
+
currency
|
|
855
|
+
)
|
|
856
|
+
};
|
|
857
|
+
});
|
|
858
|
+
|
|
747
859
|
const products = isRecentlyViewed
|
|
748
860
|
? recentlyViewedProducts
|
|
861
|
+
: isFavourites
|
|
862
|
+
? favouriteCards
|
|
749
863
|
: collectionProducts;
|
|
750
864
|
|
|
751
865
|
/**
|
|
@@ -757,7 +871,7 @@ const ProductRecommendationsBlock = ({
|
|
|
757
871
|
*/
|
|
758
872
|
const designerRows: RecommendationCard[] | null =
|
|
759
873
|
isDesigner &&
|
|
760
|
-
isRecentlyViewed &&
|
|
874
|
+
(isRecentlyViewed || isFavourites) &&
|
|
761
875
|
products.length === 0 &&
|
|
762
876
|
// An authored card layout already draws its own placeholder rows
|
|
763
877
|
// (`designerPlaceholderCount`), so adding built-in ones would double them.
|
|
@@ -765,8 +879,8 @@ const ProductRecommendationsBlock = ({
|
|
|
765
879
|
? Array.from(
|
|
766
880
|
{ length: Math.min(limit, columnsDesktop + 1) },
|
|
767
881
|
(_, index) => ({
|
|
768
|
-
key:
|
|
769
|
-
name: recentlyViewedEmptyLabel,
|
|
882
|
+
key: `${source}-placeholder-${index}`,
|
|
883
|
+
name: isFavourites ? 'Favourites' : recentlyViewedEmptyLabel,
|
|
770
884
|
url: '#',
|
|
771
885
|
image: '',
|
|
772
886
|
price: ''
|
|
@@ -1226,11 +1340,57 @@ const ProductRecommendationsBlock = ({
|
|
|
1226
1340
|
</span>
|
|
1227
1341
|
)}
|
|
1228
1342
|
|
|
1229
|
-
{showPrice &&
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1343
|
+
{showPrice &&
|
|
1344
|
+
product.price &&
|
|
1345
|
+
!(showRetailPrice || showDiscountBadge) && (
|
|
1346
|
+
<span data-product-card-price="" {...partAttrs('card-price')}>
|
|
1347
|
+
{product.price}
|
|
1348
|
+
</span>
|
|
1349
|
+
)}
|
|
1350
|
+
|
|
1351
|
+
{showPrice &&
|
|
1352
|
+
product.price &&
|
|
1353
|
+
(showRetailPrice || showDiscountBadge) && (
|
|
1354
|
+
<span
|
|
1355
|
+
data-product-card-price-row=""
|
|
1356
|
+
{...partAttrs('card-price-row')}
|
|
1357
|
+
style={{
|
|
1358
|
+
display: 'flex',
|
|
1359
|
+
alignItems: 'center',
|
|
1360
|
+
columnGap: '9px',
|
|
1361
|
+
flexWrap: 'wrap'
|
|
1362
|
+
}}
|
|
1363
|
+
>
|
|
1364
|
+
{showRetailPrice &&
|
|
1365
|
+
product.hasDiscount &&
|
|
1366
|
+
product.retailPrice && (
|
|
1367
|
+
<s
|
|
1368
|
+
data-product-card-retail-price=""
|
|
1369
|
+
{...partAttrs('card-retail-price')}
|
|
1370
|
+
>
|
|
1371
|
+
{product.retailPrice}
|
|
1372
|
+
</s>
|
|
1373
|
+
)}
|
|
1374
|
+
<span
|
|
1375
|
+
data-product-card-price=""
|
|
1376
|
+
{...partAttrs('card-price', {
|
|
1377
|
+
discounted: Boolean(product.hasDiscount)
|
|
1378
|
+
})}
|
|
1379
|
+
>
|
|
1380
|
+
{product.price}
|
|
1381
|
+
</span>
|
|
1382
|
+
{showDiscountBadge &&
|
|
1383
|
+
product.hasDiscount &&
|
|
1384
|
+
product.discountPercent !== undefined && (
|
|
1385
|
+
<span
|
|
1386
|
+
data-product-card-badge=""
|
|
1387
|
+
{...partAttrs('card-badge')}
|
|
1388
|
+
>
|
|
1389
|
+
{`-%${product.discountPercent}`}
|
|
1390
|
+
</span>
|
|
1391
|
+
)}
|
|
1392
|
+
</span>
|
|
1393
|
+
)}
|
|
1234
1394
|
</NextLink>
|
|
1235
1395
|
);
|
|
1236
1396
|
|
|
@@ -107,14 +107,24 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
107
107
|
const desktopSlides = clampNumber(slidesConfig.desktop, 1, 8);
|
|
108
108
|
const mobileSlides = clampNumber(slidesConfig.mobile, 1, 8);
|
|
109
109
|
|
|
110
|
+
const slidePeek = getConfigEntry('slide-peek');
|
|
111
|
+
const desktopPeek = parseNumber(
|
|
112
|
+
getResponsiveValue(slidePeek, 'desktop', 0),
|
|
113
|
+
0
|
|
114
|
+
);
|
|
115
|
+
const mobilePeek = parseNumber(getResponsiveValue(slidePeek, 'mobile', 0), 0);
|
|
116
|
+
const partialVisible = desktopPeek > 0 || mobilePeek > 0;
|
|
117
|
+
|
|
110
118
|
const carouselResponsive = {
|
|
111
119
|
desktop: {
|
|
112
120
|
breakpoint: { max: 4000, min: 768 },
|
|
113
|
-
items: desktopItems
|
|
121
|
+
items: desktopItems,
|
|
122
|
+
...(desktopPeek > 0 ? { partialVisibilityGutter: desktopPeek } : {})
|
|
114
123
|
},
|
|
115
124
|
mobile: {
|
|
116
125
|
breakpoint: { max: 768, min: 0 },
|
|
117
|
-
items: mobileItems
|
|
126
|
+
items: mobileItems,
|
|
127
|
+
...(mobilePeek > 0 ? { partialVisibilityGutter: mobilePeek } : {})
|
|
118
128
|
}
|
|
119
129
|
};
|
|
120
130
|
|
|
@@ -473,6 +483,12 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
473
483
|
: undefined
|
|
474
484
|
);
|
|
475
485
|
|
|
486
|
+
const arrowsOutside =
|
|
487
|
+
leftArrowButtonStyle.top !== undefined ||
|
|
488
|
+
leftArrowButtonStyle.bottom !== undefined ||
|
|
489
|
+
rightArrowButtonStyle.top !== undefined ||
|
|
490
|
+
rightArrowButtonStyle.bottom !== undefined;
|
|
491
|
+
|
|
476
492
|
const handleLeftArrowClick = (e: React.MouseEvent) => {
|
|
477
493
|
e.preventDefault();
|
|
478
494
|
e.stopPropagation();
|
|
@@ -590,10 +606,11 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
590
606
|
ref={carouselRef}
|
|
591
607
|
key={`${desktopItems}-${mobileItems}-${desktopSlides}-${mobileSlides}-${visibleCount}-${useInfinite}-${useAutoPlay}-${autoPlaySpeed}`}
|
|
592
608
|
responsive={carouselResponsive}
|
|
609
|
+
partialVisible={partialVisible}
|
|
593
610
|
slidesToSlide={currentSlidesToSlide}
|
|
594
611
|
swipeable={true}
|
|
595
612
|
draggable={true}
|
|
596
|
-
arrows={isDesigner ? false : Boolean(showArrows)}
|
|
613
|
+
arrows={isDesigner ? false : Boolean(showArrows) && !arrowsOutside}
|
|
597
614
|
showDots={Boolean(showDots)}
|
|
598
615
|
renderDotsOutside={Boolean(showDots)}
|
|
599
616
|
infinite={Boolean(useInfinite)}
|
|
@@ -604,10 +621,14 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
604
621
|
containerClass="slider-carousel"
|
|
605
622
|
dotListClass="slider-dots"
|
|
606
623
|
customLeftArrow={
|
|
607
|
-
!isDesigner && showArrows
|
|
624
|
+
!isDesigner && showArrows && !arrowsOutside ? (
|
|
625
|
+
<CustomLeftArrow />
|
|
626
|
+
) : undefined
|
|
608
627
|
}
|
|
609
628
|
customRightArrow={
|
|
610
|
-
!isDesigner && showArrows
|
|
629
|
+
!isDesigner && showArrows && !arrowsOutside ? (
|
|
630
|
+
<CustomRightArrow />
|
|
631
|
+
) : undefined
|
|
611
632
|
}
|
|
612
633
|
>
|
|
613
634
|
{sortedBlocks.map((childBlock, index) => {
|
|
@@ -652,7 +673,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
652
673
|
})}
|
|
653
674
|
</Carousel>
|
|
654
675
|
|
|
655
|
-
{isDesigner &&
|
|
676
|
+
{(isDesigner || arrowsOutside) &&
|
|
656
677
|
showArrows &&
|
|
657
678
|
sortedBlocks.length > 0 &&
|
|
658
679
|
leftArrowIconBlock &&
|
|
@@ -34,6 +34,7 @@ export const PLACEHOLDER_SLUGS = {
|
|
|
34
34
|
accountChangePassword: 'account-change-password-body',
|
|
35
35
|
accountChangeEmail: 'account-change-email-body',
|
|
36
36
|
accountCoupons: 'account-coupons-body',
|
|
37
|
+
accountLoyalty: 'account-loyalty-body',
|
|
37
38
|
accountFavourites: 'account-favourites-body',
|
|
38
39
|
accountFaq: 'account-faq-content',
|
|
39
40
|
accountContact: 'account-contact-body',
|
package/src/chrome/route-slug.ts
CHANGED
|
@@ -35,6 +35,7 @@ const DEFAULT_RULES: RouteSlugRule[] = [
|
|
|
35
35
|
[/^\/account\/change-password(\/|$)/, PLACEHOLDER_SLUGS.accountChangePassword],
|
|
36
36
|
[/^\/account\/change-email(\/|$)/, PLACEHOLDER_SLUGS.accountChangeEmail],
|
|
37
37
|
[/^\/account\/coupons(\/|$)/, PLACEHOLDER_SLUGS.accountCoupons],
|
|
38
|
+
[/^\/account\/loyalty(\/|$)/, PLACEHOLDER_SLUGS.accountLoyalty],
|
|
38
39
|
[
|
|
39
40
|
/^\/account\/(favourite-products|favorite-products|favourites|favorites|wishlist)(\/|$)/,
|
|
40
41
|
PLACEHOLDER_SLUGS.accountFavourites
|
|
@@ -127,6 +128,7 @@ const PAGE_MOUNTED_SLUGS = new Set<string>([
|
|
|
127
128
|
PLACEHOLDER_SLUGS.accountChangePassword,
|
|
128
129
|
PLACEHOLDER_SLUGS.accountChangeEmail,
|
|
129
130
|
PLACEHOLDER_SLUGS.accountCoupons,
|
|
131
|
+
PLACEHOLDER_SLUGS.accountLoyalty,
|
|
130
132
|
PLACEHOLDER_SLUGS.accountFavourites,
|
|
131
133
|
PLACEHOLDER_SLUGS.accountStockAlerts,
|
|
132
134
|
PLACEHOLDER_SLUGS.accountNotifications,
|