@mohasinac/appkit 4.26.0 โ†’ 4.28.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 (277) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
  3. package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
  4. package/dist/_internal/server/features/bundles/data.d.ts +19 -0
  5. package/dist/_internal/server/features/bundles/data.js +32 -0
  6. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  7. package/dist/_internal/server/features/bundles/index.js +1 -1
  8. package/dist/_internal/server/features/checkout/actions.js +6 -4
  9. package/dist/_internal/server/features/classified/metadata.js +3 -1
  10. package/dist/_internal/server/features/digital-code/metadata.js +3 -1
  11. package/dist/_internal/server/features/live/metadata.js +3 -1
  12. package/dist/_internal/server/features/refunds/actions.js +3 -2
  13. package/dist/_internal/server/features/reviews/actions.js +3 -2
  14. package/dist/_internal/server/features/seo/sitemap.js +189 -40
  15. package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
  16. package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
  17. package/dist/_internal/shared/actions/action-registry.js +14 -0
  18. package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
  19. package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
  20. package/dist/_internal/shared/features/checkout/config.js +6 -3
  21. package/dist/client.d.ts +32 -2
  22. package/dist/client.js +37 -2
  23. package/dist/constants/api-endpoints.d.ts +21 -3
  24. package/dist/constants/api-endpoints.js +14 -1
  25. package/dist/constants/field-names.d.ts +2 -2
  26. package/dist/constants/field-names.js +2 -2
  27. package/dist/core/newsletter.repository.js +14 -1
  28. package/dist/features/about/components/FAQPageView.js +1 -1
  29. package/dist/features/about/components/FeesView.js +1 -1
  30. package/dist/features/account/components/AddressForm.d.ts +2 -0
  31. package/dist/features/account/components/AddressForm.js +5 -1
  32. package/dist/features/account/components/UserBidsView.js +1 -1
  33. package/dist/features/account/components/UserSidebar.js +1 -1
  34. package/dist/features/account/components/UserSupportView.js +24 -2
  35. package/dist/features/account/hooks/useAddresses.d.ts +29 -1
  36. package/dist/features/account/hooks/useAddresses.js +4 -1
  37. package/dist/features/account/hooks/useProfile.d.ts +1 -1
  38. package/dist/features/account/hooks/useProfile.js +1 -1
  39. package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
  40. package/dist/features/addresses/repository/addresses.repository.js +19 -0
  41. package/dist/features/admin/actions/chat-actions.js +5 -1
  42. package/dist/features/admin/components/AdminAdsView.js +1 -1
  43. package/dist/features/admin/components/AdminBidsView.js +6 -1
  44. package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
  45. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
  46. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  47. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
  48. package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
  49. package/dist/features/admin/components/AdminSidebar.js +1 -1
  50. package/dist/features/admin/components/AdminViewCards.js +2 -2
  51. package/dist/features/admin/components/DataTable.js +2 -2
  52. package/dist/features/admin/schemas/firestore.js +9 -3
  53. package/dist/features/auctions/actions/bid-actions.js +45 -4
  54. package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
  55. package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
  56. package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
  57. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  58. package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
  59. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
  60. package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
  61. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  62. package/dist/features/auth/hooks/useAuth.d.ts +23 -1
  63. package/dist/features/auth/hooks/useAuth.js +27 -1
  64. package/dist/features/auth/permissions/constants.d.ts +1 -1
  65. package/dist/features/auth/permissions/constants.js +3 -0
  66. package/dist/features/auth/repository/session.repository.d.ts +16 -1
  67. package/dist/features/auth/repository/session.repository.js +23 -0
  68. package/dist/features/auth/repository/user.repository.js +13 -2
  69. package/dist/features/blog/components/BlogListView.js +2 -2
  70. package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
  71. package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
  72. package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
  73. package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
  74. package/dist/features/cart/components/index.d.ts +1 -1
  75. package/dist/features/cart/components/index.js +1 -1
  76. package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
  77. package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
  78. package/dist/features/cart/utils/line-total.d.ts +25 -0
  79. package/dist/features/cart/utils/line-total.js +26 -0
  80. package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
  81. package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
  82. package/dist/features/categories/components/CategoryTree.js +2 -2
  83. package/dist/features/contact/components/ContactForm.js +1 -1
  84. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
  85. package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
  86. package/dist/features/events/components/AdminEventEditorView.js +16 -9
  87. package/dist/features/events/components/AdminEventEntriesView.js +7 -13
  88. package/dist/features/events/components/EventRafflesSection.js +14 -2
  89. package/dist/features/events/components/EventStatusBadge.js +1 -1
  90. package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
  91. package/dist/features/events/components/event-entry-detail-fields.js +13 -0
  92. package/dist/features/faq/components/ContactCTA.js +1 -1
  93. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  94. package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
  95. package/dist/features/faq/repository/faqs.repository.js +64 -54
  96. package/dist/features/faq/schemas/firestore.d.ts +3 -2
  97. package/dist/features/faq/schemas/firestore.js +2 -2
  98. package/dist/features/faq/schemas/index.d.ts +3 -3
  99. package/dist/features/faq/schemas/index.js +1 -1
  100. package/dist/features/filters/FilterFacetSection.js +2 -2
  101. package/dist/features/forms/Toggle.js +1 -1
  102. package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
  103. package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
  104. package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
  105. package/dist/features/homepage/components/BrandsSection.js +1 -1
  106. package/dist/features/homepage/components/CTABannerSection.js +1 -1
  107. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
  108. package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
  109. package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
  110. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  111. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  112. package/dist/features/homepage/lib/section-renderer.js +2 -1
  113. package/dist/features/layout/AppLayoutShell.js +5 -5
  114. package/dist/features/layout/BottomActions.js +5 -5
  115. package/dist/features/layout/BottomSheet.js +1 -1
  116. package/dist/features/layout/MainNavbar.js +1 -1
  117. package/dist/features/layout/NavbarLayout.js +1 -1
  118. package/dist/features/layout/TitleBarLayout.js +3 -3
  119. package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
  120. package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
  121. package/dist/features/lottery/schemas/config-write.d.ts +141 -0
  122. package/dist/features/lottery/schemas/config-write.js +139 -0
  123. package/dist/features/media/MediaPickerModal.js +3 -1
  124. package/dist/features/media/MediaVideo.js +4 -2
  125. package/dist/features/media/components/MediaSlider.js +1 -1
  126. package/dist/features/orders/components/OrdersList.js +2 -2
  127. package/dist/features/payments/repository/payout.repository.d.ts +11 -1
  128. package/dist/features/payments/repository/payout.repository.js +14 -6
  129. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  130. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  131. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  132. package/dist/features/products/columns/productTableColumns.js +3 -3
  133. package/dist/features/products/components/CompareOverlay.js +1 -1
  134. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  135. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  136. package/dist/features/products/components/PrizeDrawsSection.js +5 -1
  137. package/dist/features/products/components/ProductDetailActions.js +11 -7
  138. package/dist/features/products/components/ProductDetailPageView.js +3 -3
  139. package/dist/features/products/components/ProductFeatureBadges.js +2 -2
  140. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  141. package/dist/features/products/components/ProductFilters.js +15 -8
  142. package/dist/features/products/components/ProductGrid.js +8 -17
  143. package/dist/features/products/components/ProductTabs.js +7 -4
  144. package/dist/features/products/components/ProductTabsShell.js +2 -7
  145. package/dist/features/products/constants/action-defs.d.ts +21 -4
  146. package/dist/features/products/constants/action-defs.js +7 -3
  147. package/dist/features/products/hooks/useRelatedProducts.js +2 -1
  148. package/dist/features/products/repository/products.repository.d.ts +1 -1
  149. package/dist/features/products/repository/products.repository.js +27 -13
  150. package/dist/features/products/schemas/firestore.d.ts +3 -2
  151. package/dist/features/products/schemas/firestore.js +1 -1
  152. package/dist/features/reviews/actions/review-actions.d.ts +8 -1
  153. package/dist/features/reviews/actions/review-actions.js +14 -3
  154. package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
  155. package/dist/features/reviews/components/ReviewModal.js +1 -1
  156. package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
  157. package/dist/features/reviews/repository/reviews.repository.js +53 -13
  158. package/dist/features/reviews/schemas/firestore.d.ts +6 -0
  159. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  160. package/dist/features/search/components/Search.js +3 -3
  161. package/dist/features/search/components/SearchResultsSection.js +1 -1
  162. package/dist/features/seller/components/SellerAddressesView.js +3 -1
  163. package/dist/features/seller/components/SellerBidsView.js +6 -1
  164. package/dist/features/seller/components/SellerSidebar.js +1 -1
  165. package/dist/features/shell/SectionForm.d.ts +21 -1
  166. package/dist/features/shell/SectionForm.js +15 -3
  167. package/dist/features/shell/build-sections.js +10 -1
  168. package/dist/features/shell/field-ui-meta.d.ts +21 -0
  169. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
  170. package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
  171. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
  172. package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
  173. package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
  174. package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
  175. package/dist/features/stores/actions/store-query-actions.js +25 -1
  176. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  177. package/dist/features/stores/components/StoreHeader.js +1 -1
  178. package/dist/features/stores/components/StoreNavTabs.js +20 -8
  179. package/dist/features/support/repository/support.repository.d.ts +12 -0
  180. package/dist/features/support/repository/support.repository.js +15 -0
  181. package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
  182. package/dist/features/support/schemas/ticket-create-form.js +82 -0
  183. package/dist/features/tester/seed-data/products-tester-seed-data.js +108 -2
  184. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +235 -13
  185. package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
  186. package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
  187. package/dist/features/wishlist/components/index.d.ts +2 -1
  188. package/dist/features/wishlist/components/index.js +1 -1
  189. package/dist/index.d.ts +10 -3
  190. package/dist/index.js +19 -4
  191. package/dist/monitoring/error-tracking.js +39 -5
  192. package/dist/monitoring/server-logger.js +70 -32
  193. package/dist/next/components/ErrorView.js +1 -1
  194. package/dist/next/routing/route-map.d.ts +39 -7
  195. package/dist/next/routing/route-map.js +34 -4
  196. package/dist/providers/db-firebase/base.repository.d.ts +30 -0
  197. package/dist/providers/db-firebase/base.repository.js +56 -9
  198. package/dist/react/ErrorBoundary.js +9 -1
  199. package/dist/react/contexts/SessionContext.js +6 -2
  200. package/dist/react/hooks/useAuthGate.d.ts +22 -0
  201. package/dist/react/hooks/useAuthGate.js +86 -7
  202. package/dist/schemas/registry.d.ts +3 -3
  203. package/dist/security/index.d.ts +1 -1
  204. package/dist/security/index.js +1 -1
  205. package/dist/security/pii-encrypt.d.ts +13 -5
  206. package/dist/security/pii-encrypt.js +132 -6
  207. package/dist/security/pii-schemas.d.ts +20 -4
  208. package/dist/security/pii-schemas.js +23 -5
  209. package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
  210. package/dist/seed/_helpers/product-search-txt.js +33 -0
  211. package/dist/seed/faq-seed-data.d.ts +16 -0
  212. package/dist/seed/faq-seed-data.js +22 -1
  213. package/dist/seed/products-art-seed-data.js +49 -7
  214. package/dist/seed/products-auctions-seed-data.js +12 -4
  215. package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
  216. package/dist/seed/products-classifieds-seed-data.js +65 -9
  217. package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
  218. package/dist/seed/products-digital-codes-seed-data.js +65 -9
  219. package/dist/seed/products-live-items-seed-data.d.ts +5 -0
  220. package/dist/seed/products-live-items-seed-data.js +33 -5
  221. package/dist/seed/products-preorders-seed-data.d.ts +1 -1
  222. package/dist/seed/products-preorders-seed-data.js +6 -2
  223. package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
  224. package/dist/seed/products-prize-draws-seed-data.js +57 -8
  225. package/dist/seed/products-standard-seed-data.js +6 -2
  226. package/dist/seed/products-stickers-seed-data.js +49 -7
  227. package/dist/seed/site-settings-seed-data.js +5 -3
  228. package/dist/seo/json-ld.d.ts +44 -2
  229. package/dist/seo/json-ld.js +57 -11
  230. package/dist/server.d.ts +1 -1
  231. package/dist/server.js +1 -1
  232. package/dist/styles.css +368 -90
  233. package/dist/tailwind-utilities.css +1 -1
  234. package/dist/tokens/index.d.ts +1 -1
  235. package/dist/tokens/index.js +1 -1
  236. package/dist/tokens/themes/default-dark.js +6 -0
  237. package/dist/tokens/themes/default-light.js +5 -0
  238. package/dist/tokens/tokens.css +84 -0
  239. package/dist/ui/DataTable.js +1 -1
  240. package/dist/ui/DataTable.style.css +3 -3
  241. package/dist/ui/components/BulkActionBar.style.css +1 -3
  242. package/dist/ui/components/Button.js +15 -6
  243. package/dist/ui/components/Dropdown.style.css +1 -4
  244. package/dist/ui/components/FilterChipGroup.js +1 -1
  245. package/dist/ui/components/FilterDrawer.style.css +1 -1
  246. package/dist/ui/components/Icon.d.ts +53 -0
  247. package/dist/ui/components/Icon.js +41 -0
  248. package/dist/ui/components/IconButton.d.ts +13 -0
  249. package/dist/ui/components/IconButton.js +13 -0
  250. package/dist/ui/components/IconButton.style.css +18 -10
  251. package/dist/ui/components/ListingLayout.style.css +2 -10
  252. package/dist/ui/components/ListingToolbar.js +6 -6
  253. package/dist/ui/components/Modal.js +1 -1
  254. package/dist/ui/components/PaginatedSelect.style.css +3 -3
  255. package/dist/ui/components/Pagination.style.css +1 -6
  256. package/dist/ui/components/RichTextEditor.js +1 -1
  257. package/dist/ui/components/RowActionMenu.style.css +1 -1
  258. package/dist/ui/components/SideDrawer.style.css +1 -1
  259. package/dist/ui/components/Slider.js +1 -1
  260. package/dist/ui/components/TabBarShell.d.ts +78 -0
  261. package/dist/ui/components/TabBarShell.js +95 -0
  262. package/dist/ui/components/Tabs.js +12 -8
  263. package/dist/ui/components/Tabs.style.css +249 -41
  264. package/dist/ui/components/ViewToggle.style.css +1 -5
  265. package/dist/ui/components/tabs-overflow.d.ts +17 -0
  266. package/dist/ui/components/tabs-overflow.js +142 -0
  267. package/dist/ui/icons/icon-registry.d.ts +38 -0
  268. package/dist/ui/icons/icon-registry.js +37 -0
  269. package/dist/ui/index.d.ts +6 -0
  270. package/dist/ui/index.js +3 -0
  271. package/dist/utils/index.d.ts +1 -1
  272. package/dist/utils/index.js +1 -1
  273. package/dist/utils/media-url.d.ts +27 -4
  274. package/dist/utils/media-url.js +37 -6
  275. package/dist/utils/search-txt.d.ts +71 -0
  276. package/dist/utils/search-txt.js +118 -0
  277. package/package.json +1 -1
@@ -15,6 +15,8 @@ import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../ho
15
15
  import { DataListingView } from "./DataListingView";
16
16
  import { apiClient } from "../../../http";
17
17
  import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
18
+ import { TextLink } from "../../../ui";
19
+ import { ROUTES } from "../../../constants/index";
18
20
  export function AdminBidsView({ children, ...props }) {
19
21
  const queryClient = useQueryClient();
20
22
  const { showToast } = useToast();
@@ -99,7 +101,10 @@ export function AdminBidsView({ children, ...props }) {
99
101
  const detailBadge = detail ? bidStatusBadge(detail.status) : null;
100
102
  return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: toStringValue(detail?.detail?.productTitle, "Bid"), badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail
101
103
  ? buildBidDetailFields(detail.detail, "admin")
102
- : undefined, extra:
104
+ : undefined, footer: detail ? (
105
+ // The full page โ€” a modal cannot be linked, bookmarked or reloaded
106
+ // into, which is the whole reason the page exists.
107
+ _jsx(TextLink, { href: String(ROUTES.ADMIN.BID_DETAIL(detail.id)), children: "Open full page \u2192" })) : undefined, extra:
103
108
  // A bid moves through more states than any record a buyer owns, and
104
109
  // every one of them happened inside a settlement batch with nothing
105
110
  // recording it until W18. Absent history renders the empty label,
@@ -2,5 +2,13 @@ import React from "react";
2
2
  import type { StackedViewShellProps } from "../../../ui";
3
3
  export interface AdminCarouselGroupEditorViewProps extends Omit<StackedViewShellProps, "sections"> {
4
4
  onCreated?: (id: string) => void;
5
+ /**
6
+ * Present โ†’ edit that carousel; absent โ†’ create a new one.
7
+ *
8
+ * This component was create-only, which is why `/admin/carousels/[id]/edit`
9
+ * did not exist. Same `featureId?`-style seam `AdminFeatureEditorView` uses.
10
+ */
11
+ carouselId?: string;
12
+ onSaved?: (id: string) => void;
5
13
  }
6
- export declare function AdminCarouselGroupEditorView({ onCreated, ...rest }: AdminCarouselGroupEditorViewProps): React.JSX.Element;
14
+ export declare function AdminCarouselGroupEditorView({ onCreated, carouselId, onSaved, ...rest }: AdminCarouselGroupEditorViewProps): React.JSX.Element;
@@ -3,6 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  import React from "react";
4
4
  import { z } from "zod";
5
5
  import { useRouter } from "next/navigation";
6
+ import { useQuery } from "@tanstack/react-query";
6
7
  import { Button, Form, Row, StackedViewShell, useToast } from "../../../ui";
7
8
  import { FieldInput, FieldSelect, FormErrorSummary } from "../../../ui/forms";
8
9
  import { useApiMutation } from "@mohasinac/appkit/client";
@@ -13,28 +14,59 @@ const carouselGroupFormSchema = z.object({
13
14
  name: z.string().min(1, "Name is required").max(200),
14
15
  status: z.enum(["active", "draft"]),
15
16
  });
16
- export function AdminCarouselGroupEditorView({ onCreated, ...rest }) {
17
+ export function AdminCarouselGroupEditorView({ onCreated, carouselId, onSaved, ...rest }) {
17
18
  const { showToast } = useToast();
18
19
  const router = useRouter();
20
+ const isEdit = Boolean(carouselId);
19
21
  const [name, setName] = React.useState("");
20
22
  const [status, setStatus] = React.useState("draft");
23
+ // Seed from the single-item GET, never from a cached list row โ€” a list
24
+ // projection is narrower than the document, and saving would write the
25
+ // missing fields back as their defaults (Root Cause #38).
26
+ const existing = useQuery({
27
+ queryKey: ["admin", "carousel", carouselId],
28
+ queryFn: () => apiClient.get(ADMIN_ENDPOINTS.CAROUSELS_BY_ID(carouselId)),
29
+ enabled: isEdit,
30
+ });
31
+ React.useEffect(() => {
32
+ const doc = existing.data?.data;
33
+ if (!doc)
34
+ return;
35
+ setName(doc.name ?? "");
36
+ setStatus(doc.status === "active" ? "active" : "draft");
37
+ }, [existing.data]);
21
38
  const createMutation = useApiMutation({
22
- mutationFn: () => apiClient.post(ADMIN_ENDPOINTS.CAROUSELS, { name, status }),
39
+ /*
40
+ * PATCH on the edit path, not PUT. `/api/admin/carousels/[id]` exports
41
+ * GET/PATCH/DELETE โ€” copying `AdminFeatureEditorView`'s `apiClient.put`
42
+ * here would have been a 405, which is exactly what
43
+ * `audit-client-verb-match` exists to catch.
44
+ */
45
+ mutationFn: () => isEdit
46
+ ? apiClient.patch(ADMIN_ENDPOINTS.CAROUSELS_BY_ID(carouselId), { name, status })
47
+ : apiClient.post(ADMIN_ENDPOINTS.CAROUSELS, { name, status }),
23
48
  onSuccess: (res) => {
24
- const id = res?.data?.id;
25
- showToast("Carousel created.", "success");
26
- if (id) {
27
- if (onCreated)
28
- onCreated(id);
49
+ const id = res?.data?.id ?? carouselId;
50
+ showToast(isEdit ? "Carousel updated." : "Carousel created.", "success");
51
+ if (!id)
52
+ return;
53
+ if (isEdit) {
54
+ if (onSaved)
55
+ onSaved(id);
29
56
  else
30
57
  router.push(String(ROUTES.ADMIN.CAROUSEL_DETAIL(id)));
58
+ return;
31
59
  }
60
+ if (onCreated)
61
+ onCreated(id);
62
+ else
63
+ router.push(String(ROUTES.ADMIN.CAROUSEL_DETAIL(id)));
32
64
  },
33
65
  onError: (err) => {
34
- showToast(err?.message ?? "Failed to create carousel.", "error");
66
+ showToast(err?.message ?? `Failed to ${isEdit ? "update" : "create"} carousel.`, "error");
35
67
  },
36
68
  });
37
- return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: "New Named Carousel", sections: [
69
+ return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Carousel" : "New Named Carousel", sections: [
38
70
  _jsx(Form, { schema: carouselGroupFormSchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "name", label: "Carousel name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Homepage Hero" }), _jsx(FieldSelect, { name: "status", label: "Status", value: status, onChange: (v) => setStatus(v), options: [
39
71
  { value: "draft", label: "Draft" },
40
72
  { value: "active", label: "Active" },
@@ -45,6 +77,6 @@ export function AdminCarouselGroupEditorView({ onCreated, ...rest }) {
45
77
  return;
46
78
  }
47
79
  createMutation.mutate();
48
- }, children: "Create carousel" }) })] })) }, "carousel-group-form"),
80
+ }, children: isEdit ? "Save changes" : "Create carousel" }) })] })) }, "carousel-group-form"),
49
81
  ] }));
50
82
  }
@@ -145,5 +145,5 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
145
145
  ] })),
146
146
  },
147
147
  ];
148
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search slide titles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides \u2014 use 'New slide' to add one", rowHrefTemplate: "/admin/carousel/{id}/edit" })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Carousel Slide", message: "Delete this carousel slide? This cannot be undone.", onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
148
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search slide titles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides \u2014 use 'New slide' to add one", rowHrefTemplate: "/admin/carousel/{id}/edit" })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Carousel Slide", message: "Delete this carousel slide? This cannot be undone.", onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
149
149
  }
@@ -19,7 +19,7 @@ const __O = {
19
19
  };
20
20
  const PILL_BASE_CLASS = "inline-flex items-center gap-[var(--appkit-space-1-5)] rounded-full border px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-medium cursor-pointer transition-colors";
21
21
  const PILL_CHECKED_CLASS = "bg-primary text-white border-primary";
22
- const PILL_UNCHECKED_CLASS = "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]";
22
+ const PILL_UNCHECKED_CLASS = "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover";
23
23
  const FIELD_LABEL_CLASS = "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)]";
24
24
  const DELETE_CONFIRM_TEXT = "Delete this feature? It will fail if any product still references it.";
25
25
  const TOAST = {
@@ -8,12 +8,13 @@ import { normalizeError } from "../../../errors/normalize";
8
8
  import { sortBy } from "@mohasinac/appkit/client";
9
9
  import React, { useState } from "react";
10
10
  import { ListingLayout } from "../../../ui";
11
- import { Button, Row, SideDrawer, Stack, Text, useToast, } from "../../../ui";
11
+ import { Button, Row, SideDrawer, Stack, Text, TextLink, useToast, } from "../../../ui";
12
12
  import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
13
13
  import { DataListingView } from "./DataListingView";
14
14
  import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
15
15
  import { apiClient } from "../../../http";
16
16
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
17
+ import { ROUTES } from "../../../next/routing/route-map";
17
18
  function ReassignProductsDrawer({ state, onClose, onSaved, }) {
18
19
  const [productIds, setProductIds] = useState(state.currentProductIds);
19
20
  const [saving, setSaving] = useState(false);
@@ -75,10 +76,21 @@ export function AdminGroupedListingsView({ children, ...props }) {
75
76
  })),
76
77
  getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
77
78
  buildFilters: () => "",
79
+ /*
80
+ * Admin had NO create path at all โ€” there was no POST route either,
81
+ * so this could not have been wired before now.
82
+ *
83
+ * `toolbarExtra` rather than `primaryAction`: the latter takes an
84
+ * `onClick(panel)` for opening a create DRAWER, and this create is a
85
+ * page. Same shape `AdminAdsView` uses, and `audit-create-affordance`
86
+ * counts both โ€” which is exactly why it has to, or four views with a
87
+ * perfectly good create link would read as having none.
88
+ */
89
+ toolbarExtra: (_jsx(TextLink, { variant: "bare", href: String(ROUTES.ADMIN.GROUPED_LISTINGS_NEW), rounded: "md", paddingX: "sm", size: "sm", weight: "medium", layout: "inline-flex", align: "center", className: "h-9 bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)]", children: "New group" })),
78
90
  // Mirrors the row action button's "Reassign products" drawer so the
79
91
  // row itself is clickable, not just the button.
80
92
  onRowClick: (row) => openReassign(row),
81
- renderRowActions: (row) => (_jsx(Button, { size: "sm", variant: "outline", onClick: () => openReassign(row), children: "Reassign products" })),
93
+ renderRowActions: (row) => (_jsxs(Row, { gap: "xs", children: [_jsx(TextLink, { href: String(ROUTES.ADMIN.GROUPED_LISTINGS_EDIT(row.id)), children: "Edit" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => openReassign(row), children: "Reassign products" })] })),
82
94
  }), [refreshKey, openReassign]);
83
95
  if (React.Children.count(children) > 0) {
84
96
  return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
@@ -17,7 +17,7 @@ const __O = {
17
17
  function NavLink({ item, isActive, onClick }) {
18
18
  return (_jsxs(Link, { href: item.href, onClick: onClick, className: `flex items-center justify-end gap-[var(--appkit-space-2-5)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[0.8125rem] font-medium leading-tight transition-colors ${isActive
19
19
  ? "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)]"
20
- : "text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/60 hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "truncate", children: item.label })] }));
20
+ : "text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "truncate", children: item.label })] }));
21
21
  }
22
22
  function GroupsContent({ groups, activePath, onItemClick, }) {
23
23
  // Accordion โ€” only one group open at a time. Initial pick: the group
@@ -52,7 +52,7 @@ function AdminCardItem({ row, view, selected, onToggleSelect, onRowClick, render
52
52
  };
53
53
  if (view === "list") {
54
54
  return (_jsxs(Row, { gap: "sm", className: [
55
- "px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] cursor-pointer transition-colors hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/50",
55
+ "px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] cursor-pointer transition-colors hover:bg-surface-hover",
56
56
  selected ? "bg-primary-50/40 dark:bg-primary-900/10" : "",
57
57
  ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: _jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }) })), _jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon }), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId }))] }), flags.length > 0 && (_jsx(Row, { gap: "xs", className: "hidden sm:flex shrink-0", children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) })), _jsx(StatusBadge, { status: row.status }), _jsx(Span, { size: "xs", color: "muted", className: "hidden sm:block shrink-0 w-24", align: "end", children: row.updatedAt }), renderRowActions && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
58
58
  }
@@ -60,7 +60,7 @@ function AdminCardItem({ row, view, selected, onToggleSelect, onRowClick, render
60
60
  "border overflow-hidden cursor-pointer transition-all hover:shadow-md hover:-translate-y-0.5",
61
61
  selected
62
62
  ? "border-primary ring-1 ring-primary/20 bg-primary-50/30 dark:bg-primary-900/10"
63
- : "border-zinc-100 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]",
63
+ : " border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]",
64
64
  ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsxs(Row, { gap: "xs", "data-no-row-click": true, paddingY: "t-sm", padding: "x-sm", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: [_jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }), flags.length > 0 && (_jsx(Row, { gap: "xs", wrap: true, children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) }))] })), _jsxs(Stack, { gap: "xs", className: `${__P.p3}.5`, children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon, size: "12" }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "line-clamp-2 leading-snug", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary })] })] }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId })), _jsxs(Row, { justify: "between", gap: "xs", children: [_jsx(StatusBadge, { status: row.status }), _jsx(Span, { color: "muted", className: "text-[11px] shrink-0", children: row.updatedAt })] }), renderRowActions && (_jsx(Row, { "data-no-row-click": true, justify: "end", gap: "xs", className: "border-t border-[var(--appkit-color-border)] -mx-[var(--appkit-space-3)] -mb-[var(--appkit-space-3)] mt-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)]", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] })] }));
65
65
  }
66
66
  export function AdminViewCards({ rows, view, isLoading, emptyLabel = "No items found", onRowClick, selectedIdSet, onToggleSelect, renderRowActions, resourceIcon, }) {
@@ -39,7 +39,7 @@ function SelectableRow({ row, columns, isSelected, onToggle, renderRowActions, o
39
39
  : undefined;
40
40
  const handleKeyDown = handleClick
41
41
  ? (event) => {
42
- if (event.key === "Enter" || event.key === " ")
42
+ if (event.key === "Enter" || event.key === "")
43
43
  handleClick();
44
44
  }
45
45
  : undefined;
@@ -54,5 +54,5 @@ export function DataTable({ columns: columnsProp, rows, isLoading, sortKey, sort
54
54
  const allRowsSelected = selectionEnabled && rows.length > 0 && rows.every((r) => selectedIds?.has(r.id));
55
55
  return (_jsxs(Div, { surface: "card", className: `${__O.hidden}`, children: [_jsx(Div, { className: `${__O.xAuto}`, children: _jsxs(Table, { "data-testid": "data-table", size: "sm", children: [_jsx(Thead, { children: _jsxs(Tr, { className: "bg-[var(--appkit-color-bg)] dark:bg-[var(--appkit-color-surface-elevated)]", border: "default", children: [selectionEnabled && (_jsx(Th, { padding: "xs-3", scope: "col", className: "w-10", children: onToggleSelectAll && (_jsx(Checkbox, { bare: true, "data-testid": "select-all-checkbox", "aria-label": allRowsSelected ? "Deselect all" : "Select all", checked: allRowsSelected, onChange: () => onToggleSelectAll(!allRowsSelected), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] accent-zinc-900 dark:accent-zinc-100" })) })), columns.map((col) => (_jsxs(Th, { scope: "col", onClick: col.sortable && onSort ? () => onSort(col.key) : undefined, className: `text-left text-[var(--appkit-color-text)] ${col.sortable && onSort ? "cursor-pointer select-none hover:text-primary" : ""} ${COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)]} ${col.className ?? ""}`, padding: "md", weight: "semibold", children: [col.header, col.sortable && sortKey === col.key && (_jsx(Span, { className: "ml-1", children: sortDir === "asc" ? "โ†‘" : "โ†“" }))] }, col.key))), renderRowActions && _jsx(Th, { padding: "xs-3", scope: "col", className: "w-12" })] }) }), _jsx(Tbody, { children: isLoading ? (Array.from({ length: 5 }).map((_, i) => (_jsxs(Tr, { border: "default", children: [selectionEnabled && _jsx(Td, { padding: "xs-3", className: "w-10" }), columns.map((col) => (_jsx(Td, { padding: "md", className: COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)], children: _jsx(Div, { className: "h-4 w-full animate-pulse bg-neutral-200", rounded: "default" }) }, col.key)))] }, i)))) : rows.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: columns.length + (selectionEnabled ? 1 : 0) + (renderRowActions ? 1 : 0), align: "center", color: "faint", paddingX: "md", paddingY: "md", children: emptyLabel }) })) : (rows.map((row) => (_jsx(SelectableRow, { row: row, columns: columns, isSelected: selectedIds?.has(row.id) ?? false, onToggle: onToggleSelect, renderRowActions: renderRowActions, onRowClick: onRowClick, rowHref: typeof rowHrefTemplate === "function"
56
56
  ? rowHrefTemplate(row)
57
- : rowHrefTemplate?.replace("{id}", encodeURIComponent(row.id)), selectionEnabled: selectionEnabled }, row.id)))) })] }) }), totalPages > 1 && onPageChange && (_jsx(Row, { className: "border-t border-neutral-200", padding: "inline", align: "center", justify: "end", gap: "sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (_jsx(Button, { onClick: () => onPageChange(p), variant: p === currentPage ? "primary" : "ghost", size: "sm", className: `h-8 w-8 rounded text-[length:var(--appkit-text-xs)] font-medium transition ${p === currentPage ? "bg-neutral-900 text-white" : "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] hover:bg-[var(--appkit-color-surface-elevated)]"}`, children: p }, p))) }))] }));
57
+ : rowHrefTemplate?.replace("{id}", encodeURIComponent(row.id)), selectionEnabled: selectionEnabled }, row.id)))) })] }) }), totalPages > 1 && onPageChange && (_jsx(Row, { className: "border-t border-neutral-200", padding: "inline", align: "center", justify: "end", gap: "sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (_jsx(Button, { onClick: () => onPageChange(p), variant: p === currentPage ? "primary" : "ghost", size: "sm", className: `h-8 w-8 rounded text-[length:var(--appkit-text-xs)] font-medium transition ${p === currentPage ? "bg-neutral-900 text-white" : "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: p }, p))) }))] }));
58
58
  }
@@ -254,11 +254,17 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
254
254
  codDepositPercent: 10,
255
255
  codHandlingFeeMin: 200,
256
256
  codHandlingFeePercent: 10,
257
- whatsappNotifyFeeEnabled: false,
257
+ // The three buyer add-ons ship ENABLED. They were `false`, which made
258
+ // StoreAddonsPicker return null on both the cart and checkout, so a fresh
259
+ // install had no add-ons at all and no indication why. `undefined` still
260
+ // means disabled everywhere (computeWhatsAppNotifyFee et al. read
261
+ // `if (!rates.XEnabled) return 0`) โ€” only the DEFAULT changed, so an
262
+ // existing siteSettings document keeps whatever it already stored.
263
+ whatsappNotifyFeeEnabled: true,
258
264
  whatsappNotifyFee: 10,
259
- giftWrapFeeEnabled: false,
265
+ giftWrapFeeEnabled: true,
260
266
  giftWrapFee: 49,
261
- shipmentProtectionFeeEnabled: false,
267
+ shipmentProtectionFeeEnabled: true,
262
268
  shipmentProtectionFeePercent: 2,
263
269
  shipmentProtectionFeeMin: 30,
264
270
  sellerShippingFixed: 0,
@@ -12,6 +12,7 @@ import { bidRepository } from "../repository/bid.repository";
12
12
  import { productRepository } from "../../products/repository/products.repository";
13
13
  import { isAuctionListing } from "../../products/utils/listing-type";
14
14
  import { userRepository } from "../../auth/repository/user.repository";
15
+ import { isSoftBanned } from "../../auth/server/checkSoftBan";
15
16
  import { unitOfWork } from "../../../core/unit-of-work";
16
17
  import { storeRepository } from "../../stores/repository/store.repository";
17
18
  import { siteSettingsRepository } from "../../../repositories";
@@ -28,6 +29,29 @@ import { cartRepository } from "../../cart/repository/cart.repository";
28
29
  import { AUCTION_BUYOUT_WINDOW_MINUTES, AUCTION_BUYOUT_WINDOW_MS, AUCTION_CHECKOUT_URL, } from "../../../_internal/shared/checkout/lanes";
29
30
  import { resolveDate } from "../../../utils";
30
31
  // --- Domain Functions ----------------------------------------------------------
32
+ /**
33
+ * Load the bidder's profile and refuse if they carry an active `place_bids`
34
+ * soft ban. Returns the profile so the caller pays for exactly one read.
35
+ *
36
+ * ๐Ÿ›‘ This belongs in the DOMAIN function, not in a route.
37
+ *
38
+ * Admins can issue a `place_bids` soft ban and `BID_ERROR_CODES.USER_BANNED`
39
+ * has always existed with a display-map entry and client copy โ€” but nothing
40
+ * ever threw it. The only `isSoftBanned(user, "place_bids")` call in the
41
+ * codebase sat in `POST /api/auctions/[id]/buy-now`, a route with zero callers,
42
+ * while the live path (the `placeBidAction` / `buyNowAction` server actions)
43
+ * checked nothing. A banned user could bid and buy out freely. Enforcing it
44
+ * here means every caller โ€” action, route, or job โ€” is covered by
45
+ * construction.
46
+ */
47
+ async function assertCanBid(userId) {
48
+ const profile = await userRepository.findById(userId);
49
+ if (profile && isSoftBanned(profile, "place_bids")) {
50
+ const ban = profile.softBans?.find((b) => b.action === "place_bids");
51
+ throw new AuthorizationError(`Your account is restricted from bidding. Reason: ${ban?.reason ?? "Policy violation"}.`, { code: BID_ERROR_CODES.USER_BANNED });
52
+ }
53
+ return profile ?? null;
54
+ }
31
55
  export async function placeBid(userId, userEmail, input) {
32
56
  const { productId, bidAmount, autoMaxBid } = input;
33
57
  const product = await productRepository.findById(productId);
@@ -50,6 +74,9 @@ export async function placeBid(userId, userEmail, input) {
50
74
  throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
51
75
  }
52
76
  }
77
+ // Before any write. The profile is reused below for the bid's denormalised
78
+ // userName/userEmail, so the ban check costs no extra read.
79
+ const profile = await assertCanBid(userId);
53
80
  // An auction with no bids yet prices off its starting bid, and that starting
54
81
  // bid is itself acceptable โ€” see ResolveMinBidOptions.hasBids.
55
82
  const hasBids = (product.currentBid ?? 0) > 0 || (product.bidCount ?? 0) > 0;
@@ -122,7 +149,6 @@ export async function placeBid(userId, userEmail, input) {
122
149
  visibleBid = newCap;
123
150
  bumpedPreviousVisible = Math.min(prevCap, target);
124
151
  }
125
- const profile = await userRepository.findById(userId);
126
152
  const bid = await bidRepository.create({
127
153
  productId,
128
154
  productTitle: product.title,
@@ -229,12 +255,24 @@ export async function placeBid(userId, userEmail, input) {
229
255
  return { bid };
230
256
  }
231
257
  export async function buyNowAuction(userId, userName, userEmail, input) {
232
- const { productId } = input;
233
- const product = await productRepository.findById(productId);
258
+ const { productId: productRef } = input;
259
+ // Auction URLs are slug-based, and this action is reachable from a route
260
+ // param, so accept either. Deliberately id-FIRST rather than
261
+ // `findByIdOrSlug`, which queries by slug first and would therefore cost two
262
+ // reads on the common id path (Rule #6) โ€” here the fallback query only runs
263
+ // when the id lookup actually misses.
264
+ //
265
+ // This was masked because every seeded auction has `slug === id`. Any auction
266
+ // whose slug differs threw NotFoundError, which the caller then swallowed.
267
+ const product = (await productRepository.findById(productRef)) ??
268
+ (await productRepository.findBySlug(productRef));
234
269
  if (!product)
235
270
  throw new NotFoundError(ERROR_MESSAGES.BID.AUCTION_NOT_FOUND);
236
271
  if (!isAuctionListing(product))
237
272
  throw new ValidationError(ERROR_MESSAGES.BID.NOT_AN_AUCTION);
273
+ // ๐Ÿ›‘ Every downstream write (the bid, the locked cart line, the notification)
274
+ // must key on the canonical document id, never on the ref the caller passed.
275
+ const productId = String(product.id);
238
276
  if (product.isSold) {
239
277
  throw new ValidationError(ERROR_MESSAGES.BID.AUCTION_ENDED, { code: BID_ERROR_CODES.AUCTION_ENDED });
240
278
  }
@@ -250,6 +288,10 @@ export async function buyNowAuction(userId, userName, userEmail, input) {
250
288
  throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
251
289
  }
252
290
  }
291
+ // Same guard as `placeBid` โ€” a buyout IS a bid (it writes a real
292
+ // `BidDocument` with `isBuyout: true`), so a `place_bids` ban must block it.
293
+ // The profile is reused for the bid's denormalised name/email below.
294
+ const profile = await assertCanBid(userId);
253
295
  const buyNowPrice = typeof product.buyNowPrice === "number" ? product.buyNowPrice : null;
254
296
  if (buyNowPrice === null || buyNowPrice <= 0) {
255
297
  throw new ValidationError(ERROR_MESSAGES.BID.BUY_NOW_NO_PRICE, { code: BID_ERROR_CODES.BUY_NOW_NO_PRICE });
@@ -286,7 +328,6 @@ export async function buyNowAuction(userId, userName, userEmail, input) {
286
328
  // The product's own currency, not the site default โ€” the cart line, the bid
287
329
  // and the listing all have to agree, and settlement uses the bid's currency.
288
330
  const currency = product.currency || getDefaultCurrency();
289
- const profile = await userRepository.findById(userId);
290
331
  // A real bid at the buy-now price โ€” this is what "the bid placed is the
291
332
  // buyout price by the buyer" means. It starts `active`/`isWinning: false`
292
333
  // like any other bid (bidRepository.create forces both) and is promoted to
@@ -1,8 +1,23 @@
1
- export interface AuctionBottomActionsProps {
2
- currentBid: number;
3
- currency: string;
1
+ import { type PlaceBidFormClientProps } from "./PlaceBidFormClient";
2
+ export interface AuctionBottomActionsProps extends PlaceBidFormClientProps {
4
3
  bidCount: number;
5
- isEnded: boolean;
6
- auctionEndDate: Date | null;
7
4
  }
8
- export declare function AuctionBottomActions({ currentBid, currency, bidCount, isEnded, auctionEndDate, }: AuctionBottomActionsProps): null;
5
+ /**
6
+ * Mobile/after-scroll CTA bar for an auction.
7
+ *
8
+ * ๐Ÿ›‘ It owns its own modal rather than pointing at one.
9
+ *
10
+ * Two defects lived here. (a) The single Place Bid action ran
11
+ * `getElementById("auction-bid-form")?.scrollIntoView(...)` โ€” but that id is
12
+ * set inside `renderBidForm`, which `AuctionDetailView` wraps in
13
+ * `hidden lg:block`. On mobile the target is `display:none`, so the bar's only
14
+ * action did nothing on the only viewport the bar exists for. A DOM id is not
15
+ * a contract; the element it names can be hidden, moved or removed without any
16
+ * signal here. (b) There was no Buy Now action at all, even though
17
+ * `MOBILE_PRIMARY_ACTIONS.auction` has declared `[PLACE_BID, BUY_NOW_AUCTION]`
18
+ * for a long time โ€” a declared CTA nothing rendered (Root Cause #8's shape).
19
+ *
20
+ * Both actions now open the real form, which is the only place the Buy Now
21
+ * button can report an error or route to `/checkout?lane=auction`.
22
+ */
23
+ export declare function AuctionBottomActions(props: AuctionBottomActionsProps): import("react").JSX.Element;
@@ -1,8 +1,13 @@
1
1
  "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState } from "react";
2
4
  import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
5
  import { formatCurrency } from "../../../utils/number.formatter";
4
6
  import { ACTION_ID, ACTION_META } from "../../products/constants/action-defs";
5
7
  import { useCountdown } from "../../../react";
8
+ import { Modal } from "../../../ui";
9
+ import { isBuyNowAvailable } from "../../../_internal/shared/features/auctions/config";
10
+ import { PlaceBidFormClient, } from "./PlaceBidFormClient";
6
11
  function formatCountdownLabel(remaining) {
7
12
  if (!remaining)
8
13
  return undefined;
@@ -13,25 +18,51 @@ function formatCountdownLabel(remaining) {
13
18
  return `Ends in ${hours}h ${minutes}m ${seconds}s`;
14
19
  return `Ends in ${minutes}m ${seconds}s`;
15
20
  }
16
- export function AuctionBottomActions({ currentBid, currency, bidCount, isEnded, auctionEndDate, }) {
17
- const placeBidMeta = ACTION_META[ACTION_ID.PLACE_BID];
21
+ /**
22
+ * Mobile/after-scroll CTA bar for an auction.
23
+ *
24
+ * ๐Ÿ›‘ It owns its own modal rather than pointing at one.
25
+ *
26
+ * Two defects lived here. (a) The single Place Bid action ran
27
+ * `getElementById("auction-bid-form")?.scrollIntoView(...)` โ€” but that id is
28
+ * set inside `renderBidForm`, which `AuctionDetailView` wraps in
29
+ * `hidden lg:block`. On mobile the target is `display:none`, so the bar's only
30
+ * action did nothing on the only viewport the bar exists for. A DOM id is not
31
+ * a contract; the element it names can be hidden, moved or removed without any
32
+ * signal here. (b) There was no Buy Now action at all, even though
33
+ * `MOBILE_PRIMARY_ACTIONS.auction` has declared `[PLACE_BID, BUY_NOW_AUCTION]`
34
+ * for a long time โ€” a declared CTA nothing rendered (Root Cause #8's shape).
35
+ *
36
+ * Both actions now open the real form, which is the only place the Buy Now
37
+ * button can report an error or route to `/checkout?lane=auction`.
38
+ */
39
+ export function AuctionBottomActions(props) {
40
+ const { currentBid, currency, bidCount, isEnded, auctionEndDate, buyNowPrice } = props;
41
+ const [open, setOpen] = useState(false);
18
42
  const remaining = useCountdown(auctionEndDate ?? undefined);
43
+ const placeBidMeta = ACTION_META[ACTION_ID.PLACE_BID];
44
+ const buyNowMeta = ACTION_META[ACTION_ID.BUY_NOW_AUCTION];
45
+ // Same single predicate the form and the card use. `!!props.onBuyNow` matters
46
+ // because the preview/demo mount passes no action.
47
+ const buyNowAvailable = isBuyNowAvailable({ buyNowPrice, currentBid, isEnded }) && !!props.onBuyNow;
19
48
  useBottomActions(isEnded
20
49
  ? {}
21
50
  : {
22
51
  actions: [
23
- {
24
- ...placeBidMeta,
25
- onClick: () => {
26
- document
27
- .getElementById("auction-bid-form")
28
- ?.scrollIntoView({ behavior: "smooth" });
29
- },
30
- },
52
+ { ...placeBidMeta, onClick: () => setOpen(true) },
53
+ ...(buyNowAvailable && buyNowPrice !== null
54
+ ? [
55
+ {
56
+ ...buyNowMeta,
57
+ label: `Buy Now โ€” ${formatCurrency(buyNowPrice, currency)}`,
58
+ onClick: () => setOpen(true),
59
+ },
60
+ ]
61
+ : []),
31
62
  ],
32
63
  secondaryLabel: formatCountdownLabel(remaining),
33
64
  infoLabel: `${formatCurrency(currentBid, currency)} ยท ${bidCount} bid${bidCount !== 1 ? "s" : ""}`,
34
65
  desktop: "after-scroll",
35
66
  });
36
- return null;
67
+ return (_jsx(Modal, { isOpen: open, onClose: () => setOpen(false), size: "md", title: "Place your bid", children: _jsx(PlaceBidFormClient, { ...props }) }));
37
68
  }
@@ -1,5 +1,5 @@
1
1
  import type { ProductFeatureDocument } from "../../products/schemas/product-features";
2
- import type { PlaceBidInput } from "./PlaceBidFormClient";
2
+ import type { PlaceBidInput, BidActionEnvelope, BuyNowSuccess } from "./PlaceBidFormClient";
3
3
  export interface AuctionDetailPageViewProps {
4
4
  id: string;
5
5
  /**
@@ -9,8 +9,13 @@ export interface AuctionDetailPageViewProps {
9
9
  * When absent, falls back to fetching by slug/id (backward compat).
10
10
  */
11
11
  initialAuction?: import("../../products/schemas/firestore").ProductDocument | null;
12
- onPlaceBid?: (input: PlaceBidInput) => Promise<unknown>;
13
- onBuyNow?: () => Promise<unknown>;
12
+ /**
13
+ * Typed against the single `ActionResult` envelope โ€” see the note on
14
+ * `BidActionEnvelope`. `Promise<unknown>` here is what let a doubly-wrapped
15
+ * result flow through to a client that read the wrong `ok`.
16
+ */
17
+ onPlaceBid?: (input: PlaceBidInput) => Promise<BidActionEnvelope<unknown>>;
18
+ onBuyNow?: () => Promise<BidActionEnvelope<BuyNowSuccess>>;
14
19
  /** SSR-loaded productFeatures (platform + store-scope). See ProductDetailPageView for semantics. */
15
20
  productFeatures?: ProductFeatureDocument[];
16
21
  }
@@ -46,7 +46,7 @@ function toDescriptionHtml(raw) {
46
46
  }
47
47
  function renderAuctionInfoPanel(props) {
48
48
  const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
49
- return (_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_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", 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-[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: "Listed 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" })] }) }))] }));
49
+ return (_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_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", 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-[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: "Listed 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" })] }) }))] }));
50
50
  }
51
51
  /**
52
52
  * Store reviews for the auction's seller.
@@ -204,5 +204,5 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
204
204
  slug: typeof r.slug === "string" ? r.slug : undefined,
205
205
  }));
206
206
  return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related, gridClassName: "grid grid-cols-2 gap-[var(--appkit-space-4)] sm:grid-cols-3 lg:grid-cols-4" })) })), _jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] }));
207
- } }), renderAuctionStoreReviews(storeId), _jsx(AuctionBottomActions, { currentBid: currentBid, currency: currency, bidCount: bidCount, isEnded: isEnded, auctionEndDate: endDate })] })] }));
207
+ } }), renderAuctionStoreReviews(storeId), onPlaceBid && (_jsx(AuctionBottomActions, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow }))] })] }));
208
208
  }