@riosst100/pwa-marketplace 3.1.8 → 3.2.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.
- package/i18n/en_US.json +30 -1
- package/i18n/id_ID.json +30 -1
- package/package.json +1 -1
- package/src/components/AboutUs/aboutUs.js +9 -0
- package/src/components/AboutUs/index.js +1 -0
- package/src/components/AgeVerification/ageVerificationModal.js +163 -0
- package/src/components/AgeVerification/ageVerificationModal.module.css +85 -0
- package/src/components/AgeVerification/ageVerificationModal.shimmer.js +21 -0
- package/src/components/AgeVerification/index.js +2 -0
- package/src/components/AgeVerification/sellerCoupon.js +119 -0
- package/src/components/AgeVerification/sellerCouponCheckout.js +164 -0
- package/src/components/HelpCenter/helpCenter.js +95 -23
- package/src/components/HelpCenter/helpcenter.module.css +15 -2
- package/src/components/HelpCenter/questionDetail.js +1 -1
- package/src/components/LiveChat/MessagesModal.js +345 -0
- package/src/components/LiveChat/chatContent.js +3 -2
- package/src/components/MaintenancePage/maintenancePage.js +2 -2
- package/src/components/SellerCoupon/sellerCouponCheckout.js +2 -2
- package/src/components/SellerDetail/sellerDetail.js +38 -36
- package/src/components/SellerMegaMenu/sellerMegaMenu.js +2 -3
- package/src/components/SellerMegaMenu/sellerMegaMenuItem.js +1 -19
- package/src/components/SellerProducts/productContent.js +1 -6
- package/src/components/WebsiteSwitcher/websiteSwitcherItem.js +17 -7
- package/src/intercept.js +21 -0
- package/src/overwrites/venia-ui/lib/components/Adapter/adapter.js +23 -3
- package/src/overwrites/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilters.js +1 -1
- package/src/overwrites/venia-ui/lib/components/Footer/footer.js +21 -22
- package/src/overwrites/venia-ui/lib/components/Footer/sampleData.js +35 -31
- package/src/overwrites/venia-ui/lib/components/Header/header.js +2 -2
- package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +105 -3
- package/src/overwrites/venia-ui/lib/components/StoreCodeRoute/storeCodeRoute.js +55 -53
- package/src/talons/HelpCenter/helpCenter.gql.js +50 -39
- package/src/talons/HelpCenter/useHelpCenter.js +67 -7
- package/src/talons/Seller/seller.gql.js +90 -0
- package/src/talons/Seller/useSeller.js +102 -4
- package/src/talons/SellerMegaMenu/megaMenu.gql.js +11 -18
- package/src/talons/SellerMegaMenu/useSellerMegaMenu.js +59 -2
- package/src/talons/SellerProducts/productContent.gql.js +5 -0
- package/src/talons/SellerProducts/useProductContent.js +61 -2
package/i18n/en_US.json
CHANGED
|
@@ -516,5 +516,34 @@
|
|
|
516
516
|
"Form.postalCode": "Postal Code is required",
|
|
517
517
|
"Form.phonenumber": "Contact Number is required",
|
|
518
518
|
"ProductOptions.productSize": "Fashion size {label}",
|
|
519
|
-
"productOptions.selectedSize": "Fashion size {label} button selected"
|
|
519
|
+
"productOptions.selectedSize": "Fashion size {label} button selected",
|
|
520
|
+
"messages.titleWithSeller": "Conversation with",
|
|
521
|
+
"messages.sidebar.title": "Messages",
|
|
522
|
+
"messages.compose.newMessage": "Create New Message",
|
|
523
|
+
"messages.loading": "Loading…",
|
|
524
|
+
"messages.noSubject": "No Subject",
|
|
525
|
+
"messages.createdAt": "Created:",
|
|
526
|
+
"messages.compose.header": "Create New Message",
|
|
527
|
+
"messages.compose.cancel": "Cancel",
|
|
528
|
+
"messages.compose.subjectPlaceholder": "Subject",
|
|
529
|
+
"messages.compose.contentPlaceholder": "Write a message to the seller…",
|
|
530
|
+
"messages.compose.send": "Send Message",
|
|
531
|
+
"messages.compose.sending": "Send Message...",
|
|
532
|
+
"messages.detail.status": "Status:",
|
|
533
|
+
"messages.detail.delete": "Delete Message",
|
|
534
|
+
"messages.detail.selectPrompt": "Select a message to view the conversation.",
|
|
535
|
+
"messages.reply.disabled.closed": "Conversation closed",
|
|
536
|
+
"messages.reply.disabled.done": "Conversation finished",
|
|
537
|
+
"messages.reply.disabled.generic": "Cannot send a reply",
|
|
538
|
+
"messages.reply.placeholder": "Write a reply…",
|
|
539
|
+
"messages.reply.send": "Send",
|
|
540
|
+
"messages.reply.sending": "Sending...",
|
|
541
|
+
"messages.status.closed": "Closed",
|
|
542
|
+
"messages.status.open": "Open",
|
|
543
|
+
"messages.status.processing": "Processing",
|
|
544
|
+
"messages.status.done": "Done",
|
|
545
|
+
"messages.loginRequired": "Please sign in to your account to send a message to the seller.",
|
|
546
|
+
"messages.messageSent": "Message sent successfully.",
|
|
547
|
+
"messages.replySent": "Reply sent successfully.",
|
|
548
|
+
"messages.messageDeleted": "Message deleted successfully."
|
|
520
549
|
}
|
package/i18n/id_ID.json
CHANGED
|
@@ -517,5 +517,34 @@
|
|
|
517
517
|
"Form.postalCode": "Postal Code is required",
|
|
518
518
|
"Form.phonenumber": "Contact Number is required",
|
|
519
519
|
"ProductOptions.productSize": "Fashion size {label}",
|
|
520
|
-
"productOptions.selectedSize": "Fashion size {label} button selected"
|
|
520
|
+
"productOptions.selectedSize": "Fashion size {label} button selected",
|
|
521
|
+
"messages.titleWithSeller": "Percakapan dengan",
|
|
522
|
+
"messages.sidebar.title": "Pesan",
|
|
523
|
+
"messages.compose.newMessage": "Buat Pesan Baru",
|
|
524
|
+
"messages.loading": "Memuat…",
|
|
525
|
+
"messages.noSubject": "Tanpa Subjek",
|
|
526
|
+
"messages.createdAt": "Dibuat:",
|
|
527
|
+
"messages.compose.header": "Buat Pesan Baru",
|
|
528
|
+
"messages.compose.cancel": "Batal",
|
|
529
|
+
"messages.compose.subjectPlaceholder": "Subjek",
|
|
530
|
+
"messages.compose.contentPlaceholder": "Tulis pesan untuk seller…",
|
|
531
|
+
"messages.compose.send": "Kirim Pesan",
|
|
532
|
+
"messages.compose.sending": "Mengirim Pesan...",
|
|
533
|
+
"messages.detail.status": "Status:",
|
|
534
|
+
"messages.detail.delete": "Hapus Pesan",
|
|
535
|
+
"messages.detail.selectPrompt": "Pilih pesan untuk melihat percakapan.",
|
|
536
|
+
"messages.reply.disabled.closed": "Percakapan ditutup",
|
|
537
|
+
"messages.reply.disabled.done": "Percakapan selesai",
|
|
538
|
+
"messages.reply.disabled.generic": "Tidak dapat mengirim balasan",
|
|
539
|
+
"messages.reply.placeholder": "Tulis balasan…",
|
|
540
|
+
"messages.reply.send": "Kirim",
|
|
541
|
+
"messages.reply.sending": "Mengirim...",
|
|
542
|
+
"messages.status.closed": "Closed",
|
|
543
|
+
"messages.status.open": "Open",
|
|
544
|
+
"messages.status.processing": "Processing",
|
|
545
|
+
"messages.status.done": "Done",
|
|
546
|
+
"messages.loginRequired": "Silakan masuk ke akun Anda untuk mengirim pesan ke seller.",
|
|
547
|
+
"messages.messageSent": "Pesan berhasil dikirim.",
|
|
548
|
+
"messages.replySent": "Balasan berhasil dikirim.",
|
|
549
|
+
"messages.messageDeleted": "Pesan berhasil dihapus."
|
|
521
550
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {default} from './aboutUs';
|
|
@@ -0,0 +1,163 @@
|
|
|
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;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
.overlay{
|
|
2
|
+
position:fixed;
|
|
3
|
+
inset:0;
|
|
4
|
+
z-index:1000;
|
|
5
|
+
display:flex;
|
|
6
|
+
align-items:flex-start;
|
|
7
|
+
justify-content:center;
|
|
8
|
+
padding:5vh 24px;
|
|
9
|
+
font-family:inherit
|
|
10
|
+
}
|
|
11
|
+
.backdrop{
|
|
12
|
+
position:absolute;
|
|
13
|
+
inset:0;
|
|
14
|
+
background:rgba(0,0,0,.4);
|
|
15
|
+
backdrop-filter:blur(2px)
|
|
16
|
+
}
|
|
17
|
+
.dialog{
|
|
18
|
+
position:relative;
|
|
19
|
+
background:#fff;
|
|
20
|
+
border-radius:12px;
|
|
21
|
+
box-shadow:0 8px 32px rgba(0,0,0,.25);
|
|
22
|
+
width:clamp(320px,80vw,370px);
|
|
23
|
+
max-height:90vh;
|
|
24
|
+
display:flex;
|
|
25
|
+
flex-direction:column
|
|
26
|
+
}
|
|
27
|
+
.header{
|
|
28
|
+
display:flex;
|
|
29
|
+
align-items:center;
|
|
30
|
+
justify-content:space-between;
|
|
31
|
+
padding:16px 20px;
|
|
32
|
+
border-bottom:1px solid #eee
|
|
33
|
+
}
|
|
34
|
+
.title{
|
|
35
|
+
font-size:20px;
|
|
36
|
+
font-weight:600;
|
|
37
|
+
margin:0;
|
|
38
|
+
color:#333
|
|
39
|
+
}
|
|
40
|
+
.closeBtn{
|
|
41
|
+
background:none;
|
|
42
|
+
border:none;
|
|
43
|
+
font-size:24px;
|
|
44
|
+
line-height:1;
|
|
45
|
+
cursor:pointer;
|
|
46
|
+
color:#777;
|
|
47
|
+
padding:4px 8px;
|
|
48
|
+
border-radius:6px
|
|
49
|
+
}
|
|
50
|
+
.closeBtn:hover{
|
|
51
|
+
background:#f5f5f5;
|
|
52
|
+
color:#222
|
|
53
|
+
}
|
|
54
|
+
.body{
|
|
55
|
+
padding:16px 20px;
|
|
56
|
+
overflow:auto
|
|
57
|
+
}
|
|
58
|
+
.triggerBtn{
|
|
59
|
+
background:#f76b1c;
|
|
60
|
+
color:#fff;
|
|
61
|
+
border:none;
|
|
62
|
+
padding:10px 18px;
|
|
63
|
+
border-radius:8px;
|
|
64
|
+
font-weight:600;
|
|
65
|
+
cursor:pointer
|
|
66
|
+
}
|
|
67
|
+
.triggerBtn:hover{
|
|
68
|
+
background:#f26313
|
|
69
|
+
}
|
|
70
|
+
/* Vertical stack wrapper for existing sellerCoupon card styles */
|
|
71
|
+
.stack{
|
|
72
|
+
display:flex;
|
|
73
|
+
flex-direction:column;
|
|
74
|
+
gap:16px;
|
|
75
|
+
padding:4px 2px 12px;
|
|
76
|
+
}
|
|
77
|
+
@media (max-width:640px){
|
|
78
|
+
.dialog{
|
|
79
|
+
width:92vw;
|
|
80
|
+
padding-bottom:8px
|
|
81
|
+
}
|
|
82
|
+
.grid{
|
|
83
|
+
grid-template-columns:repeat(auto-fill,minmax(160px,1fr))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Shimmer from '@magento/venia-ui/lib/components/Shimmer';
|
|
3
|
+
|
|
4
|
+
const AgeVerificationModal = props => {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Shimmer width="100%" height="85.1px" />
|
|
8
|
+
<Shimmer width="100%" height="85.1px" />
|
|
9
|
+
<Shimmer width="100%" height="85.1px" />
|
|
10
|
+
<Shimmer width="100%" height="85.1px" />
|
|
11
|
+
<Shimmer width="100%" height="85.1px" />
|
|
12
|
+
<Shimmer width="100%" height="85.1px" />
|
|
13
|
+
<Shimmer width="100%" height="85.1px" />
|
|
14
|
+
<Shimmer width="100%" height="85.1px" />
|
|
15
|
+
<Shimmer width="100%" height="85.1px" />
|
|
16
|
+
<Shimmer width="100%" height="85.1px" />
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default AgeVerificationModal;
|
|
@@ -0,0 +1,119 @@
|
|
|
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;
|
|
@@ -0,0 +1,164 @@
|
|
|
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;
|