@akinon/projectzero 2.0.0-beta.20 → 2.0.0-beta.22

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/app-template/CHANGELOG.md +170 -0
  3. package/app-template/next.config.mjs +0 -1
  4. package/app-template/package.json +31 -30
  5. package/app-template/src/app/[pz]/[...prettyurl]/page.tsx +2 -2
  6. package/app-template/src/app/[pz]/account/layout.tsx +2 -1
  7. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/blog/[slug]/page.tsx +4 -2
  8. package/app-template/src/app/[pz]/category/[pk]/page.tsx +11 -1
  9. package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +2 -2
  10. package/app-template/src/app/[pz]/layout.tsx +3 -1
  11. package/app-template/src/app/[pz]/list/page.tsx +11 -1
  12. package/app-template/src/app/[pz]/page.tsx +13 -35
  13. package/app-template/src/app/[pz]/pages/[slug]/page.tsx +19 -0
  14. package/app-template/src/app/[pz]/product/[pk]/page.tsx +2 -2
  15. package/app-template/src/app/api/barcode-search/route.ts +1 -1
  16. package/app-template/src/app/api/cache/route.ts +1 -1
  17. package/app-template/src/app/api/image-proxy/route.ts +1 -1
  18. package/app-template/src/app/api/logout/route.ts +1 -1
  19. package/app-template/src/app/api/product-categories/route.ts +1 -1
  20. package/app-template/src/app/api/similar-product-list/route.ts +1 -1
  21. package/app-template/src/app/api/similar-products/route.ts +1 -1
  22. package/app-template/src/app/api/virtual-try-on/route.ts +1 -1
  23. package/app-template/src/app/api/web-vitals/route.ts +1 -1
  24. package/app-template/src/components/quantity-selector.tsx +16 -4
  25. package/app-template/src/components/widget-content.tsx +3 -3
  26. package/app-template/src/routes/index.ts +6 -6
  27. package/app-template/src/utils/__tests__/theme-page-context.test.ts +145 -0
  28. package/app-template/src/utils/theme-page-context.ts +309 -0
  29. package/app-template/src/views/basket/basket-item.tsx +107 -691
  30. package/app-template/src/views/basket/index.ts +0 -2
  31. package/app-template/src/views/basket/summary.tsx +75 -496
  32. package/app-template/src/views/breadcrumb.tsx +38 -13
  33. package/app-template/src/views/category/category-header.tsx +66 -289
  34. package/app-template/src/views/category/category-info.tsx +24 -173
  35. package/app-template/src/views/category/filters/index.tsx +48 -208
  36. package/app-template/src/views/category/layout.tsx +5 -7
  37. package/app-template/src/views/checkout/index.tsx +0 -5
  38. package/app-template/src/views/checkout/steps/payment/index.tsx +2 -5
  39. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +1 -72
  40. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +40 -171
  41. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +12 -74
  42. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +45 -128
  43. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +27 -232
  44. package/app-template/src/views/checkout/summary.tsx +29 -303
  45. package/app-template/src/views/footer.tsx +13 -415
  46. package/app-template/src/views/guest-login/index.tsx +1 -1
  47. package/app-template/src/views/header/action-menu.tsx +45 -277
  48. package/app-template/src/views/header/band.tsx +21 -6
  49. package/app-template/src/views/header/index.tsx +47 -109
  50. package/app-template/src/views/header/mini-basket.tsx +45 -820
  51. package/app-template/src/views/header/navbar.tsx +111 -178
  52. package/app-template/src/views/header/search/index.tsx +32 -71
  53. package/app-template/src/views/header/search/results.tsx +65 -127
  54. package/app-template/src/views/product/accordion-wrapper.tsx +43 -135
  55. package/app-template/src/views/product/index.ts +1 -1
  56. package/app-template/src/views/product/layout.tsx +7 -2
  57. package/app-template/src/views/product/misc-buttons.tsx +25 -339
  58. package/app-template/src/views/product/product-actions.tsx +8 -137
  59. package/app-template/src/views/product/product-info.tsx +31 -69
  60. package/app-template/src/views/product/product-share.tsx +8 -11
  61. package/app-template/src/views/product/slider.tsx +79 -117
  62. package/app-template/src/views/product-item/index.tsx +46 -119
  63. package/app-template/src/widgets/footer-social.tsx +16 -47
  64. package/app-template/src/widgets/footer-subscription/index.tsx +17 -183
  65. package/codemods/migrate-auth-v5/index.js +339 -0
  66. package/codemods/migrate-auth-v5/transform.js +86 -0
  67. package/dist/commands/plugins.js +23 -2
  68. package/package.json +1 -1
  69. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +0 -15
  70. package/app-template/src/views/basket/basket-summary-context.tsx +0 -560
  71. package/app-template/src/views/basket/designer-context.tsx +0 -617
  72. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +0 -190
  73. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +0 -286
  74. package/app-template/src/views/breadcrumb/constants.ts +0 -15
  75. package/app-template/src/views/breadcrumb/index.tsx +0 -127
  76. package/app-template/src/views/category/native-widget-context.tsx +0 -257
  77. package/app-template/src/views/category/product-list-registrar.tsx +0 -665
  78. package/app-template/src/views/checkout/checkout-address-registrar.tsx +0 -254
  79. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +0 -183
  80. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +0 -259
  81. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +0 -253
  82. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +0 -183
  83. package/app-template/src/views/checkout/constants.ts +0 -5
  84. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +0 -15
  85. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +0 -18
  86. package/app-template/src/views/footer/footer-app-banner-context.tsx +0 -326
  87. package/app-template/src/views/footer/footer-bottom-context.tsx +0 -215
  88. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +0 -74
  89. package/app-template/src/views/footer/footer-layout-constants.ts +0 -35
  90. package/app-template/src/views/footer/footer-layout-registrar.tsx +0 -342
  91. package/app-template/src/views/footer/footer-layout-switcher.tsx +0 -110
  92. package/app-template/src/views/footer/footer-menu-context.tsx +0 -211
  93. package/app-template/src/views/footer/footer-native-widgets.tsx +0 -60
  94. package/app-template/src/views/footer/footer-social-context.tsx +0 -254
  95. package/app-template/src/views/footer/footer-subscription-context.tsx +0 -210
  96. package/app-template/src/views/footer/footer-utils.ts +0 -43
  97. package/app-template/src/views/footer/footer-value-props-context.tsx +0 -326
  98. package/app-template/src/views/footer/logo-settings.ts +0 -183
  99. package/app-template/src/views/footer/native-widget-config.ts +0 -262
  100. package/app-template/src/views/footer/subscription-settings.ts +0 -122
  101. package/app-template/src/views/footer/use-footer-logo.ts +0 -162
  102. package/app-template/src/views/header/designer-context.tsx +0 -261
  103. package/app-template/src/views/header/header-announcement-registrar.tsx +0 -267
  104. package/app-template/src/views/header/header-client-wrapper.tsx +0 -496
  105. package/app-template/src/views/header/header-content.tsx +0 -1026
  106. package/app-template/src/views/header/header-currency-registrar.tsx +0 -348
  107. package/app-template/src/views/header/header-icons-context.tsx +0 -262
  108. package/app-template/src/views/header/header-language-registrar.tsx +0 -348
  109. package/app-template/src/views/header/header-layout-context.tsx +0 -143
  110. package/app-template/src/views/header/header-layout-registrar.tsx +0 -658
  111. package/app-template/src/views/header/header-logo-context.tsx +0 -228
  112. package/app-template/src/views/header/header-logo.tsx +0 -118
  113. package/app-template/src/views/header/header-mini-basket-context.tsx +0 -524
  114. package/app-template/src/views/header/header-search-registrar.tsx +0 -511
  115. package/app-template/src/views/header/header-text-slider-registrar.tsx +0 -382
  116. package/app-template/src/views/header/inline-search.tsx +0 -262
  117. package/app-template/src/views/header/navbar-menu-context.tsx +0 -219
  118. package/app-template/src/views/header/search/search-input.tsx +0 -61
  119. package/app-template/src/views/header/server-settings-parser.ts +0 -1105
  120. package/app-template/src/views/header/use-header-icons.ts +0 -241
  121. package/app-template/src/views/header/use-header-logo.ts +0 -213
  122. package/app-template/src/views/header/use-navbar-menu.ts +0 -179
  123. package/app-template/src/views/product/accordion-section.tsx +0 -61
  124. package/app-template/src/views/product/custom-button-group.tsx +0 -69
  125. package/app-template/src/views/product/favorites-button-section.tsx +0 -69
  126. package/app-template/src/views/product/find-in-store-section.tsx +0 -60
  127. package/app-template/src/views/product/product-info-section.tsx +0 -140
  128. package/app-template/src/views/product/quantity-section.tsx +0 -73
  129. package/app-template/src/views/product/sale-tag.tsx +0 -10
  130. package/app-template/src/views/product/share-section.tsx +0 -357
  131. package/app-template/src/views/product/variants-section.tsx +0 -126
  132. package/app-template/src/views/product-detail/constants.ts +0 -272
  133. package/app-template/src/views/product-detail/index.ts +0 -10
  134. package/app-template/src/views/product-detail/product-detail-registrar.tsx +0 -616
  135. package/app-template/src/widgets/footer-app-banner.tsx +0 -444
  136. package/app-template/src/widgets/footer-bottom.tsx +0 -127
  137. package/app-template/src/widgets/footer-menu-compact.tsx +0 -238
  138. package/app-template/src/widgets/footer-menu-two.tsx +0 -298
  139. package/app-template/src/widgets/footer-social-client.tsx +0 -251
  140. package/app-template/src/widgets/footer-value-props.tsx +0 -201
@@ -4,15 +4,8 @@ import {
4
4
  } from '@akinon/next/data/client/basket';
5
5
  import { useAppDispatch } from '@akinon/next/redux/hooks';
6
6
  import { BasketItem as BasketItemType } from '@akinon/next/types';
7
- import {
8
- Price,
9
- Button,
10
- Icon,
11
- LoaderSpinner,
12
- Modal,
13
- Link
14
- } from '@theme/components';
15
- import { ComponentProps, useState } from 'react';
7
+ import { Price, Button, Icon, Modal, Select, Link } from '@theme/components';
8
+ import { useState } from 'react';
16
9
  import { useAddFavoriteMutation } from '@akinon/next/data/client/wishlist';
17
10
  import {
18
11
  useCommonProductAttributes,
@@ -20,89 +13,14 @@ import {
20
13
  } from '@akinon/next/hooks';
21
14
  import PluginModule, { Component } from '@akinon/next/components/plugin-module';
22
15
  import { Image } from '@akinon/next/components/image';
16
+ import clsx from 'clsx';
23
17
  import { pushRemoveFromCart } from '@theme/utils/gtm';
24
- import { WithDesignerFeatures } from '@akinon/next/components/theme-editor/components/with-designer-features';
25
- import {
26
- BASKET_ITEM_BLOCKS,
27
- BASKET_ITEMS_SECTION_ID,
28
- BASKET_PLACEHOLDER_ID,
29
- useBasketDesigner
30
- } from './designer-context';
31
18
 
32
19
  interface Props {
33
20
  basketItem?: BasketItemType;
34
21
  namespace?: string;
35
22
  }
36
23
 
37
- const convertStylesToCSS = (
38
- styles: Record<string, unknown> | undefined
39
- ): React.CSSProperties => {
40
- if (!styles) return {};
41
-
42
- const cssStyles: React.CSSProperties = {};
43
-
44
- Object.entries(styles).forEach(([key, value]) => {
45
- const rawValue =
46
- typeof value === 'object' && value !== null && 'desktop' in value
47
- ? (value as Record<string, unknown>).desktop
48
- : value;
49
-
50
- const cssValue =
51
- typeof rawValue === 'number'
52
- ? rawValue
53
- : typeof rawValue === 'string'
54
- ? rawValue
55
- : '';
56
-
57
- if (cssValue !== '' && cssValue !== null && cssValue !== undefined) {
58
- const camelKey = key.replace(/-([a-z])/g, (_, letter) =>
59
- letter.toUpperCase()
60
- );
61
- (cssStyles as Record<string, unknown>)[camelKey] = cssValue;
62
- }
63
- });
64
-
65
- return cssStyles;
66
- };
67
-
68
- const pickStyles = (
69
- styles: React.CSSProperties,
70
- keys: Array<keyof React.CSSProperties>
71
- ): React.CSSProperties => {
72
- const picked: React.CSSProperties = {};
73
-
74
- keys.forEach((key) => {
75
- const value = styles[key];
76
-
77
- if (value !== undefined && value !== null && value !== '') {
78
- (picked as Record<string, unknown>)[key as string] = value;
79
- }
80
- });
81
-
82
- return picked;
83
- };
84
-
85
- const omitStyles = (
86
- styles: React.CSSProperties,
87
- keys: Array<keyof React.CSSProperties>
88
- ): React.CSSProperties => {
89
- const omitted = { ...styles };
90
-
91
- keys.forEach((key) => {
92
- delete omitted[key];
93
- });
94
-
95
- return omitted;
96
- };
97
-
98
- const getResponsiveValue = (value: unknown) => {
99
- if (typeof value === 'object' && value !== null && 'desktop' in value) {
100
- return (value as Record<string, unknown>).desktop;
101
- }
102
-
103
- return value;
104
- };
105
-
106
24
  export const BasketItem = (props: Props) => {
107
25
  const { t } = useLocalization();
108
26
  const { basketItem, namespace } = props;
@@ -111,228 +29,15 @@ export const BasketItem = (props: Props) => {
111
29
  const [isRemoveBasketModalOpen, setRemoveBasketModalOpen] = useState(false);
112
30
  const [addFavorite, { isLoading: addFavoriteLoading }] =
113
31
  useAddFavoriteMutation();
32
+ const [updateQuantityLoading, setUpdateQuantityLoading] = useState(false);
114
33
  const commonProductAttributes = useCommonProductAttributes({
115
34
  attributes: basketItem.product.attributes_kwargs
116
35
  });
117
- const [updateQuantityLoading, setUpdateQuantityLoading] = useState(false);
118
- const { isDesigner, selectedBlockId, getBlockStyles, getBlockProperties } =
119
- useBasketDesigner();
120
- const rowStyles = convertStylesToCSS(
121
- getBlockStyles(BASKET_ITEM_BLOCKS.ITEM_ROW.id)
122
- );
123
- const imageStyles = convertStylesToCSS(
124
- getBlockStyles(BASKET_ITEM_BLOCKS.IMAGE.id)
125
- );
126
- const nameStyles = convertStylesToCSS(
127
- getBlockStyles(BASKET_ITEM_BLOCKS.NAME.id)
128
- );
129
- const attributesStyles = convertStylesToCSS(
130
- getBlockStyles(BASKET_ITEM_BLOCKS.ATTRIBUTES.id)
131
- );
132
- const attributeLabelStyles = convertStylesToCSS(
133
- getBlockStyles(BASKET_ITEM_BLOCKS.ATTRIBUTE_LABEL.id)
134
- );
135
- const attributeValueStyles = convertStylesToCSS(
136
- getBlockStyles(BASKET_ITEM_BLOCKS.ATTRIBUTE_VALUE.id)
137
- );
138
- const quantityStyles = convertStylesToCSS(
139
- getBlockStyles(BASKET_ITEM_BLOCKS.QUANTITY.id)
140
- );
141
- const quantityWrapperStyles = convertStylesToCSS(
142
- getBlockStyles(BASKET_ITEM_BLOCKS.QUANTITY_WRAPPER.id)
143
- );
144
- const quantityMinusButtonStyles = convertStylesToCSS(
145
- getBlockStyles(BASKET_ITEM_BLOCKS.QUANTITY_MINUS_BUTTON.id)
146
- );
147
- const quantityMinusIconStyles = convertStylesToCSS(
148
- getBlockStyles(BASKET_ITEM_BLOCKS.QUANTITY_MINUS_ICON.id)
149
- );
150
- const quantityValueStyles = convertStylesToCSS(
151
- getBlockStyles(BASKET_ITEM_BLOCKS.QUANTITY_VALUE.id)
152
- );
153
- const quantityPlusButtonStyles = convertStylesToCSS(
154
- getBlockStyles(BASKET_ITEM_BLOCKS.QUANTITY_PLUS_BUTTON.id)
155
- );
156
- const quantityPlusIconStyles = convertStylesToCSS(
157
- getBlockStyles(BASKET_ITEM_BLOCKS.QUANTITY_PLUS_ICON.id)
158
- );
159
-
160
- const quantityMinusIconProps =
161
- getBlockProperties(BASKET_ITEM_BLOCKS.QUANTITY_MINUS_ICON.id) || {};
162
- const quantityPlusIconProps =
163
- getBlockProperties(BASKET_ITEM_BLOCKS.QUANTITY_PLUS_ICON.id) || {};
164
- const priceStyles = convertStylesToCSS(
165
- getBlockStyles(BASKET_ITEM_BLOCKS.PRICE.id)
166
- );
167
- const priceTextBlockStyles = convertStylesToCSS(
168
- getBlockStyles(BASKET_ITEM_BLOCKS.PRICE_TEXT.id)
169
- );
170
- const removeStyles = convertStylesToCSS(
171
- getBlockStyles(BASKET_ITEM_BLOCKS.REMOVE.id)
172
- );
173
- const removeProperties = getBlockProperties(BASKET_ITEM_BLOCKS.REMOVE.id);
174
- const removeIconRaw = getResponsiveValue(removeProperties?.icon);
175
- const removeIconValue = removeIconRaw;
176
- const hasCustomRemoveSvgIcon =
177
- typeof removeIconValue === 'string' && removeIconValue.includes('<svg');
178
- const removeIconName = (
179
- typeof removeIconValue === 'string' ? removeIconValue : 'close'
180
- ) as ComponentProps<typeof Icon>['name'];
181
- const giftPackStyles = convertStylesToCSS(
182
- getBlockStyles(BASKET_ITEM_BLOCKS.GIFT_PACK.id)
183
- );
184
-
185
- const imageWrapperKeys: Array<keyof React.CSSProperties> = [
186
- 'width',
187
- 'height',
188
- 'marginRight',
189
- 'marginLeft',
190
- 'marginTop',
191
- 'marginBottom',
192
- 'alignSelf',
193
- 'justifySelf',
194
- 'flexGrow',
195
- 'flexShrink'
196
- ];
197
- const priceWrapperKeys: Array<keyof React.CSSProperties> = [
198
- 'gap',
199
- 'marginRight',
200
- 'marginLeft',
201
- 'marginTop',
202
- 'marginBottom',
203
- 'alignSelf',
204
- 'justifySelf',
205
- 'justifyContent',
206
- 'alignItems'
207
- ];
208
- const attributesWrapperKeys: Array<keyof React.CSSProperties> = [
209
- 'gap',
210
- 'marginRight',
211
- 'marginLeft',
212
- 'marginTop',
213
- 'marginBottom',
214
- 'alignSelf',
215
- 'justifySelf',
216
- 'justifyContent',
217
- 'alignItems'
218
- ];
219
- const quantityContainerKeys: Array<keyof React.CSSProperties> = [
220
- 'gap',
221
- 'marginRight',
222
- 'marginLeft',
223
- 'marginTop',
224
- 'marginBottom',
225
- 'alignSelf',
226
- 'justifySelf',
227
- 'justifyContent',
228
- 'alignItems'
229
- ];
230
-
231
- const imageWrapperStyles = pickStyles(imageStyles, imageWrapperKeys);
232
- const imageElementStyles = {
233
- ...omitStyles(imageStyles, [
234
- 'marginRight',
235
- 'marginLeft',
236
- 'marginTop',
237
- 'marginBottom',
238
- 'alignSelf',
239
- 'justifySelf',
240
- 'flexGrow',
241
- 'flexShrink'
242
- ]),
243
- width: imageStyles.width || undefined,
244
- height: imageStyles.height || undefined
245
- };
246
- const priceWrapperStyles = pickStyles(priceStyles, priceWrapperKeys);
247
- const priceTextStyles = priceTextBlockStyles;
248
- const attributesWrapperStyles = pickStyles(
249
- attributesStyles,
250
- attributesWrapperKeys
251
- );
252
- const attributesTextStyles = omitStyles(
253
- attributesStyles,
254
- attributesWrapperKeys
255
- );
256
- const finalAttributeLabelStyles = {
257
- ...attributesTextStyles,
258
- ...attributeLabelStyles
259
- };
260
- const finalAttributeValueStyles = {
261
- ...attributesTextStyles,
262
- ...attributeValueStyles
263
- };
264
- const quantityContainerStyles = pickStyles(
265
- quantityStyles,
266
- quantityContainerKeys
267
- );
268
- const minusIconRaw = getResponsiveValue(quantityMinusIconProps.icon);
269
- const plusIconRaw = getResponsiveValue(quantityPlusIconProps.icon);
270
- const minusIcon = typeof minusIconRaw === 'string' ? minusIconRaw : 'minus';
271
- const plusIcon = typeof plusIconRaw === 'string' ? plusIconRaw : 'plus';
272
-
273
- const toNumber = (value: unknown, fallback = 12) => {
274
- if (typeof value === 'number') return value;
275
- if (typeof value === 'string') {
276
- const parsed = parseInt(value, 10);
277
- return Number.isNaN(parsed) ? fallback : parsed;
278
- }
279
-
280
- return fallback;
281
- };
282
-
283
- const minusIconSize = toNumber(
284
- quantityMinusIconStyles.width ||
285
- quantityMinusIconStyles.height ||
286
- quantityMinusIconStyles.fontSize,
287
- 12
288
- );
289
- const plusIconSize = toNumber(
290
- quantityPlusIconStyles.width ||
291
- quantityPlusIconStyles.height ||
292
- quantityPlusIconStyles.fontSize,
293
- 12
294
- );
295
-
296
- const isCustomMinusSvg =
297
- typeof minusIcon === 'string' && minusIcon.includes('<svg');
298
- const isCustomPlusSvg =
299
- typeof plusIcon === 'string' && plusIcon.includes('<svg');
300
-
301
- const handleDesignerClick = (event: React.MouseEvent) => {
302
- if (isDesigner) {
303
- event.preventDefault();
304
- }
305
- };
306
-
307
- // Check if any instance of this block type is selected
308
- const isBlockSelected = (
309
- block: typeof BASKET_ITEM_BLOCKS[keyof typeof BASKET_ITEM_BLOCKS]
310
- ) => {
311
- if (selectedBlockId === block.id) return true;
312
- // Match only numbered instances (e.g., basket-item-price-1), not sibling blocks
313
- if (selectedBlockId?.match(new RegExp(`^${block.id}-\\d+$`))) return true;
314
- return false;
315
- };
316
-
317
- // Block instance - uses base ID for styles (shared across all items)
318
- const getBlockInstance = (
319
- block: typeof BASKET_ITEM_BLOCKS[keyof typeof BASKET_ITEM_BLOCKS]
320
- ) => {
321
- // Always get styles from base block ID (shared across all items)
322
- const styles = getBlockStyles(block.id);
323
- return {
324
- ...block,
325
- // Use base ID so all items share the same block in theme editor
326
- id: block.id,
327
- styles
328
- };
329
- };
330
36
 
331
37
  const updateQuantity = async (
332
38
  productPk: number,
333
39
  quantity: number,
334
- attributes: object = {},
335
- namespace?: string
40
+ attributes: object = {}
336
41
  ) => {
337
42
  const requestParams: {
338
43
  product: number;
@@ -349,11 +54,9 @@ export const BasketItem = (props: Props) => {
349
54
  requestParams.namespace = namespace;
350
55
  }
351
56
 
352
- setUpdateQuantityLoading(true);
353
-
354
57
  await updateQuantityMutation(requestParams)
355
58
  .unwrap()
356
- .then((data) => {
59
+ .then((data) =>
357
60
  dispatch(
358
61
  basketApi.util.updateQueryData(
359
62
  'getBasket',
@@ -362,34 +65,15 @@ export const BasketItem = (props: Props) => {
362
65
  Object.assign(draftBasket, data.basket);
363
66
  }
364
67
  )
365
- );
366
- })
367
- .catch((err) => {
368
- const formattedError =
369
- err?.data?.non_field_errors ||
370
- Object.keys(err?.data || {}).map(
371
- (key) => `${key}: ${err?.data[key].join(', ')}`
372
- );
373
-
374
- console.error('Error in operation:', formattedError);
375
- })
376
- .finally(() => {
377
- setTimeout(() => {
378
- setUpdateQuantityLoading(false);
379
- }, 200);
380
- });
68
+ )
69
+ );
381
70
  };
382
71
 
383
72
  const deleteProduct = async (productPk?: number) => {
384
73
  setUpdateQuantityLoading(true);
385
74
 
386
75
  try {
387
- await updateQuantity(
388
- basketItem.product.pk,
389
- 0,
390
- basketItem.attributes,
391
- namespace
392
- );
76
+ await updateQuantity(basketItem.product.pk, 0, basketItem.attributes);
393
77
  pushRemoveFromCart(basketItem?.product);
394
78
 
395
79
  if (productPk) {
@@ -403,385 +87,117 @@ export const BasketItem = (props: Props) => {
403
87
  }
404
88
  };
405
89
 
406
- const handleQuantityChange = (newQuantity: number) => {
407
- if (newQuantity === 0) {
408
- setRemoveBasketModalOpen(true);
409
- } else {
410
- updateQuantity(
411
- basketItem.product.pk,
412
- newQuantity,
413
- basketItem.attributes,
414
- namespace
415
- );
416
- }
417
- };
418
-
419
- const handleDecrease = () => {
420
- if (isDesigner) return;
421
- if (updateQuantityLoading) return;
422
- handleQuantityChange(basketItem.quantity - 1);
423
- };
424
-
425
- const handleIncrease = () => {
426
- if (isDesigner) return;
427
- if (updateQuantityLoading) return;
428
- if (basketItem.quantity >= 999) return;
429
- handleQuantityChange(basketItem.quantity + 1);
430
- };
431
-
432
90
  return (
433
91
  <>
434
- <li key={basketItem.id} data-testid="basket-item">
435
- <WithDesignerFeatures
436
- block={getBlockInstance(BASKET_ITEM_BLOCKS.ITEM_ROW)}
437
- placeholderId={BASKET_PLACEHOLDER_ID}
438
- sectionId={BASKET_ITEMS_SECTION_ID}
439
- isDesigner={isDesigner}
440
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.ITEM_ROW)}
441
- className="flex border-b border-gray-200 py-3 relative flex-col gap-4 sm:flex-row"
442
- style={rowStyles}
443
- >
444
- <WithDesignerFeatures
445
- block={getBlockInstance(BASKET_ITEM_BLOCKS.IMAGE)}
446
- placeholderId={BASKET_PLACEHOLDER_ID}
447
- sectionId={BASKET_ITEMS_SECTION_ID}
448
- isDesigner={isDesigner}
449
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.IMAGE)}
450
- className="mr-4 shrink-0"
451
- style={imageWrapperStyles}
452
- >
453
- <Link
454
- href={basketItem.product.absolute_url}
455
- passHref
456
- onClick={handleDesignerClick}
457
- >
458
- <Image
459
- src={basketItem.product.productimage_set[0]?.image}
460
- alt={basketItem.product.name}
461
- width={80}
462
- height={128}
463
- className="md:hidden"
464
- style={imageElementStyles}
465
- />
466
-
467
- <Image
468
- src={basketItem.product.productimage_set[0]?.image}
469
- alt={basketItem.product.name}
470
- width={105}
471
- height={158}
472
- className="hidden md:block"
473
- style={imageElementStyles}
474
- />
475
- </Link>
476
- </WithDesignerFeatures>
477
- <div className="w-full flex flex-col justify-between">
478
- <div className="flex h-full gap-4">
479
- <div className="flex flex-1 flex-col gap-3 sm:flex-row sm:gap-4">
480
- <div className="flex-1 space-y-2">
481
- <WithDesignerFeatures
482
- block={getBlockInstance(BASKET_ITEM_BLOCKS.NAME)}
483
- placeholderId={BASKET_PLACEHOLDER_ID}
484
- sectionId={BASKET_ITEMS_SECTION_ID}
485
- isDesigner={isDesigner}
486
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.NAME)}
487
- className="inline-block w-full text-xs"
488
- style={nameStyles}
489
- >
490
- <Link
491
- href={basketItem.product.absolute_url}
492
- data-testid="basket-product-name"
493
- passHref
494
- onClick={handleDesignerClick}
495
- >
496
- <span>{basketItem.product.name}</span>
497
- </Link>
498
- </WithDesignerFeatures>
499
- <WithDesignerFeatures
500
- block={getBlockInstance(BASKET_ITEM_BLOCKS.ATTRIBUTES)}
501
- placeholderId={BASKET_PLACEHOLDER_ID}
502
- sectionId={BASKET_ITEMS_SECTION_ID}
503
- isDesigner={isDesigner}
504
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.ATTRIBUTES)}
505
- className="flex flex-col gap-1"
506
- style={attributesWrapperStyles}
507
- >
508
- {commonProductAttributes.map((attribute, index) => (
509
- <div
510
- className="text-xs"
511
- key={index}
512
- style={attributesTextStyles}
513
- >
514
- <WithDesignerFeatures
515
- block={getBlockInstance(
516
- BASKET_ITEM_BLOCKS.ATTRIBUTE_LABEL
517
- )}
518
- placeholderId={BASKET_PLACEHOLDER_ID}
519
- sectionId={BASKET_ITEMS_SECTION_ID}
520
- isDesigner={isDesigner}
521
- isSelected={isBlockSelected(
522
- BASKET_ITEM_BLOCKS.ATTRIBUTE_LABEL
523
- )}
524
- className="inline-block"
525
- >
526
- <span style={finalAttributeLabelStyles}>
527
- {attribute.name}
528
- </span>
529
- </WithDesignerFeatures>
530
- :{' '}
531
- <WithDesignerFeatures
532
- block={getBlockInstance(
533
- BASKET_ITEM_BLOCKS.ATTRIBUTE_VALUE
534
- )}
535
- placeholderId={BASKET_PLACEHOLDER_ID}
536
- sectionId={BASKET_ITEMS_SECTION_ID}
537
- isDesigner={isDesigner}
538
- isSelected={isBlockSelected(
539
- BASKET_ITEM_BLOCKS.ATTRIBUTE_VALUE
540
- )}
541
- className="inline-block"
542
- >
543
- <span
544
- style={finalAttributeValueStyles}
545
- data-testid={`basket-item-${attribute.name.toLowerCase()}`}
546
- >
547
- {attribute.value}
548
- </span>
549
- </WithDesignerFeatures>
550
- </div>
551
- ))}
552
- </WithDesignerFeatures>
553
- </div>
554
- <WithDesignerFeatures
555
- block={getBlockInstance(BASKET_ITEM_BLOCKS.QUANTITY)}
556
- placeholderId={BASKET_PLACEHOLDER_ID}
557
- sectionId={BASKET_ITEMS_SECTION_ID}
558
- isDesigner={isDesigner}
559
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.QUANTITY)}
560
- className="flex flex-col justify-center md:flex-row md:items-center"
561
- style={quantityContainerStyles}
92
+ <li
93
+ key={basketItem.id}
94
+ className="flex border-b border-gray-200 py-3 relative"
95
+ data-testid="basket-item"
96
+ >
97
+ <div className="w-20 lg:w-[105px] mr-4 shrink-0">
98
+ <Link href={basketItem.product.absolute_url} passHref>
99
+ <Image
100
+ src={basketItem.product.productimage_set[0]?.image}
101
+ alt={basketItem.product.name}
102
+ width={80}
103
+ height={128}
104
+ className="md:hidden"
105
+ />
106
+
107
+ <Image
108
+ src={basketItem.product.productimage_set[0]?.image}
109
+ alt={basketItem.product.name}
110
+ width={105}
111
+ height={158}
112
+ className="hidden md:block"
113
+ />
114
+ </Link>
115
+ </div>
116
+ <div className="w-full flex flex-col justify-between">
117
+ <div className="flex h-full">
118
+ <div className="flex flex-1 flex-col gap-3 sm:flex-row sm:gap-1">
119
+ <div className="flex-1">
120
+ <Link
121
+ href={basketItem.product.absolute_url}
122
+ data-testid="basket-product-name"
123
+ passHref
562
124
  >
563
- <WithDesignerFeatures
564
- block={getBlockInstance(
565
- BASKET_ITEM_BLOCKS.QUANTITY_WRAPPER
566
- )}
567
- placeholderId={BASKET_PLACEHOLDER_ID}
568
- sectionId={BASKET_ITEMS_SECTION_ID}
569
- isDesigner={isDesigner}
570
- isSelected={isBlockSelected(
571
- BASKET_ITEM_BLOCKS.QUANTITY_WRAPPER
572
- )}
573
- className="w-[138px] h-11 flex items-center justify-between border p-4"
574
- style={quantityWrapperStyles}
575
- >
576
- <WithDesignerFeatures
577
- block={getBlockInstance(
578
- BASKET_ITEM_BLOCKS.QUANTITY_MINUS_BUTTON
579
- )}
580
- placeholderId={BASKET_PLACEHOLDER_ID}
581
- sectionId={BASKET_ITEMS_SECTION_ID}
582
- isDesigner={isDesigner}
583
- isSelected={isBlockSelected(
584
- BASKET_ITEM_BLOCKS.QUANTITY_MINUS_BUTTON
585
- )}
586
- className="inline-flex"
587
- >
588
- <Button
589
- className="h-auto p-0 hover:bg-transparent hover:text-black"
590
- appearance="ghost"
591
- onClick={handleDecrease}
592
- disabled={
593
- updateQuantityLoading || basketItem.quantity <= 0
594
- }
595
- style={quantityMinusButtonStyles}
596
- >
597
- <WithDesignerFeatures
598
- block={getBlockInstance(
599
- BASKET_ITEM_BLOCKS.QUANTITY_MINUS_ICON
600
- )}
601
- placeholderId={BASKET_PLACEHOLDER_ID}
602
- sectionId={BASKET_ITEMS_SECTION_ID}
603
- isDesigner={isDesigner}
604
- isSelected={isBlockSelected(
605
- BASKET_ITEM_BLOCKS.QUANTITY_MINUS_ICON
606
- )}
607
- className="inline-flex"
608
- style={quantityMinusIconStyles}
609
- >
610
- {isCustomMinusSvg ? (
611
- <div
612
- style={{
613
- width: minusIconSize,
614
- height: minusIconSize
615
- }}
616
- dangerouslySetInnerHTML={{ __html: minusIcon }}
617
- />
618
- ) : (
619
- <Icon
620
- name={
621
- minusIcon as ComponentProps<typeof Icon>['name']
622
- }
623
- size={minusIconSize}
624
- style={quantityMinusIconStyles}
625
- />
626
- )}
627
- </WithDesignerFeatures>
628
- </Button>
629
- </WithDesignerFeatures>
630
-
631
- <WithDesignerFeatures
632
- block={getBlockInstance(
633
- BASKET_ITEM_BLOCKS.QUANTITY_VALUE
634
- )}
635
- placeholderId={BASKET_PLACEHOLDER_ID}
636
- sectionId={BASKET_ITEMS_SECTION_ID}
637
- isDesigner={isDesigner}
638
- isSelected={isBlockSelected(
639
- BASKET_ITEM_BLOCKS.QUANTITY_VALUE
640
- )}
641
- className="inline-flex"
642
- style={quantityValueStyles}
643
- >
644
- {updateQuantityLoading ? (
645
- <LoaderSpinner className="w-4 h-4" />
646
- ) : (
647
- <span style={quantityValueStyles}>
648
- {basketItem.quantity}
649
- </span>
650
- )}
651
- </WithDesignerFeatures>
652
-
653
- <WithDesignerFeatures
654
- block={getBlockInstance(
655
- BASKET_ITEM_BLOCKS.QUANTITY_PLUS_BUTTON
656
- )}
657
- placeholderId={BASKET_PLACEHOLDER_ID}
658
- sectionId={BASKET_ITEMS_SECTION_ID}
659
- isDesigner={isDesigner}
660
- isSelected={isBlockSelected(
661
- BASKET_ITEM_BLOCKS.QUANTITY_PLUS_BUTTON
662
- )}
663
- className="inline-flex"
664
- >
665
- <Button
666
- className="h-auto p-0 hover:bg-transparent hover:text-black"
667
- appearance="ghost"
668
- onClick={handleIncrease}
669
- disabled={
670
- updateQuantityLoading || basketItem.quantity >= 999
671
- }
672
- style={quantityPlusButtonStyles}
125
+ <span className="text-xs">{basketItem.product.name}</span>
126
+ </Link>
127
+ <div className="flex flex-col gap-1">
128
+ {commonProductAttributes.map((attribute, index) => (
129
+ <span className="text-xs" key={index}>
130
+ <span>{attribute.name}</span>:{' '}
131
+ <span
132
+ data-testid={`basket-item-${attribute.name.toLowerCase()}`}
673
133
  >
674
- <WithDesignerFeatures
675
- block={getBlockInstance(
676
- BASKET_ITEM_BLOCKS.QUANTITY_PLUS_ICON
677
- )}
678
- placeholderId={BASKET_PLACEHOLDER_ID}
679
- sectionId={BASKET_ITEMS_SECTION_ID}
680
- isDesigner={isDesigner}
681
- isSelected={isBlockSelected(
682
- BASKET_ITEM_BLOCKS.QUANTITY_PLUS_ICON
683
- )}
684
- className="inline-flex"
685
- style={quantityPlusIconStyles}
686
- >
687
- {isCustomPlusSvg ? (
688
- <div
689
- style={{
690
- width: plusIconSize,
691
- height: plusIconSize
692
- }}
693
- dangerouslySetInnerHTML={{ __html: plusIcon }}
694
- />
695
- ) : (
696
- <Icon
697
- name={
698
- plusIcon as ComponentProps<typeof Icon>['name']
699
- }
700
- size={plusIconSize}
701
- style={quantityPlusIconStyles}
702
- />
703
- )}
704
- </WithDesignerFeatures>
705
- </Button>
706
- </WithDesignerFeatures>
707
- </WithDesignerFeatures>
708
- </WithDesignerFeatures>
709
- <WithDesignerFeatures
710
- block={getBlockInstance(BASKET_ITEM_BLOCKS.PRICE)}
711
- placeholderId={BASKET_PLACEHOLDER_ID}
712
- sectionId={BASKET_ITEMS_SECTION_ID}
713
- isDesigner={isDesigner}
714
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.PRICE)}
715
- className="flex flex-col shrink-0 text-sm gap-2 items-start justify-center lg:flex-row lg:mr-6 lg:gap-6 sm:items-center lg:justify-start"
716
- style={priceWrapperStyles}
717
- >
718
- <WithDesignerFeatures
719
- block={getBlockInstance(BASKET_ITEM_BLOCKS.PRICE_TEXT)}
720
- placeholderId={BASKET_PLACEHOLDER_ID}
721
- sectionId={BASKET_ITEMS_SECTION_ID}
722
- isDesigner={isDesigner}
723
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.PRICE_TEXT)}
724
- className="inline-block"
725
- >
726
- <Price
727
- value={basketItem.product.price}
728
- data-testid="basket-product-price"
729
- style={priceTextStyles}
730
- />
731
- </WithDesignerFeatures>
732
- </WithDesignerFeatures>
134
+ {attribute.value}
135
+ </span>
136
+ </span>
137
+ ))}
138
+ </div>
733
139
  </div>
734
- <WithDesignerFeatures
735
- block={getBlockInstance(BASKET_ITEM_BLOCKS.REMOVE)}
736
- placeholderId={BASKET_PLACEHOLDER_ID}
737
- sectionId={BASKET_ITEMS_SECTION_ID}
738
- isDesigner={isDesigner}
739
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.REMOVE)}
740
- className="self-center"
741
- style={removeStyles}
742
- >
743
- {hasCustomRemoveSvgIcon ? (
744
- <div
745
- className="cursor-pointer"
746
- style={{ width: 16, height: 16, ...removeStyles }}
747
- dangerouslySetInnerHTML={{ __html: removeIconValue }}
748
- onClick={() => setRemoveBasketModalOpen(true)}
749
- data-testid="basket-product-remove"
750
- />
751
- ) : (
752
- <Icon
753
- name={removeIconName}
754
- size={16}
755
- className="cursor-pointer hover:fill-secondary-500"
756
- style={removeStyles}
757
- onClick={() => setRemoveBasketModalOpen(true)}
758
- data-testid="basket-product-remove"
140
+ <div className="flex flex-col justify-center md:flex-row md:items-center lg:w-52">
141
+ <Select
142
+ className="px-2"
143
+ defaultValue={basketItem.quantity}
144
+ onChange={(event) => {
145
+ updateQuantity(
146
+ basketItem.product.pk,
147
+ Number(event.currentTarget.value)
148
+ );
149
+ }}
150
+ options={[
151
+ ...Array.from({ length: 10 }, (_, i) => i + 1),
152
+ basketItem.quantity > 10 && basketItem.quantity
153
+ ]
154
+ .filter((i) => i)
155
+ .map((i) => ({
156
+ label: `${t('basket.card.qty')} ${i}`,
157
+ value: `${i}`
158
+ }))}
159
+ data-testid="basket-product-quantity"
160
+ ></Select>
161
+ </div>
162
+ <div className="flex flex-col shrink-0 text-sm gap-2 items-start justify-center w-48 lg:flex-row lg:mr-6 lg:gap-6 sm:items-center lg:justify-start">
163
+ {parseFloat(basketItem.product.retail_price) >
164
+ parseFloat(basketItem.product.price) && (
165
+ <Price
166
+ className="line-through"
167
+ value={basketItem.product.retail_price}
759
168
  />
760
169
  )}
761
- </WithDesignerFeatures>
170
+ <Price
171
+ className={clsx(
172
+ parseFloat(basketItem.product.retail_price) >
173
+ parseFloat(basketItem.product.price)
174
+ ? 'text-secondary-500'
175
+ : 'text-primary'
176
+ )}
177
+ value={basketItem.product.price}
178
+ data-testid="basket-product-price"
179
+ />
180
+ </div>
762
181
  </div>
763
-
764
- <WithDesignerFeatures
765
- block={getBlockInstance(BASKET_ITEM_BLOCKS.GIFT_PACK)}
766
- placeholderId={BASKET_PLACEHOLDER_ID}
767
- sectionId={BASKET_ITEMS_SECTION_ID}
768
- isDesigner={isDesigner}
769
- isSelected={isBlockSelected(BASKET_ITEM_BLOCKS.GIFT_PACK)}
770
- className="mt-3"
771
- style={giftPackStyles}
772
- >
773
- <PluginModule
774
- component={Component.BasketGiftPack}
775
- props={{ basketItem }}
776
- />
777
- </WithDesignerFeatures>
182
+ <Icon
183
+ name="close"
184
+ size={16}
185
+ className="self-center cursor-pointer hover:fill-secondary-500" // TODO: Add hover color. Fill not working
186
+ onClick={() => setRemoveBasketModalOpen(true)}
187
+ data-testid="basket-product-remove"
188
+ />
778
189
  </div>
779
- </WithDesignerFeatures>
190
+
191
+ <PluginModule
192
+ component={Component.BasketGiftPack}
193
+ props={{ basketItem }}
194
+ />
195
+ </div>
780
196
  </li>
781
197
  <Modal
782
198
  portalId="remove-basket-item"
783
199
  title={t('basket.card.modal.title')}
784
- className="w-full md:w-[28rem] max-h-[90vh] overflow-y-auto"
200
+ className="w-full sm:w-[28rem] max-h-[90vh] overflow-y-auto"
785
201
  open={isRemoveBasketModalOpen}
786
202
  setOpen={setRemoveBasketModalOpen}
787
203
  >