@riosst100/pwa-marketplace 3.2.7 → 3.2.9
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 +1 -1
- package/src/components/LiveChat/MessagesModal.js +33 -146
- package/src/components/LiveChat/liveChat.js +12 -42
- package/src/components/Messages/index.js +1 -0
- package/src/components/Messages/messages.js +352 -0
- package/src/components/Messages/messages.module.css +36 -0
- package/src/components/Messages/messagesPage.js +345 -0
- package/src/components/MessagesPage/index.js +1 -0
- package/src/components/MessagesPage/messagesPage.js +50 -0
- package/src/components/MessagesPage/messagesPage.module.css +35 -0
- package/src/components/RFQPage/quoteDetail.js +39 -14
- package/src/intercept.js +7 -0
- package/src/overwrites/venia-ui/lib/components/Footer/footer.js +12 -3
- package/src/talons/Messages/useMessages.js +0 -0
- package/src/talons/MessagesPage/messagesPage.gql.js +299 -0
- package/src/talons/MessagesPage/useMessagesPage.js +87 -0
- package/src/talons/RFQ/rfq.gql.js +13 -0
- package/src/talons/Seller/useSeller.js +4 -2
- package/src/talons/SellerMegaMenu/useSellerMegaMenu.js +0 -11
|
@@ -0,0 +1,352 @@
|
|
|
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
|
+
|
|
12
|
+
const Messages = props => {
|
|
13
|
+
const {
|
|
14
|
+
messages,
|
|
15
|
+
seller,
|
|
16
|
+
onReply,
|
|
17
|
+
onDelete,
|
|
18
|
+
onSend,
|
|
19
|
+
loading } = props;
|
|
20
|
+
const classes = useStyle(defaultClasses, props.classes);
|
|
21
|
+
// const { becomeSellerProps } = useMessages(props);
|
|
22
|
+
const { formatMessage } = useIntl();
|
|
23
|
+
|
|
24
|
+
const [selectedThread, setSelectedThread] = useState(null);
|
|
25
|
+
const [replyText, setReplyText] = useState('');
|
|
26
|
+
const [newSubject, setNewSubject] = useState('');
|
|
27
|
+
const [newContent, setNewContent] = useState('');
|
|
28
|
+
const [isComposing, setIsComposing] = useState(false);
|
|
29
|
+
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
|
|
30
|
+
const [selectLatestAfterSend, setSelectLatestAfterSend] = useState(false);
|
|
31
|
+
const [isSendingNew, setIsSendingNew] = useState(false);
|
|
32
|
+
const [isReplying, setIsReplying] = useState(false);
|
|
33
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
34
|
+
|
|
35
|
+
const renderStatusBadge = (statusValue) => {
|
|
36
|
+
const s = Number(statusValue);
|
|
37
|
+
const map = {
|
|
38
|
+
0: { label: formatMessage({ id: 'messages.status.closed', defaultMessage: 'Closed' }), bg: 'bg-red-100', text: 'text-red-700', dot: 'bg-red-500' },
|
|
39
|
+
1: { label: formatMessage({ id: 'messages.status.open', defaultMessage: 'Open' }), bg: 'bg-green-100', text: 'text-green-700', dot: 'bg-green-500' },
|
|
40
|
+
2: { label: formatMessage({ id: 'messages.status.processing', defaultMessage: 'Processing' }), bg: 'bg-yellow-300/20', text: 'text-yellow-300', dot: 'bg-yellow-300' },
|
|
41
|
+
3: { label: formatMessage({ id: 'messages.status.done', defaultMessage: 'Done' }), bg: 'bg-[#3d84ff1a]', text: 'text-[#3d84ff]', dot: 'bg-[#3d84ff]' }
|
|
42
|
+
};
|
|
43
|
+
const conf = map[s] || { label: 'Unknown', bg: 'bg-gray-200', text: 'text-gray-700', dot: 'bg-gray-500' };
|
|
44
|
+
return (
|
|
45
|
+
<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`}>
|
|
46
|
+
<span className={`inline-block w-[8px] h-[8px] rounded-full ${conf.dot}`}></span>
|
|
47
|
+
{conf.label}
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Filter pesan yang terkait seller ini (perkiraan berdasarkan id terkait)
|
|
53
|
+
const threads = useMemo(() => {
|
|
54
|
+
const items = messages?.items || [];
|
|
55
|
+
if (!seller?.seller_id) return items;
|
|
56
|
+
return items.filter(t =>
|
|
57
|
+
t?.owner_id === seller.seller_id ||
|
|
58
|
+
t?.receiver_id === seller.seller_id ||
|
|
59
|
+
t?.sender_id === seller.seller_id
|
|
60
|
+
);
|
|
61
|
+
}, [messages, seller]);
|
|
62
|
+
|
|
63
|
+
const sortedThreads = useMemo(() => {
|
|
64
|
+
return [...threads].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
65
|
+
}, [threads]);
|
|
66
|
+
|
|
67
|
+
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
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, [sortedThreads, isComposing, selectedThread, selectLatestAfterSend]);
|
|
87
|
+
|
|
88
|
+
// Tandai sudah pernah load minimal sekali untuk mencegah flicker
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (threads.length > 0 || (!loading && messages)) {
|
|
91
|
+
setHasLoadedOnce(true);
|
|
92
|
+
}
|
|
93
|
+
}, [threads.length, loading, messages]);
|
|
94
|
+
|
|
95
|
+
const chatData = useMemo(() => {
|
|
96
|
+
const details = selectedThread?.details?.items || [];
|
|
97
|
+
const sorted = [...details].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
98
|
+
const opening = selectedThread ? [{
|
|
99
|
+
message: selectedThread.description,
|
|
100
|
+
senderName: selectedThread.sender_name,
|
|
101
|
+
timeStamp: selectedThread.created_at ? new Date(selectedThread.created_at).toLocaleString() : '',
|
|
102
|
+
type: selectedThread.sender_name === seller?.name ? 'seller' : 'customer'
|
|
103
|
+
}] : [];
|
|
104
|
+
const mapped = sorted.map(d => ({
|
|
105
|
+
message: d.content,
|
|
106
|
+
senderName: d.sender_name,
|
|
107
|
+
timeStamp: new Date(d.created_at).toLocaleString(),
|
|
108
|
+
type: d.sender_name === seller?.name ? 'seller' : 'customer'
|
|
109
|
+
}));
|
|
110
|
+
return [...opening, ...mapped];
|
|
111
|
+
}, [selectedThread, seller]);
|
|
112
|
+
|
|
113
|
+
const handleSubmitReply = async (e) => {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
if (!selectedThread?.message_id || !replyText.trim()) return;
|
|
116
|
+
try {
|
|
117
|
+
setIsReplying(true);
|
|
118
|
+
const res = await onReply({ message_id: selectedThread.message_id, content: replyText.trim() });
|
|
119
|
+
// Optimistic local append so the reply appears instantly
|
|
120
|
+
const newDetail = {
|
|
121
|
+
content: res?.content || replyText.trim(),
|
|
122
|
+
sender_name: res?.sender_name || selectedThread?.sender_name || 'You',
|
|
123
|
+
sender_email: res?.sender_email || '',
|
|
124
|
+
receiver_name: res?.receiver_name || '',
|
|
125
|
+
is_read: true,
|
|
126
|
+
created_at: res?.created_at || new Date().toISOString()
|
|
127
|
+
};
|
|
128
|
+
const nextThread = {
|
|
129
|
+
...selectedThread,
|
|
130
|
+
details: {
|
|
131
|
+
...selectedThread.details,
|
|
132
|
+
items: [...(selectedThread.details?.items || []), newDetail],
|
|
133
|
+
total_count: (selectedThread.details?.total_count || 0) + 1
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
setSelectedThread(nextThread);
|
|
137
|
+
} finally {
|
|
138
|
+
setIsReplying(false);
|
|
139
|
+
}
|
|
140
|
+
setReplyText('');
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleDeleteThread = async () => {
|
|
144
|
+
if (!selectedThread?.message_id) return;
|
|
145
|
+
try {
|
|
146
|
+
setIsDeleting(true);
|
|
147
|
+
await onDelete({ id: selectedThread.message_id });
|
|
148
|
+
} finally {
|
|
149
|
+
setIsDeleting(false);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleSubmitNewMessage = async (e) => {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
if (!newSubject.trim() || !newContent.trim()) return;
|
|
156
|
+
try {
|
|
157
|
+
setIsSendingNew(true);
|
|
158
|
+
await onSend({ subject: newSubject.trim(), content: newContent.trim(), seller_url: seller?.url_key });
|
|
159
|
+
} finally {
|
|
160
|
+
setIsSendingNew(false);
|
|
161
|
+
}
|
|
162
|
+
setNewSubject('');
|
|
163
|
+
setNewContent('');
|
|
164
|
+
setIsComposing(false);
|
|
165
|
+
setSelectLatestAfterSend(true);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<div className={cn(classes.leftContent, '')}>
|
|
171
|
+
<StoreTitle>
|
|
172
|
+
{formatMessage({
|
|
173
|
+
id: 'messagesPage.title',
|
|
174
|
+
defaultMessage: 'Messages'
|
|
175
|
+
})}
|
|
176
|
+
</StoreTitle>
|
|
177
|
+
<div className={cn(classes.leftContentContainer, 'border !border-gray-100 lg_rounded-md !rounded-lg')}>
|
|
178
|
+
<div class="auth-left">
|
|
179
|
+
<div className='px-3 py-2 border-b font-medium flex items-center justify-between'>
|
|
180
|
+
<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
|
+
</div>
|
|
192
|
+
<div className='max-h-[420px] overflow-y-auto'>
|
|
193
|
+
{(!isComposing && !hasLoadedOnce && loading && sortedThreads.length === 0) ? (
|
|
194
|
+
<div className='p-3 text-sm text-gray-500'>{formatMessage({ id: 'messages.loading', defaultMessage: 'Loading…' })}</div>
|
|
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
|
+
))
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<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">
|
|
219
|
+
{/* 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'>
|
|
249
|
+
<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
|
+
>
|
|
255
|
+
{isSendingNew
|
|
256
|
+
? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Send Message...' })
|
|
257
|
+
: formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
|
|
258
|
+
</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>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
{!isComposing && sortedThreads.length > 0 && (
|
|
288
|
+
<div className='!border-gray-100 border rounded-[6px] p-3 flex-1 min-h-[320px]'>
|
|
289
|
+
{selectedThread ? (
|
|
290
|
+
<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>
|
|
295
|
+
)}
|
|
296
|
+
{!isComposing && sortedThreads.length > 0 && selectedThread && (
|
|
297
|
+
<form onSubmit={handleSubmitReply} className='mt-3 flex gap-2'>
|
|
298
|
+
{(() => {
|
|
299
|
+
const statusNum = Number(selectedThread.status);
|
|
300
|
+
const isReplyEnabled = statusNum === 1 || statusNum === 2;
|
|
301
|
+
const disabledReason = statusNum === 0
|
|
302
|
+
? formatMessage({ id: 'messages.reply.disabled.closed', defaultMessage: 'Conversation closed' })
|
|
303
|
+
: statusNum === 3
|
|
304
|
+
? formatMessage({ id: 'messages.reply.disabled.done', defaultMessage: 'Conversation finished' })
|
|
305
|
+
: formatMessage({ id: 'messages.reply.disabled.generic', defaultMessage: 'Cannot send a reply' });
|
|
306
|
+
return (
|
|
307
|
+
<>
|
|
308
|
+
<input
|
|
309
|
+
type='text'
|
|
310
|
+
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'
|
|
311
|
+
placeholder={isReplyEnabled ? formatMessage({ id: 'messages.reply.placeholder', defaultMessage: 'Write a reply…' }) : disabledReason}
|
|
312
|
+
value={replyText}
|
|
313
|
+
onChange={(e) => setReplyText(e.target.value)}
|
|
314
|
+
disabled={!isReplyEnabled}
|
|
315
|
+
title={!isReplyEnabled ? disabledReason : undefined}
|
|
316
|
+
aria-disabled={!isReplyEnabled}
|
|
317
|
+
/>
|
|
318
|
+
<button
|
|
319
|
+
type='submit'
|
|
320
|
+
disabled={!isReplyEnabled || isReplying}
|
|
321
|
+
title={!isReplyEnabled ? disabledReason : undefined}
|
|
322
|
+
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]')}
|
|
323
|
+
aria-busy={isReplying}
|
|
324
|
+
>
|
|
325
|
+
{isReplying
|
|
326
|
+
? formatMessage({ id: 'messages.reply.sending', defaultMessage: 'Sending...' })
|
|
327
|
+
: formatMessage({ id: 'messages.reply.send', defaultMessage: 'Send' })}
|
|
328
|
+
</button>
|
|
329
|
+
</>
|
|
330
|
+
);
|
|
331
|
+
})()}
|
|
332
|
+
</form>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export default Messages;
|
|
341
|
+
|
|
342
|
+
Messages.defaultProps = {
|
|
343
|
+
signedInRedirectUrl: '/messages',
|
|
344
|
+
signInPageUrl: '/sign-in'
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
Messages.propTypes = {
|
|
348
|
+
classes: shape({
|
|
349
|
+
root: string
|
|
350
|
+
}),
|
|
351
|
+
signedInRedirectUrl: string
|
|
352
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
.rootContainer {
|
|
33
|
+
justify-content: space-around;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: normal;
|
|
36
|
+
}
|