@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.
Files changed (160) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
  2. package/dist/_internal/server/jobs/handlers/_helpers.d.ts +22 -0
  3. package/dist/_internal/server/jobs/handlers/_helpers.js +25 -1
  4. package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
  5. package/dist/_internal/shared/features/blog/schema.js +2 -1
  6. package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
  7. package/dist/_internal/shared/features/brands/schema.js +3 -1
  8. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
  9. package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
  10. package/dist/_internal/shared/features/events/schema.d.ts +2 -2
  11. package/dist/_internal/shared/features/events/schema.js +2 -1
  12. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  13. package/dist/_internal/shared/features/products/schema.js +3 -2
  14. package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
  15. package/dist/_internal/shared/features/reviews/schema.js +2 -1
  16. package/dist/_internal/shared/types/action-result.d.ts +0 -2
  17. package/dist/client/api/surface-error.d.ts +9 -0
  18. package/dist/client/api/surface-error.js +29 -3
  19. package/dist/client/api/useApiMutation.d.ts +10 -0
  20. package/dist/client/api/useApiMutation.js +2 -1
  21. package/dist/core/server-action.d.ts +3 -14
  22. package/dist/core/server-action.js +11 -2
  23. package/dist/errors/error-envelope.d.ts +73 -0
  24. package/dist/errors/error-envelope.js +96 -0
  25. package/dist/errors/error-handler.js +14 -7
  26. package/dist/features/account/components/NotificationBell.js +1 -1
  27. package/dist/features/account/components/UserSidebar.js +2 -2
  28. package/dist/features/account/components/UserSupportView.js +2 -6
  29. package/dist/features/account/hooks/useProfile.d.ts +6 -0
  30. package/dist/features/account/hooks/useProfile.js +1 -0
  31. package/dist/features/admin/components/AdminAdEditorView.js +1 -3
  32. package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
  33. package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
  34. package/dist/features/admin/components/AdminAddressesView.js +1 -3
  35. package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
  36. package/dist/features/admin/components/AdminBidsView.js +1 -3
  37. package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
  38. package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
  39. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
  40. package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
  41. package/dist/features/admin/components/AdminContactEditorView.js +1 -3
  42. package/dist/features/admin/components/AdminContactView.js +2 -6
  43. package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
  44. package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
  45. package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
  46. package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
  47. package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
  48. package/dist/features/admin/components/AdminNavEditorView.js +1 -3
  49. package/dist/features/admin/components/AdminNavigationView.js +3 -3
  50. package/dist/features/admin/components/AdminNewsletterView.js +1 -3
  51. package/dist/features/admin/components/AdminNotificationsView.js +2 -6
  52. package/dist/features/admin/components/AdminOffersView.js +1 -3
  53. package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
  54. package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
  55. package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
  56. package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
  57. package/dist/features/admin/components/AdminProductEditorView.js +2 -4
  58. package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
  59. package/dist/features/admin/components/AdminReviewsView.js +2 -6
  60. package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
  61. package/dist/features/admin/components/AdminSectionsView.js +2 -6
  62. package/dist/features/admin/components/AdminSessionsView.js +1 -3
  63. package/dist/features/admin/components/AdminSidebar.js +2 -2
  64. package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
  65. package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
  66. package/dist/features/admin/components/AdminStoresView.js +2 -6
  67. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
  68. package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
  69. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
  70. package/dist/features/admin/components/AdminUserEditorView.js +6 -18
  71. package/dist/features/admin/components/AdminUsersView.js +2 -6
  72. package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
  73. package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
  74. package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
  75. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  76. package/dist/features/auth/components/LoginForm.js +1 -1
  77. package/dist/features/auth/components/RegisterForm.js +1 -1
  78. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  79. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  80. package/dist/features/auth/repository/session.repository.js +13 -1
  81. package/dist/features/blog/actions/blog-actions.d.ts +14 -14
  82. package/dist/features/blog/actions/blog-actions.js +6 -8
  83. package/dist/features/blog/schemas/index.d.ts +1 -1
  84. package/dist/features/blog/schemas/index.js +2 -4
  85. package/dist/features/categories/api/[id]/route.js +2 -1
  86. package/dist/features/categories/api/route.js +2 -1
  87. package/dist/features/events/schemas/index.d.ts +1 -1
  88. package/dist/features/events/schemas/index.js +2 -4
  89. package/dist/features/faq/components/ContactCTA.js +1 -1
  90. package/dist/features/filters/AsyncFacetSection.js +1 -1
  91. package/dist/features/filters/FilterFacetSection.js +1 -1
  92. package/dist/features/filters/RangeFilter.js +1 -1
  93. package/dist/features/filters/SwitchFilter.js +1 -1
  94. package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
  95. package/dist/features/homepage/api/carousel/route.js +4 -3
  96. package/dist/features/homepage/api/route.js +3 -2
  97. package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
  98. package/dist/features/homepage/components/HeroBanner.js +1 -1
  99. package/dist/features/homepage/components/HeroCarousel.js +1 -1
  100. package/dist/features/homepage/components/SocialPostCard.js +1 -1
  101. package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
  102. package/dist/features/layout/AppLayoutShell.js +4 -4
  103. package/dist/features/layout/BottomNavbar.js +2 -2
  104. package/dist/features/layout/MainNavbar.js +1 -1
  105. package/dist/features/layout/TitleBarLayout.js +24 -12
  106. package/dist/features/media/AvatarUpload.js +28 -10
  107. package/dist/features/media/modals/ImageCropModal.js +1 -1
  108. package/dist/features/media/modals/ImageEditor.js +1 -1
  109. package/dist/features/products/components/MakeOfferButton.js +1 -1
  110. package/dist/features/reviews/api/[id]/route.js +3 -2
  111. package/dist/features/reviews/api/route.js +3 -2
  112. package/dist/features/search/components/Search.js +2 -2
  113. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
  114. package/dist/features/seller/components/SellerSidebar.js +2 -2
  115. package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
  116. package/dist/features/stores/repository/store.repository.d.ts +21 -0
  117. package/dist/features/stores/repository/store.repository.js +32 -0
  118. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
  119. package/dist/index.d.ts +3 -2
  120. package/dist/index.js +4 -1
  121. package/dist/next/api/api-response.d.ts +29 -12
  122. package/dist/next/api/api-response.js +9 -8
  123. package/dist/next/api/apiHandler.d.ts +6 -1
  124. package/dist/next/api/apiHandler.js +13 -1
  125. package/dist/next/api/routeHandler.js +6 -6
  126. package/dist/next/middleware/pii-scrubber.js +5 -2
  127. package/dist/providers/db-firebase/base.repository.d.ts +14 -0
  128. package/dist/providers/db-firebase/base.repository.js +22 -0
  129. package/dist/providers/db-firebase/sieve.d.ts +14 -1
  130. package/dist/providers/db-firebase/sieve.js +3 -1
  131. package/dist/security/pii-encrypt.d.ts +0 -4
  132. package/dist/security/pii-encrypt.js +79 -13
  133. package/dist/security/pii-mask.d.ts +39 -1
  134. package/dist/security/pii-mask.js +33 -4
  135. package/dist/security/pii-redact.d.ts +0 -13
  136. package/dist/security/pii-redact.js +3 -1
  137. package/dist/security/pii-schemas.d.ts +0 -2
  138. package/dist/security/pii-schemas.js +16 -2
  139. package/dist/security/settings-encryption.js +31 -3
  140. package/dist/seed/stores-seed-data.js +18 -0
  141. package/dist/seed/test-utils.js +6 -2
  142. package/dist/ui/DataTable.js +2 -2
  143. package/dist/ui/components/CollapsibleSection.js +1 -1
  144. package/dist/ui/components/ImageGallery.js +1 -1
  145. package/dist/ui/components/ListingLayout.js +1 -1
  146. package/dist/ui/forms/FormShell.d.ts +0 -19
  147. package/dist/ui/forms/FormShell.js +4 -23
  148. package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
  149. package/dist/ui/forms/apply-zod-issues.js +38 -0
  150. package/dist/ui/forms/index.d.ts +2 -1
  151. package/dist/ui/forms/index.js +2 -1
  152. package/dist/ui/icons/icon-registry.d.ts +8 -0
  153. package/dist/ui/icons/icon-registry.js +13 -1
  154. package/dist/utils/action-response.d.ts +1 -1
  155. package/dist/utils/media-url.d.ts +49 -0
  156. package/dist/utils/media-url.js +72 -1
  157. package/dist/validation/schemas.d.ts +15 -2
  158. package/dist/validation/schemas.js +18 -22
  159. package/package.json +1 -1
  160. package/scripts/seed-cli.mjs +10 -2
@@ -73,6 +73,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
73
73
  return out;
74
74
  }, [parties]);
75
75
  const updateMutation = useApiMutation({
76
+ errorMessage: "Failed to update ticket.",
76
77
  mutationFn: async () => {
77
78
  await apiClient.patch(ADMIN_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId), {
78
79
  status,
@@ -86,11 +87,9 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
86
87
  invalidate();
87
88
  onClose();
88
89
  },
89
- onError: (err) => {
90
- showToast(err?.message ?? "Failed to update ticket.", "error");
91
- },
92
90
  });
93
91
  const replyMutation = useApiMutation({
92
+ errorMessage: "Failed to send reply.",
94
93
  mutationFn: async () => {
95
94
  await apiClient.post(SUPPORT_ENDPOINTS.TICKET_MESSAGES(ticketId), { body: replyBody, newStatus: status });
96
95
  },
@@ -100,9 +99,6 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
100
99
  invalidate();
101
100
  onClose();
102
101
  },
103
- onError: (err) => {
104
- showToast(err?.message ?? "Failed to send reply.", "error");
105
- },
106
102
  });
107
103
  // ST-4 — Apply Store Change action (only relevant for store_change_request)
108
104
  const [storeStatus, setStoreStatus] = React.useState("active");
@@ -129,6 +125,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
129
125
  },
130
126
  });
131
127
  const applyStoreChange = useApiMutation({
128
+ errorMessage: "Failed to apply store change.",
132
129
  mutationFn: async () => {
133
130
  if (!linkedStoreId)
134
131
  throw new Error("No linked store on this ticket.");
@@ -149,9 +146,6 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
149
146
  .catch(console.error);
150
147
  invalidate();
151
148
  },
152
- onError: (err) => {
153
- showToast(err?.message ?? "Failed to apply store change.", "error");
154
- },
155
149
  });
156
150
  // ST-3 — Modify Order Items panel (fetches current items on demand, lets
157
151
  // admin edit quantity per line + remove lines, then PATCHes the order).
@@ -160,6 +154,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
160
154
  const linkedOrderId = parties.orderId?.trim() || orderId;
161
155
  const isOrderModificationRequest = category === "order_modification_request" || category === "order_issue";
162
156
  const loadOrderItems = useApiMutation({
157
+ errorMessage: "Failed to load order.",
163
158
  mutationFn: async () => {
164
159
  if (!linkedOrderId)
165
160
  throw new Error("No linked order on this ticket.");
@@ -174,14 +169,12 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
174
169
  })));
175
170
  setOrderItemsOpen(true);
176
171
  },
177
- onError: (err) => {
178
- showToast(err?.message ?? "Failed to load order.", "error");
179
- },
180
172
  });
181
173
  // ST-5 — Lift Ban actions for unban_request tickets
182
174
  const linkedUserId = parties.userId?.trim();
183
175
  const isUnbanRequest = category === "unban_request";
184
176
  const liftHardBan = useApiMutation({
177
+ errorMessage: "Failed to lift hard ban.",
185
178
  mutationFn: async () => {
186
179
  if (!linkedUserId)
187
180
  throw new Error("No linked user on this ticket.");
@@ -198,11 +191,9 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
198
191
  .catch(console.error);
199
192
  invalidate();
200
193
  },
201
- onError: (err) => {
202
- showToast(err?.message ?? "Failed to lift hard ban.", "error");
203
- },
204
194
  });
205
195
  const liftSoftBanTickets = useApiMutation({
196
+ errorMessage: "Failed to lift soft ban.",
206
197
  mutationFn: async () => {
207
198
  if (!linkedUserId)
208
199
  throw new Error("No linked user on this ticket.");
@@ -219,11 +210,9 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
219
210
  .catch(console.error);
220
211
  invalidate();
221
212
  },
222
- onError: (err) => {
223
- showToast(err?.message ?? "Failed to lift soft ban.", "error");
224
- },
225
213
  });
226
214
  const applyOrderItems = useApiMutation({
215
+ errorMessage: "Failed to update order items.",
227
216
  mutationFn: async () => {
228
217
  if (!linkedOrderId)
229
218
  throw new Error("No linked order on this ticket.");
@@ -256,9 +245,6 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
256
245
  invalidate();
257
246
  setOrderItemsOpen(false);
258
247
  },
259
- onError: (err) => {
260
- showToast(err?.message ?? "Failed to update order items.", "error");
261
- },
262
248
  });
263
249
  return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: subject ?? "Support Ticket", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${STATUS_COLOR[currentStatus ?? "open"] ?? STATUS_COLOR.open}`, rounded: "full", padding: "pill-sm-tall", children: (currentStatus ?? "open").replace(/_/g, " ") }), _jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: category }), orderId && (_jsxs(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: ["Order: ", orderId] }))] }), _jsxs(Div, { textSize: "sm", color: "muted", children: ["From: ", _jsx(Span, { weight: "medium", color: "primary", children: userDisplayName })] }), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), messages.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Messages (", messages.length, ")"] }), _jsx(Stack, { className: `max-h-64 ${__O.yAuto}`, gap: "sm", children: messages.map((msg, i) => (_jsxs(Div, { textSize: "sm", surface: msg.authorRole === "user" ? "default" : "info-surface", className: msg.authorRole === "user" ? "border border-[var(--appkit-color-border)]" : "border border-[var(--appkit-color-info)]", rounded: "lg", padding: "sm", children: [_jsxs(Row, { color: "muted", textSize: "xs", className: "mb-1", align: "center", gap: "sm", children: [_jsx(Span, { weight: "medium", color: "muted", children: ROLE_LABEL[msg.authorRole ?? "user"] ?? msg.authorRole }), msg.createdAt && (_jsx(Span, { children: new Date(msg.createdAt).toLocaleString() }))] }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", children: msg.body })] }, msg.id ?? i))) })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Reply to user" }), _jsx(Textarea, { value: replyBody, onChange: (e) => setReplyBody(e.target.value), rows: 3, placeholder: "Type a reply\u2026" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", isLoading: replyMutation.isPending, disabled: !replyBody.trim() || !ticketId || replyMutation.isPending, onClick: () => replyMutation.mutate(), children: "Send reply" })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(RecordStatusTimeline, { entries: statusHistory, truncatedCount: statusHistoryTruncated }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Select, { label: "Priority", options: PRIORITY_OPTIONS, value: priority, onValueChange: setPriority }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Internal notes (staff only)" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Notes visible only to admins and employees\u2026" })] }), _jsxs(Stack, { padding: "sm", gap: "sm", rounded: "lg", border: "default", children: [_jsx(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Linked parties" }), _jsx(Text, { size: "xs", color: "muted", children: "Tag the buyer / store / order / product / bid this ticket concerns." }), [
264
250
  ["userId", "User slug"],
@@ -79,6 +79,7 @@ export function AdminTesterChecklistItemEditorView({ itemId, onSaved, onDeleted,
79
79
  setPageKey(slugifyKey(value));
80
80
  };
81
81
  const saveMutation = useApiMutation({
82
+ errorMessage: "Failed to save checklist item.",
82
83
  mutationFn: async () => {
83
84
  const payload = {
84
85
  groupKey: groupKey || slugifyKey(groupLabel),
@@ -104,18 +105,15 @@ export function AdminTesterChecklistItemEditorView({ itemId, onSaved, onDeleted,
104
105
  if (onSaved && id)
105
106
  onSaved(id);
106
107
  },
107
- onError: (err) => {
108
- showToast(err?.message ?? "Failed to save checklist item.", "error");
109
- },
110
108
  });
111
109
  const deleteMutation = useApiMutation({
110
+ errorMessage: "Failed to delete checklist item.",
112
111
  mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.TESTER_CHECKLIST_ITEM_BY_ID(itemId)),
113
112
  onSuccess: () => {
114
113
  showToast("Checklist item deleted.", "success");
115
114
  if (onDeleted)
116
115
  onDeleted();
117
116
  },
118
- onError: (err) => showToast(err?.message ?? "Failed to delete checklist item.", "error"),
119
117
  });
120
118
  const isSubmitting = saveMutation.isPending || itemQuery.isLoading;
121
119
  const canSave = Boolean(groupLabel.trim()) && Boolean(pageLabel.trim()) && Boolean(label.trim());
@@ -113,6 +113,7 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
113
113
  const invalidate = () => queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
114
114
  // --- Mutations ------------------------------------------------------------
115
115
  const saveMutation = useApiMutation({
116
+ errorMessage: "Failed to update user.",
116
117
  mutationFn: async () => {
117
118
  // ST-2 — build publicProfile partial only when something changed; keeps
118
119
  // the PATCH payload minimal and avoids overwriting unrelated subkeys.
@@ -150,11 +151,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
150
151
  invalidate();
151
152
  onClose();
152
153
  },
153
- onError: (err) => {
154
- showToast(err?.message ?? "Failed to update user.", "error");
155
- },
156
154
  });
157
155
  const deleteMutation = useApiMutation({
156
+ errorMessage: "Failed to delete user.",
158
157
  mutationFn: async () => {
159
158
  await apiClient.delete(ADMIN_ENDPOINTS.USER_BY_ID(userId));
160
159
  },
@@ -164,11 +163,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
164
163
  setDeleteOpen(false);
165
164
  onClose();
166
165
  },
167
- onError: (err) => {
168
- showToast(err?.message ?? "Failed to delete user.", "error");
169
- },
170
166
  });
171
167
  const hardBanMutation = useApiMutation({
168
+ errorMessage: "Failed to ban user.",
172
169
  mutationFn: async (reason) => {
173
170
  await apiClient.post(ADMIN_ENDPOINTS.USER_HARD_BAN(userId), { reason });
174
171
  },
@@ -179,11 +176,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
179
176
  setHardBanReasonInput("");
180
177
  onClose();
181
178
  },
182
- onError: (err) => {
183
- showToast(err?.message ?? "Failed to ban user.", "error");
184
- },
185
179
  });
186
180
  const unbanMutation = useApiMutation({
181
+ errorMessage: "Failed to unban user.",
187
182
  mutationFn: async () => {
188
183
  await apiClient.post(ADMIN_ENDPOINTS.USER_UNBAN(userId), {});
189
184
  },
@@ -192,11 +187,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
192
187
  invalidate();
193
188
  onClose();
194
189
  },
195
- onError: (err) => {
196
- showToast(err?.message ?? "Failed to unban user.", "error");
197
- },
198
190
  });
199
191
  const softBanMutation = useApiMutation({
192
+ errorMessage: "Failed to apply soft ban.",
200
193
  mutationFn: async (payload) => {
201
194
  await apiClient.post(ADMIN_ENDPOINTS.USER_SOFT_BAN(userId), payload);
202
195
  },
@@ -208,11 +201,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
208
201
  setSoftBanReason("");
209
202
  setSoftBanExpiry("");
210
203
  },
211
- onError: (err) => {
212
- showToast(err?.message ?? "Failed to apply soft ban.", "error");
213
- },
214
204
  });
215
205
  const liftSoftBanMutation = useApiMutation({
206
+ errorMessage: "Failed to lift soft ban.",
216
207
  mutationFn: async (action) => {
217
208
  await apiClient.delete(ADMIN_ENDPOINTS.USER_SOFT_BAN_LIFT(userId, action));
218
209
  },
@@ -220,9 +211,6 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
220
211
  showToast("Soft ban lifted.", "success");
221
212
  invalidate();
222
213
  },
223
- onError: (err) => {
224
- showToast(err?.message ?? "Failed to lift soft ban.", "error");
225
- },
226
214
  });
227
215
  const isHardBanned = currentIsHardBanned ?? false;
228
216
  const softBans = currentSoftBans ?? [];
@@ -52,6 +52,7 @@ export function AdminUsersView({ children, ...props }) {
52
52
  // eslint-disable-next-line react-hooks/exhaustive-deps
53
53
  }, [banJobEvent.status]);
54
54
  const banUser = useApiMutation({
55
+ errorMessage: "Failed to start hard-ban.",
55
56
  mutationFn: () => {
56
57
  if (!banTargetId)
57
58
  throw new Error("No user selected");
@@ -60,9 +61,6 @@ export function AdminUsersView({ children, ...props }) {
60
61
  onSuccess: (result) => {
61
62
  banJobEvent.subscribe(result.jobId, result.customToken);
62
63
  },
63
- onError: () => {
64
- toast.showToast("Failed to start hard-ban.", "error");
65
- },
66
64
  });
67
65
  const bulkUsers = useBulkAction({
68
66
  mutationFn: (payload) => apiClient.post(ADMIN_ENDPOINTS.USERS_BULK, payload),
@@ -75,14 +73,12 @@ export function AdminUsersView({ children, ...props }) {
75
73
  },
76
74
  });
77
75
  const unbanUser = useApiMutation({
76
+ errorMessage: "Failed to lift ban.",
78
77
  mutationFn: (uid) => apiClient.post(ADMIN_ENDPOINTS.USER_UNBAN(uid), {}),
79
78
  onSuccess: () => {
80
79
  toast.showToast("Ban lifted.", "success");
81
80
  void queryClient.invalidateQueries({ queryKey: ["admin", "users", "listing"] });
82
81
  },
83
- onError: () => {
84
- toast.showToast("Failed to lift ban.", "error");
85
- },
86
82
  });
87
83
  const handleManageBulkClick = (selection) => {
88
84
  const rowId = selection.selectedIds[0];
@@ -8,7 +8,7 @@ import { apiClient } from "../../../http";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
9
  import { quickCreateTaxonomySchema } from "../schemas/small-forms";
10
10
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
11
- import { applyZodIssues } from "../../../ui/forms/FormShell";
11
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
12
12
  function toBrandSlug(str) {
13
13
  const base = str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
14
14
  return base.startsWith("brand-") ? base : `brand-${base}`;
@@ -19,6 +19,7 @@ export function BrandQuickCreateForm({ onSaved, onCancel }) {
19
19
  const [isActive, setIsActive] = React.useState(true);
20
20
  const { showToast } = useToast();
21
21
  const mutation = useApiMutation({
22
+ errorMessage: "Failed to create brand.",
22
23
  mutationFn: async () => apiClient.post(ADMIN_ENDPOINTS.BRANDS, {
23
24
  name,
24
25
  slug: toBrandSlug(name),
@@ -29,9 +30,6 @@ export function BrandQuickCreateForm({ onSaved, onCancel }) {
29
30
  const id = res?.data?.id ?? res?.id ?? "";
30
31
  onSaved(id, name);
31
32
  },
32
- onError: (err) => {
33
- showToast(err?.message ?? "Failed to create brand.", "error");
34
- },
35
33
  });
36
34
  return (_jsx(Form, { schema: quickCreateTaxonomySchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(Input, { label: "Brand name", value: name, onChange: (e) => setName(e.target.value), required: true, placeholder: "e.g. Hot Wheels", autoFocus: true }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Optional brief description" }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsxs(Row, { gap: "3", padding: "t-xs", children: [_jsx(Button, { type: "submit", isLoading: mutation.isPending, disabled: mutation.isPending, onClick: () => {
37
35
  clearErrors();
@@ -8,7 +8,7 @@ import { apiClient } from "../../../http";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
9
  import { quickCreateTaxonomySchema } from "../schemas/small-forms";
10
10
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
11
- import { applyZodIssues } from "../../../ui/forms/FormShell";
11
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
12
12
  function toCategorySlug(str) {
13
13
  return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
14
14
  }
@@ -18,6 +18,7 @@ export function CategoryQuickCreateForm({ onSaved, onCancel }) {
18
18
  const [isActive, setIsActive] = React.useState(true);
19
19
  const { showToast } = useToast();
20
20
  const mutation = useApiMutation({
21
+ errorMessage: "Failed to create category.",
21
22
  mutationFn: async () => apiClient.post(ADMIN_ENDPOINTS.CATEGORIES, {
22
23
  name,
23
24
  slug: toCategorySlug(name),
@@ -28,9 +29,6 @@ export function CategoryQuickCreateForm({ onSaved, onCancel }) {
28
29
  const id = res?.data?.id ?? res?.id ?? "";
29
30
  onSaved(id, name);
30
31
  },
31
- onError: (err) => {
32
- showToast(err?.message ?? "Failed to create category.", "error");
33
- },
34
32
  });
35
33
  return (_jsx(Form, { schema: quickCreateTaxonomySchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(Input, { label: "Category name", value: name, onChange: (e) => setName(e.target.value), required: true, placeholder: "e.g. Trading Cards", autoFocus: true }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Optional brief description" }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsxs(Row, { gap: "3", padding: "t-xs", children: [_jsx(Button, { type: "submit", isLoading: mutation.isPending, disabled: mutation.isPending, onClick: () => {
36
34
  clearErrors();
@@ -7,7 +7,7 @@ import { isAuthError } from "../../../utils/auth-error";
7
7
  import { Button, CountdownDisplay, Div, LoginRequiredModal, Modal, Row, Span, Stack, Text } from "../../../ui";
8
8
  import { Form } from "../../../ui/components/Form";
9
9
  import { FieldInput } from "../../../ui/forms/FieldInput";
10
- import { applyZodIssues } from "../../../ui/forms/FormShell";
10
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
11
11
  import { placeBidSchema } from "../schemas/bid-input";
12
12
  import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
13
13
  import { AUCTION_BUYOUT_WINDOW_MINUTES } from "../../../_internal/shared/checkout/lanes";
@@ -5,7 +5,7 @@ import { useState } from "react";
5
5
  import { Alert, Button, Div, Heading, Row, Stack, Text, useToast } from "../../../ui";
6
6
  import { Form } from "../../../ui/components/Form";
7
7
  import { FieldInput } from "../../../ui/forms/FieldInput";
8
- import { applyZodIssues } from "../../../ui/forms/FormShell";
8
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
9
9
  import { forgotPasswordSchema } from "../schemas";
10
10
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
11
11
  export function ForgotPasswordView({ onSubmit, isLoading = false, error, success, labels = {}, renderBackLink, className = "", }) {
@@ -6,7 +6,7 @@ import { Alert, Button, Div, Heading, Row, SiteMark, Stack, Text, useToast } fro
6
6
  import { Form } from "../../../ui/components/Form";
7
7
  import { FieldInput } from "../../../ui/forms/FieldInput";
8
8
  import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
9
- import { applyZodIssues } from "../../../ui/forms/FormShell";
9
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
10
10
  import { loginSchema } from "../schemas";
11
11
  import { SocialAuthButtons } from "./SocialAuthButtons";
12
12
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
@@ -7,7 +7,7 @@ import { Alert, Button, Div, Heading, Row, SiteMark, Stack, Text, useToast } fro
7
7
  import { Form } from "../../../ui/components/Form";
8
8
  import { FieldInput } from "../../../ui/forms/FieldInput";
9
9
  import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
10
- import { applyZodIssues } from "../../../ui/forms/FormShell";
10
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
11
11
  import { registerSchema, registerPasswordSchema } from "../schemas";
12
12
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
13
13
  const registerClientSchema = registerSchema.extend({
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
  import { Alert, Button, Div, Heading, Row, Stack, Text, useToast } from "../../../ui";
7
7
  import { Form } from "../../../ui/components/Form";
8
8
  import { FieldInput } from "../../../ui/forms/FieldInput";
9
- import { applyZodIssues } from "../../../ui/forms/FormShell";
9
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
10
10
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
11
11
  const resetClientSchema = z.object({
12
12
  password: z.string().min(6, "Password must be at least 6 characters"),
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Button, Div, Row, Span } from "../../../ui";
3
3
  export function SocialAuthButtons({ onGoogle, disabled, dividerLabel, googleLabel, }) {
4
- return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50", children: [_jsxs("svg", { className: "mr-2 h-5 w-5", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "currentColor", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "currentColor", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "currentColor", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })] }), googleLabel] }) })] }));
4
+ return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50", children: [_jsxs("svg", { className: "mr-2 h-5 w-5 shrink-0", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "currentColor", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "currentColor", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "currentColor", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })] }), googleLabel] }) })] }));
5
5
  }
@@ -57,8 +57,20 @@ export class SessionRepository extends BaseRepository {
57
57
  async updateActivity(sessionId, data) {
58
58
  const updateData = {
59
59
  lastActivity: new Date(),
60
- ...(data?.location && { location: data.location }),
61
60
  };
61
+ // Firestore `.update()` REPLACES a map field wholesale — it does not merge.
62
+ // Passing `location` straight through therefore destroyed whichever of
63
+ // `{ country, city }` the caller happened to omit, and `location.city` is
64
+ // PII-encrypted, so the loss is unrecoverable. Same footgun the profile
65
+ // route documents for `uiPreferences`.
66
+ //
67
+ // Latent rather than live: both current callers pass no `data` at all, so
68
+ // this branch never runs today. It costs one read, and only on the branch
69
+ // that supplies a location — the hot activity ping stays a single write.
70
+ if (data?.location) {
71
+ const current = await this.findById(sessionId); // mapDoc → already decrypted
72
+ updateData.location = { ...(current?.location ?? {}), ...data.location };
73
+ }
62
74
  try {
63
75
  await this.update(sessionId, updateData);
64
76
  }
@@ -11,10 +11,10 @@ export declare const createBlogPostSchema: z.ZodObject<{
11
11
  isFeatured: z.ZodDefault<z.ZodBoolean>;
12
12
  status: z.ZodDefault<z.ZodEnum<["draft", "published", "archived"]>>;
13
13
  coverImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<[z.ZodObject<{
14
- url: z.ZodString;
14
+ url: z.ZodEffects<z.ZodString, string, string>;
15
15
  type: z.ZodEnum<["image", "video", "file"]>;
16
16
  alt: z.ZodOptional<z.ZodString>;
17
- thumbnailUrl: z.ZodOptional<z.ZodString>;
17
+ thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
18
18
  }, "strip", z.ZodTypeAny, {
19
19
  type: "video" | "image" | "file";
20
20
  url: string;
@@ -25,15 +25,15 @@ export declare const createBlogPostSchema: z.ZodObject<{
25
25
  url: string;
26
26
  alt?: string | undefined;
27
27
  thumbnailUrl?: string | undefined;
28
- }>, z.ZodEffects<z.ZodString, {
28
+ }>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
29
29
  url: string;
30
30
  type: "image";
31
31
  }, string>]>>>;
32
32
  contentImages: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
33
- url: z.ZodString;
33
+ url: z.ZodEffects<z.ZodString, string, string>;
34
34
  type: z.ZodEnum<["image", "video", "file"]>;
35
35
  alt: z.ZodOptional<z.ZodString>;
36
- thumbnailUrl: z.ZodOptional<z.ZodString>;
36
+ thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
37
37
  }, "strip", z.ZodTypeAny, {
38
38
  type: "video" | "image" | "file";
39
39
  url: string;
@@ -46,10 +46,10 @@ export declare const createBlogPostSchema: z.ZodObject<{
46
46
  thumbnailUrl?: string | undefined;
47
47
  }>, "many">>>;
48
48
  additionalImages: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
49
- url: z.ZodString;
49
+ url: z.ZodEffects<z.ZodString, string, string>;
50
50
  type: z.ZodEnum<["image", "video", "file"]>;
51
51
  alt: z.ZodOptional<z.ZodString>;
52
- thumbnailUrl: z.ZodOptional<z.ZodString>;
52
+ thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
53
53
  }, "strip", z.ZodTypeAny, {
54
54
  type: "video" | "image" | "file";
55
55
  url: string;
@@ -150,10 +150,10 @@ export declare const updateBlogPostSchema: z.ZodObject<{
150
150
  isFeatured: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
151
151
  status: z.ZodOptional<z.ZodDefault<z.ZodEnum<["draft", "published", "archived"]>>>;
152
152
  coverImage: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<[z.ZodObject<{
153
- url: z.ZodString;
153
+ url: z.ZodEffects<z.ZodString, string, string>;
154
154
  type: z.ZodEnum<["image", "video", "file"]>;
155
155
  alt: z.ZodOptional<z.ZodString>;
156
- thumbnailUrl: z.ZodOptional<z.ZodString>;
156
+ thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
157
157
  }, "strip", z.ZodTypeAny, {
158
158
  type: "video" | "image" | "file";
159
159
  url: string;
@@ -164,15 +164,15 @@ export declare const updateBlogPostSchema: z.ZodObject<{
164
164
  url: string;
165
165
  alt?: string | undefined;
166
166
  thumbnailUrl?: string | undefined;
167
- }>, z.ZodEffects<z.ZodString, {
167
+ }>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
168
168
  url: string;
169
169
  type: "image";
170
170
  }, string>]>>>>;
171
171
  contentImages: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
172
- url: z.ZodString;
172
+ url: z.ZodEffects<z.ZodString, string, string>;
173
173
  type: z.ZodEnum<["image", "video", "file"]>;
174
174
  alt: z.ZodOptional<z.ZodString>;
175
- thumbnailUrl: z.ZodOptional<z.ZodString>;
175
+ thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
176
176
  }, "strip", z.ZodTypeAny, {
177
177
  type: "video" | "image" | "file";
178
178
  url: string;
@@ -185,10 +185,10 @@ export declare const updateBlogPostSchema: z.ZodObject<{
185
185
  thumbnailUrl?: string | undefined;
186
186
  }>, "many">>>>;
187
187
  additionalImages: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
188
- url: z.ZodString;
188
+ url: z.ZodEffects<z.ZodString, string, string>;
189
189
  type: z.ZodEnum<["image", "video", "file"]>;
190
190
  alt: z.ZodOptional<z.ZodString>;
191
- thumbnailUrl: z.ZodOptional<z.ZodString>;
191
+ thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
192
192
  }, "strip", z.ZodTypeAny, {
193
193
  type: "video" | "image" | "file";
194
194
  url: string;
@@ -1,5 +1,6 @@
1
1
  import { sortBy } from "@mohasinac/appkit";
2
2
  import { z } from "zod";
3
+ import { mediaUrlSchema } from "../../../validation/schemas";
3
4
  import { serverLogger } from "../../../monitoring";
4
5
  import { blogRepository } from "../repository/blog.repository";
5
6
  import { coerceMediaField, getMediaUrl } from "../../../utils";
@@ -8,23 +9,20 @@ import { finalizeStagedMediaField, finalizeStagedMediaObject, finalizeStagedMedi
8
9
  const singleImageMediaSchema = z
9
10
  .union([
10
11
  z.object({
11
- url: z.string().url(),
12
+ url: mediaUrlSchema,
12
13
  type: z.enum(["image", "video", "file"]),
13
14
  alt: z.string().optional(),
14
- thumbnailUrl: z.string().url().optional(),
15
+ thumbnailUrl: mediaUrlSchema.optional(),
15
16
  }),
16
- z
17
- .string()
18
- .url()
19
- .transform((url) => ({ url, type: "image" })),
17
+ mediaUrlSchema.transform((url) => ({ url, type: "image" })),
20
18
  ])
21
19
  .nullable()
22
20
  .optional();
23
21
  const mediaFieldSchema = z.object({
24
- url: z.string().url(),
22
+ url: mediaUrlSchema,
25
23
  type: z.enum(["image", "video", "file"]),
26
24
  alt: z.string().optional(),
27
- thumbnailUrl: z.string().url().optional(),
25
+ thumbnailUrl: mediaUrlSchema.optional(),
28
26
  });
29
27
  export const createBlogPostSchema = z.object({
30
28
  title: z.string().min(1),
@@ -267,7 +267,7 @@ export declare const blogPostSchema: z.ZodObject<{
267
267
  thumbnailUrl?: string | undefined;
268
268
  youtubeId?: string | undefined;
269
269
  duration?: number | undefined;
270
- }>, z.ZodEffects<z.ZodString, {
270
+ }>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
271
271
  url: string;
272
272
  type: "image";
273
273
  }, string>]>>>, {
@@ -1,5 +1,6 @@
1
1
  export * from "./firestore";
2
2
  import { z } from "zod";
3
+ import { mediaUrlSchema } from "../../../validation/schemas";
3
4
  import { mediaFieldSchema } from "../../media/types/index";
4
5
  import { auditTimestampsShape, firestoreDateSchema } from "../../../schemas/firestore-helpers";
5
6
  // ─── Firestore document schema (W2) ───────────────────────────────────────────
@@ -31,10 +32,7 @@ export const blogPostFirestoreSchema = z.object({
31
32
  const blogCoverImageSchema = z
32
33
  .union([
33
34
  mediaFieldSchema,
34
- z
35
- .string()
36
- .url()
37
- .transform((url) => ({ url, type: "image" })),
35
+ mediaUrlSchema.transform((url) => ({ url, type: "image" })),
38
36
  ])
39
37
  .nullable()
40
38
  .optional()
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { NextResponse } from "next/server.js";
12
12
  import { z } from "zod";
13
+ import { mediaUrlSchema } from "../../../../validation/schemas";
13
14
  import { getProviders } from "../../../../contracts";
14
15
  import { createRouteHandler } from "../../../../next";
15
16
  import { normalizeError } from "../../../../errors/normalize";
@@ -48,7 +49,7 @@ const categoryUpdateSchema = z
48
49
  tier: z.number().int().min(0).optional(),
49
50
  order: z.number().int().min(0).optional(),
50
51
  description: z.string().optional(),
51
- imageUrl: z.string().url().optional(),
52
+ imageUrl: mediaUrlSchema.optional(),
52
53
  isFeatured: z.boolean().optional(),
53
54
  showOnHomepage: z.boolean().optional(),
54
55
  isActive: z.boolean().optional(),
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { NextResponse } from "next/server.js";
16
16
  import { z } from "zod";
17
+ import { mediaUrlSchema } from "../../../validation/schemas";
17
18
  import { getProviders } from "../../../contracts";
18
19
  import { createRouteHandler } from "../../../next";
19
20
  import { normalizeError } from "../../../errors/normalize";
@@ -177,7 +178,7 @@ const categoryCreateSchema = z
177
178
  tier: z.number().int().min(0).optional(),
178
179
  order: z.number().int().min(0).optional(),
179
180
  description: z.string().optional(),
180
- imageUrl: z.string().url().optional(),
181
+ imageUrl: mediaUrlSchema.optional(),
181
182
  isFeatured: z.boolean().optional(),
182
183
  showOnHomepage: z.boolean().optional(),
183
184
  })
@@ -467,7 +467,7 @@ export declare const eventItemSchema: z.ZodObject<{
467
467
  thumbnailUrl?: string | undefined;
468
468
  youtubeId?: string | undefined;
469
469
  duration?: number | undefined;
470
- }>, z.ZodEffects<z.ZodString, {
470
+ }>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
471
471
  url: string;
472
472
  type: "image";
473
473
  }, string>]>>>, {
@@ -1,13 +1,11 @@
1
1
  export * from "./firestore";
2
2
  import { z } from "zod";
3
+ import { mediaUrlSchema } from "../../../validation/schemas";
3
4
  import { mediaFieldSchema } from "../../media/types/index";
4
5
  const eventCoverImageSchema = z
5
6
  .union([
6
7
  mediaFieldSchema,
7
- z
8
- .string()
9
- .url()
10
- .transform((url) => ({ url, type: "image" })),
8
+ mediaUrlSchema.transform((url) => ({ url, type: "image" })),
11
9
  ])
12
10
  .nullable()
13
11
  .optional()