@mohasinac/appkit 4.53.2 → 4.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  3. package/dist/_internal/server/features/checkout/actions.d.ts +22 -6
  4. package/dist/_internal/server/features/checkout/actions.js +96 -51
  5. package/dist/_internal/server/features/checkout/index.d.ts +1 -1
  6. package/dist/_internal/server/features/checkout/index.js +1 -1
  7. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  8. package/dist/_internal/server/features/orders/adapters.js +1 -3
  9. package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
  10. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  11. package/dist/_internal/server/features/site-settings/adapters.js +1 -1
  12. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  13. package/dist/_internal/server/features/stores/adapters.js +0 -1
  14. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +2 -5
  15. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  16. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  17. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +0 -2
  18. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +0 -1
  19. package/dist/_internal/server/jobs/core/onScamReportCreate.js +0 -1
  20. package/dist/_internal/server/jobs/core/onScamReportRejected.js +0 -1
  21. package/dist/_internal/server/jobs/core/onScamReportVerified.js +0 -1
  22. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +0 -1
  23. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +0 -1
  24. package/dist/_internal/server/notifications/channel-health.d.ts +7 -3
  25. package/dist/_internal/server/notifications/channel-health.js +2 -2
  26. package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
  27. package/dist/_internal/server/notifications/send-guard.js +2 -4
  28. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  29. package/dist/_internal/shared/actions/action-registry.js +128 -28
  30. package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
  31. package/dist/_internal/shared/checkout/order-math.js +1 -1
  32. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  33. package/dist/_internal/shared/features/checkout/config.d.ts +0 -2
  34. package/dist/_internal/shared/features/checkout/config.js +0 -2
  35. package/dist/_internal/shared/features/messaging/config.d.ts +7 -5
  36. package/dist/_internal/shared/features/messaging/config.js +1 -4
  37. package/dist/_internal/shared/features/site-settings/fees.d.ts +0 -2
  38. package/dist/_internal/shared/features/site-settings/fees.js +0 -2
  39. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  40. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  41. package/dist/_internal/shared/fees/calculator.d.ts +0 -8
  42. package/dist/_internal/shared/fees/calculator.js +0 -7
  43. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  44. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  45. package/dist/cli/index.js +0 -2
  46. package/dist/client.d.ts +25 -4
  47. package/dist/client.js +36 -5
  48. package/dist/constants/api-endpoints.d.ts +21 -15
  49. package/dist/constants/api-endpoints.js +7 -8
  50. package/dist/constants/field-names.d.ts +1 -0
  51. package/dist/constants/field-names.js +1 -0
  52. package/dist/constants/notification-types.d.ts +1 -1
  53. package/dist/constants/notification-types.js +1 -1
  54. package/dist/core/integration-keys.d.ts +5 -5
  55. package/dist/core/integration-keys.js +21 -10
  56. package/dist/features/about/components/FeesView.js +0 -6
  57. package/dist/features/about/components/GuidesHubView.js +15 -2
  58. package/dist/features/account/components/NotificationPreferencesPanel.js +3 -4
  59. package/dist/features/account/components/UserOrdersView.js +26 -7
  60. package/dist/features/account/types/index.d.ts +0 -2
  61. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  62. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  63. package/dist/features/admin/actions/notification-actions.d.ts +2 -8
  64. package/dist/features/admin/actions/notification-actions.js +9 -104
  65. package/dist/features/admin/components/AdminGuideHubView.js +1 -8
  66. package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
  67. package/dist/features/admin/components/AdminOrdersView.js +0 -2
  68. package/dist/features/admin/components/AdminSectionsView.js +3 -2
  69. package/dist/features/admin/components/AdminSiteSettingsView.js +22 -68
  70. package/dist/features/admin/components/AdminStoreEditorView.js +72 -34
  71. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  72. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  73. package/dist/features/admin/components/sections/adminSectionsTypes.js +13 -4
  74. package/dist/features/admin/components/sections/icon-options.d.ts +21 -0
  75. package/dist/features/admin/components/sections/icon-options.js +23 -0
  76. package/dist/features/admin/components/sections/sectionTemplates.js +4 -1
  77. package/dist/features/admin/constants/site-settings-tabs.d.ts +7 -12
  78. package/dist/features/admin/constants/site-settings-tabs.js +6 -12
  79. package/dist/features/admin/repository/site-settings.repository.js +1 -1
  80. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  81. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  82. package/dist/features/admin/schemas/admin-user-form.d.ts +0 -61
  83. package/dist/features/admin/schemas/admin-user-form.js +3 -44
  84. package/dist/features/admin/schemas/firestore.d.ts +28 -54
  85. package/dist/features/admin/schemas/firestore.js +40 -18
  86. package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
  87. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  88. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  89. package/dist/features/auth/checkout-value-otp.d.ts +1 -1
  90. package/dist/features/auth/checkout-value-otp.js +1 -1
  91. package/dist/features/auth/permissions/constants.d.ts +29 -2
  92. package/dist/features/auth/permissions/constants.js +67 -1
  93. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  94. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  95. package/dist/features/cart/components/CartDrawer.js +3 -2
  96. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  97. package/dist/features/cart/components/CartPriceBreakdown.d.ts +0 -2
  98. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  99. package/dist/features/cart/components/StoreAddonsPicker.d.ts +2 -5
  100. package/dist/features/cart/components/StoreAddonsPicker.js +4 -5
  101. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  102. package/dist/features/cart/repository/cart.repository.js +1 -2
  103. package/dist/features/cart/schemas/firestore.d.ts +1 -2
  104. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  105. package/dist/features/cart/utils/pending-ops.js +14 -2
  106. package/dist/features/categories/components/BrandDetailPageView.js +2 -1
  107. package/dist/features/categories/components/BundleDetailView.js +2 -1
  108. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  109. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  110. package/dist/features/categories/components/CategoryGrid.js +9 -2
  111. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  112. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  113. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  114. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -1
  115. package/dist/features/events/components/EventCard.d.ts +11 -1
  116. package/dist/features/events/components/EventCard.js +9 -2
  117. package/dist/features/grouped/components/GroupedListingDetailView.js +3 -1
  118. package/dist/features/homepage/components/CTABannerSection.d.ts +3 -1
  119. package/dist/features/homepage/components/CTABannerSection.js +6 -3
  120. package/dist/features/homepage/components/HeroCarousel.js +8 -6
  121. package/dist/features/homepage/components/MarketplaceHomepageView.js +33 -7
  122. package/dist/features/homepage/components/SectionCarousel.js +13 -5
  123. package/dist/features/homepage/components/SecurityHighlightsSection.d.ts +8 -3
  124. package/dist/features/homepage/components/SecurityHighlightsSection.js +2 -2
  125. package/dist/features/homepage/components/StatsCounterSection.d.ts +9 -4
  126. package/dist/features/homepage/components/StatsCounterSection.js +2 -2
  127. package/dist/features/homepage/components/TestimonialsCarousel.js +1 -1
  128. package/dist/features/homepage/components/TrustFeaturesSection.d.ts +18 -4
  129. package/dist/features/homepage/components/TrustFeaturesSection.js +5 -5
  130. package/dist/features/homepage/components/WelcomeSection.d.ts +13 -2
  131. package/dist/features/homepage/components/WelcomeSection.js +3 -3
  132. package/dist/features/homepage/components/WhatsAppCommunitySection.d.ts +28 -0
  133. package/dist/features/homepage/components/WhatsAppCommunitySection.js +42 -6
  134. package/dist/features/homepage/components/WhatsAppGlyph.d.ts +20 -0
  135. package/dist/features/homepage/components/WhatsAppGlyph.js +20 -0
  136. package/dist/features/homepage/constants/hero-carousel-styles.d.ts +8 -6
  137. package/dist/features/homepage/constants/hero-carousel-styles.js +8 -6
  138. package/dist/features/homepage/constants/section-copy.d.ts +12 -10
  139. package/dist/features/homepage/constants/section-copy.js +28 -6
  140. package/dist/features/homepage/constants/stat-icons.d.ts +12 -0
  141. package/dist/features/homepage/constants/stat-icons.js +17 -0
  142. package/dist/features/homepage/constants/trust-icon-aliases.d.ts +8 -0
  143. package/dist/features/homepage/constants/trust-icon-aliases.js +75 -0
  144. package/dist/features/homepage/lib/section-defaults.d.ts +16 -1
  145. package/dist/features/homepage/lib/section-defaults.js +24 -4
  146. package/dist/features/homepage/lib/section-renderer.js +48 -8
  147. package/dist/features/homepage/schemas/firestore.d.ts +18 -1
  148. package/dist/features/layout/AppLayoutShell.js +2 -1
  149. package/dist/features/layout/BottomActions.d.ts +1 -0
  150. package/dist/features/layout/BottomActions.js +330 -0
  151. package/dist/features/layout/BottomChrome.d.ts +45 -0
  152. package/dist/features/layout/BottomChrome.js +5 -17
  153. package/dist/features/layout/SideActionDock.js +36 -13
  154. package/dist/features/layout/index.d.ts +2 -7
  155. package/dist/features/layout/index.js +7 -7
  156. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  157. package/dist/features/layout/useIsDesktop.js +48 -0
  158. package/dist/features/live/components/LiveItemDetailPageView.js +2 -1
  159. package/dist/features/orders/actions/refund-actions.js +0 -1
  160. package/dist/features/orders/components/OrderAddonBadges.d.ts +1 -3
  161. package/dist/features/orders/components/OrderAddonBadges.js +3 -4
  162. package/dist/features/orders/components/OrderPaymentSummary.js +15 -2
  163. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  164. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  165. package/dist/features/orders/components/index.d.ts +2 -1
  166. package/dist/features/orders/components/index.js +1 -1
  167. package/dist/features/orders/repository/orders.repository.d.ts +0 -8
  168. package/dist/features/orders/repository/orders.repository.js +0 -2
  169. package/dist/features/orders/schemas/firestore.d.ts +17 -9
  170. package/dist/features/orders/types/index.d.ts +2 -5
  171. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  172. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  173. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  174. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  175. package/dist/features/parts/components/PartDetailPageView.js +96 -1
  176. package/dist/features/parts/schemas/firestore.d.ts +247 -0
  177. package/dist/features/parts/schemas/firestore.js +6 -6
  178. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  179. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  180. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -3
  181. package/dist/features/products/api/[id]/route.js +8 -1
  182. package/dist/features/products/api/route.js +16 -4
  183. package/dist/features/products/components/AvailabilityScopeHint.js +1 -1
  184. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  185. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  186. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  187. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  188. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  189. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  190. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -1
  191. package/dist/features/products/components/ProductDetailPageView.js +27 -2
  192. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  193. package/dist/features/products/components/ProductGrid.js +22 -5
  194. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  195. package/dist/features/reviews/api/[id]/route.js +83 -23
  196. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  197. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  198. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  199. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  200. package/dist/features/reviews/components/index.d.ts +4 -0
  201. package/dist/features/reviews/components/index.js +2 -0
  202. package/dist/features/scams/schemas/index.d.ts +10 -10
  203. package/dist/features/search/action-index/settings-entries.js +0 -9
  204. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  205. package/dist/features/seller/actions/seller-actions.js +49 -3
  206. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  207. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  208. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  209. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  210. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +1 -6
  211. package/dist/features/store-extensions/repository/store-extensions.repositories.js +1 -10
  212. package/dist/features/store-extensions/schemas/firestore.d.ts +1 -17
  213. package/dist/features/store-extensions/schemas/firestore.js +0 -10
  214. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  215. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  216. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +0 -1
  217. package/dist/features/stores/components/StoreGuideHubView.js +2 -8
  218. package/dist/features/stores/components/StoreSettingsGuideView.js +2 -6
  219. package/dist/features/stores/components/index.d.ts +0 -2
  220. package/dist/features/stores/components/index.js +0 -1
  221. package/dist/features/stores/repository/store.repository.d.ts +1 -26
  222. package/dist/features/stores/repository/store.repository.js +5 -60
  223. package/dist/features/stores/schemas/firestore.d.ts +6 -25
  224. package/dist/features/stores/schemas/firestore.js +6 -3
  225. package/dist/features/stores/schemas/index.d.ts +1 -87
  226. package/dist/features/stores/schemas/index.js +10 -15
  227. package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -48
  228. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +0 -40
  229. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +0 -16
  230. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +1 -1
  231. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +0 -79
  232. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +1 -1
  233. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +415 -69
  234. package/dist/index.d.ts +20 -32
  235. package/dist/index.js +34 -43
  236. package/dist/next/routing/route-map.d.ts +2 -8
  237. package/dist/next/routing/route-map.js +21 -4
  238. package/dist/providers/db-firebase/base.repository.d.ts +1 -1
  239. package/dist/providers/db-firebase/base.repository.js +1 -1
  240. package/dist/providers/payment-razorpay/index.d.ts +125 -0
  241. package/dist/providers/payment-razorpay/index.js +232 -0
  242. package/dist/providers/payment-razorpay/signature.d.ts +53 -0
  243. package/dist/providers/payment-razorpay/signature.js +71 -0
  244. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  245. package/dist/providers/shipping-shiprocket/index.js +356 -0
  246. package/dist/providers.d.ts +3 -0
  247. package/dist/providers.js +3 -0
  248. package/dist/react/hooks/useCanSeePrices.d.ts +0 -27
  249. package/dist/react/hooks/useCanSeePrices.js +25 -0
  250. package/dist/repositories/index.d.ts +1 -1
  251. package/dist/repositories/index.js +1 -1
  252. package/dist/schemas/registry.d.ts +10 -115
  253. package/dist/schemas/registry.js +1 -2
  254. package/dist/security/pii-encrypt.js +1 -1
  255. package/dist/seed/_helpers/category-forest.js +27 -0
  256. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
  257. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  258. package/dist/seed/_helpers/category-parts.js +13412 -3020
  259. package/dist/seed/carousel-slides-seed-data.js +47 -10
  260. package/dist/seed/categories-seed-data.js +48 -6
  261. package/dist/seed/faq-seed-data.js +2 -2
  262. package/dist/seed/homepage-sections-seed-data.js +286 -54
  263. package/dist/seed/index.d.ts +1 -1
  264. package/dist/seed/index.js +1 -1
  265. package/dist/seed/manifest.d.ts +1 -1
  266. package/dist/seed/manifest.js +1 -2
  267. package/dist/seed/model-composition-seed-data.js +6 -8
  268. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  269. package/dist/seed/part-glossary/chunk0.js +35448 -0
  270. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  271. package/dist/seed/part-glossary/chunk1.js +40494 -0
  272. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  273. package/dist/seed/part-glossary/chunk2.js +51242 -0
  274. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  275. package/dist/seed/part-glossary/chunk3.js +53337 -0
  276. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  277. package/dist/seed/part-glossary/chunk4.js +51454 -0
  278. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  279. package/dist/seed/part-glossary-seed-data.js +23 -47483
  280. package/dist/seed/site-settings-seed-data.js +34 -35
  281. package/dist/seed/store-extensions-seed-data.d.ts +1 -2
  282. package/dist/seed/store-extensions-seed-data.js +1 -23
  283. package/dist/seed/stores-seed-data.js +0 -1
  284. package/dist/server-entry.d.ts +1 -1
  285. package/dist/server-entry.js +1 -1
  286. package/dist/server.d.ts +8 -10
  287. package/dist/server.js +16 -22
  288. package/dist/styles.css +392 -43
  289. package/dist/tailwind-utilities.css +1 -1
  290. package/dist/tokens/themes/default-dark.js +1 -1
  291. package/dist/tokens/themes/default-light.js +2 -1
  292. package/dist/tokens/tokens.css +6 -2
  293. package/dist/ui/components/Button.d.ts +11 -0
  294. package/dist/ui/components/Button.js +11 -0
  295. package/dist/ui/components/Button.style.css +29 -0
  296. package/dist/ui/components/CarouselNav.d.ts +57 -0
  297. package/dist/ui/components/CarouselNav.js +25 -0
  298. package/dist/ui/components/CarouselNav.style.css +265 -0
  299. package/dist/ui/components/GatedPrice.js +30 -3
  300. package/dist/ui/components/GatedPrice.style.css +43 -40
  301. package/dist/ui/components/HorizontalScroller.d.ts +12 -1
  302. package/dist/ui/components/HorizontalScroller.js +43 -3
  303. package/dist/ui/components/ImageMosaic.d.ts +8 -1
  304. package/dist/ui/components/ImageMosaic.js +3 -2
  305. package/dist/ui/components/ImageMosaic.style.css +22 -0
  306. package/dist/ui/components/Layout.style.css +6 -0
  307. package/dist/ui/components/Semantic.d.ts +3 -2
  308. package/dist/ui/components/Semantic.js +1 -0
  309. package/dist/ui/components/Typography.d.ts +2 -0
  310. package/dist/ui/components/Typography.js +2 -0
  311. package/dist/ui/components/Typography.style.css +19 -0
  312. package/dist/ui/components/index.style.css +1 -0
  313. package/dist/ui/index.d.ts +3 -1
  314. package/dist/ui/index.js +4 -0
  315. package/package.json +3 -2
  316. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  317. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  318. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  319. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  320. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  321. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  322. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  323. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  324. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  325. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  326. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  327. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  328. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  329. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  330. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  331. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  332. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  333. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  334. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  335. package/dist/features/whatsapp-bot/components/index.js +0 -2
  336. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  337. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  338. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  339. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  340. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  341. package/dist/features/whatsapp-bot/index.js +0 -3
  342. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  343. package/dist/features/whatsapp-bot/manifest.js +0 -13
  344. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  345. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  346. package/dist/features/whatsapp-bot/server.js +0 -7
  347. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  348. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -17,5 +17,15 @@ export interface MarketplaceBundleCardProps {
17
17
  hrefBuilder?: (bundle: MarketplaceBundleCardData) => string;
18
18
  onNavigate?: (href: string) => void;
19
19
  labels?: MarketplaceBundleCardLabels;
20
+ /**
21
+ * Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
22
+ * nothing for a visitor who does not own the record.
23
+ *
24
+ * 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
25
+ * inside an `<a>` is invalid HTML that browsers resolve by closing the
26
+ * anchor early, which breaks the CARD's navigation rather than just the
27
+ * button's — `stopPropagation` stops a click but cannot fix a parse.
28
+ */
29
+ renderOwnerActions?: () => React.ReactNode;
20
30
  }
21
- export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplaceBundleCardProps): import("react").JSX.Element;
31
+ export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplaceBundleCardProps): import("react").JSX.Element;
@@ -27,7 +27,7 @@ function resolveHref(bundle, href, hrefBuilder) {
27
27
  return hrefBuilder(bundle);
28
28
  return ROUTES.PUBLIC.BUNDLE_DETAIL(bundle.slug);
29
29
  }
30
- export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
30
+ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
31
31
  const router = useRouter();
32
32
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
33
33
  const detailHref = resolveHref(bundle, href, hrefBuilder);
@@ -49,7 +49,7 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
49
49
  }
50
50
  router.push(String(detailHref));
51
51
  }, [detailHref, onNavigate, router]);
52
- 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, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(PricesOnly, { children: _jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
52
+ const card = (_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, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(PricesOnly, { children: _jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
53
53
  event.stopPropagation();
54
54
  onSelect(bundle.id, !isSelected);
55
55
  }, className: selectable || isSelected
@@ -58,4 +58,11 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
58
58
  ? formatCurrency(price, getDefaultCurrency())
59
59
  : "—" }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, getDefaultCurrency()) }))] }) }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.itemsLabel(memberCount) })] }), _jsx(Div, { className: CLS_VIEW_BTN, role: "button", tabIndex: 0, onClick: handleNavigate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ")
60
60
  handleNavigate(); }, children: "View Bundle" })] })] }));
61
+ if (!renderOwnerActions)
62
+ return card;
63
+ /*
64
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
65
+ * clears this card's own overlays, which all sit at `z-10`.
66
+ */
67
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
61
68
  }
@@ -20,5 +20,15 @@ export interface MarketplacePrizeDrawCardProps {
20
20
  hrefBuilder?: (product: MarketplacePrizeDrawCardData) => string;
21
21
  onNavigate?: (href: string) => void;
22
22
  labels?: MarketplacePrizeDrawCardLabels;
23
+ /**
24
+ * Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
25
+ * nothing for a visitor who does not own the record.
26
+ *
27
+ * 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
28
+ * inside an `<a>` is invalid HTML that browsers resolve by closing the
29
+ * anchor early, which breaks the CARD's navigation rather than just the
30
+ * button's — `stopPropagation` stops a click but cannot fix a parse.
31
+ */
32
+ renderOwnerActions?: () => React.ReactNode;
23
33
  }
24
- export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
34
+ export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
@@ -65,7 +65,7 @@ function formatCountdown(target) {
65
65
  const minutes = Math.floor((diff % 3600000) / 60000);
66
66
  return `${hours}h ${minutes}m`;
67
67
  }
68
- export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
68
+ export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
69
69
  const router = useRouter();
70
70
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
71
71
  const detailHref = resolveHref(product, href, hrefBuilder);
@@ -90,7 +90,7 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
90
90
  }
91
91
  router.push(String(detailHref));
92
92
  }, [detailHref, onNavigate, router]);
93
- 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, onTouchCancel: !isSelected ? longPress.onTouchCancel : 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, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
93
+ const card = (_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, onTouchCancel: !isSelected ? longPress.onTouchCancel : 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, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
94
94
  const it = thumbItems[i];
95
95
  const img = it?.images?.[0];
96
96
  return (_jsx(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: img ? (_jsx(MediaImage, { src: img, alt: it?.title ?? `Prize ${i + 1}`, size: "card" })) : null }, `thumb-${i}`));
@@ -100,4 +100,11 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
100
100
  }, className: selectable || isSelected
101
101
  ? "opacity-100"
102
102
  : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsxs(GatedPrice, { label: "Sign in to see the entry price", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }) }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
103
+ if (!renderOwnerActions)
104
+ return card;
105
+ /*
106
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
107
+ * clears this card's own overlays, which all sit at `z-10`.
108
+ */
109
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
103
110
  }
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link.js";
3
+ import { OwnerQuickActions } from "../../../features/admin/components/OwnerQuickActions.js";
3
4
  import { orderRepository, productRepository } from "../../../repositories/index.js";
4
5
  const __P = {
5
6
  p3: "p-[var(--appkit-space-3)]",
@@ -131,5 +132,5 @@ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUse
131
132
  price: pricePerEntry,
132
133
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
133
134
  storeName: storeName ?? undefined,
134
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Prize Draws" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", 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", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "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-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-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-[var(--appkit-color-text-muted)]" })) : undefined })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
135
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Prize Draws" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(product.title ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))) }), _jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Prize Draw" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "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-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-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-[var(--appkit-color-text-muted)]" })) : undefined })), renderBuyBar: () => (_jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(PrizeDrawBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
135
136
  }
@@ -31,6 +31,9 @@ import { ctaAnchorProps } from "../../layout/cta-anchor.js";
31
31
  import { ContextualTrail } from "../../categories/components/ContextualTrail.js";
32
32
  import { productCrumbs, withTrailHead } from "../../categories/utils/breadcrumb-chain.js";
33
33
  import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
34
+ import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
35
+ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
36
+ import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
34
37
  import { Button, Container, Div, Heading, Main, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, SpecTable, Icon, } from "../../../ui/index.js";
35
38
  import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
36
39
  import { safeDisplayName } from "../../../security/index.js";
@@ -350,7 +353,29 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
350
353
  price: product.price,
351
354
  storeId: product.storeId,
352
355
  storeName: product.storeName,
353
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
356
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: title || String(product.slug ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))), onClone: async () => {
357
+ const res = await fetch(SELLER_ENDPOINTS.PRODUCT_DUPLICATE(String(product.id)), { method: "POST" });
358
+ if (!res.ok)
359
+ return;
360
+ const json = (await res.json());
361
+ const newId = json?.data?.id;
362
+ /*
363
+ * Land on the COPY's editor. The endpoint returns the new
364
+ * draft's id precisely so a one-click clone ends somewhere
365
+ * useful rather than back on the public page, where the
366
+ * draft is invisible.
367
+ */
368
+ if (newId && typeof window !== "undefined") {
369
+ window.location.assign(String(ROUTES.STORE.PRODUCTS_EDIT(newId)));
370
+ }
371
+ }, onDelete: async () => {
372
+ await fetch(SELLER_ENDPOINTS.PRODUCT_BY_ID(String(product.id)), {
373
+ method: "DELETE",
374
+ });
375
+ if (typeof window !== "undefined") {
376
+ window.location.assign(String(ROUTES.PUBLIC.PRODUCTS));
377
+ }
378
+ } }), _jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
354
379
  ? "Brand New"
355
380
  : condition === "like_new"
356
381
  ? "Like New"
@@ -421,7 +446,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
421
446
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
422
447
  : undefined, renderGroupSection: groupId
423
448
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
424
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) })), renderBundleSection: bundleMemberships.length > 0
449
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsxs(Stack, { gap: "md", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." })] }), customTabs: toCustomSectionTabs(p.customSections) })), renderBundleSection: bundleMemberships.length > 0
425
450
  ? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
426
451
  : undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryNames[0] || categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
427
452
  ? renderPrimaryActions({
@@ -16,8 +16,21 @@ interface ProductCardProps<T extends ProductItem = ProductItem> {
16
16
  selectionMode?: boolean;
17
17
  isSelected?: boolean;
18
18
  onSelect?: (id: string) => void;
19
+ /**
20
+ * Owner-only pencil / clone / bin, for an admin or the seller who owns this
21
+ * listing. Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
22
+ *
23
+ * 🛑 Rendered OUTSIDE this card's `<Link>`, at `top-14 right-2` — below the
24
+ * wishlist heart and clear of the checkbox, badge stack and IconOverlay. See
25
+ * the return statement for why it cannot live inside the card body.
26
+ *
27
+ * Suppressed while a selection is active: a bulk-select gesture and a
28
+ * per-record edit are different intents, and offering both at once invites
29
+ * the wrong one.
30
+ */
31
+ renderOwnerActions?: () => React.ReactNode;
19
32
  }
20
- export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, }: ProductCardProps<T>): React.JSX.Element;
33
+ export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, renderOwnerActions, }: ProductCardProps<T>): React.JSX.Element;
21
34
  export interface ProductCardContext<T extends ProductItem = ProductItem> {
22
35
  onClick?: (product: T) => void;
23
36
  onWishlistToggle?: (productId: string) => void;
@@ -45,7 +45,7 @@ const CLS_DISCOUNT_TEXT = "mt-1 text-[11px] font-semibold text-error";
45
45
  const CLS_DISCOUNT_TEXT_BARE = "text-[10px] text-error";
46
46
  const CLS_STAR_BARE = "text-warning";
47
47
  const CLS_BID_TEXT = "text-[11px] text-error";
48
- export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, }) {
48
+ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, renderOwnerActions, }) {
49
49
  const discount = product.originalPrice && product.originalPrice > product.price
50
50
  ? Math.round(((product.originalPrice - product.price) / product.originalPrice) *
51
51
  100)
@@ -124,10 +124,27 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
124
124
  e.preventDefault();
125
125
  onAddToCart(product);
126
126
  }, className: "justify-center text-[length:var(--appkit-text-xs)]", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
127
- if (href && !selectionMode) {
128
- return (_jsx(Link, { href: href, className: "block h-full", children: cardBody }));
129
- }
130
- return cardBody;
127
+ const wrapped = href && !selectionMode ? (_jsx(Link, { href: href, className: "block h-full", children: cardBody })) : (cardBody);
128
+ if (!renderOwnerActions || selectionMode || isSelected)
129
+ return wrapped;
130
+ /*
131
+ * 🛑 OUTSIDE the anchor, as a sibling — never inside `cardBody`.
132
+ *
133
+ * This card wraps its entire body in a `<Link>` whenever `href` is set, and
134
+ * `InteractiveProductCard` wraps it in another one. A `<button>` inside an
135
+ * `<a>` is invalid HTML that browsers resolve by closing the anchor early,
136
+ * which breaks the CARD's navigation rather than just the button's —
137
+ * stopPropagation stops the click but cannot fix the parse.
138
+ *
139
+ * Positioned `top-14 right-2`: directly below the 44px wishlist heart at
140
+ * `top-2 right-2`, so it clears every occupied corner (checkbox and badges
141
+ * top-left, IconOverlay bottom-right) without needing to know the hero's
142
+ * height. The hero is `aspect-square`, so it is always taller than
143
+ * 3.5rem + 44px and the toolbar lands on the photo.
144
+ *
145
+ * `z-20` beats the in-card overlays, which all sit at `z-10`.
146
+ */
147
+ return (_jsxs(Div, { className: "relative h-full", children: [wrapped, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: renderOwnerActions() })] }));
131
148
  }
132
149
  function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
133
150
  const discount = product.originalPrice && product.originalPrice > product.price
@@ -221,9 +221,9 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
221
221
  itemCount: items.reduce((sum, it) => sum + it.quantity, 0),
222
222
  totalValue: items.reduce((sum, it) => sum + (it.price ?? 0) * it.quantity, 0),
223
223
  };
224
- showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency())), "success");
224
+ showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency()), canSeePrices), "success");
225
225
  dispatchCartUpdated(detail);
226
- }, [localCart, showToast]);
226
+ }, [localCart, showToast, canSeePrices]);
227
227
  const handleBuyNow = useCallback((product) => {
228
228
  requireAuth(ACTION_ID.BUY_NOW, () => {
229
229
  const snapshot = {
@@ -11,8 +11,21 @@
11
11
  import { NextResponse } from "next/server.js";
12
12
  import { z } from "zod";
13
13
  import { mediaUrlSchema } from "../../../../validation/schemas.js";
14
- import { getProviders } from "../../../../contracts/index.js";
15
14
  import { createRouteHandler } from "../../../../next/api/routeHandler.js";
15
+ /*
16
+ * 🛑 The real repository, not `db.getRepository<Review>("reviews")`.
17
+ *
18
+ * The generic FirebaseRepository knows nothing about this collection, so every
19
+ * read and write through it skipped ReviewRepository's overrides:
20
+ *
21
+ * - `update` encrypts `userName` (PII) and rebuilds `searchTxt`. Writing
22
+ * through the generic repo left the tokens describing the OLD text, so an
23
+ * edited review was findable only by what it used to say.
24
+ * - `mapDoc` decrypts `userName` and strips `helpfulVoterIds`. Reading
25
+ * through the generic repo returned the name as raw `enc:v1:…` ciphertext
26
+ * and published the voter list.
27
+ */
28
+ import { reviewRepository } from "../../repository/reviews.repository.js";
16
29
  import { normalizeError } from "../../../../errors/normalize.js";
17
30
  import { isAdminUser, isModeratorUser } from "../../../auth/role-predicates.js";
18
31
  const ERR_REVIEW_NOT_FOUND = "Review not found";
@@ -20,12 +33,14 @@ const ERR_REVIEW_NOT_FOUND = "Review not found";
20
33
  export async function GET(_request, context) {
21
34
  try {
22
35
  const { id } = await context.params;
23
- const { db } = getProviders();
24
- if (!db) {
25
- return NextResponse.json({ success: false, error: "Database provider not registered" }, { status: 503 });
26
- }
27
- const repo = db.getRepository("reviews");
28
- const review = await repo.findById(id);
36
+ /*
37
+ * Through `reviewRepository`, so `mapDoc` runs: `userName` is decrypted
38
+ * and `helpfulVoterIds` is stripped. The generic repo did neither, so this
39
+ * public endpoint returned the reviewer's name as raw `enc:v1:…`
40
+ * ciphertext and published the list of everyone who had voted the review
41
+ * helpful.
42
+ */
43
+ const review = await reviewRepository.findById(id);
29
44
  if (!review) {
30
45
  return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
31
46
  }
@@ -39,6 +54,19 @@ export async function GET(_request, context) {
39
54
  }
40
55
  export { GET as reviewItemGET };
41
56
  // ---- PATCH /api/reviews/[id] -------------------------------------------------
57
+ /*
58
+ * 🛑 `.strict()`, not `.passthrough()`.
59
+ *
60
+ * It was passthrough, so any unlisted key reached the write — including
61
+ * `userId`, `verified`, `helpfulCount` and `featured`. A reviewer could have
62
+ * marked their own review a verified purchase, or featured it on the homepage,
63
+ * by adding one field to the request body. Nothing in the handler below looked
64
+ * at the extra keys; they went straight into Firestore.
65
+ *
66
+ * `.strict()` rejects them with a 400 naming the field rather than silently
67
+ * dropping them, which is the honest answer to a caller sending something this
68
+ * endpoint does not own.
69
+ */
42
70
  const reviewUpdateSchema = z
43
71
  .object({
44
72
  rating: z
@@ -53,10 +81,28 @@ const reviewUpdateSchema = z
53
81
  title: z.string().min(1).max(100).optional(),
54
82
  comment: z.string().min(1).max(2000).optional(),
55
83
  images: z.array(mediaUrlSchema).max(10).optional(),
56
- video: mediaUrlSchema.optional(),
57
84
  status: z.enum(["pending", "approved", "rejected"]).optional(),
58
85
  })
59
- .passthrough();
86
+ .strict();
87
+ /*
88
+ * 🛑 `video` is deliberately NOT accepted here, and that is a fix rather than
89
+ * an omission.
90
+ *
91
+ * It used to be declared as a bare `mediaUrlSchema` — a URL string — while
92
+ * `ReviewDocument.video` is a `ReviewVideoField` object. So a caller sending
93
+ * the real shape failed validation, and a caller sending a string wrote a
94
+ * string into a field every renderer reads as an object, leaving `video.url`
95
+ * undefined and the player with nothing.
96
+ *
97
+ * Accepting the object instead would still be wrong on this route: the URL
98
+ * would arrive as an unfinalised `tmp/` reference, because nothing here calls
99
+ * `finalizeStagedMediaUrl`. `updateReview` (features/reviews/actions) is the
100
+ * path that owns review media — it finalises the upload and builds the full
101
+ * field — and `REVIEW_UPDATABLE_FIELDS` already documents the updatable set as
102
+ * rating / title / comment / images, with no video in it.
103
+ *
104
+ * One owner for media writes, and this is not it.
105
+ */
60
106
  export const reviewItemPATCH = createRouteHandler({
61
107
  auth: true,
62
108
  schema: reviewUpdateSchema,
@@ -64,11 +110,7 @@ export const reviewItemPATCH = createRouteHandler({
64
110
  const url = new URL(request.url);
65
111
  const segments = url.pathname.split("/");
66
112
  const id = segments[segments.length - 1];
67
- const { db } = getProviders();
68
- if (!db)
69
- return NextResponse.json({ success: false, error: "DB not configured" }, { status: 503 });
70
- const repo = db.getRepository("reviews");
71
- const review = await repo.findById(id);
113
+ const review = await reviewRepository.findById(id);
72
114
  if (!review)
73
115
  return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
74
116
  const isOwner = review.userId === user.uid;
@@ -77,13 +119,34 @@ export const reviewItemPATCH = createRouteHandler({
77
119
  if (!isOwner && !isModerator && !isAdmin) {
78
120
  return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
79
121
  }
80
- // Owners can only edit content fields, not status
122
+ /*
123
+ * Owners edit content, not moderation state. Pinning `status` rather than
124
+ * rejecting the field is deliberate and pre-existing: an approved review
125
+ * edited by its author stays approved and is NOT sent back for
126
+ * re-moderation.
127
+ */
81
128
  const patch = isOwner && !isModerator && !isAdmin
82
129
  ? { ...body, status: review.status }
83
130
  : body;
84
- const updated = await repo.update(id, {
131
+ /*
132
+ * 🛑 `hasImages` is DERIVED and has to move with `images`, or the two
133
+ * disagree. It is a real queryable field (`hasImages` is `canFilter` in
134
+ * SIEVE_FIELDS and backs the "with photos" review filter), so a review
135
+ * whose last photo was removed stayed in that filter for ever, and one
136
+ * that gained its first was absent from it.
137
+ */
138
+ const nextImages = body?.images;
139
+ const derived = nextImages !== undefined ? { hasImages: nextImages.length > 0 } : {};
140
+ /*
141
+ * 🛑 A `Date`, not an ISO string. Everything else in this codebase writes
142
+ * `Date` to `updatedAt`, and `reviewRepository`'s own Sieve config marks it
143
+ * sortable — so a string here sorts lexicographically beside real
144
+ * timestamps and silently orders wrong.
145
+ */
146
+ const updated = await reviewRepository.update(id, {
85
147
  ...patch,
86
- updatedAt: new Date().toISOString(),
148
+ ...derived,
149
+ updatedAt: new Date(),
87
150
  });
88
151
  return NextResponse.json({ success: true, data: updated });
89
152
  },
@@ -95,11 +158,8 @@ export const reviewItemDELETE = createRouteHandler({
95
158
  const url = new URL(request.url);
96
159
  const segments = url.pathname.split("/");
97
160
  const id = segments[segments.length - 1];
98
- const { db } = getProviders();
99
- if (!db)
100
- return NextResponse.json({ success: false, error: "DB not configured" }, { status: 503 });
101
- const repo = db.getRepository("reviews");
102
- const review = await repo.findById(id);
161
+ // Same repository as GET and PATCH — one collection, one repository.
162
+ const review = await reviewRepository.findById(id);
103
163
  if (!review)
104
164
  return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
105
165
  const isOwner = review.userId === user.uid;
@@ -108,7 +168,7 @@ export const reviewItemDELETE = createRouteHandler({
108
168
  if (!isOwner && !isModerator && !isAdmin) {
109
169
  return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
110
170
  }
111
- await repo.delete(id);
171
+ await reviewRepository.delete(id);
112
172
  return NextResponse.json({ success: true });
113
173
  },
114
174
  });
@@ -0,0 +1,6 @@
1
+ export interface PurchaseReviewAffordanceProps {
2
+ productId: string;
3
+ storeId?: string;
4
+ className?: string;
5
+ }
6
+ export declare function PurchaseReviewAffordance({ productId, storeId, className, }: PurchaseReviewAffordanceProps): import("react").JSX.Element;
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * The strip and the review drawer, connected.
5
+ *
6
+ * Exists because the two halves need shared client state (which review is
7
+ * open) while the product detail page that mounts them is an async Server
8
+ * Component. One small client island is cheaper than making the detail view
9
+ * take two render-prop slots and thread state between them.
10
+ *
11
+ * @tag domain:reviews
12
+ * @tag layer:components
13
+ * @tag access:client
14
+ */
15
+ import { useCallback, useState } from "react";
16
+ import { useQueryClient } from "@tanstack/react-query";
17
+ import { YourPurchaseStrip } from "../../orders/components/YourPurchaseStrip.js";
18
+ import { ReviewFormDrawer } from "./ReviewFormDrawer.js";
19
+ export function PurchaseReviewAffordance({ productId, storeId, className, }) {
20
+ const queryClient = useQueryClient();
21
+ const [open, setOpen] = useState(false);
22
+ const [editingId, setEditingId] = useState(undefined);
23
+ /*
24
+ * Invalidate the strip's own query, not just the reviews list. The strip is
25
+ * what shows "Your review: 4/5", so saving without invalidating it leaves
26
+ * the buyer looking at their old rating and reasonably concluding the edit
27
+ * did not take.
28
+ */
29
+ const handleSaved = useCallback(() => {
30
+ void queryClient.invalidateQueries({
31
+ queryKey: ["user", "purchases", productId],
32
+ });
33
+ void queryClient.invalidateQueries({ queryKey: ["reviews"] });
34
+ }, [queryClient, productId]);
35
+ return (_jsxs(_Fragment, { children: [_jsx(YourPurchaseStrip, { productId: productId, className: className, onEditReview: (reviewId) => {
36
+ setEditingId(reviewId);
37
+ setOpen(true);
38
+ }, onWriteReview: () => {
39
+ setEditingId(undefined);
40
+ setOpen(true);
41
+ } }), _jsx(ReviewFormDrawer, { isOpen: open, onClose: () => setOpen(false), reviewId: editingId, productId: productId, storeId: storeId, onSaved: handleSaved })] }));
42
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Mirrors the PATCH route's schema, which is `.strict()`. A field this form can
4
+ * send that the route does not accept is a 400 naming it — which is the point:
5
+ * the two were allowed to disagree before, and the route silently accepted
6
+ * anything.
7
+ */
8
+ export declare const reviewFormSchema: z.ZodObject<{
9
+ rating: z.ZodNumber;
10
+ title: z.ZodString;
11
+ comment: z.ZodString;
12
+ }, "strip", z.ZodTypeAny, {
13
+ title: string;
14
+ rating: number;
15
+ comment: string;
16
+ }, {
17
+ title: string;
18
+ rating: number;
19
+ comment: string;
20
+ }>;
21
+ export type ReviewFormValues = z.infer<typeof reviewFormSchema>;
22
+ export interface ReviewFormDrawerProps {
23
+ isOpen: boolean;
24
+ onClose: () => void;
25
+ /** Present = editing that review. Absent = writing a new one. */
26
+ reviewId?: string;
27
+ /** Required when writing; the review is filed against this product. */
28
+ productId?: string;
29
+ storeId?: string;
30
+ initialValues?: Partial<ReviewFormValues>;
31
+ /** Called after a successful save, so the caller can refetch. */
32
+ onSaved?: () => void;
33
+ }
34
+ export declare function ReviewFormDrawer({ isOpen, onClose, reviewId, productId, storeId, initialValues, onSaved, }: ReviewFormDrawerProps): import("react").JSX.Element;