@mohasinac/appkit 4.2.0 → 4.3.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 (258) hide show
  1. package/dist/_internal/client/features/layout/navActive.d.ts +17 -0
  2. package/dist/_internal/client/features/layout/navActive.js +33 -0
  3. package/dist/_internal/server/features/auth/actions.d.ts +0 -1
  4. package/dist/_internal/server/features/auth/actions.js +0 -1
  5. package/dist/_internal/server/features/bundles/data.d.ts +22 -4
  6. package/dist/_internal/server/features/bundles/data.js +41 -4
  7. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  8. package/dist/_internal/server/features/bundles/index.js +1 -1
  9. package/dist/_internal/server/features/checkout/actions.js +7 -78
  10. package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
  11. package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
  12. package/dist/_internal/server/features/orders/adapters.js +1 -14
  13. package/dist/_internal/server/features/products/actions.js +8 -3
  14. package/dist/_internal/server/features/products/service.d.ts +15 -0
  15. package/dist/_internal/server/features/products/service.js +41 -0
  16. package/dist/_internal/server/functions/firestore.d.ts +3 -1
  17. package/dist/_internal/server/functions/firestore.js +17 -1
  18. package/dist/_internal/server/functions/scheduled.d.ts +2 -5
  19. package/dist/_internal/server/functions/scheduled.js +6 -30
  20. package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
  21. package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
  22. package/dist/_internal/server/jobs/core/index.d.ts +4 -4
  23. package/dist/_internal/server/jobs/core/index.js +4 -4
  24. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
  25. package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
  26. package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
  27. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +2 -2
  28. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
  29. package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
  30. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
  31. package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
  32. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
  33. package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
  34. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -4
  35. package/dist/_internal/server/jobs/handlers/index.js +8 -6
  36. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
  37. package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
  38. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
  39. package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
  40. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
  41. package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
  42. package/dist/_internal/shared/actions/action-registry.js +40 -10
  43. package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
  44. package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
  45. package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
  46. package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
  47. package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
  48. package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
  49. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  50. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  51. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
  52. package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
  53. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
  54. package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
  55. package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
  56. package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
  57. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  58. package/dist/constants/api-endpoints.d.ts +6 -9
  59. package/dist/constants/api-endpoints.js +5 -5
  60. package/dist/constants/field-names.d.ts +17 -1
  61. package/dist/constants/field-names.js +17 -1
  62. package/dist/features/account/components/UserSidebar.js +21 -9
  63. package/dist/features/account/hooks/useNotifications.js +5 -0
  64. package/dist/features/admin/actions/admin-actions.js +6 -0
  65. package/dist/features/admin/actions/notification-actions.js +1 -2
  66. package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
  67. package/dist/features/admin/components/AdminGuideHubView.js +15 -1
  68. package/dist/features/admin/components/AdminPaymentsGuideView.d.ts +2 -0
  69. package/dist/features/admin/components/AdminPaymentsGuideView.js +36 -0
  70. package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
  71. package/dist/features/admin/components/AdminSidebar.js +18 -9
  72. package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
  73. package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
  74. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +2 -0
  75. package/dist/features/admin/components/AdminWhatsAppGuideView.js +35 -0
  76. package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
  77. package/dist/features/admin/hooks/useAdminListing.js +4 -2
  78. package/dist/features/admin/schemas/firestore.d.ts +13 -5
  79. package/dist/features/admin/schemas/firestore.js +1 -2
  80. package/dist/features/auctions/actions/bid-actions.js +3 -1
  81. package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
  82. package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
  83. package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
  84. package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
  85. package/dist/features/auctions/components/CollapsibleBidHistory.js +22 -3
  86. package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
  87. package/dist/features/auctions/components/LiveBidPrice.js +17 -0
  88. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  89. package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
  90. package/dist/features/auctions/hooks/useBids.d.ts +33 -0
  91. package/dist/features/auctions/hooks/useBids.js +32 -0
  92. package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
  93. package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
  94. package/dist/features/auth/checkout-value-otp.d.ts +7 -10
  95. package/dist/features/auth/checkout-value-otp.js +14 -10
  96. package/dist/features/auth/server.d.ts +0 -1
  97. package/dist/features/auth/server.js +0 -1
  98. package/dist/features/blog/api/[slug]/route.d.ts +5 -0
  99. package/dist/features/blog/api/[slug]/route.js +12 -3
  100. package/dist/features/blog/components/BlogPostView.d.ts +2 -0
  101. package/dist/features/blog/components/BlogPostView.js +5 -4
  102. package/dist/features/blog/hooks/useBlog.d.ts +2 -0
  103. package/dist/features/blog/hooks/useBlog.js +2 -0
  104. package/dist/features/blog/repository/blog.repository.d.ts +7 -0
  105. package/dist/features/blog/repository/blog.repository.js +44 -0
  106. package/dist/features/blog/schemas/firestore.d.ts +3 -0
  107. package/dist/features/blog/schemas/firestore.js +1 -0
  108. package/dist/features/blog/types/index.d.ts +2 -0
  109. package/dist/features/cart/components/index.d.ts +0 -2
  110. package/dist/features/cart/components/index.js +0 -1
  111. package/dist/features/cart/hooks/useCartCount.js +22 -5
  112. package/dist/features/cart/hooks/useGuestCart.js +11 -1
  113. package/dist/features/cart/utils/guest-cart.d.ts +4 -0
  114. package/dist/features/cart/utils/guest-cart.js +10 -0
  115. package/dist/features/cart/utils/pending-ops.d.ts +17 -0
  116. package/dist/features/cart/utils/pending-ops.js +46 -5
  117. package/dist/features/categories/components/BrandDetailPageView.js +6 -2
  118. package/dist/features/categories/components/BundleDetailView.js +4 -2
  119. package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
  120. package/dist/features/categories/schemas/firestore.d.ts +7 -0
  121. package/dist/features/checkout/actions/index.d.ts +0 -1
  122. package/dist/features/checkout/actions/index.js +0 -1
  123. package/dist/features/checkout/schemas/firestore.d.ts +2 -2
  124. package/dist/features/events/actions/event-actions.d.ts +2 -0
  125. package/dist/features/events/actions/event-actions.js +5 -0
  126. package/dist/features/events/components/EventCard.js +2 -3
  127. package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
  128. package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
  129. package/dist/features/events/components/index.d.ts +2 -0
  130. package/dist/features/events/components/index.js +1 -0
  131. package/dist/features/events/repository/events.repository.d.ts +6 -0
  132. package/dist/features/events/repository/events.repository.js +21 -0
  133. package/dist/features/history/repository/user-history.repository.d.ts +1 -1
  134. package/dist/features/history/utils/guest-history.d.ts +1 -1
  135. package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
  136. package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
  137. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  138. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  139. package/dist/features/layout/AppLayoutShell.js +44 -10
  140. package/dist/features/layout/NavbarLayout.js +1 -1
  141. package/dist/features/layout/TitleBarLayout.js +2 -2
  142. package/dist/features/media/MediaVideo.d.ts +7 -0
  143. package/dist/features/media/MediaVideo.js +36 -5
  144. package/dist/features/orders/components/OrdersList.js +1 -5
  145. package/dist/features/orders/schemas/firestore.d.ts +2 -8
  146. package/dist/features/orders/schemas/index.d.ts +3 -32
  147. package/dist/features/orders/schemas/index.js +1 -4
  148. package/dist/features/orders/types/index.d.ts +6 -9
  149. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  150. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
  151. package/dist/features/products/actions/product-actions.js +13 -10
  152. package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
  153. package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
  154. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  155. package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
  156. package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
  157. package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
  158. package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
  159. package/dist/features/products/components/PrizeRevealModal.js +18 -147
  160. package/dist/features/products/components/ProductDetailPageView.js +72 -30
  161. package/dist/features/products/components/ProductForm.js +7 -14
  162. package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
  163. package/dist/features/products/components/ProductGalleryClient.js +29 -10
  164. package/dist/features/products/components/ProductsIndexListing.js +2 -1
  165. package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
  166. package/dist/features/products/components/ShowGroupSection.js +2 -2
  167. package/dist/features/products/components/SublistingCarouselSection.js +1 -1
  168. package/dist/features/products/components/index.d.ts +4 -2
  169. package/dist/features/products/components/index.js +1 -0
  170. package/dist/features/products/constants/action-defs.d.ts +1 -3
  171. package/dist/features/products/constants/action-defs.js +0 -5
  172. package/dist/features/products/repository/products.repository.d.ts +6 -0
  173. package/dist/features/products/repository/products.repository.js +20 -1
  174. package/dist/features/products/schemas/firestore.d.ts +28 -3
  175. package/dist/features/products/schemas/index.d.ts +11 -3
  176. package/dist/features/products/schemas/index.js +10 -2
  177. package/dist/features/products/types/index.d.ts +2 -1
  178. package/dist/features/reviews/actions/review-actions.js +1 -0
  179. package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
  180. package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
  181. package/dist/features/reviews/components/ReviewsList.js +2 -1
  182. package/dist/features/reviews/schemas/firestore.d.ts +3 -0
  183. package/dist/features/reviews/schemas/index.d.ts +6 -0
  184. package/dist/features/reviews/schemas/index.js +2 -0
  185. package/dist/features/reviews/types/index.d.ts +3 -0
  186. package/dist/features/scams/actions/scam-actions.d.ts +3 -0
  187. package/dist/features/scams/actions/scam-actions.js +3 -2
  188. package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
  189. package/dist/features/scams/components/ScamProfileView.js +2 -2
  190. package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
  191. package/dist/features/scams/repository/scammer.repository.js +24 -0
  192. package/dist/features/seller/actions/seller-actions.js +8 -0
  193. package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
  194. package/dist/features/seller/components/SellerSidebar.js +20 -9
  195. package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
  196. package/dist/features/stores/components/StoreGuideHubView.js +7 -1
  197. package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
  198. package/dist/features/stores/components/StoreNavTabs.js +22 -9
  199. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +3 -0
  200. package/dist/features/stores/components/StoreWhatsAppGuideView.js +12 -0
  201. package/dist/features/stores/components/index.d.ts +2 -0
  202. package/dist/features/stores/components/index.js +1 -0
  203. package/dist/features/tester/actions/index.d.ts +1 -0
  204. package/dist/features/tester/actions/index.js +1 -0
  205. package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
  206. package/dist/features/tester/actions/leaderboard-actions.js +5 -0
  207. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
  208. package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
  209. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
  210. package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
  211. package/dist/features/tester/components/index.d.ts +2 -0
  212. package/dist/features/tester/components/index.js +1 -0
  213. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
  214. package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
  215. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
  216. package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
  217. package/dist/features/tester/schemas/firestore.d.ts +22 -1
  218. package/dist/features/tester/schemas/firestore.js +8 -0
  219. package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
  220. package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
  221. package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
  222. package/dist/features/tester/seed-data/products-tester-seed-data.js +154 -9
  223. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +252 -15
  224. package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
  225. package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
  226. package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
  227. package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
  228. package/dist/index.d.ts +23 -22
  229. package/dist/index.js +31 -58
  230. package/dist/jobs.d.ts +1 -1
  231. package/dist/jobs.js +2 -2
  232. package/dist/next/routing/route-map.d.ts +9 -2
  233. package/dist/next/routing/route-map.js +4 -1
  234. package/dist/schemas/registry.d.ts +6 -26
  235. package/dist/seed/bids-seed-data.js +14 -2
  236. package/dist/seed/blog-posts-seed-data.js +2 -1
  237. package/dist/seed/categories-seed-data.js +11 -2
  238. package/dist/seed/events-seed-data.js +11 -11
  239. package/dist/seed/faq-seed-data.js +5 -1
  240. package/dist/seed/products-auctions-seed-data.js +2 -2
  241. package/dist/seed/products-preorders-seed-data.d.ts +2 -1
  242. package/dist/seed/products-prize-draws-seed-data.js +56 -10
  243. package/dist/seed/products-standard-seed-data.js +80 -3
  244. package/dist/seed/scammers-seed-data.js +75 -2
  245. package/dist/seed/site-settings-seed-data.js +5 -2
  246. package/dist/server-entry.d.ts +0 -1
  247. package/dist/server-entry.js +0 -1
  248. package/dist/server.d.ts +4 -20
  249. package/dist/server.js +10 -57
  250. package/dist/styles.css +1 -277
  251. package/dist/tailwind-utilities.css +1 -1
  252. package/dist/ui/components/BaseListingCard.js +1 -1
  253. package/dist/ui/components/ImageLightbox.d.ts +9 -0
  254. package/dist/ui/components/ImageLightbox.js +6 -3
  255. package/dist/ui/components/ListingToolbar.d.ts +18 -1
  256. package/dist/ui/components/ListingToolbar.js +5 -2
  257. package/dist/ui/components/StickyToolbar.js +13 -8
  258. package/package.json +1 -1
@@ -37,6 +37,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
37
37
  id: z.ZodString;
38
38
  productId: z.ZodString;
39
39
  productTitle: z.ZodString;
40
+ listingType: z.ZodOptional<z.ZodString>;
40
41
  storeId: z.ZodOptional<z.ZodString>;
41
42
  storeName: z.ZodOptional<z.ZodString>;
42
43
  userId: z.ZodString;
@@ -126,6 +127,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
126
127
  } | undefined;
127
128
  storeId?: string | undefined;
128
129
  featured?: boolean | undefined;
130
+ listingType?: string | undefined;
129
131
  images?: string[] | undefined;
130
132
  storeName?: string | undefined;
131
133
  userAvatar?: string | undefined;
@@ -173,6 +175,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
173
175
  } | undefined;
174
176
  storeId?: string | undefined;
175
177
  featured?: boolean | undefined;
178
+ listingType?: string | undefined;
176
179
  images?: string[] | undefined;
177
180
  storeName?: string | undefined;
178
181
  userAvatar?: string | undefined;
@@ -241,6 +244,7 @@ export declare const reviewSchema: z.ZodObject<{
241
244
  id: z.ZodString;
242
245
  productId: z.ZodString;
243
246
  productTitle: z.ZodOptional<z.ZodString>;
247
+ listingType: z.ZodOptional<z.ZodString>;
244
248
  storeSlug: z.ZodOptional<z.ZodString>;
245
249
  storeName: z.ZodOptional<z.ZodString>;
246
250
  userId: z.ZodString;
@@ -303,6 +307,7 @@ export declare const reviewSchema: z.ZodObject<{
303
307
  createdAt?: string | undefined;
304
308
  updatedAt?: string | undefined;
305
309
  featured?: boolean | undefined;
310
+ listingType?: string | undefined;
306
311
  images?: {
307
312
  url: string;
308
313
  thumbnailUrl?: string | undefined;
@@ -333,6 +338,7 @@ export declare const reviewSchema: z.ZodObject<{
333
338
  createdAt?: string | undefined;
334
339
  updatedAt?: string | undefined;
335
340
  featured?: boolean | undefined;
341
+ listingType?: string | undefined;
336
342
  images?: {
337
343
  url: string;
338
344
  thumbnailUrl?: string | undefined;
@@ -14,6 +14,7 @@ export const reviewFirestoreSchema = z.object({
14
14
  id: z.string(),
15
15
  productId: z.string(),
16
16
  productTitle: z.string(),
17
+ listingType: z.string().optional(),
17
18
  storeId: z.string().optional(),
18
19
  storeName: z.string().optional(),
19
20
  userId: z.string(),
@@ -72,6 +73,7 @@ export const reviewSchema = z.object({
72
73
  id: z.string(),
73
74
  productId: z.string(),
74
75
  productTitle: z.string().optional(),
76
+ listingType: z.string().optional(),
75
77
  storeSlug: z.string().optional(),
76
78
  storeName: z.string().optional(),
77
79
  userId: z.string(),
@@ -1,3 +1,4 @@
1
+ import type { ListingType } from "../../products/types/index";
1
2
  export type ReviewStatus = "pending" | "approved" | "rejected";
2
3
  export interface ReviewImage {
3
4
  url: string;
@@ -14,6 +15,8 @@ export interface Review {
14
15
  id: string;
15
16
  productId: string;
16
17
  productTitle?: string;
18
+ /** Denormalized from the product at review-creation time — drives the correct detail-page link. */
19
+ listingType?: ListingType;
17
20
  storeSlug?: string;
18
21
  storeName?: string;
19
22
  userId: string;
@@ -3,7 +3,10 @@ export interface ScammerProfilePageData {
3
3
  scammer: ScammerDocument;
4
4
  incidents: ScammerIncidentDocument[];
5
5
  comments: ScammerCommentDocument[];
6
+ /** Admin-curated cross-links — confirmed to be the same person under a different alias. */
6
7
  relatedScammers: ScammerDocument[];
8
+ /** Other verified profiles with the same scamType — pattern similarity only, never identity. */
9
+ similarScamReports: ScammerDocument[];
7
10
  }
8
11
  export interface ScammerListResult {
9
12
  items: ScammerDocument[];
@@ -57,12 +57,13 @@ export async function getScammerProfilePageData(id) {
57
57
  const scammer = await getPublicScammerById(id);
58
58
  if (!scammer)
59
59
  return null;
60
- const [incidents, comments, relatedScammers] = await Promise.all([
60
+ const [incidents, comments, relatedScammers, similarScamReports] = await Promise.all([
61
61
  scammerRepository.listPublicIncidents(scammer.id),
62
62
  scammerRepository.listPublicComments(scammer.id),
63
63
  scammerRepository.findManyById(scammer.relatedScammerIds),
64
+ scammerRepository.findBySameType(scammer.scamType, scammer.id),
64
65
  ]);
65
- return { scammer, incidents, comments, relatedScammers };
66
+ return { scammer, incidents, comments, relatedScammers, similarScamReports };
66
67
  }
67
68
  const TRUST_STATUS_CLEAR = { status: "clear", matchedProfileSlugs: [] };
68
69
  /**
@@ -7,5 +7,6 @@ export interface ScamProfileViewProps {
7
7
  incidents?: ScammerIncidentDocument[];
8
8
  comments?: ScammerCommentDocument[];
9
9
  relatedScammers?: ScammerDocument[];
10
+ similarScamReports?: ScammerDocument[];
10
11
  }
11
- export declare function ScamProfileView({ scammer, isAuthenticated, incidents, comments, relatedScammers, }: ScamProfileViewProps): React.JSX.Element;
12
+ export declare function ScamProfileView({ scammer, isAuthenticated, incidents, comments, relatedScammers, similarScamReports, }: ScamProfileViewProps): React.JSX.Element;
@@ -46,7 +46,7 @@ function ScammerCommentsSection({ comments, isAuthenticated, scammerId, }) {
46
46
  function ScammerActionsColumn({ scammer, isAuthenticated, reportHref, contestLoginHref, }) {
47
47
  return (_jsxs(Stack, { gap: "md", children: [_jsxs(Card, { variant: "elevated", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Actions" }) }), _jsx(CardBody, { children: _jsxs(Stack, { gap: "sm", children: [isAuthenticated ? (_jsxs(Link, { href: reportHref, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Report another incident"] })) : (_jsxs(Link, { href: `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(reportHref)}`, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Sign in to report an incident"] })), isAuthenticated ? (_jsxs(Link, { href: `/scams/${scammer.id}/contest`, className: CLS_OUTLINE_BTN, children: [_jsx(Flag, { className: "h-4 w-4 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }), "Contest this profile"] })) : (_jsxs(Link, { href: contestLoginHref, className: "appkit-button appkit-button--ghost appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]", children: [_jsx(Flag, { className: "h-4 w-4" }), "Sign in to contest"] })), _jsx(Text, { variant: "secondary", size: "xs", children: "All contest submissions are reviewed by our moderation team before any changes are made." })] }) })] }), _jsxs(Card, { variant: "outlined", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Contest Options" }) }), _jsx(CardBody, { children: _jsx(Stack, { gap: "xs", as: "ul", children: Object.entries(CONTEST_TYPE_LABELS).map(([, label]) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { className: "mt-1.5 h-1.5 w-1.5 shrink-0 bg-[color:var(--appkit-color-border,theme(colors.zinc.300))]", rounded: "full" }), _jsx(Text, { variant: "secondary", size: "xs", children: label })] }, label))) }) })] }), scammer.verifiedAt && (_jsx(Alert, { variant: "success", compact: true, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Shield, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs(Text, { size: "xs", weight: "medium", children: ["Verified by LetItRip moderation on ", formatDate(scammer.verifiedAt)] })] }) }))] }));
48
48
  }
49
- export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comments = [], relatedScammers = [], }) {
49
+ export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comments = [], relatedScammers = [], similarScamReports = [], }) {
50
50
  const reportHref = String(ROUTES.PUBLIC.SCAM_REPORT);
51
51
  const registryHref = String(ROUTES.PUBLIC.SCAMS);
52
52
  const contestLoginHref = `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(`/scams/${scammer.id}`)}`;
@@ -55,5 +55,5 @@ export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comm
55
55
  { label: "Home", href: "/" },
56
56
  { label: "Scam Registry", href: registryHref },
57
57
  { label: scammer.displayNames[0] ?? "Profile" },
58
- ] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Grid, { cols: "twoThird", gap: "lg", children: [_jsxs(Stack, { gap: "lg", children: [_jsx(ScammerHeaderCard, { scammer: scammer }), (scammer.phones.length > 0 || scammer.upiIds.length > 0 || scammer.emails.length > 0) && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Contact Identifiers" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [scammer.phones.map((p) => (_jsx(IdentityChip, { label: "Phone", value: p, icon: _jsx(Phone, { className: "h-4 w-4" }) }, p))), scammer.upiIds.map((u) => (_jsx(IdentityChip, { label: "UPI ID", value: u, icon: _jsx(Wallet, { className: "h-4 w-4" }) }, u))), scammer.emails.map((e) => (_jsx(IdentityChip, { label: "Email", value: e, icon: _jsx(Mail, { className: "h-4 w-4" }) }, e)))] })] })), scammer.socialMedia.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Social Media Profiles" }), _jsx(Stack, { gap: "xs", children: scammer.socialMedia.map((sm, i) => (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(ExternalLink, { className: "h-4 w-4 shrink-0 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" }), _jsxs(Text, { size: "sm", weight: "medium", children: [SOCIAL_PLATFORM_LABELS[sm.platform] ?? sm.platform, ":"] }), sm.url ? (_jsx(Anchor, { href: sm.url, size: "sm", children: sm.handle })) : (_jsx(Text, { variant: "secondary", size: "sm", children: sm.handle }))] }, i))) })] })), scammer.itemInvolved && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Item Involved" }), _jsx(Text, { variant: "secondary", size: "sm", children: scammer.itemInvolved })] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "What Happened" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Text, { className: "leading-relaxed", size: "sm", children: scammer.description }) })] }), scamTypeDef && scamTypeDef.howToAvoid.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "How to Avoid This Scam" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Stack, { gap: "xs", as: "ol", children: scamTypeDef.howToAvoid.map((tip, i) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "h-5 w-5 shrink-0 bg-[color:var(--appkit-color-success,theme(colors.green.600))]/10 text-[color:var(--appkit-color-success,theme(colors.green.700))]", rounded: "full", children: i + 1 }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: tip })] }, i))) }) })] })), scammer.evidence.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Evidence" }), _jsx(Row, { gap: "sm", wrap: true, children: scammer.evidence.map((url, i) => (_jsx(Anchor, { href: url, underline: "none", rounded: "lg", shadow: "sm", className: "block h-32 w-32 overflow-hidden border hover:opacity-90", children: _jsx(MediaImage, { src: url, alt: `Evidence ${i + 1}`, size: "thumbnail" }) }, i))) })] })), _jsx(ScammerIncidentsSection, { incidents: incidents }), _jsx(ScammerCommentsSection, { comments: comments, isAuthenticated: isAuthenticated, scammerId: scammer.id }), relatedScammers.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Related Profiles" }), _jsx(Stack, { gap: "sm", children: relatedScammers.map((rel) => (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(rel.id)), className: "block", children: _jsx(Card, { variant: "outlined", padding: "sm", className: "hover:opacity-80 transition-opacity", children: _jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", children: rel.displayNames[0] }), _jsx(Text, { variant: "secondary", size: "xs", children: SCAM_TYPE_LABELS[rel.scamType] ?? rel.scamType })] }), _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Badge, { variant: statusVariant(rel.status), children: SCAMMER_STATUS_LABELS[rel.status] ?? rel.status }), _jsx(Link2, { className: "h-4 w-4 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" })] })] }) }) }, rel.id))) })] }))] }), _jsx(ScammerActionsColumn, { scammer: scammer, isAuthenticated: isAuthenticated, reportHref: reportHref, contestLoginHref: contestLoginHref })] }) }) })] }));
58
+ ] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Grid, { cols: "twoThird", gap: "lg", children: [_jsxs(Stack, { gap: "lg", children: [_jsx(ScammerHeaderCard, { scammer: scammer }), (scammer.phones.length > 0 || scammer.upiIds.length > 0 || scammer.emails.length > 0) && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Contact Identifiers" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [scammer.phones.map((p) => (_jsx(IdentityChip, { label: "Phone", value: p, icon: _jsx(Phone, { className: "h-4 w-4" }) }, p))), scammer.upiIds.map((u) => (_jsx(IdentityChip, { label: "UPI ID", value: u, icon: _jsx(Wallet, { className: "h-4 w-4" }) }, u))), scammer.emails.map((e) => (_jsx(IdentityChip, { label: "Email", value: e, icon: _jsx(Mail, { className: "h-4 w-4" }) }, e)))] })] })), scammer.socialMedia.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Social Media Profiles" }), _jsx(Stack, { gap: "xs", children: scammer.socialMedia.map((sm, i) => (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(ExternalLink, { className: "h-4 w-4 shrink-0 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" }), _jsxs(Text, { size: "sm", weight: "medium", children: [SOCIAL_PLATFORM_LABELS[sm.platform] ?? sm.platform, ":"] }), sm.url ? (_jsx(Anchor, { href: sm.url, size: "sm", children: sm.handle })) : (_jsx(Text, { variant: "secondary", size: "sm", children: sm.handle }))] }, i))) })] })), scammer.itemInvolved && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Item Involved" }), _jsx(Text, { variant: "secondary", size: "sm", children: scammer.itemInvolved })] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "What Happened" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Text, { className: "leading-relaxed", size: "sm", children: scammer.description }) })] }), scamTypeDef && scamTypeDef.howToAvoid.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "How to Avoid This Scam" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Stack, { gap: "xs", as: "ol", children: scamTypeDef.howToAvoid.map((tip, i) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "h-5 w-5 shrink-0 bg-[color:var(--appkit-color-success,theme(colors.green.600))]/10 text-[color:var(--appkit-color-success,theme(colors.green.700))]", rounded: "full", children: i + 1 }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: tip })] }, i))) }) })] })), scammer.evidence.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Evidence" }), _jsx(Row, { gap: "sm", wrap: true, children: scammer.evidence.map((url, i) => (_jsx(Anchor, { href: url, underline: "none", rounded: "lg", shadow: "sm", className: "block h-32 w-32 overflow-hidden border hover:opacity-90", children: _jsx(MediaImage, { src: url, alt: `Evidence ${i + 1}`, size: "thumbnail" }) }, i))) })] })), _jsx(ScammerIncidentsSection, { incidents: incidents }), _jsx(ScammerCommentsSection, { comments: comments, isAuthenticated: isAuthenticated, scammerId: scammer.id }), relatedScammers.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Related Profiles" }), _jsx(Stack, { gap: "sm", children: relatedScammers.map((rel) => (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(rel.id)), className: "block", children: _jsx(Card, { variant: "outlined", padding: "sm", className: "hover:opacity-80 transition-opacity", children: _jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", children: rel.displayNames[0] }), _jsx(Text, { variant: "secondary", size: "xs", children: SCAM_TYPE_LABELS[rel.scamType] ?? rel.scamType })] }), _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Badge, { variant: statusVariant(rel.status), children: SCAMMER_STATUS_LABELS[rel.status] ?? rel.status }), _jsx(Link2, { className: "h-4 w-4 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" })] })] }) }) }, rel.id))) })] })), similarScamReports.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Similar Scam Reports" }), _jsxs(Text, { variant: "secondary", size: "xs", children: ["Other profiles reported for the same scam pattern (", SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType, "). This does not mean they are the same person."] }), _jsx(Stack, { gap: "sm", children: similarScamReports.map((rel) => (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(rel.id)), className: "block", children: _jsx(Card, { variant: "outlined", padding: "sm", className: "hover:opacity-80 transition-opacity", children: _jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", children: rel.displayNames[0] }), _jsx(Text, { variant: "secondary", size: "xs", children: SCAM_TYPE_LABELS[rel.scamType] ?? rel.scamType })] }), _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Badge, { variant: statusVariant(rel.status), children: SCAMMER_STATUS_LABELS[rel.status] ?? rel.status }), _jsx(Link2, { className: "h-4 w-4 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" })] })] }) }) }, rel.id))) })] }))] }), _jsx(ScammerActionsColumn, { scammer: scammer, isAuthenticated: isAuthenticated, reportHref: reportHref, contestLoginHref: contestLoginHref })] }) }) })] }));
59
59
  }
@@ -61,6 +61,12 @@ declare class ScammerRepository extends BaseRepository<ScammerDocument> {
61
61
  findByContactField(field: "phones" | "upiIds" | "emails" | "displayNames", value: string): Promise<ScammerDocument[]>;
62
62
  listPublicIncidents(scammerId: string): Promise<ScammerIncidentDocument[]>;
63
63
  listPublicComments(scammerId: string): Promise<ScammerCommentDocument[]>;
64
+ /**
65
+ * Other verified profiles with the same scamType — a "similar scam pattern"
66
+ * discovery signal, distinct from `relatedScammerIds` (which links profiles
67
+ * confirmed to be the same person). Never implies identity, only pattern similarity.
68
+ */
69
+ findBySameType(scamType: string, excludeId: string, limit?: number): Promise<ScammerDocument[]>;
64
70
  findManyById(ids: string[]): Promise<ScammerDocument[]>;
65
71
  }
66
72
  declare const scammerRepository: ScammerRepository;
@@ -142,6 +142,30 @@ class ScammerRepository extends BaseRepository {
142
142
  return [];
143
143
  }
144
144
  }
145
+ /**
146
+ * Other verified profiles with the same scamType — a "similar scam pattern"
147
+ * discovery signal, distinct from `relatedScammerIds` (which links profiles
148
+ * confirmed to be the same person). Never implies identity, only pattern similarity.
149
+ */
150
+ async findBySameType(scamType, excludeId, limit = 5) {
151
+ try {
152
+ const snap = await this.getCollection()
153
+ .where(SCAMMER_FIELDS.STATUS, "==", "verified")
154
+ .where(SCAMMER_FIELDS.SCAM_TYPE, "==", scamType)
155
+ .orderBy(SCAMMER_FIELDS.CREATED_AT, "desc")
156
+ .limit(limit + 1)
157
+ .get();
158
+ return snap.docs
159
+ .map((d) => this.mapDoc(d))
160
+ .filter((s) => s.id !== excludeId)
161
+ .slice(0, limit);
162
+ }
163
+ catch (err) {
164
+ void normalizeError(err);
165
+ serverLogger.warn("scammer-repo: findBySameType failed — returning empty", { scamType, error: err instanceof Error ? err.message : String(err) });
166
+ return [];
167
+ }
168
+ }
145
169
  async findManyById(ids) {
146
170
  if (!ids.length)
147
171
  return [];
@@ -15,6 +15,7 @@ import { userRepository } from "../../auth/repository/user.repository";
15
15
  import { storeRepository } from "../../stores/repository/store.repository";
16
16
  import { productRepository } from "../../products/repository/products.repository";
17
17
  import { ProductStatusValues } from "../../products/schemas";
18
+ import { assertPrizeDrawNotLocked, assertPrizeDrawWonItemsImmutable, } from "../../../_internal/server/features/products/service";
18
19
  import { orderRepository } from "../../orders/repository/orders.repository";
19
20
  import { OrderStatusValues } from "../../orders/schemas";
20
21
  import { payoutRepository } from "../../payments/repository/payout.repository";
@@ -485,6 +486,12 @@ export async function sellerUpdateProduct(userId, userRole, productId, input) {
485
486
  if (!store || existing.storeId !== store.id)
486
487
  throw new AuthorizationError("You do not own this product");
487
488
  }
489
+ if (typeof input.status === "string" &&
490
+ input.status !== "published" &&
491
+ existing.status === "published") {
492
+ assertPrizeDrawNotLocked(existing, "unpublished or archived");
493
+ }
494
+ assertPrizeDrawWonItemsImmutable(existing, input.prizeDrawItems);
488
495
  const finalizedData = await finalizeProductMediaReferences(input);
489
496
  const updated = await productRepository.updateProduct(productId, finalizedData);
490
497
  serverLogger.info("sellerUpdateProduct", { userId, productId });
@@ -500,6 +507,7 @@ export async function sellerDeleteProduct(userId, userRole, productId) {
500
507
  if (!store || existing.storeId !== store.id)
501
508
  throw new AuthorizationError("You do not own this product");
502
509
  }
510
+ assertPrizeDrawNotLocked(existing, "deleted");
503
511
  await productRepository.delete(productId);
504
512
  serverLogger.info("sellerDeleteProduct", { userId, productId });
505
513
  }
@@ -132,6 +132,10 @@ export function SellerPrizeDrawsView({ children, onDelete, ...props }) {
132
132
  label: ACTIONS.STORE["edit-listing"].label,
133
133
  onClick: () => void dispatch({ type: "NAVIGATE", href: String(ROUTES.STORE.PRIZE_DRAWS_EDIT(row.id)) }),
134
134
  },
135
+ {
136
+ label: "View Entries",
137
+ onClick: () => void dispatch({ type: "NAVIGATE", href: String(ROUTES.STORE.PRIZE_DRAW_ENTRIES(row.id)) }),
138
+ },
135
139
  ...(onDelete
136
140
  ? [
137
141
  {
@@ -8,6 +8,7 @@ import { MediaImage } from "../../media/MediaImage";
8
8
  import { BottomSheet } from "../../layout/BottomSheet";
9
9
  import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
10
10
  import { useSidebarSearch } from "../../../_internal/client/features/layout/useSidebarSearch";
11
+ import { findActiveNavGroup, findActiveNavItem } from "../../../_internal/client/features/layout/navActive";
11
12
  const __O = {
12
13
  hidden: "overflow-hidden",
13
14
  yAuto: "overflow-y-auto",
@@ -18,26 +19,36 @@ const CLS_STORE_NAME = "text-[length:var(--appkit-text-sm)] font-semibold text-[
18
19
  const CLS_NAV_ACTIVE = "bg-warning-surface dark:bg-warning-surface text-warning dark:text-warning";
19
20
  const CLS_NAV_BADGE = "shrink-0 rounded-full bg-warning-surface px-[var(--appkit-space-1-5)] py-[var(--appkit-space-0-5)] text-[10px] text-white leading-none";
20
21
  const CLS_NAV_ICON_ACTIVE = "text-warning dark:text-warning";
21
- function isNavItemActive(item, activeHref) {
22
- return activeHref === item.href;
23
- }
24
22
  function NavLink({ item, isActive, onClick }) {
25
23
  return (_jsxs(Link, { href: item.href, onClick: onClick, className: `flex items-center justify-end gap-[var(--appkit-space-2-5)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[0.8125rem] font-medium leading-tight transition-colors ${isActive
26
24
  ? CLS_NAV_ACTIVE
27
25
  : "text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/60 hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "truncate", children: item.label }), item.badge != null && item.badge > 0 && (_jsx(Span, { weight: "bold", className: CLS_NAV_BADGE, children: item.badge }))] }));
28
26
  }
29
27
  function FlatContent({ items, activeHref, storeName, storeLogoURL, onItemClick, }) {
28
+ const activeItem = findActiveNavItem(items, activeHref);
30
29
  return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsx(Nav, { "aria-label": "Store navigation", padding: "y-sm", children: _jsx(Ul, { paddingX: "x-sm", spacing: "2xs", children: items.map((item) => {
31
- const isActive = activeHref === item.href;
30
+ const isActive = activeItem?.href === item.href;
32
31
  return (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: isActive, onClick: onItemClick }) }, item.href));
33
32
  }) }) })] }));
34
33
  }
35
34
  function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClick, }) {
36
- // Accordion — only one group open at a time.
35
+ // Accordion — only one group open at a time. Initial pick: the group
36
+ // containing the active path, else the first group with defaultOpen.
37
37
  const [openGroup, setOpenGroup] = useState(() => {
38
- const match = groups.find((g) => g.defaultOpen ?? g.items.some((i) => activeHref === i.href));
39
- return match?.title ?? null;
38
+ const active = findActiveNavGroup(groups, activeHref);
39
+ if (active)
40
+ return active.title;
41
+ return groups.find((g) => g.defaultOpen)?.title ?? null;
40
42
  });
43
+ // Re-sync whenever the route changes — the sidebar stays mounted across
44
+ // client-side navigation, so the lazy initializer above only fires once.
45
+ useEffect(() => {
46
+ const active = findActiveNavGroup(groups, activeHref);
47
+ if (active)
48
+ setOpenGroup(active.title);
49
+ // eslint-disable-next-line react-hooks/exhaustive-deps
50
+ }, [activeHref]);
51
+ const activeItem = findActiveNavItem(groups.flatMap((g) => g.items), activeHref);
41
52
  const { query, setQuery, isSearching, filteredGroups } = useSidebarSearch(groups);
42
53
  const toggle = useCallback((title) => {
43
54
  if (isSearching)
@@ -46,10 +57,10 @@ function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClic
46
57
  }, [isSearching]);
47
58
  return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsxs(Nav, { "aria-label": "Store navigation", padding: "y-xs", children: [_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search navigation\u2026", "aria-label": "Search navigation" }) }), isSearching && filteredGroups.length === 0 && (_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsxs(Span, { size: "xs", color: "muted", children: ["No matches for \u201C", query, "\u201D."] }) })), filteredGroups.map((group) => {
48
59
  const isOpen = isSearching || openGroup === group.title;
49
- const hasActive = group.items.some((i) => activeHref === i.href);
60
+ const hasActive = !!findActiveNavItem(group.items, activeHref);
50
61
  return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
51
62
  ? CLS_NAV_ICON_ACTIVE
52
- : "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: isNavItemActive(item, activeHref), onClick: onItemClick }) }, item.href))) }))] }, group.title));
63
+ : "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: activeItem?.href === item.href, onClick: onItemClick }) }, item.href))) }))] }, group.title));
53
64
  })] })] }));
54
65
  }
55
66
  function DrawerPanel({ title, onClose, children }) {
@@ -120,15 +120,15 @@ export async function StoreDetailLayoutView({ storeSlug, activeTab, children, sc
120
120
  const lt = TAB_LISTING_TYPE[tab.id];
121
121
  return lt ? isListingTypeEnabled(lt, settings) : true;
122
122
  });
123
+ const dropdownTabs = visibleStoreTabs.map((tab) => ({
124
+ value: tab.id,
125
+ label: tabLabel(tab.label, listingCounts[tab.id]),
126
+ href: STORE_LISTING_HREF[tab.id](storeSlug),
127
+ }));
123
128
  const tabs = [
124
- ...visibleStoreTabs.map((tab) => ({
125
- value: tab.id,
126
- label: tabLabel(tab.label, listingCounts[tab.id]),
127
- href: STORE_LISTING_HREF[tab.id](storeSlug),
128
- })),
129
129
  { value: "coupons", label: tabLabel("Coupons", couponsCount), href: String(ROUTES.PUBLIC.STORE_COUPONS(storeSlug)) },
130
130
  { value: "reviews", label: tabLabel("Reviews", reviewsCount), href: String(ROUTES.PUBLIC.STORE_REVIEWS(storeSlug)) },
131
131
  { value: "about", label: "About", href: String(ROUTES.PUBLIC.STORE_ABOUT(storeSlug)) },
132
132
  ];
133
- return (_jsxs(Main, { children: [_jsx(StoreHeader, { store: store, trust: trust }), _jsxs(Container, { size: "xl", className: "mt-6", children: [_jsx(StoreNavTabs, { tabs: tabs, activeValue: activeTab }), _jsx(Section, { padding: "t-lg", children: children })] })] }));
133
+ return (_jsxs(Main, { children: [_jsx(StoreHeader, { store: store, trust: trust }), _jsxs(Container, { size: "xl", className: "mt-6", children: [_jsx(StoreNavTabs, { dropdownTabs: dropdownTabs, dropdownPlaceholder: "Browse listings", tabs: tabs, activeValue: activeTab }), _jsx(Section, { padding: "t-lg", children: children })] })] }));
134
134
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen, } from "lucide-react";
2
+ import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen, MessageSquare, } from "lucide-react";
3
3
  import { Div, Heading, Progress, Row, Section, Stack, Text, TextLink } from "../../../ui";
4
4
  import { ROUTES } from "../../../next/routing/route-map";
5
5
  const CLS_CHECK_ICON = "w-5 h-5 flex-shrink-0 text-emerald-500";
@@ -36,6 +36,12 @@ const GUIDE_CARDS = [
36
36
  description: "Admin-granted feature flags — auctions, pre-orders, lower commissions, and more.",
37
37
  href: String(ROUTES.STORE.GUIDE_CAPABILITIES),
38
38
  },
39
+ {
40
+ Icon: MessageSquare,
41
+ title: "WhatsApp Catalog Sync",
42
+ description: "Push your product catalog to Meta Commerce Manager for WhatsApp, Facebook, and Instagram Shops.",
43
+ href: String(ROUTES.STORE.GUIDE_WHATSAPP),
44
+ },
39
45
  {
40
46
  Icon: Headphones,
41
47
  title: "Support",
@@ -6,10 +6,19 @@ export interface StoreTab {
6
6
  }
7
7
  export interface StoreNavTabsProps {
8
8
  tabs: StoreTab[];
9
+ /**
10
+ * Listing-type tabs (Products/Auctions/Pre-Orders/.../Art & Stickers) —
11
+ * rendered as a single dropdown ahead of `tabs` instead of one row entry
12
+ * each, so the bar can't overflow no matter how many listing types are
13
+ * enabled. Picking an option navigates to its `href`.
14
+ */
15
+ dropdownTabs?: StoreTab[];
16
+ /** Label shown in the dropdown when the active tab isn't a listing type (e.g. on Reviews/About). */
17
+ dropdownPlaceholder?: string;
9
18
  activeValue?: string;
10
19
  onTabChange?: (value: string) => void;
11
20
  /** Render-prop for full custom tab bar */
12
21
  renderTabBar?: (tabs: StoreTab[], activeValue: string | undefined, onChange: (v: string) => void) => React.ReactNode;
13
22
  className?: string;
14
23
  }
15
- export declare function StoreNavTabs({ tabs, activeValue, onTabChange, renderTabBar, className, }: StoreNavTabsProps): React.JSX.Element;
24
+ export declare function StoreNavTabs({ tabs, dropdownTabs, dropdownPlaceholder, activeValue, onTabChange, renderTabBar, className, }: StoreNavTabsProps): React.JSX.Element;
@@ -1,13 +1,26 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { Button, Div, TextLink } from "../../../ui";
3
- export function StoreNavTabs({ tabs, activeValue, onTabChange, renderTabBar, className = "", }) {
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRouter } from "next/navigation";
4
+ import { Button, Div, Select, TextLink } from "../../../ui";
5
+ const CLS_ACTIVE_TAB = "border-primary text-primary";
6
+ export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "Browse listings", activeValue, onTabChange, renderTabBar, className = "", }) {
7
+ const router = useRouter();
4
8
  const handleChange = (v) => onTabChange?.(v);
5
9
  if (renderTabBar) {
6
- return _jsx(_Fragment, { children: renderTabBar(tabs, activeValue, handleChange) });
10
+ return _jsx(_Fragment, { children: renderTabBar([...dropdownTabs, ...tabs], activeValue, handleChange) });
7
11
  }
8
- return (_jsx(Div, { layout: "flex", gap: "2", role: "tablist", border: "bottom", className: `overflow-x-auto ${className}`, children: tabs.map((tab) => tab.href ? (_jsx(TextLink, { variant: "bare", href: tab.href, role: "tab", "aria-selected": activeValue === tab.value, paddingX: "md", paddingY: "xs", size: "sm", weight: "medium", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
9
- ? "border-primary text-primary"
10
- : "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value)) : (_jsx(Button, { variant: "ghost", type: "button", role: "tab", "aria-selected": activeValue === tab.value, onClick: () => handleChange(tab.value), paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", rounded: "none", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
11
- ? "border-primary text-primary"
12
- : "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value))) }));
12
+ const activeDropdownTab = dropdownTabs.find((tab) => tab.value === activeValue);
13
+ const handleDropdownChange = (value) => {
14
+ const tab = dropdownTabs.find((t) => t.value === value);
15
+ if (!tab)
16
+ return;
17
+ if (tab.href)
18
+ router.push(tab.href);
19
+ handleChange(tab.value);
20
+ };
21
+ return (_jsxs(Div, { layout: "flex", gap: "2", align: "center", role: "tablist", border: "bottom", className: `overflow-x-auto ${className}`, children: [dropdownTabs.length > 0 && (_jsx(Select, { bare: true, "aria-label": dropdownPlaceholder, options: dropdownTabs.map((tab) => ({ value: tab.value, label: tab.label })), value: activeDropdownTab?.value ?? "", placeholder: activeDropdownTab ? undefined : dropdownPlaceholder, onValueChange: handleDropdownChange, className: `max-w-[12rem] flex-shrink-0 whitespace-nowrap ${activeDropdownTab ? CLS_ACTIVE_TAB : ""}` })), tabs.map((tab) => tab.href ? (_jsx(TextLink, { variant: "bare", href: tab.href, role: "tab", "aria-selected": activeValue === tab.value, paddingX: "md", paddingY: "xs", size: "sm", weight: "medium", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
22
+ ? CLS_ACTIVE_TAB
23
+ : "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value)) : (_jsx(Button, { variant: "ghost", type: "button", role: "tab", "aria-selected": activeValue === tab.value, onClick: () => handleChange(tab.value), paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", rounded: "none", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
24
+ ? CLS_ACTIVE_TAB
25
+ : "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value)))] }));
13
26
  }
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ export type StoreWhatsAppGuideViewProps = Record<string, never>;
3
+ export declare function StoreWhatsAppGuideView(_props: StoreWhatsAppGuideViewProps): React.JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { Alert, Code, Div, Heading, Li, Section, Span, Stack, Text, TextLink, Ul } from "../../../ui";
4
+ import { ROUTES } from "../../../next/routing/route-map";
5
+ function GuideSection({ title, children, }) {
6
+ return (_jsxs(Section, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "xl", shadow: "sm", padding: "lg", children: [_jsx(Heading, { level: 2, className: "mb-4 text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: title }), children] }));
7
+ }
8
+ const LIST_DISC = "list-disc leading-relaxed";
9
+ const TEXT_MUTED = "leading-relaxed text-[var(--appkit-color-text-muted)]";
10
+ export function StoreWhatsAppGuideView(_props) {
11
+ return (_jsxs(Stack, { gap: "lg", padding: "b-2xl", children: [_jsxs(TextLink, { variant: "bare", href: String(ROUTES.STORE.GUIDE), size: "sm", layout: "inline-flex", align: "center", gap: "sm", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Seller Guide"] }), _jsxs(Div, { children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "2xl", weight: "bold", children: "WhatsApp Catalog Sync" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Push your product catalog to Meta's Commerce Manager for WhatsApp catalog messages and Facebook/Instagram Shops \u2014 and import products back the other way." })] }), _jsx(GuideSection, { title: "1. Create a Commerce Manager catalog", children: _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsxs(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: ["At ", _jsx(Span, { weight: "bold", children: "business.facebook.com/commerce" }), ", create a new catalog for the store (or use an existing one)."] }), _jsxs(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: ["Note the ", _jsx(Span, { weight: "bold", children: "Catalog ID" }), " shown in the catalog's settings."] })] }) }), _jsx(GuideSection, { title: "2. Generate a catalog-scoped access token", children: _jsxs(Text, { className: TEXT_MUTED, size: "sm", children: ["You need a Meta access token with ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "catalog_management" }), " permission, scoped to that catalog. This is generated the same way as the platform-level WhatsApp System User token (ask your admin \u2014 Admin Guide \u2192 WhatsApp Integration, step 3) but assigned to the ", _jsx(Span, { weight: "bold", children: "catalog" }), " asset instead of the WhatsApp Business Account asset."] }) }), _jsxs(GuideSection, { title: "3. Enter credentials in the seller dashboard", children: [_jsxs(Text, { className: "mb-2 leading-relaxed text-[var(--appkit-color-text-muted)]", size: "sm", children: [_jsx(Span, { weight: "bold", children: "Store \u2192 WhatsApp" }), " settings page:"] }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsx(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: "Catalog ID" }), _jsx(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: "Access Token" })] }), _jsxs(Text, { className: "mt-3 leading-relaxed text-[var(--appkit-color-text-muted)]", size: "sm", children: ["This is a store capability \u2014 it must be enabled for the store (", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "whatsapp_catalog_sync" }), " capability) before the settings page and sync actions are available."] })] }), _jsx(GuideSection, { title: "4. Push products to the catalog", children: _jsxs(Text, { className: TEXT_MUTED, size: "sm", children: ["Once connected, trigger a sync that batches published ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "standard" }), "-listing products (up to 50 per API call) to the Meta catalog via the Commerce API ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "items_batch" }), " endpoint \u2014 title, description, price, image, availability, and condition."] }) }), _jsx(GuideSection, { title: "5. Import products from the catalog", children: _jsxs(Text, { className: TEXT_MUTED, size: "sm", children: ["The reverse flow fetches products already in the Meta catalog and creates them as local ", _jsx(Span, { weight: "bold", children: "draft" }), " listings, deduplicating against existing products by slug/retailer-id matching."] }) }), _jsxs(Alert, { variant: "info", children: ["This is unrelated to the platform-level WhatsApp order-notification credentials \u2014 those are for sending order-status messages to buyers, not catalog sync. Only published ", _jsx(Span, { weight: "bold", children: "standard" }), " listings sync \u2014 auctions, pre-orders, and other listing types are not included."] })] }));
12
+ }
@@ -47,3 +47,5 @@ export { StoreCapabilitiesGuideView } from "./StoreCapabilitiesGuideView";
47
47
  export type { StoreCapabilitiesGuideViewProps } from "./StoreCapabilitiesGuideView";
48
48
  export { StoreSettingsGuideView } from "./StoreSettingsGuideView";
49
49
  export type { StoreSettingsGuideViewProps } from "./StoreSettingsGuideView";
50
+ export { StoreWhatsAppGuideView } from "./StoreWhatsAppGuideView";
51
+ export type { StoreWhatsAppGuideViewProps } from "./StoreWhatsAppGuideView";
@@ -20,3 +20,4 @@ export { StoreOrdersGuideView } from "./StoreOrdersGuideView";
20
20
  export { StoreFinanceGuideView } from "./StoreFinanceGuideView";
21
21
  export { StoreCapabilitiesGuideView } from "./StoreCapabilitiesGuideView";
22
22
  export { StoreSettingsGuideView } from "./StoreSettingsGuideView";
23
+ export { StoreWhatsAppGuideView } from "./StoreWhatsAppGuideView";
@@ -1 +1,2 @@
1
1
  export * from "./checklist-item-actions";
2
+ export * from "./leaderboard-actions";
@@ -1 +1,2 @@
1
1
  export * from "./checklist-item-actions";
2
+ export * from "./leaderboard-actions";
@@ -0,0 +1,3 @@
1
+ import type { BugHunterLeaderboardEntry } from "../schemas/firestore";
2
+ /** Ranks testers by confirmed-bug count. Mirrors getEventLeaderboard()'s shape. */
3
+ export declare function getBugHunterLeaderboard(limit?: number): Promise<BugHunterLeaderboardEntry[]>;
@@ -0,0 +1,5 @@
1
+ import { testerChecklistItemRepository } from "../repository/tester-checklist-item.repository";
2
+ /** Ranks testers by confirmed-bug count. Mirrors getEventLeaderboard()'s shape. */
3
+ export async function getBugHunterLeaderboard(limit = 50) {
4
+ return testerChecklistItemRepository.getBugHunterLeaderboard(limit);
5
+ }
@@ -1,18 +1,40 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useQuery } from "@tanstack/react-query";
4
- import { Card, Details, Div, Stack, Summary, Text } from "../../../ui";
3
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { Button, Card, Details, Div, Row, Stack, Summary, Text } from "../../../ui";
5
5
  import { MediaImage } from "../../media/MediaImage";
6
6
  import { apiClient } from "../../../http";
7
+ import { useApiMutation } from "../../../client";
7
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
10
+ const REPORT_QUERY_KEY = ["admin", "tester-feedback", "report"];
8
11
  export function AdminTesterFeedbackIssuesView() {
12
+ const queryClient = useQueryClient();
9
13
  const query = useQuery({
10
- queryKey: ["admin", "tester-feedback", "report"],
14
+ queryKey: REPORT_QUERY_KEY,
11
15
  queryFn: async () => {
12
16
  const res = await apiClient.get(ADMIN_ENDPOINTS.TESTER_FEEDBACK_REPORT);
13
17
  return res?.data ?? res;
14
18
  },
15
19
  });
20
+ const confirmBugMutation = useApiMutation({
21
+ mutationFn: async (responseId) => {
22
+ await apiClient.post(ADMIN_ENDPOINTS.TESTER_FEEDBACK_CONFIRM_BUG(responseId), {});
23
+ },
24
+ successMessage: "Bug confirmed and credited.",
25
+ onSuccess: () => {
26
+ queryClient.invalidateQueries({ queryKey: REPORT_QUERY_KEY });
27
+ },
28
+ });
29
+ const reopenMutation = useApiMutation({
30
+ mutationFn: async (checklistItemId) => {
31
+ await apiClient.post(ADMIN_ENDPOINTS.TESTER_CHECKLIST_ITEM_REOPEN(checklistItemId), {});
32
+ },
33
+ successMessage: "Case reopened for retest.",
34
+ onSuccess: () => {
35
+ queryClient.invalidateQueries({ queryKey: REPORT_QUERY_KEY });
36
+ },
37
+ });
16
38
  const issues = query.data?.issues ?? [];
17
39
  if (query.isLoading)
18
40
  return _jsx(Text, { color: "muted", children: "Loading issues\u2026" });
@@ -28,6 +50,6 @@ export function AdminTesterFeedbackIssuesView() {
28
50
  const sortedPhases = Array.from(byPhase.keys()).sort((a, b) => a - b);
29
51
  return (_jsx(Stack, { gap: "md", children: sortedPhases.map((phase) => {
30
52
  const phaseIssues = byPhase.get(phase);
31
- return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" }))] }) }, issue.id))) }) })] }, phase));
53
+ return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" })), issue.bugConfirmed ? (_jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "success", children: ["\uD83D\uDC1B Confirmed \u2014 credited to ", issue.bugHunterName ?? "unknown tester"] }), !issue.supersededByItemId && (_jsx(Button, { size: "sm", action: ACTIONS.ADMIN["reopen-checklist-item"], isLoading: reopenMutation.isPending, onClick: () => reopenMutation.mutate(issue.checklistItemId) }))] })) : (_jsx(Div, { children: _jsx(Button, { size: "sm", action: ACTIONS.ADMIN["confirm-bug"], isLoading: confirmBugMutation.isPending, onClick: () => confirmBugMutation.mutate(issue.id) }) }))] }) }, issue.id))) }) })] }, phase));
32
54
  }) }));
33
55
  }
@@ -47,6 +47,14 @@ export function AdminTesterFeedbackListView(_props) {
47
47
  queryClient.invalidateQueries({ queryKey: ["admin", "tester-feedback"] });
48
48
  },
49
49
  });
50
+ const confirmBugMutation = useApiMutation({
51
+ mutationFn: async (id) => {
52
+ await apiClient.post(ADMIN_ENDPOINTS.TESTER_FEEDBACK_CONFIRM_BUG(id), {});
53
+ },
54
+ onSuccess: () => {
55
+ queryClient.invalidateQueries({ queryKey: ["admin", "tester-feedback"] });
56
+ },
57
+ });
50
58
  const config = {
51
59
  portal: "admin",
52
60
  title: "All Submissions",
@@ -95,6 +103,11 @@ export function AdminTesterFeedbackListView(_props) {
95
103
  disabled: row.status === "reviewed",
96
104
  onClick: () => patchMutation.mutate(row.id),
97
105
  },
106
+ {
107
+ label: ACTIONS.ADMIN["confirm-bug"].label,
108
+ disabled: row.answer !== "no",
109
+ onClick: () => confirmBugMutation.mutate(row.id),
110
+ },
98
111
  ] })),
99
112
  };
100
113
  return _jsx(DataListingView, { config: config });
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import type { BugHunterLeaderboardEntry } from "../schemas/firestore";
3
+ export interface BugHunterLeaderboardViewProps {
4
+ isLoading?: boolean;
5
+ isEmpty?: boolean;
6
+ /** Render the full ranked list — caller provides entries via hook */
7
+ renderList?: () => React.ReactNode;
8
+ /** Render a single row; used when caller wants item-level control */
9
+ renderEntry?: (entry: BugHunterLeaderboardEntry, index: number) => React.ReactNode;
10
+ /** Entries — used only when renderEntry is provided */
11
+ entries?: BugHunterLeaderboardEntry[];
12
+ /** Empty state node */
13
+ renderEmpty?: () => React.ReactNode;
14
+ /** Loading spinner / skeleton */
15
+ renderSkeleton?: () => React.ReactNode;
16
+ labels?: {
17
+ title?: string;
18
+ noEntries?: string;
19
+ bugs?: string;
20
+ };
21
+ className?: string;
22
+ }
23
+ export declare function BugHunterLeaderboardView({ isLoading, isEmpty, renderList, renderEntry, entries, renderEmpty, renderSkeleton, labels, className, }: BugHunterLeaderboardViewProps): React.JSX.Element;