@riosst100/pwa-marketplace 3.0.4 → 3.0.6

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/componentOverrideMapping.js +1 -0
  3. package/src/components/CrossSeller/item.js +3 -4
  4. package/src/components/LinkToOtherStores/index.js +4 -4
  5. package/src/components/ProductListTab/productListTab.js +1 -1
  6. package/src/components/commons/Select/index.js +8 -4
  7. package/src/overwrites/peregrine/lib/talons/CartPage/PriceSummary/priceSummaryFragments.gql.js +3 -0
  8. package/src/overwrites/peregrine/lib/talons/CartPage/ProductListing/productListingFragments.gql.js +4 -0
  9. package/src/overwrites/peregrine/lib/talons/CheckoutPage/PaymentInformation/usePaymentMethods.js +84 -0
  10. package/src/overwrites/peregrine/lib/talons/CheckoutPage/checkoutPage.extended.gql.js +20 -1
  11. package/src/overwrites/peregrine/lib/talons/FilterSidebar/useFilterSidebar.js +3 -3
  12. package/src/overwrites/peregrine/lib/talons/ProductFullDetail/useProductFullDetail.js +7 -8
  13. package/src/overwrites/peregrine/lib/talons/RootComponents/Product/productDetailFragment.gql.js +5 -0
  14. package/src/overwrites/venia-ui/lib/components/Breadcrumbs/breadcrumbs.js +20 -1
  15. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListing/product.js +41 -1
  16. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListing/product.module.css +1 -1
  17. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/productListingBySeller.js +41 -8
  18. package/src/overwrites/venia-ui/lib/components/CheckoutPage/ItemsReview/item.js +43 -2
  19. package/src/overwrites/venia-ui/lib/components/CheckoutPage/ItemsReview/item.module.css +36 -0
  20. package/src/overwrites/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.js +8 -2
  21. package/src/overwrites/venia-ui/lib/components/CheckoutPage/checkoutPage.js +7 -2
  22. package/src/overwrites/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilters.js +1 -1
  23. package/src/overwrites/venia-ui/lib/components/FilterSidebar/filterSidebar.js +1 -1
  24. package/src/overwrites/venia-ui/lib/components/Gallery/item.js +2 -10
  25. package/src/overwrites/venia-ui/lib/components/Gallery/item.module.css +5 -4
  26. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/CustomAttributes/customAttributes.js +10 -2
  27. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/components/preOrderDetail.js +195 -37
  28. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +194 -63
  29. package/src/talons/ProductContent/productContent.gql.js +11 -1
  30. package/src/talons/ProductContent/useProductContent.js +14 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@riosst100/pwa-marketplace",
3
3
  "author": "riosst100@gmail.com",
4
- "version": "3.0.4",
4
+ "version": "3.0.6",
5
5
  "main": "src/index.js",
6
6
  "pwa-studio": {
7
7
  "targets": {
@@ -1,4 +1,5 @@
1
1
  module.exports = componentOverrideMapping = {
2
+ ['@magento/peregrine/lib/talons/CheckoutPage/PaymentInformation/usePaymentMethods.js']: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/CheckoutPage/PaymentInformation/usePaymentMethods.js',
2
3
  ['@magento/venia-ui/lib/components/CartPage/PriceAdjustments/CouponCode/couponCode.js']: '@riosst100/pwa-marketplace/src/overwrites/venia-ui/lib/components/CartPage/PriceAdjustments/CouponCode/couponCode.js',
3
4
  ['@magento/peregrine/lib/talons/Header/useCartTrigger.js']: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/Header/useCartTrigger.js',
4
5
  ['@magento/peregrine/lib/talons/CartPage/useCartPage.js']: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/CartPage/useCartPage.js',
@@ -9,7 +9,7 @@ import logoImage from './logo_seller.png';
9
9
 
10
10
  const Item = ({ sellerName, sales, price, condition }) => {
11
11
  return (
12
- <div className='flex flex-col items-center text-center lg_text-left md_text-left lg_flex-row md_flex-row bg-white rounded-lg border border-gray-100 p-[15px] gap-y-4'>
12
+ <div className='flex flex-col items-center text-center justify-around lg_text-left md_text-left lg_flex-row md_flex-row bg-white rounded-lg border border-gray-100 p-[15px] gap-y-4'>
13
13
  <div className='seller_info-container flex flex-row gap-x-[15px] lg_w-1/3 md_w-1/3'>
14
14
  <div className='flex flex-col gap-y-[15px]'>
15
15
  <div className='seller_summary-wrapper flex flex-row gap-x-2.5 items-center'>
@@ -41,16 +41,15 @@ const Item = ({ sellerName, sales, price, condition }) => {
41
41
  onSubmit={() => { }}
42
42
  className="flex flex-col gap-x-7 gap-y-7 items-center lg_flex-row md_flex-row"
43
43
  >
44
- <QuantityStepper min={1} />
45
44
  <Button
46
- data-cy="addToCartButton"
45
+ data-cy="viewProductButton"
47
46
  classes={{
48
47
  content: 'normal-case font-medium text-[16px]'
49
48
  }}
50
49
  priority="high"
51
50
  type="button"
52
51
  >
53
- Add To Cart
52
+ View Product
54
53
  </Button>
55
54
  </Form>
56
55
  </div>
@@ -18,10 +18,10 @@ const LinkToOtherStores = (props) => {
18
18
  'product_link': 'https://tokopedia.com',
19
19
  'stores': 'Tokopedia'
20
20
  },
21
- {
22
- 'product_link': 'https://lazada.sg',
23
- 'stores': 'Lazada'
24
- }
21
+ // {
22
+ // 'product_link': 'https://lazada.sg',
23
+ // 'stores': 'Lazada'
24
+ // }
25
25
  ];
26
26
 
27
27
  const getLogo = (stores) => {
@@ -37,7 +37,7 @@ const ProductListTab = props => {
37
37
  },
38
38
  {
39
39
  'label': 'Pre Order',
40
- 'value': 'lof_preorder'
40
+ 'value': 'card_pre_orders'
41
41
  },
42
42
  // {
43
43
  // 'label': 'Auction',
@@ -7,7 +7,9 @@ const Select = (props) => {
7
7
  className,
8
8
  options = [],
9
9
  value,
10
- onChange
10
+ onChange,
11
+ showPlaceholder = true,
12
+ placeholder = 'Choose Option'
11
13
  } = props;
12
14
 
13
15
  return (
@@ -17,9 +19,11 @@ const Select = (props) => {
17
19
  value={value}
18
20
  onChange={onChange}
19
21
  >
20
- <option value=''>
21
- Choose Option
22
- </option>
22
+ {showPlaceholder && (
23
+ <option value=''>
24
+ {placeholder}
25
+ </option>
26
+ )}
23
27
  {options.map((item) => (
24
28
  <option key={item.value} value={item.value}>
25
29
  {item.label}
@@ -22,6 +22,9 @@ export const PriceSummaryFragment = gql`
22
22
  items {
23
23
  uid
24
24
  quantity
25
+ is_preorder
26
+ pre_order_payment_type
27
+ pre_order_deposit
25
28
  }
26
29
  ...ShippingSummaryFragment
27
30
  prices {
@@ -6,6 +6,9 @@ export const ProductListingFragment = gql`
6
6
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
7
7
  items {
8
8
  uid
9
+ is_preorder
10
+ pre_order_payment_type
11
+ pre_order_deposit
9
12
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
10
13
  seller {
11
14
  seller_name
@@ -17,6 +20,7 @@ export const ProductListingFragment = gql`
17
20
  name
18
21
  sku
19
22
  url_key
23
+ release_date
20
24
  thumbnail {
21
25
  url
22
26
  }
@@ -0,0 +1,84 @@
1
+ import { useCallback } from 'react';
2
+ import { useMutation, useQuery } from '@apollo/client';
3
+ import useFieldState from '@magento/peregrine/lib/hooks/hook-wrappers/useInformedFieldStateWrapper';
4
+ import DEFAULT_OPERATIONS from '@magento/peregrine/lib/talons/CheckoutPage/PaymentInformation/paymentMethods.gql';
5
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
6
+
7
+ import { useCartContext } from '@magento/peregrine/lib/context/cart';
8
+
9
+ export const usePaymentMethods = props => {
10
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
11
+ const {
12
+ getPaymentMethodsQuery,
13
+ setPaymentMethodOnCartMutation
14
+ } = operations;
15
+
16
+ const [setPaymentMethod] = useMutation(setPaymentMethodOnCartMutation);
17
+
18
+ const [{ cartId }] = useCartContext();
19
+
20
+ const { data, loading } = useQuery(getPaymentMethodsQuery, {
21
+ skip: !cartId,
22
+ variables: { cartId }
23
+ });
24
+
25
+ const { value: currentSelectedPaymentMethod } = useFieldState(
26
+ 'selectedPaymentMethod'
27
+ );
28
+
29
+ const availablePaymentMethods =
30
+ (data && data.cart.available_payment_methods) || [];
31
+
32
+ // If there is one payment method, select it by default.
33
+ // If more than one, none should be selected by default.
34
+ const defaultPaymentCode =
35
+ (availablePaymentMethods.length && availablePaymentMethods[0].code) ||
36
+ null;
37
+ const selectedPaymentCode =
38
+ (data && data.cart.selected_payment_method.code) || null;
39
+
40
+ const initialSelectedMethod =
41
+ availablePaymentMethods.length > 1
42
+ ? selectedPaymentCode
43
+ : null;
44
+
45
+ console.log('availablePaymentMethods.length',availablePaymentMethods.length)
46
+ console.log('defaultPaymentCode',defaultPaymentCode)
47
+ console.log('selectedPaymentCode',selectedPaymentCode)
48
+ console.log('initialSelectedMethod',initialSelectedMethod)
49
+
50
+ const handlePaymentMethodSelection = useCallback(
51
+ element => {
52
+ const value = element.target.value;
53
+
54
+ const paymentMethodData =
55
+ value == 'braintree'
56
+ ? {
57
+ code: value,
58
+ braintree: {
59
+ payment_method_nonce: value,
60
+ is_active_payment_token_enabler: false
61
+ }
62
+ }
63
+ : {
64
+ code: value
65
+ };
66
+
67
+ setPaymentMethod({
68
+ variables: {
69
+ cartId,
70
+ paymentMethod: paymentMethodData
71
+ }
72
+ });
73
+ },
74
+ [cartId, setPaymentMethod]
75
+ );
76
+
77
+ return {
78
+ availablePaymentMethods,
79
+ currentSelectedPaymentMethod,
80
+ handlePaymentMethodSelection,
81
+ initialSelectedMethod,
82
+ isLoading: loading
83
+ };
84
+ };
@@ -13,6 +13,20 @@ export const ITEM_SELLER_FRAGMENT = gql`
13
13
  }
14
14
  `;
15
15
 
16
+ // Extend items to include preorder fields used in ItemsReview/item.js overwrite
17
+ export const ITEM_PREORDER_FRAGMENT = gql`
18
+ fragment ItemPreorderFragment on Cart {
19
+ items {
20
+ is_preorder
21
+ pre_order_payment_type
22
+ pre_order_deposit
23
+ product {
24
+ release_date
25
+ }
26
+ }
27
+ }
28
+ `;
29
+
16
30
  export const CREATE_CART = gql`
17
31
  mutation createCart {
18
32
  cartId: createEmptyCart
@@ -64,10 +78,12 @@ export const GET_ORDER_DETAILS = gql`
64
78
  }
65
79
  ...ItemsReviewFragment
66
80
  ...ItemSellerFragment
81
+ ...ItemPreorderFragment
67
82
  }
68
83
  }
69
84
  ${ItemsReviewFragment}
70
85
  ${ITEM_SELLER_FRAGMENT}
86
+ ${ITEM_PREORDER_FRAGMENT}
71
87
  `;
72
88
 
73
89
  export const GET_CHECKOUT_DETAILS = gql`
@@ -77,11 +93,13 @@ export const GET_CHECKOUT_DETAILS = gql`
77
93
  ...CheckoutPageFragment
78
94
  ...ItemsReviewFragment
79
95
  ...ItemSellerFragment
96
+ ...ItemPreorderFragment
80
97
  }
81
98
  }
82
99
  ${CheckoutPageFragment}
83
100
  ${ItemsReviewFragment}
84
101
  ${ITEM_SELLER_FRAGMENT}
102
+ ${ITEM_PREORDER_FRAGMENT}
85
103
  `;
86
104
 
87
105
  export const GET_CUSTOMER = gql`
@@ -100,5 +118,6 @@ export default {
100
118
  getCustomerQuery: GET_CUSTOMER,
101
119
  getOrderDetailsQuery: GET_ORDER_DETAILS,
102
120
  placeOrderMutation: PLACE_ORDER,
103
- itemSellerFragment: ITEM_SELLER_FRAGMENT
121
+ itemSellerFragment: ITEM_SELLER_FRAGMENT,
122
+ itemPreorderFragment: ITEM_PREORDER_FRAGMENT
104
123
  };
@@ -189,11 +189,11 @@ export const useFilterSidebar = props => {
189
189
  },
190
190
  {
191
191
  'label': 'Preorder',
192
- 'value': 'lof_preorder',
192
+ 'value': 'card_pre_orders',
193
193
  'path': '',
194
194
  'options': [
195
- {'value':'0','label':'No','title':'No'},
196
- {'value':'1','label':'Yes','title':'Yes'}
195
+ {'value':'0','label':'','title':''},
196
+ {'value':'1','label':'Pre Orders','title':'Pre Orders'}
197
197
  ]
198
198
  },
199
199
  {
@@ -579,7 +579,7 @@ export const useProductFullDetail = props => {
579
579
 
580
580
  const handleAddToCart = useCallback(
581
581
  async formValues => {
582
- const { quantity } = formValues;
582
+ const { quantity, preorder } = formValues;
583
583
 
584
584
  /*
585
585
  @deprecated in favor of general addProductsToCart mutation. Will support until the next MAJOR.
@@ -636,15 +636,13 @@ export const useProductFullDetail = props => {
636
636
  product: {
637
637
  sku: product.sku,
638
638
  quantity
639
- },
640
- entered_options: [
641
- {
642
- uid: product.uid,
643
- value: product.name
644
- }
645
- ]
639
+ }
646
640
  };
647
641
 
642
+ if (preorder && Object.keys(preorder).length) {
643
+ variables.product.preorder = preorder;
644
+ }
645
+
648
646
  if (selectedOptionsArray.length) {
649
647
  variables.product.selected_options = selectedOptionsArray;
650
648
  }
@@ -721,6 +719,7 @@ export const useProductFullDetail = props => {
721
719
  price_range: product?.price_range,
722
720
  sku: productSku,
723
721
  publish_status: product.publish_status,
722
+ custom_table_metadata: product.custom_table_metadata,
724
723
  term_and_conditions: product.term_and_conditions,
725
724
  link_to_other_stores: product.link_to_other_stores,
726
725
  shipping_policy: product.shipping_policy,
@@ -116,6 +116,11 @@ export const ProductDetailsFragment = gql`
116
116
  pre_order_date
117
117
  }
118
118
  term_and_conditions
119
+ custom_table_metadata {
120
+ mainAttrCode
121
+ mainAttrVal
122
+ mainAttrLabel
123
+ }
119
124
  shipping_policy
120
125
  return_policy
121
126
  small_image {
@@ -22,7 +22,7 @@ import { useLocation } from 'react-router-dom';
22
22
  const Breadcrumbs = props => {
23
23
  const classes = useStyle(defaultClasses, props.classes);
24
24
 
25
- const { categoryId, currentProduct, customPage, currentFilter } = props;
25
+ const { categoryId, currentProduct, customPage, currentFilter, currentProductCustomTableMetadata, customAttributesDetails } = props;
26
26
 
27
27
  const talonProps = useBreadcrumbs({ categoryId });
28
28
 
@@ -103,6 +103,25 @@ const Breadcrumbs = props => {
103
103
 
104
104
  const params = removeShopbyParam(search);
105
105
 
106
+ if (currentProductCustomTableMetadata) {
107
+ const mainAttrCode = currentProductCustomTableMetadata.mainAttrCode;
108
+ const mainAttrVal = currentProductCustomTableMetadata.mainAttrVal;
109
+ const mainAttrLabel = currentProductCustomTableMetadata.mainAttrLabel;
110
+
111
+ filterBreadcrumbsElement.push(
112
+ <Fragment>
113
+ <span className={classes.divider}>{DELIMITER}</span>
114
+ <Link
115
+ className={classes.link}
116
+ to={resourceUrl('/' + currentCategoryPath + '?' + mainAttrCode + '[filter]=' + mainAttrLabel + ',' + mainAttrVal)}
117
+ onClick={handleClick}
118
+ >
119
+ {mainAttrLabel}
120
+ </Link>
121
+ </Fragment>
122
+ )
123
+ }
124
+
106
125
  currentFilter && currentFilter.length && currentFilter.map((filter, index) => {
107
126
  currentProduct ? (
108
127
  filterBreadcrumbsElement.push(
@@ -30,7 +30,6 @@ const HeartIcon = <Heart size="16" color="#909090" />;
30
30
 
31
31
  const Product = props => {
32
32
  const { item } = props;
33
-
34
33
  const { formatMessage } = useIntl();
35
34
  const talonProps = useProduct({
36
35
  operations: {
@@ -123,8 +122,49 @@ const Product = props => {
123
122
  </Link>
124
123
  <div className='flex flex-col'>
125
124
  <div className={cn(classes.name, 'text-[14px] font-normal max-w-[260px]')} data-cy="Product-name">
125
+ {item.is_preorder && (
126
+ <span className="inline-flex items-center gap-1 px-2 py-[2px] mb-3 rounded-full bg-blue-600 text-white border border-blue-700 text-[11px] font-medium">
127
+ Preorder
128
+ </span>
129
+ )}
130
+ <br/>
126
131
  <Link to={itemLink}>{name}</Link>
127
132
  </div>
133
+ {item.is_preorder && (
134
+ <div className="mt-2 flex flex-col gap-1" aria-label="Preorder information">
135
+ <div className="flex flex-wrap items-center gap-2">
136
+ <span className="text-[11px] text-amber-800">
137
+ Release: {(() => {
138
+ const rd = item?.product?.release_date;
139
+ if (!rd) return '-';
140
+ const d = new Date(rd);
141
+ if (isNaN(d.getTime())) return rd;
142
+ const dd = String(d.getDate()).padStart(2, '0');
143
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
144
+ const yyyy = d.getFullYear();
145
+ return `${dd}/${mm}/${yyyy}`;
146
+ })()}
147
+ </span>
148
+ {item.pre_order_payment_type === 'deposit' && item.pre_order_deposit != null && (
149
+ <span className="text-[11px] text-amber-800">
150
+ {(() => {
151
+ const raw = item.pre_order_deposit;
152
+ const n = typeof raw === 'number' ? raw : Number.parseFloat(String(raw).replace(/[^0-9.-]/g,''));
153
+ if (!Number.isFinite(n)) {
154
+ return `Deposit: ${raw}`;
155
+ }
156
+ let str = n.toString();
157
+ if (str.includes('.')) {
158
+ str = str.replace(/\.0+$/,'');
159
+ str = str.replace(/\.(\d*[1-9])0+$/,'.$1');
160
+ }
161
+ return `Deposit: ${str}%`;
162
+ })()}
163
+ </span>
164
+ )}
165
+ </div>
166
+ </div>
167
+ )}
128
168
  <ProductOptions
129
169
  options={options}
130
170
  classes={{
@@ -71,7 +71,7 @@
71
71
  /* composes: font-medium from global; */
72
72
  grid-area: name;
73
73
  display: -webkit-box;
74
- -webkit-line-clamp: 2;
74
+ -webkit-line-clamp: 3;
75
75
  -webkit-box-orient: vertical;
76
76
  overflow: hidden;
77
77
  line-height: normal;
@@ -80,7 +80,46 @@ const ProductListingBySeller = props => {
80
80
  wishlistConfig={wishlistConfig}
81
81
  />
82
82
  ));
83
-
83
+ // Helpers for currency + subtotal formatting
84
+ const aggregateSubtotal = items => {
85
+ let total = 0;
86
+ let currency = null;
87
+ for (const it of items) {
88
+ const priceObj = it?.prices?.price;
89
+ if (priceObj) {
90
+ if (!currency) currency = priceObj.currency;
91
+ const unit = Number(priceObj.value) || 0;
92
+ const qty = Number(it?.quantity) || 0;
93
+ total += unit * qty;
94
+ }
95
+ }
96
+ return { total, currency };
97
+ };
98
+
99
+ const formatMoney = (value, currency) => {
100
+ if (value == null || !isFinite(value)) return '-';
101
+ if (!currency) return value.toFixed(2);
102
+ try {
103
+ const locale = currency === 'IDR' ? 'id-ID' : (currency === 'SGD' ? 'en-SG' : 'en-US');
104
+ const opts = currency === 'IDR'
105
+ ? { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: 0 }
106
+ : { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 };
107
+ let formatted = new Intl.NumberFormat(locale, opts).format(value);
108
+ if (currency === 'SGD') {
109
+ formatted = 'SGD ' + formatted
110
+ .replace(/^S?\$/,'')
111
+ .replace(/^SGD\s*/,'')
112
+ .trim();
113
+ }
114
+ return formatted;
115
+ } catch (e) {
116
+ return currency + ' ' + value.toFixed(2);
117
+ }
118
+ };
119
+
120
+ const { total: subtotalValue, currency: detectedCurrency } = aggregateSubtotal(items);
121
+ const displayCurrency = detectedCurrency || seller.seller_currency || 'SGD';
122
+ const formattedSubtotal = formatMoney(subtotalValue, displayCurrency);
84
123
  const priceSummary = hasItems ? (
85
124
  <PriceSummary isProceedToCheckout={isProceedToCheckout} setIsProceedToCheckout={setIsProceedToCheckout} setIsCartUpdating={setIsCartUpdating} isUpdating={isCartUpdating} sellerUrl={seller.seller_url} />
86
125
  ) : null;
@@ -103,13 +142,7 @@ const ProductListingBySeller = props => {
103
142
  {productComponents}
104
143
  <div className={classes.subtotalContainer}>
105
144
  <div className={classes.subtotalLabel}>Subtotal:</div>
106
- <div className={classes.subtotalAmount}>
107
- {seller.seller_currency || 'SGD'} {items.reduce((total, item) => {
108
- const itemPrice = item?.prices?.price?.value || 0;
109
- const quantity = item?.quantity || 0;
110
- return total + (itemPrice * quantity);
111
- }, 0).toFixed(2)}
112
- </div>
145
+ <div className={classes.subtotalAmount}>{formattedSubtotal}</div>
113
146
  </div>
114
147
  <div className={classes.summary_container}>
115
148
  <div className={classes.summary_contents}>
@@ -15,11 +15,35 @@ const Item = props => {
15
15
  quantity,
16
16
  configurable_options,
17
17
  isHidden,
18
- configurableThumbnailSource
18
+ configurableThumbnailSource,
19
+ // Preorder-related fields passed via {...item}
20
+ is_preorder,
21
+ pre_order_payment_type,
22
+ pre_order_deposit,
23
+ release_date
19
24
  } = props;
20
25
  const classes = useStyle(defaultClasses, propClasses);
21
26
  const className = isHidden ? classes.root_hidden : classes.root_visible;
22
27
  const configured_variant = configuredVariant(configurable_options, product);
28
+ const formatDate = value => {
29
+ if (!value) return '-';
30
+ const d = value instanceof Date ? value : new Date(value);
31
+ if (Number.isNaN(d.getTime())) return '-';
32
+ const dd = String(d.getDate()).padStart(2, '0');
33
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
34
+ const yyyy = d.getFullYear();
35
+ return `${dd}/${mm}/${yyyy}`;
36
+ };
37
+ const formatDepositPercent = raw => {
38
+ const n = typeof raw === 'number' ? raw : Number.parseFloat(String(raw).replace(/[^0-9.-]/g,''));
39
+ if (!Number.isFinite(n)) return raw;
40
+ let str = n.toString();
41
+ if (str.includes('.')) {
42
+ str = str.replace(/\.0+$/,'');
43
+ str = str.replace(/\.(\d*[1-9])0+$/,'.$1');
44
+ }
45
+ return `${str}%`;
46
+ };
23
47
  return (
24
48
  <div className={className}>
25
49
  <Image
@@ -33,7 +57,24 @@ const Item = props => {
33
57
  : product.thumbnail.url
34
58
  }
35
59
  />
36
- <span className={classes.name}>{product.name}</span>
60
+ <div className={classes.headerRow}>
61
+ {is_preorder && (
62
+ <span className={classes.preorderBadge}>Preorder</span>
63
+ )}
64
+ <span className={classes.name}>{product.name}</span>
65
+ </div>
66
+ {is_preorder && (
67
+ <div className={classes.preorderMeta} aria-label="Preorder information">
68
+ <span className={classes.metaItem}>
69
+ Release: {formatDate(typeof release_date !== 'undefined' ? release_date : product?.release_date)}
70
+ </span>
71
+ {pre_order_payment_type === 'deposit' && pre_order_deposit != null && (
72
+ <span className={classes.metaItem}>
73
+ Deposit: {formatDepositPercent(pre_order_deposit)}
74
+ </span>
75
+ )}
76
+ </div>
77
+ )}
37
78
  <ProductOptions
38
79
  options={configurable_options}
39
80
  classes={{
@@ -63,3 +63,39 @@
63
63
  grid-column: 2 / span 1;
64
64
  grid-row: 2 / span 1;
65
65
  }
66
+
67
+ .headerRow {
68
+ grid-column: 2 / span 1;
69
+ grid-row: 1 / span 1;
70
+ align-items: center;
71
+ gap: 8px;
72
+ }
73
+
74
+ .preorderBadge {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ gap: 4px;
78
+ padding: 2px 8px;
79
+ border-radius: 9999px;
80
+ background: #f26313; /* orange accent for visibility */
81
+ color: #fff;
82
+ border: 1px solid #f26313;
83
+ font-size: 10px;
84
+ font-weight: 600;
85
+ margin-bottom: 5px;
86
+ }
87
+
88
+ .preorderMeta {
89
+ grid-column: 2 / span 1;
90
+ grid-row: 2 / span 1;
91
+ margin-top: 4px;
92
+ display: flex;
93
+ flex-wrap: wrap;
94
+ gap: 12px;
95
+ color: #000; /* amber-ish */
96
+ font-size: 11px;
97
+ }
98
+
99
+ .metaItem {
100
+ display: inline-block;
101
+ }
@@ -15,8 +15,8 @@ import defaultClasses from './itemsReview.module.css';
15
15
  * @param {Object} props.data an optional static data object to render instead of making a query for data.
16
16
  */
17
17
  const ItemsReview = props => {
18
- const { classes: propClasses } = props;
19
-
18
+ const { classes: propClasses, isPreorder, preOrderDeposit, preOrderPaymentType, releaseDate } = props;
19
+
20
20
  const classes = useStyle(defaultClasses, propClasses);
21
21
 
22
22
  const talonProps = useItemsReview({
@@ -38,6 +38,12 @@ const ItemsReview = props => {
38
38
  {...item}
39
39
  isHidden={!showAllItems && index >= 2}
40
40
  configurableThumbnailSource={configurableThumbnailSource}
41
+ // Forward explicit preorder values from parent if provided
42
+ is_preorder={typeof isPreorder !== 'undefined' ? isPreorder : item.is_preorder}
43
+ pre_order_payment_type={typeof preOrderPaymentType !== 'undefined' ? preOrderPaymentType : item.pre_order_payment_type}
44
+ pre_order_deposit={typeof preOrderDeposit !== 'undefined' ? preOrderDeposit : item.pre_order_deposit}
45
+ // release_date may come from parent or item.product
46
+ release_date={typeof releaseDate !== 'undefined' ? releaseDate : (item?.product?.release_date)}
41
47
  />
42
48
  ));
43
49
 
@@ -84,7 +84,7 @@ const CheckoutPage = props => {
84
84
  toggleAddressBookContent,
85
85
  toggleSignInContent
86
86
  } = talonProps;
87
-
87
+
88
88
  const [, { addToast }] = useToasts();
89
89
 
90
90
  const history = useHistory();
@@ -301,7 +301,12 @@ const CheckoutPage = props => {
301
301
  // Show ItemsReview from the beginning, not only at the REVIEW step
302
302
  const itemsReview = (
303
303
  <div className={classes.items_review_container}>
304
- <ItemsReview />
304
+ <ItemsReview
305
+ isPreorder={cartItems?.[0]?.is_preorder}
306
+ preOrderDeposit={cartItems?.[0]?.pre_order_deposit}
307
+ preOrderPaymentType={cartItems?.[0]?.pre_order_payment_type}
308
+ releaseDate={cartItems?.[0]?.product?.release_date}
309
+ />
305
310
  </div>
306
311
  );
307
312