medusa-contact-us 0.0.20 → 0.0.21
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/.medusa/server/src/admin/index.js +493 -4
- package/.medusa/server/src/admin/index.mjs +495 -6
- package/README.md +5 -5
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@ const react = require("react");
|
|
|
4
4
|
const adminSdk = require("@medusajs/admin-sdk");
|
|
5
5
|
const ui = require("@medusajs/ui");
|
|
6
6
|
const icons = require("@medusajs/icons");
|
|
7
|
-
const
|
|
7
|
+
const reactRouterDom = require("react-router-dom");
|
|
8
|
+
const useDebounce$1 = (value, delay) => {
|
|
8
9
|
const [debouncedValue, setDebouncedValue] = react.useState(value);
|
|
9
10
|
react.useEffect(() => {
|
|
10
11
|
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
|
@@ -27,7 +28,7 @@ const ContactEmailSubscriptionsPage = () => {
|
|
|
27
28
|
const [items, setItems] = react.useState([]);
|
|
28
29
|
const [statusFilter, setStatusFilter] = react.useState("all");
|
|
29
30
|
const [query, setQuery] = react.useState("");
|
|
30
|
-
const debouncedQuery = useDebounce(query, 300);
|
|
31
|
+
const debouncedQuery = useDebounce$1(query, 300);
|
|
31
32
|
const [isLoading, setIsLoading] = react.useState(true);
|
|
32
33
|
const [isFetchingMore, setIsFetchingMore] = react.useState(false);
|
|
33
34
|
const [error, setError] = react.useState(null);
|
|
@@ -165,10 +166,478 @@ const ContactEmailSubscriptionsPage = () => {
|
|
|
165
166
|
) }) : null
|
|
166
167
|
] }) });
|
|
167
168
|
};
|
|
168
|
-
const config = adminSdk.defineRouteConfig({
|
|
169
|
+
const config$2 = adminSdk.defineRouteConfig({
|
|
169
170
|
label: "Contact email list",
|
|
170
171
|
icon: icons.Envelope
|
|
171
172
|
});
|
|
173
|
+
const useDebounce = (value, delay) => {
|
|
174
|
+
const [debouncedValue, setDebouncedValue] = react.useState(value);
|
|
175
|
+
react.useEffect(() => {
|
|
176
|
+
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
|
177
|
+
return () => clearTimeout(handler);
|
|
178
|
+
}, [value, delay]);
|
|
179
|
+
return debouncedValue;
|
|
180
|
+
};
|
|
181
|
+
const getStatusBadgeClass$1 = (status) => {
|
|
182
|
+
const statusLower = status.toLowerCase();
|
|
183
|
+
if (statusLower === "pending") {
|
|
184
|
+
return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
|
|
185
|
+
}
|
|
186
|
+
if (statusLower === "in_progress") {
|
|
187
|
+
return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
|
|
188
|
+
}
|
|
189
|
+
if (statusLower === "resolved") {
|
|
190
|
+
return "bg-ui-tag-green-bg text-ui-tag-green-text";
|
|
191
|
+
}
|
|
192
|
+
if (statusLower === "closed") {
|
|
193
|
+
return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
|
|
194
|
+
}
|
|
195
|
+
return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
|
|
196
|
+
};
|
|
197
|
+
const ContactRequestsPage = () => {
|
|
198
|
+
const navigate = reactRouterDom.useNavigate();
|
|
199
|
+
const [items, setItems] = react.useState([]);
|
|
200
|
+
const [statusFilter, setStatusFilter] = react.useState("all");
|
|
201
|
+
const [emailQuery, setEmailQuery] = react.useState("");
|
|
202
|
+
const [sourceFilter, setSourceFilter] = react.useState("all");
|
|
203
|
+
const debouncedEmailQuery = useDebounce(emailQuery, 300);
|
|
204
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
205
|
+
const [isFetchingMore, setIsFetchingMore] = react.useState(false);
|
|
206
|
+
const [error, setError] = react.useState(null);
|
|
207
|
+
const [offset, setOffset] = react.useState(0);
|
|
208
|
+
const [count, setCount] = react.useState(0);
|
|
209
|
+
const limit = 50;
|
|
210
|
+
const loadRequests = react.useCallback(
|
|
211
|
+
async (nextOffset, replace = false) => {
|
|
212
|
+
var _a;
|
|
213
|
+
try {
|
|
214
|
+
if (replace) {
|
|
215
|
+
setIsLoading(true);
|
|
216
|
+
} else {
|
|
217
|
+
setIsFetchingMore(true);
|
|
218
|
+
}
|
|
219
|
+
setError(null);
|
|
220
|
+
const params = new URLSearchParams();
|
|
221
|
+
params.set("limit", String(limit));
|
|
222
|
+
params.set("offset", String(nextOffset));
|
|
223
|
+
if (statusFilter !== "all") {
|
|
224
|
+
params.set("status", statusFilter);
|
|
225
|
+
}
|
|
226
|
+
if (debouncedEmailQuery.trim()) {
|
|
227
|
+
params.set("email", debouncedEmailQuery.trim());
|
|
228
|
+
}
|
|
229
|
+
if (sourceFilter !== "all") {
|
|
230
|
+
params.set("source", sourceFilter);
|
|
231
|
+
}
|
|
232
|
+
params.set("order", "created_at");
|
|
233
|
+
params.set("order_direction", "DESC");
|
|
234
|
+
const response = await fetch(
|
|
235
|
+
`/admin/contact-requests?${params.toString()}`,
|
|
236
|
+
{ credentials: "include" }
|
|
237
|
+
);
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
const message = await response.text();
|
|
240
|
+
throw new Error(message || "Unable to load contact requests");
|
|
241
|
+
}
|
|
242
|
+
const payload = await response.json();
|
|
243
|
+
setCount(payload.count ?? 0);
|
|
244
|
+
setOffset(nextOffset + (((_a = payload.requests) == null ? void 0 : _a.length) ?? 0));
|
|
245
|
+
setItems(
|
|
246
|
+
(prev) => replace ? payload.requests ?? [] : [...prev, ...payload.requests ?? []]
|
|
247
|
+
);
|
|
248
|
+
} catch (loadError) {
|
|
249
|
+
const message = loadError instanceof Error ? loadError.message : "Unable to load contact requests";
|
|
250
|
+
setError(message);
|
|
251
|
+
} finally {
|
|
252
|
+
setIsLoading(false);
|
|
253
|
+
setIsFetchingMore(false);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
[statusFilter, debouncedEmailQuery, sourceFilter]
|
|
257
|
+
);
|
|
258
|
+
react.useEffect(() => {
|
|
259
|
+
void loadRequests(0, true);
|
|
260
|
+
}, [statusFilter, debouncedEmailQuery, sourceFilter, loadRequests]);
|
|
261
|
+
const hasMore = react.useMemo(() => offset < count, [offset, count]);
|
|
262
|
+
const availableStatuses = react.useMemo(() => {
|
|
263
|
+
const statuses = /* @__PURE__ */ new Set();
|
|
264
|
+
items.forEach((item) => statuses.add(item.status));
|
|
265
|
+
return Array.from(statuses).sort();
|
|
266
|
+
}, [items]);
|
|
267
|
+
const availableSources = react.useMemo(() => {
|
|
268
|
+
const sources = /* @__PURE__ */ new Set();
|
|
269
|
+
items.forEach((item) => {
|
|
270
|
+
if (item.source) {
|
|
271
|
+
sources.add(item.source);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return Array.from(sources).sort();
|
|
275
|
+
}, [items]);
|
|
276
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "mx-auto flex w-full max-w-7xl flex-col gap-6 p-6", children: [
|
|
277
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
|
|
278
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
279
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact Requests" }),
|
|
280
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Manage and track customer contact requests from the storefront" })
|
|
281
|
+
] }),
|
|
282
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "primary", onClick: () => loadRequests(0, true), children: "Refresh" })
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
|
|
285
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
286
|
+
ui.Input,
|
|
287
|
+
{
|
|
288
|
+
placeholder: "Search by email",
|
|
289
|
+
value: emailQuery,
|
|
290
|
+
onChange: (event) => setEmailQuery(event.target.value),
|
|
291
|
+
className: "md:max-w-sm"
|
|
292
|
+
}
|
|
293
|
+
),
|
|
294
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3", children: [
|
|
295
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
296
|
+
"select",
|
|
297
|
+
{
|
|
298
|
+
value: statusFilter,
|
|
299
|
+
onChange: (event) => setStatusFilter(event.target.value),
|
|
300
|
+
className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
|
|
301
|
+
children: [
|
|
302
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Statuses" }),
|
|
303
|
+
availableStatuses.map((status) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
),
|
|
307
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
308
|
+
"select",
|
|
309
|
+
{
|
|
310
|
+
value: sourceFilter,
|
|
311
|
+
onChange: (event) => setSourceFilter(event.target.value),
|
|
312
|
+
className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
|
|
313
|
+
children: [
|
|
314
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Sources" }),
|
|
315
|
+
availableSources.map((source) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: source, children: source }, source))
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
] })
|
|
320
|
+
] }),
|
|
321
|
+
error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
|
|
322
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
|
|
323
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
324
|
+
ui.Button,
|
|
325
|
+
{
|
|
326
|
+
variant: "secondary",
|
|
327
|
+
onClick: () => loadRequests(0, true),
|
|
328
|
+
children: "Try again"
|
|
329
|
+
}
|
|
330
|
+
) })
|
|
331
|
+
] }) : null,
|
|
332
|
+
isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading contact requests..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
|
|
333
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-xl", children: "No contact requests yet" }),
|
|
334
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Contact requests created through the storefront will appear here." })
|
|
335
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
|
|
336
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
337
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
|
|
338
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
|
|
339
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
|
|
340
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Created" }),
|
|
341
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Actions" })
|
|
342
|
+
] }) }),
|
|
343
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((request) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
344
|
+
"tr",
|
|
345
|
+
{
|
|
346
|
+
className: "hover:bg-ui-bg-subtle/60 cursor-pointer",
|
|
347
|
+
onClick: () => navigate(`/contact-requests/${request.id}`),
|
|
348
|
+
children: [
|
|
349
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
350
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: request.email }),
|
|
351
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
|
|
352
|
+
] }) }),
|
|
353
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
354
|
+
ui.Badge,
|
|
355
|
+
{
|
|
356
|
+
size: "2xsmall",
|
|
357
|
+
className: `uppercase ${getStatusBadgeClass$1(request.status)}`,
|
|
358
|
+
children: request.status.replace("_", " ")
|
|
359
|
+
}
|
|
360
|
+
) }),
|
|
361
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.source ?? "storefront" }),
|
|
362
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(request.created_at).toLocaleString() }),
|
|
363
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
364
|
+
ui.Button,
|
|
365
|
+
{
|
|
366
|
+
variant: "transparent",
|
|
367
|
+
size: "small",
|
|
368
|
+
onClick: (e) => {
|
|
369
|
+
e.stopPropagation();
|
|
370
|
+
navigate(`/contact-requests/${request.id}`);
|
|
371
|
+
},
|
|
372
|
+
children: "View"
|
|
373
|
+
}
|
|
374
|
+
) })
|
|
375
|
+
]
|
|
376
|
+
},
|
|
377
|
+
request.id
|
|
378
|
+
)) })
|
|
379
|
+
] }) }),
|
|
380
|
+
hasMore ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
381
|
+
ui.Button,
|
|
382
|
+
{
|
|
383
|
+
variant: "secondary",
|
|
384
|
+
isLoading: isFetchingMore,
|
|
385
|
+
onClick: () => loadRequests(offset, false),
|
|
386
|
+
children: "Load more"
|
|
387
|
+
}
|
|
388
|
+
) }) : null
|
|
389
|
+
] }) });
|
|
390
|
+
};
|
|
391
|
+
const config$1 = adminSdk.defineRouteConfig({
|
|
392
|
+
label: "Contact Requests",
|
|
393
|
+
icon: icons.ChatBubbleLeftRight
|
|
394
|
+
});
|
|
395
|
+
const getStatusBadgeClass = (status) => {
|
|
396
|
+
const statusLower = status.toLowerCase();
|
|
397
|
+
if (statusLower === "pending") {
|
|
398
|
+
return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
|
|
399
|
+
}
|
|
400
|
+
if (statusLower === "in_progress") {
|
|
401
|
+
return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
|
|
402
|
+
}
|
|
403
|
+
if (statusLower === "resolved") {
|
|
404
|
+
return "bg-ui-tag-green-bg text-ui-tag-green-text";
|
|
405
|
+
}
|
|
406
|
+
if (statusLower === "closed") {
|
|
407
|
+
return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
|
|
408
|
+
}
|
|
409
|
+
return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
|
|
410
|
+
};
|
|
411
|
+
const ContactRequestDetailPage = () => {
|
|
412
|
+
const navigate = reactRouterDom.useNavigate();
|
|
413
|
+
const { id } = reactRouterDom.useParams();
|
|
414
|
+
const [request, setRequest] = react.useState(null);
|
|
415
|
+
const [nextAllowedStatuses, setNextAllowedStatuses] = react.useState([]);
|
|
416
|
+
const [selectedStatus, setSelectedStatus] = react.useState("");
|
|
417
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
418
|
+
const [isUpdating, setIsUpdating] = react.useState(false);
|
|
419
|
+
const [error, setError] = react.useState(null);
|
|
420
|
+
const [updateError, setUpdateError] = react.useState(null);
|
|
421
|
+
const [updateSuccess, setUpdateSuccess] = react.useState(false);
|
|
422
|
+
react.useEffect(() => {
|
|
423
|
+
if (!id) {
|
|
424
|
+
navigate("/contact-requests");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const loadRequest = async () => {
|
|
428
|
+
try {
|
|
429
|
+
setIsLoading(true);
|
|
430
|
+
setError(null);
|
|
431
|
+
const response = await fetch(`/admin/contact-requests/${id}`, {
|
|
432
|
+
credentials: "include"
|
|
433
|
+
});
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
const message = await response.text();
|
|
436
|
+
throw new Error(message || "Unable to load contact request");
|
|
437
|
+
}
|
|
438
|
+
const payload = await response.json();
|
|
439
|
+
setRequest(payload.request);
|
|
440
|
+
setNextAllowedStatuses(payload.next_allowed_statuses ?? []);
|
|
441
|
+
setSelectedStatus("");
|
|
442
|
+
} catch (loadError) {
|
|
443
|
+
const message = loadError instanceof Error ? loadError.message : "Unable to load contact request";
|
|
444
|
+
setError(message);
|
|
445
|
+
} finally {
|
|
446
|
+
setIsLoading(false);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
void loadRequest();
|
|
450
|
+
}, [id, navigate]);
|
|
451
|
+
const handleStatusUpdate = async () => {
|
|
452
|
+
if (!id || !selectedStatus) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
setIsUpdating(true);
|
|
457
|
+
setUpdateError(null);
|
|
458
|
+
setUpdateSuccess(false);
|
|
459
|
+
const response = await fetch(`/admin/contact-requests/${id}/status`, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: {
|
|
462
|
+
"Content-Type": "application/json"
|
|
463
|
+
},
|
|
464
|
+
credentials: "include",
|
|
465
|
+
body: JSON.stringify({ status: selectedStatus })
|
|
466
|
+
});
|
|
467
|
+
if (!response.ok) {
|
|
468
|
+
const message = await response.text();
|
|
469
|
+
throw new Error(message || "Unable to update status");
|
|
470
|
+
}
|
|
471
|
+
const payload = await response.json();
|
|
472
|
+
setRequest(payload.request);
|
|
473
|
+
setSelectedStatus("");
|
|
474
|
+
setUpdateSuccess(true);
|
|
475
|
+
setTimeout(() => setUpdateSuccess(false), 3e3);
|
|
476
|
+
const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
|
|
477
|
+
credentials: "include"
|
|
478
|
+
});
|
|
479
|
+
if (detailResponse.ok) {
|
|
480
|
+
const detailPayload = await detailResponse.json();
|
|
481
|
+
setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
|
|
482
|
+
}
|
|
483
|
+
} catch (updateErr) {
|
|
484
|
+
const message = updateErr instanceof Error ? updateErr.message : "Unable to update status";
|
|
485
|
+
setUpdateError(message);
|
|
486
|
+
} finally {
|
|
487
|
+
setIsUpdating(false);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
if (isLoading) {
|
|
491
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading contact request..." }) }) }) });
|
|
492
|
+
}
|
|
493
|
+
if (error || !request) {
|
|
494
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
|
|
495
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error || "Contact request not found" }),
|
|
496
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => navigate("/contact-requests"), children: "Back to list" }) })
|
|
497
|
+
] }) }) });
|
|
498
|
+
}
|
|
499
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
|
|
500
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3", children: [
|
|
501
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
502
|
+
ui.Button,
|
|
503
|
+
{
|
|
504
|
+
variant: "transparent",
|
|
505
|
+
size: "small",
|
|
506
|
+
onClick: () => navigate("/contact-requests"),
|
|
507
|
+
className: "w-fit",
|
|
508
|
+
children: [
|
|
509
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ArrowLeft, { className: "mr-2" }),
|
|
510
|
+
"Back to list"
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
),
|
|
514
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 md:flex-row md:items-center md:justify-between", children: [
|
|
515
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
516
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact Request Details" }),
|
|
517
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
|
|
518
|
+
] }),
|
|
519
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
520
|
+
ui.Badge,
|
|
521
|
+
{
|
|
522
|
+
size: "small",
|
|
523
|
+
className: `uppercase ${getStatusBadgeClass(request.status)}`,
|
|
524
|
+
children: request.status.replace("_", " ")
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
] })
|
|
528
|
+
] }),
|
|
529
|
+
nextAllowedStatuses.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
530
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Update Status" }),
|
|
531
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [
|
|
532
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
|
|
533
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "New Status" }),
|
|
534
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
535
|
+
"select",
|
|
536
|
+
{
|
|
537
|
+
value: selectedStatus,
|
|
538
|
+
onChange: (event) => setSelectedStatus(event.target.value),
|
|
539
|
+
className: "h-9 w-full rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
|
|
540
|
+
children: [
|
|
541
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select new status" }),
|
|
542
|
+
nextAllowedStatuses.map((status) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
|
|
543
|
+
]
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
] }),
|
|
547
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
548
|
+
ui.Button,
|
|
549
|
+
{
|
|
550
|
+
variant: "primary",
|
|
551
|
+
onClick: handleStatusUpdate,
|
|
552
|
+
disabled: !selectedStatus || isUpdating,
|
|
553
|
+
isLoading: isUpdating,
|
|
554
|
+
children: "Update Status"
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
] }),
|
|
558
|
+
updateError && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-error", children: updateError }),
|
|
559
|
+
updateSuccess && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-success", children: "Status updated successfully" })
|
|
560
|
+
] }),
|
|
561
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [
|
|
562
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
563
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Contact Information" }),
|
|
564
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
565
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
566
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Email" }),
|
|
567
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: request.email })
|
|
568
|
+
] }),
|
|
569
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
570
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Source" }),
|
|
571
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: request.source ?? "storefront" })
|
|
572
|
+
] }),
|
|
573
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
574
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Created" }),
|
|
575
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: new Date(request.created_at).toLocaleString() })
|
|
576
|
+
] }),
|
|
577
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
578
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Last Updated" }),
|
|
579
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: new Date(request.updated_at).toLocaleString() })
|
|
580
|
+
] })
|
|
581
|
+
] })
|
|
582
|
+
] }),
|
|
583
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
584
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Status History" }),
|
|
585
|
+
request.status_history && request.status_history.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: request.status_history.map((entry, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
586
|
+
"div",
|
|
587
|
+
{
|
|
588
|
+
className: "flex items-center justify-between border-b border-ui-border-subtle pb-2 last:border-0",
|
|
589
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
590
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
591
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
592
|
+
ui.Badge,
|
|
593
|
+
{
|
|
594
|
+
size: "2xsmall",
|
|
595
|
+
className: `uppercase ${getStatusBadgeClass(entry.to)}`,
|
|
596
|
+
children: entry.to.replace("_", " ")
|
|
597
|
+
}
|
|
598
|
+
),
|
|
599
|
+
entry.from && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
600
|
+
"from ",
|
|
601
|
+
entry.from.replace("_", " ")
|
|
602
|
+
] })
|
|
603
|
+
] }),
|
|
604
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
605
|
+
new Date(entry.changed_at).toLocaleString(),
|
|
606
|
+
entry.changed_by && ` by ${entry.changed_by}`
|
|
607
|
+
] })
|
|
608
|
+
] })
|
|
609
|
+
},
|
|
610
|
+
index
|
|
611
|
+
)) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No status history available" })
|
|
612
|
+
] })
|
|
613
|
+
] }),
|
|
614
|
+
request.payload && Object.keys(request.payload).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
615
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Request Details" }),
|
|
616
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: Object.entries(request.payload).map(([key, value]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
617
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: key.charAt(0).toUpperCase() + key.slice(1).replace("_", " ") }),
|
|
618
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
619
|
+
ui.Textarea,
|
|
620
|
+
{
|
|
621
|
+
value: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
622
|
+
readOnly: true,
|
|
623
|
+
className: "mt-1 min-h-[60px]"
|
|
624
|
+
}
|
|
625
|
+
)
|
|
626
|
+
] }, key)) })
|
|
627
|
+
] }),
|
|
628
|
+
request.metadata && Object.keys(request.metadata).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
629
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Metadata" }),
|
|
630
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: Object.entries(request.metadata).map(([key, value]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
|
|
631
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: key }),
|
|
632
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-medium", children: typeof value === "string" ? value : JSON.stringify(value) })
|
|
633
|
+
] }, key)) })
|
|
634
|
+
] })
|
|
635
|
+
] }) });
|
|
636
|
+
};
|
|
637
|
+
const config = adminSdk.defineRouteConfig({
|
|
638
|
+
label: "Contact Request Details",
|
|
639
|
+
icon: icons.ChatBubbleLeftRight
|
|
640
|
+
});
|
|
172
641
|
const en = {};
|
|
173
642
|
const i18nTranslations0 = {
|
|
174
643
|
en
|
|
@@ -179,15 +648,35 @@ const routeModule = {
|
|
|
179
648
|
{
|
|
180
649
|
Component: ContactEmailSubscriptionsPage,
|
|
181
650
|
path: "/contact-email-subscriptions"
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
Component: ContactRequestsPage,
|
|
654
|
+
path: "/contact-requests"
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
Component: ContactRequestDetailPage,
|
|
658
|
+
path: "/contact-requests/:id"
|
|
182
659
|
}
|
|
183
660
|
]
|
|
184
661
|
};
|
|
185
662
|
const menuItemModule = {
|
|
186
663
|
menuItems: [
|
|
664
|
+
{
|
|
665
|
+
label: config$2.label,
|
|
666
|
+
icon: config$2.icon,
|
|
667
|
+
path: "/contact-email-subscriptions",
|
|
668
|
+
nested: void 0
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
label: config$1.label,
|
|
672
|
+
icon: config$1.icon,
|
|
673
|
+
path: "/contact-requests",
|
|
674
|
+
nested: void 0
|
|
675
|
+
},
|
|
187
676
|
{
|
|
188
677
|
label: config.label,
|
|
189
678
|
icon: config.icon,
|
|
190
|
-
path: "/contact-
|
|
679
|
+
path: "/contact-requests/:id",
|
|
191
680
|
nested: void 0
|
|
192
681
|
}
|
|
193
682
|
]
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
3
3
|
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
4
|
-
import { Container, Heading, Text, Button, Input, Badge } from "@medusajs/ui";
|
|
5
|
-
import { Envelope } from "@medusajs/icons";
|
|
6
|
-
|
|
4
|
+
import { Container, Heading, Text, Button, Input, Badge, Textarea } from "@medusajs/ui";
|
|
5
|
+
import { Envelope, ChatBubbleLeftRight, ArrowLeft } from "@medusajs/icons";
|
|
6
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
7
|
+
const useDebounce$1 = (value, delay) => {
|
|
7
8
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
|
@@ -26,7 +27,7 @@ const ContactEmailSubscriptionsPage = () => {
|
|
|
26
27
|
const [items, setItems] = useState([]);
|
|
27
28
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
28
29
|
const [query, setQuery] = useState("");
|
|
29
|
-
const debouncedQuery = useDebounce(query, 300);
|
|
30
|
+
const debouncedQuery = useDebounce$1(query, 300);
|
|
30
31
|
const [isLoading, setIsLoading] = useState(true);
|
|
31
32
|
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
|
32
33
|
const [error, setError] = useState(null);
|
|
@@ -164,10 +165,478 @@ const ContactEmailSubscriptionsPage = () => {
|
|
|
164
165
|
) }) : null
|
|
165
166
|
] }) });
|
|
166
167
|
};
|
|
167
|
-
const config = defineRouteConfig({
|
|
168
|
+
const config$2 = defineRouteConfig({
|
|
168
169
|
label: "Contact email list",
|
|
169
170
|
icon: Envelope
|
|
170
171
|
});
|
|
172
|
+
const useDebounce = (value, delay) => {
|
|
173
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
|
176
|
+
return () => clearTimeout(handler);
|
|
177
|
+
}, [value, delay]);
|
|
178
|
+
return debouncedValue;
|
|
179
|
+
};
|
|
180
|
+
const getStatusBadgeClass$1 = (status) => {
|
|
181
|
+
const statusLower = status.toLowerCase();
|
|
182
|
+
if (statusLower === "pending") {
|
|
183
|
+
return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
|
|
184
|
+
}
|
|
185
|
+
if (statusLower === "in_progress") {
|
|
186
|
+
return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
|
|
187
|
+
}
|
|
188
|
+
if (statusLower === "resolved") {
|
|
189
|
+
return "bg-ui-tag-green-bg text-ui-tag-green-text";
|
|
190
|
+
}
|
|
191
|
+
if (statusLower === "closed") {
|
|
192
|
+
return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
|
|
193
|
+
}
|
|
194
|
+
return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
|
|
195
|
+
};
|
|
196
|
+
const ContactRequestsPage = () => {
|
|
197
|
+
const navigate = useNavigate();
|
|
198
|
+
const [items, setItems] = useState([]);
|
|
199
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
200
|
+
const [emailQuery, setEmailQuery] = useState("");
|
|
201
|
+
const [sourceFilter, setSourceFilter] = useState("all");
|
|
202
|
+
const debouncedEmailQuery = useDebounce(emailQuery, 300);
|
|
203
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
204
|
+
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
|
205
|
+
const [error, setError] = useState(null);
|
|
206
|
+
const [offset, setOffset] = useState(0);
|
|
207
|
+
const [count, setCount] = useState(0);
|
|
208
|
+
const limit = 50;
|
|
209
|
+
const loadRequests = useCallback(
|
|
210
|
+
async (nextOffset, replace = false) => {
|
|
211
|
+
var _a;
|
|
212
|
+
try {
|
|
213
|
+
if (replace) {
|
|
214
|
+
setIsLoading(true);
|
|
215
|
+
} else {
|
|
216
|
+
setIsFetchingMore(true);
|
|
217
|
+
}
|
|
218
|
+
setError(null);
|
|
219
|
+
const params = new URLSearchParams();
|
|
220
|
+
params.set("limit", String(limit));
|
|
221
|
+
params.set("offset", String(nextOffset));
|
|
222
|
+
if (statusFilter !== "all") {
|
|
223
|
+
params.set("status", statusFilter);
|
|
224
|
+
}
|
|
225
|
+
if (debouncedEmailQuery.trim()) {
|
|
226
|
+
params.set("email", debouncedEmailQuery.trim());
|
|
227
|
+
}
|
|
228
|
+
if (sourceFilter !== "all") {
|
|
229
|
+
params.set("source", sourceFilter);
|
|
230
|
+
}
|
|
231
|
+
params.set("order", "created_at");
|
|
232
|
+
params.set("order_direction", "DESC");
|
|
233
|
+
const response = await fetch(
|
|
234
|
+
`/admin/contact-requests?${params.toString()}`,
|
|
235
|
+
{ credentials: "include" }
|
|
236
|
+
);
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const message = await response.text();
|
|
239
|
+
throw new Error(message || "Unable to load contact requests");
|
|
240
|
+
}
|
|
241
|
+
const payload = await response.json();
|
|
242
|
+
setCount(payload.count ?? 0);
|
|
243
|
+
setOffset(nextOffset + (((_a = payload.requests) == null ? void 0 : _a.length) ?? 0));
|
|
244
|
+
setItems(
|
|
245
|
+
(prev) => replace ? payload.requests ?? [] : [...prev, ...payload.requests ?? []]
|
|
246
|
+
);
|
|
247
|
+
} catch (loadError) {
|
|
248
|
+
const message = loadError instanceof Error ? loadError.message : "Unable to load contact requests";
|
|
249
|
+
setError(message);
|
|
250
|
+
} finally {
|
|
251
|
+
setIsLoading(false);
|
|
252
|
+
setIsFetchingMore(false);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
[statusFilter, debouncedEmailQuery, sourceFilter]
|
|
256
|
+
);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
void loadRequests(0, true);
|
|
259
|
+
}, [statusFilter, debouncedEmailQuery, sourceFilter, loadRequests]);
|
|
260
|
+
const hasMore = useMemo(() => offset < count, [offset, count]);
|
|
261
|
+
const availableStatuses = useMemo(() => {
|
|
262
|
+
const statuses = /* @__PURE__ */ new Set();
|
|
263
|
+
items.forEach((item) => statuses.add(item.status));
|
|
264
|
+
return Array.from(statuses).sort();
|
|
265
|
+
}, [items]);
|
|
266
|
+
const availableSources = useMemo(() => {
|
|
267
|
+
const sources = /* @__PURE__ */ new Set();
|
|
268
|
+
items.forEach((item) => {
|
|
269
|
+
if (item.source) {
|
|
270
|
+
sources.add(item.source);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return Array.from(sources).sort();
|
|
274
|
+
}, [items]);
|
|
275
|
+
return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxs(Container, { className: "mx-auto flex w-full max-w-7xl flex-col gap-6 p-6", children: [
|
|
276
|
+
/* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
278
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Contact Requests" }),
|
|
279
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Manage and track customer contact requests from the storefront" })
|
|
280
|
+
] }),
|
|
281
|
+
/* @__PURE__ */ jsx(Button, { variant: "primary", onClick: () => loadRequests(0, true), children: "Refresh" })
|
|
282
|
+
] }),
|
|
283
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
|
|
284
|
+
/* @__PURE__ */ jsx(
|
|
285
|
+
Input,
|
|
286
|
+
{
|
|
287
|
+
placeholder: "Search by email",
|
|
288
|
+
value: emailQuery,
|
|
289
|
+
onChange: (event) => setEmailQuery(event.target.value),
|
|
290
|
+
className: "md:max-w-sm"
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
|
|
294
|
+
/* @__PURE__ */ jsxs(
|
|
295
|
+
"select",
|
|
296
|
+
{
|
|
297
|
+
value: statusFilter,
|
|
298
|
+
onChange: (event) => setStatusFilter(event.target.value),
|
|
299
|
+
className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
|
|
300
|
+
children: [
|
|
301
|
+
/* @__PURE__ */ jsx("option", { value: "all", children: "All Statuses" }),
|
|
302
|
+
availableStatuses.map((status) => /* @__PURE__ */ jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
),
|
|
306
|
+
/* @__PURE__ */ jsxs(
|
|
307
|
+
"select",
|
|
308
|
+
{
|
|
309
|
+
value: sourceFilter,
|
|
310
|
+
onChange: (event) => setSourceFilter(event.target.value),
|
|
311
|
+
className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
|
|
312
|
+
children: [
|
|
313
|
+
/* @__PURE__ */ jsx("option", { value: "all", children: "All Sources" }),
|
|
314
|
+
availableSources.map((source) => /* @__PURE__ */ jsx("option", { value: source, children: source }, source))
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
] })
|
|
319
|
+
] }),
|
|
320
|
+
error ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
|
|
321
|
+
/* @__PURE__ */ jsx(Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
|
|
322
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsx(
|
|
323
|
+
Button,
|
|
324
|
+
{
|
|
325
|
+
variant: "secondary",
|
|
326
|
+
onClick: () => loadRequests(0, true),
|
|
327
|
+
children: "Try again"
|
|
328
|
+
}
|
|
329
|
+
) })
|
|
330
|
+
] }) : null,
|
|
331
|
+
isLoading ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsx(Text, { children: "Loading contact requests..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
|
|
332
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-xl", children: "No contact requests yet" }),
|
|
333
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Contact requests created through the storefront will appear here." })
|
|
334
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
|
|
335
|
+
/* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
336
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
|
|
337
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
|
|
338
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
|
|
339
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Created" }),
|
|
340
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Actions" })
|
|
341
|
+
] }) }),
|
|
342
|
+
/* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((request) => /* @__PURE__ */ jsxs(
|
|
343
|
+
"tr",
|
|
344
|
+
{
|
|
345
|
+
className: "hover:bg-ui-bg-subtle/60 cursor-pointer",
|
|
346
|
+
onClick: () => navigate(`/contact-requests/${request.id}`),
|
|
347
|
+
children: [
|
|
348
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
349
|
+
/* @__PURE__ */ jsx("span", { children: request.email }),
|
|
350
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
|
|
351
|
+
] }) }),
|
|
352
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsx(
|
|
353
|
+
Badge,
|
|
354
|
+
{
|
|
355
|
+
size: "2xsmall",
|
|
356
|
+
className: `uppercase ${getStatusBadgeClass$1(request.status)}`,
|
|
357
|
+
children: request.status.replace("_", " ")
|
|
358
|
+
}
|
|
359
|
+
) }),
|
|
360
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.source ?? "storefront" }),
|
|
361
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(request.created_at).toLocaleString() }),
|
|
362
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsx(
|
|
363
|
+
Button,
|
|
364
|
+
{
|
|
365
|
+
variant: "transparent",
|
|
366
|
+
size: "small",
|
|
367
|
+
onClick: (e) => {
|
|
368
|
+
e.stopPropagation();
|
|
369
|
+
navigate(`/contact-requests/${request.id}`);
|
|
370
|
+
},
|
|
371
|
+
children: "View"
|
|
372
|
+
}
|
|
373
|
+
) })
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
request.id
|
|
377
|
+
)) })
|
|
378
|
+
] }) }),
|
|
379
|
+
hasMore ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
|
|
380
|
+
Button,
|
|
381
|
+
{
|
|
382
|
+
variant: "secondary",
|
|
383
|
+
isLoading: isFetchingMore,
|
|
384
|
+
onClick: () => loadRequests(offset, false),
|
|
385
|
+
children: "Load more"
|
|
386
|
+
}
|
|
387
|
+
) }) : null
|
|
388
|
+
] }) });
|
|
389
|
+
};
|
|
390
|
+
const config$1 = defineRouteConfig({
|
|
391
|
+
label: "Contact Requests",
|
|
392
|
+
icon: ChatBubbleLeftRight
|
|
393
|
+
});
|
|
394
|
+
const getStatusBadgeClass = (status) => {
|
|
395
|
+
const statusLower = status.toLowerCase();
|
|
396
|
+
if (statusLower === "pending") {
|
|
397
|
+
return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
|
|
398
|
+
}
|
|
399
|
+
if (statusLower === "in_progress") {
|
|
400
|
+
return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
|
|
401
|
+
}
|
|
402
|
+
if (statusLower === "resolved") {
|
|
403
|
+
return "bg-ui-tag-green-bg text-ui-tag-green-text";
|
|
404
|
+
}
|
|
405
|
+
if (statusLower === "closed") {
|
|
406
|
+
return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
|
|
407
|
+
}
|
|
408
|
+
return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
|
|
409
|
+
};
|
|
410
|
+
const ContactRequestDetailPage = () => {
|
|
411
|
+
const navigate = useNavigate();
|
|
412
|
+
const { id } = useParams();
|
|
413
|
+
const [request, setRequest] = useState(null);
|
|
414
|
+
const [nextAllowedStatuses, setNextAllowedStatuses] = useState([]);
|
|
415
|
+
const [selectedStatus, setSelectedStatus] = useState("");
|
|
416
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
417
|
+
const [isUpdating, setIsUpdating] = useState(false);
|
|
418
|
+
const [error, setError] = useState(null);
|
|
419
|
+
const [updateError, setUpdateError] = useState(null);
|
|
420
|
+
const [updateSuccess, setUpdateSuccess] = useState(false);
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
if (!id) {
|
|
423
|
+
navigate("/contact-requests");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const loadRequest = async () => {
|
|
427
|
+
try {
|
|
428
|
+
setIsLoading(true);
|
|
429
|
+
setError(null);
|
|
430
|
+
const response = await fetch(`/admin/contact-requests/${id}`, {
|
|
431
|
+
credentials: "include"
|
|
432
|
+
});
|
|
433
|
+
if (!response.ok) {
|
|
434
|
+
const message = await response.text();
|
|
435
|
+
throw new Error(message || "Unable to load contact request");
|
|
436
|
+
}
|
|
437
|
+
const payload = await response.json();
|
|
438
|
+
setRequest(payload.request);
|
|
439
|
+
setNextAllowedStatuses(payload.next_allowed_statuses ?? []);
|
|
440
|
+
setSelectedStatus("");
|
|
441
|
+
} catch (loadError) {
|
|
442
|
+
const message = loadError instanceof Error ? loadError.message : "Unable to load contact request";
|
|
443
|
+
setError(message);
|
|
444
|
+
} finally {
|
|
445
|
+
setIsLoading(false);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
void loadRequest();
|
|
449
|
+
}, [id, navigate]);
|
|
450
|
+
const handleStatusUpdate = async () => {
|
|
451
|
+
if (!id || !selectedStatus) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
setIsUpdating(true);
|
|
456
|
+
setUpdateError(null);
|
|
457
|
+
setUpdateSuccess(false);
|
|
458
|
+
const response = await fetch(`/admin/contact-requests/${id}/status`, {
|
|
459
|
+
method: "POST",
|
|
460
|
+
headers: {
|
|
461
|
+
"Content-Type": "application/json"
|
|
462
|
+
},
|
|
463
|
+
credentials: "include",
|
|
464
|
+
body: JSON.stringify({ status: selectedStatus })
|
|
465
|
+
});
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
const message = await response.text();
|
|
468
|
+
throw new Error(message || "Unable to update status");
|
|
469
|
+
}
|
|
470
|
+
const payload = await response.json();
|
|
471
|
+
setRequest(payload.request);
|
|
472
|
+
setSelectedStatus("");
|
|
473
|
+
setUpdateSuccess(true);
|
|
474
|
+
setTimeout(() => setUpdateSuccess(false), 3e3);
|
|
475
|
+
const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
|
|
476
|
+
credentials: "include"
|
|
477
|
+
});
|
|
478
|
+
if (detailResponse.ok) {
|
|
479
|
+
const detailPayload = await detailResponse.json();
|
|
480
|
+
setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
|
|
481
|
+
}
|
|
482
|
+
} catch (updateErr) {
|
|
483
|
+
const message = updateErr instanceof Error ? updateErr.message : "Unable to update status";
|
|
484
|
+
setUpdateError(message);
|
|
485
|
+
} finally {
|
|
486
|
+
setIsUpdating(false);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
if (isLoading) {
|
|
490
|
+
return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsx(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsx(Text, { children: "Loading contact request..." }) }) }) });
|
|
491
|
+
}
|
|
492
|
+
if (error || !request) {
|
|
493
|
+
return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsx(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
|
|
494
|
+
/* @__PURE__ */ jsx(Text, { weight: "plus", className: "text-ui-fg-error", children: error || "Contact request not found" }),
|
|
495
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => navigate("/contact-requests"), children: "Back to list" }) })
|
|
496
|
+
] }) }) });
|
|
497
|
+
}
|
|
498
|
+
return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxs(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
|
|
499
|
+
/* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3", children: [
|
|
500
|
+
/* @__PURE__ */ jsxs(
|
|
501
|
+
Button,
|
|
502
|
+
{
|
|
503
|
+
variant: "transparent",
|
|
504
|
+
size: "small",
|
|
505
|
+
onClick: () => navigate("/contact-requests"),
|
|
506
|
+
className: "w-fit",
|
|
507
|
+
children: [
|
|
508
|
+
/* @__PURE__ */ jsx(ArrowLeft, { className: "mr-2" }),
|
|
509
|
+
"Back to list"
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
),
|
|
513
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 md:flex-row md:items-center md:justify-between", children: [
|
|
514
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
515
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Contact Request Details" }),
|
|
516
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
|
|
517
|
+
] }),
|
|
518
|
+
/* @__PURE__ */ jsx(
|
|
519
|
+
Badge,
|
|
520
|
+
{
|
|
521
|
+
size: "small",
|
|
522
|
+
className: `uppercase ${getStatusBadgeClass(request.status)}`,
|
|
523
|
+
children: request.status.replace("_", " ")
|
|
524
|
+
}
|
|
525
|
+
)
|
|
526
|
+
] })
|
|
527
|
+
] }),
|
|
528
|
+
nextAllowedStatuses.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
529
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Update Status" }),
|
|
530
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [
|
|
531
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
532
|
+
/* @__PURE__ */ jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "New Status" }),
|
|
533
|
+
/* @__PURE__ */ jsxs(
|
|
534
|
+
"select",
|
|
535
|
+
{
|
|
536
|
+
value: selectedStatus,
|
|
537
|
+
onChange: (event) => setSelectedStatus(event.target.value),
|
|
538
|
+
className: "h-9 w-full rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
|
|
539
|
+
children: [
|
|
540
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Select new status" }),
|
|
541
|
+
nextAllowedStatuses.map((status) => /* @__PURE__ */ jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
|
|
542
|
+
]
|
|
543
|
+
}
|
|
544
|
+
)
|
|
545
|
+
] }),
|
|
546
|
+
/* @__PURE__ */ jsx(
|
|
547
|
+
Button,
|
|
548
|
+
{
|
|
549
|
+
variant: "primary",
|
|
550
|
+
onClick: handleStatusUpdate,
|
|
551
|
+
disabled: !selectedStatus || isUpdating,
|
|
552
|
+
isLoading: isUpdating,
|
|
553
|
+
children: "Update Status"
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
] }),
|
|
557
|
+
updateError && /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-error", children: updateError }),
|
|
558
|
+
updateSuccess && /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-success", children: "Status updated successfully" })
|
|
559
|
+
] }),
|
|
560
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [
|
|
561
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
562
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Contact Information" }),
|
|
563
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
564
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
565
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Email" }),
|
|
566
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: request.email })
|
|
567
|
+
] }),
|
|
568
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
569
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Source" }),
|
|
570
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: request.source ?? "storefront" })
|
|
571
|
+
] }),
|
|
572
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
573
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Created" }),
|
|
574
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: new Date(request.created_at).toLocaleString() })
|
|
575
|
+
] }),
|
|
576
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
577
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Last Updated" }),
|
|
578
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: new Date(request.updated_at).toLocaleString() })
|
|
579
|
+
] })
|
|
580
|
+
] })
|
|
581
|
+
] }),
|
|
582
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
583
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Status History" }),
|
|
584
|
+
request.status_history && request.status_history.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: request.status_history.map((entry, index) => /* @__PURE__ */ jsx(
|
|
585
|
+
"div",
|
|
586
|
+
{
|
|
587
|
+
className: "flex items-center justify-between border-b border-ui-border-subtle pb-2 last:border-0",
|
|
588
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
589
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
590
|
+
/* @__PURE__ */ jsx(
|
|
591
|
+
Badge,
|
|
592
|
+
{
|
|
593
|
+
size: "2xsmall",
|
|
594
|
+
className: `uppercase ${getStatusBadgeClass(entry.to)}`,
|
|
595
|
+
children: entry.to.replace("_", " ")
|
|
596
|
+
}
|
|
597
|
+
),
|
|
598
|
+
entry.from && /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
599
|
+
"from ",
|
|
600
|
+
entry.from.replace("_", " ")
|
|
601
|
+
] })
|
|
602
|
+
] }),
|
|
603
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
604
|
+
new Date(entry.changed_at).toLocaleString(),
|
|
605
|
+
entry.changed_by && ` by ${entry.changed_by}`
|
|
606
|
+
] })
|
|
607
|
+
] })
|
|
608
|
+
},
|
|
609
|
+
index
|
|
610
|
+
)) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No status history available" })
|
|
611
|
+
] })
|
|
612
|
+
] }),
|
|
613
|
+
request.payload && Object.keys(request.payload).length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
614
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Request Details" }),
|
|
615
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-3", children: Object.entries(request.payload).map(([key, value]) => /* @__PURE__ */ jsxs("div", { children: [
|
|
616
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: key.charAt(0).toUpperCase() + key.slice(1).replace("_", " ") }),
|
|
617
|
+
/* @__PURE__ */ jsx(
|
|
618
|
+
Textarea,
|
|
619
|
+
{
|
|
620
|
+
value: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
621
|
+
readOnly: true,
|
|
622
|
+
className: "mt-1 min-h-[60px]"
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
] }, key)) })
|
|
626
|
+
] }),
|
|
627
|
+
request.metadata && Object.keys(request.metadata).length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
628
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Metadata" }),
|
|
629
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: Object.entries(request.metadata).map(([key, value]) => /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
630
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: key }),
|
|
631
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "font-medium", children: typeof value === "string" ? value : JSON.stringify(value) })
|
|
632
|
+
] }, key)) })
|
|
633
|
+
] })
|
|
634
|
+
] }) });
|
|
635
|
+
};
|
|
636
|
+
const config = defineRouteConfig({
|
|
637
|
+
label: "Contact Request Details",
|
|
638
|
+
icon: ChatBubbleLeftRight
|
|
639
|
+
});
|
|
171
640
|
const en = {};
|
|
172
641
|
const i18nTranslations0 = {
|
|
173
642
|
en
|
|
@@ -178,15 +647,35 @@ const routeModule = {
|
|
|
178
647
|
{
|
|
179
648
|
Component: ContactEmailSubscriptionsPage,
|
|
180
649
|
path: "/contact-email-subscriptions"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
Component: ContactRequestsPage,
|
|
653
|
+
path: "/contact-requests"
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
Component: ContactRequestDetailPage,
|
|
657
|
+
path: "/contact-requests/:id"
|
|
181
658
|
}
|
|
182
659
|
]
|
|
183
660
|
};
|
|
184
661
|
const menuItemModule = {
|
|
185
662
|
menuItems: [
|
|
663
|
+
{
|
|
664
|
+
label: config$2.label,
|
|
665
|
+
icon: config$2.icon,
|
|
666
|
+
path: "/contact-email-subscriptions",
|
|
667
|
+
nested: void 0
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
label: config$1.label,
|
|
671
|
+
icon: config$1.icon,
|
|
672
|
+
path: "/contact-requests",
|
|
673
|
+
nested: void 0
|
|
674
|
+
},
|
|
186
675
|
{
|
|
187
676
|
label: config.label,
|
|
188
677
|
icon: config.icon,
|
|
189
|
-
path: "/contact-
|
|
678
|
+
path: "/contact-requests/:id",
|
|
190
679
|
nested: void 0
|
|
191
680
|
}
|
|
192
681
|
]
|
package/README.md
CHANGED
|
@@ -428,7 +428,7 @@ export async function action(formData: FormData) {
|
|
|
428
428
|
### Email subscriptions
|
|
429
429
|
|
|
430
430
|
```ts
|
|
431
|
-
import { upsertContactSubscription } from "medusa-contact-us"
|
|
431
|
+
import { upsertContactSubscription } from "medusa-contact-us/helpers"
|
|
432
432
|
|
|
433
433
|
await upsertContactSubscription(
|
|
434
434
|
{
|
|
@@ -447,7 +447,7 @@ Using a Medusa JS client keeps credentials in one place while still letting you
|
|
|
447
447
|
|
|
448
448
|
```ts
|
|
449
449
|
import Medusa from "@medusajs/medusa-js"
|
|
450
|
-
import { upsertContactSubscription } from "medusa-contact-us"
|
|
450
|
+
import { upsertContactSubscription } from "medusa-contact-us/helpers"
|
|
451
451
|
|
|
452
452
|
const medusa = new Medusa({
|
|
453
453
|
baseUrl: "https://store.myshop.com",
|
|
@@ -472,7 +472,7 @@ await upsertContactSubscription(
|
|
|
472
472
|
For SSR or edge runtimes, preconfigure the helper once:
|
|
473
473
|
|
|
474
474
|
```ts
|
|
475
|
-
import { createUpsertContactSubscription } from "medusa-contact-us"
|
|
475
|
+
import { createUpsertContactSubscription } from "medusa-contact-us/helpers"
|
|
476
476
|
|
|
477
477
|
export const upsertSubscription = createUpsertContactSubscription({
|
|
478
478
|
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
|
|
@@ -482,8 +482,8 @@ export const upsertSubscription = createUpsertContactSubscription({
|
|
|
482
482
|
export async function action(formData: FormData) {
|
|
483
483
|
await upsertSubscription({
|
|
484
484
|
email: formData.get("email") as string,
|
|
485
|
-
status:
|
|
486
|
-
source:
|
|
485
|
+
status: formData.get("unsubscribe") ? "unsubscribed" : "subscribed",
|
|
486
|
+
source: "footer_form",
|
|
487
487
|
})
|
|
488
488
|
}
|
|
489
489
|
```
|
package/package.json
CHANGED