@mohasinac/appkit 2.6.1 → 2.6.2

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 (301) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +38 -0
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +75 -0
  3. package/dist/_internal/client/features/layout/RoleGuard.d.ts +15 -0
  4. package/dist/_internal/client/features/layout/RoleGuard.js +25 -0
  5. package/dist/_internal/client/features/layout/index.d.ts +5 -0
  6. package/dist/_internal/client/features/layout/index.js +4 -0
  7. package/dist/_internal/server/features/brands/actions.d.ts +3 -3
  8. package/dist/_internal/server/features/brands/actions.js +72 -5
  9. package/dist/_internal/server/features/brands/data.d.ts +8 -8
  10. package/dist/_internal/server/features/brands/data.js +10 -11
  11. package/dist/_internal/server/features/brands/service.d.ts +2 -2
  12. package/dist/_internal/server/features/brands/service.js +5 -5
  13. package/dist/_internal/server/features/categories/og.d.ts +33 -0
  14. package/dist/_internal/server/features/categories/og.js +75 -0
  15. package/dist/_internal/server/features/checkout/actions.d.ts +24 -0
  16. package/dist/_internal/server/features/checkout/actions.js +442 -13
  17. package/dist/_internal/server/features/checkout/index.d.ts +1 -1
  18. package/dist/_internal/server/features/checkout/index.js +1 -1
  19. package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +59 -0
  20. package/dist/_internal/server/features/checkout/prize-bundle-gates.js +99 -0
  21. package/dist/_internal/server/features/grouped/data.js +12 -5
  22. package/dist/_internal/server/features/homepage/data.d.ts +1 -1
  23. package/dist/_internal/server/features/homepage/data.js +2 -2
  24. package/dist/_internal/server/features/media/contextGuards.d.ts +52 -0
  25. package/dist/_internal/server/features/media/contextGuards.js +198 -0
  26. package/dist/_internal/server/features/orders/adapters.js +12 -0
  27. package/dist/_internal/server/features/products/data.d.ts +1 -1
  28. package/dist/_internal/server/features/sublisting-categories/data.d.ts +1 -1
  29. package/dist/_internal/server/features/sublisting-categories/data.js +2 -2
  30. package/dist/_internal/server/jobs/handlers/assignSpinPrize.d.ts +24 -0
  31. package/dist/_internal/server/jobs/handlers/assignSpinPrize.js +86 -0
  32. package/dist/_internal/server/jobs/handlers/bundleStockSync.d.ts +18 -0
  33. package/dist/_internal/server/jobs/handlers/bundleStockSync.js +80 -0
  34. package/dist/_internal/server/jobs/handlers/index.d.ts +8 -0
  35. package/dist/_internal/server/jobs/handlers/index.js +13 -0
  36. package/dist/_internal/server/jobs/handlers/listingProcessor.js +13 -3
  37. package/dist/_internal/server/jobs/handlers/onProductStockChange.d.ts +17 -0
  38. package/dist/_internal/server/jobs/handlers/onProductStockChange.js +136 -0
  39. package/dist/_internal/server/jobs/handlers/onProductWrite.js +17 -1
  40. package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +9 -0
  41. package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +29 -0
  42. package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +10 -0
  43. package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +58 -0
  44. package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +10 -0
  45. package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +65 -0
  46. package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +9 -0
  47. package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +45 -0
  48. package/dist/_internal/server/jobs/handlers/triggerEventRaffle.d.ts +30 -0
  49. package/dist/_internal/server/jobs/handlers/triggerEventRaffle.js +94 -0
  50. package/dist/_internal/shared/features/brands/schema.d.ts +3 -3
  51. package/dist/_internal/shared/features/cart/schema.d.ts +8 -8
  52. package/dist/_internal/shared/features/cart/schema.js +1 -1
  53. package/dist/_internal/shared/features/categories/bundle-config.d.ts +17 -0
  54. package/dist/_internal/shared/features/categories/bundle-config.js +17 -0
  55. package/dist/_internal/shared/features/layout/config.d.ts +35 -0
  56. package/dist/_internal/shared/features/layout/config.js +58 -0
  57. package/dist/_internal/shared/features/layout/index.d.ts +3 -0
  58. package/dist/_internal/shared/features/layout/index.js +2 -0
  59. package/dist/_internal/shared/features/layout/types.d.ts +137 -0
  60. package/dist/_internal/shared/features/layout/types.js +13 -0
  61. package/dist/_internal/shared/features/products/types.d.ts +1 -1
  62. package/dist/_internal/shared/listing-types/_registry.d.ts +57 -0
  63. package/dist/_internal/shared/listing-types/_registry.js +28 -0
  64. package/dist/_internal/shared/listing-types/auction/config.d.ts +7 -0
  65. package/dist/_internal/shared/listing-types/auction/config.js +8 -0
  66. package/dist/_internal/shared/listing-types/auction/ctas.d.ts +1 -0
  67. package/dist/_internal/shared/listing-types/auction/ctas.js +2 -0
  68. package/dist/_internal/shared/listing-types/auction/og.d.ts +1 -0
  69. package/dist/_internal/shared/listing-types/auction/og.js +1 -0
  70. package/dist/_internal/shared/listing-types/auction/schema.d.ts +1 -0
  71. package/dist/_internal/shared/listing-types/auction/schema.js +1 -0
  72. package/dist/_internal/shared/listing-types/auction/seed-factory.d.ts +1 -0
  73. package/dist/_internal/shared/listing-types/auction/seed-factory.js +1 -0
  74. package/dist/_internal/shared/listing-types/capabilities.d.ts +41 -0
  75. package/dist/_internal/shared/listing-types/capabilities.js +75 -0
  76. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +7 -0
  77. package/dist/_internal/shared/listing-types/pre-order/config.js +8 -0
  78. package/dist/_internal/shared/listing-types/pre-order/ctas.d.ts +1 -0
  79. package/dist/_internal/shared/listing-types/pre-order/ctas.js +2 -0
  80. package/dist/_internal/shared/listing-types/pre-order/og.d.ts +1 -0
  81. package/dist/_internal/shared/listing-types/pre-order/og.js +1 -0
  82. package/dist/_internal/shared/listing-types/pre-order/schema.d.ts +1 -0
  83. package/dist/_internal/shared/listing-types/pre-order/schema.js +1 -0
  84. package/dist/_internal/shared/listing-types/pre-order/seed-factory.d.ts +1 -0
  85. package/dist/_internal/shared/listing-types/pre-order/seed-factory.js +1 -0
  86. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +7 -0
  87. package/dist/_internal/shared/listing-types/prize-draw/config.js +8 -0
  88. package/dist/_internal/shared/listing-types/prize-draw/ctas.d.ts +1 -0
  89. package/dist/_internal/shared/listing-types/prize-draw/ctas.js +2 -0
  90. package/dist/_internal/shared/listing-types/prize-draw/og.d.ts +1 -0
  91. package/dist/_internal/shared/listing-types/prize-draw/og.js +1 -0
  92. package/dist/_internal/shared/listing-types/prize-draw/schema.d.ts +1 -0
  93. package/dist/_internal/shared/listing-types/prize-draw/schema.js +1 -0
  94. package/dist/_internal/shared/listing-types/prize-draw/seed-factory.d.ts +1 -0
  95. package/dist/_internal/shared/listing-types/prize-draw/seed-factory.js +1 -0
  96. package/dist/_internal/shared/listing-types/standard/config.d.ts +7 -0
  97. package/dist/_internal/shared/listing-types/standard/config.js +8 -0
  98. package/dist/_internal/shared/listing-types/standard/ctas.d.ts +1 -0
  99. package/dist/_internal/shared/listing-types/standard/ctas.js +3 -0
  100. package/dist/_internal/shared/listing-types/standard/og.d.ts +1 -0
  101. package/dist/_internal/shared/listing-types/standard/og.js +1 -0
  102. package/dist/_internal/shared/listing-types/standard/schema.d.ts +1 -0
  103. package/dist/_internal/shared/listing-types/standard/schema.js +1 -0
  104. package/dist/_internal/shared/listing-types/standard/seed-factory.d.ts +1 -0
  105. package/dist/_internal/shared/listing-types/standard/seed-factory.js +1 -0
  106. package/dist/_internal/shared/media/limits.d.ts +33 -0
  107. package/dist/_internal/shared/media/limits.js +97 -0
  108. package/dist/_internal/shared/schema-versions.d.ts +76 -0
  109. package/dist/_internal/shared/schema-versions.js +82 -0
  110. package/dist/client.d.ts +9 -0
  111. package/dist/client.js +7 -0
  112. package/dist/constants/api-endpoints.d.ts +6 -3
  113. package/dist/constants/api-endpoints.js +2 -1
  114. package/dist/errors/messages.d.ts +1 -1
  115. package/dist/errors/messages.js +1 -1
  116. package/dist/features/account/migrations.d.ts +2 -0
  117. package/dist/features/account/migrations.js +10 -0
  118. package/dist/features/admin/components/AdminMediaView.js +1 -1
  119. package/dist/features/admin/components/AdminProductsView.js +7 -3
  120. package/dist/features/admin/migrations.d.ts +2 -0
  121. package/dist/features/admin/migrations.js +10 -0
  122. package/dist/features/admin/types/product.types.d.ts +1 -1
  123. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +1 -1
  124. package/dist/features/auctions/migrations.d.ts +2 -0
  125. package/dist/features/auctions/migrations.js +10 -0
  126. package/dist/features/auctions/schemas/index.d.ts +3 -3
  127. package/dist/features/auctions/schemas/index.js +1 -1
  128. package/dist/features/auth/migrations.d.ts +2 -0
  129. package/dist/features/auth/migrations.js +10 -0
  130. package/dist/features/blog/migrations.d.ts +2 -0
  131. package/dist/features/blog/migrations.js +10 -0
  132. package/dist/features/brands/migrations.d.ts +2 -0
  133. package/dist/features/brands/migrations.js +10 -0
  134. package/dist/features/bundles/components/BundlesByCategoryListing.d.ts +6 -0
  135. package/dist/features/bundles/components/BundlesByCategoryListing.js +50 -0
  136. package/dist/features/bundles/components/index.d.ts +2 -0
  137. package/dist/features/bundles/components/index.js +1 -0
  138. package/dist/features/bundles/migrations.d.ts +2 -0
  139. package/dist/features/bundles/migrations.js +10 -0
  140. package/dist/features/bundles/schemas/index.d.ts +1 -0
  141. package/dist/features/bundles/schemas/index.js +1 -0
  142. package/dist/features/bundles/schemas/zod.d.ts +377 -0
  143. package/dist/features/bundles/schemas/zod.js +71 -0
  144. package/dist/features/cart/migrations.d.ts +2 -0
  145. package/dist/features/cart/migrations.js +10 -0
  146. package/dist/features/cart/schemas/firestore.d.ts +2 -2
  147. package/dist/features/categories/components/BrandDetailPageView.js +35 -4
  148. package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
  149. package/dist/features/categories/components/BrandDetailTabs.js +22 -8
  150. package/dist/features/categories/components/CategoryBundlesListing.d.ts +6 -0
  151. package/dist/features/categories/components/CategoryBundlesListing.js +74 -0
  152. package/dist/features/categories/components/CategoryDetailPageView.js +29 -4
  153. package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
  154. package/dist/features/categories/components/CategoryDetailTabs.js +22 -8
  155. package/dist/features/categories/migrations.d.ts +2 -0
  156. package/dist/features/categories/migrations.js +10 -0
  157. package/dist/features/categories/repository/categories.repository.d.ts +29 -0
  158. package/dist/features/categories/repository/categories.repository.js +83 -0
  159. package/dist/features/categories/schemas/firestore.d.ts +59 -2
  160. package/dist/features/categories/schemas/firestore.js +6 -0
  161. package/dist/features/categories/types/index.d.ts +11 -3
  162. package/dist/features/events/migrations.d.ts +2 -0
  163. package/dist/features/events/migrations.js +10 -0
  164. package/dist/features/faq/migrations.d.ts +2 -0
  165. package/dist/features/faq/migrations.js +10 -0
  166. package/dist/features/grouped/migrations.d.ts +2 -0
  167. package/dist/features/grouped/migrations.js +10 -0
  168. package/dist/features/grouped/schemas/firestore.d.ts +29 -10
  169. package/dist/features/grouped/schemas/firestore.js +10 -5
  170. package/dist/features/history/migrations.d.ts +2 -0
  171. package/dist/features/history/migrations.js +10 -0
  172. package/dist/features/homepage/hooks/useFeaturedAuctions.js +2 -2
  173. package/dist/features/homepage/hooks/useFeaturedPreOrders.js +2 -2
  174. package/dist/features/homepage/lib/section-renderer.js +5 -3
  175. package/dist/features/media/AvatarUpload.js +6 -28
  176. package/dist/features/media/hooks/useMedia.d.ts +31 -15
  177. package/dist/features/media/hooks/useMedia.js +48 -13
  178. package/dist/features/media/upload/ImageUpload.js +1 -1
  179. package/dist/features/media/upload/MediaUploadField.js +1 -1
  180. package/dist/features/messages/migrations.d.ts +2 -0
  181. package/dist/features/messages/migrations.js +10 -0
  182. package/dist/features/orders/components/OrdersList.js +10 -1
  183. package/dist/features/orders/migrations.d.ts +2 -0
  184. package/dist/features/orders/migrations.js +10 -0
  185. package/dist/features/orders/repository/orders.repository.d.ts +16 -0
  186. package/dist/features/orders/repository/orders.repository.js +49 -0
  187. package/dist/features/orders/schemas/firestore.d.ts +8 -0
  188. package/dist/features/orders/types/index.d.ts +12 -0
  189. package/dist/features/orders/utils/order-splitter.d.ts +2 -2
  190. package/dist/features/orders/utils/order-splitter.js +5 -0
  191. package/dist/features/payments/migrations.d.ts +2 -0
  192. package/dist/features/payments/migrations.js +10 -0
  193. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
  194. package/dist/features/products/actions/product-actions.d.ts +1 -1
  195. package/dist/features/products/api/[id]/route.js +34 -0
  196. package/dist/features/products/api/route.js +1 -19
  197. package/dist/features/products/components/CompareOverlay.d.ts +1 -1
  198. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +24 -0
  199. package/dist/features/products/components/MarketplacePrizeDrawCard.js +102 -0
  200. package/dist/features/products/components/PrizeDrawCollage.d.ts +32 -0
  201. package/dist/features/products/components/PrizeDrawCollage.js +22 -0
  202. package/dist/features/products/components/PrizeDrawDetailPageView.d.ts +27 -0
  203. package/dist/features/products/components/PrizeDrawDetailPageView.js +118 -0
  204. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +19 -0
  205. package/dist/features/products/components/PrizeDrawEntryActions.js +48 -0
  206. package/dist/features/products/components/PrizeDrawItemsEditor.d.ts +13 -0
  207. package/dist/features/products/components/PrizeDrawItemsEditor.js +97 -0
  208. package/dist/features/products/components/PrizeDrawsIndexListing.d.ts +8 -0
  209. package/dist/features/products/components/PrizeDrawsIndexListing.js +128 -0
  210. package/dist/features/products/components/PrizeDrawsListingView.d.ts +15 -0
  211. package/dist/features/products/components/PrizeDrawsListingView.js +49 -0
  212. package/dist/features/products/components/PrizeRevealModal.d.ts +34 -0
  213. package/dist/features/products/components/PrizeRevealModal.js +124 -0
  214. package/dist/features/products/components/ProductDetailPageView.js +13 -1
  215. package/dist/features/products/components/ProductForm.js +35 -2
  216. package/dist/features/products/components/ProductGrid.js +3 -1
  217. package/dist/features/products/components/index.d.ts +16 -0
  218. package/dist/features/products/components/index.js +8 -0
  219. package/dist/features/products/constants/listing-tabs.d.ts +113 -0
  220. package/dist/features/products/constants/listing-tabs.js +43 -0
  221. package/dist/features/products/index.d.ts +1 -0
  222. package/dist/features/products/index.js +1 -0
  223. package/dist/features/products/migrations.d.ts +2 -0
  224. package/dist/features/products/migrations.js +10 -0
  225. package/dist/features/products/repository/products.repository.d.ts +11 -7
  226. package/dist/features/products/repository/products.repository.js +49 -24
  227. package/dist/features/products/schemas/firestore.d.ts +3 -3
  228. package/dist/features/products/schemas/firestore.js +2 -2
  229. package/dist/features/products/schemas/index.d.ts +5 -5
  230. package/dist/features/products/schemas/index.js +3 -1
  231. package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
  232. package/dist/features/products/types/index.d.ts +17 -1
  233. package/dist/features/products/utils/listing-type.d.ts +7 -4
  234. package/dist/features/products/utils/listing-type.js +8 -4
  235. package/dist/features/promotions/actions/coupon-actions.d.ts +1 -1
  236. package/dist/features/promotions/hooks/useCouponValidate.d.ts +1 -1
  237. package/dist/features/promotions/migrations.d.ts +2 -0
  238. package/dist/features/promotions/migrations.js +10 -0
  239. package/dist/features/promotions/repository/coupons.repository.d.ts +1 -1
  240. package/dist/features/promotions/schemas/index.d.ts +2 -2
  241. package/dist/features/reviews/migrations.d.ts +2 -0
  242. package/dist/features/reviews/migrations.js +10 -0
  243. package/dist/features/scams/migrations.d.ts +2 -0
  244. package/dist/features/scams/migrations.js +10 -0
  245. package/dist/features/search/api/route.d.ts +1 -1
  246. package/dist/features/search/api/route.js +3 -3
  247. package/dist/features/search/components/Search.d.ts +1 -1
  248. package/dist/features/search/schemas/index.d.ts +3 -3
  249. package/dist/features/search/schemas/index.js +3 -1
  250. package/dist/features/search/types/index.d.ts +2 -2
  251. package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
  252. package/dist/features/seller/components/SellerProductsView.js +20 -6
  253. package/dist/features/seller/migrations.d.ts +2 -0
  254. package/dist/features/seller/migrations.js +10 -0
  255. package/dist/features/seller/schemas/index.d.ts +2 -2
  256. package/dist/features/stores/components/StoreBundlesPageView.d.ts +12 -0
  257. package/dist/features/stores/components/StoreBundlesPageView.js +24 -0
  258. package/dist/features/stores/components/StoreDetailLayoutView.js +15 -3
  259. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +11 -0
  260. package/dist/features/stores/components/StorePrizeDrawsPageView.js +27 -0
  261. package/dist/features/stores/components/index.d.ts +2 -0
  262. package/dist/features/stores/migrations.d.ts +2 -0
  263. package/dist/features/stores/migrations.js +10 -0
  264. package/dist/features/stores/schemas/index.d.ts +2 -2
  265. package/dist/features/stores/types/index.d.ts +1 -1
  266. package/dist/features/sublisting/migrations.d.ts +2 -0
  267. package/dist/features/sublisting/migrations.js +10 -0
  268. package/dist/features/sublisting/schemas/firestore.d.ts +2 -0
  269. package/dist/features/support/migrations.d.ts +2 -0
  270. package/dist/features/support/migrations.js +10 -0
  271. package/dist/features/wishlist/migrations.d.ts +2 -0
  272. package/dist/features/wishlist/migrations.js +10 -0
  273. package/dist/features/wishlist/types/index.d.ts +1 -1
  274. package/dist/index.d.ts +26 -18
  275. package/dist/index.js +41 -24
  276. package/dist/jobs.d.ts +1 -1
  277. package/dist/jobs.js +4 -0
  278. package/dist/next/routing/route-map.d.ts +4 -0
  279. package/dist/next/routing/route-map.js +2 -0
  280. package/dist/providers/db-firebase/filter-aliases.d.ts +2 -2
  281. package/dist/repositories/index.d.ts +0 -5
  282. package/dist/repositories/index.js +5 -4
  283. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  284. package/dist/seed/categories-seed-data.js +1105 -6
  285. package/dist/seed/faq-seed-data.js +160 -0
  286. package/dist/seed/grouped-listings-seed-data.js +32 -32
  287. package/dist/seed/homepage-sections-seed-data.js +52 -6
  288. package/dist/seed/index.d.ts +1 -3
  289. package/dist/seed/index.js +4 -3
  290. package/dist/seed/manifest.js +8 -13
  291. package/dist/seed/products-prize-draws-seed-data.d.ts +17 -0
  292. package/dist/seed/products-prize-draws-seed-data.js +313 -0
  293. package/dist/seo/json-ld.d.ts +1 -1
  294. package/dist/server-entry.d.ts +2 -2
  295. package/dist/server-entry.js +5 -3
  296. package/dist/server.d.ts +9 -2
  297. package/dist/server.js +11 -5
  298. package/dist/tailwind-utilities.css +1 -1
  299. package/dist/validation/schemas.d.ts +8 -8
  300. package/package.json +1 -1
  301. package/scripts/seed-cli.mjs +2 -4
@@ -19,6 +19,22 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
19
19
  cancelOrder(orderId: string, reason: string, refundAmount?: number): Promise<OrderDocument>;
20
20
  findRecentByUser(userId: string): Promise<OrderDocument[]>;
21
21
  hasUserPurchased(userId: string, productId: string): Promise<boolean>;
22
+ /**
23
+ * SB6-B — count this user's "active" orders for a given product. Used by
24
+ * the order-creation API to enforce `product.maxPerUser` on pre-orders +
25
+ * prize draws. Active = pending / confirmed / processing / shipped /
26
+ * delivered. Cancelled + refunded are intentionally excluded.
27
+ *
28
+ * Firestore caveat: an `in` over the 5-status set keeps this a single
29
+ * round-trip; we filter in memory if the active set ever grows past 10.
30
+ */
31
+ countByUserAndProduct(userId: string, productId: string): Promise<number>;
32
+ /**
33
+ * SB6-B — count this user's active orders for a given bundle. Same
34
+ * active-status filter as `countByUserAndProduct`. Bundle orders set
35
+ * `order.bundleId` at creation time (SB3 / SB6-C).
36
+ */
37
+ countByUserAndBundle(userId: string, bundleId: string): Promise<number>;
22
38
  deleteByUser(userId: string): Promise<number>;
23
39
  static readonly SELLER_SIEVE_FIELDS: {
24
40
  id: {
@@ -6,13 +6,30 @@ import { createOrderId, ORDER_COLLECTION, OrderStatusValues, } from "../schemas"
6
6
  const ORDER_FIELDS = {
7
7
  USER_ID: "userId",
8
8
  PRODUCT_ID: "productId",
9
+ BUNDLE_ID: "bundleId",
9
10
  STATUS: "status",
10
11
  STATUS_VALUES: {
12
+ PENDING: "pending",
11
13
  CONFIRMED: "confirmed",
14
+ PROCESSING: "processing",
12
15
  SHIPPED: "shipped",
13
16
  DELIVERED: "delivered",
17
+ CANCELLED: "cancelled",
18
+ REFUNDED: "refunded",
14
19
  },
15
20
  };
21
+ /**
22
+ * Statuses that count toward a user's per-product / per-bundle purchase
23
+ * allowance (SB6 maxPerUser enforcement). Cancelled and refunded orders are
24
+ * intentionally excluded — the inventory was returned to circulation.
25
+ */
26
+ const ACTIVE_ALLOWANCE_STATUSES = [
27
+ ORDER_FIELDS.STATUS_VALUES.PENDING,
28
+ ORDER_FIELDS.STATUS_VALUES.CONFIRMED,
29
+ ORDER_FIELDS.STATUS_VALUES.PROCESSING,
30
+ ORDER_FIELDS.STATUS_VALUES.SHIPPED,
31
+ ORDER_FIELDS.STATUS_VALUES.DELIVERED,
32
+ ];
16
33
  class OrderRepository extends BaseRepository {
17
34
  constructor() {
18
35
  super(ORDER_COLLECTION);
@@ -109,6 +126,38 @@ class OrderRepository extends BaseRepository {
109
126
  .get();
110
127
  return snapshot.docs.some((doc) => purchasedStatuses.has(doc.data()[ORDER_FIELDS.STATUS]));
111
128
  }
129
+ /**
130
+ * SB6-B — count this user's "active" orders for a given product. Used by
131
+ * the order-creation API to enforce `product.maxPerUser` on pre-orders +
132
+ * prize draws. Active = pending / confirmed / processing / shipped /
133
+ * delivered. Cancelled + refunded are intentionally excluded.
134
+ *
135
+ * Firestore caveat: an `in` over the 5-status set keeps this a single
136
+ * round-trip; we filter in memory if the active set ever grows past 10.
137
+ */
138
+ async countByUserAndProduct(userId, productId) {
139
+ const snapshot = await this.db
140
+ .collection(this.collection)
141
+ .where(ORDER_FIELDS.USER_ID, "==", userId)
142
+ .where(ORDER_FIELDS.PRODUCT_ID, "==", productId)
143
+ .where(ORDER_FIELDS.STATUS, "in", ACTIVE_ALLOWANCE_STATUSES)
144
+ .get();
145
+ return snapshot.size;
146
+ }
147
+ /**
148
+ * SB6-B — count this user's active orders for a given bundle. Same
149
+ * active-status filter as `countByUserAndProduct`. Bundle orders set
150
+ * `order.bundleId` at creation time (SB3 / SB6-C).
151
+ */
152
+ async countByUserAndBundle(userId, bundleId) {
153
+ const snapshot = await this.db
154
+ .collection(this.collection)
155
+ .where(ORDER_FIELDS.USER_ID, "==", userId)
156
+ .where(ORDER_FIELDS.BUNDLE_ID, "==", bundleId)
157
+ .where(ORDER_FIELDS.STATUS, "in", ACTIVE_ALLOWANCE_STATUSES)
158
+ .get();
159
+ return snapshot.size;
160
+ }
112
161
  async deleteByUser(userId) {
113
162
  try {
114
163
  const snapshot = await this.getCollection()
@@ -53,6 +53,14 @@ export interface OrderDocumentItem {
53
53
  quantity: number;
54
54
  unitPrice: number;
55
55
  totalPrice: number;
56
+ /** SB8-F — set when the item is a prize-draw entry; drives the reveals badge. */
57
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
58
+ /** SB8-F — per-item reveal status; flips through pending → open → revealed/closed. */
59
+ prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
60
+ /** SB8-F — ISO timestamp; deadline by which the buyer must claim the prize. */
61
+ prizeRevealDeadline?: string;
62
+ /** SB8-F — set after the reveal API picks a winner. */
63
+ revealedItemNumber?: number;
56
64
  }
57
65
  /** One applied discount/coupon saved on the order for accounting and display */
58
66
  export interface AppliedOrderDiscount {
@@ -23,6 +23,18 @@ export interface OrderItem {
23
23
  currency?: string;
24
24
  storeId?: string;
25
25
  attributes?: Record<string, string>;
26
+ /** Listing kind at the time of order — needed for prize-draw UI hints (SB8-F). */
27
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
28
+ /**
29
+ * Per-item prize-draw reveal status (SB8-F). Populated by the checkout
30
+ * actions when listingType === "prize-draw". Used to render the
31
+ * "X reveals pending" badge on user orders.
32
+ */
33
+ prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
34
+ /** ISO timestamp — when the user must claim their reveal before forfeit. */
35
+ prizeRevealDeadline?: string;
36
+ /** Set after the reveal endpoint picks a winner — the prize item index. */
37
+ revealedItemNumber?: number;
26
38
  }
27
39
  export interface OrderTimeline {
28
40
  status: OrderStatus;
@@ -1,4 +1,4 @@
1
- export type OrderType = "standard" | "preorder" | "auction" | "offer";
1
+ export type OrderType = "standard" | "preorder" | "auction" | "offer" | "prize-draw";
2
2
  export interface OrderGroup<T> {
3
3
  items: T[];
4
4
  orderType: OrderType;
@@ -18,7 +18,7 @@ export declare function splitCartIntoOrderGroups<T extends {
18
18
  item: {
19
19
  itemId: string;
20
20
  storeId?: string;
21
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle";
21
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
22
22
  isOffer?: boolean;
23
23
  };
24
24
  }>(checks: T[]): OrderGroup<T>[];
@@ -23,6 +23,11 @@ export function splitCartIntoOrderGroups(checks) {
23
23
  key = `offer:${item.itemId}`;
24
24
  orderType = "offer";
25
25
  }
26
+ else if (item.listingType === "prize-draw") {
27
+ // Each prize-draw entry is its own order (single reveal per order).
28
+ key = `prize-draw:${item.itemId}`;
29
+ orderType = "prize-draw";
30
+ }
26
31
  else if (item.listingType === "pre-order") {
27
32
  key = `preorder:${item.storeId ?? "unknown"}`;
28
33
  orderType = "preorder";
@@ -0,0 +1,2 @@
1
+ import type { MigrationStep } from "../../_internal/shared/schema-versions";
2
+ export declare const migrations: Record<number, MigrationStep<any>>;
@@ -0,0 +1,10 @@
1
+ // SB-UNI X3 — migrations shell for payout.
2
+ // v1 schema is current; no migrations needed yet.
3
+ //
4
+ // To add a migration:
5
+ // 1. Bump the relevant SCHEMA_VERSIONS entry in
6
+ // `_internal/shared/schema-versions.ts` (e.g. `payments: 2`)
7
+ // 2. Add a row below keyed by the FROM-version, returning the upgraded doc
8
+ // 3. Repository wraps reads with `migrateDocument(doc, current, migrations)`
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export const migrations = {};
@@ -74,6 +74,9 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
74
74
  ? p.preOrderProductionStatus
75
75
  : null;
76
76
  const isCancellable = p.preOrderCancellable === true;
77
+ const maxPerUser = typeof p.maxPerUser === "number" && p.maxPerUser > 0
78
+ ? p.maxPerUser
79
+ : null;
77
80
  const condition = typeof p.condition === "string" ? p.condition : null;
78
81
  const featured = p.featured === true;
79
82
  const shippingPaidBy = p.shippingPaidBy;
@@ -111,7 +114,7 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
111
114
  price: typeof p.price === "number" ? p.price : undefined,
112
115
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
113
116
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
114
- } }), _jsxs(Container, { size: "xl", className: "px-4 py-6", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between flex-wrap gap-2", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: "hover:text-primary-600 transition-colors", children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 transition-colors", children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", className: "mb-2 flex-wrap", children: [_jsx(Span, { className: "inline-block rounded-full bg-indigo-100 dark:bg-indigo-900/30 px-2.5 py-0.5 text-xs font-semibold text-indigo-700 dark:text-indigo-300", children: "Pre-Order" }), productionStatus && (_jsx(Span, { className: "inline-block rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-0.5 text-xs font-medium text-zinc-600 dark:text-zinc-300", children: PRODUCTION_STATUS_LABELS[productionStatus] ?? productionStatus }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), deliveryDate && (_jsxs(Row, { align: "center", gap: "xs", className: "text-sm text-zinc-600 dark:text-zinc-400", children: [_jsx(Span, { children: "\uD83D\uDCC5" }), _jsx(Span, { children: "Estimated delivery:" }), _jsx(Span, { className: "font-medium", children: deliveryDate.toLocaleDateString(undefined, { year: "numeric", month: "long" }) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: isCancellable, labels: {
117
+ } }), _jsxs(Container, { size: "xl", className: "px-4 py-6", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between flex-wrap gap-2", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: "hover:text-primary-600 transition-colors", children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 transition-colors", children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", className: "mb-2 flex-wrap", children: [_jsx(Span, { className: "inline-block rounded-full bg-indigo-100 dark:bg-indigo-900/30 px-2.5 py-0.5 text-xs font-semibold text-indigo-700 dark:text-indigo-300", children: "Pre-Order" }), productionStatus && (_jsx(Span, { className: "inline-block rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-0.5 text-xs font-medium text-zinc-600 dark:text-zinc-300", children: PRODUCTION_STATUS_LABELS[productionStatus] ?? productionStatus })), maxPerUser !== null && (_jsxs(Span, { className: "inline-block rounded-full bg-amber-100 dark:bg-amber-900/30 px-2.5 py-0.5 text-xs font-medium text-amber-800 dark:text-amber-200", children: ["Limit: ", maxPerUser, " per customer"] }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), deliveryDate && (_jsxs(Row, { align: "center", gap: "xs", className: "text-sm text-zinc-600 dark:text-zinc-400", children: [_jsx(Span, { children: "\uD83D\uDCC5" }), _jsx(Span, { children: "Estimated delivery:" }), _jsx(Span, { className: "font-medium", children: deliveryDate.toLocaleDateString(undefined, { year: "numeric", month: "long" }) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: isCancellable, labels: {
115
118
  featured: "Featured",
116
119
  fasterDelivery: "Faster Delivery",
117
120
  ratedSeller: "Rated Seller",
@@ -6,7 +6,7 @@ export interface ProductListActionParams {
6
6
  page?: number;
7
7
  pageSize?: number;
8
8
  /** Canonical listing-kind discriminator (SB1-G Phase 4). */
9
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle";
9
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
10
10
  featured?: boolean;
11
11
  storeId?: string;
12
12
  categoriesIn?: string[];
@@ -15,7 +15,14 @@ import { getProviders } from "../../../../contracts";
15
15
  import { createRouteHandler } from "../../../../next";
16
16
  import { mediaFieldSchema } from "../../../media/types/index";
17
17
  import { storeRepository } from "../../../stores/repository/store.repository";
18
+ // SB-UNI-V — bundlesRepository deleted; bundle stock-sync moves to the
19
+ // `onProductStockChange` Firebase Function (see functions/src/bundle-stock-sync.ts).
18
20
  import { sanitizeProductForPublic } from "../../utils/sanitize";
21
+ const UNAVAILABLE_PRODUCT_STATUSES = new Set([
22
+ "sold",
23
+ "out_of_stock",
24
+ "discontinued",
25
+ ]);
19
26
  function getRepo() {
20
27
  const { db } = getProviders();
21
28
  if (!db)
@@ -102,10 +109,36 @@ export const PATCH = createRouteHandler({
102
109
  if (!isOwner && !isModerator && !isAdmin) {
103
110
  return NextResponse.json({ success: false, error: "Not authorized to update this product" }, { status: 403 });
104
111
  }
112
+ // SB4 — once any prize in a prize-draw listing has been won, the listing
113
+ // is frozen. Sellers can no longer edit it; admins inherit the same lock
114
+ // (anything they need to change should go through a new listing or a
115
+ // dedicated remediation script).
116
+ const prizeItems = product.prizeDrawItems ??
117
+ [];
118
+ const anyPrizeWon = prizeItems.some((it) => it?.isWon);
119
+ if (product.listingType === "prize-draw" &&
120
+ anyPrizeWon) {
121
+ return NextResponse.json({
122
+ success: false,
123
+ error: "This prize-draw listing is locked because at least one prize has been revealed. Clone it into a new listing if you want to rerun the draw.",
124
+ }, { status: 409 });
125
+ }
105
126
  const updated = await repo.update(id, {
106
127
  ...body,
107
128
  updatedAt: new Date().toISOString(),
108
129
  });
130
+ // SB1-H stock-sync — if this PATCH transitioned the product to an
131
+ // unavailable status, propagate `isSold` to every bundle that lists it.
132
+ const beforeStatus = product.status;
133
+ const afterStatus = body?.status;
134
+ const becameUnavailable = typeof afterStatus === "string" &&
135
+ UNAVAILABLE_PRODUCT_STATUSES.has(afterStatus) &&
136
+ beforeStatus !== afterStatus;
137
+ // SB-UNI-V — bundle stock-sync moved to onProductStockChange Function;
138
+ // the API route no longer fires fire-and-forget. The product write
139
+ // triggers the Function via Firestore onWrite. Reference to
140
+ // `becameUnavailable` retained for telemetry callers.
141
+ void becameUnavailable;
109
142
  return NextResponse.json({ success: true, data: updated });
110
143
  },
111
144
  });
@@ -134,6 +167,7 @@ export const DELETE = createRouteHandler({
134
167
  status: "discontinued",
135
168
  updatedAt: new Date().toISOString(),
136
169
  });
170
+ // SB-UNI-V — bundle stock-sync handled by onProductStockChange Function.
137
171
  return NextResponse.json({ success: true });
138
172
  },
139
173
  });
@@ -43,8 +43,6 @@ const productMutateSchema = z
43
43
  tags: z.array(z.string()).optional(),
44
44
  featured: z.boolean().optional(),
45
45
  isPromoted: z.boolean().optional(),
46
- isAuction: z.boolean().optional(),
47
- isPreOrder: z.boolean().optional(),
48
46
  sellerId: z.string().optional(),
49
47
  sellerName: z.string().optional(),
50
48
  sellerEmail: z.string().email().optional(),
@@ -73,10 +71,7 @@ const SAFE_PRODUCT_FILTER_FIELDS = new Set([
73
71
  "storeId",
74
72
  "title",
75
73
  "price",
76
- // SB1-G — canonical discriminator first; legacy booleans retained transitional.
77
74
  "listingType",
78
- "isAuction",
79
- "isPreOrder",
80
75
  "featured",
81
76
  "isPromoted",
82
77
  "stockQuantity",
@@ -126,24 +121,11 @@ function buildFilters(url) {
126
121
  const inStock = param(url, "inStock");
127
122
  if (inStock === "true")
128
123
  parts.push("stockQuantity>0");
129
- // SB1-G — public URL still accepts the legacy boolean params; map them to
130
- // the canonical listingType clause. ?listingType=auction|pre-order|standard
131
- // takes precedence when present.
124
+ // SB1-G — canonical discriminator. Public URL accepts only ?listingType=X.
132
125
  const listingTypeParam = param(url, "listingType");
133
- const isAuction = param(url, "isAuction");
134
- const isPreOrder = param(url, "isPreOrder");
135
126
  if (listingTypeParam) {
136
127
  parts.push(`listingType==${listingTypeParam}`);
137
128
  }
138
- else if (isAuction === "true") {
139
- parts.push("listingType==auction");
140
- }
141
- else if (isPreOrder === "true") {
142
- parts.push("listingType==pre-order");
143
- }
144
- else if (isAuction === "false" && isPreOrder === "false") {
145
- parts.push("listingType==standard");
146
- }
147
129
  const featured = param(url, "featured");
148
130
  if (featured === "true")
149
131
  parts.push("featured==true");
@@ -15,7 +15,7 @@ export interface CompareProductLike {
15
15
  storeName?: string;
16
16
  storeSlug?: string;
17
17
  /** Canonical discriminator (SB1-G Phase 4). */
18
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle";
18
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
19
19
  features?: string[];
20
20
  }
21
21
  interface CompareFieldLabels {
@@ -0,0 +1,24 @@
1
+ import type { ProductItem } from "../types";
2
+ export type MarketplacePrizeDrawCardData = ProductItem;
3
+ export interface MarketplacePrizeDrawCardLabels {
4
+ prizeDrawBadge?: string;
5
+ pendingBadge?: string;
6
+ openBadge?: string;
7
+ closedBadge?: string;
8
+ entriesRemainingLabel?: (remaining: number, max: number) => string;
9
+ pricePerEntryLabel?: string;
10
+ enterDraw?: string;
11
+ }
12
+ export interface MarketplacePrizeDrawCardProps {
13
+ product: MarketplacePrizeDrawCardData;
14
+ className?: string;
15
+ variant?: "grid" | "list";
16
+ selectable?: boolean;
17
+ isSelected?: boolean;
18
+ onSelect?: (id: string, selected: boolean) => void;
19
+ href?: string;
20
+ hrefBuilder?: (product: MarketplacePrizeDrawCardData) => string;
21
+ onNavigate?: (href: string) => void;
22
+ labels?: MarketplacePrizeDrawCardLabels;
23
+ }
24
+ export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplacePrizeDrawCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useLongPress } from "../../../react/hooks/useLongPress";
6
+ import { ROUTES } from "../../../next";
7
+ import { formatCurrency } from "../../../utils";
8
+ import { getDefaultCurrency } from "../../../core/baseline-resolver";
9
+ import { BaseListingCard, Button, Div, Row, Span, Text, TextLink, } from "../../../ui";
10
+ import { THEME_CONSTANTS } from "../../../tokens";
11
+ const DEFAULT_LABELS = {
12
+ prizeDrawBadge: "Prize draw",
13
+ pendingBadge: "Reveal pending",
14
+ openBadge: "Reveal open",
15
+ closedBadge: "Draw closed",
16
+ entriesRemainingLabel: (remaining, max) => `${remaining}/${max} entries left`,
17
+ pricePerEntryLabel: "per entry",
18
+ enterDraw: "Enter draw",
19
+ };
20
+ function statusVariant(status) {
21
+ switch (status) {
22
+ case "open":
23
+ return "bg-emerald-600 text-white";
24
+ case "closed":
25
+ return "bg-zinc-500 text-white";
26
+ case "pending":
27
+ default:
28
+ return "bg-amber-500 text-white";
29
+ }
30
+ }
31
+ function statusLabel(status, labels) {
32
+ switch (status) {
33
+ case "open":
34
+ return labels.openBadge;
35
+ case "closed":
36
+ return labels.closedBadge;
37
+ case "pending":
38
+ default:
39
+ return labels.pendingBadge;
40
+ }
41
+ }
42
+ function resolveHref(product, href, hrefBuilder) {
43
+ if (href)
44
+ return href;
45
+ if (hrefBuilder)
46
+ return hrefBuilder(product);
47
+ return ROUTES.PUBLIC.PRIZE_DRAW_DETAIL(product.slug ?? product.id);
48
+ }
49
+ function formatCountdown(target) {
50
+ if (!target)
51
+ return null;
52
+ const t = target instanceof Date ? target.getTime() : new Date(target).getTime();
53
+ if (Number.isNaN(t))
54
+ return null;
55
+ const diff = t - Date.now();
56
+ if (diff <= 0)
57
+ return null;
58
+ const days = Math.floor(diff / 86400000);
59
+ const hours = Math.floor((diff % 86400000) / 3600000);
60
+ if (days > 0)
61
+ return `${days}d ${hours}h`;
62
+ const minutes = Math.floor((diff % 3600000) / 60000);
63
+ return `${hours}h ${minutes}m`;
64
+ }
65
+ export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
66
+ const router = useRouter();
67
+ const mergedLabels = { ...DEFAULT_LABELS, ...labels };
68
+ const detailHref = resolveHref(product, href, hrefBuilder);
69
+ const longPress = useLongPress(() => onSelect?.(product.id, !isSelected));
70
+ const items = product.prizeDrawItems ?? [];
71
+ const thumbItems = items.slice(0, 4);
72
+ const status = product.prizeRevealStatus;
73
+ const max = product.prizeMaxEntries ?? 0;
74
+ const current = product.prizeCurrentEntries ?? 0;
75
+ const remaining = Math.max(0, max - current);
76
+ const pricePerEntry = product.pricePerEntry ?? product.price;
77
+ const countdownTarget = status === "pending"
78
+ ? product.prizeRevealWindowStart
79
+ : status === "open"
80
+ ? product.prizeRevealWindowEnd
81
+ : undefined;
82
+ const countdown = formatCountdown(countdownTarget);
83
+ const handleNavigate = useCallback(() => {
84
+ if (onNavigate) {
85
+ onNavigate(String(detailHref));
86
+ return;
87
+ }
88
+ router.push(String(detailHref));
89
+ }, [detailHref, onNavigate, router]);
90
+ return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { className: "grid grid-cols-2 grid-rows-2 gap-0.5 h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
91
+ const it = thumbItems[i];
92
+ const img = it?.images?.[0];
93
+ return (_jsx(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: img ? (
94
+ /* eslint-disable-next-line @next/next/no-raw-media-elements, @next/next/no-img-element */
95
+ _jsx("img", { src: img, alt: it?.title ?? `Prize ${i + 1}`, loading: "lazy", className: "absolute inset-0 h-full w-full object-cover" })) : null }, `thumb-${i}`));
96
+ }) })) : (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center bg-[var(--appkit-color-surface-muted)]", children: _jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: "No prizes" }) })) }), _jsxs(Div, { className: "absolute right-2 top-2 flex flex-col items-end gap-1", children: [_jsx(Span, { className: "inline-flex items-center rounded-full bg-fuchsia-600 px-2 py-0.5 text-xs font-medium text-white", children: mergedLabels.prizeDrawBadge }), _jsx(Span, { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${statusVariant(status)}`, children: statusLabel(status, mergedLabels) })] }), status === "closed" ? (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center bg-black/40", children: _jsx(Span, { className: "rounded bg-zinc-900/80 px-3 py-1 text-xs font-bold uppercase tracking-wider text-white", children: mergedLabels.closedBadge }) })) : null, onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
97
+ event.stopPropagation();
98
+ onSelect(product.id, !isSelected);
99
+ }, className: selectable || isSelected
100
+ ? "opacity-100"
101
+ : "opacity-0 group-hover:opacity-100 transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-medium text-zinc-900 dark:text-zinc-100`, children: product.title }) }), _jsxs(Row, { justify: "between", className: "mt-1 gap-2", children: [_jsxs(Text, { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { className: "text-xs font-normal text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }), max > 0 ? (_jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", className: "mt-2 w-full text-xs", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
102
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * PrizeDrawCollage (SB4-B)
3
+ *
4
+ * Read-only display of a prize-draw's full prize pool. Each cell shows the
5
+ * primary image, item-number badge, and title. Items with `isWon === true`
6
+ * are dimmed under a diagonal overlay with a "Won" label so the public
7
+ * collage truthfully reflects what's still up for grabs.
8
+ *
9
+ * Optional `highlightItemNumber` (passed by the reveal modal during animation)
10
+ * adds an emphasis ring to the cell that just won.
11
+ */
12
+ import type { PrizeDrawItem } from "../schemas/firestore";
13
+ export interface PrizeDrawCollageProps {
14
+ items: PrizeDrawItem[];
15
+ highlightItemNumber?: number;
16
+ /** Custom click handler — e.g. open detail modal for an item. */
17
+ onItemClick?: (item: PrizeDrawItem) => void;
18
+ /** Defaults to "Won". Use for localisation. */
19
+ wonLabel?: string;
20
+ /**
21
+ * Public buyer surfaces pass `true` so the diagonal "Won" overlay never
22
+ * renders — otherwise potential buyers would see their favorite prize is
23
+ * already gone and drop out. The seller / admin / winner views leave this
24
+ * `false` (default) to show real pool state.
25
+ *
26
+ * The product adapter for public reads should ALSO strip `isWon` before
27
+ * sending the items array client-side; this prop is the visual fallback.
28
+ */
29
+ hideWonState?: boolean;
30
+ }
31
+ export declare function PrizeDrawCollage({ items, highlightItemNumber, onItemClick, wonLabel, hideWonState, }: PrizeDrawCollageProps): import("react/jsx-runtime").JSX.Element;
32
+ export default PrizeDrawCollage;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Div, Text } from "../../../ui";
3
+ export function PrizeDrawCollage({ items, highlightItemNumber, onItemClick, wonLabel = "Won", hideWonState = false, }) {
4
+ if (!items.length) {
5
+ return (_jsx(Div, { className: "rounded border border-dashed border-[var(--appkit-color-border)] p-6 text-center", children: _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "No prizes configured yet." }) }));
6
+ }
7
+ return (_jsx(Div, { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4", children: items.map((it) => {
8
+ const cover = it.images?.[0];
9
+ const isHighlight = highlightItemNumber === it.itemNumber;
10
+ const Cell = onItemClick ? "button" : "div";
11
+ return (_jsxs(Cell, { type: onItemClick ? "button" : undefined, onClick: onItemClick ? () => onItemClick(it) : undefined, className: [
12
+ "group relative overflow-hidden rounded-lg border bg-[var(--appkit-color-surface)] text-left transition-transform",
13
+ "border-[var(--appkit-color-border)]",
14
+ isHighlight
15
+ ? "ring-2 ring-offset-2 ring-[var(--appkit-color-primary)] scale-[1.02]"
16
+ : "hover:scale-[1.01]",
17
+ ].join(" "), children: [_jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (
18
+ /* eslint-disable-next-line @next/next/no-img-element */
19
+ _jsx("img", { src: cover, alt: it.title || `Prize #${it.itemNumber}`, className: "absolute inset-0 h-full w-full object-cover", loading: "lazy" })) : (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center bg-[var(--appkit-color-surface-muted)]", children: _jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: "No image" }) })), _jsxs(Div, { className: "absolute left-2 top-2 rounded bg-black/70 px-1.5 py-0.5 text-xs font-semibold text-white", children: ["#", it.itemNumber] }), it.isWon && !hideWonState ? (_jsxs(_Fragment, { children: [_jsx(Div, { "aria-hidden": true, className: "absolute inset-0 bg-gradient-to-br from-black/60 via-black/30 to-black/60 mix-blend-multiply" }), _jsx(Div, { className: "absolute inset-0 flex items-center justify-center", children: _jsx(Text, { className: "rotate-[-12deg] rounded bg-red-600 px-3 py-1 text-xs font-bold uppercase tracking-wider text-white shadow", children: wonLabel }) })] })) : null] }), _jsxs(Div, { className: "p-2", children: [_jsx(Text, { className: "line-clamp-2 text-sm font-medium", children: it.title || `Prize #${it.itemNumber}` }), it.estimatedValue != null ? (_jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: ["est. \u20B9", (it.estimatedValue / 100).toLocaleString("en-IN")] })) : null] })] }, `collage-${it.itemNumber}`));
20
+ }) }));
21
+ }
22
+ export default PrizeDrawCollage;
@@ -0,0 +1,27 @@
1
+ import type { ProductDocument } from "../schemas/firestore";
2
+ export interface PrizeDrawDetailPageViewProps {
3
+ id: string;
4
+ /**
5
+ * Pre-fetched product document from the page's server data layer.
6
+ * When provided, the internal repository call is skipped — deduplicating
7
+ * the fetch with generateMetadata() via React.cache().
8
+ */
9
+ initialPrizeDraw?: ProductDocument | null;
10
+ /**
11
+ * Authenticated buyer's uid. When set, the view server-fetches the
12
+ * buyer's existing-entry count for this draw and renders the SB6-D
13
+ * personalised "You have X/Y entries used" badge.
14
+ */
15
+ currentUserId?: string;
16
+ }
17
+ /**
18
+ * Public prize-draw detail page (SB4-G).
19
+ *
20
+ * Server-fetches the prize-draw product, strips `isWon` from each item before
21
+ * passing it to `PrizeDrawCollage` so public buyers stay unspoiled, and
22
+ * delegates the buy-bar to the client `PrizeDrawEntryActions` which surfaces
23
+ * the `NonRefundableConsentModal` before any add-to-cart happens.
24
+ *
25
+ * Reuses the `PreOrderDetailView` (grid-2) shell for layout parity.
26
+ */
27
+ export declare function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUserId, }: PrizeDrawDetailPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;