@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
@@ -0,0 +1,118 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Link from "next/link";
3
+ import { orderRepository, productRepository } from "../../../repositories";
4
+ import { ROUTES } from "../../../next";
5
+ import { getDefaultCurrency } from "../../../core/baseline-resolver";
6
+ import { normalizeRichTextHtml } from "../../../utils/string.formatter";
7
+ import { safeDisplayName } from "../../../security";
8
+ import { Container, Div, Heading, Main, RichText, Row, Section, Span, Stack, Text, } from "../../../ui";
9
+ import { PreOrderDetailView } from "./PreOrderDetailView";
10
+ import { BuyBar } from "./BuyBar";
11
+ import { ProductTabsShell } from "./ProductTabsShell";
12
+ import { ShareButton } from "./ShareButton";
13
+ import { PrizeDrawCollage } from "./PrizeDrawCollage";
14
+ import { PrizeDrawEntryActions } from "./PrizeDrawEntryActions";
15
+ import { HistoryTracker } from "../../history/components/HistoryTracker";
16
+ import { formatCurrency } from "../../../utils/number.formatter";
17
+ function toDescriptionHtml(raw) {
18
+ if (!raw)
19
+ return "";
20
+ const s = typeof raw === "string" ? raw : JSON.stringify(raw);
21
+ return normalizeRichTextHtml(s);
22
+ }
23
+ function statusLabel(s) {
24
+ switch (s) {
25
+ case "open":
26
+ return "Reveal open";
27
+ case "closed":
28
+ return "Draw closed";
29
+ case "pending":
30
+ default:
31
+ return "Reveal pending";
32
+ }
33
+ }
34
+ function statusClass(s) {
35
+ switch (s) {
36
+ case "open":
37
+ return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300";
38
+ case "closed":
39
+ return "bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-200";
40
+ case "pending":
41
+ default:
42
+ return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300";
43
+ }
44
+ }
45
+ function stripIsWon(items) {
46
+ if (!items)
47
+ return [];
48
+ return items.map((it) => ({ ...it, isWon: false }));
49
+ }
50
+ /**
51
+ * Public prize-draw detail page (SB4-G).
52
+ *
53
+ * Server-fetches the prize-draw product, strips `isWon` from each item before
54
+ * passing it to `PrizeDrawCollage` so public buyers stay unspoiled, and
55
+ * delegates the buy-bar to the client `PrizeDrawEntryActions` which surfaces
56
+ * the `NonRefundableConsentModal` before any add-to-cart happens.
57
+ *
58
+ * Reuses the `PreOrderDetailView` (grid-2) shell for layout parity.
59
+ */
60
+ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUserId, }) {
61
+ const product = initialPrizeDraw !== undefined
62
+ ? initialPrizeDraw ?? undefined
63
+ : await productRepository.findByIdOrSlug(id).catch(() => undefined);
64
+ if (!product) {
65
+ return (_jsx(Main, { children: _jsx(Section, { className: "py-20", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "center", gap: "md", className: "text-center", children: [_jsx(Heading, { level: 1, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-50", children: "Prize Draw Not Found" }), _jsx(Text, { className: "text-zinc-500", children: "The prize draw you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "text-sm font-medium text-primary-600 hover:underline", children: "Browse Prize Draws" })] }) }) }) }));
66
+ }
67
+ const p = product;
68
+ const currency = p.currency || getDefaultCurrency();
69
+ const title = String(p.title ?? p.name ?? "Prize Draw");
70
+ const slug = typeof p.slug === "string" ? p.slug : String(p.id ?? "");
71
+ const items = stripIsWon(p.prizeDrawItems);
72
+ const pricePerEntry = typeof p.pricePerEntry === "number"
73
+ ? p.pricePerEntry
74
+ : typeof p.price === "number"
75
+ ? p.price
76
+ : 0;
77
+ const max = typeof p.prizeMaxEntries === "number" ? p.prizeMaxEntries : 0;
78
+ const current = typeof p.prizeCurrentEntries === "number" ? p.prizeCurrentEntries : 0;
79
+ const remaining = Math.max(0, max - current);
80
+ const revealStatus = typeof p.prizeRevealStatus === "string"
81
+ ? p.prizeRevealStatus
82
+ : undefined;
83
+ const revealStart = p.prizeRevealWindowStart
84
+ ? new Date(p.prizeRevealWindowStart)
85
+ : null;
86
+ const revealEnd = p.prizeRevealWindowEnd
87
+ ? new Date(p.prizeRevealWindowEnd)
88
+ : null;
89
+ const githubUrl = typeof p.prizeGithubFileUrl === "string" ? p.prizeGithubFileUrl : undefined;
90
+ const maxPerUser = typeof p.maxPerUser === "number" && p.maxPerUser > 0
91
+ ? p.maxPerUser
92
+ : null;
93
+ // SB6-D post-auth — fetch this buyer's existing-entry count when we have
94
+ // an authenticated uid. `orderRepository.countByUserAndProduct` already
95
+ // filters by active statuses (pending/confirmed/processing/shipped/
96
+ // delivered) so cancelled/refunded entries don't count.
97
+ const userEntriesUsed = currentUserId && maxPerUser != null
98
+ ? await orderRepository
99
+ .countByUserAndProduct(currentUserId, String(product.id))
100
+ .catch(() => 0)
101
+ : null;
102
+ const thumb = items[0]?.images?.[0];
103
+ const storeName = typeof p.storeName === "string" ? p.storeName : null;
104
+ const safeSeller = storeName ? safeDisplayName(storeName, "") : null;
105
+ const storeSlug = (typeof p.storeSlug === "string" ? p.storeSlug : null) ||
106
+ (typeof p.storeId === "string" ? p.storeId : null);
107
+ const storeHref = storeSlug
108
+ ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug))
109
+ : null;
110
+ const descriptionHtml = toDescriptionHtml(p.description);
111
+ return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
112
+ title,
113
+ thumb,
114
+ price: pricePerEntry,
115
+ storeId: typeof p.storeId === "string" ? p.storeId : undefined,
116
+ storeName: storeName ?? undefined,
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.PRIZE_DRAWS), className: "hover:text-primary-600 transition-colors", children: "Prize Draws" }), _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(PrizeDrawCollage, { items: items, hideWonState: true })), 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-fuchsia-100 dark:bg-fuchsia-900/30 px-2.5 py-0.5 text-xs font-semibold text-fuchsia-700 dark:text-fuchsia-300", children: "Prize Draw" }), _jsx(Span, { className: `inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold ${statusClass(revealStatus)}`, children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(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-700 dark:text-zinc-300", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { className: "inline-block rounded-full bg-fuchsia-100 dark:bg-fuchsia-900/30 px-2.5 py-0.5 text-xs font-semibold text-fuchsia-700 dark:text-fuchsia-300", children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), _jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Entries" }), _jsxs(Text, { className: "text-sm font-semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Reveal window opens" }), _jsx(Text, { className: "text-sm font-medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Reveal window closes" }), _jsx(Text, { className: "text-sm font-medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-500 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined })), renderBuyBar: () => (_jsx(Div, { id: "prize-draw-buy-bar", className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, thumb: thumb, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })) }), _jsxs(BuyBar, { children: [_jsx(Span, { className: "mr-auto text-sm font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(pricePerEntry, currency) }), _jsx("a", { href: "#prize-draw-buy-bar", className: "appkit-button appkit-button--primary appkit-button--sm flex-1", children: _jsx("span", { className: "appkit-button__content", children: "Enter draw" }) })] })] })] }));
118
+ }
@@ -0,0 +1,19 @@
1
+ export interface PrizeDrawEntryActionsProps {
2
+ productId: string;
3
+ productSlug?: string;
4
+ title: string;
5
+ thumb?: string;
6
+ pricePerEntry: number;
7
+ currency: string;
8
+ remainingEntries: number;
9
+ revealStatus: "pending" | "open" | "closed" | undefined;
10
+ prizeGithubFileUrl?: string;
11
+ }
12
+ /**
13
+ * Client buy panel for a prize-draw detail page (SB4-G).
14
+ *
15
+ * - Enter Draw → NonRefundableConsentModal (listingType="prize-draw")
16
+ * - On confirm → add 1 entry to guest cart and route to /user/cart
17
+ * - "View RNG source" link → prizeGithubFileUrl
18
+ */
19
+ export declare function PrizeDrawEntryActions({ productId, productSlug, title, thumb, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }: PrizeDrawEntryActionsProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button, Stack, Text, useToast } from "../../../ui";
6
+ import { ROUTES } from "../../../next";
7
+ import { formatCurrency } from "../../../utils/number.formatter";
8
+ import { useGuestCart } from "../../cart/hooks/useGuestCart";
9
+ import { pushCartOp } from "../../cart/utils/pending-ops";
10
+ import { NonRefundableConsentModal } from "./NonRefundableConsentModal";
11
+ /**
12
+ * Client buy panel for a prize-draw detail page (SB4-G).
13
+ *
14
+ * - Enter Draw → NonRefundableConsentModal (listingType="prize-draw")
15
+ * - On confirm → add 1 entry to guest cart and route to /user/cart
16
+ * - "View RNG source" link → prizeGithubFileUrl
17
+ */
18
+ export function PrizeDrawEntryActions({ productId, productSlug, title, thumb, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }) {
19
+ const router = useRouter();
20
+ const cart = useGuestCart();
21
+ const { showToast } = useToast();
22
+ const [consentOpen, setConsentOpen] = useState(false);
23
+ const closed = revealStatus === "closed" || remainingEntries === 0;
24
+ const handleEnter = useCallback(() => {
25
+ if (closed)
26
+ return;
27
+ setConsentOpen(true);
28
+ }, [closed]);
29
+ const handleConfirm = useCallback(() => {
30
+ cart.add(productId, 1, {
31
+ productTitle: title,
32
+ productImage: thumb,
33
+ price: pricePerEntry,
34
+ });
35
+ pushCartOp({
36
+ op: "add",
37
+ productId,
38
+ quantity: 1,
39
+ productTitle: title,
40
+ productImage: thumb,
41
+ price: pricePerEntry,
42
+ });
43
+ setConsentOpen(false);
44
+ showToast("Entry added to cart", "success");
45
+ router.push(String(ROUTES.USER.CART));
46
+ }, [cart, productId, title, thumb, pricePerEntry, router, showToast]);
47
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: [formatCurrency(pricePerEntry, currency), _jsx(Text, { as: "span", className: "ml-1 text-xs font-normal text-[var(--appkit-color-text-muted)]", children: "per entry" })] }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: closed, onClick: handleEnter, children: closed ? "Draw closed" : "Enter draw" }), prizeGithubFileUrl ? (_jsx("a", { href: prizeGithubFileUrl, target: "_blank", rel: "noopener noreferrer", className: "text-center text-xs font-medium text-primary-600 underline-offset-4 hover:underline dark:text-primary-400", children: "View RNG source code on GitHub \u2192" })) : null, _jsx(Text, { className: "text-center text-xs text-[var(--appkit-color-text-muted)]", children: "Winners chosen by Node.js crypto.randomInt \u2014 fully auditable. Entries are locked once paid; refunds only if the prize pool is exhausted." }), _jsx(NonRefundableConsentModal, { open: consentOpen, listingType: "prize-draw", itemTitle: title, priceLabel: formatCurrency(pricePerEntry, currency), onCancel: () => setConsentOpen(false), onConfirm: handleConfirm })] }));
48
+ }
@@ -0,0 +1,13 @@
1
+ import type { PrizeDrawItem } from "../schemas/firestore";
2
+ export interface PrizeDrawItemsEditorProps {
3
+ items: PrizeDrawItem[];
4
+ onChange: (items: PrizeDrawItem[]) => void;
5
+ /** Upload an image File → returns the resolved storage URL (or media slug). */
6
+ onUploadImage: (file: File, itemNumber: number) => Promise<string>;
7
+ /** Optional video uploader. */
8
+ onUploadVideo?: (file: File, itemNumber: number) => Promise<string>;
9
+ /** Show a non-editable warning above the editor (e.g. "Draw already opened"). */
10
+ warning?: string;
11
+ }
12
+ export declare function PrizeDrawItemsEditor({ items, onChange, onUploadImage, onUploadVideo, warning, }: PrizeDrawItemsEditorProps): import("react/jsx-runtime").JSX.Element;
13
+ export default PrizeDrawItemsEditor;
@@ -0,0 +1,97 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * PrizeDrawItemsEditor (SB4-A)
5
+ *
6
+ * Editor for the `prizeDrawItems` array on a prize-draw ProductDocument.
7
+ * Owns: add / remove / reorder, per-item title/description/condition/
8
+ * estimatedValue, 1–2 images per item, optional video, and a locked-overlay
9
+ * for items already won during a previous reveal.
10
+ *
11
+ * Min 3 items, max 16 (mirrors PrizeDrawItem schema constraints).
12
+ */
13
+ import { useCallback } from "react";
14
+ import { Button, Div, FormField, Heading, Row, Stack, Text } from "../../../ui";
15
+ import { ImageUpload } from "../../media";
16
+ const MIN_ITEMS = 3;
17
+ const MAX_ITEMS = 16;
18
+ const MAX_IMAGES_PER_ITEM = 2;
19
+ const CONDITION_OPTIONS = [
20
+ { value: "new", label: "New" },
21
+ { value: "like_new", label: "Like New" },
22
+ { value: "good", label: "Good" },
23
+ { value: "used", label: "Used" },
24
+ { value: "graded", label: "Graded" },
25
+ { value: "refurbished", label: "Refurbished" },
26
+ ];
27
+ export function PrizeDrawItemsEditor({ items, onChange, onUploadImage, onUploadVideo, warning, }) {
28
+ // Once any prize has been revealed (isWon=true), the array is frozen for
29
+ // the seller — add/remove/reorder are all blocked. Per-item edits already
30
+ // respect the per-item `locked` flag below.
31
+ const anyWon = items.some((it) => it.isWon);
32
+ const update = useCallback((index, patch) => {
33
+ const next = items.map((it, i) => (i === index ? { ...it, ...patch } : it));
34
+ onChange(next);
35
+ }, [items, onChange]);
36
+ const renumber = (list) => list.map((it, i) => ({ ...it, itemNumber: i + 1 }));
37
+ const addItem = () => {
38
+ if (items.length >= MAX_ITEMS)
39
+ return;
40
+ onChange(renumber([
41
+ ...items,
42
+ {
43
+ itemNumber: items.length + 1,
44
+ title: "",
45
+ description: "",
46
+ images: [],
47
+ condition: "new",
48
+ isWon: false,
49
+ },
50
+ ]));
51
+ };
52
+ const removeItem = (index) => {
53
+ const target = items[index];
54
+ if (target?.isWon)
55
+ return; // cannot remove a won item
56
+ if (items.length <= MIN_ITEMS)
57
+ return;
58
+ onChange(renumber(items.filter((_, i) => i !== index)));
59
+ };
60
+ const move = (index, direction) => {
61
+ const target = index + direction;
62
+ if (target < 0 || target >= items.length)
63
+ return;
64
+ const next = [...items];
65
+ [next[index], next[target]] = [next[target], next[index]];
66
+ onChange(renumber(next));
67
+ };
68
+ const setImage = (index, slot, url) => {
69
+ const cur = items[index];
70
+ const nextImgs = [...(cur.images ?? [])];
71
+ nextImgs[slot] = url;
72
+ update(index, { images: nextImgs.filter(Boolean) });
73
+ };
74
+ const removeImage = (index, slot) => {
75
+ const cur = items[index];
76
+ const nextImgs = (cur.images ?? []).filter((_, i) => i !== slot);
77
+ update(index, { images: nextImgs });
78
+ };
79
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Heading, { level: 3, children: ["Prize Items (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "secondary", onClick: addItem, disabled: anyWon || items.length >= MAX_ITEMS, children: ["+ Add prize (", items.length, "/", MAX_ITEMS, ")"] })] }), _jsxs(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: ["Add between ", MIN_ITEMS, " and ", MAX_ITEMS, " prizes. Each entry will reveal exactly one of these. Items marked won during a prior reveal cannot be edited or removed."] }), anyWon ? (_jsxs(Div, { className: "rounded border border-red-400/40 bg-red-50 px-3 py-2 text-sm text-red-900 dark:bg-red-900/30 dark:text-red-100", children: [_jsx("strong", { children: "Draw locked." }), " At least one prize has been revealed \u2014 this listing can no longer be edited. To run a similar draw, clone it into a new prize-draw listing."] })) : warning ? (_jsx(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-3 py-2 text-sm text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: warning })) : null, _jsx(Stack, { gap: "md", children: items.map((it, index) => {
80
+ const locked = it.isWon || anyWon;
81
+ return (_jsxs(Div, { className: `relative rounded-lg border border-[var(--appkit-color-border)] p-4 ${locked ? "opacity-60" : ""}`, children: [locked ? (_jsx(Div, { className: "absolute inset-0 z-10 flex items-center justify-center bg-black/10 dark:bg-black/40 rounded-lg pointer-events-none", children: _jsx(Text, { className: "rounded bg-red-600 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white", children: "Won \u2014 locked" }) })) : null, _jsxs(Row, { justify: "between", align: "center", className: "mb-3", children: [_jsxs(Heading, { level: 4, children: ["Prize #", it.itemNumber] }), _jsxs(Row, { gap: "xs", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => move(index, -1), disabled: locked || index === 0, "aria-label": "Move up", children: "\u2191" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => move(index, 1), disabled: locked || index === items.length - 1, "aria-label": "Move down", children: "\u2193" }), _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: () => removeItem(index), disabled: locked || items.length <= MIN_ITEMS, children: "Remove" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(FormField, { name: `item-${index}-title`, label: "Title", type: "text", value: it.title, onChange: (v) => update(index, { title: v }), disabled: locked, placeholder: "e.g. PSA 9 Charizard Base Set Holo" }), _jsx(FormField, { name: `item-${index}-description`, label: "Description (optional)", type: "textarea", value: it.description ?? "", onChange: (v) => update(index, { description: v }), disabled: locked, placeholder: "What makes this prize special?" }), _jsxs(Row, { gap: "md", children: [_jsx(Div, { className: "flex-1", children: _jsx(FormField, { name: `item-${index}-condition`, label: "Condition", type: "select", value: it.condition, onChange: (v) => update(index, { condition: v }), disabled: locked, options: CONDITION_OPTIONS }) }), _jsx(Div, { className: "flex-1", children: _jsx(FormField, { name: `item-${index}-value`, label: "Estimated value (\u20B9)", type: "number", value: it.estimatedValue != null
82
+ ? String(Math.round(it.estimatedValue / 100))
83
+ : "", onChange: (v) => update(index, {
84
+ estimatedValue: Math.round((parseFloat(v) || 0) * 100),
85
+ }), disabled: locked, placeholder: "2999" }) })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { className: "text-sm font-medium", children: ["Images (", (it.images ?? []).length, "/", MAX_IMAGES_PER_ITEM, ")"] }), _jsx(Row, { gap: "sm", className: "flex-wrap", children: Array.from({ length: MAX_IMAGES_PER_ITEM }).map((_, slot) => {
86
+ const existing = (it.images ?? [])[slot];
87
+ return (_jsxs(Div, { className: "w-40", children: [_jsx(ImageUpload, { currentImage: existing, label: existing ? "Replace" : `Image ${slot + 1}`, onUpload: (file) => onUploadImage(file, it.itemNumber), onChange: (url) => setImage(index, slot, url) }), existing && !locked ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeImage(index, slot), children: "Remove" })) : null] }, slot));
88
+ }) })] }), onUploadVideo ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium", children: "Video (optional)" }), _jsx("input", { type: "file", accept: "video/mp4,video/webm", disabled: locked, onChange: async (e) => {
89
+ const file = e.target.files?.[0];
90
+ if (!file)
91
+ return;
92
+ const url = await onUploadVideo(file, it.itemNumber);
93
+ update(index, { video: { url } });
94
+ } }), it.video?.url ? (_jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)] truncate", children: ["Current: ", it.video.url] })) : null] })) : null] })] }, `prize-item-${it.itemNumber}-${index}`));
95
+ }) }), items.length < MIN_ITEMS ? (_jsxs(Div, { className: "rounded border border-red-400/40 bg-red-50 px-3 py-2 text-sm text-red-900 dark:bg-red-900/30 dark:text-red-100", children: ["At least ", MIN_ITEMS, " prizes are required."] })) : null] }));
96
+ }
97
+ export default PrizeDrawItemsEditor;
@@ -0,0 +1,8 @@
1
+ export interface PrizeDrawsIndexListingProps {
2
+ initialData?: any;
3
+ categorySlug?: string;
4
+ brandName?: string;
5
+ /** When set, the listing is hard-scoped to this store id — overrides URL `storeId`. */
6
+ storeId?: string;
7
+ }
8
+ export declare function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, storeId: forcedStoreId, }: PrizeDrawsIndexListingProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useCallback, useMemo } from "react";
4
+ import { SlidersHorizontal, X } from "lucide-react";
5
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
+ import { useProducts } from "../hooks/useProducts";
7
+ import { Pagination, ListingToolbar } from "../../../ui";
8
+ import { useCategoryTree, categoriesToFacetOptions } from "../../categories/hooks/useCategoryTree";
9
+ import { useBrands } from "../hooks/useBrands";
10
+ import { MarketplacePrizeDrawCard } from "./MarketplacePrizeDrawCard";
11
+ import { ProductFilters } from "./ProductFilters";
12
+ const PRIZE_DRAW_SORT_OPTIONS = [
13
+ { value: "-createdAt", label: "Newest First" },
14
+ { value: "createdAt", label: "Oldest First" },
15
+ { value: "prizeRevealWindowStart", label: "Reveal: Soonest" },
16
+ { value: "-prizeRevealWindowStart", label: "Reveal: Furthest" },
17
+ { value: "pricePerEntry", label: "Entry: Low to High" },
18
+ { value: "-pricePerEntry", label: "Entry: High to Low" },
19
+ ];
20
+ const FILTER_KEYS = [
21
+ "category",
22
+ "brand",
23
+ "minPrice",
24
+ "maxPrice",
25
+ "storeId",
26
+ "prizeRevealStatus",
27
+ ];
28
+ export function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, storeId: forcedStoreId, }) {
29
+ const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
30
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
31
+ const [filterOpen, setFilterOpen] = useState(false);
32
+ const showClosed = table.get("showClosed") === "true";
33
+ const [view, setView] = useState(table.get("view") || "grid");
34
+ const { categories } = useCategoryTree();
35
+ const categoryOptions = categoriesToFacetOptions(categories);
36
+ const { brandOptions } = useBrands();
37
+ const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
38
+ const pendingTable = useMemo(() => ({
39
+ get: (key) => pendingFilters[key] ?? "",
40
+ getNumber: (key, fallback = 0) => {
41
+ const v = pendingFilters[key];
42
+ if (!v)
43
+ return fallback;
44
+ const n = Number(v);
45
+ return isNaN(n) ? fallback : n;
46
+ },
47
+ set: (key, value) => setPendingFilters((p) => ({ ...p, [key]: value })),
48
+ setMany: (updates) => setPendingFilters((p) => ({ ...p, ...updates })),
49
+ clear: (keys) => {
50
+ const ks = keys ?? FILTER_KEYS;
51
+ setPendingFilters((p) => ({
52
+ ...p,
53
+ ...Object.fromEntries(ks.map((k) => [k, ""])),
54
+ }));
55
+ },
56
+ setPage: (_) => { },
57
+ setPageSize: (_) => { },
58
+ setSort: (_) => { },
59
+ buildSieveParams: () => "",
60
+ buildSearchParams: () => "",
61
+ params: new URLSearchParams(),
62
+ }), [pendingFilters]);
63
+ const openFilters = useCallback(() => {
64
+ setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
65
+ setFilterOpen(true);
66
+ }, [table]);
67
+ const applyFilters = useCallback(() => {
68
+ const updates = { page: "1" };
69
+ for (const k of FILTER_KEYS)
70
+ updates[k] = pendingFilters[k] ?? "";
71
+ table.setMany(updates);
72
+ setFilterOpen(false);
73
+ }, [pendingFilters, table]);
74
+ const clearFilters = useCallback(() => {
75
+ setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
76
+ }, []);
77
+ const resetAll = useCallback(() => {
78
+ const updates = { q: "", sort: "", showClosed: "" };
79
+ for (const k of FILTER_KEYS)
80
+ updates[k] = "";
81
+ table.setMany(updates);
82
+ setSearchInput("");
83
+ }, [table]);
84
+ const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
85
+ const hasActiveState = !!table.get("q") ||
86
+ showClosed ||
87
+ table.get("sort") !== "-createdAt" ||
88
+ activeFilterCount > 0;
89
+ const params = {
90
+ q: table.get("q") || undefined,
91
+ category: table.get("category") || undefined,
92
+ categorySlug: categorySlug || undefined,
93
+ brand: brandName || table.get("brand") || undefined,
94
+ minPrice: table.get("minPrice") ? Number(table.get("minPrice")) : undefined,
95
+ maxPrice: table.get("maxPrice") ? Number(table.get("maxPrice")) : undefined,
96
+ storeId: forcedStoreId || table.get("storeId") || undefined,
97
+ sort: table.get("sort") || "-createdAt",
98
+ page: table.getNumber("page", 1),
99
+ perPage: table.getNumber("pageSize", 24),
100
+ listingType: "prize-draw",
101
+ status: showClosed ? undefined : "published",
102
+ };
103
+ const { products: draws, totalPages, page, isLoading } = useProducts(params, { initialData });
104
+ // Client-side filter for prizeRevealStatus until the server query supports it.
105
+ const revealFilter = table.get("prizeRevealStatus");
106
+ const filteredDraws = revealFilter
107
+ ? draws.filter((d) => d.prizeRevealStatus === revealFilter)
108
+ : draws;
109
+ const commitSearch = useCallback(() => {
110
+ table.set("q", searchInput.trim());
111
+ table.setPage(1);
112
+ }, [searchInput, table]);
113
+ const handleSearchKeyDown = (e) => {
114
+ if (e.key === "Enter")
115
+ commitSearch();
116
+ };
117
+ const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4";
118
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search prize draws...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PRIZE_DRAW_SORT_OPTIONS, onSortChange: (v) => {
119
+ table.set("sort", v);
120
+ table.setPage(1);
121
+ }, view: view, onViewChange: (v) => {
122
+ setView(v);
123
+ table.set("view", v);
124
+ }, onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show closed" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showClosed, onClick: () => table.set("showClosed", showClosed ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showClosed ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showClosed ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : filteredDraws.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No prize draws found." })) : (_jsx("div", { className: gridClass, children: filteredDraws.map((product) => (_jsx(MarketplacePrizeDrawCard, { product: product, variant: view }, product.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "prizeRevealStatusFilter", className: "block text-xs font-semibold text-zinc-700 dark:text-zinc-300 mb-1.5", children: "Reveal status" }), _jsxs("select", { id: "prizeRevealStatusFilter", value: pendingFilters.prizeRevealStatus ?? "", onChange: (e) => setPendingFilters((p) => ({
125
+ ...p,
126
+ prizeRevealStatus: e.target.value,
127
+ })), className: "w-full rounded border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-2 py-1.5 text-sm", children: [_jsx("option", { value: "", children: "Any" }), _jsx("option", { value: "pending", children: "Reveal pending" }), _jsx("option", { value: "open", children: "Reveal open" }), _jsx("option", { value: "closed", children: "Closed" })] })] }), _jsx(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions })] }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
128
+ }
@@ -0,0 +1,15 @@
1
+ type SearchParams = Record<string, string | string[]>;
2
+ export interface PrizeDrawsListingViewProps {
3
+ searchParams?: SearchParams;
4
+ }
5
+ /**
6
+ * Public listing page (SB4-F). Server-fetches published prize-draw products,
7
+ * hydrates the client `PrizeDrawsIndexListing` which renders the filter
8
+ * toolbar + collage-thumb grid. URL params: `?storeId=…&prizeRevealStatus=…`.
9
+ *
10
+ * Per the public-buyer contract, the product adapter strips `isWon` from
11
+ * `prizeDrawItems[]` server-side; the cards never reveal which prizes are
12
+ * already gone (matches `PrizeDrawCollage`'s `hideWonState` prop).
13
+ */
14
+ export declare function PrizeDrawsListingView({ searchParams, }: PrizeDrawsListingViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
15
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { productRepository } from "../../../repositories";
3
+ import { Container, Main, Heading, Section, Text } from "../../../ui";
4
+ import { AdSlot } from "../../homepage/components/AdSlot";
5
+ import { parseListingSearchParams } from "../../../utils/listing-params";
6
+ import { PrizeDrawsIndexListing } from "./PrizeDrawsIndexListing";
7
+ const DEFAULT_PAGE = 1;
8
+ const DEFAULT_PAGE_SIZE = 24;
9
+ const DEFAULT_SORT = "-createdAt";
10
+ function sp(params, key) {
11
+ const v = params[key];
12
+ return Array.isArray(v) ? v[0] ?? "" : v ?? "";
13
+ }
14
+ function buildPrizeDrawFilters(params) {
15
+ const parts = ["status==published", "listingType==prize-draw"];
16
+ const minPrice = sp(params, "minPrice");
17
+ const maxPrice = sp(params, "maxPrice");
18
+ if (minPrice)
19
+ parts.push(`pricePerEntry>=${minPrice}`);
20
+ if (maxPrice)
21
+ parts.push(`pricePerEntry<=${maxPrice}`);
22
+ const store = sp(params, "storeId");
23
+ if (store)
24
+ parts.push(`storeId==${store}`);
25
+ const status = sp(params, "prizeRevealStatus");
26
+ if (status)
27
+ parts.push(`prizeRevealStatus==${status}`);
28
+ return parts.join(",");
29
+ }
30
+ /**
31
+ * Public listing page (SB4-F). Server-fetches published prize-draw products,
32
+ * hydrates the client `PrizeDrawsIndexListing` which renders the filter
33
+ * toolbar + collage-thumb grid. URL params: `?storeId=…&prizeRevealStatus=…`.
34
+ *
35
+ * Per the public-buyer contract, the product adapter strips `isWon` from
36
+ * `prizeDrawItems[]` server-side; the cards never reveal which prizes are
37
+ * already gone (matches `PrizeDrawCollage`'s `hideWonState` prop).
38
+ */
39
+ export async function PrizeDrawsListingView({ searchParams = {}, }) {
40
+ const std = parseListingSearchParams(searchParams);
41
+ const sort = std.sorts ?? DEFAULT_SORT;
42
+ const page = std.page ?? DEFAULT_PAGE;
43
+ const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
44
+ const filters = buildPrizeDrawFilters(searchParams);
45
+ const result = await productRepository
46
+ .list({ filters, sorts: sort, page, pageSize })
47
+ .catch(() => null);
48
+ return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-100", children: "Prize Draws" }), _jsx(Text, { className: "mb-6 text-sm text-[var(--appkit-color-text-muted)]", children: "Fair-RNG draws for sealed Pok\u00E9mon, Hot Wheels Super Treasure Hunts, Gundam kits and more. Every winner picked by crypto.randomInt \u2014 proof on GitHub." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(PrizeDrawsIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
49
+ }
@@ -0,0 +1,34 @@
1
+ import type { PrizeDrawItem } from "../schemas/firestore";
2
+ export interface PrizeRevealResponse {
3
+ prizeWon?: {
4
+ itemNumber: number;
5
+ title: string;
6
+ images: string[];
7
+ estimatedValue?: number;
8
+ };
9
+ alreadyRevealed?: boolean;
10
+ rngSourceUrl?: string;
11
+ refunded?: true;
12
+ reason?: string;
13
+ }
14
+ export interface PrizeRevealModalProps {
15
+ open: boolean;
16
+ onClose: () => void;
17
+ /** Items to render in the collage (won state hidden — buyers don't see prior wins). */
18
+ items: PrizeDrawItem[];
19
+ /** Order id used by the reveal endpoint. */
20
+ orderId: string;
21
+ /** Product id used in the reveal URL. */
22
+ productId: string;
23
+ /** Override the default `/api/prize-draws/[id]/reveal` POST call. */
24
+ onReveal?: (args: {
25
+ orderId: string;
26
+ productId: string;
27
+ }) => Promise<PrizeRevealResponse>;
28
+ /** Already-revealed prize, passed in if the order has `prizeWon` populated. */
29
+ initialPrizeWon?: PrizeRevealResponse["prizeWon"];
30
+ /** Public proof-of-fairness URL — shown in the disclaimer. */
31
+ rngSourceUrl?: string;
32
+ }
33
+ export declare function PrizeRevealModal({ open, onClose, items, orderId, productId, onReveal, initialPrizeWon, rngSourceUrl, }: PrizeRevealModalProps): import("react/jsx-runtime").JSX.Element;
34
+ export default PrizeRevealModal;