@mohasinac/appkit 2.7.59 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/dist/_internal/client/features/filters/filter-load-options.d.ts +4 -5
  2. package/dist/_internal/server/features/auctions/og.js +8 -32
  3. package/dist/_internal/server/features/blog/og.js +10 -49
  4. package/dist/_internal/server/features/checkout/actions.js +6 -0
  5. package/dist/_internal/server/features/classified/og.js +9 -51
  6. package/dist/_internal/server/features/digital-code/actions.d.ts +11 -0
  7. package/dist/_internal/server/features/digital-code/actions.js +30 -0
  8. package/dist/_internal/server/features/digital-code/index.d.ts +1 -0
  9. package/dist/_internal/server/features/digital-code/index.js +1 -0
  10. package/dist/_internal/server/features/digital-code/og.js +9 -51
  11. package/dist/_internal/server/features/events/og.js +10 -49
  12. package/dist/_internal/server/features/item-requests/data.d.ts +17 -0
  13. package/dist/_internal/server/features/item-requests/data.js +20 -0
  14. package/dist/_internal/server/features/item-requests/index.d.ts +2 -0
  15. package/dist/_internal/server/features/item-requests/index.js +2 -0
  16. package/dist/_internal/server/features/item-requests/og.d.ts +40 -0
  17. package/dist/_internal/server/features/item-requests/og.js +136 -0
  18. package/dist/_internal/server/features/live/actions.d.ts +5 -0
  19. package/dist/_internal/server/features/live/actions.js +24 -0
  20. package/dist/_internal/server/features/live/index.d.ts +1 -0
  21. package/dist/_internal/server/features/live/index.js +1 -0
  22. package/dist/_internal/server/features/live/og.js +9 -51
  23. package/dist/_internal/server/features/media/contextGuards.js +19 -8
  24. package/dist/_internal/server/features/pre-orders/og.js +8 -32
  25. package/dist/_internal/server/features/prize-draws/data.d.ts +17 -0
  26. package/dist/_internal/server/features/prize-draws/data.js +20 -0
  27. package/dist/_internal/server/features/prize-draws/index.d.ts +2 -0
  28. package/dist/_internal/server/features/prize-draws/index.js +2 -0
  29. package/dist/_internal/server/features/prize-draws/og.d.ts +30 -0
  30. package/dist/_internal/server/features/prize-draws/og.js +50 -0
  31. package/dist/_internal/server/features/products/og.js +8 -33
  32. package/dist/_internal/server/features/seo/og-layout.d.ts +42 -0
  33. package/dist/_internal/server/features/seo/og-layout.js +69 -0
  34. package/dist/_internal/server/features/stores/og.js +8 -41
  35. package/dist/_internal/server/features/sublisting-categories/og.js +11 -42
  36. package/dist/client.d.ts +10 -6
  37. package/dist/client.js +7 -3
  38. package/dist/constants/index.d.ts +4 -0
  39. package/dist/constants/index.js +2 -0
  40. package/dist/constants/notification-types.d.ts +20 -0
  41. package/dist/constants/notification-types.js +37 -0
  42. package/dist/constants/pagination-presets.d.ts +39 -0
  43. package/dist/constants/pagination-presets.js +38 -0
  44. package/dist/errors/messages.d.ts +0 -4
  45. package/dist/errors/messages.js +0 -4
  46. package/dist/features/account/components/UserOffersPanel.js +1 -1
  47. package/dist/features/account/components/UserSupportView.d.ts +1 -2
  48. package/dist/features/account/components/UserSupportView.js +9 -1
  49. package/dist/features/admin/actions/admin-actions.js +16 -1
  50. package/dist/features/admin/components/AdminAllEventEntriesView.d.ts +2 -3
  51. package/dist/features/admin/components/AdminAllEventEntriesView.js +50 -83
  52. package/dist/features/admin/components/AdminBidsView.d.ts +2 -3
  53. package/dist/features/admin/components/AdminBidsView.js +64 -98
  54. package/dist/features/admin/components/AdminBlogView.d.ts +3 -7
  55. package/dist/features/admin/components/AdminBlogView.js +53 -83
  56. package/dist/features/admin/components/AdminBrandsView.d.ts +2 -3
  57. package/dist/features/admin/components/AdminBrandsView.js +84 -83
  58. package/dist/features/admin/components/AdminBundlesView.js +82 -125
  59. package/dist/features/admin/components/AdminCarouselView.d.ts +2 -2
  60. package/dist/features/admin/components/AdminCarouselView.js +17 -52
  61. package/dist/features/admin/components/AdminCartsView.d.ts +2 -3
  62. package/dist/features/admin/components/AdminCartsView.js +39 -88
  63. package/dist/features/admin/components/AdminCategoriesView.d.ts +3 -3
  64. package/dist/features/admin/components/AdminCategoriesView.js +98 -92
  65. package/dist/features/admin/components/AdminCategoryEditorView.js +2 -2
  66. package/dist/features/admin/components/AdminClassifiedView.d.ts +3 -0
  67. package/dist/features/admin/components/AdminClassifiedView.js +44 -0
  68. package/dist/features/admin/components/AdminContactView.d.ts +2 -2
  69. package/dist/features/admin/components/AdminContactView.js +104 -111
  70. package/dist/features/admin/components/AdminCouponsView.d.ts +3 -6
  71. package/dist/features/admin/components/AdminCouponsView.js +66 -86
  72. package/dist/features/admin/components/AdminDigitalCodesView.d.ts +3 -0
  73. package/dist/features/admin/components/AdminDigitalCodesView.js +51 -0
  74. package/dist/features/admin/components/AdminFaqsView.d.ts +4 -4
  75. package/dist/features/admin/components/AdminFaqsView.js +77 -33
  76. package/dist/features/admin/components/AdminFeaturesView.d.ts +2 -3
  77. package/dist/features/admin/components/AdminFeaturesView.js +57 -70
  78. package/dist/features/admin/components/AdminGroupedListingsView.d.ts +3 -0
  79. package/dist/features/admin/components/AdminGroupedListingsView.js +43 -0
  80. package/dist/features/admin/components/AdminHistoryView.d.ts +2 -3
  81. package/dist/features/admin/components/AdminHistoryView.js +35 -52
  82. package/dist/features/admin/components/AdminListingScaffold.d.ts +2 -2
  83. package/dist/features/admin/components/AdminListingScaffold.js +3 -3
  84. package/dist/features/admin/components/AdminLiveView.d.ts +3 -0
  85. package/dist/features/admin/components/AdminLiveView.js +50 -0
  86. package/dist/features/admin/components/AdminNewsletterView.d.ts +2 -2
  87. package/dist/features/admin/components/AdminNewsletterView.js +67 -97
  88. package/dist/features/admin/components/AdminNotificationsView.d.ts +2 -3
  89. package/dist/features/admin/components/AdminNotificationsView.js +67 -93
  90. package/dist/features/admin/components/AdminOrderEditorView.js +10 -10
  91. package/dist/features/admin/components/AdminOrdersView.d.ts +2 -3
  92. package/dist/features/admin/components/AdminOrdersView.js +93 -107
  93. package/dist/features/admin/components/AdminPayoutsView.d.ts +2 -3
  94. package/dist/features/admin/components/AdminPayoutsView.js +51 -91
  95. package/dist/features/admin/components/AdminPrizeDrawsView.d.ts +2 -3
  96. package/dist/features/admin/components/AdminPrizeDrawsView.js +39 -76
  97. package/dist/features/admin/components/AdminProductEditorView.js +3 -3
  98. package/dist/features/admin/components/AdminProductsView.d.ts +4 -4
  99. package/dist/features/admin/components/AdminProductsView.js +167 -163
  100. package/dist/features/admin/components/AdminReturnRequestsView.d.ts +1 -1
  101. package/dist/features/admin/components/AdminReturnRequestsView.js +64 -66
  102. package/dist/features/admin/components/AdminReviewsView.d.ts +3 -7
  103. package/dist/features/admin/components/AdminReviewsView.js +94 -110
  104. package/dist/features/admin/components/AdminScammersView.d.ts +2 -3
  105. package/dist/features/admin/components/AdminScammersView.js +53 -97
  106. package/dist/features/admin/components/AdminSectionsView.js +1 -1
  107. package/dist/features/admin/components/AdminSessionsView.d.ts +2 -3
  108. package/dist/features/admin/components/AdminSessionsView.js +62 -89
  109. package/dist/features/admin/components/AdminStoreAddressesView.d.ts +1 -1
  110. package/dist/features/admin/components/AdminStoreAddressesView.js +39 -53
  111. package/dist/features/admin/components/AdminStoreEditorView.js +21 -8
  112. package/dist/features/admin/components/AdminStoresView.d.ts +2 -3
  113. package/dist/features/admin/components/AdminStoresView.js +67 -99
  114. package/dist/features/admin/components/AdminSublistingCategoriesView.d.ts +1 -1
  115. package/dist/features/admin/components/AdminSublistingCategoriesView.js +28 -44
  116. package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +10 -1
  117. package/dist/features/admin/components/AdminSupportTicketDetailView.js +191 -5
  118. package/dist/features/admin/components/AdminSupportTicketsView.d.ts +2 -3
  119. package/dist/features/admin/components/AdminSupportTicketsView.js +65 -96
  120. package/dist/features/admin/components/AdminTeamView.d.ts +2 -2
  121. package/dist/features/admin/components/AdminTeamView.js +40 -74
  122. package/dist/features/admin/components/AdminUserEditorView.d.ts +11 -1
  123. package/dist/features/admin/components/AdminUserEditorView.js +80 -6
  124. package/dist/features/admin/components/AdminUsersView.d.ts +2 -3
  125. package/dist/features/admin/components/AdminUsersView.js +108 -100
  126. package/dist/features/admin/components/AdminViewCards.d.ts +1 -1
  127. package/dist/features/admin/components/AdminWishlistsView.d.ts +2 -3
  128. package/dist/features/admin/components/AdminWishlistsView.js +36 -62
  129. package/dist/features/admin/components/DataListingView.d.ts +122 -0
  130. package/dist/features/admin/components/DataListingView.js +66 -0
  131. package/dist/features/admin/components/index.d.ts +10 -4
  132. package/dist/features/admin/components/index.js +6 -2
  133. package/dist/features/admin/constants/filter-tabs.d.ts +23 -17
  134. package/dist/features/admin/constants/filter-tabs.js +21 -17
  135. package/dist/features/admin/hooks/useAdminListing.d.ts +1 -0
  136. package/dist/features/admin/hooks/useAdminListing.js +1 -1
  137. package/dist/features/admin/schemas/firestore.js +6 -7
  138. package/dist/features/auctions/server.d.ts +0 -1
  139. package/dist/features/auctions/server.js +0 -1
  140. package/dist/features/auth/actions/realtime-token-actions.js +1 -1
  141. package/dist/features/auth/repository/user.repository.js +1 -1
  142. package/dist/features/cart/repository/cart.repository.d.ts +4 -1
  143. package/dist/features/cart/repository/cart.repository.js +12 -0
  144. package/dist/features/checkout/hooks/useCheckoutApi.d.ts +0 -2
  145. package/dist/features/checkout/index.d.ts +1 -1
  146. package/dist/features/events/components/AdminEventsView.d.ts +3 -3
  147. package/dist/features/events/components/AdminEventsView.js +50 -86
  148. package/dist/features/events/components/EventOfferCard.d.ts +19 -0
  149. package/dist/features/events/components/EventOfferCard.js +29 -0
  150. package/dist/features/events/components/EventRaffleEntryForm.d.ts +25 -0
  151. package/dist/features/events/components/EventRaffleEntryForm.js +37 -0
  152. package/dist/features/events/components/EventRafflesSection.d.ts +1 -4
  153. package/dist/features/events/components/EventRafflesSection.js +17 -6
  154. package/dist/features/events/components/EventSaleBanner.d.ts +21 -0
  155. package/dist/features/events/components/EventSaleBanner.js +18 -0
  156. package/dist/features/events/components/index.d.ts +6 -0
  157. package/dist/features/events/components/index.js +4 -0
  158. package/dist/features/events/repository/event-entry.repository.d.ts +5 -0
  159. package/dist/features/events/repository/event-entry.repository.js +7 -0
  160. package/dist/features/filters/AsyncFacetSection.d.ts +1 -1
  161. package/dist/features/homepage/components/FAQSection.d.ts +0 -5
  162. package/dist/features/homepage/lib/live-stats.js +1 -1
  163. package/dist/features/homepage/schemas/firestore.d.ts +0 -2
  164. package/dist/features/layout/index.d.ts +0 -2
  165. package/dist/features/layout/index.js +0 -1
  166. package/dist/features/pre-orders/server.d.ts +0 -1
  167. package/dist/features/pre-orders/server.js +0 -1
  168. package/dist/features/products/components/GroupSettingsPanel.js +2 -2
  169. package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
  170. package/dist/features/products/components/PrizeDrawsSection.d.ts +1 -4
  171. package/dist/features/products/components/PrizeDrawsSection.js +17 -6
  172. package/dist/features/products/components/PrizeRevealModal.js +1 -1
  173. package/dist/features/products/components/ProductCardMetadataSection.d.ts +11 -0
  174. package/dist/features/products/components/ProductCardMetadataSection.js +25 -0
  175. package/dist/features/products/components/ProductDetailPageView.js +3 -1
  176. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  177. package/dist/features/products/components/ProductGradingTab.d.ts +13 -0
  178. package/dist/features/products/components/ProductGradingTab.js +25 -0
  179. package/dist/features/products/components/ProductGrid.js +14 -2
  180. package/dist/features/products/components/ProductTabsShell.d.ts +5 -1
  181. package/dist/features/products/components/ProductTabsShell.js +6 -1
  182. package/dist/features/products/components/index.d.ts +4 -0
  183. package/dist/features/products/components/index.js +3 -0
  184. package/dist/features/products/constants/action-defs.d.ts +5 -0
  185. package/dist/features/products/constants/action-defs.js +52 -32
  186. package/dist/features/products/constants/sieve.d.ts +24 -4
  187. package/dist/features/products/constants/sieve.js +20 -11
  188. package/dist/features/products/schemas/firestore.d.ts +17 -0
  189. package/dist/features/products/schemas/product-features.validators.d.ts +2 -2
  190. package/dist/features/products/schemas/product-templates.d.ts +2 -4
  191. package/dist/features/products/types/index.d.ts +4 -0
  192. package/dist/features/promotions/server.d.ts +0 -1
  193. package/dist/features/promotions/server.js +0 -1
  194. package/dist/features/search/api/route.js +44 -1
  195. package/dist/features/search/components/Search.js +2 -0
  196. package/dist/features/search/hooks/useNavSuggestions.d.ts +5 -1
  197. package/dist/features/search/hooks/useNavSuggestions.js +31 -4
  198. package/dist/features/seller/components/BrandInlineSelect.js +2 -2
  199. package/dist/features/seller/components/CategoryInlineSelect.js +2 -2
  200. package/dist/features/seller/components/SellerAddressesView.js +1 -1
  201. package/dist/features/seller/components/SellerAuctionsView.d.ts +3 -3
  202. package/dist/features/seller/components/SellerAuctionsView.js +60 -90
  203. package/dist/features/seller/components/SellerBidsView.js +1 -1
  204. package/dist/features/seller/components/SellerBundlesView.js +64 -89
  205. package/dist/features/seller/components/SellerClassifiedView.js +67 -90
  206. package/dist/features/seller/components/SellerCouponEditorView.js +1 -1
  207. package/dist/features/seller/components/SellerCouponsView.d.ts +2 -2
  208. package/dist/features/seller/components/SellerCouponsView.js +45 -84
  209. package/dist/features/seller/components/SellerDigitalCodesView.js +63 -88
  210. package/dist/features/seller/components/SellerGroupedListingsView.js +33 -49
  211. package/dist/features/seller/components/SellerLiveView.js +63 -84
  212. package/dist/features/seller/components/SellerOffersPanel.js +1 -1
  213. package/dist/features/seller/components/SellerOffersView.d.ts +2 -2
  214. package/dist/features/seller/components/SellerOffersView.js +37 -69
  215. package/dist/features/seller/components/SellerOrdersView.d.ts +2 -2
  216. package/dist/features/seller/components/SellerOrdersView.js +4 -4
  217. package/dist/features/seller/components/SellerPayoutMethodsView.js +1 -1
  218. package/dist/features/seller/components/SellerPayoutsView.d.ts +2 -2
  219. package/dist/features/seller/components/SellerPayoutsView.js +34 -68
  220. package/dist/features/seller/components/SellerPreOrdersView.d.ts +2 -2
  221. package/dist/features/seller/components/SellerPreOrdersView.js +49 -82
  222. package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +2 -2
  223. package/dist/features/seller/components/SellerPrizeDrawsView.js +49 -82
  224. package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
  225. package/dist/features/seller/components/SellerProductsView.d.ts +2 -2
  226. package/dist/features/seller/components/SellerProductsView.js +15 -10
  227. package/dist/features/seller/components/SellerShippingConfigsView.js +1 -1
  228. package/dist/features/seller/components/SellerSidebar.d.ts +0 -4
  229. package/dist/features/seller/components/SellerSidebar.js +0 -2
  230. package/dist/features/seller/components/SellerStoreCategoriesView.js +1 -1
  231. package/dist/features/seller/components/SellerTemplatesView.js +2 -2
  232. package/dist/features/seller/components/SellersListView.d.ts +1 -2
  233. package/dist/features/seller/components/analytics/SellerAnalyticsStats.d.ts +5 -0
  234. package/dist/features/seller/components/analytics/SellerAnalyticsStats.js +2 -8
  235. package/dist/features/seller/components/index.d.ts +0 -3
  236. package/dist/features/seller/components/index.js +0 -2
  237. package/dist/features/seller/server.d.ts +0 -1
  238. package/dist/features/seller/server.js +0 -1
  239. package/dist/features/shell/FormShell.d.ts +1 -1
  240. package/dist/features/shell/FormShell.js +4 -1
  241. package/dist/features/shell/field-groups/ImageFieldGroup.d.ts +18 -0
  242. package/dist/features/shell/field-groups/ImageFieldGroup.js +15 -0
  243. package/dist/features/shell/field-groups/SeoFieldGroup.d.ts +22 -0
  244. package/dist/features/shell/field-groups/SeoFieldGroup.js +16 -0
  245. package/dist/features/shell/field-groups/StatusFieldGroup.d.ts +35 -0
  246. package/dist/features/shell/field-groups/StatusFieldGroup.js +16 -0
  247. package/dist/features/shell/field-groups/TitleDescriptionGroup.d.ts +40 -0
  248. package/dist/features/shell/field-groups/TitleDescriptionGroup.js +16 -0
  249. package/dist/features/shell/field-groups/index.d.ts +13 -0
  250. package/dist/features/shell/field-groups/index.js +9 -0
  251. package/dist/features/shell/index.d.ts +2 -0
  252. package/dist/features/shell/index.js +1 -0
  253. package/dist/features/stores/components/StoreCapabilitiesGuideView.d.ts +1 -2
  254. package/dist/features/stores/components/StoreFinanceGuideView.d.ts +1 -2
  255. package/dist/features/stores/components/StoreHeader.js +2 -1
  256. package/dist/features/stores/components/StoreListingsGuideView.d.ts +1 -2
  257. package/dist/features/stores/components/StoreOrdersGuideView.d.ts +1 -2
  258. package/dist/features/stores/components/StoreScopedSearch.d.ts +16 -0
  259. package/dist/features/stores/components/StoreScopedSearch.js +27 -0
  260. package/dist/features/stores/components/StoreSettingsGuideView.d.ts +1 -2
  261. package/dist/features/stores/server.d.ts +0 -1
  262. package/dist/features/stores/server.js +0 -1
  263. package/dist/features/support/schemas/firestore.d.ts +22 -1
  264. package/dist/features/support/schemas/firestore.js +10 -0
  265. package/dist/features/wishlist/server.d.ts +0 -1
  266. package/dist/features/wishlist/server.js +0 -1
  267. package/dist/index.d.ts +9 -27
  268. package/dist/index.js +14 -50
  269. package/dist/next/routing/route-map.d.ts +8 -0
  270. package/dist/next/routing/route-map.js +4 -0
  271. package/dist/react/hooks/useListingTypeFlags.d.ts +32 -0
  272. package/dist/react/hooks/useListingTypeFlags.js +19 -0
  273. package/dist/security/index.d.ts +1 -1
  274. package/dist/security/index.js +1 -1
  275. package/dist/security/pii-redact.d.ts +8 -0
  276. package/dist/security/pii-redact.js +10 -1
  277. package/dist/seed/grouped-listings-seed-data.d.ts +4 -0
  278. package/dist/seed/products-preorders-seed-data.d.ts +4 -0
  279. package/dist/seed/products-prize-draws-seed-data.js +3 -0
  280. package/dist/seed/site-settings-seed-data.js +5 -5
  281. package/dist/seed/sub-listings-seed-data.d.ts +4 -0
  282. package/dist/server.d.ts +2 -0
  283. package/dist/server.js +4 -0
  284. package/dist/styles.css +54 -395
  285. package/dist/tailwind-utilities.css +1 -1
  286. package/dist/ui/components/BaseListingCard.d.ts +10 -0
  287. package/dist/ui/components/BaseListingCard.js +17 -1
  288. package/dist/ui/components/DetailPageGallery.d.ts +32 -0
  289. package/dist/ui/components/DetailPageGallery.js +47 -0
  290. package/dist/ui/components/DetailPageHero.d.ts +30 -0
  291. package/dist/ui/components/DetailPageHero.js +22 -0
  292. package/dist/ui/components/DetailPageTabs.d.ts +34 -0
  293. package/dist/ui/components/DetailPageTabs.js +38 -0
  294. package/dist/ui/components/HorizontalScroller.style.css +15 -2
  295. package/dist/ui/components/ListingLayout.d.ts +11 -2
  296. package/dist/ui/components/ListingLayout.js +12 -6
  297. package/dist/ui/components/PaginatedSelect.d.ts +66 -0
  298. package/dist/ui/components/PaginatedSelect.js +201 -0
  299. package/dist/ui/components/PaginatedSelect.style.css +254 -0
  300. package/dist/ui/components/Semantic.d.ts +0 -4
  301. package/dist/ui/components/Semantic.js +0 -2
  302. package/dist/ui/components/SettingsSection.d.ts +47 -0
  303. package/dist/ui/components/SettingsSection.js +38 -0
  304. package/dist/ui/components/index.style.css +1 -4
  305. package/dist/ui/index.d.ts +12 -14
  306. package/dist/ui/index.js +6 -7
  307. package/dist/utils/id-generators.d.ts +1 -18
  308. package/dist/utils/id-generators.js +51 -18
  309. package/dist/validation/schemas.d.ts +25 -0
  310. package/dist/validation/schemas.js +28 -0
  311. package/package.json +1 -1
@@ -2,16 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
3
  import { Container, Heading, Section, Stack, Text, } from "../../../ui";
4
4
  import { ROUTES } from "../../../next";
5
+ import { eventRepository } from "../repository/events.repository";
6
+ import { EventCard } from "./EventCard";
5
7
  /**
6
8
  * Renders a live-raffle/spin-wheel strip on the homepage.
7
- *
8
- * Reads events with `hasRaffle=true` and status in {"active","upcoming"}. The
9
- * `hasRaffle` flag is added later with the raffle feature; for now the
10
- * component renders an empty state and the section ships disabled in seed.
9
+ * W1-38 (2026-05-23): fetches events with hasRaffle=true and renders the EventCard grid.
11
10
  */
12
11
  export async function EventRafflesSection({ config, }) {
13
12
  const title = config.title ?? "Live Raffles & Spin Wheels";
14
13
  const subtitle = config.subtitle ?? "Participate in community events and win prizes";
15
- const events = [];
16
- return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-50", children: title }), subtitle ? (_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: subtitle })) : null] }), events.length === 0 ? (_jsxs(Stack, { align: "center", gap: "sm", className: "rounded-2xl border border-dashed border-zinc-300 px-6 py-12 text-center dark:border-slate-700", children: [_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No upcoming raffles \u2014 keep an eye on the events page." }), _jsx(Link, { href: String(ROUTES.PUBLIC.EVENTS), className: "text-sm font-medium text-primary hover:underline", children: "Browse all events \u2192" })] })) : null] }) }) }));
14
+ const limit = 6;
15
+ let events = [];
16
+ try {
17
+ const result = await eventRepository.list({
18
+ filters: "hasRaffle==true,status==active",
19
+ sorts: "-startsAt",
20
+ pageSize: limit,
21
+ });
22
+ events = (result.items ?? []);
23
+ }
24
+ catch {
25
+ events = [];
26
+ }
27
+ return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-50", children: title }), subtitle ? (_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: subtitle })) : null] }), events.length === 0 ? (_jsxs(Stack, { align: "center", gap: "sm", className: "rounded-2xl border border-dashed border-zinc-300 px-6 py-12 text-center dark:border-slate-700", children: [_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No upcoming raffles \u2014 keep an eye on the events page." }), _jsx(Link, { href: String(ROUTES.PUBLIC.EVENTS), className: "text-sm font-medium text-primary hover:underline", children: "Browse all events \u2192" })] })) : (_jsx("div", { className: "fluid-grid-card gap-3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) }))] }) }) }));
17
28
  }
@@ -0,0 +1,21 @@
1
+ export interface EventSaleBannerProps {
2
+ /** Discount percent (e.g. 20 = 20% off). */
3
+ discountPercent?: number;
4
+ /** Coupon code (when the sale is gated by a code). */
5
+ couponCode?: string;
6
+ /** Sale headline override. */
7
+ title?: string;
8
+ /** Sale subheading override. */
9
+ subtitle?: string;
10
+ /** Filter the catalog page to a specific category / tag. */
11
+ filterParam?: {
12
+ key: string;
13
+ value: string;
14
+ };
15
+ }
16
+ /**
17
+ * `EventSaleBanner` — W1-18 — sale event UI noted as missing in the plan.
18
+ * Renders a hero banner with the discount, optional coupon, and a CTA to the
19
+ * filtered products catalog.
20
+ */
21
+ export declare function EventSaleBanner({ discountPercent, couponCode, title, subtitle, filterParam, }: EventSaleBannerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import Link from "next/link";
4
+ import { Div, Heading, Stack, Text } from "../../../ui";
5
+ import { Button } from "../../../ui/components/Button";
6
+ import { ROUTES } from "../../../next/routing/route-map";
7
+ /**
8
+ * `EventSaleBanner` — W1-18 — sale event UI noted as missing in the plan.
9
+ * Renders a hero banner with the discount, optional coupon, and a CTA to the
10
+ * filtered products catalog.
11
+ */
12
+ export function EventSaleBanner({ discountPercent, couponCode, title, subtitle, filterParam, }) {
13
+ const headline = title ?? (discountPercent ? `${discountPercent}% off, today only` : "Special sale");
14
+ const href = filterParam
15
+ ? `${ROUTES.PUBLIC.PRODUCTS}?${encodeURIComponent(filterParam.key)}=${encodeURIComponent(filterParam.value)}`
16
+ : `${ROUTES.PUBLIC.PRODUCTS}?onSale=true`;
17
+ return (_jsx(Div, { className: "rounded-2xl bg-gradient-to-br from-[var(--appkit-color-primary)] to-[var(--appkit-color-secondary)] p-6 text-white shadow-lg", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-3xl font-bold", children: headline }), subtitle ? _jsx(Text, { className: "text-base text-white/90", children: subtitle }) : null] }), couponCode ? (_jsxs(Div, { className: "inline-flex items-center gap-3 rounded-lg bg-white/10 px-4 py-2 backdrop-blur", children: [_jsx("span", { className: "text-xs uppercase tracking-wide opacity-80", children: "Code" }), _jsx("span", { className: "font-mono text-lg font-bold", children: couponCode })] })) : null, _jsx(Div, { children: _jsx(Link, { href: href, children: _jsx(Button, { variant: "secondary", children: "Shop the sale" }) }) })] }) }));
18
+ }
@@ -29,3 +29,9 @@ export { EventRaffleWinnerView } from "./EventRaffleWinnerView";
29
29
  export type { EventRaffleWinnerViewProps } from "./EventRaffleWinnerView";
30
30
  export { SpinWheelView } from "./SpinWheelView";
31
31
  export type { SpinWheelViewProps } from "./SpinWheelView";
32
+ export { EventRaffleEntryForm } from "./EventRaffleEntryForm";
33
+ export type { EventRaffleEntryFormProps } from "./EventRaffleEntryForm";
34
+ export { EventSaleBanner } from "./EventSaleBanner";
35
+ export type { EventSaleBannerProps } from "./EventSaleBanner";
36
+ export { EventOfferCard } from "./EventOfferCard";
37
+ export type { EventOfferCardProps } from "./EventOfferCard";
@@ -15,3 +15,7 @@ export { EventPollWidget } from "./EventPollWidget";
15
15
  export { EventRafflesSection } from "./EventRafflesSection";
16
16
  export { EventRaffleWinnerView } from "./EventRaffleWinnerView";
17
17
  export { SpinWheelView } from "./SpinWheelView";
18
+ // W1-18: Event participation flows (raffle entry / sale banner / offer coupon)
19
+ export { EventRaffleEntryForm } from "./EventRaffleEntryForm";
20
+ export { EventSaleBanner } from "./EventSaleBanner";
21
+ export { EventOfferCard } from "./EventOfferCard";
@@ -7,6 +7,11 @@ declare class EventEntryRepository extends BaseRepository<EventEntryDocument> {
7
7
  protected mapDoc<D = EventEntryDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
8
8
  listForEvent(eventId: string, model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
9
9
  listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
10
+ /**
11
+ * W1-42 — admin-facing list (all events, all users). Filterable on eventId,
12
+ * userId, reviewStatus, submittedAt; sortable on submittedAt + points.
13
+ */
14
+ list(model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
10
15
  hasUserEntered(eventId: string, userId: string): Promise<boolean>;
11
16
  countUserEntries(eventId: string, userId: string): Promise<number>;
12
17
  getLeaderboard(eventId: string, limit?: number): Promise<import("../types").LeaderboardEntry[]>;
@@ -23,6 +23,13 @@ class EventEntryRepository extends BaseRepository {
23
23
  baseQuery: this.getCollection().where(EVENT_ENTRY_FIELDS.USER_ID, "==", userId),
24
24
  });
25
25
  }
26
+ /**
27
+ * W1-42 — admin-facing list (all events, all users). Filterable on eventId,
28
+ * userId, reviewStatus, submittedAt; sortable on submittedAt + points.
29
+ */
30
+ async list(model) {
31
+ return this.sieveQuery(model, EventEntryRepository.SIEVE_FIELDS);
32
+ }
26
33
  async hasUserEntered(eventId, userId) {
27
34
  try {
28
35
  const snapshot = await this.getCollection()
@@ -1,5 +1,5 @@
1
1
  import type { FacetOption } from "./FilterFacetSection";
2
- import type { AsyncPage } from "../../ui/components/DynamicSelect";
2
+ import type { AsyncPage } from "../../ui/components/PaginatedSelect";
3
3
  export interface AsyncFacetSectionProps {
4
4
  title: string;
5
5
  selected: string[];
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  export interface FAQTab {
3
2
  value: string;
4
3
  label: string;
@@ -26,10 +25,6 @@ export interface FAQSectionProps {
26
25
  viewMoreLabel?: string;
27
26
  hasMore?: boolean;
28
27
  moreCount?: number;
29
- /** @deprecated Tabs are now derived internally or from the tabs prop */
30
- activeTab?: string;
31
- /** @deprecated Use showCategoryTabs + tabs props instead */
32
- renderTabs?: () => React.ReactNode;
33
28
  className?: string;
34
29
  }
35
30
  export declare function FAQSection({ title, subtitle, items, tabs: tabsProp, showCategoryTabs, allowMultipleOpen, defaultOpenCount, viewMoreHref, viewMoreLabel, hasMore, moreCount, className, }: FAQSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -70,7 +70,7 @@ export async function fetchLiveStats(requests) {
70
70
  tasks.push((async () => {
71
71
  try {
72
72
  const db = getAdminDb();
73
- let ref = db.collection(collection);
73
+ const ref = db.collection(collection);
74
74
  const query = filterField !== undefined && filterValue !== undefined
75
75
  ? ref.where(filterField, "==", filterValue)
76
76
  : ref;
@@ -61,8 +61,6 @@ export interface CarouselCard {
61
61
  /** Legacy — kept for backward compat; use `zone` instead */
62
62
  gridCol?: 1 | 2 | 3;
63
63
  }
64
- /** @deprecated Use CarouselCard. This alias is kept for backward compatibility. */
65
- export type GridCard = CarouselCard;
66
64
  export type GridCardCreateInput = Omit<CarouselCard, "id">;
67
65
  /** A named carousel that holds up to MAX_SLIDES_PER_CAROUSEL slide references. */
68
66
  export interface CarouselDocument {
@@ -22,8 +22,6 @@ export { LocaleSwitcher } from "./LocaleSwitcher";
22
22
  export type { LocaleSwitcherProps, LocaleSwitcherOption, } from "./LocaleSwitcher";
23
23
  export { BackToTop, SkipToMain } from "./BackToTop";
24
24
  export type { BackToTopProps } from "./BackToTop";
25
- export { ListingLayout } from "./ListingLayout";
26
- export type { ListingLayoutProps, ListingLayoutLabels } from "./ListingLayout";
27
25
  export { LayoutClient } from "./LayoutClient";
28
26
  export type { LayoutClientProps, LayoutProvider } from "./LayoutClient";
29
27
  export { AppLayoutShell } from "./AppLayoutShell";
@@ -12,7 +12,6 @@ export { AutoBreadcrumbs } from "./AutoBreadcrumbs";
12
12
  export { Breadcrumbs, BreadcrumbItem } from "./Breadcrumbs";
13
13
  export { LocaleSwitcher } from "./LocaleSwitcher";
14
14
  export { BackToTop, SkipToMain } from "./BackToTop";
15
- export { ListingLayout } from "./ListingLayout";
16
15
  export { LayoutClient } from "./LayoutClient";
17
16
  export { AppLayoutShell } from "./AppLayoutShell";
18
17
  export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext";
@@ -3,5 +3,4 @@
3
3
  *
4
4
  * Server-only entry point — repositories and API route handlers.
5
5
  */
6
- export { PreordersRepository } from "./repository/preorders.repository";
7
6
  export { GET as preOrdersGET, POST as preOrdersPOST, GET, POST, } from "./api/route";
@@ -3,5 +3,4 @@
3
3
  *
4
4
  * Server-only entry point — repositories and API route handlers.
5
5
  */
6
- export { PreordersRepository } from "./repository/preorders.repository";
7
6
  export { GET as preOrdersGET, POST as preOrdersPOST, GET, POST, } from "./api/route";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button, ConfirmDeleteModal, Div, FormField, Heading, Modal, Row, Select, SideDrawer, Stack, Text, Tabs, TabsContent, TabsList, TabsTrigger, DynamicSelect, useToast, } from "../../../ui";
4
+ import { Button, ConfirmDeleteModal, Div, FormField, Heading, Modal, Row, Select, SideDrawer, Stack, Text, Tabs, TabsContent, TabsList, TabsTrigger, PaginatedSelect, useToast, } from "../../../ui";
5
5
  import { apiClient } from "../../../http";
6
6
  const CONDITION_OPTIONS = [
7
7
  { value: "new", label: "New" },
@@ -204,5 +204,5 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
204
204
  _jsx("img", { src: child.images[0], alt: child.title, className: "w-8 h-8 rounded-full object-cover border border-zinc-200 dark:border-zinc-700" })) : (_jsx("div", { className: "w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-800" })), _jsx(Text, { className: "text-sm text-zinc-800 dark:text-zinc-200 flex-1 truncate", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: "text-red-500 hover:text-red-600 text-xs", children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", className: "flex-wrap", children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600", children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { className: "text-sm text-zinc-600 dark:text-zinc-400", children: ["Part of:", " ", _jsx("span", { className: "font-medium text-zinc-800 dark:text-zinc-200", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: ["Parent listing: ", _jsx("code", { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600 w-fit", children: "Leave group" })] }))] })), (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })) : (_jsx(Modal, { open: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", size: "lg", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })), confirmAction && (_jsx(ConfirmDeleteModal, { isOpen: true, title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onClose: () => setConfirmAction(null), isDeleting: loading }))] }));
205
205
  }
206
206
  function AddChildContent({ addTab, setAddTab, createForm, setCreateForm, productSlug, linkTarget, setLinkTarget, loadLinkOptions, onAddCreate, onAddLink, loading, }) {
207
- return (_jsx(Stack, { gap: "md", children: _jsxs(Tabs, { value: addTab, onChange: (v) => setAddTab(v), children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "create", children: "Create new child" }), _jsx(TabsTrigger, { value: "link", children: "Link existing" })] }), _jsx(TabsContent, { value: "create", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(FormField, { name: "childTitle", label: "Title *", type: "text", value: createForm.title, onChange: (v) => setCreateForm({ ...createForm, title: v }), placeholder: `${productSlug}-part` }), _jsx(FormField, { name: "childPrice", label: "Price (\u20B9) *", type: "number", value: createForm.price, onChange: (v) => setCreateForm({ ...createForm, price: v }), placeholder: "0" }), _jsx(Select, { label: "Condition", value: createForm.condition, onValueChange: (v) => setCreateForm({ ...createForm, condition: v }), options: CONDITION_OPTIONS }), _jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400/80", children: "Other fields (category, brand, shipping, return policy) are inherited from this parent listing. Need more control? Edit the full listing after saving." }), _jsx(Button, { type: "button", onClick: onAddCreate, isLoading: loading, disabled: !createForm.title || !createForm.price, children: "Create and link child" })] }) }), _jsx(TabsContent, { value: "link", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Search your existing products or pre-orders. Auctions cannot be linked." }), _jsx(DynamicSelect, { value: linkTarget, onChange: (v) => setLinkTarget(v), loadOptions: loadLinkOptions, placeholder: "Search listings\u2026", searchPlaceholder: "Type title or slug\u2026", noResultsText: "No matching listings found", ariaLabel: "Listing to link" }), _jsx(Button, { type: "button", onClick: onAddLink, isLoading: loading, disabled: !linkTarget, children: "Link to group" })] }) })] }) }));
207
+ return (_jsx(Stack, { gap: "md", children: _jsxs(Tabs, { value: addTab, onChange: (v) => setAddTab(v), children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "create", children: "Create new child" }), _jsx(TabsTrigger, { value: "link", children: "Link existing" })] }), _jsx(TabsContent, { value: "create", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(FormField, { name: "childTitle", label: "Title *", type: "text", value: createForm.title, onChange: (v) => setCreateForm({ ...createForm, title: v }), placeholder: `${productSlug}-part` }), _jsx(FormField, { name: "childPrice", label: "Price (\u20B9) *", type: "number", value: createForm.price, onChange: (v) => setCreateForm({ ...createForm, price: v }), placeholder: "0" }), _jsx(Select, { label: "Condition", value: createForm.condition, onValueChange: (v) => setCreateForm({ ...createForm, condition: v }), options: CONDITION_OPTIONS }), _jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400/80", children: "Other fields (category, brand, shipping, return policy) are inherited from this parent listing. Need more control? Edit the full listing after saving." }), _jsx(Button, { type: "button", onClick: onAddCreate, isLoading: loading, disabled: !createForm.title || !createForm.price, children: "Create and link child" })] }) }), _jsx(TabsContent, { value: "link", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Search your existing products or pre-orders. Auctions cannot be linked." }), _jsx(PaginatedSelect, { value: linkTarget, onChange: (v) => setLinkTarget(v), loadOptions: loadLinkOptions, placeholder: "Search listings\u2026", searchPlaceholder: "Type title or slug\u2026", noResultsText: "No matching listings found", ariaLabel: "Listing to link" }), _jsx(Button, { type: "button", onClick: onAddLink, isLoading: loading, disabled: !linkTarget, children: "Link to group" })] }) })] }) }));
208
208
  }
@@ -84,7 +84,7 @@ export function PrizeDrawItemsEditor({ items, onChange, onUploadImage, onUploadV
84
84
  const nextImgs = (cur.images ?? []).filter((_, i) => i !== slot);
85
85
  update(index, { images: nextImgs });
86
86
  };
87
- 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-error-surface px-3 py-2 text-sm text-error", children: [_jsx(Span, { weight: "bold", 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) => {
87
+ 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-error/40 bg-error-surface px-3 py-2 text-sm text-error", children: [_jsx(Span, { weight: "bold", 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) => {
88
88
  const locked = it.isWon || anyWon;
89
89
  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
90
90
  ? String(Math.round(it.estimatedValue / 100))
@@ -94,6 +94,6 @@ export function PrizeDrawItemsEditor({ items, onChange, onUploadImage, onUploadV
94
94
  const existing = (it.images ?? [])[slot];
95
95
  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));
96
96
  }) })] }), onUploadVideo ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium", children: "Video (optional)" }), _jsx("input", { type: "file", accept: "video/*", disabled: locked, onChange: (e) => handleVideoFileChange(e, index, it.itemNumber) }), 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}`));
97
- }) }), items.length < MIN_ITEMS ? (_jsxs(Div, { className: "rounded border border-red-400/40 bg-error-surface px-3 py-2 text-sm text-error", children: ["At least ", MIN_ITEMS, " prizes are required."] })) : null] }));
97
+ }) }), items.length < MIN_ITEMS ? (_jsxs(Div, { className: "rounded border border-error/40 bg-error-surface px-3 py-2 text-sm text-error", children: ["At least ", MIN_ITEMS, " prizes are required."] })) : null] }));
98
98
  }
99
99
  export default PrizeDrawItemsEditor;
@@ -4,9 +4,6 @@ export interface PrizeDrawsSectionProps {
4
4
  }
5
5
  /**
6
6
  * Renders a prize-draws strip on the homepage.
7
- *
8
- * Reads products with `listingType="prize-draw"` and `prizeRevealStatus` in the
9
- * configured set. Those fields are added later in the prize-draw block; for now
10
- * the component renders an empty state and the section ships disabled in seed.
7
+ * W1-38 (2026-05-23): fetches active prize-draw listings from productRepository.
11
8
  */
12
9
  export declare function PrizeDrawsSection({ config, }: PrizeDrawsSectionProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -2,16 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
3
  import { Container, Heading, Section, Stack, Text, } from "../../../ui";
4
4
  import { ROUTES } from "../../../next";
5
+ import { productRepository } from "../repository/products.repository";
6
+ import { InteractiveProductCard } from "./InteractiveProductCard";
5
7
  /**
6
8
  * Renders a prize-draws strip on the homepage.
7
- *
8
- * Reads products with `listingType="prize-draw"` and `prizeRevealStatus` in the
9
- * configured set. Those fields are added later in the prize-draw block; for now
10
- * the component renders an empty state and the section ships disabled in seed.
9
+ * W1-38 (2026-05-23): fetches active prize-draw listings from productRepository.
11
10
  */
12
11
  export async function PrizeDrawsSection({ config, }) {
13
12
  const title = config.title ?? "Prize Draws";
14
13
  const subtitle = config.subtitle ?? "Enter for a chance to win rare collectibles";
15
- const draws = [];
16
- return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-50", children: title }), subtitle ? (_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: subtitle })) : null] }), draws.length === 0 ? (_jsxs(Stack, { align: "center", gap: "sm", className: "rounded-2xl border border-dashed border-zinc-300 px-6 py-12 text-center dark:border-slate-700", children: [_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No active prize draws \u2014 new draws are announced regularly." }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: "text-sm font-medium text-primary hover:underline", children: "Browse live auctions \u2192" })] })) : null] }) }) }));
14
+ const limit = 8;
15
+ let draws = [];
16
+ try {
17
+ const result = await productRepository.list({
18
+ filters: "listingType==prize-draw,status==published",
19
+ sorts: "-createdAt",
20
+ pageSize: limit,
21
+ });
22
+ draws = (result.items ?? []);
23
+ }
24
+ catch {
25
+ draws = [];
26
+ }
27
+ return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-50", children: title }), subtitle ? (_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: subtitle })) : null] }), draws.length === 0 ? (_jsxs(Stack, { align: "center", gap: "sm", className: "rounded-2xl border border-dashed border-zinc-300 px-6 py-12 text-center dark:border-slate-700", children: [_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No active prize draws \u2014 new draws are announced regularly." }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: "text-sm font-medium text-primary hover:underline", children: "Browse live auctions \u2192" })] })) : (_jsx("div", { className: "fluid-grid-card gap-3", children: draws.map((draw) => (_jsx(InteractiveProductCard, { product: draw, href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(draw.slug ?? draw.id ?? "")) }, draw.id))) }))] }) }) }));
17
28
  }
@@ -143,7 +143,7 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
143
143
  }, REVEAL_DURATION_MS);
144
144
  }, [onReveal, orderId, phase, productId, startAnimation]);
145
145
  const winnerImg = winner?.images?.[0];
146
- return (_jsxs(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reveal your prize. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "md", children: [phase === "refunded" ? (_jsxs(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-4 py-3 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Pool exhausted \u2014 you've been refunded" }), _jsx(Text, { className: "text-sm", children: "Every prize in this draw was already claimed by the time your entry rolled. Your order has been marked refunded automatically." })] })) : null, phase === "error" ? (_jsxs(Div, { className: "rounded border border-red-400/40 bg-error-surface px-4 py-3 text-error", children: [_jsx(Text, { className: "text-sm font-semibold", children: "Something went wrong" }), _jsx(Text, { className: "text-sm", children: errorMessage })] })) : null, _jsx(PrizeDrawCollage, { items: items, hideWonState: true, highlightItemNumber: highlight }), phase === "idle" ? (_jsx(Button, { type: "button", variant: "primary", size: "lg", onClick: handleRevealClick, children: "\u2728 Reveal my prize" })) : null, phase === "revealing" ? (_jsxs(Div, { className: "rounded bg-[var(--appkit-color-surface-muted)] p-4 text-center", children: [_jsx(Text, { className: "text-lg font-semibold", children: "Rolling\u2026" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "The winner was locked by the server before this animation started. Hang tight \u2014 we're just making it look pretty." })] })) : null, phase === "won" && winner ? (_jsxs(Div, { className: "rounded-lg border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] p-4 text-center", children: [_jsx(Text, { className: "text-xs uppercase tracking-wider text-[var(--appkit-color-text-muted)]", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (
146
+ return (_jsxs(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reveal your prize. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "md", children: [phase === "refunded" ? (_jsxs(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-4 py-3 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Pool exhausted \u2014 you've been refunded" }), _jsx(Text, { className: "text-sm", children: "Every prize in this draw was already claimed by the time your entry rolled. Your order has been marked refunded automatically." })] })) : null, phase === "error" ? (_jsxs(Div, { className: "rounded border border-error/40 bg-error-surface px-4 py-3 text-error", children: [_jsx(Text, { className: "text-sm font-semibold", children: "Something went wrong" }), _jsx(Text, { className: "text-sm", children: errorMessage })] })) : null, _jsx(PrizeDrawCollage, { items: items, hideWonState: true, highlightItemNumber: highlight }), phase === "idle" ? (_jsx(Button, { type: "button", variant: "primary", size: "lg", onClick: handleRevealClick, children: "\u2728 Reveal my prize" })) : null, phase === "revealing" ? (_jsxs(Div, { className: "rounded bg-[var(--appkit-color-surface-muted)] p-4 text-center", children: [_jsx(Text, { className: "text-lg font-semibold", children: "Rolling\u2026" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "The winner was locked by the server before this animation started. Hang tight \u2014 we're just making it look pretty." })] })) : null, phase === "won" && winner ? (_jsxs(Div, { className: "rounded-lg border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] p-4 text-center", children: [_jsx(Text, { className: "text-xs uppercase tracking-wider text-[var(--appkit-color-text-muted)]", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (
147
147
  /* eslint-disable-next-line @next/next/no-img-element */
148
148
  _jsx("img", { src: winnerImg, alt: winner.title, className: "mx-auto max-h-64 rounded" })) : null, winner.estimatedValue != null ? (_jsxs(Text, { className: "mt-2 text-sm text-[var(--appkit-color-text-muted)]", children: ["Estimated value: \u20B9", (winner.estimatedValue / 100).toLocaleString("en-IN")] })) : null] })) : null, _jsxs(Div, { className: "rounded border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] px-3 py-2 text-xs text-[var(--appkit-color-text-muted)]", children: [_jsx(Span, { weight: "bold", className: "text-[var(--appkit-color-text)]", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server before the animation starts. The animation is theatrical \u2014 neither the store nor LetItRip staff can influence the outcome.", " ", effectiveRngUrl ? (_jsx("a", { href: effectiveRngUrl, target: "_blank", rel: "noreferrer noopener", className: "underline", children: "View RNG source code \u2192" })) : null] })] })] }));
149
149
  }
@@ -0,0 +1,11 @@
1
+ import type { ProductCardMetadata } from "../schemas/firestore";
2
+ export interface ProductCardMetadataSectionProps {
3
+ card: ProductCardMetadata;
4
+ }
5
+ /**
6
+ * `ProductCardMetadataSection` — W1-34 — renders single-card aftermarket
7
+ * metadata (set name / year / card number / rarity / language). Designed to
8
+ * be embedded inside the "Specifications" tab content; rendered as a key/value
9
+ * table for predictable line-up on PDPs.
10
+ */
11
+ export declare function ProductCardMetadataSection({ card }: ProductCardMetadataSectionProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { Div, Heading, Stack, Text } from "../../../ui";
5
+ /**
6
+ * `ProductCardMetadataSection` — W1-34 — renders single-card aftermarket
7
+ * metadata (set name / year / card number / rarity / language). Designed to
8
+ * be embedded inside the "Specifications" tab content; rendered as a key/value
9
+ * table for predictable line-up on PDPs.
10
+ */
11
+ export function ProductCardMetadataSection({ card }) {
12
+ const rows = [];
13
+ rows.push(["Set", card.setName]);
14
+ if (card.setYear)
15
+ rows.push(["Year", String(card.setYear)]);
16
+ if (card.cardNumber)
17
+ rows.push(["Card #", card.cardNumber]);
18
+ if (card.rarity)
19
+ rows.push(["Rarity", card.rarity]);
20
+ if (card.language)
21
+ rows.push(["Language", card.language.toUpperCase()]);
22
+ if (rows.length === 0)
23
+ return null;
24
+ return (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, className: "text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "Card details" }), _jsx(Div, { className: "grid grid-cols-2 gap-y-1 text-sm sm:grid-cols-[140px_1fr]", children: rows.map(([label, value]) => (_jsxs(React.Fragment, { children: [_jsx(Text, { className: "text-zinc-500 dark:text-zinc-400", children: label }), _jsx(Text, { className: "font-medium text-zinc-900 dark:text-zinc-100", children: value })] }, label))) })] }));
25
+ }
@@ -262,7 +262,9 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
262
262
  codAvailable: "Cash on Delivery",
263
263
  wishlistCount: (n) => `${n} wishlisted`,
264
264
  categoryProductCount: (n, cat) => `${n} in ${cat}`,
265
- } }), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this product" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), 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-400 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" }))] }) }))] })), renderActions: () => (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [formattedPrice && (_jsxs(Div, { children: [_jsxs(Row, { align: "baseline", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formattedPrice }), formattedOriginal && discount && (_jsxs(_Fragment, { children: [_jsx(Span, { className: "text-sm text-zinc-400 line-through dark:text-zinc-400", children: formattedOriginal }), _jsxs(Span, { className: "rounded-full bg-red-500 px-2 py-0.5 text-xs font-bold text-white", children: ["-", discount, "%"] })] }))] }), inStock && effectiveStock !== null && effectiveStock <= 10 && (_jsxs(Text, { className: "mt-1 text-xs text-warning", children: ["Only ", effectiveStock, " left \u2014 order soon!"] }))] })), _jsxs(Stack, { gap: "sm", children: [renderPrimaryActions ? (renderPrimaryActions({
265
+ } }), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this product" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), 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-400 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller }), typeof p.storeRating === "number" && p.storeRating > 0 && (_jsx(Row, { gap: "xs", align: "center", className: "mt-0.5", children: _jsxs(Text, { size: "xs", color: "muted", children: [_jsx("span", { className: "text-warning", "aria-hidden": "true", children: "\u2605" }), " ", p.storeRating.toFixed(1), typeof p.storeReviewCount === "number" &&
266
+ p.storeReviewCount > 0 &&
267
+ ` · ${p.storeReviewCount} reviews`] }) }))] }), 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" }))] }) }))] })), renderActions: () => (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [formattedPrice && (_jsxs(Div, { children: [_jsxs(Row, { align: "baseline", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formattedPrice }), formattedOriginal && discount && (_jsxs(_Fragment, { children: [_jsx(Span, { className: "text-sm text-zinc-400 line-through dark:text-zinc-400", children: formattedOriginal }), _jsxs(Span, { className: "rounded-full bg-red-500 px-2 py-0.5 text-xs font-bold text-white", children: ["-", discount, "%"] })] }))] }), inStock && effectiveStock !== null && effectiveStock <= 10 && (_jsxs(Text, { className: "mt-1 text-xs text-warning", children: ["Only ", effectiveStock, " left \u2014 order soon!"] }))] })), _jsxs(Stack, { gap: "sm", children: [renderPrimaryActions ? (renderPrimaryActions({
266
268
  productId: product.id,
267
269
  productSlug: product.slug ?? slug,
268
270
  productTitle: product.title,
@@ -12,7 +12,7 @@ const ROW_BASE_CLASS = "flex items-start gap-2 rounded-lg border px-3 py-2 text-
12
12
  const ROW_CHECKED_CLASS = "border-primary bg-primary/5 dark:bg-primary/10";
13
13
  const ROW_DISABLED_CLASS = "border-zinc-200 dark:border-slate-700 opacity-50 cursor-not-allowed";
14
14
  const ROW_DEFAULT_CLASS = "border-zinc-200 dark:border-slate-700 hover:bg-zinc-50 dark:hover:bg-slate-800";
15
- const OVER_LIMIT_BANNER_CLASS = "rounded-lg border border-red-200 dark:border-red-900/60 bg-error-surface px-3 py-2 text-xs text-error";
15
+ const OVER_LIMIT_BANNER_CLASS = "rounded-lg border border-error/20 bg-error-surface px-3 py-2 text-xs text-error";
16
16
  const EMPTY_STATE_CLASS = "rounded-lg border border-zinc-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 py-4 text-xs text-zinc-500 dark:text-zinc-400 text-center";
17
17
  function applyProductType(list, productType) {
18
18
  if (productType === "all")
@@ -0,0 +1,13 @@
1
+ import type { ProductGrading } from "../schemas/firestore";
2
+ export interface ProductGradingTabProps {
3
+ grading: ProductGrading;
4
+ /** Optional resolved media URL for the slab/grade image. */
5
+ slabImageUrl?: string | null;
6
+ }
7
+ /**
8
+ * `ProductGradingTab` — W1-34 — renders TCG slab grading metadata
9
+ * (`product.grading` ProductGrading). Used as the `gradingContent` slot of
10
+ * `ProductTabsShell`. Always renders the service + grade prominently; cert
11
+ * lookup link is wired for PSA/BGS where the URL pattern is well-known.
12
+ */
13
+ export declare function ProductGradingTab({ grading, slabImageUrl }: ProductGradingTabProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Div, Heading, Stack, Text } from "../../../ui";
4
+ import { Badge } from "../../../ui/components/Badge";
5
+ const PSA_CERT_URL = (cert) => `https://www.psacard.com/cert/${cert}`;
6
+ const BGS_CERT_URL = (cert) => `https://www.beckett.com/grading/card-lookup/${cert}`;
7
+ function buildCertLookupUrl(service, certNumber) {
8
+ if (!certNumber)
9
+ return null;
10
+ if (service === "PSA")
11
+ return PSA_CERT_URL(certNumber);
12
+ if (service === "BGS")
13
+ return BGS_CERT_URL(certNumber);
14
+ return null;
15
+ }
16
+ /**
17
+ * `ProductGradingTab` — W1-34 — renders TCG slab grading metadata
18
+ * (`product.grading` ProductGrading). Used as the `gradingContent` slot of
19
+ * `ProductTabsShell`. Always renders the service + grade prominently; cert
20
+ * lookup link is wired for PSA/BGS where the URL pattern is well-known.
21
+ */
22
+ export function ProductGradingTab({ grading, slabImageUrl }) {
23
+ const lookupUrl = buildCertLookupUrl(grading.service, grading.certNumber);
24
+ return (_jsxs(Stack, { gap: "lg", children: [_jsxs(Div, { className: "flex items-center gap-4", children: [_jsxs(Div, { className: "flex flex-col items-center justify-center rounded-2xl border-2 border-[var(--appkit-color-primary)] px-6 py-4 min-w-[120px]", children: [_jsx(Text, { className: "text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: grading.service }), _jsx(Text, { className: "text-4xl font-extrabold", children: grading.grade })] }), _jsxs(Stack, { gap: "xs", className: "flex-1", children: [_jsxs(Heading, { level: 3, className: "text-base font-semibold", children: ["Graded by ", grading.service] }), grading.certNumber ? (_jsxs(Text, { className: "text-sm text-zinc-600 dark:text-zinc-400", children: ["Cert #", grading.certNumber, lookupUrl ? (_jsxs(_Fragment, { children: [" — ", _jsx("a", { href: lookupUrl, target: "_blank", rel: "noopener noreferrer", className: "text-[var(--appkit-color-primary)] hover:underline", children: "Verify" })] })) : null] })) : null] })] }), slabImageUrl ? (_jsx(Div, { className: "overflow-hidden rounded-2xl border border-zinc-200 dark:border-zinc-700", children: _jsx("img", { src: slabImageUrl, alt: "Slab certificate image", className: "w-full h-auto" }) })) : null, grading.attributes && Object.keys(grading.attributes).length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, className: "text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "Subgrades & attributes" }), _jsx(Div, { className: "flex flex-wrap gap-2", children: Object.entries(grading.attributes).map(([key, value]) => (_jsxs(Badge, { variant: "default", children: [key, ": ", value] }, key))) })] })) : null] }));
25
+ }
@@ -49,7 +49,7 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
49
49
  .filter(Boolean)
50
50
  .join(" "), children: [_jsxs(Div, { className: "relative overflow-hidden bg-zinc-100 dark:bg-slate-800 aspect-square", children: [product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", className: "transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Div, { className: "h-full w-full flex items-center justify-center bg-gradient-to-br from-zinc-100 to-zinc-200 dark:from-slate-800 dark:to-slate-700", children: _jsx(Span, { className: "text-4xl opacity-30", children: "\uD83D\uDECD\uFE0F" }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(product.id); }, label: isSelected ? "Deselect" : "Select", position: "top-2 left-2", className: selectionMode || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsx(Div, { className: `absolute top-2 flex flex-col gap-1 ${onSelect ? "left-10" : "left-2"}`, children: !selectionMode && !isSelected && (_jsxs(_Fragment, { children: [discount && (_jsxs(Span, { className: "rounded-full bg-rose-500 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", children: ["-", discount, "%"] })), typeBadge && (_jsx(Span, { className: `rounded-full px-2 py-0.5 text-[10px] font-bold shadow-sm ${typeBadge.cls}`, children: typeBadge.label })), product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: "rounded-full bg-teal-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: product.partOfBundleTitles && product.partOfBundleTitles.length > 0
51
51
  ? `In bundle: ${product.partOfBundleTitles[0]}`
52
- : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: "rounded-full bg-indigo-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: "rounded-full bg-sky-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), onAddToWishlist && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => {
52
+ : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: "rounded-full bg-indigo-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: "rounded-full bg-sky-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), _jsx(BaseListingCard.IconOverlay, { groupIcon: product.groupIcon, sublistingIcon: product.sublistingIcon, position: "bottom-2 right-2" }), onAddToWishlist && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => {
53
53
  e.stopPropagation();
54
54
  e.preventDefault();
55
55
  onAddToWishlist(product.id);
@@ -59,7 +59,19 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
59
59
  isWishlisted
60
60
  ? "bg-rose-500 text-white hover:bg-rose-600"
61
61
  : "bg-white/90 dark:bg-slate-800/90 text-zinc-500 dark:text-zinc-400 hover:text-rose-500 hover:bg-white dark:hover:bg-slate-800",
62
- ].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Div, { className: "flex flex-1 flex-col p-3 pt-2.5", children: [_jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-semibold text-zinc-900 dark:text-white leading-snug`, children: product.title }), ((Array.isArray(product.categoryNames) ? product.categoryNames[0] : product.categoryName) || product.brand) && (_jsxs(Row, { className: "mt-1 gap-1 flex-wrap", children: [(Array.isArray(product.categoryNames) ? product.categoryNames[0] : product.categoryName) && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:text-zinc-400 truncate max-w-[100px]", children: [_jsx("svg", { className: "h-2.5 w-2.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 7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" }) }), (Array.isArray(product.categoryNames) ? product.categoryNames[0] : product.categoryName)] })), product.brand && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:text-zinc-400 truncate max-w-[90px]", children: [_jsx("svg", { className: "h-2.5 w-2.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: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), product.brand] }))] })), (() => {
62
+ ].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Div, { className: "flex flex-1 flex-col p-3 pt-2.5", children: [_jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-semibold text-zinc-900 dark:text-white leading-snug`, children: product.title }), (() => {
63
+ const categories = Array.isArray(product.categoryNames)
64
+ ? product.categoryNames.filter(Boolean)
65
+ : product.categoryName
66
+ ? [product.categoryName]
67
+ : [];
68
+ if (!categories.length && !product.brand)
69
+ return null;
70
+ const MAX_CHIPS = 3;
71
+ const visible = categories.slice(0, MAX_CHIPS);
72
+ const overflow = Math.max(0, categories.length - MAX_CHIPS);
73
+ return (_jsxs(Row, { className: "mt-1 gap-1 flex-wrap", children: [visible.map((name) => (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:text-zinc-400 truncate max-w-[100px]", children: [_jsx("svg", { className: "h-2.5 w-2.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 7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" }) }), name] }, name))), overflow > 0 && (_jsxs(Span, { className: "inline-flex items-center rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] font-medium text-zinc-600 dark:text-zinc-400", title: categories.slice(MAX_CHIPS).join(", "), children: ["+", overflow] })), product.brand && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:text-zinc-400 truncate max-w-[90px]", children: [_jsx("svg", { className: "h-2.5 w-2.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: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), product.brand] }))] }));
74
+ })(), (() => {
63
75
  const seller = safeDisplayName(product.storeName, "");
64
76
  return seller ? (_jsxs(Text, { className: "mt-0.5 text-[11px] text-zinc-400 dark:text-zinc-400 truncate", children: ["by ", seller] })) : null;
65
77
  })(), product.rating !== undefined && (_jsxs(Row, { className: "mt-1 gap-1", children: [_jsx(Span, { className: "text-[11px] text-amber-400", children: "\u2605" }), _jsxs(Span, { className: "text-[11px] text-zinc-500 dark:text-zinc-400", children: [product.rating.toFixed(1), product.reviewCount ? ` (${product.reviewCount})` : ""] })] })), _jsxs(Div, { className: "mt-auto pt-2", children: [_jsxs(Row, { align: "baseline", className: "gap-2", children: [_jsx(Span, { className: "text-base font-bold text-primary dark:text-primary-400", children: formatCurrency(product.price, getDefaultCurrency()) }), product.originalPrice && product.originalPrice > product.price && (_jsx(Span, { className: "text-xs text-zinc-400 line-through dark:text-zinc-400", children: formatCurrency(product.originalPrice, getDefaultCurrency()) }))] }), (() => {
@@ -7,11 +7,15 @@ export interface CustomTabDef {
7
7
  export interface ProductTabsShellProps {
8
8
  descriptionContent?: React.ReactNode;
9
9
  specsContent?: React.ReactNode;
10
+ /** W1-34 — grading tab content (PSA/BGS/CGC etc). Pass non-null to surface the tab. */
11
+ gradingContent?: React.ReactNode;
10
12
  ingredientsContent?: React.ReactNode;
11
13
  howToUseContent?: React.ReactNode;
12
14
  reviewsContent?: React.ReactNode;
15
+ /** W1-34 — Q&A tab content. Pass non-null to surface the tab. */
16
+ qaContent?: React.ReactNode;
13
17
  /** Additional tabs from custom sections — rendered after standard tabs */
14
18
  customTabs?: CustomTabDef[];
15
19
  className?: string;
16
20
  }
17
- export declare function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, customTabs, className, }: ProductTabsShellProps): import("react/jsx-runtime").JSX.Element | null;
21
+ export declare function ProductTabsShell({ descriptionContent, specsContent, gradingContent, ingredientsContent, howToUseContent, reviewsContent, qaContent, customTabs, className, }: ProductTabsShellProps): import("react/jsx-runtime").JSX.Element | null;
@@ -4,17 +4,22 @@ import { useState } from "react";
4
4
  const STATIC_TABS = [
5
5
  { id: "description", label: "Description" },
6
6
  { id: "specs", label: "Specifications" },
7
+ { id: "grading", label: "Grading" },
7
8
  { id: "ingredients", label: "Ingredients" },
8
9
  { id: "howToUse", label: "How to Use" },
9
10
  { id: "reviews", label: "Reviews" },
11
+ // W1-34 — Q&A tab; consumers pass `qaContent` to surface it.
12
+ { id: "qa", label: "Q&A" },
10
13
  ];
11
- export function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, customTabs = [], className = "", }) {
14
+ export function ProductTabsShell({ descriptionContent, specsContent, gradingContent, ingredientsContent, howToUseContent, reviewsContent, qaContent, customTabs = [], className = "", }) {
12
15
  const staticContentMap = {
13
16
  description: descriptionContent,
14
17
  specs: specsContent,
18
+ grading: gradingContent,
15
19
  ingredients: ingredientsContent,
16
20
  howToUse: howToUseContent,
17
21
  reviews: reviewsContent,
22
+ qa: qaContent,
18
23
  };
19
24
  const visibleStatic = STATIC_TABS.filter((t) => staticContentMap[t.id] != null);
20
25
  const visibleCustom = customTabs.filter((t) => t.content != null);
@@ -54,6 +54,10 @@ export { ProductGalleryClient } from "./ProductGalleryClient";
54
54
  export type { ProductGalleryClientProps } from "./ProductGalleryClient";
55
55
  export { ProductTabsShell } from "./ProductTabsShell";
56
56
  export type { ProductTabsShellProps, CustomTabDef } from "./ProductTabsShell";
57
+ export { ProductGradingTab } from "./ProductGradingTab";
58
+ export type { ProductGradingTabProps } from "./ProductGradingTab";
59
+ export { ProductCardMetadataSection } from "./ProductCardMetadataSection";
60
+ export type { ProductCardMetadataSectionProps } from "./ProductCardMetadataSection";
57
61
  export { CustomFieldsEditor } from "./CustomFieldsEditor";
58
62
  export type { CustomFieldsEditorProps } from "./CustomFieldsEditor";
59
63
  export { CustomSectionsEditor } from "./CustomSectionsEditor";
@@ -32,6 +32,9 @@ export { MakeOfferButton } from "./MakeOfferButton";
32
32
  export { RelatedProducts } from "./RelatedProducts";
33
33
  export { ProductGalleryClient } from "./ProductGalleryClient";
34
34
  export { ProductTabsShell } from "./ProductTabsShell";
35
+ // W1-34: Product detail missing sections
36
+ export { ProductGradingTab } from "./ProductGradingTab";
37
+ export { ProductCardMetadataSection } from "./ProductCardMetadataSection";
35
38
  export { CustomFieldsEditor } from "./CustomFieldsEditor";
36
39
  export { CustomSectionsEditor } from "./CustomSectionsEditor";
37
40
  export { CustomSectionTabContent } from "./CustomSectionTabContent";
@@ -9,6 +9,11 @@
9
9
  * Client — useAuthGate() shows LoginRequiredModal for Tier 1 (public) actions
10
10
  * Server — checkActionAllowed() enforces the same guards on every server action
11
11
  * Admin — ActionPermissionsManager toggles defaultEnabled via siteSettings.actionConfig
12
+ *
13
+ * Label source of truth: `ACTIONS.{RESOURCE}["..."].label` in
14
+ * _internal/shared/actions/action-registry.ts. `ACTION_META` derives its labels
15
+ * via `regLabel()` where a registry counterpart exists; entries with no
16
+ * counterpart keep an inline literal until a registry leaf is added (W1-5).
12
17
  */
13
18
  export type ActionVariant = "primary" | "secondary" | "outline" | "ghost" | "danger" | "warning";
14
19
  export declare const ACTION_ID: {