@riosst100/pwa-marketplace 3.3.0 → 3.3.1

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 (22) hide show
  1. package/package.json +1 -1
  2. package/src/components/AgeVerification/ageVerification.js +121 -0
  3. package/src/components/AgeVerification/index.js +1 -2
  4. package/src/components/AgeVerificationModal/ageVerificationModal.js +83 -0
  5. package/src/components/{AgeVerification → AgeVerificationModal}/ageVerificationModal.module.css +77 -10
  6. package/src/components/AgeVerificationModal/index.js +2 -0
  7. package/src/components/Messages/messages.js +0 -10
  8. package/src/components/RFQPage/quoteDetail.js +2 -3
  9. package/src/components/ShowMore/showMore.js +8 -5
  10. package/src/intercept.js +9 -0
  11. package/src/overwrites/peregrine/lib/talons/RootComponents/Category/useCategory.js +0 -4
  12. package/src/overwrites/venia-ui/lib/RootComponents/Category/category.js +2 -0
  13. package/src/overwrites/venia-ui/lib/RootComponents/Category/categoryContent.js +1 -1
  14. package/src/overwrites/venia-ui/lib/RootComponents/Product/product.js +2 -0
  15. package/src/overwrites/venia-ui/lib/components/Adapter/adapter.js +0 -23
  16. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +2 -3
  17. package/src/talons/AgeVerification/ageVerification.gql.js +31 -0
  18. package/src/talons/AgeVerification/useAgeVerification.js +126 -0
  19. package/src/components/AgeVerification/ageVerificationModal.js +0 -163
  20. package/src/components/AgeVerification/sellerCoupon.js +0 -119
  21. package/src/components/AgeVerification/sellerCouponCheckout.js +0 -164
  22. /package/src/components/{AgeVerification → AgeVerificationModal}/ageVerificationModal.shimmer.js +0 -0
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.3.0",
4
+ "version": "3.3.1",
5
5
  "main": "src/index.js",
6
6
  "pwa-studio": {
7
7
  "targets": {
@@ -0,0 +1,121 @@
1
+ import React, { useMemo, useState, useEffect, useCallback } from 'react';
2
+ import AgeVerificationModal from '@riosst100/pwa-marketplace/src/components/AgeVerificationModal';
3
+ import { useHistory, useLocation } from 'react-router-dom';
4
+ import { useToasts } from '@magento/peregrine/lib/Toasts';
5
+ import { BrowserPersistence } from '@magento/peregrine/lib/util';
6
+ import { useAgeVerification } from '@riosst100/pwa-marketplace/src/talons/AgeVerification/useAgeVerification'
7
+
8
+ const storage = new BrowserPersistence();
9
+
10
+ const AgeVerification = (props) => {
11
+ const { pageType, product } = props;
12
+
13
+ const [modalOpen, setModalOpen] = useState(false);
14
+
15
+ const [month, setMonth] = useState("");
16
+ const [day, setDay] = useState("");
17
+ const [year, setYear] = useState("");
18
+ const [ageVerificationErrorMessage, setAgeVerificationErrorMessage] = useState("");
19
+
20
+ const history = useHistory();
21
+ const location = useLocation();
22
+
23
+ const [, { addToast }] = useToasts();
24
+
25
+ const handleClose = () => setModalOpen(false);
26
+
27
+ const {
28
+ error,
29
+ loading,
30
+ ageVerificationConfig
31
+ } = useAgeVerification({ pageType, product });
32
+
33
+ const months = [
34
+ "January","February","March","April","May","June",
35
+ "July","August","September","October","November","December"
36
+ ];
37
+
38
+ const savedAge = storage.getItem('age');
39
+
40
+ useEffect(() => {
41
+ if (ageVerificationConfig && ageVerificationConfig.status) {
42
+ if (savedAge && savedAge > 18) {
43
+ setModalOpen(false);
44
+ } else {
45
+ setModalOpen(true);
46
+ }
47
+ }
48
+ }, [loading, setModalOpen, savedAge, ageVerificationConfig]);
49
+
50
+ const redirectUser = () => {
51
+ const currentPathname = location.pathname;
52
+ let historyPathname = history.location.pathname;
53
+ if (historyPathname == currentPathname) {
54
+ historyPathname = '/';
55
+ }
56
+
57
+ addToast({
58
+ type: 'error',
59
+ message: 'Access is restricted because you are under 18 years old.',
60
+ dismissable: true,
61
+ timeout: 5000
62
+ });
63
+
64
+ history.push(historyPathname)
65
+ };
66
+
67
+ const handleUnderAge = useCallback(() => {
68
+ redirectUser();
69
+ }, [])
70
+
71
+ const handleOverAge = useCallback(() => {
72
+ if (!day || !month || !year) {
73
+ setAgeVerificationErrorMessage('Please enter your birthday!');
74
+ return false;
75
+ }
76
+
77
+ const birthDate = new Date(
78
+ year,
79
+ months.indexOf(month),
80
+ day
81
+ );
82
+ const today = new Date();
83
+ const age = today.getFullYear() - birthDate.getFullYear();
84
+ const isBirthdayPassed =
85
+ today >= new Date(today.getFullYear(), birthDate.getMonth(), birthDate.getDate());
86
+
87
+ if (age > 18 || (age === 18 && isBirthdayPassed)) {
88
+ storage.setItem('age', age);
89
+
90
+ addToast({
91
+ type: 'success',
92
+ message: 'Thank you for confirming your age. Your birthday has been saved. You may now continue.',
93
+ dismissable: true,
94
+ timeout: 5000
95
+ });
96
+
97
+ handleClose();
98
+ return true;
99
+ }
100
+
101
+ redirectUser();
102
+ return;
103
+ }, [day, month, year])
104
+
105
+
106
+
107
+ return modalOpen ? (
108
+ <AgeVerificationModal
109
+ ageVerificationErrorMessage={ageVerificationErrorMessage}
110
+ modalOpen={modalOpen}
111
+ months={months}
112
+ setMonth={setMonth}
113
+ setDay={setDay}
114
+ setYear={setYear}
115
+ handleOverAge={handleOverAge}
116
+ handleUnderAge={handleUnderAge}
117
+ />
118
+ ) : ''
119
+ };
120
+
121
+ export default AgeVerification;
@@ -1,2 +1 @@
1
- export { default } from './ageVerificationModal';
2
- // export { default as AgeVerificationModalShimmer } from './ageVerificationModal.shimmer';
1
+ export { default } from './ageVerification';
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import modalClasses from './ageVerificationModal.module.css';
4
+ import Icon from '@magento/venia-ui/lib/components/Icon';
5
+ import { AlertTriangle } from 'react-feather';
6
+
7
+
8
+ // Simple portal root fallback
9
+ const getPortalRoot = () => {
10
+ let root = document.getElementById('age-verification-portal');
11
+ if (!root) {
12
+ root = document.createElement('div');
13
+ root.id = 'age-verification-portal';
14
+ document.body.appendChild(root);
15
+ }
16
+ return root;
17
+ };
18
+
19
+ const AgeVerificationModal = props => {
20
+
21
+ const {
22
+ setMonth,
23
+ setDay,
24
+ setYear,
25
+ handleOverAge,
26
+ handleUnderAge,
27
+ ageVerificationErrorMessage,
28
+ modalOpen,
29
+ months
30
+ } = props;
31
+
32
+ const currentYear = new Date().getFullYear();
33
+ const years = Array.from({ length: 100 }, (_, i) => currentYear - i);
34
+ const days = Array.from({ length: 31 }, (_, i) => i + 1);
35
+
36
+ const modal = modalOpen ? (
37
+ <div className={modalClasses.overlay} role="dialog" aria-modal="true" aria-label="Modal">
38
+ <div className={modalClasses.backdrop} />
39
+ <div className={modalClasses.dialog}>
40
+ <div className={modalClasses.body}>
41
+ <div><Icon src={AlertTriangle} attrs={{ width: 20 }} /></div>
42
+ <h2 className={modalClasses.title}>Are you over 18 years old?</h2>
43
+ <p>You must be at least 18 years old to access this page. Please verify your age.</p>
44
+ <label>Your Birthday</label>
45
+ <div className={modalClasses.birthday}>
46
+ {/* Month */}
47
+ <select onChange={(e) => setMonth(e.target.value)}>
48
+ <option value="">Month</option>
49
+ {months.map((m) => (
50
+ <option key={m} value={m}>{m}</option>
51
+ ))}
52
+ </select>
53
+
54
+ {/* Day */}
55
+ <select onChange={(e) => setDay(e.target.value)}>
56
+ <option value="">Day</option>
57
+ {days.map((d) => (
58
+ <option key={d} value={d}>{d}</option>
59
+ ))}
60
+ </select>
61
+
62
+ {/* Year */}
63
+ <select onChange={(e) => setYear(e.target.value)}>
64
+ <option value="">Year</option>
65
+ {years.map((y) => (
66
+ <option key={y} value={y}>{y}</option>
67
+ ))}
68
+ </select>
69
+ </div>
70
+ {ageVerificationErrorMessage && <div>{ageVerificationErrorMessage}</div>}
71
+ <div className={modalClasses.actions}>
72
+ <button className={modalClasses.btnUnder} onClick={handleUnderAge}>I'm under 18 years old</button>
73
+ <button className={modalClasses.btnOver} onClick={handleOverAge}>I'm 18 years old</button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ ) : null;
79
+
80
+ return <>{modalOpen ? ReactDOM.createPortal(modal, getPortalRoot()) : null}</>;
81
+ };
82
+
83
+ export default AgeVerificationModal;
@@ -1,26 +1,89 @@
1
+ .modal label {
2
+ display: block;
3
+ font-size: 14px;
4
+ font-weight: bold;
5
+ margin-bottom: 8px;
6
+ text-align: left;
7
+ }
8
+
9
+ /* Birthday selects */
10
+ .birthday {
11
+ display: flex;
12
+ gap: 10px;
13
+ margin-bottom: 20px;
14
+ }
15
+
16
+ .birthday select {
17
+ flex: 1;
18
+ padding: 8px;
19
+ border-radius: 6px;
20
+ border: 1px solid #ccc;
21
+ }
22
+
23
+ /* Buttons */
24
+ .actions {
25
+ display: flex;
26
+ gap: 10px;
27
+ }
28
+
29
+ .btnUnder {
30
+ flex: 1;
31
+ padding: 10px;
32
+ border: none;
33
+ border-radius: 6px;
34
+ color: white;
35
+ font-size: 14px;
36
+ cursor: pointer;
37
+ }
38
+
39
+ .btnOver {
40
+ flex: 1;
41
+ padding: 10px;
42
+ border: none;
43
+ border-radius: 6px;
44
+ color: white;
45
+ font-size: 14px;
46
+ cursor: pointer;
47
+ }
48
+
49
+ .btnUnder {
50
+ background: #ef4444;
51
+ }
52
+
53
+ .btnOver {
54
+ background: #22c55e;
55
+ }
56
+
57
+ .btnOver:hover {
58
+ opacity: 0.9;
59
+ }
60
+
61
+ .btnUnder:hover {
62
+ opacity: 0.9;
63
+ }
64
+
1
65
  .overlay{
2
66
  position:fixed;
3
67
  inset:0;
4
- z-index:1000;
5
68
  display:flex;
6
- align-items:flex-start;
7
- justify-content:center;
8
69
  padding:5vh 24px;
9
- font-family:inherit
70
+ font-family:inherit;
71
+ align-items: center;
72
+ justify-content: center;
73
+ z-index: 9999;
10
74
  }
11
75
  .backdrop{
12
76
  position:absolute;
13
77
  inset:0;
14
- background:rgba(0,0,0,.4);
15
- backdrop-filter:blur(2px)
78
+ background: rgba(0, 0, 0, 0.64);
79
+ backdrop-filter: blur(8px);
16
80
  }
17
81
  .dialog{
18
82
  position:relative;
19
83
  background:#fff;
20
84
  border-radius:12px;
21
85
  box-shadow:0 8px 32px rgba(0,0,0,.25);
22
- width:clamp(320px,80vw,370px);
23
- max-height:90vh;
86
+ width:clamp(320px,80vw,500px);
24
87
  display:flex;
25
88
  flex-direction:column
26
89
  }
@@ -52,8 +115,12 @@
52
115
  color:#222
53
116
  }
54
117
  .body{
55
- padding:16px 20px;
56
- overflow:auto
118
+ padding: 20px 50px;
119
+ overflow: auto;
120
+ display: flex;
121
+ flex-direction: column;
122
+ text-align: center;
123
+ flex-wrap: wrap;
57
124
  }
58
125
  .triggerBtn{
59
126
  background:#f76b1c;
@@ -0,0 +1,2 @@
1
+ export { default } from './ageVerificationModal';
2
+ // export { default as AgeVerificationModalShimmer } from './ageVerificationModal.shimmer';
@@ -20,7 +20,6 @@ const Messages = props => {
20
20
  onSend,
21
21
  loading } = props;
22
22
  const classes = useStyle(defaultClasses, props.classes);
23
- // const { becomeSellerProps } = useMessages(props);
24
23
  const { formatMessage } = useIntl();
25
24
 
26
25
  const [selectedThread, setSelectedThread] = useState(null);
@@ -69,20 +68,11 @@ const Messages = props => {
69
68
  const { location } = globalThis;
70
69
 
71
70
  const query = new URLSearchParams(location.search);
72
-
73
-
74
- // console.log('query',query)
75
- // console.log('messageId',messageId)
76
71
 
77
72
  useEffect(() => {
78
73
  const messageId = query.get('id') || null;
79
- console.log('query',query)
80
- console.log('messageId',messageId)
81
-
82
74
  if (messageId && sortedThreads?.length) {
83
75
  const selectedThread = sortedThreads.find(t => t.message_id == messageId);
84
- console.log('sortedThreads',sortedThreads)
85
- console.log('selectedThread',selectedThread)
86
76
  if (selectedThread) {
87
77
  console.log('selectedThread',selectedThread)
88
78
  setSelectedThread(selectedThread);
@@ -26,12 +26,11 @@ const quoteDetail = () => {
26
26
  const { fetchCartId, addToCartResponseData, isAddProductLoading, errorAddingProductToCart, loadRfqDetail, rfqDetailState, handleSendRfqMessage, startDetailPolling, stopDetailPolling, handleConvertQuickrfqToCart } = useRFQ();
27
27
 
28
28
  const apolloClient = useApolloClient();
29
- const [{ createCart, removeCart }] = useCartContext();
29
+ const [{ cartId }, { createCart, removeCart }] = useCartContext();
30
30
 
31
31
  useEffect(async() => {
32
32
  if (
33
33
  addToCartResponseData
34
- && !addToCartResponseData?.addProductsToCart?.user_errors.length
35
34
  && !isAddProductLoading
36
35
  && !errorAddingProductToCart
37
36
  ) {
@@ -369,7 +368,7 @@ const quoteDetail = () => {
369
368
  classes={{
370
369
  content: 'capitalize text-[16px] font-medium'
371
370
  }}
372
- disabled={isConverting || !quickrfqIdValue || isNaN(quickrfqIdValue) || status === 'Done'}
371
+ // disabled={isConverting || !quickrfqIdValue || isNaN(quickrfqIdValue) || status === 'Done'}
373
372
  aria-busy={isConverting}
374
373
  onClick={handleConvertQuickrfqToCart}
375
374
  >
@@ -82,25 +82,28 @@ const ShowMore = props => {
82
82
  if (shopby == 'sc_brands') {
83
83
  additionalFilter = '&shopby=release&active_tab=brand';
84
84
  }
85
-
85
+
86
86
  const setRelases = newAvailableGroups && newAvailableGroups.length && newAvailableGroups.map((group, index) => {
87
87
  const optionsResult = [];
88
88
 
89
89
  if (active === 'all' || active === group) {
90
90
  dataResult[group] && dataResult[group].length && dataResult[group].map((option, index) => {
91
91
  const { count, label, value } = option;
92
+
93
+ const attrCodeParam = attributeData.attribute_code + '[filter]';
94
+ const attrValParam = label + ',' + value;
92
95
 
93
- const filter = attributeData.attribute_code + '[filter]=' + label + ',' + value;
94
- const params = filterSearch ? filterSearch + '&' + filter + additionalFilter : '?' + filter + additionalFilter;
96
+ const params = new URLSearchParams();
97
+ params.set(attrCodeParam, attrValParam);
95
98
 
96
99
  const categoryUrl = resourceUrl(
97
- `/${category?.url_path}${categoryUrlSuffix || ''}${params}`
100
+ `/${category?.url_path}${categoryUrlSuffix || ''}${'?' + params.toString()}`
98
101
  );
99
102
 
100
103
  optionsResult.push(<li key={index} className='list-none'>
101
104
  <Link to={categoryUrl} className="hover_bg-darkblue-900 hover_text-white w-full block text-[14px] py-[2px] px-0">
102
105
  {label}
103
- {/* <span className='text-[12px]'>({count})</span> */}
106
+ {/* <span className='text-[12px]'>({count})</span> */}
104
107
  </Link>
105
108
  </li>)
106
109
  });
package/src/intercept.js CHANGED
@@ -185,6 +185,7 @@ module.exports = targets => {
185
185
  pattern: "/favorite-seller",
186
186
  path: require.resolve("./components/FavoriteSellerPage/index.js"),
187
187
  authed: true,
188
+ redirectTo: "/sign-in"
188
189
  },
189
190
  {
190
191
  exact: true,
@@ -192,6 +193,7 @@ module.exports = targets => {
192
193
  pattern: "/quotes",
193
194
  path: require.resolve("./components/RFQPage/index.js"),
194
195
  authed: true,
196
+ redirectTo: "/sign-in"
195
197
  },
196
198
  {
197
199
  exact: true,
@@ -199,6 +201,7 @@ module.exports = targets => {
199
201
  pattern: "/quotes/:urlKey",
200
202
  path: require.resolve("./components/RFQPage/quoteDetailPage.js"),
201
203
  authed: true,
204
+ redirectTo: "/sign-in"
202
205
  },
203
206
  {
204
207
  exact: true,
@@ -206,6 +209,7 @@ module.exports = targets => {
206
209
  pattern: "/return",
207
210
  path: require.resolve("./components/RMAPage/index.js"),
208
211
  authed: true,
212
+ redirectTo: "/sign-in"
209
213
  },
210
214
  {
211
215
  exact: true,
@@ -213,6 +217,7 @@ module.exports = targets => {
213
217
  pattern: "/return/select",
214
218
  path: require.resolve("./components/RMAPage/RMASelectPage.js"),
215
219
  authed: true,
220
+ redirectTo: "/sign-in"
216
221
  },
217
222
  {
218
223
  exact: true,
@@ -220,6 +225,7 @@ module.exports = targets => {
220
225
  pattern: "/return/view/:urlKey",
221
226
  path: require.resolve("./components/RMAPage/RMADetailPage.js"),
222
227
  authed: true,
228
+ redirectTo: "/sign-in"
223
229
  },
224
230
  {
225
231
  exact: true,
@@ -227,6 +233,7 @@ module.exports = targets => {
227
233
  pattern: "/return/create/:urlKey",
228
234
  path: require.resolve("./components/RMAPage/RMACreatePage.js"),
229
235
  authed: true,
236
+ redirectTo: "/sign-in"
230
237
  },
231
238
  {
232
239
  exact: true,
@@ -234,6 +241,7 @@ module.exports = targets => {
234
241
  pattern: "/order-history/view/:urlKey",
235
242
  path: require.resolve("./components/OrderDetail/orderDetailPage.js"),
236
243
  authed: true,
244
+ redirectTo: "/sign-in"
237
245
  },
238
246
  {
239
247
  exact: true,
@@ -241,6 +249,7 @@ module.exports = targets => {
241
249
  pattern: "/messages",
242
250
  path: require.resolve("./components/MessagesPage/index.js"),
243
251
  authed: true,
252
+ redirectTo: "/sign-in"
244
253
  }
245
254
  ];
246
255
 
@@ -107,10 +107,6 @@ export const useCategory = props => {
107
107
  loading: introspectionLoading
108
108
  } = useQuery(getFilterInputsQuery);
109
109
 
110
- // console.log(introspectionCalled)
111
- // console.log(introspectionLoading)
112
- // console.log(introspectionData)
113
-
114
110
  // Create a type map we can reference later to ensure we pass valid args
115
111
  // to the graphql query.
116
112
  // For example: { category_id: 'FilterEqualTypeInput', price: 'FilterRangeTypeInput' }
@@ -21,6 +21,7 @@ import TrainsSets from '@riosst100/pwa-marketplace/src/components/TrainsSets/tra
21
21
  import SetsData from '@riosst100/pwa-marketplace/src/components/SetsData/setsData';
22
22
  import FilterContent from '@riosst100/pwa-marketplace/src/components/FilterContent/filterContent';
23
23
  import ShowMore from '@riosst100/pwa-marketplace/src/components/ShowMore/showMore';
24
+ import AgeVerification from '@riosst100/pwa-marketplace/src/components/AgeVerification'
24
25
 
25
26
  const MESSAGES = new Map().set(
26
27
  'NOT_FOUND',
@@ -205,6 +206,7 @@ const Category = props => {
205
206
  return (
206
207
  <Fragment>
207
208
  <Meta name="description" content={metaDescription} />
209
+ <AgeVerification pageType="CATEGORY" />
208
210
  {categoryContent}
209
211
  </Fragment>
210
212
  );
@@ -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
 
@@ -8,6 +8,7 @@ import { StoreTitle, Meta } from '@magento/venia-ui/lib/components/Head';
8
8
  import ProductFullDetail from '@magento/venia-ui/lib/components/ProductFullDetail';
9
9
  import mapProduct from '@magento/venia-ui/lib/util/mapProduct';
10
10
  import ProductShimmer from '@magento/venia-ui/lib/RootComponents/Product/product.shimmer';
11
+ import AgeVerification from '@riosst100/pwa-marketplace/src/components/AgeVerification'
11
12
 
12
13
  /*
13
14
  * As of this writing, there is no single Product query type in the M2.3 schema.
@@ -46,6 +47,7 @@ const Product = props => {
46
47
  <Fragment>
47
48
  <StoreTitle>{product.name}</StoreTitle>
48
49
  <Meta name="description" content={product.meta_description} />
50
+ <AgeVerification pageType="PRODUCT" product={product} />
49
51
  <ProductFullDetail product={product} />
50
52
  </Fragment>
51
53
  );
@@ -31,29 +31,6 @@ const Adapter = props => {
31
31
  return null;
32
32
  }
33
33
 
34
- // console.log('isMaintenance',isMaintenance)
35
-
36
- // useEffect(() => {
37
- // const fetchData = async () => {
38
- // axios.get(apiBase).then((response) => {
39
- // // setIsMaintenance(false)
40
- // }).catch((error) => {
41
- // const status = error.response.status;
42
- // if (status != 200) {
43
- // console.log('status',status)
44
- // return <MaintenancePage />;
45
- // // setIsMaintenance(true)
46
- // }
47
- // });
48
- // };
49
-
50
- // fetchData();
51
-
52
-
53
- // if (isMaintenance) {
54
- // return <MaintenancePage />;
55
- // }
56
-
57
34
  const websiteCodes = [];
58
35
  const websiteStores = useMemo(() => ({}), []);
59
36
  const storeCurrencies = useMemo(() => ({}), []);
@@ -47,8 +47,7 @@ import MessagesModal from '@riosst100/pwa-marketplace/src/components/LiveChat/Me
47
47
  import SellerOperations from '@riosst100/pwa-marketplace/src/talons/Seller/seller.gql';
48
48
 
49
49
  import { useLocation, useHistory } from 'react-router-dom';
50
-
51
- import AgeVerificationModal from '@riosst100/pwa-marketplace/src/components/AgeVerification/ageVerificationModal';
50
+ import AgeVerification from '@riosst100/pwa-marketplace/src/components/AgeVerification'
52
51
 
53
52
  import { totalListings, lowestPrice } from '@riosst100/pwa-marketplace/src/components/CrossSeller/crossSellerBuy';
54
53
 
@@ -818,7 +817,7 @@ const ProductFullDetail = props => {
818
817
  <Fragment>
819
818
  {breadcrumbs}
820
819
  {productPreviewMessages}
821
- {/* <AgeVerificationModal /> */}
820
+ <AgeVerification />
822
821
  <Form
823
822
  className={classes.root}
824
823
  data-cy="ProductFullDetail-root"
@@ -0,0 +1,31 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ export const GET_AGE_VERIFICATION_CONFIG_QUERY = gql`
4
+ query GetAgeVerificationConfig(
5
+ $attributeCode: String
6
+ $attributeOption: String
7
+ ) {
8
+ getAgeVerificationConfig(
9
+ attribute_code: $attributeCode
10
+ attribute_option: $attributeOption
11
+ ) {
12
+ status
13
+ minimum_age
14
+ }
15
+ }
16
+ `;
17
+
18
+ export const GET_CATALOG_CONFIG_DATA_QUERY = gql`
19
+ query getStoreConfigData {
20
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
21
+ storeConfig {
22
+ store_code
23
+ marketplace_menu_topfilters_attributes
24
+ }
25
+ }
26
+ `;
27
+
28
+ export default {
29
+ getAgeVerificationConfigQuery: GET_AGE_VERIFICATION_CONFIG_QUERY,
30
+ getMenuTopFiltersAttributesConfigDataQuery: GET_CATALOG_CONFIG_DATA_QUERY
31
+ };
@@ -0,0 +1,126 @@
1
+ import { useQuery, useLazyQuery } from '@apollo/client';
2
+ import { useMemo, useEffect } from 'react';
3
+
4
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
5
+ import DEFAULT_OPERATIONS from './ageVerification.gql';
6
+
7
+ export const useAgeVerification = props => {
8
+ const { pageType, product } = props;
9
+
10
+ const operations = mergeOperations(DEFAULT_OPERATIONS, null);
11
+ const { getAgeVerificationConfigQuery, getMenuTopFiltersAttributesConfigDataQuery } = operations;
12
+
13
+ const { location } = globalThis;
14
+
15
+ const { data: storeConfigData } = useQuery(getMenuTopFiltersAttributesConfigDataQuery, {
16
+ fetchPolicy: 'cache-and-network',
17
+ nextFetchPolicy: 'cache-first'
18
+ });
19
+
20
+ const activeMenuTopFilterAttribute = useMemo(() => {
21
+ if (storeConfigData) {
22
+
23
+ const menuTopFiltersAttributes = storeConfigData.storeConfig.marketplace_menu_topfilters_attributes;
24
+ if (!menuTopFiltersAttributes) {
25
+ return null;
26
+ }
27
+
28
+ const menuTopFiltersAttributeCodes = Object.values(JSON.parse(menuTopFiltersAttributes)).map(
29
+ item => item.attribute_code
30
+ );
31
+
32
+ if (!menuTopFiltersAttributeCodes) {
33
+ return null;
34
+ }
35
+
36
+ let result = null;
37
+
38
+ if (pageType == "CATEGORY") {
39
+ const params = new URLSearchParams(location.search);
40
+ if (!params.size) {
41
+ return null;
42
+ }
43
+
44
+ params.forEach((value, param) => {
45
+ const attrCode = param.replace("[filter]", "");
46
+ if (attrCode && menuTopFiltersAttributeCodes.includes(attrCode)) {
47
+ const attrOptionLabel = value.split(",")[0];
48
+ result = {
49
+ attributeCode: attrCode,
50
+ attributeOption: attrOptionLabel
51
+ };
52
+
53
+ return true;
54
+ }
55
+ });
56
+ }
57
+
58
+ if (pageType == "PRODUCT" && product) {
59
+ product.custom_attributes.map(attr => {
60
+ const attrCode = attr.attribute_metadata?.code;
61
+ if (attrCode && menuTopFiltersAttributeCodes.includes(attrCode)) {
62
+ let label = null;
63
+
64
+ // SELECT / MULTISELECT
65
+ if (attr?.selected_attribute_options?.attribute_option?.length) {
66
+ label = attr.selected_attribute_options.attribute_option.length ?
67
+ attr.selected_attribute_options.attribute_option.map(o => o.label)[0] : '';
68
+ // label = labelArr?.attributeOption?.[0] ?? "";
69
+ }
70
+
71
+ // TEXT / NUMBER
72
+ if (attr?.entered_attribute_value?.value) {
73
+ label = attr.entered_attribute_value.value;
74
+ }
75
+
76
+ result = {
77
+ attributeCode: attrCode,
78
+ attributeOption: label
79
+ };
80
+ }
81
+ });
82
+ }
83
+
84
+ return result;
85
+ }
86
+ }, [storeConfigData, pageType, product]);
87
+
88
+ const [getAgeVerificationConfig, { data, error, loading }] = useLazyQuery(
89
+ getAgeVerificationConfigQuery,
90
+ {
91
+ fetchPolicy: 'cache-and-network',
92
+ nextFetchPolicy: 'cache-first'
93
+ }
94
+ );
95
+
96
+
97
+ useEffect(() => {
98
+ if (activeMenuTopFilterAttribute) {
99
+ getAgeVerificationConfig({
100
+ variables: {
101
+ attributeCode: activeMenuTopFilterAttribute.attributeCode,
102
+ attributeOption: activeMenuTopFilterAttribute.attributeOption
103
+ }
104
+ });
105
+ }
106
+ }, [activeMenuTopFilterAttribute, getAgeVerificationConfig]);
107
+
108
+ const ageVerificationConfig = useMemo(() => {
109
+ if (!data) {
110
+ return null;
111
+ }
112
+
113
+ const getAgeVerificationConfig = data.getAgeVerificationConfig;
114
+ if (!getAgeVerificationConfig) {
115
+ return null;
116
+ }
117
+
118
+ return getAgeVerificationConfig;
119
+ }, [data]);
120
+
121
+ return {
122
+ error,
123
+ loading,
124
+ ageVerificationConfig
125
+ };
126
+ };
@@ -1,163 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import ReactDOM from 'react-dom';
3
- import modalClasses from '@riosst100/pwa-marketplace/src/components/AgeVerification/ageVerificationModal.module.css';
4
- // import { AgeVerificationModalShimmer } from '@riosst100/pwa-marketplace/src/components/SellerCoupon';
5
-
6
- // Reuse day diff logic
7
- const dayDiff = (toDate) => {
8
- if (!toDate) return null;
9
- const end = new Date(toDate);
10
- if (Number.isNaN(end.getTime())) return null;
11
- const now = new Date();
12
- const diffMs = end.setHours(23, 59, 59, 999) - now.getTime();
13
- const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
14
- return diffDays;
15
- };
16
-
17
- // Simple portal root fallback
18
- const getPortalRoot = () => {
19
- let root = document.getElementById('seller-coupon-portal');
20
- if (!root) {
21
- root = document.createElement('div');
22
- root.id = 'seller-coupon-portal';
23
- document.body.appendChild(root);
24
- }
25
- return root;
26
- };
27
-
28
- const AgeVerificationModal = ({
29
- couponData,
30
- couponError,
31
- setCouponModalOpen,
32
- couponModalOpen,
33
- couponLoading,
34
- isCopyAction,
35
- triggerLabel = 'View Seller Coupons',
36
- onSelectCoupon,
37
- onTriggerRender, // optional custom trigger renderer
38
- autoOpen = false, // auto open modal when mounted
39
- closeOnClaim = true // close after claiming
40
- }) => {
41
- // const [open, setOpen] = useState(false);
42
- const [copied, setCopied] = useState(null);
43
-
44
- const items = couponData?.sellerCoupons?.items || [];
45
-
46
- const handleOpen = () => setCouponModalOpen(true);
47
- const handleClose = () => setCouponModalOpen(false);
48
-
49
- useEffect(() => {
50
- if (!couponModalOpen) return;
51
- const onKey = (e) => {
52
- if (e.key === 'Escape') {
53
- handleClose();
54
- }
55
- };
56
- document.addEventListener('keydown', onKey);
57
- return () => document.removeEventListener('keydown', onKey);
58
- }, [couponModalOpen]);
59
-
60
- const handleClaim = async (item) => {
61
- try {
62
- if (navigator?.clipboard?.writeText) {
63
- await navigator.clipboard.writeText(item.code);
64
- }
65
- } catch (_) {
66
- /* ignore */
67
- }
68
- console.log('item.code',item.code)
69
- setCopied(item.code);
70
- setTimeout(() => setCopied(null), 2000);
71
- if (!isCopyAction) {
72
- if (onSelectCoupon) onSelectCoupon(item);
73
- if (closeOnClaim) handleClose();
74
- }
75
- };
76
-
77
- // Auto open behavior
78
- useEffect(() => {
79
- if (autoOpen && !couponModalOpen) {
80
- setCouponModalOpen(true);
81
- }
82
- }, [autoOpen, couponModalOpen]);
83
-
84
- console.log('copied',copied)
85
-
86
- const modal = couponModalOpen ? (
87
- <div className={modalClasses.overlay} role="dialog" aria-modal="true" aria-label="Seller coupons list">
88
- <div className={modalClasses.backdrop} onClick={handleClose} />
89
- <div className={modalClasses.dialog}>
90
- <div className={modalClasses.header}>
91
- <h2 className={modalClasses.title}>{isCopyAction ? 'Store Coupons' : 'Apply Coupon'}</h2>
92
- <button type="button" className={modalClasses.closeBtn} onClick={handleClose} aria-label="Close coupon modal">×</button>
93
- </div>
94
- <div className={modalClasses.body}>
95
- {/* {couponLoading && <AgeVerificationModalShimmer />} */}
96
- {!couponLoading && couponError && <p className={modalClasses.metaText}>Failed to load coupons.</p>}
97
- {!couponLoading && !couponError && !items.length && <p className={modalClasses.metaText}>No coupons available.</p>}
98
- {!couponLoading && !couponError && items.length > 0 && (
99
- <div className={modalClasses.stack}>
100
- {items.map(item => {
101
- const key = item.couponcode_id || item.coupon_id || item.code;
102
- const daysLeft = dayDiff(item.to_date);
103
- const expiryLabel = daysLeft === null
104
- ? 'No expiry'
105
- : daysLeft <= 0
106
- ? 'Ends today'
107
- : `Ends in ${daysLeft} day${daysLeft > 1 ? 's' : ''}`;
108
- const title = item.description || item.name || `Discount ${item.discount_amount || ''}`;
109
- return (
110
- <div key={key} className={modalClasses.card} role="group" aria-label={`Coupon ${item.code}`}>
111
- <div className={modalClasses.perf} aria-hidden="true" />
112
- <div className={modalClasses.content}>
113
- <div className={modalClasses.title}>{title}</div>
114
- <div className={modalClasses.subtext}>Code: <span className={modalClasses.code}>{item.code}</span></div>
115
- <div className={modalClasses.expiry}>{expiryLabel}</div>
116
- </div>
117
- <div className={modalClasses.divider} aria-hidden="true" />
118
- <div className={modalClasses.actions}>
119
- {isCopyAction ? <button
120
- type="button"
121
- className={copied === item.code ? modalClasses.claimedBtn : modalClasses.claimBtn}
122
- onClick={() => handleClaim(item)}
123
- aria-label={`Claim coupon ${item.code}`}
124
- >
125
- {copied === item.code ? 'Copied' : 'Claim'}
126
- </button> :
127
- <button
128
- type="button"
129
- className={copied === item.code ? modalClasses.claimedBtn : modalClasses.claimBtn}
130
- onClick={() => handleClaim(item)}
131
- aria-label={`Apply coupon ${item.code}`}
132
- >
133
- {copied === item.code ? 'Applied' : 'Apply'}
134
- </button>}
135
- </div>
136
- </div>
137
- );
138
- })}
139
- </div>
140
- )}
141
- </div>
142
- </div>
143
- </div>
144
- ) : null;
145
-
146
- const trigger = onTriggerRender
147
- ? onTriggerRender({ couponModalOpen, setCouponModalOpen, handleOpen })
148
- : (
149
- <button
150
- type="button"
151
- className={modalClasses.triggerBtn}
152
- onClick={handleOpen}
153
- aria-haspopup="dialog"
154
- aria-expanded={couponModalOpen}
155
- >
156
- {triggerLabel}
157
- </button>
158
- );
159
-
160
- return <>{onTriggerRender ? trigger : trigger}{couponModalOpen ? ReactDOM.createPortal(modal, getPortalRoot()) : null}</>;
161
- };
162
-
163
- export default AgeVerificationModal;
@@ -1,119 +0,0 @@
1
- import React, { useMemo, useState, useCallback } from 'react';
2
- import classes from '@riosst100/pwa-marketplace/src/components/AgeVerification/sellerCoupon.module.css';
3
- import SellerCouponCheckout from '@riosst100/pwa-marketplace/src/components/SellerCoupon/sellerCouponCheckout';
4
-
5
- const dayDiff = (toDate) => {
6
- if (!toDate) return null;
7
- const end = new Date(toDate);
8
- if (Number.isNaN(end.getTime())) return null;
9
- const now = new Date();
10
- const diffMs = end.setHours(23, 59, 59, 999) - now.getTime();
11
- const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
12
- return diffDays;
13
- };
14
-
15
- const SellerCoupon = ({ couponData, couponError, couponLoading }) => {
16
- if (couponLoading) {
17
- return '';
18
- // return <div className={classes.container}><p className={classes.metaText}>Loading coupons...</p></div>;
19
- }
20
-
21
- if (couponError) {
22
- return '';
23
- // return <div className={classes.container}><p className={classes.metaText}>Failed to load coupons.</p></div>;
24
- }
25
-
26
- const items = couponData?.sellerCoupons?.items || [];
27
-
28
- if (!items.length) {
29
- return '';
30
- // return <div className={classes.container}><p className={classes.metaText}>No coupons available.</p></div>;
31
- }
32
-
33
- const [copied, setCopied] = useState(null);
34
- const [couponModalOpen, setCouponModalOpen] = useState(false);
35
-
36
- const handleClaim = async (code) => {
37
- try {
38
- if (navigator?.clipboard?.writeText) {
39
- await navigator.clipboard.writeText(code);
40
- }
41
- } catch (_) {
42
- // ignore clipboard issues
43
- }
44
- setCopied(code);
45
- setTimeout(() => setCopied(null), 2000);
46
- };
47
-
48
- const handleViewCoupons = useCallback(() => {
49
- setCouponModalOpen(true);
50
- }, [setCouponModalOpen]);
51
-
52
- return (
53
- <section className={classes.container} aria-label="Seller coupons">
54
- <div className={classes.scroller}>
55
- {items.slice(0, 3).map(item => {
56
- const key = item.couponcode_id || item.coupon_id || item.code;
57
- const daysLeft = dayDiff(item.to_date);
58
- const expiryLabel = daysLeft === null
59
- ? 'No expiry'
60
- : daysLeft <= 0
61
- ? 'Ends today'
62
- : `Ends in ${daysLeft} day${daysLeft > 1 ? 's' : ''}`;
63
-
64
- const title = item.description || item.name || `Discount ${item.discount_amount || ''}`;
65
-
66
- return (
67
- <div className={classes.card} key={key} role="group" aria-label={`Coupon ${item.code}`}>
68
- <div className={classes.perf} aria-hidden="true" />
69
- <div className={classes.content}>
70
- <div className={classes.title}>{title}</div>
71
- <div className={classes.subtext}>Code: <span className={classes.code}>{item.code}</span></div>
72
- <div className={classes.expiry}>{expiryLabel}</div>
73
- </div>
74
- <div className={classes.divider} aria-hidden="true" />
75
- <div className={classes.actions}>
76
- <button
77
- type="button"
78
- className={copied === item.code ? classes.claimedBtn : classes.claimBtn}
79
- onClick={() => handleClaim(item.code)}
80
- aria-label={`Claim coupon ${item.code}`}
81
- >
82
- {copied === item.code ? 'Copied' : 'Claim'}
83
- </button>
84
- </div>
85
- </div>
86
- );
87
- })}
88
- {couponModalOpen && (
89
- <SellerCouponCheckout
90
- couponData={couponData}
91
- couponLoading={couponLoading}
92
- couponError={couponError}
93
- autoOpen={true}
94
- closeOnClaim={true}
95
- couponModalOpen={couponModalOpen}
96
- setCouponModalOpen={setCouponModalOpen}
97
- onSelectCoupon={handleClaim}
98
- onTriggerRender={() => null}
99
- isCopyAction={true}
100
- />
101
- )}
102
- <div className={classes.viewAllCard} role="group" aria-label="View All Coupons">
103
- <div className={classes.actions}>
104
- <button
105
- type="button"
106
- className={classes.viewAllBtn}
107
- onClick={handleViewCoupons}
108
- aria-label="View All Coupons"
109
- >
110
- View All Coupons
111
- </button>
112
- </div>
113
- </div>
114
- </div>
115
- </section>
116
- );
117
- };
118
-
119
- export default SellerCoupon;
@@ -1,164 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import ReactDOM from 'react-dom';
3
- import listClasses from '@riosst100/pwa-marketplace/src/components/AgeVerification/sellerCoupon.module.css';
4
- import modalClasses from '@riosst100/pwa-marketplace/src/components/AgeVerification/sellerCouponCheckout.module.css';
5
- import { SellerCouponCheckoutShimmer } from '@riosst100/pwa-marketplace/src/components/SellerCoupon';
6
-
7
- // Reuse day diff logic
8
- const dayDiff = (toDate) => {
9
- if (!toDate) return null;
10
- const end = new Date(toDate);
11
- if (Number.isNaN(end.getTime())) return null;
12
- const now = new Date();
13
- const diffMs = end.setHours(23, 59, 59, 999) - now.getTime();
14
- const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
15
- return diffDays;
16
- };
17
-
18
- // Simple portal root fallback
19
- const getPortalRoot = () => {
20
- let root = document.getElementById('seller-coupon-portal');
21
- if (!root) {
22
- root = document.createElement('div');
23
- root.id = 'seller-coupon-portal';
24
- document.body.appendChild(root);
25
- }
26
- return root;
27
- };
28
-
29
- const SellerCouponCheckout = ({
30
- couponData,
31
- couponError,
32
- setCouponModalOpen,
33
- couponModalOpen,
34
- couponLoading,
35
- isCopyAction,
36
- triggerLabel = 'View Seller Coupons',
37
- onSelectCoupon,
38
- onTriggerRender, // optional custom trigger renderer
39
- autoOpen = false, // auto open modal when mounted
40
- closeOnClaim = true // close after claiming
41
- }) => {
42
- // const [open, setOpen] = useState(false);
43
- const [copied, setCopied] = useState(null);
44
-
45
- const items = couponData?.sellerCoupons?.items || [];
46
-
47
- const handleOpen = () => setCouponModalOpen(true);
48
- const handleClose = () => setCouponModalOpen(false);
49
-
50
- useEffect(() => {
51
- if (!couponModalOpen) return;
52
- const onKey = (e) => {
53
- if (e.key === 'Escape') {
54
- handleClose();
55
- }
56
- };
57
- document.addEventListener('keydown', onKey);
58
- return () => document.removeEventListener('keydown', onKey);
59
- }, [couponModalOpen]);
60
-
61
- const handleClaim = async (item) => {
62
- try {
63
- if (navigator?.clipboard?.writeText) {
64
- await navigator.clipboard.writeText(item.code);
65
- }
66
- } catch (_) {
67
- /* ignore */
68
- }
69
- console.log('item.code',item.code)
70
- setCopied(item.code);
71
- setTimeout(() => setCopied(null), 2000);
72
- if (!isCopyAction) {
73
- if (onSelectCoupon) onSelectCoupon(item);
74
- if (closeOnClaim) handleClose();
75
- }
76
- };
77
-
78
- // Auto open behavior
79
- useEffect(() => {
80
- if (autoOpen && !couponModalOpen) {
81
- setCouponModalOpen(true);
82
- }
83
- }, [autoOpen, couponModalOpen]);
84
-
85
- console.log('copied',copied)
86
-
87
- const modal = couponModalOpen ? (
88
- <div className={modalClasses.overlay} role="dialog" aria-modal="true" aria-label="Seller coupons list">
89
- <div className={modalClasses.backdrop} onClick={handleClose} />
90
- <div className={modalClasses.dialog}>
91
- <div className={modalClasses.header}>
92
- <h2 className={modalClasses.title}>{isCopyAction ? 'Store Coupons' : 'Apply Coupon'}</h2>
93
- <button type="button" className={modalClasses.closeBtn} onClick={handleClose} aria-label="Close coupon modal">×</button>
94
- </div>
95
- <div className={modalClasses.body}>
96
- {couponLoading && <SellerCouponCheckoutShimmer />}
97
- {!couponLoading && couponError && <p className={listClasses.metaText}>Failed to load coupons.</p>}
98
- {!couponLoading && !couponError && !items.length && <p className={listClasses.metaText}>No coupons available.</p>}
99
- {!couponLoading && !couponError && items.length > 0 && (
100
- <div className={modalClasses.stack}>
101
- {items.map(item => {
102
- const key = item.couponcode_id || item.coupon_id || item.code;
103
- const daysLeft = dayDiff(item.to_date);
104
- const expiryLabel = daysLeft === null
105
- ? 'No expiry'
106
- : daysLeft <= 0
107
- ? 'Ends today'
108
- : `Ends in ${daysLeft} day${daysLeft > 1 ? 's' : ''}`;
109
- const title = item.description || item.name || `Discount ${item.discount_amount || ''}`;
110
- return (
111
- <div key={key} className={listClasses.card} role="group" aria-label={`Coupon ${item.code}`}>
112
- <div className={listClasses.perf} aria-hidden="true" />
113
- <div className={listClasses.content}>
114
- <div className={listClasses.title}>{title}</div>
115
- <div className={listClasses.subtext}>Code: <span className={listClasses.code}>{item.code}</span></div>
116
- <div className={listClasses.expiry}>{expiryLabel}</div>
117
- </div>
118
- <div className={listClasses.divider} aria-hidden="true" />
119
- <div className={listClasses.actions}>
120
- {isCopyAction ? <button
121
- type="button"
122
- className={copied === item.code ? listClasses.claimedBtn : listClasses.claimBtn}
123
- onClick={() => handleClaim(item)}
124
- aria-label={`Claim coupon ${item.code}`}
125
- >
126
- {copied === item.code ? 'Copied' : 'Claim'}
127
- </button> :
128
- <button
129
- type="button"
130
- className={copied === item.code ? listClasses.claimedBtn : listClasses.claimBtn}
131
- onClick={() => handleClaim(item)}
132
- aria-label={`Apply coupon ${item.code}`}
133
- >
134
- {copied === item.code ? 'Applied' : 'Apply'}
135
- </button>}
136
- </div>
137
- </div>
138
- );
139
- })}
140
- </div>
141
- )}
142
- </div>
143
- </div>
144
- </div>
145
- ) : null;
146
-
147
- const trigger = onTriggerRender
148
- ? onTriggerRender({ couponModalOpen, setCouponModalOpen, handleOpen })
149
- : (
150
- <button
151
- type="button"
152
- className={modalClasses.triggerBtn}
153
- onClick={handleOpen}
154
- aria-haspopup="dialog"
155
- aria-expanded={couponModalOpen}
156
- >
157
- {triggerLabel}
158
- </button>
159
- );
160
-
161
- return <>{onTriggerRender ? trigger : trigger}{couponModalOpen ? ReactDOM.createPortal(modal, getPortalRoot()) : null}</>;
162
- };
163
-
164
- export default SellerCouponCheckout;