@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
|
@@ -2,14 +2,39 @@ import React, { useMemo, useCallback, useState } from 'react';
|
|
|
2
2
|
import { Link, useLocation, useHistory } from 'react-router-dom';
|
|
3
3
|
import useHelpCenter from '@riosst100/pwa-marketplace/src/talons/HelpCenter/useHelpCenter';
|
|
4
4
|
import classes from './helpcenter.module.css';
|
|
5
|
+
import Pagination from '@riosst100/pwa-marketplace/src/overwrites/venia-ui/lib/components/Pagination';
|
|
6
|
+
import ArraySearchInput from '@riosst100/pwa-marketplace/src/components/ArraySearchInput';
|
|
7
|
+
import { ChevronUp, ChevronDown, XCircle } from 'react-feather';
|
|
8
|
+
import Icon from '@magento/venia-ui/lib/components/Icon';
|
|
5
9
|
|
|
6
10
|
const HelpCenter = () => {
|
|
7
|
-
const
|
|
11
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
12
|
+
|
|
13
|
+
const { loading, error, data, totalPagesFromData, pageControl } = useHelpCenter({ searchQuery });
|
|
8
14
|
const history = useHistory();
|
|
9
15
|
const location = useLocation();
|
|
10
16
|
|
|
11
17
|
const queryParams = new URLSearchParams(location.search);
|
|
12
18
|
const activeTag = queryParams.get('tag') || '';
|
|
19
|
+
const activeCategories = queryParams.get('categories') || '';
|
|
20
|
+
|
|
21
|
+
const categoriesCounts = useMemo(() => {
|
|
22
|
+
if (data) {
|
|
23
|
+
return data.categories;
|
|
24
|
+
}
|
|
25
|
+
}, [data]);
|
|
26
|
+
|
|
27
|
+
const handleSetSearchQuery = (query) => {
|
|
28
|
+
const params = new URLSearchParams(location.search);
|
|
29
|
+
params.delete('categories');
|
|
30
|
+
params.delete('tag');
|
|
31
|
+
|
|
32
|
+
setSearchQuery(query);
|
|
33
|
+
|
|
34
|
+
history.push({ pathname: location.pathname, search: params.toString() });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// console.log('categoriesCounts',categoriesCounts)
|
|
13
38
|
|
|
14
39
|
const tagCounts = useMemo(() => {
|
|
15
40
|
const counts = {};
|
|
@@ -56,44 +81,88 @@ const HelpCenter = () => {
|
|
|
56
81
|
const params = new URLSearchParams(location.search);
|
|
57
82
|
if (tag) {
|
|
58
83
|
params.set('tag', tag);
|
|
84
|
+
params.delete('categories');
|
|
59
85
|
} else {
|
|
60
86
|
params.delete('tag');
|
|
61
87
|
}
|
|
62
88
|
history.push({ pathname: location.pathname, search: params.toString() });
|
|
63
89
|
}, [history, location]);
|
|
64
90
|
|
|
91
|
+
const handleRemoveCategories = useCallback(() => {
|
|
92
|
+
const params = new URLSearchParams(location.search);
|
|
93
|
+
params.delete('categories');
|
|
94
|
+
history.push({ pathname: location.pathname, search: params.toString() });
|
|
95
|
+
}, [history, location]);
|
|
96
|
+
|
|
97
|
+
const onSelectTopic = useCallback(topic => {
|
|
98
|
+
const params = new URLSearchParams(location.search);
|
|
99
|
+
if (topic) {
|
|
100
|
+
params.set('categories', topic);
|
|
101
|
+
params.delete('tag');
|
|
102
|
+
} else {
|
|
103
|
+
params.delete('categories');
|
|
104
|
+
}
|
|
105
|
+
history.push({ pathname: location.pathname, search: params.toString() });
|
|
106
|
+
}, [history, location]);
|
|
107
|
+
|
|
65
108
|
if (loading) {
|
|
66
|
-
return <div>Loading Help Center…</div>;
|
|
109
|
+
// return <div>Loading Help Center…</div>;
|
|
67
110
|
}
|
|
68
111
|
|
|
69
112
|
if (error) {
|
|
70
113
|
return <div>We\'re sorry, an error has occurred while generating this content.</div>;
|
|
71
114
|
}
|
|
115
|
+
|
|
116
|
+
const pagination = totalPagesFromData ? (
|
|
117
|
+
<Pagination pageControl={pageControl} />
|
|
118
|
+
) : null;
|
|
72
119
|
|
|
73
120
|
return (
|
|
74
121
|
<div className={classes.container}>
|
|
75
|
-
<h1 className={classes.title}>
|
|
122
|
+
<h1 className={classes.title}>Search our help library</h1>
|
|
123
|
+
<div style={{"width":"100%"}} className="pb-6"><ArraySearchInput searchQuery={searchQuery} placeholder={'Type something like, "question about a charge"'} isOpen={true} setSearchQuery={handleSetSearchQuery} /></div>
|
|
76
124
|
<div className={classes.grid}>
|
|
77
|
-
<aside className={classes.
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
<aside className={classes.sidebarRoot}>
|
|
126
|
+
<aside className={classes.sidebar}>
|
|
127
|
+
<div className={classes.sidebarHeader}>All help topics</div>
|
|
128
|
+
<div className={classes.tagsWrap}>
|
|
129
|
+
{categoriesCounts.length ? categoriesCounts.map((data, index) => (
|
|
130
|
+
<button
|
|
131
|
+
key={index}
|
|
132
|
+
onClick={() => onSelectTopic(data.title)}
|
|
133
|
+
className={`${classes.tagButton} ${activeTag === data.title ? classes.tagButtonActive : ''}`}
|
|
134
|
+
>
|
|
135
|
+
{data.title}
|
|
136
|
+
</button>
|
|
137
|
+
)) : <span className={classes.emptyState}>No topics.</span>}
|
|
138
|
+
</div>
|
|
139
|
+
</aside>
|
|
140
|
+
{/* <aside className={classes.sidebarTags}>
|
|
141
|
+
<div className={classes.sidebarHeader}>All help tags</div>
|
|
142
|
+
<div className={classes.tagsWrap}>
|
|
143
|
+
{Object.keys(tagCounts).length === 0 && (
|
|
144
|
+
<span className={classes.emptyState}>No tags.</span>
|
|
145
|
+
)}
|
|
146
|
+
{Object.entries(tagCounts).map(([tag, count]) => (
|
|
147
|
+
<button
|
|
148
|
+
key={tag}
|
|
149
|
+
onClick={() => onSelectTag(tag)}
|
|
150
|
+
className={`${classes.tagButton} ${activeTag === tag ? classes.tagButtonActive : ''}`}
|
|
151
|
+
>
|
|
152
|
+
{tag.toUpperCase()}
|
|
153
|
+
</button>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
</aside> */}
|
|
93
157
|
</aside>
|
|
94
|
-
<main className={classes.main}>
|
|
158
|
+
{loading ? 'Loading..' : <main className={classes.main}>
|
|
95
159
|
{activeTag && (
|
|
96
|
-
<h2 className={classes.tagTitle}>Tag
|
|
160
|
+
<h2 className={classes.tagTitle}>Tag: {activeTag}</h2>
|
|
161
|
+
)}
|
|
162
|
+
{activeCategories && (
|
|
163
|
+
<>
|
|
164
|
+
<div className={classes.tagTitle}>Topic: <b>{activeCategories}</b><span onClick={handleRemoveCategories}><Icon src={XCircle} attrs={{ color: "#b7b7b7",width: 18 }} /></span></div>
|
|
165
|
+
</>
|
|
97
166
|
)}
|
|
98
167
|
{visibleQuestions.map(q => {
|
|
99
168
|
const isOpen = expandedId === q.question_id;
|
|
@@ -103,7 +172,9 @@ const HelpCenter = () => {
|
|
|
103
172
|
onClick={() => toggleExpanded(q.question_id)}
|
|
104
173
|
className={`${classes.questionHeader} ${isOpen ? classes.questionHeaderOpen : ''}`}
|
|
105
174
|
>
|
|
106
|
-
<span className={classes.plusIcon}>
|
|
175
|
+
<span className={classes.plusIcon}>
|
|
176
|
+
{isOpen ? <Icon src={ChevronUp} attrs={{ width: 20 }} /> : <Icon src={ChevronDown} attrs={{ width: 20 }} />}
|
|
177
|
+
</span>
|
|
107
178
|
{q.title}
|
|
108
179
|
</header>
|
|
109
180
|
{isOpen && (
|
|
@@ -142,7 +213,8 @@ const HelpCenter = () => {
|
|
|
142
213
|
{visibleQuestions.length === 0 && (
|
|
143
214
|
<div className={classes.emptyState}>No questions found.</div>
|
|
144
215
|
)}
|
|
145
|
-
|
|
216
|
+
{totalPagesFromData && <div className={classes.pagination}>{pagination}</div>}
|
|
217
|
+
</main>}
|
|
146
218
|
</div>
|
|
147
219
|
</div>
|
|
148
220
|
);
|
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
font-weight: 700;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
.sidebarRoot {
|
|
25
|
+
max-height: max-content;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.sidebarTags {
|
|
29
|
+
border: 1px solid #E6E9EA;
|
|
30
|
+
border-radius: 6px;
|
|
31
|
+
max-height: max-content;
|
|
32
|
+
margin-top: 30px;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
.sidebar {
|
|
25
36
|
border: 1px solid #E6E9EA;
|
|
26
37
|
border-radius: 6px;
|
|
@@ -57,6 +68,8 @@
|
|
|
57
68
|
|
|
58
69
|
.tagTitle {
|
|
59
70
|
margin: 0;
|
|
71
|
+
display: flex;
|
|
72
|
+
column-gap: 10px;
|
|
60
73
|
margin-bottom: 16px;
|
|
61
74
|
}
|
|
62
75
|
|
|
@@ -74,7 +87,7 @@
|
|
|
74
87
|
.questionHeader {
|
|
75
88
|
padding: 12px;
|
|
76
89
|
background: #F2F2F2;
|
|
77
|
-
color: #f76b1c;
|
|
90
|
+
/* color: #f76b1c; */
|
|
78
91
|
font-weight: 700;
|
|
79
92
|
display: flex;
|
|
80
93
|
align-items: center;
|
|
@@ -93,7 +106,7 @@
|
|
|
93
106
|
width: 18px;
|
|
94
107
|
height: 18px;
|
|
95
108
|
border-radius: 3px;
|
|
96
|
-
background: #f76b1c;
|
|
109
|
+
/* background: #f76b1c; */
|
|
97
110
|
color: #fff;
|
|
98
111
|
font-size: 14px;
|
|
99
112
|
line-height: 18px;
|
|
@@ -65,7 +65,7 @@ const QuestionDetail = () => {
|
|
|
65
65
|
</p>
|
|
66
66
|
</div>
|
|
67
67
|
<div className={classes.tagsLine}>
|
|
68
|
-
<strong>
|
|
68
|
+
<strong>Topics:</strong>
|
|
69
69
|
{Array.isArray(question.tags) ? (
|
|
70
70
|
question.tags.map((t, idx) => (
|
|
71
71
|
<span key={idx} className={classes.tagPill}>
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import React, { useMemo, useState, useEffect } from 'react';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import ChatContent from '@riosst100/pwa-marketplace/src/components/LiveChat/chatContent';
|
|
4
|
+
import { useIntl } from 'react-intl';
|
|
5
|
+
|
|
6
|
+
const MessagesModal = ({
|
|
7
|
+
isOpen,
|
|
8
|
+
onClose,
|
|
9
|
+
messages,
|
|
10
|
+
seller,
|
|
11
|
+
onReply,
|
|
12
|
+
onDelete,
|
|
13
|
+
onSend,
|
|
14
|
+
loading
|
|
15
|
+
}) => {
|
|
16
|
+
|
|
17
|
+
const { formatMessage } = useIntl();
|
|
18
|
+
const [selectedThread, setSelectedThread] = useState(null);
|
|
19
|
+
const [replyText, setReplyText] = useState('');
|
|
20
|
+
const [newSubject, setNewSubject] = useState('');
|
|
21
|
+
const [newContent, setNewContent] = useState('');
|
|
22
|
+
const [isComposing, setIsComposing] = useState(false);
|
|
23
|
+
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
|
|
24
|
+
const [selectLatestAfterSend, setSelectLatestAfterSend] = useState(false);
|
|
25
|
+
const [isSendingNew, setIsSendingNew] = useState(false);
|
|
26
|
+
const [isReplying, setIsReplying] = useState(false);
|
|
27
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
28
|
+
|
|
29
|
+
const renderStatusBadge = (statusValue) => {
|
|
30
|
+
const s = Number(statusValue);
|
|
31
|
+
const map = {
|
|
32
|
+
0: { label: formatMessage({ id: 'messages.status.closed', defaultMessage: 'Closed' }), bg: 'bg-red-100', text: 'text-red-700', dot: 'bg-red-500' },
|
|
33
|
+
1: { label: formatMessage({ id: 'messages.status.open', defaultMessage: 'Open' }), bg: 'bg-green-100', text: 'text-green-700', dot: 'bg-green-500' },
|
|
34
|
+
2: { label: formatMessage({ id: 'messages.status.processing', defaultMessage: 'Processing' }), bg: 'bg-yellow-300/20', text: 'text-yellow-300', dot: 'bg-yellow-300' },
|
|
35
|
+
3: { label: formatMessage({ id: 'messages.status.done', defaultMessage: 'Done' }), bg: 'bg-[#3d84ff1a]', text: 'text-[#3d84ff]', dot: 'bg-[#3d84ff]' }
|
|
36
|
+
};
|
|
37
|
+
const conf = map[s] || { label: 'Unknown', bg: 'bg-gray-200', text: 'text-gray-700', dot: 'bg-gray-500' };
|
|
38
|
+
return (
|
|
39
|
+
<span className={`inline-flex items-center justify-center gap-1 px-2 h-[24px] rounded-full text-[12px] font-medium ${conf.bg} ${conf.text} whitespace-nowrap`}>
|
|
40
|
+
<span className={`inline-block w-[8px] h-[8px] rounded-full ${conf.dot}`}></span>
|
|
41
|
+
{conf.label}
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Filter pesan yang terkait seller ini (perkiraan berdasarkan id terkait)
|
|
47
|
+
const threads = useMemo(() => {
|
|
48
|
+
const items = messages?.items || [];
|
|
49
|
+
if (!seller?.seller_id) return items;
|
|
50
|
+
return items.filter(t =>
|
|
51
|
+
t?.owner_id === seller.seller_id ||
|
|
52
|
+
t?.receiver_id === seller.seller_id ||
|
|
53
|
+
t?.sender_id === seller.seller_id
|
|
54
|
+
);
|
|
55
|
+
}, [messages, seller]);
|
|
56
|
+
|
|
57
|
+
const sortedThreads = useMemo(() => {
|
|
58
|
+
return [...threads].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
59
|
+
}, [threads]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!isComposing && sortedThreads?.length) {
|
|
63
|
+
const latest = sortedThreads[sortedThreads.length - 1];
|
|
64
|
+
if (!selectedThread) {
|
|
65
|
+
setSelectedThread(latest);
|
|
66
|
+
} else {
|
|
67
|
+
const stillThere = sortedThreads.find(t => t.message_id === selectedThread.message_id);
|
|
68
|
+
if (!stillThere) {
|
|
69
|
+
setSelectedThread(latest);
|
|
70
|
+
} else {
|
|
71
|
+
// Replace with the fresh thread data so new details from polling appear
|
|
72
|
+
setSelectedThread(stillThere);
|
|
73
|
+
if (selectLatestAfterSend) {
|
|
74
|
+
setSelectedThread(latest);
|
|
75
|
+
setSelectLatestAfterSend(false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, [sortedThreads, isComposing, selectedThread, selectLatestAfterSend]);
|
|
81
|
+
|
|
82
|
+
// Tandai sudah pernah load minimal sekali untuk mencegah flicker
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (threads.length > 0 || (!loading && messages)) {
|
|
85
|
+
setHasLoadedOnce(true);
|
|
86
|
+
}
|
|
87
|
+
}, [threads.length, loading, messages]);
|
|
88
|
+
|
|
89
|
+
const chatData = useMemo(() => {
|
|
90
|
+
const details = selectedThread?.details?.items || [];
|
|
91
|
+
const sorted = [...details].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
92
|
+
const opening = selectedThread ? [{
|
|
93
|
+
message: selectedThread.description,
|
|
94
|
+
senderName: selectedThread.sender_name,
|
|
95
|
+
timeStamp: selectedThread.created_at ? new Date(selectedThread.created_at).toLocaleString() : '',
|
|
96
|
+
type: selectedThread.sender_name === seller?.name ? 'seller' : 'customer'
|
|
97
|
+
}] : [];
|
|
98
|
+
const mapped = sorted.map(d => ({
|
|
99
|
+
message: d.content,
|
|
100
|
+
senderName: d.sender_name,
|
|
101
|
+
timeStamp: new Date(d.created_at).toLocaleString(),
|
|
102
|
+
type: d.sender_name === seller?.name ? 'seller' : 'customer'
|
|
103
|
+
}));
|
|
104
|
+
return [...opening, ...mapped];
|
|
105
|
+
}, [selectedThread, seller]);
|
|
106
|
+
|
|
107
|
+
const handleSubmitReply = async (e) => {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
if (!selectedThread?.message_id || !replyText.trim()) return;
|
|
110
|
+
try {
|
|
111
|
+
setIsReplying(true);
|
|
112
|
+
const res = await onReply({ message_id: selectedThread.message_id, content: replyText.trim() });
|
|
113
|
+
// Optimistic local append so the reply appears instantly
|
|
114
|
+
const newDetail = {
|
|
115
|
+
content: res?.content || replyText.trim(),
|
|
116
|
+
sender_name: res?.sender_name || selectedThread?.sender_name || 'You',
|
|
117
|
+
sender_email: res?.sender_email || '',
|
|
118
|
+
receiver_name: res?.receiver_name || '',
|
|
119
|
+
is_read: true,
|
|
120
|
+
created_at: res?.created_at || new Date().toISOString()
|
|
121
|
+
};
|
|
122
|
+
const nextThread = {
|
|
123
|
+
...selectedThread,
|
|
124
|
+
details: {
|
|
125
|
+
...selectedThread.details,
|
|
126
|
+
items: [...(selectedThread.details?.items || []), newDetail],
|
|
127
|
+
total_count: (selectedThread.details?.total_count || 0) + 1
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
setSelectedThread(nextThread);
|
|
131
|
+
} finally {
|
|
132
|
+
setIsReplying(false);
|
|
133
|
+
}
|
|
134
|
+
setReplyText('');
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleDeleteThread = async () => {
|
|
138
|
+
if (!selectedThread?.message_id) return;
|
|
139
|
+
try {
|
|
140
|
+
setIsDeleting(true);
|
|
141
|
+
await onDelete({ id: selectedThread.message_id });
|
|
142
|
+
} finally {
|
|
143
|
+
setIsDeleting(false);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleSubmitNewMessage = async (e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
if (!newSubject.trim() || !newContent.trim()) return;
|
|
150
|
+
try {
|
|
151
|
+
setIsSendingNew(true);
|
|
152
|
+
await onSend({ subject: newSubject.trim(), content: newContent.trim(), seller_url: seller?.url_key });
|
|
153
|
+
} finally {
|
|
154
|
+
setIsSendingNew(false);
|
|
155
|
+
}
|
|
156
|
+
setNewSubject('');
|
|
157
|
+
setNewContent('');
|
|
158
|
+
setIsComposing(false);
|
|
159
|
+
setSelectLatestAfterSend(true);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (!isOpen) return null;
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
className={cn('fixed inset-0 z-50 flex items-center justify-center bg-black/30')}
|
|
167
|
+
onClick={(e) => {
|
|
168
|
+
if (e.target === e.currentTarget) onClose();
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<div
|
|
172
|
+
className='bg-white rounded-[6px] shadow-lg w-full max-w-[960px] p-5'
|
|
173
|
+
onClick={(e) => e.stopPropagation()}
|
|
174
|
+
>
|
|
175
|
+
<div className='flex items-center justify-between mb-3'>
|
|
176
|
+
<h3 className='text-[18px] font-semibold'>{formatMessage({ id: 'messages.titleWithSeller', defaultMessage: 'Conversation with' })} {seller?.name}</h3>
|
|
177
|
+
<button onClick={onClose} aria-label='Close' className='text-gray-600 text-[30px] hover:text-black'>×</button>
|
|
178
|
+
</div>
|
|
179
|
+
<div className='flex gap-4'>
|
|
180
|
+
{/* Sidebar list thread (shown only when there are threads) */}
|
|
181
|
+
{sortedThreads.length > 0 && (
|
|
182
|
+
<div className='w-[320px] border rounded-[6px] overflow-hidden'>
|
|
183
|
+
<div className='px-3 py-2 border-b font-medium flex items-center justify-between'>
|
|
184
|
+
<span>{formatMessage({ id: 'messages.sidebar.title', defaultMessage: 'Messages' })}</span>
|
|
185
|
+
<button
|
|
186
|
+
type='button'
|
|
187
|
+
onClick={() => {
|
|
188
|
+
setIsComposing(true);
|
|
189
|
+
setSelectedThread(null);
|
|
190
|
+
}}
|
|
191
|
+
className='text-[12px] px-2 py-1 rounded border border-[#FF6E26] text-[#FF6E26] hover:bg-[#FF6E26] hover:text-white'
|
|
192
|
+
>
|
|
193
|
+
{formatMessage({ id: 'messages.compose.newMessage', defaultMessage: 'Create New Message' })}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
<div className='max-h-[420px] overflow-y-auto'>
|
|
197
|
+
{(!isComposing && !hasLoadedOnce && loading && sortedThreads.length === 0) ? (
|
|
198
|
+
<div className='p-3 text-sm text-gray-500'>{formatMessage({ id: 'messages.loading', defaultMessage: 'Loading…' })}</div>
|
|
199
|
+
) : (
|
|
200
|
+
sortedThreads.map(t => (
|
|
201
|
+
<button
|
|
202
|
+
key={t.message_id}
|
|
203
|
+
onClick={() => setSelectedThread(t)}
|
|
204
|
+
className={cn('w-full text-left px-3 py-2 border-b hover:bg-gray-50',
|
|
205
|
+
selectedThread?.message_id === t.message_id && 'bg-gray-100')}
|
|
206
|
+
>
|
|
207
|
+
<div className='flex items-center justify-between gap-2'>
|
|
208
|
+
<div className='text-[13px] font-semibold line-clamp-1'>{t.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
|
|
209
|
+
<div className='text-[12px] flex-shrink-0'>
|
|
210
|
+
{renderStatusBadge(t.status)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div className='text-[11px] text-gray-500 mt-1'>{formatMessage({ id: 'messages.createdAt', defaultMessage: 'Created:' })} {new Date(t.created_at).toLocaleString()}</div>
|
|
214
|
+
</button>
|
|
215
|
+
))
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* Detail percakapan */}
|
|
222
|
+
<div className='flex-1 flex flex-col'>
|
|
223
|
+
{/* Compose new message (shown only when composing OR when no thread exists) */}
|
|
224
|
+
{(isComposing || sortedThreads.length === 0) && (
|
|
225
|
+
<form onSubmit={handleSubmitNewMessage} className='mb-3 border rounded-[6px] p-3 flex flex-col gap-2'>
|
|
226
|
+
<div className='text-[13px] font-semibold flex items-center justify-between'>
|
|
227
|
+
<span>{formatMessage({ id: 'messages.compose.header', defaultMessage: 'Create New Message' })}</span>
|
|
228
|
+
{(sortedThreads.length > 0) && (
|
|
229
|
+
<button
|
|
230
|
+
type='button'
|
|
231
|
+
onClick={() => setIsComposing(false)}
|
|
232
|
+
className='text-[12px] px-2 py-1 rounded border text-gray-600 hover:bg-gray-50'
|
|
233
|
+
>
|
|
234
|
+
{formatMessage({ id: 'messages.compose.cancel', defaultMessage: 'Cancel' })}
|
|
235
|
+
</button>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
<input
|
|
239
|
+
type='text'
|
|
240
|
+
className='border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
|
|
241
|
+
placeholder={formatMessage({ id: 'messages.compose.subjectPlaceholder', defaultMessage: 'Subject' })}
|
|
242
|
+
value={newSubject}
|
|
243
|
+
onChange={(e) => setNewSubject(e.target.value)}
|
|
244
|
+
/>
|
|
245
|
+
<textarea
|
|
246
|
+
rows={3}
|
|
247
|
+
className='border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
|
|
248
|
+
placeholder={formatMessage({ id: 'messages.compose.contentPlaceholder', defaultMessage: 'Write a message to the seller…' })}
|
|
249
|
+
value={newContent}
|
|
250
|
+
onChange={(e) => setNewContent(e.target.value)}
|
|
251
|
+
/>
|
|
252
|
+
<div className='flex justify-end'>
|
|
253
|
+
<button
|
|
254
|
+
type='submit'
|
|
255
|
+
className={cn('px-4 py-2 rounded-[6px] text-white disabled:opacity-50 disabled:cursor-not-allowed', isSendingNew ? 'bg-gray-500 text-gray-700' : 'bg-[#FF6E26]')}
|
|
256
|
+
disabled={isSendingNew}
|
|
257
|
+
aria-busy={isSendingNew}
|
|
258
|
+
>
|
|
259
|
+
{isSendingNew
|
|
260
|
+
? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Send Message...' })
|
|
261
|
+
: formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
</form>
|
|
265
|
+
)}
|
|
266
|
+
{!isComposing && sortedThreads.length > 0 && (
|
|
267
|
+
<div className='flex items-center justify-between mb-2'>
|
|
268
|
+
<div>
|
|
269
|
+
<div className='text-[14px] font-semibold'>{selectedThread?.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
|
|
270
|
+
<div className='text-[12px] text-gray-600 flex items-center gap-2'>
|
|
271
|
+
<span>{formatMessage({ id: 'messages.detail.status', defaultMessage: 'Status:' })}</span>
|
|
272
|
+
{selectedThread && renderStatusBadge(selectedThread.status)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
{selectedThread && (
|
|
276
|
+
<button
|
|
277
|
+
onClick={handleDeleteThread}
|
|
278
|
+
disabled={isDeleting}
|
|
279
|
+
className='text-[12px] px-3 py-1 rounded border text-red-600 border-red-300 hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2'
|
|
280
|
+
>
|
|
281
|
+
{isDeleting && (
|
|
282
|
+
<span className='w-3 h-3 border-2 border-red-200 border-t-red-500 rounded-full animate-spin' aria-hidden='true'></span>
|
|
283
|
+
)}
|
|
284
|
+
{isDeleting
|
|
285
|
+
? formatMessage({ id: 'messages.detail.deleting', defaultMessage: 'Deleting...' })
|
|
286
|
+
: formatMessage({ id: 'messages.detail.delete', defaultMessage: 'Delete Message' })}
|
|
287
|
+
</button>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
{!isComposing && sortedThreads.length > 0 && (
|
|
292
|
+
<div className='border rounded-[6px] p-3 flex-1 min-h-[320px]'>
|
|
293
|
+
{selectedThread ? (
|
|
294
|
+
<ChatContent chatData={chatData} />
|
|
295
|
+
) : (
|
|
296
|
+
<div className='text-sm text-gray-500'>{formatMessage({ id: 'messages.detail.selectPrompt', defaultMessage: 'Select a message to view the conversation.' })}</div>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
{!isComposing && sortedThreads.length > 0 && selectedThread && (
|
|
301
|
+
<form onSubmit={handleSubmitReply} className='mt-3 flex gap-2'>
|
|
302
|
+
{(() => {
|
|
303
|
+
const statusNum = Number(selectedThread.status);
|
|
304
|
+
const isReplyEnabled = statusNum === 1 || statusNum === 2;
|
|
305
|
+
const disabledReason = statusNum === 0
|
|
306
|
+
? formatMessage({ id: 'messages.reply.disabled.closed', defaultMessage: 'Conversation closed' })
|
|
307
|
+
: statusNum === 3
|
|
308
|
+
? formatMessage({ id: 'messages.reply.disabled.done', defaultMessage: 'Conversation finished' })
|
|
309
|
+
: formatMessage({ id: 'messages.reply.disabled.generic', defaultMessage: 'Cannot send a reply' });
|
|
310
|
+
return (
|
|
311
|
+
<>
|
|
312
|
+
<input
|
|
313
|
+
type='text'
|
|
314
|
+
className='flex-1 border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26] disabled:bg-gray-100 disabled:text-gray-500'
|
|
315
|
+
placeholder={isReplyEnabled ? formatMessage({ id: 'messages.reply.placeholder', defaultMessage: 'Write a reply…' }) : disabledReason}
|
|
316
|
+
value={replyText}
|
|
317
|
+
onChange={(e) => setReplyText(e.target.value)}
|
|
318
|
+
disabled={!isReplyEnabled}
|
|
319
|
+
title={!isReplyEnabled ? disabledReason : undefined}
|
|
320
|
+
aria-disabled={!isReplyEnabled}
|
|
321
|
+
/>
|
|
322
|
+
<button
|
|
323
|
+
type='submit'
|
|
324
|
+
disabled={!isReplyEnabled || isReplying}
|
|
325
|
+
title={!isReplyEnabled ? disabledReason : undefined}
|
|
326
|
+
className={cn('px-4 py-2 rounded-[6px] text-white disabled:opacity-50 disabled:cursor-not-allowed', isReplying ? 'bg-gray-500 text-gray-700' : 'bg-[#FF6E26]')}
|
|
327
|
+
aria-busy={isReplying}
|
|
328
|
+
>
|
|
329
|
+
{isReplying
|
|
330
|
+
? formatMessage({ id: 'messages.reply.sending', defaultMessage: 'Sending...' })
|
|
331
|
+
: formatMessage({ id: 'messages.reply.send', defaultMessage: 'Send' })}
|
|
332
|
+
</button>
|
|
333
|
+
</>
|
|
334
|
+
);
|
|
335
|
+
})()}
|
|
336
|
+
</form>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export default MessagesModal;
|
|
@@ -4,8 +4,9 @@ import cn from 'classnames';
|
|
|
4
4
|
const chatContent = (props) => {
|
|
5
5
|
const { chatData } = props;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const orderedChatData = Array.isArray(chatData)
|
|
8
|
+
? [...chatData]
|
|
9
|
+
: [];
|
|
9
10
|
const messagesContainerRef = useRef(null);
|
|
10
11
|
|
|
11
12
|
useEffect(() => {
|
|
@@ -16,10 +16,10 @@ const MaintenancePage = props => {
|
|
|
16
16
|
</head>
|
|
17
17
|
<body className={classes.body}>
|
|
18
18
|
<div className={classes.container}>
|
|
19
|
-
<img src="https://seller
|
|
19
|
+
<img src="https://seller.tcgcollective.co/media/logo/default/FullLogo_Transparent_NoBuffer_14_.png" alt="Logo" className={classes.logo} />
|
|
20
20
|
<br />
|
|
21
21
|
<h1 className={classes.h1}>We're Temporarily Offline</h1>
|
|
22
|
-
<img src="https://seller
|
|
22
|
+
<img src="https://seller.tcgcollective.co/media/maintenance.webp" alt="Logo" className={classes.logo} />
|
|
23
23
|
<p className={classes.p}>
|
|
24
24
|
Our website is currently undergoing scheduled maintenance. <br/>
|
|
25
25
|
We should be back shortly. Thank you for your patience.
|
|
@@ -66,7 +66,7 @@ const SellerCouponCheckout = ({
|
|
|
66
66
|
} catch (_) {
|
|
67
67
|
/* ignore */
|
|
68
68
|
}
|
|
69
|
-
console.log('item.code',item.code)
|
|
69
|
+
// console.log('item.code',item.code)
|
|
70
70
|
setCopied(item.code);
|
|
71
71
|
setTimeout(() => setCopied(null), 2000);
|
|
72
72
|
if (!isCopyAction) {
|
|
@@ -82,7 +82,7 @@ const SellerCouponCheckout = ({
|
|
|
82
82
|
}
|
|
83
83
|
}, [autoOpen, couponModalOpen]);
|
|
84
84
|
|
|
85
|
-
console.log('copied',copied)
|
|
85
|
+
// console.log('copied',copied)
|
|
86
86
|
|
|
87
87
|
const modal = couponModalOpen ? (
|
|
88
88
|
<div className={modalClasses.overlay} role="dialog" aria-modal="true" aria-label="Seller coupons list">
|