@mohasinac/appkit 4.29.0 → 4.30.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/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
- package/dist/_internal/server/jobs/handlers/_helpers.d.ts +22 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +25 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/blog/schema.js +2 -1
- package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
- package/dist/_internal/shared/features/brands/schema.js +3 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
- package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/events/schema.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.js +3 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/_internal/shared/features/reviews/schema.js +2 -1
- package/dist/_internal/shared/types/action-result.d.ts +0 -2
- package/dist/client/api/surface-error.d.ts +9 -0
- package/dist/client/api/surface-error.js +29 -3
- package/dist/client/api/useApiMutation.d.ts +10 -0
- package/dist/client/api/useApiMutation.js +2 -1
- package/dist/core/server-action.d.ts +3 -14
- package/dist/core/server-action.js +11 -2
- package/dist/errors/error-envelope.d.ts +73 -0
- package/dist/errors/error-envelope.js +96 -0
- package/dist/errors/error-handler.js +14 -7
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserSidebar.js +2 -2
- package/dist/features/account/components/UserSupportView.js +2 -6
- package/dist/features/account/hooks/useProfile.d.ts +6 -0
- package/dist/features/account/hooks/useProfile.js +1 -0
- package/dist/features/admin/components/AdminAdEditorView.js +1 -3
- package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
- package/dist/features/admin/components/AdminAddressesView.js +1 -3
- package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
- package/dist/features/admin/components/AdminBidsView.js +1 -3
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
- package/dist/features/admin/components/AdminContactEditorView.js +1 -3
- package/dist/features/admin/components/AdminContactView.js +2 -6
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
- package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
- package/dist/features/admin/components/AdminNavEditorView.js +1 -3
- package/dist/features/admin/components/AdminNavigationView.js +3 -3
- package/dist/features/admin/components/AdminNewsletterView.js +1 -3
- package/dist/features/admin/components/AdminNotificationsView.js +2 -6
- package/dist/features/admin/components/AdminOffersView.js +1 -3
- package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
- package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
- package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
- package/dist/features/admin/components/AdminProductEditorView.js +2 -4
- package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
- package/dist/features/admin/components/AdminReviewsView.js +2 -6
- package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
- package/dist/features/admin/components/AdminSectionsView.js +2 -6
- package/dist/features/admin/components/AdminSessionsView.js +1 -3
- package/dist/features/admin/components/AdminSidebar.js +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
- package/dist/features/admin/components/AdminStoresView.js +2 -6
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
- package/dist/features/admin/components/AdminUserEditorView.js +6 -18
- package/dist/features/admin/components/AdminUsersView.js +2 -6
- package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
- package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
- package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/repository/session.repository.js +13 -1
- package/dist/features/blog/actions/blog-actions.d.ts +14 -14
- package/dist/features/blog/actions/blog-actions.js +6 -8
- package/dist/features/blog/schemas/index.d.ts +1 -1
- package/dist/features/blog/schemas/index.js +2 -4
- package/dist/features/categories/api/[id]/route.js +2 -1
- package/dist/features/categories/api/route.js +2 -1
- package/dist/features/events/schemas/index.d.ts +1 -1
- package/dist/features/events/schemas/index.js +2 -4
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/filters/AsyncFacetSection.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/filters/RangeFilter.js +1 -1
- package/dist/features/filters/SwitchFilter.js +1 -1
- package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
- package/dist/features/homepage/api/carousel/route.js +4 -3
- package/dist/features/homepage/api/route.js +3 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
- package/dist/features/homepage/components/HeroBanner.js +1 -1
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +4 -4
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +24 -12
- package/dist/features/media/AvatarUpload.js +28 -10
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/products/components/MakeOfferButton.js +1 -1
- package/dist/features/reviews/api/[id]/route.js +3 -2
- package/dist/features/reviews/api/route.js +3 -2
- package/dist/features/search/components/Search.js +2 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
- package/dist/features/seller/components/SellerSidebar.js +2 -2
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
- package/dist/features/stores/repository/store.repository.d.ts +21 -0
- package/dist/features/stores/repository/store.repository.js +32 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -1
- package/dist/next/api/api-response.d.ts +29 -12
- package/dist/next/api/api-response.js +9 -8
- package/dist/next/api/apiHandler.d.ts +6 -1
- package/dist/next/api/apiHandler.js +13 -1
- package/dist/next/api/routeHandler.js +6 -6
- package/dist/next/middleware/pii-scrubber.js +5 -2
- package/dist/providers/db-firebase/base.repository.d.ts +14 -0
- package/dist/providers/db-firebase/base.repository.js +22 -0
- package/dist/providers/db-firebase/sieve.d.ts +14 -1
- package/dist/providers/db-firebase/sieve.js +3 -1
- package/dist/security/pii-encrypt.d.ts +0 -4
- package/dist/security/pii-encrypt.js +79 -13
- package/dist/security/pii-mask.d.ts +39 -1
- package/dist/security/pii-mask.js +33 -4
- package/dist/security/pii-redact.d.ts +0 -13
- package/dist/security/pii-redact.js +3 -1
- package/dist/security/pii-schemas.d.ts +0 -2
- package/dist/security/pii-schemas.js +16 -2
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/stores-seed-data.js +18 -0
- package/dist/seed/test-utils.js +6 -2
- package/dist/ui/DataTable.js +2 -2
- package/dist/ui/components/CollapsibleSection.js +1 -1
- package/dist/ui/components/ImageGallery.js +1 -1
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/forms/FormShell.d.ts +0 -19
- package/dist/ui/forms/FormShell.js +4 -23
- package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
- package/dist/ui/forms/apply-zod-issues.js +38 -0
- package/dist/ui/forms/index.d.ts +2 -1
- package/dist/ui/forms/index.js +2 -1
- package/dist/ui/icons/icon-registry.d.ts +8 -0
- package/dist/ui/icons/icon-registry.js +13 -1
- package/dist/utils/action-response.d.ts +1 -1
- package/dist/utils/media-url.d.ts +49 -0
- package/dist/utils/media-url.js +72 -1
- package/dist/validation/schemas.d.ts +15 -2
- package/dist/validation/schemas.js +18 -22
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +10 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE shape of an HTTP error body.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
* --------------------
|
|
6
|
+
* There were four independent producers of an error response, and the client
|
|
7
|
+
* read a shape none of them fully emitted:
|
|
8
|
+
*
|
|
9
|
+
* - `next/api/api-response.ts` → `{success, error, details}` — no `code`
|
|
10
|
+
* - `next/api/routeHandler.ts` → `{ok, success, code, error, issues, requestId}`
|
|
11
|
+
* - `errors/error-handler.ts` → `{..., data: {issues}}` — issues nested
|
|
12
|
+
* - `core/server-action.ts` → `{ok, error, fieldErrors}`
|
|
13
|
+
*
|
|
14
|
+
* while `http/ApiClient.ts`'s `ApiClientError` reads exactly three keys off the
|
|
15
|
+
* body: `code`, `issues`, `requestId`.
|
|
16
|
+
*
|
|
17
|
+
* The consequence was silent and app-wide. On every route built with
|
|
18
|
+
* `createApiHandler`, a Zod rejection serialised its issues under `details`,
|
|
19
|
+
* which nothing reads — so `err.code` was `undefined`, `surfaceError`'s
|
|
20
|
+
* `hasStableErrorCode()` check failed, the `ERROR_DISPLAY_MAP` branch was
|
|
21
|
+
* skipped, and the user got a bare toast reading "Validation failed" with no
|
|
22
|
+
* field name. `applyZodIssues(err.issues, …)` was dead code everywhere.
|
|
23
|
+
*
|
|
24
|
+
* Every producer now builds its body here, so the wire shape and the reader
|
|
25
|
+
* cannot drift again. `scripts/audit-api-error-envelope.mjs` enforces it in
|
|
26
|
+
* both directions.
|
|
27
|
+
*/
|
|
28
|
+
import { HTTP_ERROR_CODES } from "./error-mapping";
|
|
29
|
+
/**
|
|
30
|
+
* Default stable code for an HTTP status, for producers that don't carry one.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the mapping already applied in `mapToHttpError`'s status branch —
|
|
33
|
+
* extracted so there is one table rather than two that can disagree about
|
|
34
|
+
* what a 409 is called.
|
|
35
|
+
*/
|
|
36
|
+
export function codeForStatus(status) {
|
|
37
|
+
if (status === 401)
|
|
38
|
+
return HTTP_ERROR_CODES.UNAUTHENTICATED;
|
|
39
|
+
if (status === 403)
|
|
40
|
+
return HTTP_ERROR_CODES.FORBIDDEN;
|
|
41
|
+
if (status === 404)
|
|
42
|
+
return HTTP_ERROR_CODES.NOT_FOUND;
|
|
43
|
+
if (status === 409)
|
|
44
|
+
return HTTP_ERROR_CODES.ALREADY_EXISTS;
|
|
45
|
+
if (status === 429)
|
|
46
|
+
return HTTP_ERROR_CODES.RATE_LIMITED;
|
|
47
|
+
if (status >= 500)
|
|
48
|
+
return HTTP_ERROR_CODES.INTERNAL;
|
|
49
|
+
return HTTP_ERROR_CODES.VALIDATION_FAILED;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Narrow raw Zod issues to the wire shape.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately picks ONLY `path`, `message` and `code`. A `ZodIssue` can also
|
|
55
|
+
* carry `received` / `expected` / `unionErrors`, and `received` is the value
|
|
56
|
+
* the user actually submitted — i.e. potentially their email, phone or
|
|
57
|
+
* address. Spreading the issue wholesale would publish that in an error body
|
|
58
|
+
* and into any client-error log that captures it.
|
|
59
|
+
*
|
|
60
|
+
* The `unknown` parameter is the point: this is the sole entry point for issue
|
|
61
|
+
* arrays arriving from THREE mutually unassignable sources — zod v3
|
|
62
|
+
* `ZodIssue[]` (appkit), zod v4 `$ZodIssue[]` (the consumer app, a different
|
|
63
|
+
* major), and the `unknown[]` that `mapToHttpError` already returns. Narrowing
|
|
64
|
+
* it to any one of them makes the other two uncallable. This function exists
|
|
65
|
+
* precisely to be the place that narrows.
|
|
66
|
+
*/
|
|
67
|
+
export function toApiIssues(input) {
|
|
68
|
+
if (!Array.isArray(input) || input.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
const mapped = [];
|
|
71
|
+
for (const raw of input) {
|
|
72
|
+
if (typeof raw !== "object" || raw === null)
|
|
73
|
+
continue;
|
|
74
|
+
const issue = raw;
|
|
75
|
+
if (typeof issue.message !== "string")
|
|
76
|
+
continue;
|
|
77
|
+
mapped.push({
|
|
78
|
+
message: issue.message,
|
|
79
|
+
...(Array.isArray(issue.path)
|
|
80
|
+
? { path: issue.path.filter((p) => typeof p === "string" || typeof p === "number") }
|
|
81
|
+
: {}),
|
|
82
|
+
...(typeof issue.code === "string" ? { code: issue.code } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return mapped.length ? mapped : undefined;
|
|
86
|
+
}
|
|
87
|
+
export function buildErrorEnvelope(input) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
success: false,
|
|
91
|
+
code: input.code ?? codeForStatus(input.status ?? 400),
|
|
92
|
+
error: input.message,
|
|
93
|
+
...(input.issues?.length ? { issues: input.issues } : {}),
|
|
94
|
+
...(input.requestId ? { requestId: input.requestId } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server.js";
|
|
2
2
|
import { AppError } from "./base-error";
|
|
3
3
|
import { mapToHttpError } from "./error-mapping";
|
|
4
|
+
import { buildErrorEnvelope, toApiIssues } from "./error-envelope";
|
|
4
5
|
/**
|
|
5
6
|
* Handle API errors with consistent response format.
|
|
6
7
|
* Use in Next.js API route catch blocks.
|
|
@@ -23,16 +24,22 @@ export function handleApiError(error) {
|
|
|
23
24
|
data: error instanceof AppError ? error.data : undefined,
|
|
24
25
|
});
|
|
25
26
|
}
|
|
27
|
+
// `issues` go at the TOP LEVEL. They used to be nested under
|
|
28
|
+
// `data.issues`, and `ApiClientError` reads `body.issues` — so this
|
|
29
|
+
// producer's field errors were as dead as `api-response.ts`'s were.
|
|
30
|
+
//
|
|
31
|
+
// `statusCode` is dropped: it only ever duplicated the HTTP status, and
|
|
32
|
+
// nothing read it off the body.
|
|
26
33
|
return NextResponse.json({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
...buildErrorEnvelope({
|
|
35
|
+
message: mapped.message,
|
|
36
|
+
status: mapped.status,
|
|
37
|
+
code: mapped.code,
|
|
38
|
+
issues: toApiIssues(mapped.issues),
|
|
39
|
+
}),
|
|
31
40
|
...(error instanceof AppError && error.data !== undefined
|
|
32
41
|
? { data: error.data }
|
|
33
|
-
:
|
|
34
|
-
? { data: { issues: mapped.issues } }
|
|
35
|
-
: {}),
|
|
42
|
+
: {}),
|
|
36
43
|
}, { status: mapped.status });
|
|
37
44
|
}
|
|
38
45
|
/**
|
|
@@ -96,7 +96,7 @@ export function NotificationBell({ limit = 10, viewAllHref, labels, icons, rende
|
|
|
96
96
|
}
|
|
97
97
|
function renderBellButton(props) {
|
|
98
98
|
const { hideOnMobile, buttonClassName, labels, unreadCount, handleToggle, isOpen } = props;
|
|
99
|
-
return (_jsxs(Button, { onClick: handleToggle, rounded: "xl", className: `${hideOnMobile ? "hidden md:flex" : "flex"} p-[0.625rem] md:p-[0.75rem] transition-colors relative text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-bg)] hover:text-[var(--appkit-color-text)] ${buttonClassName}`, "aria-label": labels.title, "aria-expanded": isOpen, children: [_jsx("svg", { className: "w-6 h-6 md:w-7 md:h-7", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" }) }), unreadCount > 0 && (_jsx(Span, { size: "xs", weight: "semibold", className: CLS_UNREAD_BADGE, children: unreadCount > 99 ? "99+" : unreadCount }))] }));
|
|
99
|
+
return (_jsxs(Button, { onClick: handleToggle, rounded: "xl", className: `${hideOnMobile ? "hidden md:flex" : "flex"} p-[0.625rem] md:p-[0.75rem] transition-colors relative text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-bg)] hover:text-[var(--appkit-color-text)] ${buttonClassName}`, "aria-label": labels.title, "aria-expanded": isOpen, children: [_jsx("svg", { className: "w-6 h-6 md:w-7 md:h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" }) }), unreadCount > 0 && (_jsx(Span, { size: "xs", weight: "semibold", className: CLS_UNREAD_BADGE, children: unreadCount > 99 ? "99+" : unreadCount }))] }));
|
|
100
100
|
}
|
|
101
101
|
function renderNotificationDropdown(props) {
|
|
102
102
|
const { dropdownClassName, labels, unreadCount, isMarkingAll, handleMarkAllRead, isLoading, notifications, notificationIcons, handleMarkRead, handleMarkReadAndClose, renderActionLink, viewAllHref, setIsOpen } = props;
|
|
@@ -73,13 +73,13 @@ function DrawerContent({ groups, items, activeHref, onItemClick, }) {
|
|
|
73
73
|
const hasActive = !!findActiveNavItem(group.items, activeHref);
|
|
74
74
|
return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
|
|
75
75
|
? "text-primary-600 dark:text-primary-400"
|
|
76
|
-
: "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: activeItem?.href === item.href, onClick: onItemClick }) }, item.href))) }))] }, group.title));
|
|
76
|
+
: "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""} shrink-0`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: activeItem?.href === item.href, onClick: onItemClick }) }, item.href))) }))] }, group.title));
|
|
77
77
|
})] }));
|
|
78
78
|
}
|
|
79
79
|
function DrawerPanel({ title, onClose, children, }) {
|
|
80
80
|
const { hand } = useHandMode();
|
|
81
81
|
const { edgeClass, borderClass } = getSidebarOverlayClasses(hand);
|
|
82
|
-
return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: `fixed top-0 ${edgeClass} z-50 h-full w-64 ${borderClass}`, surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
|
|
82
|
+
return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: `fixed top-0 ${edgeClass} z-50 h-full w-64 ${borderClass}`, surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
|
|
83
83
|
}
|
|
84
84
|
export function UserSidebar({ items, groups, mobileOpen = false, onCloseMobile, desktopOpen = false, onToggle, variant = "overlay", renderFooter }) {
|
|
85
85
|
const pathname = usePathname();
|
|
@@ -69,6 +69,7 @@ export function UserSupportView(_props) {
|
|
|
69
69
|
const tickets = data?.tickets ?? [];
|
|
70
70
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["user", "support-tickets"] });
|
|
71
71
|
const createMutation = useApiMutation({
|
|
72
|
+
errorMessage: "Failed to create ticket.",
|
|
72
73
|
mutationFn: async () => {
|
|
73
74
|
/*
|
|
74
75
|
* This drawer validated NOTHING. Its sibling page
|
|
@@ -106,11 +107,9 @@ export function UserSupportView(_props) {
|
|
|
106
107
|
setNewOrderId("");
|
|
107
108
|
invalidate();
|
|
108
109
|
},
|
|
109
|
-
onError: (err) => {
|
|
110
|
-
showToast(err?.message ?? "Failed to create ticket.", "error");
|
|
111
|
-
},
|
|
112
110
|
});
|
|
113
111
|
const replyMutation = useApiMutation({
|
|
112
|
+
errorMessage: "Failed to send reply.",
|
|
114
113
|
mutationFn: async () => {
|
|
115
114
|
await apiClient.post(SUPPORT_ENDPOINTS.TICKET_MESSAGES(selectedTicket.id), { body: replyBody.trim() });
|
|
116
115
|
},
|
|
@@ -119,9 +118,6 @@ export function UserSupportView(_props) {
|
|
|
119
118
|
setReplyBody("");
|
|
120
119
|
invalidate();
|
|
121
120
|
},
|
|
122
|
-
onError: (err) => {
|
|
123
|
-
showToast(err?.message ?? "Failed to send reply.", "error");
|
|
124
|
-
},
|
|
125
121
|
});
|
|
126
122
|
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "mx-auto max-w-2xl", paddingY: "y-lg", paddingX: "x-md", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", children: [_jsx(Text, { size: "xl", weight: "semibold", color: "primary", children: "Support Tickets" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setNewTicketOpen(true), children: "New ticket" })] }), renderTicketListArea({ isLoading, error, tickets, setSelectedTicket, setDetailOpen })] }), renderNewTicketDrawer({ newTicketOpen, setNewTicketOpen, newCategory, setNewCategory, newSubject, setNewSubject, newOrderId, setNewOrderId, newDescription, setNewDescription, createMutation }), renderTicketDetailDrawer({ detailOpen, setDetailOpen, selectedTicket, replyBody, setReplyBody, replyMutation })] }));
|
|
127
123
|
}
|
|
@@ -45,4 +45,10 @@ export declare function useUpdateCurrentProfile(options?: {
|
|
|
45
45
|
onSuccess?: (data: JsonValue) => void;
|
|
46
46
|
onError?: (error: Error) => void;
|
|
47
47
|
endpoint?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Curated failure copy, forwarded to the ONE error surface. A wrapper that
|
|
50
|
+
* omits this forces its callers to add a second toast of their own, which
|
|
51
|
+
* is exactly how one failed avatar save produced three stacked banners.
|
|
52
|
+
*/
|
|
53
|
+
errorMessage?: string;
|
|
48
54
|
}): import("@tanstack/react-query").UseMutationResult<JsonValue, Error, UpdateCurrentProfileInput, unknown>;
|
|
@@ -14,6 +14,7 @@ export function useUpdateCurrentProfile(options) {
|
|
|
14
14
|
const queryClient = useQueryClient();
|
|
15
15
|
const endpoint = options?.endpoint ?? ACCOUNT_ENDPOINTS.PROFILE;
|
|
16
16
|
return useApiMutation({
|
|
17
|
+
errorMessage: options?.errorMessage,
|
|
17
18
|
mutationFn: (data) => apiClient.patch(endpoint, data),
|
|
18
19
|
onSuccess: async (data) => {
|
|
19
20
|
await queryClient.invalidateQueries({ queryKey: ["profile"] });
|
|
@@ -83,6 +83,7 @@ export function AdminAdEditorView({ adId, endpointBase = ADMIN_ENDPOINTS.ADS, la
|
|
|
83
83
|
setThirdPartyUrl(item.creative?.thirdPartyUrl || "");
|
|
84
84
|
}, [adQuery.data]);
|
|
85
85
|
const saveMutation = useApiMutation({
|
|
86
|
+
errorMessage: "Save failed",
|
|
86
87
|
mutationFn: async () => {
|
|
87
88
|
const payload = {
|
|
88
89
|
name,
|
|
@@ -116,9 +117,6 @@ export function AdminAdEditorView({ adId, endpointBase = ADMIN_ENDPOINTS.ADS, la
|
|
|
116
117
|
onSaved?.(savedId);
|
|
117
118
|
}
|
|
118
119
|
},
|
|
119
|
-
onError: (error) => {
|
|
120
|
-
showToast(error instanceof Error ? error.message : "Save failed", "error");
|
|
121
|
-
},
|
|
122
120
|
});
|
|
123
121
|
const placements = adQuery.data?.placements ?? metaQuery.data?.placements ?? [];
|
|
124
122
|
const providerCredentialStatus = metaQuery.data?.providerCredentialStatus;
|
|
@@ -22,6 +22,7 @@ export function AdminAddressClustersView(_props) {
|
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
24
|
const flagMutation = useApiMutation({
|
|
25
|
+
errorMessage: "Failed to flag address.",
|
|
25
26
|
mutationFn: async (id) => {
|
|
26
27
|
await apiClient.patch(ADMIN_ENDPOINTS.ADDRESS_BY_ID(id), { action: "flag_suspicious" });
|
|
27
28
|
},
|
|
@@ -29,9 +30,6 @@ export function AdminAddressClustersView(_props) {
|
|
|
29
30
|
showToast("Address flagged as suspicious.", "success");
|
|
30
31
|
void queryClient.invalidateQueries({ queryKey: ["admin", "address-clusters"] });
|
|
31
32
|
},
|
|
32
|
-
onError: (err) => {
|
|
33
|
-
showToast(err.message ?? "Failed to flag address.", "error");
|
|
34
|
-
},
|
|
35
33
|
});
|
|
36
34
|
const clusters = data?.clusters ?? [];
|
|
37
35
|
return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Address Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Users sharing the same physical address. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared addresses found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.addressHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.addresses.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.addresses.map((addr) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "primary", className: "truncate", children: [addr.ownerType === "user" ? "User" : "Store", ": ", addr.ownerId] }), addr.city && (_jsx(Text, { size: "xs", color: "muted", children: [addr.city, addr.state, addr.postalCode].filter(Boolean).join(", ") }))] }), _jsxs(Row, { gap: "xs", align: "center", children: [addr.banStatus && (_jsx(Badge, { variant: BAN_BADGE[addr.banStatus] ?? "secondary", size: "sm", children: addr.banStatus.replace(/_/g, " ") })), addr.banStatus !== "suspicious" && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => flagMutation.mutate(addr.id), disabled: flagMutation.isPending, children: "Flag Suspicious" }))] })] }, addr.id))) })] }, cluster.addressHash)))] }));
|
|
@@ -64,6 +64,7 @@ export function AdminAddressEditorView({ addressId, onSaved, onDeleted, ...rest
|
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
const saveMutation = useApiMutation({
|
|
67
|
+
errorMessage: "Failed to save address.",
|
|
67
68
|
mutationFn: async () => {
|
|
68
69
|
const payload = {
|
|
69
70
|
...form,
|
|
@@ -81,11 +82,9 @@ export function AdminAddressEditorView({ addressId, onSaved, onDeleted, ...rest
|
|
|
81
82
|
if (onSaved && id)
|
|
82
83
|
onSaved(String(id));
|
|
83
84
|
},
|
|
84
|
-
onError: (err) => {
|
|
85
|
-
showToast(err?.message ?? "Failed to save address.", "error");
|
|
86
|
-
},
|
|
87
85
|
});
|
|
88
86
|
const deleteMutation = useApiMutation({
|
|
87
|
+
errorMessage: "Failed to delete address.",
|
|
89
88
|
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.ADDRESS_BY_ID(addressId)),
|
|
90
89
|
onSuccess: () => {
|
|
91
90
|
showToast("Address deleted.", "success");
|
|
@@ -93,7 +92,6 @@ export function AdminAddressEditorView({ addressId, onSaved, onDeleted, ...rest
|
|
|
93
92
|
if (onDeleted)
|
|
94
93
|
onDeleted();
|
|
95
94
|
},
|
|
96
|
-
onError: (err) => showToast(err?.message ?? "Failed to delete address.", "error"),
|
|
97
95
|
});
|
|
98
96
|
const isSubmitting = saveMutation.isPending || isLoading;
|
|
99
97
|
const canSave = form.ownerId &&
|
|
@@ -133,6 +133,7 @@ export function AdminAddressesView(_props) {
|
|
|
133
133
|
const queryClient = useQueryClient();
|
|
134
134
|
const { showToast } = useToast();
|
|
135
135
|
const actionMutation = useApiMutation({
|
|
136
|
+
errorMessage: "Failed to update address.",
|
|
136
137
|
mutationFn: async ({ id, action }) => {
|
|
137
138
|
await apiClient.patch(ADMIN_ENDPOINTS.ADDRESS_BY_ID(id), { action });
|
|
138
139
|
},
|
|
@@ -140,9 +141,6 @@ export function AdminAddressesView(_props) {
|
|
|
140
141
|
showToast("Address updated.", "success");
|
|
141
142
|
void queryClient.invalidateQueries({ queryKey: ["admin", "addresses"] });
|
|
142
143
|
},
|
|
143
|
-
onError: (err) => {
|
|
144
|
-
showToast(err.message ?? "Failed to update address.", "error");
|
|
145
|
-
},
|
|
146
144
|
});
|
|
147
145
|
const handleAction = (id, action) => {
|
|
148
146
|
actionMutation.mutate({ id, action });
|
|
@@ -17,6 +17,7 @@ export function AdminAllEventEntriesView({ children, ...props }) {
|
|
|
17
17
|
const { showToast } = useToast();
|
|
18
18
|
const [selected, setSelected] = React.useState(null);
|
|
19
19
|
const updateMutation = useApiMutation({
|
|
20
|
+
errorMessage: "Failed to update entry.",
|
|
20
21
|
mutationFn: async ({ id, status }) => {
|
|
21
22
|
await apiClient.patch(ADMIN_ENDPOINTS.ADMIN_EVENT_ENTRY_BY_ID(id), { status });
|
|
22
23
|
},
|
|
@@ -24,9 +25,6 @@ export function AdminAllEventEntriesView({ children, ...props }) {
|
|
|
24
25
|
showToast("Entry updated.", "success");
|
|
25
26
|
queryClient.invalidateQueries({ queryKey: ["admin", "event-entries"] });
|
|
26
27
|
},
|
|
27
|
-
onError: (err) => {
|
|
28
|
-
showToast(err?.message ?? "Failed to update entry.", "error");
|
|
29
|
-
},
|
|
30
28
|
});
|
|
31
29
|
if (React.Children.count(children) > 0) {
|
|
32
30
|
return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
|
|
@@ -24,6 +24,7 @@ export function AdminBidsView({ children, ...props }) {
|
|
|
24
24
|
const [selectedRow, setSelectedRow] = useState(null);
|
|
25
25
|
const [detail, setDetail] = useState(null);
|
|
26
26
|
const cancelMutation = useApiMutation({
|
|
27
|
+
errorMessage: "Failed to cancel bid.",
|
|
27
28
|
mutationFn: async (bidId) => {
|
|
28
29
|
await apiClient.patch(ADMIN_ENDPOINTS.BID_BY_ID(bidId), { status: "cancelled" });
|
|
29
30
|
},
|
|
@@ -33,9 +34,6 @@ export function AdminBidsView({ children, ...props }) {
|
|
|
33
34
|
setCancelOpen(false);
|
|
34
35
|
setSelectedRow(null);
|
|
35
36
|
},
|
|
36
|
-
onError: (err) => {
|
|
37
|
-
showToast(err?.message ?? "Failed to cancel bid.", "error");
|
|
38
|
-
},
|
|
39
37
|
});
|
|
40
38
|
if (React.Children.count(children) > 0) {
|
|
41
39
|
return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
|
|
@@ -114,6 +114,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
114
114
|
}, [postQuery.data]);
|
|
115
115
|
// --- save ---
|
|
116
116
|
const saveMutation = useApiMutation({
|
|
117
|
+
errorMessage: "Failed to save post.",
|
|
117
118
|
mutationFn: async () => {
|
|
118
119
|
const payload = {
|
|
119
120
|
title: draft.title,
|
|
@@ -164,18 +165,17 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
164
165
|
if (issues && issues.length > 0) {
|
|
165
166
|
applyZodIssues(issues, setFieldError);
|
|
166
167
|
}
|
|
167
|
-
showToast(err?.message ?? "Failed to save post.", "error");
|
|
168
168
|
},
|
|
169
169
|
});
|
|
170
170
|
// --- delete ---
|
|
171
171
|
const deleteMutation = useApiMutation({
|
|
172
|
+
errorMessage: "Failed to delete post.",
|
|
172
173
|
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.BLOG_BY_ID(postId)),
|
|
173
174
|
onSuccess: () => {
|
|
174
175
|
showToast("Post deleted.", "success");
|
|
175
176
|
if (onDeleted)
|
|
176
177
|
onDeleted();
|
|
177
178
|
},
|
|
178
|
-
onError: (err) => showToast(err?.message ?? "Failed to delete post.", "error"),
|
|
179
179
|
});
|
|
180
180
|
const isLoading = saveMutation.isPending || postQuery.isLoading;
|
|
181
181
|
/**
|
|
@@ -5,6 +5,7 @@ import { useApiMutation } from "@mohasinac/appkit/client";
|
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { useQuery } from "@tanstack/react-query";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { mediaUrlSchema } from "../../../validation/schemas";
|
|
8
9
|
import { Button, ConfirmDeleteModal, Div, Form, Input, StackedViewShell, Toggle, useToast, } from "../../../ui";
|
|
9
10
|
import { FieldInput, FormErrorSummary } from "../../../ui/forms";
|
|
10
11
|
import { ImageUpload } from "../../media/upload/ImageUpload";
|
|
@@ -15,7 +16,7 @@ const brandFormSchema = z.object({
|
|
|
15
16
|
name: z.string().min(1, "Brand name is required").max(120),
|
|
16
17
|
slug: z.string().regex(/^[a-z0-9-]+$/i).optional().or(z.literal("")),
|
|
17
18
|
description: z.string().max(2000).optional().or(z.literal("")),
|
|
18
|
-
logoURL:
|
|
19
|
+
logoURL: mediaUrlSchema.optional().or(z.literal("")),
|
|
19
20
|
website: z.string().url().optional().or(z.literal("")),
|
|
20
21
|
country: z.string().max(80).optional().or(z.literal("")),
|
|
21
22
|
founded: z.number().int().min(1800).max(new Date().getFullYear()).optional(),
|
|
@@ -75,6 +76,7 @@ export function AdminBrandEditorView({ brandId, onSaved, onDeleted, embedded, ..
|
|
|
75
76
|
setSlug(toBrandSlug(value));
|
|
76
77
|
};
|
|
77
78
|
const saveMutation = useApiMutation({
|
|
79
|
+
errorMessage: "Failed to save brand.",
|
|
78
80
|
mutationFn: async () => {
|
|
79
81
|
const payload = {
|
|
80
82
|
name,
|
|
@@ -98,18 +100,15 @@ export function AdminBrandEditorView({ brandId, onSaved, onDeleted, embedded, ..
|
|
|
98
100
|
if (onSaved && id)
|
|
99
101
|
onSaved(id);
|
|
100
102
|
},
|
|
101
|
-
onError: (err) => {
|
|
102
|
-
showToast(err?.message ?? "Failed to save brand.", "error");
|
|
103
|
-
},
|
|
104
103
|
});
|
|
105
104
|
const deleteMutation = useApiMutation({
|
|
105
|
+
errorMessage: "Failed to delete brand.",
|
|
106
106
|
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.BRAND_BY_ID(brandId)),
|
|
107
107
|
onSuccess: () => {
|
|
108
108
|
showToast("Brand deleted.", "success");
|
|
109
109
|
if (onDeleted)
|
|
110
110
|
onDeleted();
|
|
111
111
|
},
|
|
112
|
-
onError: (err) => showToast(err?.message ?? "Failed to delete brand.", "error"),
|
|
113
112
|
});
|
|
114
113
|
const { upload } = useMediaUpload();
|
|
115
114
|
const isSubmitting = saveMutation.isPending || brandQuery.isLoading;
|
|
@@ -36,6 +36,7 @@ export function AdminCarouselGroupEditorView({ onCreated, carouselId, onSaved, .
|
|
|
36
36
|
setStatus(doc.status === "active" ? "active" : "draft");
|
|
37
37
|
}, [existing.data]);
|
|
38
38
|
const createMutation = useApiMutation({
|
|
39
|
+
errorMessage: "create",
|
|
39
40
|
/*
|
|
40
41
|
* PATCH on the edit path, not PUT. `/api/admin/carousels/[id]` exports
|
|
41
42
|
* GET/PATCH/DELETE — copying `AdminFeatureEditorView`'s `apiClient.put`
|
|
@@ -62,9 +63,6 @@ export function AdminCarouselGroupEditorView({ onCreated, carouselId, onSaved, .
|
|
|
62
63
|
else
|
|
63
64
|
router.push(String(ROUTES.ADMIN.CAROUSEL_DETAIL(id)));
|
|
64
65
|
},
|
|
65
|
-
onError: (err) => {
|
|
66
|
-
showToast(err?.message ?? `Failed to ${isEdit ? "update" : "create"} carousel.`, "error");
|
|
67
|
-
},
|
|
68
66
|
});
|
|
69
67
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Carousel" : "New Named Carousel", sections: [
|
|
70
68
|
_jsx(Form, { schema: carouselGroupFormSchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "name", label: "Carousel name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Homepage Hero" }), _jsx(FieldSelect, { name: "status", label: "Status", value: status, onChange: (v) => setStatus(v), options: [
|
|
@@ -82,6 +82,7 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
|
|
|
82
82
|
setSlug(toCategorySlug(value));
|
|
83
83
|
};
|
|
84
84
|
const saveMutation = useApiMutation({
|
|
85
|
+
errorMessage: "Failed to save category.",
|
|
85
86
|
mutationFn: async () => {
|
|
86
87
|
const payload = {
|
|
87
88
|
name,
|
|
@@ -103,18 +104,15 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
|
|
|
103
104
|
if (onSaved && id)
|
|
104
105
|
onSaved(id);
|
|
105
106
|
},
|
|
106
|
-
onError: (err) => {
|
|
107
|
-
showToast(err?.message ?? "Failed to save category.", "error");
|
|
108
|
-
},
|
|
109
107
|
});
|
|
110
108
|
const deleteMutation = useApiMutation({
|
|
109
|
+
errorMessage: "Failed to delete category.",
|
|
111
110
|
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.CATEGORY_BY_ID(categoryId)),
|
|
112
111
|
onSuccess: () => {
|
|
113
112
|
showToast("Category deleted.", "success");
|
|
114
113
|
if (onDeleted)
|
|
115
114
|
onDeleted();
|
|
116
115
|
},
|
|
117
|
-
onError: (err) => showToast(err?.message ?? "Failed to delete category.", "error"),
|
|
118
116
|
});
|
|
119
117
|
const isSubmitting = saveMutation.isPending || categoryQuery.isLoading;
|
|
120
118
|
const actionSidebar = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Card, { variant: "outlined", padding: "md", spacing: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: isEdit ? (isActive ? "Active" : "Inactive") : "New" }), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "submit", form: "category-editor-form", className: "w-full", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete category" }))] }) }));
|
|
@@ -16,6 +16,7 @@ export function AdminContactEditorView({ open, onClose, submissionId, subject, n
|
|
|
16
16
|
const queryClient = useQueryClient();
|
|
17
17
|
const { showToast } = useToast();
|
|
18
18
|
const actionMutation = useApiMutation({
|
|
19
|
+
errorMessage: "Action failed.",
|
|
19
20
|
mutationFn: async (action) => {
|
|
20
21
|
await apiClient.patch(ADMIN_ENDPOINTS.CONTACT_SUBMISSION_BY_ID(submissionId), { action });
|
|
21
22
|
},
|
|
@@ -25,9 +26,6 @@ export function AdminContactEditorView({ open, onClose, submissionId, subject, n
|
|
|
25
26
|
queryClient.invalidateQueries({ queryKey: ["admin", "contact"] });
|
|
26
27
|
onClose();
|
|
27
28
|
},
|
|
28
|
-
onError: (err) => {
|
|
29
|
-
showToast(err?.message ?? "Action failed.", "error");
|
|
30
|
-
},
|
|
31
29
|
});
|
|
32
30
|
const statusColor = {
|
|
33
31
|
new: "bg-info-surface text-info",
|
|
@@ -22,6 +22,7 @@ export function AdminContactView({ children, onBulkMarkRead, onBulkArchive, onBu
|
|
|
22
22
|
const [selectedRow, setSelectedRow] = useState(null);
|
|
23
23
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
24
24
|
const actionMutation = useApiMutation({
|
|
25
|
+
errorMessage: "Action failed.",
|
|
25
26
|
mutationFn: async ({ id, action }) => {
|
|
26
27
|
await apiClient.patch(ADMIN_ENDPOINTS.CONTACT_SUBMISSION_BY_ID(id), { action });
|
|
27
28
|
},
|
|
@@ -29,11 +30,9 @@ export function AdminContactView({ children, onBulkMarkRead, onBulkArchive, onBu
|
|
|
29
30
|
showToast(`${action === "read" ? "Marked as read" : "Archived"}.`, "success");
|
|
30
31
|
queryClient.invalidateQueries({ queryKey: ["admin", "contact"] });
|
|
31
32
|
},
|
|
32
|
-
onError: (err) => {
|
|
33
|
-
showToast(err?.message ?? "Action failed.", "error");
|
|
34
|
-
},
|
|
35
33
|
});
|
|
36
34
|
const deleteMutation = useApiMutation({
|
|
35
|
+
errorMessage: "Failed to delete.",
|
|
37
36
|
mutationFn: async (id) => {
|
|
38
37
|
await apiClient.delete(ADMIN_ENDPOINTS.CONTACT_SUBMISSION_BY_ID(id));
|
|
39
38
|
},
|
|
@@ -43,9 +42,6 @@ export function AdminContactView({ children, onBulkMarkRead, onBulkArchive, onBu
|
|
|
43
42
|
setDeleteOpen(false);
|
|
44
43
|
setSelectedRow(null);
|
|
45
44
|
},
|
|
46
|
-
onError: (err) => {
|
|
47
|
-
showToast(err?.message ?? "Failed to delete.", "error");
|
|
48
|
-
},
|
|
49
45
|
});
|
|
50
46
|
if (React.Children.count(children) > 0) {
|
|
51
47
|
return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
|
|
@@ -132,6 +132,7 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
132
132
|
};
|
|
133
133
|
// --- save ---
|
|
134
134
|
const saveMutation = useApiMutation({
|
|
135
|
+
errorMessage: "Failed to save coupon.",
|
|
135
136
|
mutationFn: async () => {
|
|
136
137
|
const payload = {
|
|
137
138
|
name,
|
|
@@ -180,19 +181,16 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
180
181
|
if (onSaved && id)
|
|
181
182
|
onSaved(id);
|
|
182
183
|
},
|
|
183
|
-
onError: (err) => {
|
|
184
|
-
showToast(err?.message ?? "Failed to save coupon.", "error");
|
|
185
|
-
},
|
|
186
184
|
});
|
|
187
185
|
// --- delete ---
|
|
188
186
|
const deleteMutation = useApiMutation({
|
|
187
|
+
errorMessage: "Failed to delete coupon.",
|
|
189
188
|
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.COUPON_BY_ID(couponId)),
|
|
190
189
|
onSuccess: () => {
|
|
191
190
|
showToast("Coupon deleted.", "success");
|
|
192
191
|
if (onDeleted)
|
|
193
192
|
onDeleted();
|
|
194
193
|
},
|
|
195
|
-
onError: (err) => showToast(err?.message ?? "Failed to delete coupon.", "error"),
|
|
196
194
|
});
|
|
197
195
|
const isSubmitting = saveMutation.isPending || couponQuery.isLoading;
|
|
198
196
|
const canSave = Boolean(name && (isEdit ? true : code) && discountValue !== "" && startDate);
|
|
@@ -76,6 +76,7 @@ export function AdminEmployeeEditorView({ open, onClose, mode, userId, displayNa
|
|
|
76
76
|
});
|
|
77
77
|
};
|
|
78
78
|
const inviteMutation = useApiMutation({
|
|
79
|
+
errorMessage: "Failed to invite employee",
|
|
79
80
|
mutationFn: async () => {
|
|
80
81
|
await apiClient.post(ADMIN_ENDPOINTS.TEAM, {
|
|
81
82
|
email: email.trim(),
|
|
@@ -88,11 +89,9 @@ export function AdminEmployeeEditorView({ open, onClose, mode, userId, displayNa
|
|
|
88
89
|
queryClient.invalidateQueries({ queryKey: ["admin", "team"] });
|
|
89
90
|
onClose();
|
|
90
91
|
},
|
|
91
|
-
onError: (err) => {
|
|
92
|
-
showToast(err.message ?? "Failed to invite employee", "error");
|
|
93
|
-
},
|
|
94
92
|
});
|
|
95
93
|
const updateMutation = useApiMutation({
|
|
94
|
+
errorMessage: "Failed to update permissions",
|
|
96
95
|
mutationFn: async () => {
|
|
97
96
|
await apiClient.put(ADMIN_ENDPOINTS.TEAM_MEMBER(userId), {
|
|
98
97
|
permissionGroup: group,
|
|
@@ -104,11 +103,9 @@ export function AdminEmployeeEditorView({ open, onClose, mode, userId, displayNa
|
|
|
104
103
|
queryClient.invalidateQueries({ queryKey: ["admin", "team"] });
|
|
105
104
|
onClose();
|
|
106
105
|
},
|
|
107
|
-
onError: (err) => {
|
|
108
|
-
showToast(err.message ?? "Failed to update permissions", "error");
|
|
109
|
-
},
|
|
110
106
|
});
|
|
111
107
|
const revokeMutation = useApiMutation({
|
|
108
|
+
errorMessage: "Failed to revoke access",
|
|
112
109
|
mutationFn: async () => {
|
|
113
110
|
await apiClient.delete(ADMIN_ENDPOINTS.TEAM_MEMBER(userId));
|
|
114
111
|
},
|
|
@@ -118,9 +115,6 @@ export function AdminEmployeeEditorView({ open, onClose, mode, userId, displayNa
|
|
|
118
115
|
setRevokeOpen(false);
|
|
119
116
|
onClose();
|
|
120
117
|
},
|
|
121
|
-
onError: (err) => {
|
|
122
|
-
showToast(err.message ?? "Failed to revoke access", "error");
|
|
123
|
-
},
|
|
124
118
|
});
|
|
125
119
|
const isBusy = inviteMutation.isPending ||
|
|
126
120
|
updateMutation.isPending ||
|
|
@@ -91,6 +91,7 @@ export function AdminFaqEditorView({ faqId, onSaved, onDeleted, embedded, ...res
|
|
|
91
91
|
};
|
|
92
92
|
// --- save ---
|
|
93
93
|
const saveMutation = useApiMutation({
|
|
94
|
+
errorMessage: "Failed to save FAQ.",
|
|
94
95
|
mutationFn: async () => {
|
|
95
96
|
const payload = {
|
|
96
97
|
question,
|
|
@@ -116,19 +117,16 @@ export function AdminFaqEditorView({ faqId, onSaved, onDeleted, embedded, ...res
|
|
|
116
117
|
if (onSaved && id)
|
|
117
118
|
onSaved(id);
|
|
118
119
|
},
|
|
119
|
-
onError: (err) => {
|
|
120
|
-
showToast(err?.message ?? "Failed to save FAQ.", "error");
|
|
121
|
-
},
|
|
122
120
|
});
|
|
123
121
|
// --- delete ---
|
|
124
122
|
const deleteMutation = useApiMutation({
|
|
123
|
+
errorMessage: "Failed to delete FAQ.",
|
|
125
124
|
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.FAQ_BY_ID(faqId)),
|
|
126
125
|
onSuccess: () => {
|
|
127
126
|
showToast("FAQ deleted.", "success");
|
|
128
127
|
if (onDeleted)
|
|
129
128
|
onDeleted();
|
|
130
129
|
},
|
|
131
|
-
onError: (err) => showToast(err?.message ?? "Failed to delete FAQ.", "error"),
|
|
132
130
|
});
|
|
133
131
|
const isSubmitting = saveMutation.isPending || faqQuery.isLoading;
|
|
134
132
|
const canSave = Boolean(question.trim()) && Boolean(answer.trim());
|