@mohasinac/appkit 4.29.1 → 4.30.1

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 (165) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
  2. package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
  3. package/dist/_internal/shared/features/blog/schema.js +2 -1
  4. package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
  5. package/dist/_internal/shared/features/brands/schema.js +3 -1
  6. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
  7. package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
  8. package/dist/_internal/shared/features/events/schema.d.ts +2 -2
  9. package/dist/_internal/shared/features/events/schema.js +2 -1
  10. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  11. package/dist/_internal/shared/features/products/schema.js +3 -2
  12. package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
  13. package/dist/_internal/shared/features/reviews/schema.js +2 -1
  14. package/dist/_internal/shared/types/action-result.d.ts +0 -2
  15. package/dist/client/api/surface-error.d.ts +9 -0
  16. package/dist/client/api/surface-error.js +29 -3
  17. package/dist/client/api/useApiMutation.d.ts +10 -0
  18. package/dist/client/api/useApiMutation.js +2 -1
  19. package/dist/core/server-action.d.ts +3 -14
  20. package/dist/core/server-action.js +11 -2
  21. package/dist/errors/error-envelope.d.ts +73 -0
  22. package/dist/errors/error-envelope.js +96 -0
  23. package/dist/errors/error-handler.js +14 -7
  24. package/dist/features/account/components/NotificationBell.js +1 -1
  25. package/dist/features/account/components/UserSidebar.js +2 -2
  26. package/dist/features/account/components/UserSupportView.js +2 -6
  27. package/dist/features/account/hooks/useProfile.d.ts +6 -0
  28. package/dist/features/account/hooks/useProfile.js +1 -0
  29. package/dist/features/admin/components/AdminAdEditorView.js +1 -3
  30. package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
  31. package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
  32. package/dist/features/admin/components/AdminAddressesView.js +1 -3
  33. package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
  34. package/dist/features/admin/components/AdminBidsView.js +1 -3
  35. package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
  36. package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
  37. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
  38. package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
  39. package/dist/features/admin/components/AdminContactEditorView.js +1 -3
  40. package/dist/features/admin/components/AdminContactView.js +2 -6
  41. package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
  42. package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
  43. package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
  44. package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
  45. package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
  46. package/dist/features/admin/components/AdminNavEditorView.js +1 -3
  47. package/dist/features/admin/components/AdminNavigationView.js +3 -3
  48. package/dist/features/admin/components/AdminNewsletterView.js +1 -3
  49. package/dist/features/admin/components/AdminNotificationsView.js +2 -6
  50. package/dist/features/admin/components/AdminOffersView.js +1 -3
  51. package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
  52. package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
  53. package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
  54. package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
  55. package/dist/features/admin/components/AdminProductEditorView.js +2 -4
  56. package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
  57. package/dist/features/admin/components/AdminReviewsView.js +2 -6
  58. package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
  59. package/dist/features/admin/components/AdminSectionsView.js +2 -6
  60. package/dist/features/admin/components/AdminSessionsView.js +1 -3
  61. package/dist/features/admin/components/AdminSidebar.js +2 -2
  62. package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
  63. package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
  64. package/dist/features/admin/components/AdminStoresView.js +2 -6
  65. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
  66. package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
  67. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
  68. package/dist/features/admin/components/AdminUserEditorView.js +6 -18
  69. package/dist/features/admin/components/AdminUsersView.js +2 -6
  70. package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
  71. package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
  72. package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
  73. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  74. package/dist/features/auth/components/LoginForm.js +1 -1
  75. package/dist/features/auth/components/RegisterForm.js +1 -1
  76. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  77. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  78. package/dist/features/auth/repository/session.repository.js +13 -1
  79. package/dist/features/auth/repository/token.repository.d.ts +25 -2
  80. package/dist/features/auth/repository/token.repository.js +26 -18
  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 -4
  120. package/dist/index.js +4 -7
  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 +48 -4
  131. package/dist/security/browser.d.ts +1 -1
  132. package/dist/security/browser.js +1 -1
  133. package/dist/security/index.d.ts +2 -2
  134. package/dist/security/index.js +2 -2
  135. package/dist/security/pii-encrypt.d.ts +12 -10
  136. package/dist/security/pii-encrypt.js +91 -29
  137. package/dist/security/pii-mask.d.ts +39 -1
  138. package/dist/security/pii-mask.js +33 -4
  139. package/dist/security/pii-redact.d.ts +0 -13
  140. package/dist/security/pii-redact.js +3 -1
  141. package/dist/security/pii-schemas.d.ts +0 -6
  142. package/dist/security/pii-schemas.js +29 -6
  143. package/dist/security/settings-encryption.js +31 -3
  144. package/dist/seed/test-utils.js +6 -2
  145. package/dist/server.d.ts +1 -1
  146. package/dist/server.js +1 -1
  147. package/dist/ui/DataTable.js +2 -2
  148. package/dist/ui/components/CollapsibleSection.js +1 -1
  149. package/dist/ui/components/ImageGallery.js +1 -1
  150. package/dist/ui/components/ListingLayout.js +1 -1
  151. package/dist/ui/forms/FormShell.d.ts +0 -19
  152. package/dist/ui/forms/FormShell.js +4 -23
  153. package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
  154. package/dist/ui/forms/apply-zod-issues.js +38 -0
  155. package/dist/ui/forms/index.d.ts +2 -1
  156. package/dist/ui/forms/index.js +2 -1
  157. package/dist/ui/icons/icon-registry.d.ts +8 -0
  158. package/dist/ui/icons/icon-registry.js +13 -1
  159. package/dist/utils/action-response.d.ts +1 -1
  160. package/dist/utils/media-url.d.ts +49 -0
  161. package/dist/utils/media-url.js +72 -1
  162. package/dist/validation/schemas.d.ts +15 -2
  163. package/dist/validation/schemas.js +18 -22
  164. package/package.json +1 -1
  165. package/scripts/seed-cli.mjs +21 -6
@@ -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
  }
@@ -3,8 +3,23 @@ import { BaseRepository } from "../../../providers/db-firebase";
3
3
  import { type EmailVerificationTokenDocument, type PasswordResetTokenDocument } from "../schemas";
4
4
  export declare class EmailVerificationTokenRepository extends BaseRepository<EmailVerificationTokenDocument> {
5
5
  constructor();
6
+ /**
7
+ * Declared, not hand-rolled in a `create` override.
8
+ *
9
+ * The override this replaces computed `encryptPiiFields(...)` and then
10
+ * OVERWROTE the result with `addPiiIndices(data, MAP)` — which returns
11
+ * `{...plaintext, ...indices}` — so the ciphertext was discarded and every
12
+ * verification token's email was written to Firestore in CLEARTEXT. It was
13
+ * invisible because `mapDoc` decrypts on read and `decryptPiiFields` passes
14
+ * plaintext straight through, so reads looked identical either way.
15
+ *
16
+ * `applyWriteHooks` covers create/createWithId/update and the `*InTx` /
17
+ * `*InBatch` family, so no write path can skip encryption the way an
18
+ * override of `create` alone did.
19
+ */
20
+ protected piiFields: readonly ["email"];
21
+ protected piiIndexMap: Record<string, string>;
6
22
  protected mapDoc<D = EmailVerificationTokenDocument>(snap: DocumentSnapshot): D;
7
- create(data: Partial<EmailVerificationTokenDocument>): Promise<EmailVerificationTokenDocument>;
8
23
  findByToken(token: string): Promise<EmailVerificationTokenDocument | null>;
9
24
  findByUserId(userId: string): Promise<EmailVerificationTokenDocument[]>;
10
25
  findByEmail(email: string): Promise<EmailVerificationTokenDocument[]>;
@@ -15,8 +30,16 @@ export declare class EmailVerificationTokenRepository extends BaseRepository<Ema
15
30
  }
16
31
  export declare class PasswordResetTokenRepository extends BaseRepository<PasswordResetTokenDocument> {
17
32
  constructor();
33
+ /**
34
+ * Same defect as its sibling above, one spread further along: the override
35
+ * this replaces built `{...encrypted, ...addPiiIndices(data, MAP)}`, and
36
+ * because `addPiiIndices` returns `{...plaintext, ...indices}` the plaintext
37
+ * `email` was spread back OVER the ciphertext. Reset-token emails were
38
+ * written in cleartext too.
39
+ */
40
+ protected piiFields: readonly ["email"];
41
+ protected piiIndexMap: Record<string, string>;
18
42
  protected mapDoc<D = PasswordResetTokenDocument>(snap: DocumentSnapshot): D;
19
- create(data: Partial<PasswordResetTokenDocument>): Promise<PasswordResetTokenDocument>;
20
43
  findByToken(token: string): Promise<PasswordResetTokenDocument | null>;
21
44
  findByUserId(userId: string): Promise<PasswordResetTokenDocument[]>;
22
45
  findByEmail(email: string): Promise<PasswordResetTokenDocument[]>;
@@ -1,12 +1,28 @@
1
1
  import { normalizeError } from "../../../errors/normalize";
2
2
  import { DatabaseError } from "../../../errors";
3
3
  import { BaseRepository } from "../../../providers/db-firebase";
4
- import { TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, addPiiIndices, decryptPiiFields, encryptPiiFields, piiBlindIndex, } from "../../../security";
4
+ import { TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, decryptPiiFields, piiBlindIndex, } from "../../../security";
5
5
  import { resolveDate } from "../../../utils";
6
6
  import { EMAIL_VERIFICATION_COLLECTION, PASSWORD_RESET_COLLECTION, TOKEN_FIELDS, } from "../schemas";
7
7
  export class EmailVerificationTokenRepository extends BaseRepository {
8
8
  constructor() {
9
9
  super(EMAIL_VERIFICATION_COLLECTION);
10
+ /**
11
+ * Declared, not hand-rolled in a `create` override.
12
+ *
13
+ * The override this replaces computed `encryptPiiFields(...)` and then
14
+ * OVERWROTE the result with `addPiiIndices(data, MAP)` — which returns
15
+ * `{...plaintext, ...indices}` — so the ciphertext was discarded and every
16
+ * verification token's email was written to Firestore in CLEARTEXT. It was
17
+ * invisible because `mapDoc` decrypts on read and `decryptPiiFields` passes
18
+ * plaintext straight through, so reads looked identical either way.
19
+ *
20
+ * `applyWriteHooks` covers create/createWithId/update and the `*InTx` /
21
+ * `*InBatch` family, so no write path can skip encryption the way an
22
+ * override of `create` alone did.
23
+ */
24
+ this.piiFields = TOKEN_PII_FIELDS;
25
+ this.piiIndexMap = TOKEN_PII_INDEX_MAP;
10
26
  }
11
27
  mapDoc(snap) {
12
28
  const raw = super.mapDoc(snap);
@@ -14,15 +30,6 @@ export class EmailVerificationTokenRepository extends BaseRepository {
14
30
  ...TOKEN_PII_FIELDS,
15
31
  ]);
16
32
  }
17
- async create(data) {
18
- let encrypted = encryptPiiFields(data, [...TOKEN_PII_FIELDS]);
19
- encrypted = addPiiIndices(data, TOKEN_PII_INDEX_MAP);
20
- const merged = {
21
- ...encrypted,
22
- ...addPiiIndices(data, TOKEN_PII_INDEX_MAP),
23
- };
24
- return super.create(merged);
25
- }
26
33
  async findByToken(token) {
27
34
  return this.findOneBy(TOKEN_FIELDS.TOKEN, token);
28
35
  }
@@ -80,6 +87,15 @@ export class EmailVerificationTokenRepository extends BaseRepository {
80
87
  export class PasswordResetTokenRepository extends BaseRepository {
81
88
  constructor() {
82
89
  super(PASSWORD_RESET_COLLECTION);
90
+ /**
91
+ * Same defect as its sibling above, one spread further along: the override
92
+ * this replaces built `{...encrypted, ...addPiiIndices(data, MAP)}`, and
93
+ * because `addPiiIndices` returns `{...plaintext, ...indices}` the plaintext
94
+ * `email` was spread back OVER the ciphertext. Reset-token emails were
95
+ * written in cleartext too.
96
+ */
97
+ this.piiFields = TOKEN_PII_FIELDS;
98
+ this.piiIndexMap = TOKEN_PII_INDEX_MAP;
83
99
  }
84
100
  mapDoc(snap) {
85
101
  const raw = super.mapDoc(snap);
@@ -87,14 +103,6 @@ export class PasswordResetTokenRepository extends BaseRepository {
87
103
  ...TOKEN_PII_FIELDS,
88
104
  ]);
89
105
  }
90
- async create(data) {
91
- let encrypted = encryptPiiFields(data, [...TOKEN_PII_FIELDS]);
92
- encrypted = {
93
- ...encrypted,
94
- ...addPiiIndices(data, TOKEN_PII_INDEX_MAP),
95
- };
96
- return super.create(encrypted);
97
- }
98
106
  async findByToken(token) {
99
107
  return this.findOneBy(TOKEN_FIELDS.TOKEN, token);
100
108
  }
@@ -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()
@@ -6,5 +6,5 @@ const __P = {
6
6
  const CLS_CONTACT_CARD = "flex items-center gap-[var(--appkit-space-3)] rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-elevated)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] hover:border-primary/40 transition-colors";
7
7
  const CLS_CONTACT_LABEL = "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]";
8
8
  export function ContactCTA({ email, phone, contactHref, labels, }) {
9
- return (_jsxs(Card, { variant: "outlined", padding: "lg", className: "text-center", children: [_jsx(Row, { className: "mb-6", justify: "center", children: _jsx(Div, { className: `bg-primary/10 ${__P.p8}`, rounded: "full", children: _jsx("svg", { className: "h-12 w-12 text-primary", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }) }) }), _jsx(Heading, { level: 2, className: "mb-3", size: "2xl", weight: "semibold", children: labels?.title ?? "Need More Help?" }), _jsx(Text, { className: "mx-auto mb-6 max-w-2xl", color: "muted", children: labels?.description ?? "Our support team is here to help you." }), _jsxs(Div, { layout: "grid", gap: "4", className: "mb-8 grid-cols-1 md:grid-cols-3", children: [_jsxs(TextLink, { href: `mailto:${email}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.emailUs ?? "Email Us" }), _jsx(Text, { size: "xs", color: "muted", children: email })] }), _jsxs(TextLink, { href: `tel:${phone}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.callUs ?? "Call Us" }), _jsx(Text, { size: "xs", color: "muted", children: phone })] }), _jsxs(TextLink, { href: contactHref, className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.contactForm ?? "Contact Form" }), _jsx(Text, { size: "xs", color: "muted", children: labels?.submitRequest ?? "Submit a request" })] })] }), _jsxs(TextLink, { rounded: "xl", href: contactHref, layout: "inline-flex", align: "center", gap: "sm", className: "bg-primary p-[1.5rem] text-white transition-colors hover:bg-primary/90", weight: "medium", children: [_jsx(Span, { children: labels?.contactTeam ?? "Contact Team" }), _jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] })] }));
9
+ return (_jsxs(Card, { variant: "outlined", padding: "lg", className: "text-center", children: [_jsx(Row, { className: "mb-6", justify: "center", children: _jsx(Div, { className: `bg-primary/10 ${__P.p8}`, rounded: "full", children: _jsx("svg", { className: "h-12 w-12 text-primary", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }) }) }), _jsx(Heading, { level: 2, className: "mb-3", size: "2xl", weight: "semibold", children: labels?.title ?? "Need More Help?" }), _jsx(Text, { className: "mx-auto mb-6 max-w-2xl", color: "muted", children: labels?.description ?? "Our support team is here to help you." }), _jsxs(Div, { layout: "grid", gap: "4", className: "mb-8 grid-cols-1 md:grid-cols-3", children: [_jsxs(TextLink, { href: `mailto:${email}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.emailUs ?? "Email Us" }), _jsx(Text, { size: "xs", color: "muted", children: email })] }), _jsxs(TextLink, { href: `tel:${phone}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.callUs ?? "Call Us" }), _jsx(Text, { size: "xs", color: "muted", children: phone })] }), _jsxs(TextLink, { href: contactHref, className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.contactForm ?? "Contact Form" }), _jsx(Text, { size: "xs", color: "muted", children: labels?.submitRequest ?? "Submit a request" })] })] }), _jsxs(TextLink, { rounded: "xl", href: contactHref, layout: "inline-flex", align: "center", gap: "sm", className: "bg-primary p-[1.5rem] text-white transition-colors hover:bg-primary/90", weight: "medium", children: [_jsx(Span, { children: labels?.contactTeam ?? "Contact Team" }), _jsx("svg", { className: "h-5 w-5 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] })] }));
10
10
  }