@riosst100/pwa-marketplace 3.2.9 → 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.9",
4
+ "version": "3.3.0",
5
5
  "main": "src/index.js",
6
6
  "pwa-studio": {
7
7
  "targets": {
@@ -173,7 +173,7 @@ 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'>
@@ -184,7 +184,7 @@ const MessagesModal = ({
184
184
 
185
185
  <form onSubmit={handleSubmitNewMessage} className='mb-3 rounded-[6px] p-1 flex flex-col gap-2'>
186
186
  <div className='text-[13px] font-semibold flex items-center justify-between'>
187
- <span>{formatMessage({ id: 'messages.compose.header', defaultMessage: 'Create New Message' })}</span>
187
+ <span>Send Message to {seller?.name}</span>
188
188
  {/* {(sortedThreads.length > 0) && (
189
189
  <button
190
190
  type='button'
@@ -217,7 +217,7 @@ const MessagesModal = ({
217
217
  aria-busy={isSendingNew}
218
218
  >
219
219
  {isSendingNew
220
- ? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Send Message...' })
220
+ ? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Sending Message...' })
221
221
  : formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
222
222
  </button>
223
223
  </div>
@@ -8,6 +8,8 @@ import defaultClasses from './messages.module.css';
8
8
  import cn from 'classnames';
9
9
  import React, { useMemo, useState, useEffect } from 'react';
10
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';
11
13
 
12
14
  const Messages = props => {
13
15
  const {
@@ -61,29 +63,32 @@ const Messages = props => {
61
63
  }, [messages, seller]);
62
64
 
63
65
  const sortedThreads = useMemo(() => {
64
- return [...threads].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
66
+ return [...threads].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
65
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)
66
76
 
67
77
  useEffect(() => {
68
- if (!isComposing && sortedThreads?.length) {
69
- const latest = sortedThreads[sortedThreads.length - 1];
70
- if (!selectedThread) {
71
- setSelectedThread(latest);
72
- } else {
73
- const stillThere = sortedThreads.find(t => t.message_id === selectedThread.message_id);
74
- if (!stillThere) {
75
- setSelectedThread(latest);
76
- } else {
77
- // Replace with the fresh thread data so new details from polling appear
78
- setSelectedThread(stillThere);
79
- if (selectLatestAfterSend) {
80
- setSelectedThread(latest);
81
- setSelectLatestAfterSend(false);
82
- }
83
- }
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);
84
89
  }
85
90
  }
86
- }, [sortedThreads, isComposing, selectedThread, selectLatestAfterSend]);
91
+ }, [sortedThreads, query]);
87
92
 
88
93
  // Tandai sudah pernah load minimal sekali untuk mencegah flicker
89
94
  useEffect(() => {
@@ -165,6 +170,9 @@ const Messages = props => {
165
170
  setSelectLatestAfterSend(true);
166
171
  };
167
172
 
173
+
174
+
175
+
168
176
  return (
169
177
  <>
170
178
  <div className={cn(classes.leftContent, '')}>
@@ -176,124 +184,99 @@ const Messages = props => {
176
184
  </StoreTitle>
177
185
  <div className={cn(classes.leftContentContainer, 'border !border-gray-100 lg_rounded-md !rounded-lg')}>
178
186
  <div class="auth-left">
179
- <div className='px-3 py-2 border-b font-medium flex items-center justify-between'>
187
+ <div className='text-[16px] px-3 py-3 border-gray-100 border-b font-medium flex items-center justify-between'>
180
188
  <span>{formatMessage({ id: 'messages.sidebar.title', defaultMessage: 'Messages' })}</span>
181
- <button
182
- type='button'
183
- onClick={() => {
184
- setIsComposing(true);
185
- setSelectedThread(null);
186
- }}
187
- className='text-[12px] px-2 py-1 rounded border border-[#FF6E26] text-[#FF6E26] hover:bg-[#FF6E26] hover:text-white'
188
- >
189
- {formatMessage({ id: 'messages.compose.newMessage', defaultMessage: 'Send New Message' })}
190
- </button>
191
189
  </div>
192
190
  <div className='max-h-[420px] overflow-y-auto'>
193
- {(!isComposing && !hasLoadedOnce && loading && sortedThreads.length === 0) ? (
191
+ {(!hasLoadedOnce && loading) ? (
194
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
195
  ) : (
196
- sortedThreads.map(t => (
197
- <button
198
- key={t.message_id}
199
- onClick={() => setSelectedThread(t)}
200
- className={cn('w-full text-left px-3 py-2 border-b hover:bg-gray-50',
201
- selectedThread?.message_id === t.message_id && 'bg-gray-100')}
202
- >
203
- <div className='flex items-center justify-between gap-2'>
204
- <div className='text-[13px] font-semibold line-clamp-1'>{t.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
205
- {/* <div className='text-[12px] flex-shrink-0'>
206
- {renderStatusBadge(t.status)}
207
- </div> */}
208
- </div>
209
- <div className='text-[11px] text-gray-500 mt-1'>{formatMessage({ id: 'messages.createdAt', defaultMessage: 'Created:' })} {new Date(t.created_at).toLocaleString()}</div>
210
- </button>
211
- ))
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
+ )
212
225
  )}
213
226
  </div>
214
227
  </div>
215
228
  </div>
216
229
  </div>
217
230
  <div className={classes.root}>
218
- <div className="lg_border-2 lg_border-solid lg_border-subtle lg_rounded-md !border !border-gray-100 p-6 !rounded-lg">
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">
219
232
  {/* Compose new message (shown only when composing OR when no thread exists) */}
220
- {(isComposing || sortedThreads.length === 0) && (
221
- <form onSubmit={handleSubmitNewMessage} className='mb-3 rounded-[6px] p-1 flex flex-col gap-2'>
222
- <div className='text-[13px] font-semibold flex items-center justify-between'>
223
- <span>{formatMessage({ id: 'messages.compose.header', defaultMessage: 'Create New Message' })}</span>
224
- {(sortedThreads.length > 0) && (
225
- <button
226
- type='button'
227
- onClick={() => setIsComposing(false)}
228
- className='text-[12px] px-2 py-1 !border-gray-100 rounded border text-gray-600 hover:bg-gray-50'
229
- >
230
- {formatMessage({ id: 'messages.compose.cancel', defaultMessage: 'Cancel' })}
231
- </button>
232
- )}
233
- </div>
234
- <input
235
- type='text'
236
- className='border border-gray-100 rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
237
- placeholder={formatMessage({ id: 'messages.compose.subjectPlaceholder', defaultMessage: 'Subject' })}
238
- value={newSubject}
239
- onChange={(e) => setNewSubject(e.target.value)}
240
- />
241
- <textarea
242
- rows={3}
243
- className='border border-gray-100 rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
244
- placeholder={formatMessage({ id: 'messages.compose.contentPlaceholder', defaultMessage: 'Write a message to the seller…' })}
245
- value={newContent}
246
- onChange={(e) => setNewContent(e.target.value)}
247
- />
248
- <div className='flex justify-end'>
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>
249
253
  <button
250
- type='submit'
251
- 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]')}
252
- disabled={isSendingNew}
253
- aria-busy={isSendingNew}
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'
254
257
  >
255
- {isSendingNew
256
- ? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Send Message...' })
257
- : formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
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' })}
258
264
  </button>
259
- </div>
260
- </form>
261
- )}
262
- {!isComposing && sortedThreads.length > 0 && (
263
- <div className='flex items-center justify-between mb-2'>
264
- <div>
265
- <div className='text-[14px] font-semibold'>{selectedThread?.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
266
- {/* <div className='text-[12px] text-gray-600 flex items-center gap-2'>
267
- <span>{formatMessage({ id: 'messages.detail.status', defaultMessage: 'Status:' })}</span>
268
- {selectedThread && renderStatusBadge(selectedThread.status)}
269
- </div> */}
270
- </div>
271
- {selectedThread && (
272
- <button
273
- onClick={handleDeleteThread}
274
- disabled={isDeleting}
275
- 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'
276
- >
277
- {isDeleting && (
278
- <span className='w-3 h-3 border-2 border-red-200 border-t-red-500 rounded-full animate-spin' aria-hidden='true'></span>
279
- )}
280
- {isDeleting
281
- ? formatMessage({ id: 'messages.detail.deleting', defaultMessage: 'Deleting...' })
282
- : formatMessage({ id: 'messages.detail.delete', defaultMessage: 'Delete Message' })}
283
- </button>
265
+ </>
284
266
  )}
285
267
  </div>
286
268
  )}
287
- {!isComposing && sortedThreads.length > 0 && (
288
- <div className='!border-gray-100 border rounded-[6px] p-3 flex-1 min-h-[320px]'>
289
- {selectedThread ? (
269
+ {sortedThreads.length > 0 && selectedThread ? (
270
+ <div className='!border-gray-100 border rounded-[6px] p-3 flex-1 min-h-[320px]'>
290
271
  <ChatContent chatData={chatData} />
291
- ) : (
292
- <div className='text-sm text-gray-500'>{formatMessage({ id: 'messages.detail.selectPrompt', defaultMessage: 'Select a message to view the conversation.' })}</div>
293
- )}
294
- </div>
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>
295
278
  )}
296
- {!isComposing && sortedThreads.length > 0 && selectedThread && (
279
+ {sortedThreads.length > 0 && selectedThread && (
297
280
  <form onSubmit={handleSubmitReply} className='mt-3 flex gap-2'>
298
281
  {(() => {
299
282
  const statusNum = Number(selectedThread.status);
@@ -29,6 +29,21 @@
29
29
  text-align: left;
30
30
  }
31
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
+
32
47
  .rootContainer {
33
48
  justify-content: space-around;
34
49
  display: flex;
@@ -30,15 +30,42 @@ const RFQModalForm = (props) => {
30
30
 
31
31
  // Upload File with remove capability
32
32
  const [uploadedFiles, setUploadedFiles] = useState([]);
33
+ // Calculate safe raw-size limit to keep base64 payload under 2MB
34
+ const GRAPHQL_BASE64_LIMIT = 2 * 1024 * 1024;
35
+ const SAFE_RAW_LIMIT = Math.floor((GRAPHQL_BASE64_LIMIT * 3) / 4) - 8 * 1024;
36
+ const estimateBase64Size = size => Math.ceil((4 * size) / 3);
37
+
33
38
  const onDrop = useCallback((accepted) => {
34
- setUploadedFiles(prev => [...prev, ...accepted]);
35
- }, []);
39
+ const safe = [];
40
+ const rejected = [];
41
+ accepted.forEach(f => {
42
+ const est = estimateBase64Size(f.size || 0);
43
+ if (est <= GRAPHQL_BASE64_LIMIT) {
44
+ safe.push(f);
45
+ } else {
46
+ rejected.push({ name: f.name || f.path, size: f.size });
47
+ }
48
+ });
49
+ if (safe.length) {
50
+ setUploadedFiles(prev => [...prev, ...safe]);
51
+ }
52
+ if (rejected.length) {
53
+ addToast({
54
+ type: 'error',
55
+ message: formatMessage({
56
+ id: 'productFullDetail.rfqFileTooLarge',
57
+ defaultMessage: 'One or more files are too large. Max ~1.5MB each.'
58
+ }),
59
+ timeout: 4000
60
+ });
61
+ }
62
+ }, [addToast, formatMessage]);
36
63
 
37
64
  const {
38
65
  getRootProps,
39
66
  getInputProps,
40
67
  isDragActive
41
- } = useDropzone({ onDrop });
68
+ } = useDropzone({ onDrop, maxSize: SAFE_RAW_LIMIT, multiple: true });
42
69
 
43
70
  const handleRemoveFile = useCallback((index) => {
44
71
  setUploadedFiles(prev => prev.filter((_, i) => i !== index));
@@ -59,8 +86,60 @@ const RFQModalForm = (props) => {
59
86
  </div>
60
87
  ));
61
88
 
89
+ // Helper: convert File to base64 string (without data URL prefix)
90
+ const toBase64 = useCallback(
91
+ file =>
92
+ new Promise((resolve, reject) => {
93
+ try {
94
+ const reader = new FileReader();
95
+ reader.onload = () => {
96
+ const result = reader.result;
97
+ // reader.result is a Data URL: "data:<mime>;base64,<payload>"
98
+ const base64 = typeof result === 'string' && result.includes(',')
99
+ ? result.split(',')[1]
100
+ : '';
101
+ resolve(base64);
102
+ };
103
+ reader.onerror = reject;
104
+ reader.readAsDataURL(file);
105
+ } catch (e) {
106
+ reject(e);
107
+ }
108
+ }),
109
+ []
110
+ );
111
+
62
112
  const handleSubmit = useCallback(async values => {
63
- console.log('[RFQ] submit values', values);
113
+ // console.log('[RFQ] submit values', values);
114
+ // Prepare base64 conversions if any files were uploaded
115
+ let filesPayload = undefined;
116
+ if (uploadedFiles && uploadedFiles.length) {
117
+ // Hard guard: abort if any file would exceed limit after base64 encoding
118
+ const oversize = uploadedFiles.find(f => estimateBase64Size(f.size || 0) > GRAPHQL_BASE64_LIMIT);
119
+ if (oversize) {
120
+ addToast({
121
+ type: 'error',
122
+ message: formatMessage({
123
+ id: 'productFullDetail.rfqFileTooLargeSubmit',
124
+ defaultMessage: 'A selected file is too large (limit ~1.5MB).'
125
+ }),
126
+ timeout: 4000
127
+ });
128
+ return;
129
+ }
130
+ try {
131
+ const base64List = await Promise.all(
132
+ uploadedFiles.map(file => toBase64(file))
133
+ );
134
+ filesPayload = uploadedFiles.map((file, i) => ({
135
+ file_name: (file && (file.name || file.path)) || '',
136
+ file_type: (file && file.type) ? file.type : 'application/octet-stream',
137
+ base64: base64List[i] || ''
138
+ }));
139
+ } catch (e) {
140
+ console.error('[RFQ] file base64 conversion failed', e);
141
+ }
142
+ }
64
143
  const input = {
65
144
  product_id: productId,
66
145
  quantity: values.quote_qty ? Number(values.quote_qty) : undefined,
@@ -70,24 +149,16 @@ const RFQModalForm = (props) => {
70
149
  ? Number(values.quote_price_per_unit)
71
150
  : undefined,
72
151
  date_need_quote: startDate ? startDate.toISOString() : undefined,
73
- files: uploadedFiles.length
74
- ? uploadedFiles.map(file => ({
75
- file_name: (file && (file.name || file.path)) || '',
76
- // Browsers do not expose real file system paths; use name as a stable placeholder
77
- file_path: (file && (file.path || file.name)) || '',
78
- // Ensure a valid MIME string; default if missing
79
- file_type: (file && file.type) ? file.type : 'application/octet-stream'
80
- }))
81
- : undefined
152
+ files: filesPayload
82
153
  };
83
154
 
84
- console.log('[RFQ] input payload', input);
155
+ // console.log('[RFQ] input payload', input);
85
156
 
86
157
  Object.keys(input).forEach(key => input[key] === undefined && delete input[key]);
87
158
 
88
159
  try {
89
160
  const result = await handleCreateQuickRfq(input);
90
- console.log('[RFQ] mutation result', result);
161
+ // console.log('[RFQ] mutation result', result);
91
162
  addToast({
92
163
  type: 'success',
93
164
  message: formatMessage({
@@ -108,7 +179,7 @@ const RFQModalForm = (props) => {
108
179
  timeout: 4000
109
180
  });
110
181
  }
111
- }, [uploadedFiles, addToast, formatMessage, handleCreateQuickRfq, setOpen, startDate]);
182
+ }, [uploadedFiles, addToast, formatMessage, handleCreateQuickRfq, setOpen, startDate, toBase64]);
112
183
 
113
184
  return (
114
185
  <>
@@ -271,6 +342,7 @@ const RFQModalForm = (props) => {
271
342
  color="#f76b1c"
272
343
  />
273
344
  <div className='mt-2 text-blue-600'>Drop files or click here to upload</div>
345
+ <div className='mt-1 text-gray-500 text-xs'>Max size per file ~1.5MB (transport limit 2MB)</div>
274
346
  </div>
275
347
  {files && files.length > 0 && (
276
348
  <div className="wfp--dropzone__file-list mt-4">
@@ -288,7 +360,7 @@ const RFQModalForm = (props) => {
288
360
  }}
289
361
  type="button"
290
362
  onClick={() => {
291
- console.log('[RFQ] Cancel clicked, closing modal');
363
+ // console.log('[RFQ] Cancel clicked, closing modal');
292
364
  setOpen(false);
293
365
  }}
294
366
  >
@@ -307,7 +379,7 @@ const RFQModalForm = (props) => {
307
379
  }}
308
380
  type="button"
309
381
  onPress={() => {
310
- console.log('[RFQ] Submit pressed, calling formApi.submitForm()');
382
+ // console.log('[RFQ] Submit pressed, calling formApi.submitForm()');
311
383
  formApi.submitForm();
312
384
  }}
313
385
  disabled={createState?.loading}
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
  import { StoreTitle } from '@magento/venia-ui/lib/components/Head';
4
4
  import Image from '@magento/venia-ui/lib/components/Image';
5
- import { Link, useParams, useLocation } from 'react-router-dom';
5
+ import { useParams, useLocation, useHistory } from 'react-router-dom';
6
6
  import Button from '@magento/venia-ui/lib/components/Button';
7
7
  import ChatContent from '../LiveChat/chatContent';
8
8
  import cn from 'classnames';
@@ -10,16 +10,52 @@ import { Send } from 'iconsax-react';
10
10
  import Price from '@magento/venia-ui/lib/components/Price';
11
11
  import { useRFQ } from '@riosst100/pwa-marketplace/src/talons/RFQ/useRFQ';
12
12
  import LoadingIndicator from '@magento/venia-ui/lib/components/LoadingIndicator';
13
+ import { useToasts } from '@magento/peregrine/lib';
14
+ import { useCartContext } from '@magento/peregrine/lib/context/cart';
15
+ import {
16
+ useApolloClient
17
+ } from '@apollo/client';
13
18
 
14
19
  const quoteDetail = () => {
20
+ const history = useHistory();
21
+
15
22
  const { formatMessage } = useIntl();
16
23
  const { urlKey } = useParams();
17
24
  const location = useLocation();
18
25
  const locationRfq = location && location.state && location.state.rfq ? location.state.rfq : null;
19
- const { loadRfqDetail, rfqDetailState, handleSendRfqMessage, startDetailPolling, stopDetailPolling, handleConvertQuickrfqToCart } = useRFQ();
26
+ const { fetchCartId, addToCartResponseData, isAddProductLoading, errorAddingProductToCart, loadRfqDetail, rfqDetailState, handleSendRfqMessage, startDetailPolling, stopDetailPolling, handleConvertQuickrfqToCart } = useRFQ();
27
+
28
+ const apolloClient = useApolloClient();
29
+ const [{ createCart, removeCart }] = useCartContext();
30
+
31
+ useEffect(async() => {
32
+ if (
33
+ addToCartResponseData
34
+ && !addToCartResponseData?.addProductsToCart?.user_errors.length
35
+ && !isAddProductLoading
36
+ && !errorAddingProductToCart
37
+ ) {
38
+ try {
39
+ await removeCart();
40
+ await apolloClient.clearCacheData(apolloClient, 'cart');
41
+
42
+ // masked_cart_id
43
+ await createCart({ fetchCartId });
44
+
45
+ history.push('/checkout');
46
+ } catch (cleanupErr) {
47
+ // eslint-disable-next-line no-console
48
+ console.log(cleanupErr);
49
+ }
50
+
51
+
52
+ }
53
+ }, [history, fetchCartId, apolloClient, removeCart, createCart, addToCartResponseData, errorAddingProductToCart, isAddProductLoading]);
54
+
20
55
  const [messageText, setMessageText] = useState('');
21
56
  const [isSending, setIsSending] = useState(false);
22
57
  const [sendError, setSendError] = useState(null);
58
+
23
59
  const detailData = rfqDetailState && rfqDetailState.data ? rfqDetailState.data.quickrfqDetail : null;
24
60
  const detailLoading = rfqDetailState ? rfqDetailState.loading : false;
25
61
  const detailError = rfqDetailState ? rfqDetailState.error : null;
@@ -27,10 +63,9 @@ const quoteDetail = () => {
27
63
  id: 'Quotes.pageTitleTextQuoteDetail',
28
64
  defaultMessage: 'Quote Detail'
29
65
  });
30
-
31
- // prefer image from info_buy_request if available; otherwise use placeholder
32
- const urlImage = 'https://pwa-tcgcollective.local:8255/media/catalog/product/s/-/s-l1600_6__1.jpg?auto=webp&format=pjpg&width=495&height=618.75&fit=cover';
33
-
66
+
67
+ const placeholderImage = 'https://pwa-tcgcollective.local:8255/media/catalog/product/s/-/s-l1600_6__1.jpg?auto=webp&format=pjpg&width=495&height=618.75&fit=cover';
68
+
34
69
  useEffect(() => {
35
70
  if (!locationRfq) {
36
71
  const fromParam = urlKey ? parseInt(urlKey, 10) : NaN;
@@ -311,8 +346,8 @@ const quoteDetail = () => {
311
346
  <div className='relative flex w-full md_w-6/12 justify-center flex-col mb-10 md_mb-0'>
312
347
  <Image
313
348
  alt='product image'
314
- className="relative max-w-[300px]"
315
- src={urlImage}
349
+ className="relative max-w-[200px]"
350
+ src={rfq && rfq.image_url ? rfq.image_url : placeholderImage}
316
351
  classes={{
317
352
  root: ' relative self-center mb-5'
318
353
  }}
@@ -334,32 +369,15 @@ const quoteDetail = () => {
334
369
  classes={{
335
370
  content: 'capitalize text-[16px] font-medium'
336
371
  }}
337
- disabled={isConverting || !quickrfqIdValue || isNaN(quickrfqIdValue)}
372
+ disabled={isConverting || !quickrfqIdValue || isNaN(quickrfqIdValue) || status === 'Done'}
338
373
  aria-busy={isConverting}
339
- onPress={async () => {
340
- const qid = quickrfqIdValue;
341
- if (!qid || isNaN(qid)) return;
342
- try {
343
- setConvertError(null);
344
- setConvertSuccess(null);
345
- setIsConverting(true);
346
- const res = await handleConvertQuickrfqToCart(qid);
347
- const payload = res && res.data && res.data.convertQuickrfqToCart ? res.data.convertQuickrfqToCart : null;
348
- if (payload && payload.status) {
349
- setConvertSuccess(payload.message || 'Converted to cart.');
350
- } else {
351
- setConvertError('Failed to convert quote to cart.');
352
- }
353
- } catch (e) {
354
- setConvertError('Failed to convert quote to cart.');
355
- } finally {
356
- setIsConverting(false);
357
- }
358
- }}
374
+ onClick={handleConvertQuickrfqToCart}
359
375
  >
360
376
  {isConverting
361
377
  ? formatMessage({ id: 'Quotes.converting', defaultMessage: 'Add To Cart…' })
362
- : formatMessage({ id: 'Quotes.addToCart', defaultMessage: 'Add To Cart' })}
378
+ : status === 'Done'
379
+ ? formatMessage({ id: 'Quotes.completed', defaultMessage: 'Completed' })
380
+ : formatMessage({ id: 'Quotes.addToCart', defaultMessage: 'Add To Cart' })}
363
381
  </Button>
364
382
  {convertError ? (
365
383
  <div className='text-red-600 text-[12px] mt-2 text-center'>{convertError}</div>
@@ -41,12 +41,13 @@ import ProductLabel from '@riosst100/pwa-marketplace/src/components/ProductLabel
41
41
  import RFQ from '@riosst100/pwa-marketplace/src/components/RFQ';
42
42
  import LinkToOtherStores from '@riosst100/pwa-marketplace/src/components/LinkToOtherStores';
43
43
  import Collapsible from '@riosst100/pwa-marketplace/src/components/commons/Collapsible';
44
- import { useLocation } from 'react-router-dom';
45
44
  import Icon from '@magento/venia-ui/lib/components/Icon';
46
45
  import { ShoppingCart } from 'react-feather';
47
46
  import MessagesModal from '@riosst100/pwa-marketplace/src/components/LiveChat/MessagesModal';
48
47
  import SellerOperations from '@riosst100/pwa-marketplace/src/talons/Seller/seller.gql';
49
48
 
49
+ import { useLocation, useHistory } from 'react-router-dom';
50
+
50
51
  import AgeVerificationModal from '@riosst100/pwa-marketplace/src/components/AgeVerification/ageVerificationModal';
51
52
 
52
53
  import { totalListings, lowestPrice } from '@riosst100/pwa-marketplace/src/components/CrossSeller/crossSellerBuy';
@@ -87,6 +88,8 @@ const ProductDetailsCollapsible = (props) => {
87
88
  const ProductFullDetail = props => {
88
89
  const { product } = props;
89
90
 
91
+ const history = useHistory();
92
+
90
93
  const { search } = useLocation();
91
94
 
92
95
  const params = new URLSearchParams(search);
@@ -174,7 +177,10 @@ const ProductFullDetail = props => {
174
177
  message: formatMessage({ id: 'messages.messageSent', defaultMessage: 'Message sent successfully.' }),
175
178
  timeout: 3000
176
179
  });
177
- await refetchMessages({ fetchPolicy: 'network-only' });
180
+ if (result) {
181
+ var messageId = result?.data?.customerSendMessage.message_id;
182
+ history.push('/messages?id=' + messageId);
183
+ }
178
184
  return result?.data?.customerSendMessage;
179
185
  };
180
186
 
@@ -223,6 +223,10 @@ export const GET_CUSTOMER_SELLER_MESSAGES = gql`
223
223
  total_pages
224
224
  }
225
225
  items {
226
+ seller {
227
+ name
228
+ url_key
229
+ }
226
230
  message_id
227
231
  subject
228
232
  description
@@ -94,6 +94,7 @@ export const GET_QUICK_RFQ_DETAIL_QUERY = gql`
94
94
  date_need_quote
95
95
  email
96
96
  expiry
97
+ image_url
97
98
  info_buy_request
98
99
  messages {
99
100
  created_at
@@ -166,7 +167,14 @@ export const CONVERT_QUICK_RFQ_TO_CART_MUTATION = gql`
166
167
  }
167
168
  `;
168
169
 
170
+ export const CREATE_CART = gql`
171
+ mutation createCart {
172
+ cartId: createEmptyCart
173
+ }
174
+ `;
175
+
169
176
  export default {
177
+ createCartMutation: CREATE_CART,
170
178
  convertQuickrfqToCartMutation: CONVERT_QUICK_RFQ_TO_CART_MUTATION,
171
179
  sendRfqMessageMutation: SEND_RFQ_MESSAGE_MUTATION,
172
180
  getQuickRfqDetailQuery: GET_QUICK_RFQ_DETAIL_QUERY,
@@ -4,14 +4,23 @@ import { useCallback } from 'react';
4
4
  import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
5
5
  import DEFAULT_OPERATIONS from './rfq.gql';
6
6
 
7
+ import { useLocation } from 'react-router-dom';
8
+
7
9
  export const useRFQ = props => {
10
+ const { pathname } = useLocation();
11
+ const rfqQuoteId = pathname.split('/')[2];
12
+
13
+ console.log('rfqQuoteId',rfqQuoteId)
14
+
8
15
  const operations = mergeOperations(DEFAULT_OPERATIONS, props?.operations);
16
+
9
17
  const {
10
18
  getQuickRfqListQuery,
11
19
  getQuickRfqDetailQuery,
12
20
  createQuickRfqMutation,
13
21
  sendRfqMessageMutation,
14
- convertQuickrfqToCartMutation
22
+ convertQuickrfqToCartMutation,
23
+ createCartMutation
15
24
  } = operations;
16
25
 
17
26
  const [fetchRfqList, rfqListState] = useLazyQuery(getQuickRfqListQuery, {
@@ -28,7 +37,20 @@ export const useRFQ = props => {
28
37
 
29
38
  const [createQuickRfq, createState] = useMutation(createQuickRfqMutation);
30
39
  const [sendRfqMessage, sendMessageState] = useMutation(sendRfqMessageMutation);
31
- const [convertQuickrfqToCart, convertState] = useMutation(convertQuickrfqToCartMutation);
40
+
41
+ const [fetchCartId] = useMutation(createCartMutation);
42
+ // const [convertQuickrfqToCart, convertState] = useMutation(convertQuickrfqToCartMutation);
43
+
44
+ const convertState = null;
45
+
46
+ const [
47
+ convertQuickrfqToCart,
48
+ {
49
+ data: addToCartResponseData,
50
+ error: errorAddingProductToCart,
51
+ loading: isAddProductLoading
52
+ }
53
+ ] = useMutation(convertQuickrfqToCartMutation);
32
54
 
33
55
  const loadRfqList = useCallback(
34
56
  variables => fetchRfqList({ variables }),
@@ -57,12 +79,15 @@ export const useRFQ = props => {
57
79
  [sendRfqMessage]
58
80
  );
59
81
 
60
- const handleConvertQuickrfqToCart = useCallback(
61
- quickrfqId => convertQuickrfqToCart({ variables: { quickrfqId } }),
62
- [convertQuickrfqToCart]
63
- );
82
+ const handleConvertQuickrfqToCart = useCallback(() => {
83
+ convertQuickrfqToCart({ variables: { quickrfqId: rfqQuoteId } })
84
+ }, [convertQuickrfqToCart, rfqQuoteId]);
64
85
 
65
86
  return {
87
+ fetchCartId,
88
+ addToCartResponseData,
89
+ isAddProductLoading,
90
+ errorAddingProductToCart,
66
91
  loadRfqList,
67
92
  rfqListState,
68
93
  loadRfqDetail,
@@ -154,7 +154,10 @@ export const useSeller = props => {
154
154
  }),
155
155
  timeout: 3000
156
156
  });
157
- history.push('/messages');
157
+ if (result) {
158
+ var messageId = result?.data?.customerSendMessage.message_id;
159
+ history.push('/messages?id=' + messageId);
160
+ }
158
161
  return result?.data?.customerSendMessage;
159
162
  }, [sendMessage, refetchMessages, addToast, formatMessage]);
160
163