@riosst100/pwa-marketplace 2.9.7 → 3.0.0

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 (19) hide show
  1. package/package.json +1 -1
  2. package/src/componentOverrideMapping.js +2 -1
  3. package/src/components/FilterContent/filterContent.js +4 -0
  4. package/src/components/RMAPage/RMADetail.js +1 -1
  5. package/src/components/ShopBy/shopBy.js +4 -1
  6. package/src/overwrites/peregrine/lib/talons/CartPage/PriceSummary/priceSummaryFragments.gql.js +54 -0
  7. package/src/overwrites/peregrine/lib/talons/CartPage/PriceSummary/usePriceSummary.js +2 -4
  8. package/src/overwrites/peregrine/lib/talons/ProductFullDetail/productReview.gql.js +89 -0
  9. package/src/overwrites/peregrine/lib/talons/ProductFullDetail/useProductFullDetail.js +72 -3
  10. package/src/overwrites/peregrine/lib/talons/RootComponents/Category/categoryContent.gql.js +5 -1
  11. package/src/overwrites/peregrine/lib/talons/RootComponents/Category/useCategoryContent.js +2 -1
  12. package/src/overwrites/venia-ui/lib/RootComponents/Category/categoryContent.js +4 -3
  13. package/src/overwrites/venia-ui/lib/components/CartPage/PriceSummary/priceSummary.js +97 -23
  14. package/src/overwrites/venia-ui/lib/components/FilterModal/FilterList/filterList.js +0 -2
  15. package/src/overwrites/venia-ui/lib/components/FilterSidebar/filterSidebar.js +29 -0
  16. package/src/overwrites/venia-ui/lib/components/FilterSidebar/filterSidebar.module.css +1 -1
  17. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/components/modalFormReview.js +102 -95
  18. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/components/productReview.js +111 -70
  19. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +19 -3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@riosst100/pwa-marketplace",
3
3
  "author": "riosst100@gmail.com",
4
- "version": "2.9.7",
4
+ "version": "3.0.0",
5
5
  "main": "src/index.js",
6
6
  "pwa-studio": {
7
7
  "targets": {
@@ -1,4 +1,6 @@
1
1
  module.exports = componentOverrideMapping = {
2
+ [`@magento/venia-ui/lib/components/CartPage/PriceSummary/priceSummary.js`]: '@riosst100/pwa-marketplace/src/overwrites/venia-ui/lib/components/CartPage/PriceSummary/priceSummary.js',
3
+ [`@magento/peregrine/lib/talons/CartPage/PriceSummary/priceSummaryFragments.gql.js`]: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/CartPage/PriceSummary/priceSummaryFragments.gql.js',
2
4
  [`@magento/venia-ui/lib/components/Adapter/adapter.js`]: '@riosst100/pwa-marketplace/src/overwrites/venia-ui/lib/components/Adapter/adapter.js',
3
5
  [`@magento/venia-ui/lib/components/ToastContainer/toast.module.css`]: '@riosst100/pwa-marketplace/src/overwrites/venia-ui/lib/components/ToastContainer/toast.module.css',
4
6
  [`@magento/venia-ui/lib/components/ToastContainer/toastContainer.module.css`]: '@riosst100/pwa-marketplace/src/overwrites/venia-ui/lib/components/ToastContainer/toastContainer.module.css',
@@ -87,7 +89,6 @@ module.exports = componentOverrideMapping = {
87
89
  [`@magento/peregrine/lib/talons/ProductOptions/useTile.js`]: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/ProductOptions/useTile.js',
88
90
  [`@magento/peregrine/lib/talons/ProductImageCarousel/useProductImageCarousel.js`]: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/ProductImageCarousel/useProductImageCarousel.js',
89
91
  [`@magento/peregrine/lib/talons/OrderHistoryPage/orderHistoryPage.gql.js`]: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/OrderHistoryPage/orderHistoryPage.gql.js',
90
- // Added overrides to fix TypeError in useOrderRow and to ensure required GQL queries are present
91
92
  [`@magento/peregrine/lib/talons/OrderHistoryPage/useOrderRow.js`]: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/OrderHistoryPage/useOrderRow.js',
92
93
  [`@magento/peregrine/lib/talons/OrderHistoryPage/orderRow.gql.js`]: '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/OrderHistoryPage/orderRow.gql.js'
93
94
  };
@@ -31,6 +31,10 @@ const FilterContent = props => {
31
31
  const [searchQuery, setSearchQuery] = useState('');
32
32
 
33
33
  const { search } = useLocation();
34
+
35
+ useEffect(() => {
36
+ window.scrollTo(0, 0);
37
+ }, []);
34
38
 
35
39
  const sortProps = useCustomSort({ sortFromSearch: false, defaultSort: {
36
40
  sortText: 'All (A-Z)',
@@ -69,7 +69,7 @@ const RMADetail = () => {
69
69
  timeStamp: m.created_at,
70
70
  senderName: m.sender_name || ''
71
71
  }));
72
- return mapped.length > 0 ? mapped : dummyChat;
72
+ return mapped.length > 0 ? mapped : [];
73
73
  }, [rmaDetail]);
74
74
 
75
75
  const formatted = useMemo(() => {
@@ -1,4 +1,4 @@
1
- import React, { Fragment, Suspense, useMemo, useRef, useState } from 'react';
1
+ import React, { Fragment, useEffect, Suspense, useMemo, useRef, useState } from 'react';
2
2
  import { FormattedMessage } from 'react-intl';
3
3
  import { array, number, shape, string } from 'prop-types';
4
4
 
@@ -84,6 +84,9 @@ const ShopBy = props => {
84
84
  // attributesBlock,
85
85
  // category,
86
86
  // } = talonProps;
87
+ useEffect(() => {
88
+ window.scrollTo(0, 0);
89
+ }, []);
87
90
 
88
91
  const [active, setActive] = useState('all')
89
92
  const [activeTab, setActiveTab] = useState('all');
@@ -0,0 +1,54 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ import { DiscountSummaryFragment } from '@magento/peregrine/lib/talons/CartPage/PriceSummary/discountSummary.gql';
4
+ import { GiftCardSummaryFragment } from '@magento/peregrine/lib/talons/CartPage/PriceSummary/queries/giftCardSummary';
5
+ import { GiftOptionsSummaryFragment } from '@magento/peregrine/lib/talons/CartPage/PriceSummary/queries/giftOptionsSummary';
6
+ import { ShippingSummaryFragment } from '@magento/peregrine/lib/talons/CartPage/PriceSummary/shippingSummary.gql';
7
+ import { TaxSummaryFragment } from '@magento/peregrine/lib/talons/CartPage/PriceSummary/taxSummary.gql';
8
+
9
+ export const GrandTotalFragment = gql`
10
+ fragment GrandTotalFragment on CartPrices {
11
+ grand_total {
12
+ currency
13
+ value
14
+ }
15
+ }
16
+ `;
17
+
18
+ export const PriceSummaryFragment = gql`
19
+ fragment PriceSummaryFragment on Cart {
20
+ id
21
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
22
+ items {
23
+ uid
24
+ quantity
25
+ }
26
+ ...ShippingSummaryFragment
27
+ prices {
28
+ ...TaxSummaryFragment
29
+ ...DiscountSummaryFragment
30
+ ...GrandTotalFragment
31
+ subtotal_excluding_tax {
32
+ currency
33
+ value
34
+ }
35
+ subtotal_including_tax {
36
+ currency
37
+ value
38
+ }
39
+ }
40
+ payment_fees {
41
+ title
42
+ value
43
+ currency
44
+ }
45
+ ...GiftCardSummaryFragment
46
+ ...GiftOptionsSummaryFragment
47
+ }
48
+ ${DiscountSummaryFragment}
49
+ ${GiftCardSummaryFragment}
50
+ ${GiftOptionsSummaryFragment}
51
+ ${GrandTotalFragment}
52
+ ${ShippingSummaryFragment}
53
+ ${TaxSummaryFragment}
54
+ `;
@@ -28,7 +28,8 @@ const flattenData = data => {
28
28
  giftCards: data.cart.applied_gift_cards,
29
29
  giftOptions: data.cart.prices.gift_options,
30
30
  taxes: data.cart.prices.applied_taxes,
31
- shipping: data.cart.shipping_addresses
31
+ shipping: data.cart.shipping_addresses,
32
+ payment_fees: data.cart.payment_fees
32
33
  };
33
34
  };
34
35
 
@@ -99,9 +100,6 @@ export const usePriceSummary = (props = {}) => {
99
100
 
100
101
  await createSellerCart({ initCheckoutSplitCart, sellerUrl });
101
102
 
102
- console.log('initCheckoutSplitCartData',initCheckoutSplitCartData)
103
- console.log('fetchCartId',fetchCartId)
104
-
105
103
  await removeCart();
106
104
  await apolloClient.clearCacheData(apolloClient, 'cart');
107
105
 
@@ -0,0 +1,89 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ const GET_PRODUCT_REVIEW_RATINGS_METADATA = gql`
4
+ query getProductReviewRatingsMetadata {
5
+ productReviewRatingsMetadata {
6
+ items {
7
+ id
8
+ name
9
+ values {
10
+ value
11
+ value_id
12
+ }
13
+ }
14
+ }
15
+ }
16
+ `;
17
+
18
+ const CREATE_PRODUCT_REVIEW = gql`
19
+ mutation CreateProductReview($input: CreateProductReviewInput!) {
20
+ createProductReview(input: $input) {
21
+ review {
22
+ average_rating
23
+ created_at
24
+ nickname
25
+ product {
26
+ uuid: uid
27
+ uid
28
+ name
29
+ sku
30
+ }
31
+ ratings_breakdown {
32
+ name
33
+ value
34
+ }
35
+ summary
36
+ text
37
+ }
38
+ }
39
+ }
40
+ `;
41
+
42
+ const GET_PRODUCT_REVIEWS = gql`
43
+ query getProductReviews($url_key: String) {
44
+ products(
45
+ filter: { url_key: { eq: $url_key } }
46
+ pageSize: 20
47
+ currentPage: 1
48
+ ) {
49
+ items {
50
+ uid
51
+ rating_summary
52
+ review_count
53
+ reviews(pageSize: 20, currentPage: 1){
54
+ items {
55
+ average_rating
56
+ created_at
57
+ nickname
58
+ summary
59
+ text
60
+ product {
61
+ uid
62
+ name
63
+ sku
64
+ }
65
+ ratings_breakdown {
66
+ name
67
+ value
68
+ }
69
+ }
70
+ page_info {
71
+ total_pages
72
+ current_page
73
+ page_size
74
+ }
75
+ }
76
+ }
77
+ page_info {
78
+ total_pages
79
+ current_page
80
+ }
81
+ }
82
+ }
83
+ `;
84
+
85
+ export default {
86
+ getProductReviews: GET_PRODUCT_REVIEWS,
87
+ createProductReview: CREATE_PRODUCT_REVIEW,
88
+ getProductReviewRatingsMetadata: GET_PRODUCT_REVIEW_RATINGS_METADATA
89
+ };
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useState, useMemo } from 'react';
2
+ import { useToasts } from '@magento/peregrine/lib';
2
3
  import { useIntl } from 'react-intl';
3
4
  import { useMutation, useQuery } from '@apollo/client';
4
5
  import { useCartContext } from '@magento/peregrine/lib/context/cart';
@@ -11,6 +12,7 @@ import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/ut
11
12
  import { deriveErrorMessage } from '@magento/peregrine/lib/util/deriveErrorMessage';
12
13
  import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
13
14
  import defaultOperations from '@magento/peregrine/lib/talons/ProductFullDetail/productFullDetail.gql';
15
+ import productReviewOperations from './productReview.gql';
14
16
  import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
15
17
  import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
16
18
 
@@ -334,12 +336,60 @@ export const useProductFullDetail = props => {
334
336
  isPreview
335
337
  } = props;
336
338
 
339
+ const [, { addToast }] = useToasts();
340
+
341
+ const { data: productReviewData, loading: loadingProductReview, error: errorProductReview, refetch: refetchProductReviews } = useQuery(
342
+ productReviewOperations.getProductReviews,
343
+ {
344
+ variables: { url_key: product?.url_key },
345
+ skip: !product?.url_key,
346
+ fetchPolicy: 'network-only'
347
+ }
348
+ );
349
+
350
+ // Query for ratings metadata
351
+ const { data: ratingsMetadataData, loading: loadingRatingsMetadata } = useQuery(
352
+ productReviewOperations.getProductReviewRatingsMetadata,
353
+ { fetchPolicy: 'network-only' }
354
+ );
355
+
356
+ // Mutation for creating review (allow partial data with errors)
357
+ const [createProductReview, { loading: loadingCreateReview, error: errorCreateReview }] = useMutation(
358
+ productReviewOperations.createProductReview,
359
+ { errorPolicy: 'all' }
360
+ );
361
+
362
+ // Handler for submitting review with robust toast handling
363
+ const handleSubmitReview = async (formValues) => {
364
+ try {
365
+ const result = await createProductReview({
366
+ variables: { input: formValues },
367
+ errorPolicy: 'all'
368
+ });
369
+
370
+ const gqlErrors = result?.errors || [];
371
+ const review = result?.data?.createProductReview?.review;
372
+
373
+ if (gqlErrors.length > 0 || !review) {
374
+ const message = gqlErrors[0]?.message || 'Failed to submit review!';
375
+ addToast({ type: 'error', message });
376
+ return { success: false, error: gqlErrors };
377
+ }
378
+
379
+ await refetchProductReviews();
380
+ addToast({ type: 'success', message: 'Review submitted successfully!' });
381
+ return { success: true };
382
+ } catch (e) {
383
+ addToast({ type: 'error', message: e?.message || 'Failed to submit review!' });
384
+ return { success: false, error: e };
385
+ }
386
+ };
387
+
337
388
  const [, { dispatch }] = useEventingContext();
338
389
 
339
390
  const hasDeprecatedOperationProp = !!(
340
391
  addConfigurableProductToCartMutation || addSimpleProductToCartMutation
341
392
  );
342
-
343
393
  const operations = mergeOperations(defaultOperations, props.operations);
344
394
 
345
395
  const productType = product.__typename;
@@ -347,7 +397,7 @@ export const useProductFullDetail = props => {
347
397
  const isSupportedProductType = isSupported(productType);
348
398
 
349
399
  const [{ cartId }] = useCartContext();
350
- const [{ isSignedIn }] = useUserContext();
400
+ const [{ isSignedIn, currentUser }] = useUserContext();
351
401
  const { formatMessage } = useIntl();
352
402
 
353
403
  const { data: storeConfigData } = useQuery(
@@ -718,6 +768,16 @@ export const useProductFullDetail = props => {
718
768
  storeConfig: storeConfigData ? storeConfigData.storeConfig : {}
719
769
  };
720
770
 
771
+ const defaultNickname = useMemo(() => {
772
+ if (!currentUser) return '';
773
+ const first = currentUser.firstname || '';
774
+ const last = currentUser.lastname || '';
775
+ const full = `${first} ${last}`.trim();
776
+ if (full) return full;
777
+ const email = currentUser.email || '';
778
+ return email ? email.split('@')[0] : '';
779
+ }, [currentUser]);
780
+
721
781
  return {
722
782
  breadcrumbCategoryId,
723
783
  errorMessage: derivedErrorMessage,
@@ -744,6 +804,15 @@ export const useProductFullDetail = props => {
744
804
  customAttributes,
745
805
  wishlistButtonProps,
746
806
  wishlistItemOptions,
747
- sellerDetails
807
+ sellerDetails,
808
+ productReviewData,
809
+ loadingProductReview,
810
+ errorProductReview,
811
+ ratingsMetadataData,
812
+ loadingRatingsMetadata,
813
+ handleSubmitReview,
814
+ loadingCreateReview,
815
+ errorCreateReview,
816
+ defaultNickname
748
817
  };
749
818
  };
@@ -3,8 +3,12 @@ import { gql } from '@apollo/client';
3
3
  export const GET_PRODUCT_FILTERS_BY_CATEGORY = gql`
4
4
  query getProductFiltersByCategory(
5
5
  $filters: ProductAttributeFilterInput!
6
+ $category_uid: String
6
7
  ) {
7
- products(filter: $filters) {
8
+ products(
9
+ filter: $filters
10
+ category_uid: $category_uid
11
+ ) {
8
12
  aggregations {
9
13
  label
10
14
  count
@@ -150,7 +150,8 @@ export const useCategoryContent = props => {
150
150
 
151
151
  getFilters({
152
152
  variables: {
153
- filters: newFilters
153
+ filters: newFilters,
154
+ category_uid: categoryId
154
155
  }
155
156
  });
156
157
  }
@@ -142,9 +142,10 @@ const CategoryContent = props => {
142
142
 
143
143
  const sidebarRef = useRef(null);
144
144
  const classes = useStyle(defaultClasses, props.classes);
145
- const shouldRenderSidebarContent = useIsInViewport({
146
- elementRef: sidebarRef
147
- });
145
+ // const shouldRenderSidebarContent = useIsInViewport({
146
+ // elementRef: sidebarRef
147
+ // });
148
+ const shouldRenderSidebarContent = true;
148
149
 
149
150
  const shouldShowFilterButtons = filters && filters.length;
150
151
  const shouldShowFilterShimmer = filters === null;
@@ -67,7 +67,8 @@ const PriceSummary = props => {
67
67
  giftCards,
68
68
  giftOptions,
69
69
  taxes,
70
- shipping
70
+ shipping,
71
+ payment_fees
71
72
  } = flatData;
72
73
 
73
74
  const isPriceUpdating = isUpdating || isLoading;
@@ -109,30 +110,103 @@ const PriceSummary = props => {
109
110
 
110
111
  return (
111
112
  <div className={cn(classes.root, 'pb-6 px-3')} data-cy="PriceSummary-root">
112
- {/* <div>
113
- <ul>
114
- <li className={classes.lineItems}>
115
- <span
116
- data-cy="PriceSummary-lineItemLabel"
117
- className={classes.lineItemLabel}
118
- >
119
- <FormattedMessage
120
- id={'priceSummary.lineItemLabel'}
121
- defaultMessage={'Subtotal'}
113
+ {isCheckout && (
114
+ <div>
115
+ <ul>
116
+ <li className={classes.lineItems}>
117
+ <span
118
+ data-cy="PriceSummary-lineItemLabel"
119
+ className={classes.lineItemLabel}
120
+ >
121
+ <FormattedMessage
122
+ id={'priceSummary.lineItemLabel'}
123
+ defaultMessage={'Subtotal'}
124
+ />
125
+ </span>
126
+ <span
127
+ data-cy="PriceSummary-subtotalValue"
128
+ className={priceClass}
129
+ >
130
+ <Price
131
+ value={subtotal.value}
132
+ currencyCode={subtotal.currency}
133
+ />
134
+ </span>
135
+ </li>
136
+ <DiscountSummary
137
+ classes={{
138
+ lineItems: classes.lineItems,
139
+ lineItemLabel: classes.lineItemLabel,
140
+ price: priceClass
141
+ }}
142
+ data={discounts}
143
+ />
144
+ <li className={classes.lineItems}>
145
+ <GiftCardSummary
146
+ classes={{
147
+ lineItemLabel: classes.lineItemLabel,
148
+ price: priceClass
149
+ }}
150
+ data={giftCards}
122
151
  />
123
- </span>
124
- <span
125
- data-cy="PriceSummary-subtotalValue"
126
- className={priceClass}
127
- >
128
- <Price
129
- value={subtotal.value}
130
- currencyCode={subtotal.currency}
152
+ </li>
153
+ <li className={classes.lineItems}>
154
+ <GiftOptionsSummary
155
+ classes={{
156
+ lineItemLabel: classes.lineItemLabel,
157
+ price: priceClass
158
+ }}
159
+ data={giftOptions}
131
160
  />
132
- </span>
133
- </li>
134
- </ul>
135
- </div> */}
161
+ </li>
162
+ <li className={classes.lineItems}>
163
+ <TaxSummary
164
+ classes={{
165
+ lineItemLabel: classes.lineItemLabel,
166
+ price: priceClass
167
+ }}
168
+ data={taxes}
169
+ isCheckout={isCheckout}
170
+ />
171
+ </li>
172
+ <li className={classes.lineItems}>
173
+ <ShippingSummary
174
+ classes={{
175
+ lineItemLabel: classes.lineItemLabel,
176
+ price: priceClass
177
+ }}
178
+ data={shipping}
179
+ isCheckout={isCheckout}
180
+ />
181
+ </li>
182
+ {payment_fees && payment_fees.length > 0 && payment_fees.map(fee => (
183
+ <li className={classes.lineItems} key={fee.title}>
184
+ <span className={classes.lineItemLabel}>{fee.title}</span>
185
+ <span className={priceClass}>
186
+ <Price value={fee.value} currencyCode={fee.currency} />
187
+ </span>
188
+ </li>
189
+ ))}
190
+ <li className={classes.lineItems}>
191
+ <span
192
+ data-cy="PriceSummary-totalLabel"
193
+ className={classes.totalLabel}
194
+ >
195
+ {totalPriceLabel}
196
+ </span>
197
+ <span
198
+ data-cy="PriceSummary-totalValue"
199
+ className={totalPriceClass}
200
+ >
201
+ <Price
202
+ value={total.value}
203
+ currencyCode={total.currency}
204
+ />
205
+ </span>
206
+ </li>
207
+ </ul>
208
+ </div>
209
+ )}
136
210
  {proceedToCheckoutButton}
137
211
  </div>
138
212
  );
@@ -260,8 +260,6 @@ const FilterList = props => {
260
260
  const { pathname, search } = useLocation();
261
261
 
262
262
  const showMoreItem = useMemo(() => {
263
- console.log('itemCountToShow')
264
- console.log(itemCountToShow)
265
263
  if (items.length <= itemCountToShow) {
266
264
  return null;
267
265
  }
@@ -52,6 +52,8 @@ const FilterSidebar = props => {
52
52
  // const windowScrollY =
53
53
  // window.scrollY + filterTop - SCROLL_OFFSET;
54
54
  // window.scrollTo(0, windowScrollY);
55
+
56
+ window.scrollTo(0, 0);
55
57
  }
56
58
 
57
59
  handleApply(...args);
@@ -89,6 +91,33 @@ const FilterSidebar = props => {
89
91
  allowedFiltersArr.push(val.code);
90
92
  });
91
93
 
94
+ // const ordering = ['card_artist','card_product_type', 'card_type'];
95
+ // const sorted = new Map(
96
+ // [...filterItems.entries()].sort((a, b) => {
97
+ // const keyA = a[0];
98
+ // const keyB = b[0];
99
+
100
+ // const matchA = ordering.findIndex(o => keyA.endsWith(o));
101
+ // const matchB = ordering.findIndex(o => keyB.endsWith(o));
102
+
103
+ // const orderA = matchA === -1 ? Infinity : matchA;
104
+ // const orderB = matchB === -1 ? Infinity : matchB;
105
+
106
+ // // jika dua-duanya tidak match ordering → sort alphabetis
107
+ // if (orderA === Infinity && orderB === Infinity) {
108
+ // return keyA.localeCompare(keyB);
109
+ // }
110
+
111
+ // // urutkan sesuai ordering
112
+ // return orderA - orderB;
113
+ // })
114
+ // );
115
+
116
+ // // console.log([...sorted.entries()]);
117
+ // const filterItems = [...sorted.entries()];
118
+
119
+ // console.log('filterItems',filterItems)
120
+
92
121
  const filtersList = useMemo(
93
122
  () =>
94
123
  Array.from(filterItems, ([group, items], iteration) => {
@@ -11,7 +11,7 @@
11
11
  composes: lg_block from global;
12
12
  composes: border from global;
13
13
  composes: border-gray-100 from global;
14
- composes: shadow-type-1 from global;
14
+ composes: hover_shadow-type-1 from global;
15
15
  composes: rounded-[6px] from global;
16
16
  composes: py-2.5 from global;
17
17
 
@@ -1,24 +1,46 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import Modal from '@riosst100/pwa-marketplace/src/components/Modal';
3
3
  import { X } from 'react-feather';
4
4
  import Field from '@magento/venia-ui/lib/components/Field';
5
- import TextInput from '@magento/venia-ui/lib/components/TextInput';
5
+ // import TextInput from '@magento/venia-ui/lib/components/TextInput';
6
6
  import Button from '@magento/venia-ui/lib/components/Button';
7
7
  import { isRequired } from '@magento/venia-ui/lib/util/formValidators';
8
- import { Form } from 'informed';
9
8
  import StarRating from './starInput';
10
9
 
11
10
  import { primary900 } from '@riosst100/pwa-marketplace/src/theme/vars';
12
11
 
13
12
  const modalFormReview = (props) => {
13
+ const { open, setOpen, ratingsMetadata = [], loadingRatingsMetadata, onSubmit, submitting, defaultNickname } = props;
14
14
 
15
- const { open, setOpen } = props;
16
- const [currentRating, setCurrentRating] = useState(0);
15
+ const [formState, setFormState] = useState({ nickname: defaultNickname || '' });
17
16
 
18
- const handleRatingChange = (newRating) => {
19
- setCurrentRating(newRating);
17
+ useEffect(() => {
18
+ if (open && defaultNickname && !formState.nickname) {
19
+ setFormState(prev => ({ ...prev, nickname: defaultNickname }));
20
+ }
21
+ // eslint-disable-next-line react-hooks/exhaustive-deps
22
+ }, [open, defaultNickname]);
23
+
24
+ // ratings: [{ id, value_id }]
25
+ const [ratings, setRatings] = useState([]);
26
+
27
+ const handleRatingChange = (id, value_id) => {
28
+ setRatings(prev => {
29
+ const filtered = prev.filter(r => r.id !== id);
30
+ return [...filtered, { id, value_id }];
31
+ });
20
32
  };
21
33
 
34
+ const handleChange = (field, value) => {
35
+ setFormState(prev => ({ ...prev, [field]: value }));
36
+ };
37
+
38
+ const handleSubmit = (e) => {
39
+ e.preventDefault();
40
+ if (onSubmit) {
41
+ onSubmit({ ...formState, ratings });
42
+ }
43
+ };
22
44
 
23
45
  return (
24
46
  <>
@@ -35,106 +57,88 @@ const modalFormReview = (props) => {
35
57
  <X size={24} color={primary900} />
36
58
  </button>
37
59
  </div>
38
-
39
- <Form
40
- data-cy="form_review"
41
- className="flex flex-col gap-y-3"
42
- initialValues={{}}
43
- onSubmit={() => { }}
44
- onChange={() => { }}
45
- >
46
- <Field
47
- id="nickname_field"
48
- label={'Nickname'}
49
- >
50
- <TextInput
60
+ <form className="flex flex-col gap-y-3" onSubmit={handleSubmit}>
61
+ <Field id="nickname_field" label={'Nickname'}>
62
+ <input
51
63
  id="nickname"
52
- field="nickname"
53
- validate={isRequired}
54
- validateOnBlur
55
- mask={value => value && value.trim()}
56
- maskOnBlur={true}
64
+ name="nickname"
65
+ type="text"
66
+ required
67
+ readOnly
68
+ value={formState.nickname || ''}
69
+ onChange={e => handleChange('nickname', e.target.value)}
57
70
  data-cy="nickname"
58
71
  aria-label={'nickname'}
59
72
  placeholder={'e.g John Doe'}
73
+ className="border border-gray-100 rounded px-3 py-2 bg-gray-50 cursor-not-allowed"
74
+ aria-readonly="true"
60
75
  />
61
76
  </Field>
62
-
63
- <Field
64
- id="rating_field"
65
- label={'Rating'}
66
- >
67
- <StarRating rating={currentRating} onRatingChange={handleRatingChange} />
68
- </Field>
69
-
70
- <Field
71
- id="summary_field"
72
- label={'Summary'}
73
- >
74
- <TextInput
77
+ {/* Ratings breakdown from metadata */}
78
+ <div className="flex flex-col gap-y-3">
79
+ {loadingRatingsMetadata ? (
80
+ <div>Loading ratings...</div>
81
+ ) : ratingsMetadata.length > 0 ? (
82
+ ratingsMetadata.map(rating => {
83
+ const selected = ratings.find(r => r.id === rating.id)?.value_id || '';
84
+ // Find value (1-5) for selected value_id
85
+ const selectedValue = rating.values.find(v => v.value_id === selected)?.value || '';
86
+ return (
87
+ <div key={rating.id} className="mb-2">
88
+ <label className="block mb-1 font-bold text-gray-700">{rating.name}</label>
89
+ <div className="flex items-center gap-1">
90
+ {[1,2,3,4,5].map(star => {
91
+ const valObj = rating.values.find(v => v.value === String(star));
92
+ if (!valObj) return null;
93
+ return (
94
+ <button
95
+ key={valObj.value_id}
96
+ type="button"
97
+ aria-label={`${star} star${star > 1 ? 's' : ''}`}
98
+ className={`focus:outline-none ${selectedValue == star ? 'text-yellow-400' : 'text-gray-300'}`}
99
+ onClick={() => handleRatingChange(rating.id, valObj.value_id)}
100
+ >
101
+ <svg xmlns="http://www.w3.org/2000/svg" fill={selectedValue >= star ? '#F7C317' : '#D9D9D9'} viewBox="0 0 20 20" width="15" height="15">
102
+ <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.967a1 1 0 00.95.69h4.175c.969 0 1.371 1.24.588 1.81l-3.38 2.455a1 1 0 00-.364 1.118l1.287 3.966c.3.922-.755 1.688-1.54 1.118l-3.38-2.454a1 1 0 00-1.175 0l-3.38 2.454c-.784.57-1.838-.196-1.54-1.118l1.287-3.966a1 1 0 00-.364-1.118L2.05 9.394c-.783-.57-.38-1.81.588-1.81h4.175a1 1 0 00.95-.69l1.286-3.967z" />
103
+ </svg>
104
+ </button>
105
+ );
106
+ })}
107
+ {/* <span className="ml-2 text-sm text-gray-600">{selectedValue || ''}</span> */}
108
+ </div>
109
+ </div>
110
+ );
111
+ })
112
+ ) : null}
113
+ </div>
114
+ <Field id="summary_field" label={'Summary'}>
115
+ <input
75
116
  id="summary"
76
- field="summary"
77
- validate={isRequired}
78
- validateOnBlur
79
- mask={value => value && value.trim()}
80
- maskOnBlur={true}
117
+ name="summary"
118
+ type="text"
119
+ required
120
+ value={formState.summary || ''}
121
+ onChange={e => handleChange('summary', e.target.value)}
81
122
  data-cy="summary"
82
123
  aria-label={'summary'}
83
124
  placeholder={'Summary of your rating'}
125
+ className="border border-gray-100 rounded px-3 py-2"
84
126
  />
85
127
  </Field>
86
-
87
- <Field
88
- id="review_field"
89
- label={'Review'}
90
- >
91
- <TextInput
128
+ <Field id="review_field" label={'Review'}>
129
+ <textarea
92
130
  id="review"
93
- field="review"
94
- validate={isRequired}
95
- validateOnBlur
96
- mask={value => value && value.trim()}
97
- maskOnBlur={true}
131
+ name="review"
132
+ required
133
+ value={formState.review || ''}
134
+ onChange={e => handleChange('review', e.target.value)}
98
135
  data-cy="review"
99
136
  aria-label={'review'}
100
- placeholder={'Let us know your thougts'}
101
- />
102
- </Field>
103
-
104
- <Field
105
- id="like_reason_field"
106
- label={'I like about'}
107
- >
108
- <TextInput
109
- id="like_reason"
110
- field="like_reason"
111
- validate={isRequired}
112
- validateOnBlur
113
- mask={value => value && value.trim()}
114
- maskOnBlur={true}
115
- data-cy="like_reason"
116
- aria-label={'like_reason'}
117
- placeholder={'Summary of your rating'}
137
+ placeholder={'Let us know your thoughts'}
138
+ className="border border-gray-100 rounded px-3 py-2"
139
+ rows={3}
118
140
  />
119
141
  </Field>
120
-
121
- <Field
122
- id="dont_like_reason_field"
123
- label={"I dont't like about"}
124
- >
125
- <TextInput
126
- id="dont_like_reason"
127
- field="dont_like_reason"
128
- validate={isRequired}
129
- validateOnBlur
130
- mask={value => value && value.trim()}
131
- maskOnBlur={true}
132
- data-cy="dont_like_reason"
133
- aria-label={'dont_like_reason'}
134
- placeholder={'Summary of your rating'}
135
- />
136
- </Field>
137
-
138
142
  <div className='actions flex justify-end gap-x-2.5 mt-4'>
139
143
  <Button
140
144
  priority='low'
@@ -142,6 +146,7 @@ const modalFormReview = (props) => {
142
146
  content: 'capitalize text-[16px] font-medium'
143
147
  }}
144
148
  onClick={() => setOpen(false)}
149
+ type="button"
145
150
  >
146
151
  Cancel
147
152
  </Button>
@@ -150,15 +155,17 @@ const modalFormReview = (props) => {
150
155
  classes={{
151
156
  content: 'capitalize text-[16px] font-medium'
152
157
  }}
158
+ type="submit"
159
+ disabled={submitting}
153
160
  >
154
- Submit Review
161
+ {submitting ? 'Submitting...' : 'Submit Review'}
155
162
  </Button>
156
163
  </div>
157
- </Form>
164
+ </form>
158
165
  </div>
159
166
  </Modal>
160
167
  </>
161
- )
162
- }
168
+ );
169
+ };
163
170
 
164
- export default modalFormReview
171
+ export default modalFormReview;
@@ -4,98 +4,139 @@ import { Star1 } from 'iconsax-react';
4
4
  import Button from '../../Button';
5
5
  import ModalFormReview from './modalFormReview';
6
6
 
7
+
8
+
7
9
  const productReview = (props) => {
10
+ const {
11
+ className,
12
+ productReviewData,
13
+ loadingProductReview,
14
+ errorProductReview,
15
+ ratingsMetadataData,
16
+ loadingRatingsMetadata,
17
+ handleSubmitReview,
18
+ defaultNickname,
19
+ product
20
+ } = props;
8
21
 
9
- const { className } = props;
10
22
  const [open, setOpen] = useState(false);
11
23
  const [filter, setFilter] = useState('All');
24
+ const [submitting, setSubmitting] = useState(false);
12
25
 
13
-
14
- // Dummy reviews data
15
- const dummyReviews = {
16
- __typename: 'ProductRates',
17
- total_count: 6,
18
- items: [
19
- {
20
- __typename: 'ProductRate',
21
- id: 1,
22
- name: 'John Doe',
23
- date: '18 January 2024',
24
- rating: 5,
25
- comment: 'Got item at a great price. Arrived way quicker than expected, extremely well packaged and exactly as described. Highly recommend the seller.'
26
- },
27
- {
28
- __typename: 'ProductRate',
29
- id: 2,
30
- name: 'Roger Taylor',
31
- date: '25 January 2024',
32
- rating: 2,
33
- comment: 'Arrived late and packaging was damaged. Not satisfied.'
34
- },
35
- {
36
- __typename: 'ProductRate',
37
- id: 3,
38
- name: 'Sarah Smith',
39
- date: '02 February 2024',
40
- rating: 4,
41
- comment: 'Good product, but delivery could be faster.'
42
- },
43
- {
26
+ let reviewsData = null;
27
+ if (
28
+ productReviewData &&
29
+ productReviewData.products &&
30
+ productReviewData.products.items &&
31
+ productReviewData.products.items.length > 0
32
+ ) {
33
+ const item = productReviewData.products.items[0];
34
+ const reviewItems = (item.reviews && item.reviews.items) || [];
35
+ reviewsData = {
36
+ __typename: 'ProductRates',
37
+ total_count: item.review_count || reviewItems.length,
38
+ items: reviewItems.map((r, idx) => ({
44
39
  __typename: 'ProductRate',
45
- id: 4,
46
- name: 'Michael Johnson',
47
- date: '10 February 2024',
48
- rating: 3,
49
- comment: 'Average experience, item as described but nothing special.'
50
- },
51
- {
52
- __typename: 'ProductRate',
53
- id: 5,
54
- name: 'Emily Davis',
55
- date: '15 February 2024',
56
- rating: 5,
57
- comment: 'Excellent service and product quality! Will buy again.'
58
- },
59
- {
60
- __typename: 'ProductRate',
61
- id: 6,
62
- name: 'David Lee',
63
- date: '20 February 2024',
64
- rating: 1,
65
- comment: 'Item not as described. Very disappointed.'
40
+ id: idx + 1,
41
+ name: r.nickname,
42
+ date: r.created_at,
43
+ rating: r.average_rating ? Math.round(r.average_rating / 20) : (r.ratings_breakdown && r.ratings_breakdown[0] ? parseInt(r.ratings_breakdown[0].value) : 0),
44
+ comment: r.text || r.summary || '',
45
+ summary: r.summary || '',
46
+ productName: r.product?.name || '',
47
+ ratings_breakdown: r.ratings_breakdown || []
48
+ })),
49
+ page_info: productReviewData.products.page_info || {
50
+ total_pages: 1,
51
+ current_page: 1
66
52
  }
67
- ],
68
- page_info: {
69
- __typename: 'SearchResultPageInfo',
70
- total_pages: 1,
71
- page_size: 10,
72
- current_page: 1,
73
- total_count: 6
74
- }
75
- };
53
+ };
54
+ }
76
55
 
56
+ if (!reviewsData || !reviewsData.items.length) {
57
+ return (
58
+ <>
59
+ <ModalFormReview
60
+ open={open}
61
+ setOpen={setOpen}
62
+ defaultNickname={defaultNickname}
63
+ ratingsMetadata={ratingsMetadataData?.productReviewRatingsMetadata?.items || []}
64
+ loadingRatingsMetadata={loadingRatingsMetadata}
65
+ onSubmit={async (formValues) => {
66
+ setSubmitting(true);
67
+ const input = {
68
+ nickname: formValues.nickname,
69
+ summary: formValues.summary,
70
+ text: formValues.review,
71
+ sku: product?.sku,
72
+ ratings: formValues.ratings
73
+ };
74
+ const result = await handleSubmitReview(input);
75
+ setSubmitting(false);
76
+ if (result.success) setOpen(false);
77
+ }}
78
+ submitting={submitting}
79
+ />
80
+ <div className={className}>
81
+ <div className="flex items-center justify-between mb-6">
82
+ <div />
83
+ <Button
84
+ priority='low'
85
+ classes={{
86
+ content: 'normal-case font-normal text-base'
87
+ }}
88
+ onClick={() => setOpen(true)}
89
+ >
90
+ Write a review
91
+ </Button>
92
+ </div>
93
+ <div className="text-center py-8 text-gray-500">No reviews yet.</div>
94
+ </div>
95
+ </>
96
+ );
97
+ }
77
98
 
78
- const totalReviews = dummyReviews.items.length;
99
+ const totalReviews = reviewsData.items.length;
79
100
  const averageRating = totalReviews > 0
80
- ? (dummyReviews.items.reduce((sum, item) => sum + item.rating, 0) / totalReviews).toFixed(1)
101
+ ? (reviewsData.items.reduce((sum, item) => sum + item.rating, 0) / totalReviews).toFixed(1)
81
102
  : 0;
82
103
 
83
104
  const starCounts = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 };
84
- dummyReviews.items.forEach(item => {
105
+ reviewsData.items.forEach(item => {
85
106
  if (starCounts[item.rating] !== undefined) {
86
107
  starCounts[item.rating]++;
87
108
  }
88
109
  });
89
110
 
90
111
  const getPercent = (count) => totalReviews > 0 ? Math.round((count / totalReviews) * 100) : 0;
91
-
112
+
92
113
  const filteredReviews = filter === 'All'
93
- ? dummyReviews.items
94
- : dummyReviews.items.filter(item => item.rating === parseInt(filter));
114
+ ? reviewsData.items
115
+ : reviewsData.items.filter(item => item.rating === parseInt(filter));
95
116
 
96
117
  return (
97
118
  <>
98
- <ModalFormReview open={open} setOpen={setOpen} />
119
+ <ModalFormReview
120
+ open={open}
121
+ setOpen={setOpen}
122
+ defaultNickname={defaultNickname}
123
+ ratingsMetadata={ratingsMetadataData?.productReviewRatingsMetadata?.items || []}
124
+ loadingRatingsMetadata={loadingRatingsMetadata}
125
+ onSubmit={async (formValues) => {
126
+ setSubmitting(true);
127
+ const input = {
128
+ nickname: formValues.nickname,
129
+ summary: formValues.summary,
130
+ text: formValues.review,
131
+ sku: product?.sku,
132
+ ratings: formValues.ratings
133
+ };
134
+ const result = await handleSubmitReview(input);
135
+ setSubmitting(false);
136
+ if (result.success) setOpen(false);
137
+ }}
138
+ submitting={submitting}
139
+ />
99
140
  <div className={className}>
100
141
  <div className="w-full flex items-start xs_flex-col lg_flex-row gap-[30px]">
101
142
  <div className="w-full xs_max-w-full lg_max-w-[365px] border border-[#E6E9EA] rounded-md p-6">
@@ -167,7 +208,7 @@ const productReview = (props) => {
167
208
  {/* Reviews List */}
168
209
  <div className='space-y-4 mb-6'>
169
210
  <Review reviews={{
170
- ...dummyReviews,
211
+ ...reviewsData,
171
212
  items: filteredReviews
172
213
  }} />
173
214
  </div>
@@ -57,7 +57,6 @@ const ERROR_FIELD_TO_MESSAGE_MAPPING = {
57
57
 
58
58
  const ProductDetailsCollapsible = (props) => {
59
59
  const { data } = props;
60
-
61
60
  return (
62
61
  <>
63
62
  {data.map((_data) => (
@@ -99,7 +98,14 @@ const ProductFullDetail = props => {
99
98
  productDetails,
100
99
  customAttributes,
101
100
  wishlistButtonProps,
102
- sellerDetails
101
+ sellerDetails,
102
+ productReviewData,
103
+ loadingProductReview,
104
+ errorProductReview,
105
+ ratingsMetadataData,
106
+ loadingRatingsMetadata,
107
+ handleSubmitReview,
108
+ defaultNickname
103
109
  } = talonProps;
104
110
 
105
111
  const [, { addToast }] = useToasts();
@@ -493,7 +499,17 @@ const ProductFullDetail = props => {
493
499
  {
494
500
  id: 'product-reviews',
495
501
  title: 'Reviews',
496
- content: <ProductReviews className={cn(contentContainerClass, classes.contentContainerTabOverride)} />
502
+ content: <ProductReviews
503
+ className={cn(contentContainerClass, classes.contentContainerTabOverride)}
504
+ productReviewData={productReviewData}
505
+ loadingProductReview={loadingProductReview}
506
+ errorProductReview={errorProductReview}
507
+ ratingsMetadataData={ratingsMetadataData}
508
+ loadingRatingsMetadata={loadingRatingsMetadata}
509
+ handleSubmitReview={handleSubmitReview}
510
+ defaultNickname={defaultNickname}
511
+ product={product}
512
+ />
497
513
  }
498
514
  ];
499
515