@mohasinac/appkit 3.6.5 → 3.8.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 (298) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +17 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
  3. package/dist/_internal/client/features/tour/TourProvider.d.ts +5 -2
  4. package/dist/_internal/client/features/tour/TourProvider.js +125 -12
  5. package/dist/_internal/client/theme/ThemeProvider.js +1 -1
  6. package/dist/_internal/server/features/categories/data.js +2 -2
  7. package/dist/_internal/server/features/faqs/og.js +5 -1
  8. package/dist/_internal/server/features/reviews/og.js +5 -1
  9. package/dist/_internal/server/features/seo/manifest.js +2 -1
  10. package/dist/_internal/server/features/seo/og.js +7 -1
  11. package/dist/_internal/server/features/tester/visibility.d.ts +13 -0
  12. package/dist/_internal/server/features/tester/visibility.js +28 -0
  13. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  14. package/dist/_internal/server/functions/scheduled.js +9 -1
  15. package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
  16. package/dist/_internal/server/jobs/core/resetOtpVerification.d.ts +17 -0
  17. package/dist/_internal/server/jobs/core/resetOtpVerification.js +88 -0
  18. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +6 -0
  19. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +35 -0
  20. package/dist/_internal/server/jobs/handlers/_helpers.d.ts +6 -0
  21. package/dist/_internal/server/jobs/handlers/_helpers.js +12 -0
  22. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  23. package/dist/_internal/server/jobs/handlers/index.js +2 -0
  24. package/dist/_internal/server/jobs/handlers/messages.d.ts +2 -0
  25. package/dist/_internal/server/jobs/handlers/messages.js +2 -0
  26. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +2 -0
  27. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +2 -0
  28. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  29. package/dist/_internal/shared/actions/action-registry.js +56 -0
  30. package/dist/_internal/shared/tokens/index.d.ts +12 -12
  31. package/dist/client.d.ts +15 -3
  32. package/dist/client.js +10 -2
  33. package/dist/constants/api-endpoints.d.ts +39 -12
  34. package/dist/constants/api-endpoints.js +17 -8
  35. package/dist/constants/field-names.d.ts +9 -0
  36. package/dist/constants/field-names.js +12 -0
  37. package/dist/constants/index.d.ts +1 -1
  38. package/dist/constants/index.js +1 -1
  39. package/dist/features/account/components/LinkedAccountsSection.d.ts +11 -0
  40. package/dist/features/account/components/LinkedAccountsSection.js +11 -0
  41. package/dist/features/account/components/UserSidebar.d.ts +3 -1
  42. package/dist/features/account/components/UserSidebar.js +9 -13
  43. package/dist/features/account/hooks/useCollapsedSections.d.ts +20 -0
  44. package/dist/features/account/hooks/useCollapsedSections.js +63 -0
  45. package/dist/features/account/hooks/useProfile.d.ts +8 -0
  46. package/dist/features/account/index.d.ts +1 -0
  47. package/dist/features/account/index.js +1 -0
  48. package/dist/features/admin/components/AdminAddressEditorView.js +2 -2
  49. package/dist/features/admin/components/AdminBidsView.js +6 -6
  50. package/dist/features/admin/components/AdminBrandsView.js +2 -2
  51. package/dist/features/admin/components/AdminCarouselView.js +2 -2
  52. package/dist/features/admin/components/AdminCategoryEditorView.js +2 -2
  53. package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
  54. package/dist/features/admin/components/AdminProductEditorView.js +5 -2
  55. package/dist/features/admin/components/AdminReviewsView.js +5 -5
  56. package/dist/features/admin/components/AdminSectionsView.js +3 -18
  57. package/dist/features/admin/components/AdminSidebar.js +8 -6
  58. package/dist/features/admin/components/AdminSiteSettingsView.js +133 -127
  59. package/dist/features/admin/components/AdminSublistingCategoriesView.js +1 -1
  60. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.d.ts +9 -0
  61. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +129 -0
  62. package/dist/features/admin/components/AdminTesterChecklistView.d.ts +6 -0
  63. package/dist/features/admin/components/AdminTesterChecklistView.js +93 -0
  64. package/dist/features/admin/components/AdminUserEditorView.d.ts +3 -1
  65. package/dist/features/admin/components/AdminUserEditorView.js +7 -1
  66. package/dist/features/admin/components/AdminUsersView.js +1 -1
  67. package/dist/features/admin/components/DataListingView.js +2 -2
  68. package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +8 -0
  69. package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +41 -0
  70. package/dist/features/admin/components/index.d.ts +6 -0
  71. package/dist/features/admin/components/index.js +3 -0
  72. package/dist/features/admin/schemas/firestore.d.ts +0 -7
  73. package/dist/features/admin/schemas/firestore.js +6 -6
  74. package/dist/features/analytics/components/PageViewTracker.d.ts +12 -0
  75. package/dist/features/analytics/components/PageViewTracker.js +22 -0
  76. package/dist/features/analytics/repository/page-views.repository.d.ts +32 -0
  77. package/dist/features/analytics/repository/page-views.repository.js +43 -0
  78. package/dist/features/analytics/types.d.ts +6 -0
  79. package/dist/features/analytics/types.js +13 -0
  80. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  81. package/dist/features/auth/hooks/useAuth.d.ts +12 -0
  82. package/dist/features/auth/hooks/useAuth.js +109 -0
  83. package/dist/features/auth/permissions/constants.d.ts +1 -1
  84. package/dist/features/auth/repository/session.repository.d.ts +2 -0
  85. package/dist/features/auth/repository/session.repository.js +4 -1
  86. package/dist/features/auth/schemas/firestore.d.ts +17 -1
  87. package/dist/features/auth/schemas/firestore.js +2 -0
  88. package/dist/features/blog/components/BlogIndexListing.js +2 -2
  89. package/dist/features/blog/schemas/firestore.d.ts +4 -0
  90. package/dist/features/cart/actions/cart-actions.js +20 -2
  91. package/dist/features/categories/api/route.js +2 -2
  92. package/dist/features/categories/components/CategoriesIndexListing.js +3 -3
  93. package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
  94. package/dist/features/categories/components/CategoryDetailPageView.js +3 -1
  95. package/dist/features/categories/components/CategoryProductsListing.js +2 -2
  96. package/dist/features/categories/schemas/firestore.d.ts +4 -0
  97. package/dist/features/classified/components/ClassifiedIndexListing.js +2 -2
  98. package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +2 -2
  99. package/dist/features/email/primitives.d.ts +2 -2
  100. package/dist/features/email/primitives.js +2 -2
  101. package/dist/features/events/components/EventsIndexListing.js +2 -2
  102. package/dist/features/events/repository/event-entry.repository.js +1 -0
  103. package/dist/features/events/schemas/firestore.d.ts +7 -1
  104. package/dist/features/events/schemas/firestore.js +1 -0
  105. package/dist/features/layout/BackToTop.d.ts +6 -1
  106. package/dist/features/layout/BackToTop.js +31 -6
  107. package/dist/features/layout/BottomNavbar.js +1 -1
  108. package/dist/features/layout/NavItem.js +1 -1
  109. package/dist/features/layout/NavbarLayout.js +2 -2
  110. package/dist/features/live/components/LiveItemsIndexListing.js +2 -2
  111. package/dist/features/media/MediaVideo.js +5 -1
  112. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -2
  113. package/dist/features/products/components/AuctionsIndexListing.js +2 -2
  114. package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -2
  115. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  116. package/dist/features/products/constants/action-defs.d.ts +2 -0
  117. package/dist/features/products/constants/action-defs.js +2 -0
  118. package/dist/features/products/constants/sieve.d.ts +0 -6
  119. package/dist/features/products/constants/sieve.js +4 -2
  120. package/dist/features/products/repository/products.repository.d.ts +20 -0
  121. package/dist/features/products/repository/products.repository.js +5 -0
  122. package/dist/features/products/schemas/firestore.d.ts +4 -0
  123. package/dist/features/promotions/components/CouponsIndexListing.js +18 -5
  124. package/dist/features/reviews/components/ReviewsIndexListing.js +2 -2
  125. package/dist/features/search/hooks/useNavSuggestions.js +20 -1
  126. package/dist/features/seller/actions/seller-actions.js +11 -6
  127. package/dist/features/seller/components/SellerAddressesView.js +2 -2
  128. package/dist/features/seller/components/SellerBidsView.js +2 -2
  129. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  130. package/dist/features/seller/components/SellerOrdersView.js +2 -2
  131. package/dist/features/seller/components/SellerProductsView.js +2 -2
  132. package/dist/features/seller/components/SellerSidebar.d.ts +3 -1
  133. package/dist/features/seller/components/SellerSidebar.js +9 -8
  134. package/dist/features/seller/components/SellerStoreCategoriesView.js +2 -2
  135. package/dist/features/seller/components/SellerTemplatesView.js +2 -2
  136. package/dist/features/shell/StepForm.js +4 -4
  137. package/dist/features/stores/actions/store-query-actions.d.ts +5 -4
  138. package/dist/features/stores/actions/store-query-actions.js +11 -8
  139. package/dist/features/stores/components/StoreAuctionsListing.js +2 -2
  140. package/dist/features/stores/components/StoreClassifiedsListing.js +2 -2
  141. package/dist/features/stores/components/StoreDetailLayoutView.js +1 -1
  142. package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -2
  143. package/dist/features/stores/components/StoreLiveItemsListing.js +2 -2
  144. package/dist/features/stores/components/StorePreOrdersListing.js +2 -2
  145. package/dist/features/stores/components/StoreProductsListing.js +2 -2
  146. package/dist/features/stores/components/StoreReviewsListing.js +2 -2
  147. package/dist/features/stores/components/StoresIndexListing.js +2 -2
  148. package/dist/features/stores/repository/store.repository.js +3 -0
  149. package/dist/features/stores/schemas/firestore.d.ts +4 -0
  150. package/dist/features/tester/actions/checklist-item-actions.d.ts +69 -0
  151. package/dist/features/tester/actions/checklist-item-actions.js +42 -0
  152. package/dist/features/tester/actions/index.d.ts +1 -0
  153. package/dist/features/tester/actions/index.js +1 -0
  154. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.d.ts +2 -0
  155. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +22 -0
  156. package/dist/features/tester/components/AdminTesterFeedbackListView.d.ts +3 -0
  157. package/dist/features/tester/components/AdminTesterFeedbackListView.js +100 -0
  158. package/dist/features/tester/components/AdminTesterFeedbackReportView.d.ts +2 -0
  159. package/dist/features/tester/components/AdminTesterFeedbackReportView.js +30 -0
  160. package/dist/features/tester/components/AdminTesterFeedbackView.d.ts +2 -0
  161. package/dist/features/tester/components/AdminTesterFeedbackView.js +29 -0
  162. package/dist/features/tester/components/TesterChecklistStepRow.d.ts +18 -0
  163. package/dist/features/tester/components/TesterChecklistStepRow.js +29 -0
  164. package/dist/features/tester/components/TesterFeedbackChart.d.ts +14 -0
  165. package/dist/features/tester/components/TesterFeedbackChart.js +18 -0
  166. package/dist/features/tester/components/TesterHubView.d.ts +5 -0
  167. package/dist/features/tester/components/TesterHubView.js +71 -0
  168. package/dist/features/tester/components/index.d.ts +11 -0
  169. package/dist/features/tester/components/index.js +7 -0
  170. package/dist/features/tester/index.d.ts +2 -0
  171. package/dist/features/tester/index.js +2 -0
  172. package/dist/features/tester/repository/index.d.ts +2 -0
  173. package/dist/features/tester/repository/index.js +2 -0
  174. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +13 -0
  175. package/dist/features/tester/repository/tester-checklist-item.repository.js +55 -0
  176. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +50 -0
  177. package/dist/features/tester/repository/tester-checklist-response.repository.js +196 -0
  178. package/dist/features/tester/schemas/firestore.d.ts +85 -0
  179. package/dist/features/tester/schemas/firestore.js +93 -0
  180. package/dist/features/tester/schemas/index.d.ts +1 -0
  181. package/dist/features/tester/schemas/index.js +1 -0
  182. package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +2 -0
  183. package/dist/features/tester/seed-data/blog-tester-seed-data.js +40 -0
  184. package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +2 -0
  185. package/dist/features/tester/seed-data/categories-tester-seed-data.js +177 -0
  186. package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +2 -0
  187. package/dist/features/tester/seed-data/events-tester-seed-data.js +50 -0
  188. package/dist/features/tester/seed-data/index.d.ts +6 -0
  189. package/dist/features/tester/seed-data/index.js +6 -0
  190. package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +2 -0
  191. package/dist/features/tester/seed-data/products-tester-seed-data.js +151 -0
  192. package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +2 -0
  193. package/dist/features/tester/seed-data/stores-tester-seed-data.js +40 -0
  194. package/dist/features/tester/seed-data/tester-checklist-seed-data.d.ts +2 -0
  195. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +248 -0
  196. package/dist/features/tester/seed-data/tester-ttl.d.ts +4 -0
  197. package/dist/features/tester/seed-data/tester-ttl.js +6 -0
  198. package/dist/features/tester/server.d.ts +9 -0
  199. package/dist/features/tester/server.js +8 -0
  200. package/dist/index.d.ts +28 -2
  201. package/dist/index.js +51 -1
  202. package/dist/next/api/api-response.js +5 -1
  203. package/dist/next/api/routeHandler.js +10 -1
  204. package/dist/next/middleware/chain.js +1 -1
  205. package/dist/next/routing/route-map.d.ts +6 -7
  206. package/dist/next/routing/route-map.js +3 -7
  207. package/dist/providers/payment-manual/index.d.ts +2 -2
  208. package/dist/providers/payment-manual/index.js +2 -2
  209. package/dist/react/contexts/SessionContext.d.ts +4 -0
  210. package/dist/react/contexts/SessionContext.js +4 -0
  211. package/dist/repositories/index.d.ts +4 -0
  212. package/dist/repositories/index.js +3 -0
  213. package/dist/security/pii-encrypt.js +12 -2
  214. package/dist/seed/addresses-seed-data.js +1 -1
  215. package/dist/seed/bids-seed-data.js +64 -185
  216. package/dist/seed/blog-posts-seed-data.js +1 -1
  217. package/dist/seed/carousel-slides-seed-data.js +22 -16
  218. package/dist/seed/carousels-seed-data.js +1 -1
  219. package/dist/seed/cart-seed-data.js +1 -1
  220. package/dist/seed/categories-seed-data.js +1 -1
  221. package/dist/seed/claimed-coupons-seed-data.js +1 -1
  222. package/dist/seed/conversations-seed-data.js +1 -1
  223. package/dist/seed/coupon-usage-seed-data.js +1 -1
  224. package/dist/seed/coupons-seed-data.js +1 -1
  225. package/dist/seed/events-seed-data.js +1 -1
  226. package/dist/seed/faq-seed-data.js +119 -3
  227. package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
  228. package/dist/seed/grouped-listings-seed-data.js +1 -1
  229. package/dist/seed/history-seed-data.js +1 -1
  230. package/dist/seed/homepage-sections-seed-data.js +1 -1
  231. package/dist/seed/index.d.ts +2 -2
  232. package/dist/seed/index.js +7 -3
  233. package/dist/seed/lottery-entries-seed-data.js +1 -1
  234. package/dist/seed/manifest.d.ts +4 -3
  235. package/dist/seed/manifest.js +13 -6
  236. package/dist/seed/notifications-seed-data.js +1 -1
  237. package/dist/seed/offers-seed-data.js +1 -1
  238. package/dist/seed/orders-seed-data.js +1 -1
  239. package/dist/seed/payouts-seed-data.js +1 -1
  240. package/dist/seed/products-art-seed-data.js +1 -1
  241. package/dist/seed/products-auctions-seed-data.js +1 -1
  242. package/dist/seed/products-classifieds-seed-data.js +1 -1
  243. package/dist/seed/products-digital-codes-seed-data.js +1 -1
  244. package/dist/seed/products-live-items-seed-data.js +1 -1
  245. package/dist/seed/products-preorders-seed-data.d.ts +2 -0
  246. package/dist/seed/products-preorders-seed-data.js +1 -1
  247. package/dist/seed/products-prize-draws-seed-data.js +1 -1
  248. package/dist/seed/products-standard-seed-data.js +1 -1
  249. package/dist/seed/products-stickers-seed-data.js +1 -1
  250. package/dist/seed/reviews-seed-data.js +1 -1
  251. package/dist/seed/scammers-seed-data.js +1 -1
  252. package/dist/seed/sessions-seed-data.js +1 -1
  253. package/dist/seed/site-settings-seed-data.js +22 -16
  254. package/dist/seed/store-addresses-seed-data.js +1 -1
  255. package/dist/seed/store-extensions-seed-data.js +1 -1
  256. package/dist/seed/stores-seed-data.js +1 -1
  257. package/dist/seed/support-tickets-seed-data.js +1 -1
  258. package/dist/seed/tester-checklist-seed-data.d.ts +2 -0
  259. package/dist/seed/tester-checklist-seed-data.js +248 -0
  260. package/dist/seed/users-seed-data.js +1 -1
  261. package/dist/seed/wishlists-seed-data.js +1 -1
  262. package/dist/server.d.ts +7 -2
  263. package/dist/server.js +19 -12
  264. package/dist/styles.css +181 -165
  265. package/dist/tailwind-utilities.css +1 -1
  266. package/dist/tokens/color-pairs.d.ts +46 -0
  267. package/dist/tokens/color-pairs.js +93 -0
  268. package/dist/tokens/index.d.ts +60 -60
  269. package/dist/tokens/index.js +30 -30
  270. package/dist/tokens/themes/cobalt-night.d.ts +3 -2
  271. package/dist/tokens/themes/cobalt-night.js +3 -2
  272. package/dist/tokens/themes/default-dark.d.ts +4 -1
  273. package/dist/tokens/themes/default-dark.js +53 -50
  274. package/dist/tokens/themes/default-light.d.ts +5 -1
  275. package/dist/tokens/themes/default-light.js +48 -44
  276. package/dist/tokens/themes/sunset.js +4 -4
  277. package/dist/tokens/tokens.css +128 -122
  278. package/dist/ui/components/CollapsibleSection.d.ts +15 -0
  279. package/dist/ui/components/CollapsibleSection.js +13 -0
  280. package/dist/ui/components/ListingToolbar.js +3 -2
  281. package/dist/ui/components/Semantic.style.css +3 -3
  282. package/dist/ui/components/SideDrawer.style.css +19 -19
  283. package/dist/ui/components/Slider.js +1 -1
  284. package/dist/ui/components/StickyToolbar.d.ts +36 -7
  285. package/dist/ui/components/StickyToolbar.js +63 -5
  286. package/dist/ui/components/Tabs.js +17 -1
  287. package/dist/ui/components/Tabs.style.css +10 -0
  288. package/dist/ui/components/Typography.js +7 -0
  289. package/dist/ui/components/surface-tokens.d.ts +0 -7
  290. package/dist/ui/components/surface-tokens.js +7 -0
  291. package/dist/ui/index.d.ts +6 -0
  292. package/dist/ui/index.js +3 -0
  293. package/dist/ui/rich-text/RichText.style.css +20 -20
  294. package/dist/utils/id-generators.d.ts +13 -0
  295. package/dist/utils/id-generators.js +17 -0
  296. package/package.json +1 -1
  297. package/scripts/seed-cli-loader.mjs +15 -0
  298. package/scripts/seed-cli.mjs +71 -17
@@ -183,6 +183,115 @@ export function useGoogleLogin(options) {
183
183
  authEvent.status === RealtimeEventStatus.PENDING;
184
184
  return { mutate, isLoading };
185
185
  }
186
+ /** Connects the current (already-logged-in) user's account to a Google account,
187
+ * without switching sessions. Reuses the same popup + RTDB/postMessage signal
188
+ * mechanism as useGoogleLogin, but POSTs { mode: "link" } to EVENT_INIT so the
189
+ * server callback records the link on the caller's own profile instead of
190
+ * resolving/creating a separate Firebase Auth user. */
191
+ export function useLinkGoogleAccount(options) {
192
+ const authEvent = useAuthEvent();
193
+ const [initiating, setInitiating] = useState(false);
194
+ const [popupPending, setPopupPending] = useState(false);
195
+ const calledRef = useRef(false);
196
+ const closePollRef = useRef(null);
197
+ const clearClosePoll = useCallback(() => {
198
+ if (closePollRef.current !== null) {
199
+ clearInterval(closePollRef.current);
200
+ closePollRef.current = null;
201
+ }
202
+ }, []);
203
+ const onSuccessRef = useRef(options?.onSuccess);
204
+ const onErrorRef = useRef(options?.onError);
205
+ const authEventResetRef = useRef(authEvent.reset);
206
+ useEffect(() => { onSuccessRef.current = options?.onSuccess; }, [options?.onSuccess]);
207
+ useEffect(() => { onErrorRef.current = options?.onError; }, [options?.onError]);
208
+ useEffect(() => { authEventResetRef.current = authEvent.reset; }, [authEvent.reset]);
209
+ useEffect(() => clearClosePoll, [clearClosePoll]);
210
+ useEffect(() => {
211
+ if (authEvent.status === RealtimeEventStatus.SUCCESS) {
212
+ setPopupPending(false);
213
+ if (calledRef.current)
214
+ return;
215
+ calledRef.current = true;
216
+ clearClosePoll();
217
+ onSuccessRef.current?.();
218
+ }
219
+ else if (authEvent.status === RealtimeEventStatus.TIMEOUT) {
220
+ setPopupPending(false);
221
+ if (calledRef.current)
222
+ return;
223
+ calledRef.current = true;
224
+ clearClosePoll();
225
+ onErrorRef.current?.(new Error(authEvent.error ?? "Linking timed out. Please try again."));
226
+ }
227
+ }, [authEvent.status, authEvent.error, clearClosePoll]);
228
+ useEffect(() => {
229
+ const handleMessage = (event) => {
230
+ if (event.origin !== window.location.origin)
231
+ return;
232
+ if (!event.data || event.data.type !== "letitrip_auth_close")
233
+ return;
234
+ if (calledRef.current)
235
+ return;
236
+ calledRef.current = true;
237
+ setPopupPending(false);
238
+ clearClosePoll();
239
+ authEventResetRef.current();
240
+ if (event.data.status === "success") {
241
+ onSuccessRef.current?.();
242
+ }
243
+ else {
244
+ onErrorRef.current?.(new Error(event.data.error ?? "Linking failed. Please try again."));
245
+ }
246
+ };
247
+ window.addEventListener("message", handleMessage);
248
+ return () => window.removeEventListener("message", handleMessage);
249
+ }, [clearClosePoll]);
250
+ const mutate = useCallback(async () => {
251
+ calledRef.current = false;
252
+ const popup = window.open(`${window.location.origin}/auth.html`, "oauth_google_link", "width=500,height=660,left=400,top=100");
253
+ if (!popup) {
254
+ onErrorRef.current?.(new Error("Popup blocked. Please allow popups for this site."));
255
+ return;
256
+ }
257
+ setPopupPending(true);
258
+ clearClosePoll();
259
+ closePollRef.current = setInterval(() => {
260
+ if (popup.closed && !calledRef.current) {
261
+ calledRef.current = true;
262
+ clearClosePoll();
263
+ setPopupPending(false);
264
+ authEventResetRef.current();
265
+ onErrorRef.current?.(new Error("Linking was cancelled."));
266
+ }
267
+ }, 500);
268
+ try {
269
+ setInitiating(true);
270
+ authEvent.reset();
271
+ const { eventId, customToken, rtdbEnabled } = await apiClient.post(AUTH_ENDPOINTS.EVENT_INIT, { mode: "link" });
272
+ const url = `${window.location.origin}${AUTH_ENDPOINTS.GOOGLE_START}?eventId=${encodeURIComponent(eventId)}`;
273
+ localStorage.setItem("letitrip_oauth_redirect", url);
274
+ if (rtdbEnabled !== false) {
275
+ authEvent.subscribe(eventId, customToken);
276
+ }
277
+ }
278
+ catch (err) {
279
+ void normalizeError(err);
280
+ clearClosePoll();
281
+ popup.close();
282
+ setPopupPending(false);
283
+ onErrorRef.current?.(err instanceof Error ? err : new Error("Failed to start account linking."));
284
+ }
285
+ finally {
286
+ setInitiating(false);
287
+ }
288
+ }, [authEvent, clearClosePoll]);
289
+ const isLoading = initiating ||
290
+ popupPending ||
291
+ authEvent.status === RealtimeEventStatus.SUBSCRIBING ||
292
+ authEvent.status === RealtimeEventStatus.PENDING;
293
+ return { mutate, isLoading };
294
+ }
186
295
  export function useRegister(options) {
187
296
  return useMutation({
188
297
  mutationFn: async (data) => {
@@ -11,7 +11,7 @@
11
11
  * Permission naming: admin:resource:action
12
12
  * Ban action naming: plain verb phrase — write_reviews, place_bids, etc.
13
13
  */
14
- export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
14
+ export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
15
15
  /**
16
16
  * Granular user actions that can be soft-banned individually.
17
17
  * A user may have multiple simultaneous soft bans of different types.
@@ -25,6 +25,8 @@ export declare class SessionRepository extends BaseRepository<SessionDocument> {
25
25
  findAllForAdmin(options?: {
26
26
  userId?: string;
27
27
  limit?: number;
28
+ isActive?: boolean;
29
+ sortAscending?: boolean;
28
30
  }): Promise<{
29
31
  sessions: SessionDocument[];
30
32
  stats: {
@@ -195,10 +195,13 @@ export class SessionRepository extends BaseRepository {
195
195
  }
196
196
  async findAllForAdmin(options) {
197
197
  try {
198
- let query = this.getCollection().orderBy(SESSION_FIELDS.LAST_ACTIVITY, "desc");
198
+ let query = this.getCollection().orderBy(SESSION_FIELDS.LAST_ACTIVITY, options?.sortAscending ? "asc" : "desc");
199
199
  if (options?.userId) {
200
200
  query = query.where(SESSION_FIELDS.USER_ID, "==", options.userId);
201
201
  }
202
+ if (options?.isActive !== undefined) {
203
+ query = query.where(SESSION_FIELDS.IS_ACTIVE, "==", options.isActive);
204
+ }
202
205
  if (options?.limit && options.limit > 0) {
203
206
  query = query.limit(options.limit);
204
207
  }
@@ -47,6 +47,7 @@ export interface UserDocument extends BaseDocument {
47
47
  storeId?: string;
48
48
  storeSlug?: string;
49
49
  storeStatus?: "pending" | "approved" | "rejected";
50
+ isTester?: boolean;
50
51
  publicProfile?: {
51
52
  isPublic: boolean;
52
53
  showEmail: boolean;
@@ -99,6 +100,20 @@ export interface UserDocument extends BaseDocument {
99
100
  /** User-controlled notification channel and type preferences. */
100
101
  notificationPreferences?: import("../../account/types").NotificationPreferences;
101
102
  dismissedBannerHash?: string;
103
+ /** True once this account has completed Google sign-in/linking at least once.
104
+ * Set from the server-side OAuth callback — Firebase Auth's own providerData
105
+ * is NOT populated here because the app uses a server-side custom-token
106
+ * exchange rather than the client SDK's linkWithCredential(). */
107
+ googleLinked?: boolean;
108
+ /** The Google account email this profile is linked to, once linked. */
109
+ googleLinkedEmail?: string | null;
110
+ uiPreferences?: {
111
+ /** IDs of collapsible dashboard sections currently collapsed, flat across
112
+ * every page (IDs are page-prefixed, e.g. "admin-dashboard:alerts", to
113
+ * stay unique). Absent/missing IDs default to collapsed for a section the
114
+ * user has never interacted with. See useCollapsedSections. */
115
+ collapsedSections?: string[];
116
+ };
102
117
  }
103
118
  export interface UserSoftBan {
104
119
  action: BannedAction;
@@ -150,7 +165,7 @@ export interface SellerPayoutDetails {
150
165
  }
151
166
  export declare const USER_COLLECTION: "users";
152
167
  export declare const DEFAULT_USER_DATA: Partial<UserDocument>;
153
- export declare const USER_INDEXED_FIELDS: readonly ["email", "emailIndex", "phoneNumber", "phoneIndex", "role", "disabled", "emailVerified", "phoneVerified", "storeSlug", "storeStatus"];
168
+ export declare const USER_INDEXED_FIELDS: readonly ["email", "emailIndex", "phoneNumber", "phoneIndex", "role", "disabled", "emailVerified", "phoneVerified", "storeSlug", "storeStatus", "isTester"];
154
169
  export declare const USER_PUBLIC_FIELDS: readonly ["uid", "displayName", "photoURL", "avatarMetadata", "role", "createdAt", "storeSlug", "publicProfile", "stats"];
155
170
  export declare const USER_UPDATABLE_FIELDS: readonly ["displayName", "photoURL"];
156
171
  export type UserCreateInput = Omit<UserDocument, "uid" | "id" | "createdAt" | "updatedAt">;
@@ -196,6 +211,7 @@ export declare const USER_FIELDS: {
196
211
  readonly STORE_ID: "storeId";
197
212
  readonly STORE_SLUG: "storeSlug";
198
213
  readonly STORE_STATUS: "storeStatus";
214
+ readonly IS_TESTER: "isTester";
199
215
  readonly AVATAR: {
200
216
  readonly URL: "avatarMetadata.url";
201
217
  readonly POSITION: "avatarMetadata.position";
@@ -57,6 +57,7 @@ export const USER_INDEXED_FIELDS = [
57
57
  "phoneVerified",
58
58
  "storeSlug",
59
59
  "storeStatus",
60
+ "isTester",
60
61
  ];
61
62
  export const USER_PUBLIC_FIELDS = [
62
63
  "uid",
@@ -106,6 +107,7 @@ export const USER_FIELDS = {
106
107
  STORE_ID: "storeId",
107
108
  STORE_SLUG: "storeSlug",
108
109
  STORE_STATUS: "storeStatus",
110
+ IS_TESTER: "isTester",
109
111
  AVATAR: {
110
112
  URL: "avatarMetadata.url",
111
113
  POSITION: "avatarMetadata.position",
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { useBlogPosts } from "../hooks/useBlog";
6
- import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text } from "../../../ui";
6
+ import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
7
7
  import { ROUTES } from "../../../next";
8
8
  import { BlogCard } from "./BlogListView";
9
9
  import { BlogFilters, BLOG_PUBLIC_SORT_OPTIONS } from "./BlogFilters";
@@ -113,5 +113,5 @@ export function BlogIndexListing({ initialData }) {
113
113
  const commitSearch = useCallback(() => {
114
114
  table.set(TABLE_KEYS.QUERY, searchInput.trim());
115
115
  }, [searchInput, table]);
116
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: renderBlogGrid({ isLoading, posts, view }) }), renderBlogFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable })] }));
116
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderBlogGrid({ isLoading, posts, view }) }), renderBlogFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable })] }));
117
117
  }
@@ -14,6 +14,10 @@ export interface BlogPostDocument extends BaseDocument {
14
14
  title: string;
15
15
  slug: string;
16
16
  excerpt: string;
17
+ /** Tester sandbox flag — set on the shared admin-seeded test post; swept by testerSandboxCleanup. */
18
+ isTestData?: boolean;
19
+ /** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
20
+ testDataExpiresAt?: Date;
17
21
  content: string;
18
22
  coverImage?: MediaFieldInput;
19
23
  contentImages?: MediaField[];
@@ -8,8 +8,24 @@ import { serverLogger } from "../../../monitoring";
8
8
  import { ValidationError } from "../../../errors";
9
9
  import { cartRepository } from "../repository/cart.repository";
10
10
  import { productRepository } from "../../products/repository/products.repository";
11
+ import { storeRepository } from "../../stores/repository/store.repository";
11
12
  import { normalizeListingType } from "../../products/utils/listing-type";
12
13
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
14
+ /**
15
+ * Product documents don't always carry a denormalized `storeName` — the
16
+ * create routes only set it when the caller explicitly passes one, so
17
+ * listings created without it leave the field undefined forever. Cart items
18
+ * built from those products then fell back to displaying the raw `storeId`
19
+ * slug (e.g. "store-letitrip-official") in the UI. Resolving from the store
20
+ * document here — the single add-to-cart chokepoint — fixes every caller at
21
+ * once instead of patching each product-creation call site.
22
+ */
23
+ async function resolveStoreName(storeId, candidate) {
24
+ if (candidate)
25
+ return candidate;
26
+ const store = await storeRepository.findById(storeId).catch(() => null);
27
+ return store?.storeName ?? storeId;
28
+ }
13
29
  export async function addItemToCart(userId, input) {
14
30
  serverLogger.debug("addItemToCart", { userId, productId: input.productId });
15
31
  // SB-UNI-F 2026-05-13 — capability gate. Classified + live listings reject
@@ -19,7 +35,8 @@ export async function addItemToCart(userId, input) {
19
35
  if (!canAddToCart(input.listingType)) {
20
36
  throw new ValidationError(`Listings of type "${input.listingType}" cannot be added to the cart.`);
21
37
  }
22
- return cartRepository.addItem(userId, input);
38
+ const storeName = await resolveStoreName(input.storeId, input.storeName);
39
+ return cartRepository.addItem(userId, { ...input, storeName });
23
40
  }
24
41
  export async function updateCartItem(userId, itemId, input) {
25
42
  return cartRepository.updateItem(userId, itemId, input);
@@ -39,6 +56,7 @@ export async function mergeGuestCart(userId, items) {
39
56
  if (product.availableQuantity < 1)
40
57
  continue;
41
58
  const safeQty = Math.min(item.quantity, product.availableQuantity);
59
+ const storeName = await resolveStoreName(product.storeId, product.storeName);
42
60
  await cartRepository.addItem(userId, {
43
61
  productId: product.id,
44
62
  productTitle: product.title,
@@ -47,7 +65,7 @@ export async function mergeGuestCart(userId, items) {
47
65
  currency: product.currency ?? getDefaultCurrency(),
48
66
  quantity: safeQty,
49
67
  storeId: product.storeId,
50
- storeName: product.storeName ?? "",
68
+ storeName,
51
69
  listingType: normalizeListingType(product),
52
70
  });
53
71
  }
@@ -47,7 +47,7 @@ function numParam(url, key) {
47
47
  return Number.isFinite(n) ? n : null;
48
48
  }
49
49
  const SAFE_CATEGORY_FILTER_FIELDS = new Set([
50
- "type", "parentIds", "isFeatured", "showOnHomepage",
50
+ "categoryType", "parentIds", "isFeatured", "showOnHomepage",
51
51
  "tier", "isActive", "isLeaf", "isBrand", "isSearchable",
52
52
  ]);
53
53
  function validateSieveFilters(raw, allowedFields) {
@@ -96,7 +96,7 @@ export async function GET(request) {
96
96
  // -- Build Sieve filter string from query params ----------------------------
97
97
  const parts = [];
98
98
  if (type)
99
- parts.push(`type==${type}`);
99
+ parts.push(`categoryType==${type}`);
100
100
  if (parentId)
101
101
  parts.push(`parentIds@=${parentId}`);
102
102
  if (featured === "true")
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { useCategoriesFiltered } from "../hooks/useCategories";
5
5
  import { ROUTES } from "../../../next";
6
- import { Button, Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text } from "../../../ui";
6
+ import { Button, Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
7
7
  import { CategoryCard } from "./CategoryGrid";
8
8
  import { CategoryFilters } from "./CategoryFilters";
9
9
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
@@ -43,7 +43,7 @@ const FILTER_KEYS = [TABLE_KEYS.IS_FEATURED, "isBrand", "rootOnly", "tier", "min
43
43
  const SORT_OPTIONS = [
44
44
  { value: sortBy(CATEGORY_FIELDS.NAME, "ASC"), label: "Name A–Z" },
45
45
  { value: sortBy(CATEGORY_FIELDS.NAME), label: "Name Z–A" },
46
- { value: sortBy("productCount", "DESC"), label: "Most Products" },
46
+ { value: sortBy("metrics.productCount", "DESC"), label: "Most Products" },
47
47
  ];
48
48
  export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
49
49
  const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
@@ -147,5 +147,5 @@ export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
147
147
  : "border-transparent text-[var(--appkit-color-text-muted)] hover:text-zinc-800 text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text-muted)]",
148
148
  ].join(" "), children: tab.label }, tab.key))) })), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: activeTab === "brands" || brandsOnly ? "Search brands..." : "Search categories...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
149
149
  { label: "Featured only", active: table.get(TABLE_KEYS.IS_FEATURED) === "true", onChange: (next) => table.set(TABLE_KEYS.IS_FEATURED, next ? "true" : "") },
150
- ] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryGrid({ isLoading, categories, view, activeSearch, activeTab, brandsOnly }) }), renderCategoryFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable })] }));
150
+ ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryGrid({ isLoading, categories, view, activeSearch, activeTab, brandsOnly }) }), renderCategoryFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable })] }));
151
151
  }
@@ -10,7 +10,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
10
10
  */
11
11
  import { Row, Stack, sortBy } from "@mohasinac/appkit";
12
12
  import { useMemo, useCallback, useState } from "react";
13
- import { Div, Label, Span, Text, Toggle } from "../../../ui";
13
+ import { Div, Label, Span, Text, Toggle, StickyToolbar } from "../../../ui";
14
14
  import { ListingToolbar, Pagination, FilterDrawer } from "../../../ui";
15
15
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
16
16
  import { BundleBuyNowCta } from "./BundleBuyNowCta";
@@ -93,5 +93,5 @@ export function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, })
93
93
  // Apply → close. <FilterDrawer> below stays available for parity
94
94
  // with every other listing page's filter affordance.
95
95
  { label: "Show out-of-stock", active: showOutOfStock, onChange: (next) => table.setMany({ showOutOfStock: next ? "true" : "", page: "1" }) },
96
- ] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: pageItems.length === 0 ? (_jsxs(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: pageItems.map((bundle) => (_jsxs(Stack, { children: [_jsx(MarketplaceBundleCard, { bundle: bundle }), onBuyNow && (_jsx(Div, { border: "default", surface: "default", className: "-mt-px border-t border-t-0 border-[var(--appkit-color-border)] pt-[0.5rem] pb-[0.75rem]", padding: "x-sm", roundedBottom: "lg", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: bundle.bundleStockStatus === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }, bundle.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: () => setPendingShowOutOfStock(false), activeCount: filterActiveCount, hideTrigger: true, children: _jsx(Stack, { className: `${__P.p4}`, gap: "md", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "justify-[space-between] cursor-pointer", children: [_jsx(Span, { size: "sm", weight: "medium", color: "muted", children: "Show out-of-stock bundles" }), _jsx(Toggle, { checked: pendingShowOutOfStock, onChange: setPendingShowOutOfStock, size: "sm" })] }) }) })] }));
96
+ ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: pageItems.length === 0 ? (_jsxs(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: pageItems.map((bundle) => (_jsxs(Stack, { children: [_jsx(MarketplaceBundleCard, { bundle: bundle }), onBuyNow && (_jsx(Div, { border: "default", surface: "default", className: "-mt-px border-t border-t-0 border-[var(--appkit-color-border)] pt-[0.5rem] pb-[0.75rem]", padding: "x-sm", roundedBottom: "lg", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: bundle.bundleStockStatus === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }, bundle.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: () => setPendingShowOutOfStock(false), activeCount: filterActiveCount, hideTrigger: true, children: _jsx(Stack, { className: `${__P.p4}`, gap: "md", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "justify-[space-between] cursor-pointer", children: [_jsx(Span, { size: "sm", weight: "medium", color: "muted", children: "Show out-of-stock bundles" }), _jsx(Toggle, { checked: pendingShowOutOfStock, onChange: setPendingShowOutOfStock, size: "sm" })] }) }) })] }));
97
97
  }
@@ -112,7 +112,9 @@ export async function CategoryDetailPageView({ slug }) {
112
112
  const storeCount = categoryStores.length;
113
113
  const coverImage = category?.display?.coverImage;
114
114
  const hasCover = Boolean(coverImage);
115
- return (_jsxs(Main, { children: [_jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORIES), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Categories" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: category?.name ?? slug })] }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), childCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: "max-w-7xl mx-auto", padding: "inline", children: _jsx(Div, { layout: "flex", gap: "2", className: `.5 ${__O.xAuto} [scrollbar-width:none]`, padding: "b-2xs", children: childCategories.map((child) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(child.slug)), className: "flex-shrink-0 flex items-center gap-[var(--appkit-space-1-5)] rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-elevated)] px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:border-primary hover:text-primary transition-colors whitespace-nowrap", children: [child.display?.icon && (_jsx(Span, { className: "leading-none", children: child.display.icon })), child.name, (child.metrics?.productCount ?? 0) > 0 && (_jsx(Span, { size: "xs", color: "muted", children: (child.metrics?.productCount ?? 0).toLocaleString() }))] }, child.id))) }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: {
115
+ return (_jsxs(Main, { children: [_jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORIES), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Categories" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: category?.name ?? slug })] }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), childCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: "max-w-7xl mx-auto", padding: "inline", children: _jsx(Div, { layout: "flex", gap: "2", className: `.5 ${__O.xAuto} [scrollbar-width:none]`, padding: "b-2xs", children: childCategories.map((child) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(child.slug)),
116
+ // audit-responsive-wrap-ok: deliberate horizontal-scroll chip row (parent has overflow-x-auto) — each chip staying single-line is the intended design, not hidden content
117
+ className: "flex-shrink-0 flex items-center gap-[var(--appkit-space-1-5)] rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-elevated)] px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:border-primary hover:text-primary transition-colors whitespace-nowrap", children: [child.display?.icon && (_jsx(Span, { className: "leading-none", children: child.display.icon })), child.name, (child.metrics?.productCount ?? 0) > 0 && (_jsx(Span, { size: "xs", color: "muted", children: (child.metrics?.productCount ?? 0).toLocaleString() }))] }, child.id))) }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: {
116
118
  products: productCount,
117
119
  auctions: auctionCount,
118
120
  preOrders: preOrderCount,
@@ -4,7 +4,7 @@ import { useState, useCallback } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { usePendingTable } from "../../../react/hooks/usePendingTable";
6
6
  import { useProducts } from "../../products/hooks/useProducts";
7
- import { Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast } from "../../../ui";
7
+ import { Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast, StickyToolbar } from "../../../ui";
8
8
  import { useAuthGate } from "../../../react/hooks/useAuthGate";
9
9
  import { ACTION_ID } from "../../products/constants/action-defs";
10
10
  import { ROUTES } from "../../../next";
@@ -103,7 +103,7 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
103
103
  }, [localCart, showToast]);
104
104
  return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: `Search in ${categorySlug.replace(/-/g, " ")}...`, onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => {
105
105
  table.set("sort", v);
106
- }, view: view === "card" ? "grid" : "list", onViewChange: (v) => handleViewToggle(v === "grid" ? "card" : "list"), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryProductGrid({
106
+ }, view: view === "card" ? "grid" : "list", onViewChange: (v) => handleViewToggle(v === "grid" ? "card" : "list"), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryProductGrid({
107
107
  isLoading, products: products, view,
108
108
  getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)),
109
109
  onWishlistToggle: handleWishlistToggle, wishlistedIds, onAddToCart: handleAddToCart,
@@ -82,6 +82,10 @@ export interface CategoryDocument extends BaseDocument {
82
82
  name: string;
83
83
  slug: string;
84
84
  description?: string;
85
+ /** Tester sandbox flag — set on shared admin-seeded test categories/brands/bundles; swept by testerSandboxCleanup. */
86
+ isTestData?: boolean;
87
+ /** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
88
+ testDataExpiresAt?: Date;
85
89
  rootId: string;
86
90
  parentIds: string[];
87
91
  childrenIds: string[];
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { useProducts } from "../../products/hooks/useProducts";
6
- import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text } from "../../../ui";
6
+ import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
7
7
  import { usePendingTable } from "../../../react/hooks/usePendingTable";
8
8
  import { useAuthGate } from "../../../react/hooks/useAuthGate";
9
9
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
@@ -119,5 +119,5 @@ export function ClassifiedIndexListing({ initialData }) {
119
119
  // → close for every toggle. The full facet set (category/city/price/
120
120
  // negotiable) remains in <FilterDrawer> below.
121
121
  { label: "Ships nationwide", active: table.get(TABLE_KEYS.ACCEPTS_SHIPPING) === "true", onChange: (next) => table.set(TABLE_KEYS.ACCEPTS_SHIPPING, next ? "true" : "") },
122
- ] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No classified listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.CLASSIFIED_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(ClassifiedFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
122
+ ] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No classified listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.CLASSIFIED_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(ClassifiedFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
123
123
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { useProducts } from "../../products/hooks/useProducts";
6
- import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast } from "../../../ui";
6
+ import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast, StickyToolbar } from "../../../ui";
7
7
  import { usePendingTable } from "../../../react/hooks/usePendingTable";
8
8
  import { useAuthGate } from "../../../react/hooks/useAuthGate";
9
9
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
@@ -110,5 +110,5 @@ export function DigitalCodesIndexListing({ initialData }) {
110
110
  useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] } } : {});
111
111
  return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search digital codes\u2026", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: (e) => { if (e.key === "Enter")
112
112
  commitSearch(); }, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: (v) => { if (v !== "table")
113
- setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No digital code listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.DIGITAL_CODE_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(DigitalCodeFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
113
+ setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No digital code listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.DIGITAL_CODE_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(DigitalCodeFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
114
114
  }
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Brand colours flow through the active light-theme palette
16
16
  * (`var(--appkit-color-*)`). Because email clients don't resolve CSS
17
- * variables, we hard-code the cobalt + lime brand defaults inline; admin
17
+ * variables, we hard-code the teal + magenta brand defaults inline; admin
18
18
  * themes do not affect email rendering.
19
19
  */
20
20
  import type { ReactNode } from "react";
@@ -39,7 +39,7 @@ export declare function EmailContainer({ width, background, children, }: EmailCo
39
39
  export interface EmailHeaderProps {
40
40
  /** Brand name shown in the header. */
41
41
  brandName: string;
42
- /** Optional brand colour override (defaults to the cobalt primary). */
42
+ /** Optional brand colour override (defaults to the teal primary). */
43
43
  background?: string;
44
44
  children?: ReactNode;
45
45
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  const BRAND = {
3
- primary: "#3570fc",
4
- secondary: "#65c408",
3
+ primary: "#0d9488",
4
+ secondary: "#c026d3",
5
5
  surface: "#ffffff",
6
6
  bg: "#fafafa",
7
7
  text: "#18181b",
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { useEvents } from "../hooks/useEvents";
6
- import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text } from "../../../ui";
6
+ import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
7
7
  import { EventCard } from "./EventCard";
8
8
  import { EventFilters, EVENT_PUBLIC_SORT_OPTIONS } from "./EventFilters";
9
9
  import { EVENT_FIELDS } from "../../../constants/field-names";
@@ -123,5 +123,5 @@ export function EventsIndexListing({ initialData }) {
123
123
  }, [searchInput, table]);
124
124
  return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), sortOptions: EVENT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
125
125
  { label: "Show expired", active: showExpired, onChange: (next) => table.set(TABLE_KEYS.SHOW_EXPIRED, next ? "true" : "") },
126
- ] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8 mt-2", surface: "subtle", rounded: "default" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No events found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(EventFilters, { table: pendingTable, variant: "public" }) })] }));
126
+ ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8 mt-2", surface: "subtle", rounded: "default" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No events found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(EventFilters, { table: pendingTable, variant: "public" }) })] }));
127
127
  }
@@ -150,6 +150,7 @@ EventEntryRepository.SIEVE_FIELDS = {
150
150
  userId: { canFilter: true, canSort: false },
151
151
  userDisplayName: { canFilter: false, canSort: false },
152
152
  reviewStatus: { canFilter: true, canSort: false },
153
+ status: { canFilter: true, canSort: false },
153
154
  submittedAt: { canFilter: true, canSort: true },
154
155
  points: { canFilter: true, canSort: true },
155
156
  };
@@ -14,6 +14,10 @@ export interface EventDocument extends BaseDocument {
14
14
  title: string;
15
15
  description: string;
16
16
  status: EventStatus;
17
+ /** Tester sandbox flag — set on the shared admin-seeded test event; swept by testerSandboxCleanup. */
18
+ isTestData?: boolean;
19
+ /** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
20
+ testDataExpiresAt?: Date;
17
21
  startsAt: Date;
18
22
  endsAt: Date;
19
23
  coverImage?: MediaField | null;
@@ -65,6 +69,8 @@ export type EventEntryDocument = {
65
69
  reviewedBy?: string;
66
70
  reviewedAt?: Date;
67
71
  reviewNote?: string;
72
+ /** RSVP-style status, distinct from `reviewStatus` (moderation workflow). Drives raffle eligibility + admin confirm/waitlist/cancel actions. */
73
+ status?: "CONFIRMED" | "WAITLISTED" | "CANCELLED";
68
74
  points?: number;
69
75
  raffleEligible?: boolean;
70
76
  spinUsed?: boolean;
@@ -75,7 +81,7 @@ export type EventEntryDocument = {
75
81
  submittedAt: Date;
76
82
  };
77
83
  export declare const EVENT_INDEXED_FIELDS: readonly ["type", "status", "startsAt", "endsAt", "createdAt"];
78
- export declare const EVENT_ENTRY_INDEXED_FIELDS: readonly ["eventId", "userId", "reviewStatus", "submittedAt", "points"];
84
+ export declare const EVENT_ENTRY_INDEXED_FIELDS: readonly ["eventId", "userId", "reviewStatus", "status", "submittedAt", "points"];
79
85
  export declare const EVENT_FIELDS: {
80
86
  readonly ID: "id";
81
87
  readonly TYPE: "type";