@shoppexio/storefront-react 0.0.0 → 0.1.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/account.css +1171 -0
- package/dist/account.d.ts +23 -0
- package/dist/account.js +12685 -0
- package/dist/account.js.map +1 -0
- package/dist/chunk-D6XKDVAV.js +181 -0
- package/dist/chunk-D6XKDVAV.js.map +1 -0
- package/dist/index.d.ts +3087 -0
- package/dist/index.js +2801 -0
- package/dist/index.js.map +1 -0
- package/eject/account/account-availability.ts +171 -0
- package/eject/account/account-downloads.ts +118 -0
- package/eject/account/account-favorites.ts +99 -0
- package/eject/account/account-loyalty.ts +241 -0
- package/eject/account/account-page.tsx +410 -0
- package/eject/account/account-payment-method.ts +99 -0
- package/eject/account/account-referrals.ts +68 -0
- package/eject/account/account-reseller-invite.ts +88 -0
- package/eject/account/account-reseller.ts +685 -0
- package/eject/account/account-rewards.ts +104 -0
- package/eject/account/account-settings.ts +181 -0
- package/eject/account/account-signin-link.ts +122 -0
- package/eject/account/account-subscriptions.ts +130 -0
- package/eject/account/account-support.ts +155 -0
- package/eject/account/account-warranties.ts +118 -0
- package/eject/account/customer-account.ts +93 -0
- package/eject/account/dashboard.tsx +139 -0
- package/eject/account/downloads.tsx +343 -0
- package/eject/account/favorites.tsx +201 -0
- package/eject/account/i18n.ts +175 -0
- package/eject/account/icons.tsx +218 -0
- package/eject/account/login.tsx +253 -0
- package/eject/account/messages/ar.json +464 -0
- package/eject/account/messages/de.json +464 -0
- package/eject/account/messages/en.json +464 -0
- package/eject/account/messages/es.json +464 -0
- package/eject/account/messages/fi.json +464 -0
- package/eject/account/messages/fr.json +464 -0
- package/eject/account/messages/hi.json +464 -0
- package/eject/account/messages/it.json +464 -0
- package/eject/account/messages/lt.json +464 -0
- package/eject/account/messages/nl.json +464 -0
- package/eject/account/messages/pl.json +464 -0
- package/eject/account/messages/pt.json +464 -0
- package/eject/account/messages/ru.json +464 -0
- package/eject/account/messages/tr.json +464 -0
- package/eject/account/messages/uk.json +464 -0
- package/eject/account/messages/zh.json +464 -0
- package/eject/account/orders.tsx +791 -0
- package/eject/account/overview.tsx +147 -0
- package/eject/account/referrals.tsx +327 -0
- package/eject/account/reseller.tsx +1365 -0
- package/eject/account/rewards.tsx +641 -0
- package/eject/account/settings.tsx +699 -0
- package/eject/account/shared.tsx +398 -0
- package/eject/account/subscriptions.tsx +421 -0
- package/eject/account/support.tsx +453 -0
- package/package.json +75 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The buyer's conversations with the shop.
|
|
3
|
+
*
|
|
4
|
+
* The ticket LIST arrives inside the dashboard payload — `tickets.routes.ts`
|
|
5
|
+
* publishes per-ticket routes and no collection — while a thread is a read of
|
|
6
|
+
* its own. Opening a ticket and replying to one are the only writes this lane
|
|
7
|
+
* admits. Closing is the merchant's, so a closed ticket loses its reply box
|
|
8
|
+
* rather than gaining a control the producer answers 400 to.
|
|
9
|
+
*
|
|
10
|
+
* Three panels, three states each: a skeleton while a read is out, the
|
|
11
|
+
* producer's own sentence plus a retry when one fails, and an empty state that
|
|
12
|
+
* still offers the one thing a buyer with no tickets wants — a new ticket.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useId, useState } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
isOwnMessage,
|
|
18
|
+
isSendableMessage,
|
|
19
|
+
isTicketClosed,
|
|
20
|
+
messageAuthorKey,
|
|
21
|
+
narrowSupportTicket,
|
|
22
|
+
readCreatedTicketUniqid,
|
|
23
|
+
ticketTitleToSend,
|
|
24
|
+
type SupportTicket,
|
|
25
|
+
} from './account-support';
|
|
26
|
+
import { callCustomerApi } from './customer-account';
|
|
27
|
+
import { t } from './i18n';
|
|
28
|
+
import { MessageSquareIcon } from './icons';
|
|
29
|
+
import { useAccountDashboard } from './dashboard';
|
|
30
|
+
import {
|
|
31
|
+
AccountBackButton,
|
|
32
|
+
AccountCard,
|
|
33
|
+
AccountEmpty,
|
|
34
|
+
AccountError,
|
|
35
|
+
AccountFieldError,
|
|
36
|
+
AccountRowsSkeleton,
|
|
37
|
+
AccountSectionTitle,
|
|
38
|
+
BusySpinner,
|
|
39
|
+
StatusChip,
|
|
40
|
+
formatAccountDateTime,
|
|
41
|
+
useAccountResource,
|
|
42
|
+
} from './shared';
|
|
43
|
+
|
|
44
|
+
/** The contact form's control skin — see `components/contact/contact-form.tsx`. */
|
|
45
|
+
const FIELD =
|
|
46
|
+
'sx-account-field sxa:w-full sxa:rounded-card sxa:border sxa:bg-[color-mix(in_oklab,_var(--sx-bg)_40%,_transparent)] sxa:px-3 sxa:py-2.5 sxa:text-small sxa:shadow-card sxa:placeholder:text-muted sxa:focus-visible:border-accent sxa:focus-visible:ring-[3px] sxa:focus-visible:ring-[color-mix(in_oklab,_var(--sx-accent)_50%,_transparent)] sxa:focus-visible:outline-none';
|
|
47
|
+
|
|
48
|
+
type Mode =
|
|
49
|
+
| { name: 'list' }
|
|
50
|
+
| { name: 'create' }
|
|
51
|
+
| { name: 'thread'; uniqid: string };
|
|
52
|
+
|
|
53
|
+
export function AccountSupport() {
|
|
54
|
+
const { state, reload } = useAccountDashboard();
|
|
55
|
+
const [mode, setMode] = useState<Mode>({ name: 'list' });
|
|
56
|
+
|
|
57
|
+
if (state.status === 'loading') {
|
|
58
|
+
return <AccountRowsSkeleton rows={3} label={t('support.loading')} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (state.status === 'error') {
|
|
62
|
+
return <AccountError message={state.message} onRetry={reload} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tickets = state.data.tickets;
|
|
66
|
+
|
|
67
|
+
if (mode.name === 'create') {
|
|
68
|
+
return (
|
|
69
|
+
<CreateTicket
|
|
70
|
+
canGoBack={tickets.length > 0}
|
|
71
|
+
onCancel={() => setMode({ name: 'list' })}
|
|
72
|
+
onCreated={(uniqid) => {
|
|
73
|
+
// The list lives in the dashboard payload, so a new ticket only
|
|
74
|
+
// exists here once that payload has been read again.
|
|
75
|
+
reload();
|
|
76
|
+
setMode({ name: 'thread', uniqid });
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (mode.name === 'thread') {
|
|
83
|
+
return (
|
|
84
|
+
<SupportThread
|
|
85
|
+
uniqid={mode.uniqid}
|
|
86
|
+
onBack={() => setMode({ name: 'list' })}
|
|
87
|
+
onReplied={reload}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (tickets.length === 0) {
|
|
93
|
+
return (
|
|
94
|
+
<AccountEmpty
|
|
95
|
+
title={t('support.emptyTitle')}
|
|
96
|
+
body={t('support.emptyBody')}
|
|
97
|
+
action={(
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className="sx-account-cta sxa:cta-button sxa:inline-flex sxa:h-10 sxa:items-center sxa:px-5 sxa:text-small sxa:font-semibold"
|
|
101
|
+
onClick={() => setMode({ name: 'create' })}
|
|
102
|
+
>
|
|
103
|
+
{t('support.newTicket')}
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div>
|
|
112
|
+
<div className="sxa:flex sxa:flex-wrap sxa:items-center sxa:justify-between sxa:gap-3">
|
|
113
|
+
<AccountSectionTitle>{t('support.ticketsTitle')}</AccountSectionTitle>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
className="sx-account-button sxa:card-button sxa:card-button--add"
|
|
117
|
+
onClick={() => setMode({ name: 'create' })}
|
|
118
|
+
>
|
|
119
|
+
{t('support.newTicket')}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<ul className="sxa:mt-4 sxa:space-y-3">
|
|
124
|
+
{tickets.map((ticket) => (
|
|
125
|
+
<li key={ticket.uniqid}>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={() => setMode({ name: 'thread', uniqid: ticket.uniqid })}
|
|
129
|
+
className="sxa:flex sxa:w-full sxa:items-center sxa:gap-4 sxa:rounded-input sxa:border sxa:bg-surface sxa:p-5 sxa:text-left sxa:shadow-card sxa:transition-colors sxa:hover:bg-raised"
|
|
130
|
+
>
|
|
131
|
+
<MessageSquareIcon className="sxa:h-4 sxa:w-4 sxa:shrink-0 sxa:text-muted" />
|
|
132
|
+
<div className="sxa:min-w-0 sxa:flex-1">
|
|
133
|
+
<p className="sxa:truncate sxa:text-small sxa:font-semibold">{ticket.title}</p>
|
|
134
|
+
<p className="sxa:mt-1 sxa:text-micro sxa:text-muted">
|
|
135
|
+
{t('support.updated', {
|
|
136
|
+
date: formatAccountDateTime(ticket.updated_at),
|
|
137
|
+
})}
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
<StatusChip status={ticket.status} />
|
|
141
|
+
</button>
|
|
142
|
+
</li>
|
|
143
|
+
))}
|
|
144
|
+
</ul>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function CreateTicket({
|
|
150
|
+
canGoBack,
|
|
151
|
+
onCancel,
|
|
152
|
+
onCreated,
|
|
153
|
+
}: {
|
|
154
|
+
canGoBack: boolean;
|
|
155
|
+
onCancel: () => void;
|
|
156
|
+
onCreated: (uniqid: string) => void;
|
|
157
|
+
}) {
|
|
158
|
+
const fieldId = useId();
|
|
159
|
+
const [title, setTitle] = useState('');
|
|
160
|
+
const [message, setMessage] = useState('');
|
|
161
|
+
const [busy, setBusy] = useState(false);
|
|
162
|
+
const [error, setError] = useState<string | null>(null);
|
|
163
|
+
|
|
164
|
+
const submit = async () => {
|
|
165
|
+
if (!isSendableMessage(message)) {
|
|
166
|
+
setError(t('support.tooShort'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const subject = ticketTitleToSend(title);
|
|
171
|
+
|
|
172
|
+
setBusy(true);
|
|
173
|
+
setError(null);
|
|
174
|
+
const result = await callCustomerApi((sdk) => sdk.createTicket({
|
|
175
|
+
...(subject === undefined ? {} : { title: subject }),
|
|
176
|
+
message: message.trim(),
|
|
177
|
+
}));
|
|
178
|
+
setBusy(false);
|
|
179
|
+
|
|
180
|
+
if (!result.success) {
|
|
181
|
+
setError(result.message?.trim() || t('support.createFailed'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const uniqid = readCreatedTicketUniqid(result.data);
|
|
186
|
+
if (uniqid === null) {
|
|
187
|
+
// The ticket may well exist, but without its id there is nothing to
|
|
188
|
+
// open — saying so beats sending the buyer into a blank thread.
|
|
189
|
+
setError(t('support.createFailed'));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
onCreated(uniqid);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div>
|
|
198
|
+
{canGoBack ? (
|
|
199
|
+
<AccountBackButton onClick={onCancel} label={t('support.backToTickets')} />
|
|
200
|
+
) : null}
|
|
201
|
+
|
|
202
|
+
<AccountCard className={`sxa:p-6 ${canGoBack ? 'sxa:mt-5' : ''}`}>
|
|
203
|
+
<form
|
|
204
|
+
noValidate
|
|
205
|
+
onSubmit={(event) => {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
void submit();
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<label htmlFor={`${fieldId}-title`} className="sxa:mb-1.5 sxa:block sxa:text-small sxa:font-medium">
|
|
211
|
+
{t('support.subjectLabel')}
|
|
212
|
+
<span className="sxa:ml-2 sxa:text-micro sxa:font-normal sxa:text-muted">
|
|
213
|
+
{t('support.subjectOptional')}
|
|
214
|
+
</span>
|
|
215
|
+
</label>
|
|
216
|
+
<input
|
|
217
|
+
id={`${fieldId}-title`}
|
|
218
|
+
type="text"
|
|
219
|
+
value={title}
|
|
220
|
+
disabled={busy}
|
|
221
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
222
|
+
className={FIELD}
|
|
223
|
+
/>
|
|
224
|
+
|
|
225
|
+
<label
|
|
226
|
+
htmlFor={`${fieldId}-message`}
|
|
227
|
+
className="sxa:mt-5 sxa:mb-1.5 sxa:block sxa:text-small sxa:font-medium"
|
|
228
|
+
>
|
|
229
|
+
{t('support.messageLabel')}
|
|
230
|
+
</label>
|
|
231
|
+
<textarea
|
|
232
|
+
id={`${fieldId}-message`}
|
|
233
|
+
rows={5}
|
|
234
|
+
value={message}
|
|
235
|
+
disabled={busy}
|
|
236
|
+
aria-invalid={error ? true : undefined}
|
|
237
|
+
onChange={(event) => {
|
|
238
|
+
setMessage(event.target.value);
|
|
239
|
+
setError(null);
|
|
240
|
+
}}
|
|
241
|
+
className={`${FIELD} sxa:resize-none`}
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
{error ? <AccountFieldError message={error} /> : null}
|
|
245
|
+
|
|
246
|
+
<button
|
|
247
|
+
type="submit"
|
|
248
|
+
disabled={busy}
|
|
249
|
+
aria-busy={busy || undefined}
|
|
250
|
+
className="sx-account-cta sxa:cta-button sxa:mt-5 sxa:inline-flex sxa:h-10 sxa:items-center sxa:px-5 sxa:text-small sxa:font-semibold sxa:disabled:cursor-not-allowed sxa:disabled:opacity-50"
|
|
251
|
+
>
|
|
252
|
+
{busy ? <BusySpinner className="sxa:mr-1.5" /> : null}
|
|
253
|
+
{busy ? t('support.creating') : t('support.create')}
|
|
254
|
+
</button>
|
|
255
|
+
</form>
|
|
256
|
+
</AccountCard>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function SupportThread({
|
|
262
|
+
uniqid,
|
|
263
|
+
onBack,
|
|
264
|
+
onReplied,
|
|
265
|
+
}: {
|
|
266
|
+
uniqid: string;
|
|
267
|
+
onBack: () => void;
|
|
268
|
+
onReplied: () => void;
|
|
269
|
+
}) {
|
|
270
|
+
const { state, reload } = useAccountResource(`ticket:${uniqid}`, () =>
|
|
271
|
+
callCustomerApi((sdk) => sdk.ticket(uniqid)));
|
|
272
|
+
|
|
273
|
+
const back = <AccountBackButton onClick={onBack} label={t('support.backToTickets')} />;
|
|
274
|
+
|
|
275
|
+
if (state.status === 'loading') {
|
|
276
|
+
return (
|
|
277
|
+
<div>
|
|
278
|
+
{back}
|
|
279
|
+
<div className="sxa:mt-5">
|
|
280
|
+
<AccountRowsSkeleton rows={3} label={t('support.loadingThread')} />
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (state.status === 'error') {
|
|
287
|
+
return (
|
|
288
|
+
<div>
|
|
289
|
+
{back}
|
|
290
|
+
<div className="sxa:mt-5">
|
|
291
|
+
<AccountError message={state.message} onRetry={reload} />
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const narrowed = narrowSupportTicket(state.data);
|
|
298
|
+
if (!narrowed.ok) {
|
|
299
|
+
// A thread missing one message reads as the merchant never having answered,
|
|
300
|
+
// so a partial read is not shown at all.
|
|
301
|
+
return (
|
|
302
|
+
<div>
|
|
303
|
+
{back}
|
|
304
|
+
<div className="sxa:mt-5">
|
|
305
|
+
<AccountError message={t('common.genericError')} onRetry={reload} />
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div>
|
|
313
|
+
{back}
|
|
314
|
+
<ThreadBody
|
|
315
|
+
ticket={narrowed.ticket}
|
|
316
|
+
onReplied={() => {
|
|
317
|
+
// Both reads move: the thread gains a message, and the dashboard's
|
|
318
|
+
// row for it gains a new status and "updated" time.
|
|
319
|
+
reload();
|
|
320
|
+
onReplied();
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function ThreadBody({
|
|
328
|
+
ticket,
|
|
329
|
+
onReplied,
|
|
330
|
+
}: {
|
|
331
|
+
ticket: SupportTicket;
|
|
332
|
+
onReplied: () => void;
|
|
333
|
+
}) {
|
|
334
|
+
const replyId = useId();
|
|
335
|
+
const [reply, setReply] = useState('');
|
|
336
|
+
const [busy, setBusy] = useState(false);
|
|
337
|
+
const [error, setError] = useState<string | null>(null);
|
|
338
|
+
const closed = isTicketClosed(ticket.status);
|
|
339
|
+
|
|
340
|
+
const send = async () => {
|
|
341
|
+
if (!isSendableMessage(reply)) {
|
|
342
|
+
setError(t('support.tooShort'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
setBusy(true);
|
|
347
|
+
setError(null);
|
|
348
|
+
const result = await callCustomerApi((sdk) => sdk.replyToTicket(ticket.uniqid, reply.trim()));
|
|
349
|
+
setBusy(false);
|
|
350
|
+
|
|
351
|
+
if (!result.success) {
|
|
352
|
+
setError(result.message?.trim() || t('support.replyFailed'));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
setReply('');
|
|
357
|
+
onReplied();
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div className="sxa:mt-5">
|
|
362
|
+
<div className="sxa:flex sxa:flex-wrap sxa:items-start sxa:justify-between sxa:gap-4">
|
|
363
|
+
<div className="sxa:min-w-0">
|
|
364
|
+
<h2 className="sxa:font-heading sxa:truncate sxa:text-h3 sxa:font-semibold">
|
|
365
|
+
{ticket.title ?? t('support.untitled')}
|
|
366
|
+
</h2>
|
|
367
|
+
<p className="sxa:mt-1 sxa:text-small sxa:text-muted">
|
|
368
|
+
{t('support.updated', { date: formatAccountDateTime(ticket.updated_at) })}
|
|
369
|
+
</p>
|
|
370
|
+
{ticket.invoice_id ? (
|
|
371
|
+
<p className="sxa:mt-1 sxa:text-small sxa:text-muted">
|
|
372
|
+
{t('support.aboutOrder', { id: ticket.invoice_id })}
|
|
373
|
+
</p>
|
|
374
|
+
) : null}
|
|
375
|
+
</div>
|
|
376
|
+
<StatusChip status={ticket.status} />
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
{ticket.messages.length === 0 ? (
|
|
380
|
+
<p className="sxa:mt-5 sxa:text-small sxa:leading-relaxed sxa:text-muted">
|
|
381
|
+
{t('support.noMessages')}
|
|
382
|
+
</p>
|
|
383
|
+
) : (
|
|
384
|
+
<ul className="sxa:mt-5 sxa:space-y-3">
|
|
385
|
+
{ticket.messages.map((message) => (
|
|
386
|
+
<li key={message.id}>
|
|
387
|
+
<AccountCard className="sxa:p-5">
|
|
388
|
+
<div className="sxa:flex sxa:flex-wrap sxa:items-baseline sxa:justify-between sxa:gap-2">
|
|
389
|
+
<p
|
|
390
|
+
className={`sxa:text-small sxa:font-semibold ${isOwnMessage(message.role) ? 'sxa:text-accent' : ''}`}
|
|
391
|
+
>
|
|
392
|
+
{t(messageAuthorKey(message.role))}
|
|
393
|
+
</p>
|
|
394
|
+
<p className="sxa:text-micro sxa:text-muted">
|
|
395
|
+
{formatAccountDateTime(message.created_at)}
|
|
396
|
+
</p>
|
|
397
|
+
</div>
|
|
398
|
+
{/* A written message keeps its own line breaks. */}
|
|
399
|
+
<p className="sxa:mt-2 sxa:text-small sxa:leading-relaxed sxa:text-muted sxa:whitespace-pre-wrap">
|
|
400
|
+
{message.message}
|
|
401
|
+
</p>
|
|
402
|
+
</AccountCard>
|
|
403
|
+
</li>
|
|
404
|
+
))}
|
|
405
|
+
</ul>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{closed ? (
|
|
409
|
+
<AccountCard className="sxa:mt-5 sxa:p-5">
|
|
410
|
+
<p className="sxa:text-small sxa:font-semibold">{t('support.closedTitle')}</p>
|
|
411
|
+
<p className="sxa:mt-1 sxa:text-small sxa:leading-relaxed sxa:text-muted">
|
|
412
|
+
{t('support.closedBody')}
|
|
413
|
+
</p>
|
|
414
|
+
</AccountCard>
|
|
415
|
+
) : (
|
|
416
|
+
<form
|
|
417
|
+
noValidate
|
|
418
|
+
className="sxa:mt-5"
|
|
419
|
+
onSubmit={(event) => {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
void send();
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
<label htmlFor={replyId} className="sxa:mb-1.5 sxa:block sxa:text-small sxa:font-medium">
|
|
425
|
+
{t('support.replyLabel')}
|
|
426
|
+
</label>
|
|
427
|
+
<textarea
|
|
428
|
+
id={replyId}
|
|
429
|
+
rows={4}
|
|
430
|
+
value={reply}
|
|
431
|
+
disabled={busy}
|
|
432
|
+
aria-invalid={error ? true : undefined}
|
|
433
|
+
onChange={(event) => {
|
|
434
|
+
setReply(event.target.value);
|
|
435
|
+
setError(null);
|
|
436
|
+
}}
|
|
437
|
+
className={`${FIELD} sxa:resize-none`}
|
|
438
|
+
/>
|
|
439
|
+
{error ? <AccountFieldError message={error} /> : null}
|
|
440
|
+
<button
|
|
441
|
+
type="submit"
|
|
442
|
+
disabled={busy}
|
|
443
|
+
aria-busy={busy || undefined}
|
|
444
|
+
className="sx-account-cta sxa:cta-button sxa:mt-4 sxa:inline-flex sxa:h-10 sxa:items-center sxa:px-5 sxa:text-small sxa:font-semibold sxa:disabled:cursor-not-allowed sxa:disabled:opacity-50"
|
|
445
|
+
>
|
|
446
|
+
{busy ? <BusySpinner className="sxa:mr-1.5" /> : null}
|
|
447
|
+
{busy ? t('support.replying') : t('support.reply')}
|
|
448
|
+
</button>
|
|
449
|
+
</form>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +1,75 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@shoppexio/storefront-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Commerce state, hooks, models and the customer account UI for Shoppex code storefronts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ShoppexIO/shoppex.git",
|
|
9
|
+
"directory": "packages/storefront-react"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.shoppex.io/storefront/code-storefront-development",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"*.css"
|
|
17
|
+
],
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./account": {
|
|
25
|
+
"types": "./dist/account.d.ts",
|
|
26
|
+
"import": "./dist/account.js"
|
|
27
|
+
},
|
|
28
|
+
"./account.css": "./dist/account.css",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"eject",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup && bun run build:css && bun run build:eject",
|
|
40
|
+
"build:css": "tailwindcss --input src/account/account.css --output dist/account.css --optimize",
|
|
41
|
+
"lint": "biome lint src tests scripts",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "bun run build:css && bun test ./tests",
|
|
44
|
+
"build:eject": "bun scripts/build-eject.ts"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"shoppex",
|
|
48
|
+
"storefront",
|
|
49
|
+
"react",
|
|
50
|
+
"ecommerce"
|
|
51
|
+
],
|
|
52
|
+
"author": "Shoppex",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"dompurify": "^3.2.7",
|
|
56
|
+
"marked": "^17.0.4"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"react": "^19.2.7",
|
|
60
|
+
"react-dom": "^19.2.7",
|
|
61
|
+
"react-router-dom": "^7.18.1"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@tailwindcss/cli": "4.2.1",
|
|
65
|
+
"@types/bun": "^1.4.0",
|
|
66
|
+
"@types/react": "19.2.14",
|
|
67
|
+
"@types/react-dom": "19.2.3",
|
|
68
|
+
"react": "19.2.7",
|
|
69
|
+
"react-dom": "19.2.7",
|
|
70
|
+
"react-router-dom": "7.18.1",
|
|
71
|
+
"tailwindcss": "4.2.1",
|
|
72
|
+
"tsup": "^8.5.0",
|
|
73
|
+
"typescript": "^5.8.3"
|
|
74
|
+
}
|
|
75
|
+
}
|