@riosst100/pwa-marketplace 3.2.8 → 3.3.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@riosst100/pwa-marketplace",
3
3
  "author": "riosst100@gmail.com",
4
- "version": "3.2.8",
4
+ "version": "3.3.0",
5
5
  "main": "src/index.js",
6
6
  "pwa-studio": {
7
7
  "targets": {
@@ -173,168 +173,55 @@ const MessagesModal = ({
173
173
  onClick={(e) => e.stopPropagation()}
174
174
  >
175
175
  <div className='flex items-center justify-between mb-3'>
176
- <h3 className='text-[18px] font-semibold'>{formatMessage({ id: 'messages.titleWithSeller', defaultMessage: 'Start Conversation With ' })} {seller?.name}</h3>
176
+ <h3 className='text-[18px] font-semibold'>Send Message</h3>
177
177
  <button onClick={onClose} aria-label='Close' className='text-gray-600 text-[30px] hover:text-black'>×</button>
178
178
  </div>
179
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: 'Send 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
180
 
221
181
  {/* Detail percakapan */}
222
182
  <div className='flex-1 flex flex-col'>
223
183
  {/* Compose new message (shown only when composing OR when no thread exists) */}
224
- {(isComposing || sortedThreads.length === 0) && (
225
- <form onSubmit={handleSubmitNewMessage} className='mb-3 rounded-[6px] p-1 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 border-gray-100 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 border-gray-100 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'>
184
+
185
+ <form onSubmit={handleSubmitNewMessage} className='mb-3 rounded-[6px] p-1 flex flex-col gap-2'>
186
+ <div className='text-[13px] font-semibold flex items-center justify-between'>
187
+ <span>Send Message to {seller?.name}</span>
188
+ {/* {(sortedThreads.length > 0) && (
253
189
  <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}
190
+ type='button'
191
+ onClick={() => setIsComposing(false)}
192
+ className='text-[12px] px-2 py-1 rounded border text-gray-600 hover:bg-gray-50'
258
193
  >
259
- {isSendingNew
260
- ? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Send Message...' })
261
- : formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
194
+ {formatMessage({ id: 'messages.compose.cancel', defaultMessage: 'Cancel' })}
262
195
  </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>
196
+ )} */}
274
197
  </div>
275
- {selectedThread && (
198
+ <input
199
+ type='text'
200
+ className='border border-gray-100 rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
201
+ placeholder={formatMessage({ id: 'messages.compose.subjectPlaceholder', defaultMessage: 'Subject' })}
202
+ value={newSubject}
203
+ onChange={(e) => setNewSubject(e.target.value)}
204
+ />
205
+ <textarea
206
+ rows={3}
207
+ className='border border-gray-100 rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
208
+ placeholder={formatMessage({ id: 'messages.compose.contentPlaceholder', defaultMessage: 'Write a message to the seller…' })}
209
+ value={newContent}
210
+ onChange={(e) => setNewContent(e.target.value)}
211
+ />
212
+ <div className='flex justify-end'>
276
213
  <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'
214
+ type='submit'
215
+ 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]')}
216
+ disabled={isSendingNew}
217
+ aria-busy={isSendingNew}
280
218
  >
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' })}
219
+ {isSendingNew
220
+ ? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Sending Message...' })
221
+ : formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
287
222
  </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
- })()}
223
+ </div>
336
224
  </form>
337
- )}
338
225
  </div>
339
226
  </div>
340
227
  </div>
@@ -0,0 +1 @@
1
+ export { default } from './messages';
@@ -0,0 +1,335 @@
1
+ import { shape, string } from 'prop-types';
2
+ import { FormattedMessage, useIntl } from 'react-intl';
3
+
4
+ // import { useMessages } from '@riosst100/pwa-marketplace/src/talons/Messages/useMessages';
5
+ import { useStyle } from '@magento/venia-ui/lib/classify';
6
+ import { StoreTitle } from '@magento/venia-ui/lib/components/Head';
7
+ import defaultClasses from './messages.module.css';
8
+ import cn from 'classnames';
9
+ import React, { useMemo, useState, useEffect } from 'react';
10
+ import ChatContent from '@riosst100/pwa-marketplace/src/components/LiveChat/chatContent';
11
+ import { Link } from "react-router-dom";
12
+ import { Star1, Verify, Sms, Message, Shop, ArrowUp2 } from 'iconsax-react';
13
+
14
+ const Messages = props => {
15
+ const {
16
+ messages,
17
+ seller,
18
+ onReply,
19
+ onDelete,
20
+ onSend,
21
+ loading } = props;
22
+ const classes = useStyle(defaultClasses, props.classes);
23
+ // const { becomeSellerProps } = useMessages(props);
24
+ const { formatMessage } = useIntl();
25
+
26
+ const [selectedThread, setSelectedThread] = useState(null);
27
+ const [replyText, setReplyText] = useState('');
28
+ const [newSubject, setNewSubject] = useState('');
29
+ const [newContent, setNewContent] = useState('');
30
+ const [isComposing, setIsComposing] = useState(false);
31
+ const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
32
+ const [selectLatestAfterSend, setSelectLatestAfterSend] = useState(false);
33
+ const [isSendingNew, setIsSendingNew] = useState(false);
34
+ const [isReplying, setIsReplying] = useState(false);
35
+ const [isDeleting, setIsDeleting] = useState(false);
36
+
37
+ const renderStatusBadge = (statusValue) => {
38
+ const s = Number(statusValue);
39
+ const map = {
40
+ 0: { label: formatMessage({ id: 'messages.status.closed', defaultMessage: 'Closed' }), bg: 'bg-red-100', text: 'text-red-700', dot: 'bg-red-500' },
41
+ 1: { label: formatMessage({ id: 'messages.status.open', defaultMessage: 'Open' }), bg: 'bg-green-100', text: 'text-green-700', dot: 'bg-green-500' },
42
+ 2: { label: formatMessage({ id: 'messages.status.processing', defaultMessage: 'Processing' }), bg: 'bg-yellow-300/20', text: 'text-yellow-300', dot: 'bg-yellow-300' },
43
+ 3: { label: formatMessage({ id: 'messages.status.done', defaultMessage: 'Done' }), bg: 'bg-[#3d84ff1a]', text: 'text-[#3d84ff]', dot: 'bg-[#3d84ff]' }
44
+ };
45
+ const conf = map[s] || { label: 'Unknown', bg: 'bg-gray-200', text: 'text-gray-700', dot: 'bg-gray-500' };
46
+ return (
47
+ <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`}>
48
+ <span className={`inline-block w-[8px] h-[8px] rounded-full ${conf.dot}`}></span>
49
+ {conf.label}
50
+ </span>
51
+ );
52
+ };
53
+
54
+ // Filter pesan yang terkait seller ini (perkiraan berdasarkan id terkait)
55
+ const threads = useMemo(() => {
56
+ const items = messages?.items || [];
57
+ if (!seller?.seller_id) return items;
58
+ return items.filter(t =>
59
+ t?.owner_id === seller.seller_id ||
60
+ t?.receiver_id === seller.seller_id ||
61
+ t?.sender_id === seller.seller_id
62
+ );
63
+ }, [messages, seller]);
64
+
65
+ const sortedThreads = useMemo(() => {
66
+ return [...threads].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
67
+ }, [threads]);
68
+
69
+ const { location } = globalThis;
70
+
71
+ const query = new URLSearchParams(location.search);
72
+
73
+
74
+ // console.log('query',query)
75
+ // console.log('messageId',messageId)
76
+
77
+ useEffect(() => {
78
+ const messageId = query.get('id') || null;
79
+ console.log('query',query)
80
+ console.log('messageId',messageId)
81
+
82
+ if (messageId && sortedThreads?.length) {
83
+ const selectedThread = sortedThreads.find(t => t.message_id == messageId);
84
+ console.log('sortedThreads',sortedThreads)
85
+ console.log('selectedThread',selectedThread)
86
+ if (selectedThread) {
87
+ console.log('selectedThread',selectedThread)
88
+ setSelectedThread(selectedThread);
89
+ }
90
+ }
91
+ }, [sortedThreads, query]);
92
+
93
+ // Tandai sudah pernah load minimal sekali untuk mencegah flicker
94
+ useEffect(() => {
95
+ if (threads.length > 0 || (!loading && messages)) {
96
+ setHasLoadedOnce(true);
97
+ }
98
+ }, [threads.length, loading, messages]);
99
+
100
+ const chatData = useMemo(() => {
101
+ const details = selectedThread?.details?.items || [];
102
+ const sorted = [...details].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
103
+ const opening = selectedThread ? [{
104
+ message: selectedThread.description,
105
+ senderName: selectedThread.sender_name,
106
+ timeStamp: selectedThread.created_at ? new Date(selectedThread.created_at).toLocaleString() : '',
107
+ type: selectedThread.sender_name === seller?.name ? 'seller' : 'customer'
108
+ }] : [];
109
+ const mapped = sorted.map(d => ({
110
+ message: d.content,
111
+ senderName: d.sender_name,
112
+ timeStamp: new Date(d.created_at).toLocaleString(),
113
+ type: d.sender_name === seller?.name ? 'seller' : 'customer'
114
+ }));
115
+ return [...opening, ...mapped];
116
+ }, [selectedThread, seller]);
117
+
118
+ const handleSubmitReply = async (e) => {
119
+ e.preventDefault();
120
+ if (!selectedThread?.message_id || !replyText.trim()) return;
121
+ try {
122
+ setIsReplying(true);
123
+ const res = await onReply({ message_id: selectedThread.message_id, content: replyText.trim() });
124
+ // Optimistic local append so the reply appears instantly
125
+ const newDetail = {
126
+ content: res?.content || replyText.trim(),
127
+ sender_name: res?.sender_name || selectedThread?.sender_name || 'You',
128
+ sender_email: res?.sender_email || '',
129
+ receiver_name: res?.receiver_name || '',
130
+ is_read: true,
131
+ created_at: res?.created_at || new Date().toISOString()
132
+ };
133
+ const nextThread = {
134
+ ...selectedThread,
135
+ details: {
136
+ ...selectedThread.details,
137
+ items: [...(selectedThread.details?.items || []), newDetail],
138
+ total_count: (selectedThread.details?.total_count || 0) + 1
139
+ }
140
+ };
141
+ setSelectedThread(nextThread);
142
+ } finally {
143
+ setIsReplying(false);
144
+ }
145
+ setReplyText('');
146
+ };
147
+
148
+ const handleDeleteThread = async () => {
149
+ if (!selectedThread?.message_id) return;
150
+ try {
151
+ setIsDeleting(true);
152
+ await onDelete({ id: selectedThread.message_id });
153
+ } finally {
154
+ setIsDeleting(false);
155
+ }
156
+ };
157
+
158
+ const handleSubmitNewMessage = async (e) => {
159
+ e.preventDefault();
160
+ if (!newSubject.trim() || !newContent.trim()) return;
161
+ try {
162
+ setIsSendingNew(true);
163
+ await onSend({ subject: newSubject.trim(), content: newContent.trim(), seller_url: seller?.url_key });
164
+ } finally {
165
+ setIsSendingNew(false);
166
+ }
167
+ setNewSubject('');
168
+ setNewContent('');
169
+ setIsComposing(false);
170
+ setSelectLatestAfterSend(true);
171
+ };
172
+
173
+
174
+
175
+
176
+ return (
177
+ <>
178
+ <div className={cn(classes.leftContent, '')}>
179
+ <StoreTitle>
180
+ {formatMessage({
181
+ id: 'messagesPage.title',
182
+ defaultMessage: 'Messages'
183
+ })}
184
+ </StoreTitle>
185
+ <div className={cn(classes.leftContentContainer, 'border !border-gray-100 lg_rounded-md !rounded-lg')}>
186
+ <div class="auth-left">
187
+ <div className='text-[16px] px-3 py-3 border-gray-100 border-b font-medium flex items-center justify-between'>
188
+ <span>{formatMessage({ id: 'messages.sidebar.title', defaultMessage: 'Messages' })}</span>
189
+ </div>
190
+ <div className='max-h-[420px] overflow-y-auto'>
191
+ {(!hasLoadedOnce && loading) ? (
192
+ <div className='p-3 text-sm text-gray-500'>{formatMessage({ id: 'messages.loading', defaultMessage: 'Loading…' })}</div>
193
+ ) : !sortedThreads || sortedThreads.length === 0 ? (
194
+ <div className={cn(classes.noMessages, 'text-sm text-gray-500')}>{formatMessage({ id: 'messages.detail.noMessages', defaultMessage: 'No messages yet.' })}</div>
195
+ ) : (
196
+ sortedThreads && sortedThreads.length && sortedThreads.map(t =>
197
+ // console.log('fffff',t)
198
+ <div>
199
+ <hr className="!border-gray-100" style={{"width": "100%"}} />
200
+ <button
201
+ key={t.message_id}
202
+ onClick={() => setSelectedThread(t)}
203
+ className={cn('w-full text-left px-3 py-2 border-b hover:bg-gray-50',
204
+ selectedThread?.message_id === t.message_id && 'bg-gray-100')}
205
+ >
206
+ <div className='flex items-center justify-between gap-2'>
207
+ <div style={{"color": "#f76b1c"}}className='text-[12px] font-semibold line-clamp-1'>{t.seller ? t.seller.name : 'Unknown Seller' }</div>
208
+ {/* <div className='text-[12px] flex-shrink-0'>
209
+ {renderStatusBadge(t.status)}
210
+ </div> */}
211
+ </div>
212
+ <div className='flex items-center justify-between gap-2'>
213
+ <div className='text-[12px] line-clamp-1'>Subject: {t.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
214
+ {/* <div className='text-[12px] flex-shrink-0'>
215
+ {renderStatusBadge(t.status)}
216
+ </div> */}
217
+ </div>
218
+ <div className='text-[11px] text-gray-500 mt-1'>
219
+ {t.details.total_count ? t.details.items[t.details.items.length-1].content : t.description}
220
+ </div>
221
+ <div className='text-[10px] text-gray-500 mt-2'>{new Date(t.created_at).toLocaleString()}</div>
222
+ </button>
223
+ </div>
224
+ )
225
+ )}
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ <div className={classes.root}>
231
+ <div style={{"padding": "20px"}} className="lg_border-2 lg_border-solid lg_border-subtle lg_rounded-md !border !border-gray-100 p-6 !rounded-lg">
232
+ {/* Compose new message (shown only when composing OR when no thread exists) */}
233
+ {sortedThreads.length > 0 && (
234
+ <div className='flex items-center justify-between mb-2'>
235
+ {selectedThread && (
236
+ <>
237
+ <div style={{"textAlign": "left"}}>
238
+ {selectedThread.seller && <Link to={"/seller/"+selectedThread.seller.url_key}>
239
+ {/* <div class="flex items-center justify-center gap-[10px] relative">
240
+ <Shop color="#f76b1c" size={14} variant="Outline" className='stroke-[#f76b1c]' />
241
+ <div class="relative xs_hidden lg_flex w-fit text-[12px] text-[#f76b1c] text-[14px] tracking-[0] leading-[15px] whitespace-nowrap">
242
+ Visit Store
243
+ </div>
244
+ </div> */}
245
+ <div style={{"color": "#f76b1c"}}className='text-[14px] font-semibold line-clamp-1'>{selectedThread.seller ? selectedThread.seller.name : 'Unknown Seller' }</div>
246
+ </Link>}
247
+ <div className='mt-3 mb-1 text-[12px]'>Subject: {selectedThread?.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
248
+ {/* <div className='text-[12px] text-gray-600 flex items-center gap-2'>
249
+ <span>{formatMessage({ id: 'messages.detail.status', defaultMessage: 'Status:' })}</span>
250
+ {selectedThread && renderStatusBadge(selectedThread.status)}
251
+ </div> */}
252
+ </div>
253
+ <button
254
+ onClick={handleDeleteThread}
255
+ disabled={isDeleting}
256
+ 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'
257
+ >
258
+ {isDeleting && (
259
+ <span className='w-3 h-3 border-2 border-red-200 border-t-red-500 rounded-full animate-spin' aria-hidden='true'></span>
260
+ )}
261
+ {isDeleting
262
+ ? formatMessage({ id: 'messages.detail.deleting', defaultMessage: 'Deleting...' })
263
+ : formatMessage({ id: 'messages.detail.delete', defaultMessage: 'Delete Message' })}
264
+ </button>
265
+ </>
266
+ )}
267
+ </div>
268
+ )}
269
+ {sortedThreads.length > 0 && selectedThread ? (
270
+ <div className='!border-gray-100 border rounded-[6px] p-3 flex-1 min-h-[320px]'>
271
+ <ChatContent chatData={chatData} />
272
+ </div>
273
+ ) : (
274
+ <div className={classes.noConversationContainer}>
275
+ <img src="https://cdni.iconscout.com/illustration/premium/thumb/empty-message-illustration-svg-download-png-8593289.png" style={{"width":"20%"}}/>
276
+ <div className='text-sm text-gray-500'>{formatMessage({ id: 'messages.detail.noConversation', defaultMessage: 'Select a message to view the conversation.' })}</div>
277
+ </div>
278
+ )}
279
+ {sortedThreads.length > 0 && selectedThread && (
280
+ <form onSubmit={handleSubmitReply} className='mt-3 flex gap-2'>
281
+ {(() => {
282
+ const statusNum = Number(selectedThread.status);
283
+ const isReplyEnabled = statusNum === 1 || statusNum === 2;
284
+ const disabledReason = statusNum === 0
285
+ ? formatMessage({ id: 'messages.reply.disabled.closed', defaultMessage: 'Conversation closed' })
286
+ : statusNum === 3
287
+ ? formatMessage({ id: 'messages.reply.disabled.done', defaultMessage: 'Conversation finished' })
288
+ : formatMessage({ id: 'messages.reply.disabled.generic', defaultMessage: 'Cannot send a reply' });
289
+ return (
290
+ <>
291
+ <input
292
+ type='text'
293
+ className='flex-1 !border-gray-100 border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26] disabled:bg-gray-100 disabled:text-gray-500'
294
+ placeholder={isReplyEnabled ? formatMessage({ id: 'messages.reply.placeholder', defaultMessage: 'Write a reply…' }) : disabledReason}
295
+ value={replyText}
296
+ onChange={(e) => setReplyText(e.target.value)}
297
+ disabled={!isReplyEnabled}
298
+ title={!isReplyEnabled ? disabledReason : undefined}
299
+ aria-disabled={!isReplyEnabled}
300
+ />
301
+ <button
302
+ type='submit'
303
+ disabled={!isReplyEnabled || isReplying}
304
+ title={!isReplyEnabled ? disabledReason : undefined}
305
+ 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]')}
306
+ aria-busy={isReplying}
307
+ >
308
+ {isReplying
309
+ ? formatMessage({ id: 'messages.reply.sending', defaultMessage: 'Sending...' })
310
+ : formatMessage({ id: 'messages.reply.send', defaultMessage: 'Send' })}
311
+ </button>
312
+ </>
313
+ );
314
+ })()}
315
+ </form>
316
+ )}
317
+ </div>
318
+ </div>
319
+ </>
320
+ )
321
+ }
322
+
323
+ export default Messages;
324
+
325
+ Messages.defaultProps = {
326
+ signedInRedirectUrl: '/messages',
327
+ signInPageUrl: '/sign-in'
328
+ };
329
+
330
+ Messages.propTypes = {
331
+ classes: shape({
332
+ root: string
333
+ }),
334
+ signedInRedirectUrl: string
335
+ };
@@ -0,0 +1,51 @@
1
+ .root {
2
+ composes: gap-y-md from global;
3
+ composes: grid from global;
4
+ /* composes: justify-center from global; */
5
+ composes: px-0 from global;
6
+ composes: py-md from global;
7
+ composes: text-center from global;
8
+ grid-template-columns: minmax(auto, 95%);
9
+ justify-content: right;
10
+ width: 100%;
11
+ }
12
+
13
+ .leftContent {
14
+ composes: gap-y-md from global;
15
+ composes: grid from global;
16
+ composes: justify-center from global;
17
+ composes: px-0 from global;
18
+ composes: py-md from global;
19
+ composes: text-center from global;
20
+ grid-template-columns: minmax(auto, 100%);
21
+ width: 30%;
22
+ }
23
+
24
+ .header {
25
+ composes: font-serif from global;
26
+ }
27
+
28
+ .leftContentContainer {
29
+ text-align: left;
30
+ }
31
+
32
+ .noConversationContainer {
33
+ flex-direction: column;
34
+ display: flex;
35
+ flex-wrap: wrap;
36
+ align-content: center;
37
+ justify-content: flex-start;
38
+ align-items: center;
39
+ row-gap: 15px;
40
+ }
41
+
42
+ .noMessages {
43
+ text-align: center;
44
+ padding: 60px;
45
+ }
46
+
47
+ .rootContainer {
48
+ justify-content: space-around;
49
+ display: flex;
50
+ align-items: normal;
51
+ }