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

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 (138) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/app-template/CHANGELOG.md +138 -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/dist/commands/plugins.js +23 -2
  66. package/package.json +1 -1
  67. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +0 -15
  68. package/app-template/src/views/basket/basket-summary-context.tsx +0 -560
  69. package/app-template/src/views/basket/designer-context.tsx +0 -617
  70. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +0 -190
  71. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +0 -286
  72. package/app-template/src/views/breadcrumb/constants.ts +0 -15
  73. package/app-template/src/views/breadcrumb/index.tsx +0 -127
  74. package/app-template/src/views/category/native-widget-context.tsx +0 -257
  75. package/app-template/src/views/category/product-list-registrar.tsx +0 -665
  76. package/app-template/src/views/checkout/checkout-address-registrar.tsx +0 -254
  77. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +0 -183
  78. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +0 -259
  79. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +0 -253
  80. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +0 -183
  81. package/app-template/src/views/checkout/constants.ts +0 -5
  82. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +0 -15
  83. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +0 -18
  84. package/app-template/src/views/footer/footer-app-banner-context.tsx +0 -326
  85. package/app-template/src/views/footer/footer-bottom-context.tsx +0 -215
  86. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +0 -74
  87. package/app-template/src/views/footer/footer-layout-constants.ts +0 -35
  88. package/app-template/src/views/footer/footer-layout-registrar.tsx +0 -342
  89. package/app-template/src/views/footer/footer-layout-switcher.tsx +0 -110
  90. package/app-template/src/views/footer/footer-menu-context.tsx +0 -211
  91. package/app-template/src/views/footer/footer-native-widgets.tsx +0 -60
  92. package/app-template/src/views/footer/footer-social-context.tsx +0 -254
  93. package/app-template/src/views/footer/footer-subscription-context.tsx +0 -210
  94. package/app-template/src/views/footer/footer-utils.ts +0 -43
  95. package/app-template/src/views/footer/footer-value-props-context.tsx +0 -326
  96. package/app-template/src/views/footer/logo-settings.ts +0 -183
  97. package/app-template/src/views/footer/native-widget-config.ts +0 -262
  98. package/app-template/src/views/footer/subscription-settings.ts +0 -122
  99. package/app-template/src/views/footer/use-footer-logo.ts +0 -162
  100. package/app-template/src/views/header/designer-context.tsx +0 -261
  101. package/app-template/src/views/header/header-announcement-registrar.tsx +0 -267
  102. package/app-template/src/views/header/header-client-wrapper.tsx +0 -496
  103. package/app-template/src/views/header/header-content.tsx +0 -1026
  104. package/app-template/src/views/header/header-currency-registrar.tsx +0 -348
  105. package/app-template/src/views/header/header-icons-context.tsx +0 -262
  106. package/app-template/src/views/header/header-language-registrar.tsx +0 -348
  107. package/app-template/src/views/header/header-layout-context.tsx +0 -143
  108. package/app-template/src/views/header/header-layout-registrar.tsx +0 -658
  109. package/app-template/src/views/header/header-logo-context.tsx +0 -228
  110. package/app-template/src/views/header/header-logo.tsx +0 -118
  111. package/app-template/src/views/header/header-mini-basket-context.tsx +0 -524
  112. package/app-template/src/views/header/header-search-registrar.tsx +0 -511
  113. package/app-template/src/views/header/header-text-slider-registrar.tsx +0 -382
  114. package/app-template/src/views/header/inline-search.tsx +0 -262
  115. package/app-template/src/views/header/navbar-menu-context.tsx +0 -219
  116. package/app-template/src/views/header/search/search-input.tsx +0 -61
  117. package/app-template/src/views/header/server-settings-parser.ts +0 -1105
  118. package/app-template/src/views/header/use-header-icons.ts +0 -241
  119. package/app-template/src/views/header/use-header-logo.ts +0 -213
  120. package/app-template/src/views/header/use-navbar-menu.ts +0 -179
  121. package/app-template/src/views/product/accordion-section.tsx +0 -61
  122. package/app-template/src/views/product/custom-button-group.tsx +0 -69
  123. package/app-template/src/views/product/favorites-button-section.tsx +0 -69
  124. package/app-template/src/views/product/find-in-store-section.tsx +0 -60
  125. package/app-template/src/views/product/product-info-section.tsx +0 -140
  126. package/app-template/src/views/product/quantity-section.tsx +0 -73
  127. package/app-template/src/views/product/sale-tag.tsx +0 -10
  128. package/app-template/src/views/product/share-section.tsx +0 -357
  129. package/app-template/src/views/product/variants-section.tsx +0 -126
  130. package/app-template/src/views/product-detail/constants.ts +0 -272
  131. package/app-template/src/views/product-detail/index.ts +0 -10
  132. package/app-template/src/views/product-detail/product-detail-registrar.tsx +0 -616
  133. package/app-template/src/widgets/footer-app-banner.tsx +0 -444
  134. package/app-template/src/widgets/footer-bottom.tsx +0 -127
  135. package/app-template/src/widgets/footer-menu-compact.tsx +0 -238
  136. package/app-template/src/widgets/footer-menu-two.tsx +0 -298
  137. package/app-template/src/widgets/footer-social-client.tsx +0 -251
  138. package/app-template/src/widgets/footer-value-props.tsx +0 -201
@@ -1,64 +1,24 @@
1
1
  'use client';
2
2
 
3
+ import clsx from 'clsx';
3
4
  import React, { useEffect, useState } from 'react';
5
+ import { PriceWrapper } from '@theme/views/product';
4
6
  import { ProductPageProps } from './layout';
7
+ import MiscButtons from './misc-buttons';
5
8
  import { pushProductViewed } from '@theme/utils/gtm';
6
9
  import { useSession } from 'next-auth/react';
7
10
  import { isVariantSelectionComplete } from '../../utils/variant-validation';
8
11
  import { useProductCart } from '../../hooks/use-product-cart';
9
12
  import { useStockAlert } from '../../hooks/use-stock-alert';
13
+ import { ProductVariants } from './product-variants';
10
14
  import { ProductActions } from './product-actions';
11
- import { QuantitySection } from './quantity-section';
12
- import { VariantsSection } from './variants-section';
13
- import { ProductInfoSection } from './product-info-section';
14
- import ProductShare from './product-share';
15
- import MiscButtons from './misc-buttons';
15
+ import { ProductShare } from './product-share';
16
16
  import PluginModule, { Component } from '@akinon/next/components/plugin-module';
17
- import {
18
- ProductDetailRegistrar,
19
- AddToCartStyles,
20
- QuantityStyles,
21
- QuantityProperties,
22
- VariantsStyles,
23
- ProductInfoStyles,
24
- ShareStyles,
25
- ShareProperties,
26
- AccordionStyles,
27
- FavoritesButtonStyles,
28
- FavoritesButtonProperties,
29
- FindInStoreStyles,
30
- FindInStoreProperties
31
- } from '@theme/views/product-detail';
32
- import { AccordionWrapper } from '@theme/views/product';
33
-
34
- export interface ProductDetailSettings {
35
- addToCartStyles?: AddToCartStyles;
36
- quantityStyles?: QuantityStyles;
37
- quantityProperties?: QuantityProperties;
38
- variantsStyles?: VariantsStyles;
39
- productInfoStyles?: ProductInfoStyles;
40
- shareStyles?: ShareStyles;
41
- shareProperties?: ShareProperties;
42
- accordionStyles?: AccordionStyles;
43
- favoritesButtonStyles?: FavoritesButtonStyles;
44
- favoritesButtonProperties?: FavoritesButtonProperties;
45
- findInStoreStyles?: FindInStoreStyles;
46
- findInStoreProperties?: FindInStoreProperties;
47
- }
48
17
 
49
- interface ProductInfoProps extends ProductPageProps {
50
- productDetailSettings?: ProductDetailSettings;
51
- deliveryReturn?: any;
52
- }
53
-
54
- export default function ProductInfo({
55
- data,
56
- productDetailSettings,
57
- deliveryReturn
58
- }: ProductInfoProps) {
18
+ export default function ProductInfo({ data }: ProductPageProps) {
59
19
  const { data: session } = useSession();
60
20
  const [isVariantLoading, setIsVariantLoading] = useState(false);
61
- const [quantity, setQuantity] = useState(1);
21
+
62
22
  const inStock = data.selected_variant !== null || data.product.in_stock;
63
23
 
64
24
  const {
@@ -112,26 +72,30 @@ export default function ProductInfo({
112
72
  };
113
73
 
114
74
  return (
115
- <ProductDetailRegistrar initialSettings={productDetailSettings}>
116
- <ProductInfoSection product={data.product} />
117
-
118
- <VariantsSection
75
+ <>
76
+ <div
77
+ className={clsx(
78
+ 'fixed bottom-0 left-0 w-1/2 h-14 z-[20] bg-white mt-0 border-t border-gray-500 items-center justify-center',
79
+ 'sm:relative sm:flex sm:items-center sm:mt-5 sm:border-none'
80
+ )}
81
+ >
82
+ <PriceWrapper
83
+ price={data.product.price}
84
+ retailPrice={data.product.retail_price}
85
+ />
86
+ </div>
87
+
88
+ <ProductVariants
119
89
  variants={data.variants}
120
90
  onVariantChange={handleVariantChange}
121
91
  />
122
92
 
123
- <QuantitySection
124
- quantity={quantity}
125
- onChange={setQuantity}
126
- disabled={!inStock || isVariantLoading}
127
- />
128
-
129
93
  <ProductActions
130
94
  product={data.product}
131
95
  variants={data.variants}
132
96
  inStock={inStock}
133
97
  isVariantLoading={isVariantLoading}
134
- onAddToCart={() => addProductToCart(quantity)}
98
+ onAddToCart={addProductToCart}
135
99
  onAddToStockAlert={addProductToStockAlertList}
136
100
  onClearError={clearProductError}
137
101
  isAddToCartLoading={isAddToCartLoading}
@@ -142,16 +106,6 @@ export default function ProductInfo({
142
106
  onCloseModal={closeModal}
143
107
  />
144
108
 
145
- <ProductShare productName={data.product.name} />
146
-
147
- <MiscButtons
148
- productName={data.product.name}
149
- productPk={data.product.pk}
150
- variants={data.variants}
151
- />
152
-
153
- <AccordionWrapper data={data} deliveryReturn={deliveryReturn} />
154
-
155
109
  <PluginModule
156
110
  component={Component.VirtualTryOnPlugin}
157
111
  props={{
@@ -159,6 +113,14 @@ export default function ProductInfo({
159
113
  className: 'hidden sm:flex'
160
114
  }}
161
115
  />
162
- </ProductDetailRegistrar>
116
+
117
+ <MiscButtons
118
+ productName={data.product.name}
119
+ productPk={data.product.pk}
120
+ variants={data.variants}
121
+ />
122
+
123
+ <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
124
+ </>
163
125
  );
164
126
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, useEffect } from 'react';
4
- import { ShareSection } from './share-section';
4
+ import Share from '@theme/views/share';
5
5
  import { useLocalization } from '@akinon/next/hooks';
6
6
 
7
7
  interface ProductShareProps {
@@ -9,7 +9,7 @@ interface ProductShareProps {
9
9
  className?: string;
10
10
  }
11
11
 
12
- const ProductShare: React.FC<ProductShareProps> = ({
12
+ export const ProductShare: React.FC<ProductShareProps> = ({
13
13
  productName,
14
14
  className
15
15
  }) => {
@@ -30,32 +30,29 @@ const ProductShare: React.FC<ProductShareProps> = ({
30
30
  currentUrl
31
31
  )}`,
32
32
  iconName: 'facebook' as const,
33
- iconSize: 16
33
+ iconSize: 22
34
34
  },
35
35
  {
36
36
  href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
37
37
  currentUrl
38
38
  )}`,
39
39
  iconName: 'twitter' as const,
40
- iconSize: 16
40
+ iconSize: 22
41
41
  },
42
42
  {
43
43
  href: `https://api.whatsapp.com/send?text=${productName}%20${encodeURIComponent(
44
44
  currentUrl
45
45
  )}`,
46
46
  iconName: 'whatsapp' as const,
47
- iconSize: 16
47
+ iconSize: 22
48
48
  }
49
49
  ];
50
50
 
51
51
  return (
52
- <ShareSection
53
- productName={productName}
52
+ <Share
53
+ className={className}
54
54
  buttonText={t('product.share')}
55
55
  items={shareItems}
56
- className={className}
57
56
  />
58
57
  );
59
- };
60
-
61
- export default ProductShare;
58
+ };
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
 
3
- import React, { useRef, useState, useCallback } from 'react';
3
+ import React, { useState, useRef } from 'react';
4
4
  import { CarouselCore } from '@theme/components/carousel-core';
5
+ import { Icon } from '@theme/components';
5
6
  import { Product } from '@akinon/next/types';
6
7
  import { Image } from '@akinon/next/components/image';
7
- import { Icon, Button } from '@theme/components';
8
- import CustomButtonGroup from './custom-button-group';
8
+ import useFavButton from '../../hooks/use-fav-button';
9
+ import { twMerge } from 'tailwind-merge';
9
10
  import PluginModule, { Component } from '@akinon/next/components/plugin-module';
10
11
 
11
12
  type ProductSliderItem = {
@@ -13,32 +14,81 @@ type ProductSliderItem = {
13
14
  };
14
15
 
15
16
  export default function ProductInfoSlider({ product }: ProductSliderItem) {
17
+ const { FavButton } = useFavButton(product.pk);
16
18
  const carouselRef = useRef(null);
17
- const [zoomedImage, setZoomedImage] = useState<string | null>(null);
18
- const handleOpenZoom = useCallback((src: string) => setZoomedImage(src), []);
19
- const handleCloseZoom = useCallback(() => setZoomedImage(null), []);
20
- const productImages = product?.productimage_set ?? [];
21
19
  const [activeIndex, setActiveIndex] = useState(0);
22
20
 
23
- const firstImage = productImages[0];
24
- const remainingImages = productImages.slice(1);
25
- const smallImagePairs = [];
26
- let trailingLargeImage = null;
21
+ const goToPrev = () => {
22
+ const newIndex =
23
+ activeIndex === 0
24
+ ? product?.productimage_set?.length - 1
25
+ : activeIndex - 1;
26
+ setActiveIndex(newIndex);
27
+ carouselRef.current?.previous();
28
+ };
27
29
 
28
- for (let i = 0; i < remainingImages.length; i += 2) {
29
- if (remainingImages[i + 1]) {
30
- smallImagePairs.push([remainingImages[i], remainingImages[i + 1]]);
31
- } else {
32
- trailingLargeImage = remainingImages[i];
33
- }
34
- }
30
+ const goToNext = () => {
31
+ const newIndex =
32
+ activeIndex === product?.productimage_set?.length - 1
33
+ ? 0
34
+ : activeIndex + 1;
35
+ setActiveIndex(newIndex);
36
+ carouselRef.current?.next();
37
+ };
38
+
39
+ const handleThumbnailClick = (index) => {
40
+ setActiveIndex(index);
41
+ carouselRef.current?.goToSlide(index);
42
+ };
35
43
 
36
44
  return (
37
- <>
38
- <div className="relative lg:hidden">
39
- <div className="absolute left-6 top-6 z-[20] w-8 h-8 flex items-center justify-center rounded-full bg-white border border-gray-30">
40
- <Icon name="zoom" size={11} className="text-primary" />
45
+ <div className="lg:grid lg:grid-cols-6">
46
+ <div className="lg:col-span-1">
47
+ <div className="flex flex-col items-center justify-center md:mr-[6px]">
48
+ <button
49
+ onClick={goToPrev}
50
+ className={twMerge(
51
+ 'hidden justify-center p-2 mb-3 border border-gray-100 rounded-full cursor-pointer lg:block',
52
+ [activeIndex === 0 && 'cursor-not-allowed opacity-45']
53
+ )}
54
+ disabled={activeIndex === 0}
55
+ >
56
+ <Icon name="chevron-up" size={15} className="fill-[#000000]" />
57
+ </button>
58
+ <div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
59
+ {product?.productimage_set?.map((item, index) => (
60
+ <Image
61
+ key={index}
62
+ src={item.image}
63
+ alt={`Thumbnail ${index}`}
64
+ width={80}
65
+ height={128}
66
+ className={twMerge('cursor-pointer', [
67
+ activeIndex === index && 'border-2 border-primary'
68
+ ])}
69
+ onClick={() => handleThumbnailClick(index)}
70
+ />
71
+ ))}
72
+ </div>
73
+ <button
74
+ onClick={goToNext}
75
+ className={twMerge(
76
+ 'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
77
+ [
78
+ activeIndex === product.productimage_set.length - 1 &&
79
+ 'cursor-not-allowed opacity-45'
80
+ ]
81
+ )}
82
+ disabled={activeIndex === product.productimage_set.length - 1}
83
+ >
84
+ <Icon name="chevron-down" size={15} className="fill-[#000000]" />
85
+ </button>
41
86
  </div>
87
+ </div>
88
+
89
+ <div className="relative lg:col-span-5">
90
+ <FavButton className="absolute right-8 top-6 z-[20] sm:hidden" />
91
+
42
92
  <PluginModule
43
93
  component={Component.ProductImageSearchFeature}
44
94
  props={{
@@ -46,8 +96,7 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
46
96
  activeIndex,
47
97
  showResetButton: true,
48
98
  enableTextSearch: true,
49
- isEnabled: true,
50
- className: 'right-2 top-6 absolute z-[30] w-auto px-4 text-xs mt-0'
99
+ isEnabled: true
51
100
  }}
52
101
  />
53
102
 
@@ -59,6 +108,7 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
59
108
  'sm:hidden absolute bottom-[70px] right-[5px] z-[30] w-auto px-4 text-xs mt-0'
60
109
  }}
61
110
  />
111
+
62
112
  <CarouselCore
63
113
  responsive={{
64
114
  all: {
@@ -67,117 +117,29 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
67
117
  }
68
118
  }}
69
119
  arrows={false}
70
- swipeable
120
+ swipeable={true}
71
121
  ref={carouselRef}
72
122
  afterChange={(previousSlide, { currentSlide }) => {
73
123
  setActiveIndex(currentSlide);
74
124
  }}
75
- containerAspectRatio={{ mobile: 1, desktop: 1 }}
76
- renderButtonGroupOutside
77
- customButtonGroup={<CustomButtonGroup />}
125
+ containerAspectRatio={{ mobile: 520 / 798, desktop: 484 / 726 }}
78
126
  >
79
- {productImages.map((item, i) => (
127
+ {product?.productimage_set?.map((item, i) => (
80
128
  <Image
81
129
  key={i}
82
130
  src={item.image}
83
131
  alt={product.name}
84
132
  draggable={false}
85
- aspectRatio={1}
133
+ aspectRatio={484 / 726}
86
134
  sizes="(min-width: 425px) 512px,
87
135
  (min-width: 601px) 576px,
88
136
  (min-width: 768px) 336px,
89
137
  (min-width: 1024px) 484px, 368px"
90
138
  fill
91
- className="cursor-zoom-in"
92
- onClick={() => handleOpenZoom(item.image)}
93
139
  />
94
140
  ))}
95
141
  </CarouselCore>
96
142
  </div>
97
- <div className="hidden lg:flex lg:flex-col lg:gap-2.5">
98
- {firstImage && (
99
- <div className="relative w-full overflow-hidden bg-gray-50 aspect-square">
100
- <div className="absolute left-6 top-6 z-[20] w-8 h-8 flex items-center justify-center rounded-full bg-white border border-gray-30">
101
- <Icon name="zoom" size={11} className="text-primary" />
102
- </div>
103
-
104
- <Image
105
- src={firstImage.image}
106
- alt={`${product.name} 1`}
107
- draggable={false}
108
- fill
109
- aspectRatio={1}
110
- sizes="(min-width: 1024px) 50vw, 90vw"
111
- imageClassName="object-cover cursor-zoom-in"
112
- onClick={() => handleOpenZoom(firstImage.image)}
113
- />
114
- </div>
115
- )}
116
- {smallImagePairs.map((pair, pairIndex) => (
117
- <div key={pairIndex} className="grid grid-cols-2 gap-2.5">
118
- {pair.map((item, itemIndex) => (
119
- <div
120
- key={itemIndex}
121
- className="relative w-full overflow-hidden bg-gray-50 aspect-square"
122
- >
123
- <Image
124
- src={item.image}
125
- alt={`${product.name} ${pairIndex * 2 + itemIndex + 2}`}
126
- draggable={false}
127
- fill
128
- aspectRatio={1}
129
- sizes="(min-width: 1024px) 25vw, 45vw"
130
- imageClassName="object-cover cursor-zoom-in"
131
- onClick={() => handleOpenZoom(item.image)}
132
- />
133
- </div>
134
- ))}
135
- </div>
136
- ))}
137
- {trailingLargeImage && (
138
- <div className="relative w-full overflow-hidden bg-gray-50 aspect-square">
139
- <Image
140
- src={trailingLargeImage.image}
141
- alt={`${product.name} ${productImages.length}`}
142
- draggable={false}
143
- fill
144
- aspectRatio={1}
145
- sizes="(min-width: 1024px) 50vw, 90vw"
146
- imageClassName="object-cover cursor-zoom-in"
147
- onClick={() => handleOpenZoom(trailingLargeImage.image)}
148
- />
149
- </div>
150
- )}
151
- </div>
152
- {zoomedImage && (
153
- <div
154
- className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4"
155
- onClick={handleCloseZoom}
156
- >
157
- <div
158
- className="relative w-full max-w-3xl aspect-square"
159
- onClick={(event) => event.stopPropagation()}
160
- >
161
- <Button
162
- type="button"
163
- aria-label="Close zoomed image"
164
- className="absolute right-3 top-3 z-10 rounded-full bg-white/90 px-3 py-1 text-sm font-medium text-gray-900 shadow cursor-pointer"
165
- onClick={handleCloseZoom}
166
- >
167
- ×
168
- </Button>
169
- <Image
170
- src={zoomedImage}
171
- alt={`${product.name} zoomed`}
172
- draggable={false}
173
- fill
174
- aspectRatio={1}
175
- sizes="100vw"
176
- imageClassName="object-contain"
177
- />
178
- </div>
179
- </div>
180
- )}
181
- </>
143
+ </div>
182
144
  );
183
145
  }
@@ -10,9 +10,6 @@ import useFavButton from '../../hooks/use-fav-button';
10
10
  import { Product } from '@akinon/next/types';
11
11
  import { Image } from '@akinon/next/components/image';
12
12
  import { Price, Link } from '@theme/components';
13
- import { SaleTag } from '../product/sale-tag';
14
- import { useProductList } from '../category/product-list-registrar';
15
- import { useNativeWidget } from '../category/native-widget-context';
16
13
 
17
14
  interface Props {
18
15
  product: Product;
@@ -22,42 +19,17 @@ interface Props {
22
19
  }
23
20
 
24
21
  export const ProductItem = (props: Props) => {
22
+ // TODO: Static image will change (TR)
25
23
  const { product, width, height, index } = props;
26
24
  const [viewed, setViewed] = useState(false);
27
25
  const { FavButton } = useFavButton(product.pk);
28
26
  const { ref, inView } = useInView();
29
27
 
30
- // Get product-item properties from ProductListRegistrar
31
- const {
32
- productItemProperties: registrarProps,
33
- productItemStyles: registrarStyles
34
- } = useProductList();
35
-
36
- // Get product-item properties from NativeWidgetProvider
37
- const {
38
- productItemProperties: widgetProps,
39
- productItemStyles: widgetStyles
40
- } = useNativeWidget();
41
-
42
- // Designer mode uses registrar props, saved widget uses widget props
43
- const isDesigner = typeof window !== 'undefined' && window.parent !== window;
44
- const properties = isDesigner
45
- ? { ...widgetProps, ...registrarProps }
46
- : widgetProps;
47
- const styles = isDesigner
48
- ? { ...widgetStyles, ...registrarStyles }
49
- : widgetStyles;
50
-
51
- const showFavButton = properties['show-fav-button'] !== false;
52
- const favButtonPosition =
53
- (properties['fav-button-position'] as string) || 'top-right';
54
-
55
- const image_url = product.productimage_set?.[0]?.image;
28
+ const image_url = product.productimage_set[0]?.image;
56
29
  const absolute_url = product.absolute_url;
57
30
  const product_name = product.name;
58
31
  const retail_price = product.retail_price;
59
32
  const price = product.price;
60
- const isOnSale = parseFloat(String(retail_price)) > parseFloat(String(price));
61
33
 
62
34
  useEffect(() => {
63
35
  if (!viewed && inView) {
@@ -66,103 +38,58 @@ export const ProductItem = (props: Props) => {
66
38
  }
67
39
  }, [inView, viewed, product]);
68
40
 
69
- // Position classes
70
- const positionClasses = {
71
- 'top-left': 'top-2.5 left-2.5',
72
- 'top-right': 'top-2.5 right-2.5',
73
- 'bottom-left': 'bottom-2.5 left-2.5',
74
- 'bottom-right': 'bottom-2.5 right-2.5'
75
- };
76
-
77
- const favButtonStyles: React.CSSProperties = {
78
- width: (styles['fav-button-size'] as string) || '32px',
79
- height: (styles['fav-button-size'] as string) || '32px',
80
- backgroundColor:
81
- (styles['fav-button-bg-color'] as string) || 'rgba(255, 255, 255, 0.8)',
82
- color: (styles['fav-button-icon-color'] as string) || '#000000',
83
- borderRadius: (styles['fav-button-border-radius'] as string) || '50%'
84
- };
85
-
86
- const productNameStyles: React.CSSProperties = {
87
- fontSize: (styles['product-name-font-size'] as string) || '14px',
88
- color: (styles['product-name-color'] as string) || '#1a1a1a',
89
- fontWeight: (styles['product-name-font-weight'] as string) || '400',
90
- display: '-webkit-box',
91
- WebkitLineClamp: parseInt(
92
- (styles['product-name-line-clamp'] as string) || '2',
93
- 10
94
- ),
95
- WebkitBoxOrient: 'vertical' as const,
96
- overflow: 'hidden'
97
- };
98
-
99
- const priceStyles: React.CSSProperties = {
100
- fontSize: (styles['price-font-size'] as string) || '16px',
101
- color: (styles['price-color'] as string) || '#1a1a1a',
102
- fontWeight: (styles['price-font-weight'] as string) || '400'
103
- };
104
-
105
- const oldPriceStyles: React.CSSProperties = {
106
- fontSize: (styles['old-price-font-size'] as string) || '14px',
107
- color: (styles['old-price-color'] as string) || '#6b7280'
108
- };
109
-
110
41
  return (
111
42
  <div
112
- className="flex flex-col group w-full"
43
+ className="text-sm text-left flex flex-col justify-between"
113
44
  data-testid="product-box"
114
45
  ref={ref}
115
- data-section-id="product-item-section"
116
46
  >
117
- <div className="relative mb-2 w-full aspect-square">
47
+ <div className="relative mb-3 h-full">
118
48
  <Link href={absolute_url} onClick={() => pushProductClicked(product)}>
119
- <Image
120
- loading="lazy"
121
- src={image_url || '/noimage.jpg'}
122
- alt={product_name}
123
- aspectRatio={width / height}
124
- fill
125
- crop="center"
126
- sizes={`(max-width: 768px) ${
127
- width === 260 ? 170 : width
128
- }px, ${width}px`}
129
- imageClassName="object-cover"
130
- />
49
+ {image_url ? (
50
+ <Image
51
+ fill
52
+ loading="lazy"
53
+ src={image_url}
54
+ alt={product_name}
55
+ aspectRatio={width / height}
56
+ sizes="
57
+ (max-width: 768px) 50vw,
58
+ (max-width: 1024px) 30vw,
59
+ 33vw"
60
+ crop="center"
61
+ />
62
+ ) : (
63
+ <Image
64
+ className="h-full"
65
+ src="/noimage.jpg"
66
+ fill
67
+ aspectRatio={width / height}
68
+ sizes="100vw"
69
+ alt={product_name}
70
+ imageClassName="object-cover"
71
+ />
72
+ )}
131
73
  </Link>
132
-
133
- {showFavButton && (
134
- <FavButton
135
- className={`absolute ${positionClasses[favButtonPosition]} hover:opacity-100 transition-all flex items-center justify-center z-10`}
136
- style={favButtonStyles}
137
- />
138
- )}
139
-
140
- {isOnSale && (
141
- <div className="absolute bottom-4 left-2 z-10">
142
- <SaleTag />
143
- </div>
144
- )}
74
+ <FavButton className="absolute top-4 right-4" />
145
75
  </div>
146
-
147
- <Link
148
- href={absolute_url}
149
- data-testid={`${product_name}-${index}`}
150
- onClick={() => pushProductClicked(product)}
151
- className="hover:underline"
152
- style={productNameStyles}
153
- >
154
- {product_name}
155
- </Link>
156
-
157
- <div className="flex items-center gap-2">
158
- {isOnSale && (
159
- <Price
160
- value={retail_price}
161
- className="line-through"
162
- style={oldPriceStyles}
163
- />
164
- )}
165
- <Price value={price} data-testid="product-price" style={priceStyles} />
76
+ <div>
77
+ <Link
78
+ href={absolute_url}
79
+ data-testid={`${product_name}-${index}`}
80
+ onClick={() => pushProductClicked(product)}
81
+ >
82
+ {product_name}
83
+ </Link>
84
+ <div className="font-semibold">
85
+ {parseFloat(retail_price) > parseFloat(price) && (
86
+ <Price
87
+ value={retail_price}
88
+ className="font-normal line-through mr-3"
89
+ />
90
+ )}
91
+ <Price value={price} data-testid="product-price" />
92
+ </div>
166
93
  </div>
167
94
  </div>
168
95
  );