@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.
Files changed (39) hide show
  1. package/i18n/en_US.json +30 -1
  2. package/i18n/id_ID.json +30 -1
  3. package/package.json +1 -1
  4. package/src/components/AboutUs/aboutUs.js +9 -0
  5. package/src/components/AboutUs/index.js +1 -0
  6. package/src/components/AgeVerification/ageVerificationModal.js +163 -0
  7. package/src/components/AgeVerification/ageVerificationModal.module.css +85 -0
  8. package/src/components/AgeVerification/ageVerificationModal.shimmer.js +21 -0
  9. package/src/components/AgeVerification/index.js +2 -0
  10. package/src/components/AgeVerification/sellerCoupon.js +119 -0
  11. package/src/components/AgeVerification/sellerCouponCheckout.js +164 -0
  12. package/src/components/HelpCenter/helpCenter.js +95 -23
  13. package/src/components/HelpCenter/helpcenter.module.css +15 -2
  14. package/src/components/HelpCenter/questionDetail.js +1 -1
  15. package/src/components/LiveChat/MessagesModal.js +345 -0
  16. package/src/components/LiveChat/chatContent.js +3 -2
  17. package/src/components/MaintenancePage/maintenancePage.js +2 -2
  18. package/src/components/SellerCoupon/sellerCouponCheckout.js +2 -2
  19. package/src/components/SellerDetail/sellerDetail.js +38 -36
  20. package/src/components/SellerMegaMenu/sellerMegaMenu.js +2 -3
  21. package/src/components/SellerMegaMenu/sellerMegaMenuItem.js +1 -19
  22. package/src/components/SellerProducts/productContent.js +1 -6
  23. package/src/components/WebsiteSwitcher/websiteSwitcherItem.js +17 -7
  24. package/src/intercept.js +21 -0
  25. package/src/overwrites/venia-ui/lib/components/Adapter/adapter.js +23 -3
  26. package/src/overwrites/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilters.js +1 -1
  27. package/src/overwrites/venia-ui/lib/components/Footer/footer.js +21 -22
  28. package/src/overwrites/venia-ui/lib/components/Footer/sampleData.js +35 -31
  29. package/src/overwrites/venia-ui/lib/components/Header/header.js +2 -2
  30. package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +105 -3
  31. package/src/overwrites/venia-ui/lib/components/StoreCodeRoute/storeCodeRoute.js +55 -53
  32. package/src/talons/HelpCenter/helpCenter.gql.js +50 -39
  33. package/src/talons/HelpCenter/useHelpCenter.js +67 -7
  34. package/src/talons/Seller/seller.gql.js +90 -0
  35. package/src/talons/Seller/useSeller.js +102 -4
  36. package/src/talons/SellerMegaMenu/megaMenu.gql.js +11 -18
  37. package/src/talons/SellerMegaMenu/useSellerMegaMenu.js +59 -2
  38. package/src/talons/SellerProducts/productContent.gql.js +5 -0
  39. 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@riosst100/pwa-marketplace",
3
3
  "author": "riosst100@gmail.com",
4
- "version": "3.1.8",
4
+ "version": "3.2.0",
5
5
  "main": "src/index.js",
6
6
  "pwa-studio": {
7
7
  "targets": {
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import CMSPage from '@riosst100/pwa-marketplace/src/overwrites/venia-ui/lib/RootComponents/CMS/cms';
3
+
4
+
5
+ const AboutUs = () => {
6
+ return <CMSPage identifier="about-us" />
7
+ }
8
+
9
+ export default AboutUs;
@@ -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,2 @@
1
+ export { default } from './ageVerificationModal';
2
+ export { default as AgeVerificationModalShimmer } from './ageVerificationModal.shimmer';
@@ -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;