@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.
- package/package.json +1 -1
- package/src/components/AgeVerification/ageVerification.js +121 -0
- package/src/components/AgeVerification/index.js +1 -2
- package/src/components/AgeVerificationModal/ageVerificationModal.js +83 -0
- package/src/components/{AgeVerification → AgeVerificationModal}/ageVerificationModal.module.css +77 -10
- package/src/components/AgeVerificationModal/index.js +2 -0
- package/src/components/Messages/messages.js +0 -10
- package/src/components/RFQPage/quoteDetail.js +2 -3
- package/src/components/ShowMore/showMore.js +8 -5
- package/src/intercept.js +9 -0
- package/src/overwrites/peregrine/lib/talons/RootComponents/Category/useCategory.js +0 -4
- package/src/overwrites/venia-ui/lib/RootComponents/Category/category.js +2 -0
- package/src/overwrites/venia-ui/lib/RootComponents/Category/categoryContent.js +1 -1
- package/src/overwrites/venia-ui/lib/RootComponents/Product/product.js +2 -0
- package/src/overwrites/venia-ui/lib/components/Adapter/adapter.js +0 -23
- package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +2 -3
- package/src/talons/AgeVerification/ageVerification.gql.js +31 -0
- package/src/talons/AgeVerification/useAgeVerification.js +126 -0
- package/src/components/AgeVerification/ageVerificationModal.js +0 -163
- package/src/components/AgeVerification/sellerCoupon.js +0 -119
- package/src/components/AgeVerification/sellerCouponCheckout.js +0 -164
- /package/src/components/{AgeVerification → AgeVerificationModal}/ageVerificationModal.shimmer.js +0 -0
package/package.json
CHANGED
|
@@ -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 './
|
|
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;
|
package/src/components/{AgeVerification → AgeVerificationModal}/ageVerificationModal.module.css
RENAMED
|
@@ -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
|
|
15
|
-
backdrop-filter:blur(
|
|
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,
|
|
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:
|
|
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;
|
|
@@ -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
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
/package/src/components/{AgeVerification → AgeVerificationModal}/ageVerificationModal.shimmer.js
RENAMED
|
File without changes
|