@mohasinac/appkit 4.42.18 → 4.44.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 (263) hide show
  1. package/dist/_internal/client/features/lottery/LotteryDetailView.js +22 -1
  2. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  3. package/dist/_internal/server/features/categories/adapters.js +235 -0
  4. package/dist/_internal/server/features/checkout/actions.js +161 -22
  5. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  6. package/dist/_internal/server/features/orders/adapters.js +21 -0
  7. package/dist/_internal/server/features/products/list-public.d.ts +24 -0
  8. package/dist/_internal/server/features/products/list-public.js +10 -2
  9. package/dist/_internal/server/features/refunds/actions.js +1 -1
  10. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  11. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  12. package/dist/_internal/server/features/stores/adapters.js +6 -0
  13. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  14. package/dist/_internal/server/functions/scheduled.js +64 -6
  15. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  16. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  17. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  18. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  19. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  20. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  21. package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
  22. package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
  23. package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
  24. package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
  25. package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
  26. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  27. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  28. package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
  29. package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
  30. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  31. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  32. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  33. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  34. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  35. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  36. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  37. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  38. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  39. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  40. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  41. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  42. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  43. package/dist/_internal/shared/actions/action-registry.js +34 -0
  44. package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
  45. package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
  46. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  47. package/dist/_internal/shared/checkout/order-math.js +194 -7
  48. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  49. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  50. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  51. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  52. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  53. package/dist/_internal/shared/fees/calculator.js +143 -0
  54. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  55. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  56. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  57. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  58. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  59. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  60. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  61. package/dist/_internal/shared/templating/placeholders.js +73 -0
  62. package/dist/client.d.ts +2 -0
  63. package/dist/client.js +1 -0
  64. package/dist/constants/api-endpoints.d.ts +42 -0
  65. package/dist/constants/api-endpoints.js +14 -0
  66. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  67. package/dist/features/admin/actions/admin-actions.js +1 -1
  68. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  69. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  70. package/dist/features/admin/components/AdminProductsView.js +78 -7
  71. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  72. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  73. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  74. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  75. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  76. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  77. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  78. package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
  79. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
  80. package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
  81. package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
  82. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  83. package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
  84. package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
  85. package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
  86. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  87. package/dist/features/blog/api/route.js +16 -1
  88. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  89. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  90. package/dist/features/categories/actions/category-actions.d.ts +2 -2
  91. package/dist/features/categories/actions/category-actions.js +22 -10
  92. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  93. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  94. package/dist/features/categories/api/route.js +156 -11
  95. package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
  96. package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
  97. package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
  98. package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
  99. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  100. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  101. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  102. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  103. package/dist/features/categories/constants/listing-categories.js +53 -0
  104. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  105. package/dist/features/categories/repository/categories.repository.d.ts +73 -12
  106. package/dist/features/categories/repository/categories.repository.js +203 -42
  107. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  108. package/dist/features/categories/repository/price-index.repository.js +85 -0
  109. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  110. package/dist/features/categories/schemas/category-content.js +28 -0
  111. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  112. package/dist/features/categories/schemas/firestore.js +74 -3
  113. package/dist/features/categories/schemas/index.d.ts +1047 -0
  114. package/dist/features/categories/schemas/index.js +96 -0
  115. package/dist/features/categories/server.d.ts +1 -0
  116. package/dist/features/categories/server.js +7 -0
  117. package/dist/features/categories/types/index.d.ts +9 -0
  118. package/dist/features/events/schemas/firestore.d.ts +2 -2
  119. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  120. package/dist/features/faq/schemas/firestore.js +20 -8
  121. package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
  122. package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
  123. package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
  124. package/dist/features/homepage/lib/live-stats.js +18 -8
  125. package/dist/features/media/MediaImage.js +19 -2
  126. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  127. package/dist/features/orders/types/index.d.ts +55 -0
  128. package/dist/features/products/actions/product-actions.d.ts +0 -9
  129. package/dist/features/products/actions/product-actions.js +25 -0
  130. package/dist/features/products/columns/index.d.ts +0 -1
  131. package/dist/features/products/columns/index.js +5 -1
  132. package/dist/features/products/components/ArtStickersListView.js +7 -1
  133. package/dist/features/products/components/PrizeDrawsSection.js +5 -0
  134. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  135. package/dist/features/products/components/ProductForm.d.ts +27 -1
  136. package/dist/features/products/components/ProductForm.js +26 -12
  137. package/dist/features/products/components/ProductsIndexPageView.js +1 -1
  138. package/dist/features/products/constants/sieve.d.ts +0 -6
  139. package/dist/features/products/constants/sieve.js +35 -2
  140. package/dist/features/products/repository/products.repository.d.ts +115 -1
  141. package/dist/features/products/repository/products.repository.js +491 -39
  142. package/dist/features/products/schemas/firestore.d.ts +65 -5
  143. package/dist/features/products/schemas/firestore.js +9 -0
  144. package/dist/features/products/schemas/product-features.d.ts +140 -2
  145. package/dist/features/products/schemas/product-features.js +62 -1
  146. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  147. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  148. package/dist/features/promotions/schemas/firestore.js +76 -2
  149. package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
  150. package/dist/features/reviews/repository/reviews.repository.js +35 -0
  151. package/dist/features/seller/actions/seller-actions.js +17 -2
  152. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  153. package/dist/features/seller/components/PrintCenterView.js +3 -2
  154. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  155. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  156. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  157. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  158. package/dist/features/seller/components/SellerProductShell.js +101 -9
  159. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  160. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  161. package/dist/features/seller/components/SellerProductsView.js +108 -3
  162. package/dist/features/seller/types/index.d.ts +17 -0
  163. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  164. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  165. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  166. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  167. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  168. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  169. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  170. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  171. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  172. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  173. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  174. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  175. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  176. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  177. package/dist/features/tester/actions/index.d.ts +0 -1
  178. package/dist/features/tester/actions/index.js +0 -1
  179. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  180. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  181. package/dist/features/tester/components/index.d.ts +0 -4
  182. package/dist/features/tester/components/index.js +0 -2
  183. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  184. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  185. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  186. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  187. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  188. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  189. package/dist/features/tester/seed-data/authored/index.js +0 -2
  190. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  191. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
  192. package/dist/index.d.ts +21 -7
  193. package/dist/index.js +53 -7
  194. package/dist/next/routing/route-map.d.ts +2 -4
  195. package/dist/next/routing/route-map.js +1 -2
  196. package/dist/repositories/index.d.ts +1 -0
  197. package/dist/repositories/index.js +1 -0
  198. package/dist/schemas/registry.d.ts +506 -0
  199. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  200. package/dist/seed/_helpers/brand-rows.js +576 -0
  201. package/dist/seed/_helpers/category-content.d.ts +39 -0
  202. package/dist/seed/_helpers/category-content.js +634 -0
  203. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  204. package/dist/seed/_helpers/category-forest.js +585 -243
  205. package/dist/seed/_helpers/category-models.d.ts +5 -0
  206. package/dist/seed/_helpers/category-models.js +1364 -0
  207. package/dist/seed/_helpers/category-tree.js +17 -1
  208. package/dist/seed/_helpers/character-features.d.ts +10 -0
  209. package/dist/seed/_helpers/character-features.js +118 -0
  210. package/dist/seed/_helpers/media.d.ts +1 -1
  211. package/dist/seed/_helpers/media.js +44 -8
  212. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  213. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  214. package/dist/seed/categories-seed-data.js +147 -169
  215. package/dist/seed/index.d.ts +1 -0
  216. package/dist/seed/index.js +1 -0
  217. package/dist/seed/manifest.d.ts +1 -1
  218. package/dist/seed/manifest.js +5 -0
  219. package/dist/seed/orders-seed-data.js +5 -3
  220. package/dist/seed/product-features-seed-data.d.ts +1 -8
  221. package/dist/seed/product-features-seed-data.js +358 -154
  222. package/dist/seed/products-art-seed-data.js +16 -4
  223. package/dist/seed/products-auctions-seed-data.js +6 -6
  224. package/dist/seed/products-classifieds-seed-data.js +17 -7
  225. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  226. package/dist/seed/products-preorders-seed-data.js +8 -8
  227. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  228. package/dist/seed/products-standard-seed-data.js +10 -60
  229. package/dist/seed/products-stickers-seed-data.js +6 -6
  230. package/dist/seed/site-settings-seed-data.js +24 -0
  231. package/dist/seed/store-extensions-seed-data.js +10 -4
  232. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  233. package/dist/seed/tax-codes-seed-data.js +100 -0
  234. package/dist/seed/test-data-ttl.d.ts +29 -0
  235. package/dist/seed/test-data-ttl.js +31 -0
  236. package/dist/server-entry.d.ts +1 -1
  237. package/dist/server-entry.js +6 -1
  238. package/dist/server.d.ts +4 -1
  239. package/dist/server.js +11 -2
  240. package/dist/styles.css +40 -0
  241. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  242. package/dist/ui/components/PaginatedSelect.js +0 -0
  243. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  244. package/dist/utils/media-url.d.ts +26 -0
  245. package/dist/utils/media-url.js +144 -0
  246. package/dist/utils/physical-location.d.ts +23 -0
  247. package/dist/utils/physical-location.js +52 -0
  248. package/dist/utils/search-txt-builders.d.ts +20 -0
  249. package/dist/utils/search-txt-builders.js +36 -0
  250. package/package.json +1 -1
  251. package/scripts/seed-cli.mjs +4 -1
  252. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  253. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  254. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  255. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  256. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  257. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  258. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  259. package/dist/features/tester/components/TesterHubView.js +0 -110
  260. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  261. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  262. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  263. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { StackedViewShellProps } from "../../../ui";
3
+ export interface AdminTaxCodeEditorViewProps extends Omit<StackedViewShellProps, "sections"> {
4
+ taxCodeId?: string;
5
+ /** When true, omit the StackedViewShell wrapper (SideDrawer body). */
6
+ embedded?: boolean;
7
+ onSaved?: (id: string) => void;
8
+ onDeleted?: () => void;
9
+ }
10
+ export declare function AdminTaxCodeEditorView({ taxCodeId, embedded, onSaved, onDeleted, ...rest }: AdminTaxCodeEditorViewProps): React.JSX.Element;
@@ -0,0 +1,160 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useApiMutation } from "@mohasinac/appkit/client";
4
+ import React from "react";
5
+ import { useQuery } from "@tanstack/react-query";
6
+ import { ConfirmDeleteModal, Div, StackedViewShell, useToast } from "../../../ui";
7
+ import { apiClient } from "../../../http";
8
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
+ import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
10
+ import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell";
11
+ import { buildSectionsFromSchema, visibleValues } from "../../shell/build-sections";
12
+ import { SectionForm, useSectionFormNav } from "../../shell/SectionForm";
13
+ import { taxCodeFormSchema, GST_RATE_OPTIONS, HSN_CHAPTER_OPTIONS, } from "../../tax-codes/schemas/tax-code-form";
14
+ const __P = { p4: "p-[var(--appkit-space-4)]" };
15
+ const __O = { yAuto: "overflow-y-auto" };
16
+ /**
17
+ * 🛑 The delete copy says what the server will actually do.
18
+ *
19
+ * `DELETE /api/admin/tax-codes/[id]` REFUSES with a 409 while any category
20
+ * still points here, and names the referrers. Promising an unconditional
21
+ * delete and then showing a 409 is how a confirmation dialog loses its
22
+ * meaning — and the honest alternative (deactivate) is the one an admin
23
+ * usually wants anyway, since an inactive code is already skipped by
24
+ * `findResolvable` and gone from every picker.
25
+ */
26
+ const DELETE_CONFIRM_TEXT = "Delete this tax code? This is refused while any category still points at it — " +
27
+ "set it inactive instead to retire it without breaking those categories.";
28
+ const TOAST = {
29
+ CREATED: "Tax code created.",
30
+ UPDATED: "Tax code updated.",
31
+ DELETED: "Tax code deleted.",
32
+ };
33
+ export function AdminTaxCodeEditorView({ taxCodeId, embedded, onSaved, onDeleted, ...rest }) {
34
+ const isEdit = Boolean(taxCodeId);
35
+ const { showToast } = useToast();
36
+ const [form, setForm] = React.useState({
37
+ label: "",
38
+ hsnCode: "",
39
+ // The default rate is the one 70 of this catalogue's products need.
40
+ gstRate: "5",
41
+ chapter: "9503",
42
+ description: "",
43
+ notes: "",
44
+ isActive: true,
45
+ });
46
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
47
+ const patch = (partial) => setForm((prev) => Object.assign({}, prev, partial));
48
+ const codeQuery = useQuery({
49
+ queryKey: ["admin", "tax-code", taxCodeId],
50
+ queryFn: async () => {
51
+ const res = await apiClient.get(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId));
52
+ return res?.data ?? res;
53
+ },
54
+ enabled: isEdit,
55
+ });
56
+ React.useEffect(() => {
57
+ const t = codeQuery.data;
58
+ if (!t)
59
+ return;
60
+ patch({
61
+ label: t.label ?? "",
62
+ hsnCode: t.hsnCode ?? "",
63
+ /*
64
+ * `?? 5` would be wrong here and the distinction is the whole point of
65
+ * this field: rate 0 is a DELIBERATE exemption, and `t.gstRate ?? 5`
66
+ * would silently re-rate an exempt row to 5% the moment an admin opened
67
+ * it and saved anything. Test for null, not falsiness.
68
+ */
69
+ gstRate: t.gstRate == null ? "5" : String(t.gstRate),
70
+ chapter: t.chapter ?? "",
71
+ description: t.description ?? "",
72
+ notes: t.notes ?? "",
73
+ isActive: t.isActive ?? true,
74
+ });
75
+ }, [codeQuery.data]);
76
+ const saveMutation = useApiMutation({
77
+ errorMessage: "Failed to save tax code.",
78
+ mutationFn: async () => {
79
+ const payload = {
80
+ label: form.label,
81
+ hsnCode: form.hsnCode.trim() || undefined,
82
+ gstRate: Number(form.gstRate),
83
+ chapter: form.chapter || undefined,
84
+ description: form.description || undefined,
85
+ notes: form.notes || undefined,
86
+ isActive: form.isActive,
87
+ };
88
+ /*
89
+ * PATCH, not PUT — the route is `.strict()` and partial by design, so a
90
+ * PUT would 404. (`AdminFeatureEditorView` uses PUT because ITS route
91
+ * declares PUT; copying the verb rather than reading the route is how
92
+ * that kind of mismatch ships.)
93
+ */
94
+ if (isEdit) {
95
+ return apiClient.patch(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId), payload);
96
+ }
97
+ return apiClient.post(ADMIN_ENDPOINTS.TAX_CODES, payload);
98
+ },
99
+ onSuccess: (res) => {
100
+ const id = res?.data?.id ??
101
+ res?.id ??
102
+ taxCodeId;
103
+ showToast(isEdit ? TOAST.UPDATED : TOAST.CREATED, "success");
104
+ if (onSaved && id)
105
+ onSaved(String(id));
106
+ },
107
+ });
108
+ const deleteMutation = useApiMutation({
109
+ errorMessage: "Failed to delete tax code.",
110
+ mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId)),
111
+ onSuccess: () => {
112
+ showToast(TOAST.DELETED, "success");
113
+ if (onDeleted)
114
+ onDeleted();
115
+ },
116
+ });
117
+ const isSubmitting = saveMutation.isPending || codeQuery.isLoading;
118
+ const sections = React.useMemo(() => buildSectionsFromSchema(taxCodeFormSchema, {
119
+ // `FieldUiMeta` has no `options` key — a select's VALUES are schema and
120
+ // its option LIST is supplied here. Same split as the bundle editor.
121
+ options: {
122
+ gstRate: [...GST_RATE_OPTIONS],
123
+ chapter: [{ value: "", label: "No chapter" }, ...HSN_CHAPTER_OPTIONS],
124
+ },
125
+ }), []);
126
+ const nav = useSectionFormNav(sections, form, { scope: "admin:tax-code-editor" });
127
+ const { shellCtx, setFieldError, clearErrors } = useFormShellState(taxCodeFormSchema, {
128
+ sections: nav.sectionMeta,
129
+ onGoToSection: nav.goToSection,
130
+ fieldToSectionIndex: nav.fieldToSectionIndex,
131
+ });
132
+ const onSubmit = () => {
133
+ clearErrors();
134
+ const draft = visibleValues(taxCodeFormSchema, form);
135
+ const parsed = taxCodeFormSchema.safeParse({
136
+ ...draft,
137
+ hsnCode: form.hsnCode.trim() || undefined,
138
+ gstRate: Number(form.gstRate),
139
+ chapter: form.chapter || undefined,
140
+ description: form.description || undefined,
141
+ notes: form.notes || undefined,
142
+ });
143
+ if (!parsed.success) {
144
+ applyZodIssues(parsed.error.issues, setFieldError);
145
+ return;
146
+ }
147
+ saveMutation.mutate();
148
+ };
149
+ const formSection = (_jsxs(FormShellContext.Provider, { value: shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: form, onChange: patch, onSubmit: onSubmit, schema: taxCodeFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, isLoading: isSubmitting, submitLabel: isEdit ? "Save changes" : "Create tax code", destructiveAction: isEdit
150
+ ? { label: "Delete", onClick: () => setDeleteConfirmOpen(true) }
151
+ : undefined })] }));
152
+ const deleteModal = deleteConfirmOpen && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Tax Code", message: DELETE_CONFIRM_TEXT, onConfirm: () => {
153
+ deleteMutation.mutate();
154
+ setDeleteConfirmOpen(false);
155
+ }, onClose: () => setDeleteConfirmOpen(false), isDeleting: deleteMutation.isPending }));
156
+ if (embedded) {
157
+ return (_jsxs(Div, { className: `${__O.yAuto} ${__P.p4}`, children: [formSection, deleteModal] }));
158
+ }
159
+ return (_jsxs(_Fragment, { children: [_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Tax Code" : "New Tax Code", sections: [formSection] }), deleteModal] }));
160
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import type { ListingLayoutProps } from "../../../ui";
3
+ export type AdminTaxCodesViewProps = ListingLayoutProps;
4
+ /**
5
+ * `/admin/tax-codes` — the HSN + GST rate catalogue.
6
+ *
7
+ * 🛑 No bulk actions, deliberately. The two that would be offered by habit are
8
+ * both wrong here: a bulk DELETE would hit the referential guard row by row
9
+ * and report a partial failure an admin cannot act on, and a bulk
10
+ * activate/deactivate across a mixed selection is a rate change applied to
11
+ * things nobody looked at. There are ~10 rows; each one is a per-row decision.
12
+ */
13
+ export declare function AdminTaxCodesView({ children, ...props }: AdminTaxCodesViewProps): React.JSX.Element;
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { sortBy } from "@mohasinac/appkit/client";
4
+ import React from "react";
5
+ import { ListingLayout } from "../../../ui";
6
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
7
+ import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
8
+ import { DataListingView } from "./DataListingView";
9
+ import { AdminTaxCodeEditorView } from "./AdminTaxCodeEditorView";
10
+ const PAGE_SIZE = 50;
11
+ function mapTaxCodeRow(item, index) {
12
+ const isActive = typeof item.isActive === "boolean" ? item.isActive : true;
13
+ const rate = typeof item.gstRate === "number" ? item.gstRate : null;
14
+ const hsn = toStringValue(item.hsnCode, "");
15
+ return {
16
+ id: toStringValue(item.id, `tax-${index}`),
17
+ primary: toStringValue(item.label, "Untitled tax code"),
18
+ /*
19
+ * The RATE leads, because it is the number an admin is checking. `0%` is
20
+ * rendered explicitly rather than falling back to an em-dash: a blank
21
+ * would read as "not set", and 0 is a deliberate exemption — the same
22
+ * `undefined`-vs-`0` distinction the field itself depends on. A rate that
23
+ * genuinely has no value says so.
24
+ */
25
+ secondary: [
26
+ rate == null ? "rate not set" : `${rate}% GST`,
27
+ hsn ? `HSN ${hsn}` : "no HSN (exempt)",
28
+ toStringValue(item.chapter, "") ? `chapter ${item.chapter}` : "",
29
+ ]
30
+ .filter(Boolean)
31
+ .join(" — "),
32
+ status: isActive ? "Active" : "Inactive",
33
+ updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
34
+ };
35
+ }
36
+ /**
37
+ * `/admin/tax-codes` — the HSN + GST rate catalogue.
38
+ *
39
+ * 🛑 No bulk actions, deliberately. The two that would be offered by habit are
40
+ * both wrong here: a bulk DELETE would hit the referential guard row by row
41
+ * and report a partial failure an admin cannot act on, and a bulk
42
+ * activate/deactivate across a mixed selection is a rate change applied to
43
+ * things nobody looked at. There are ~10 rows; each one is a per-row decision.
44
+ */
45
+ export function AdminTaxCodesView({ children, ...props }) {
46
+ if (React.Children.count(children) > 0) {
47
+ return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
48
+ }
49
+ const config = {
50
+ portal: "admin",
51
+ title: "Tax Codes",
52
+ // No search box: this endpoint does not read `q`, and a box that accepts
53
+ // typing and changes nothing is worse than no box
54
+ // (audit-listing-search-capability's rule). ~10 rows do not need one.
55
+ emptyLabel: "No tax codes yet",
56
+ filterKeys: [],
57
+ defaultSort: sortBy("label", "ASC"),
58
+ pageSize: PAGE_SIZE,
59
+ queryKey: ["admin", "tax-codes", "listing"],
60
+ /*
61
+ * `?all=true` — the admin list MUST show inactive rows, because
62
+ * deactivating is how a code is retired and reactivating has to be
63
+ * possible. The route's default is active-only because its other caller is
64
+ * the category editor's picker, where offering an inactive code would let
65
+ * an admin point a category at something `findResolvable` then refuses.
66
+ */
67
+ endpoint: `${ADMIN_ENDPOINTS.TAX_CODES}?all=true`,
68
+ sortOptions: [
69
+ { value: "label", label: "Label A–Z" },
70
+ { value: sortBy("label", "DESC"), label: "Label Z–A" },
71
+ { value: "gstRate", label: "Rate, low to high" },
72
+ { value: sortBy("gstRate", "DESC"), label: "Rate, high to low" },
73
+ { value: "hsnCode", label: "HSN code" },
74
+ ],
75
+ mapRows: (response) => toRecordArray(response.items).map(mapTaxCodeRow),
76
+ getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
77
+ buildFilters: () => undefined,
78
+ primaryAction: {
79
+ label: "Add Tax Code",
80
+ onClick: ({ openCreatePanel }) => openCreatePanel(),
81
+ },
82
+ renderEditor: ({ editId, closePanel }) => (_jsx(AdminTaxCodeEditorView, { taxCodeId: editId ?? undefined, onSaved: closePanel, onDeleted: closePanel, embedded: true })),
83
+ resolveEditorTitle: ({ isCreate }) => isCreate ? "Add Tax Code" : "Edit Tax Code",
84
+ };
85
+ return _jsx(DataListingView, { config: config });
86
+ }
@@ -6,7 +6,7 @@ import React, { useMemo } from "react";
6
6
  import { useQueryClient } from "@tanstack/react-query";
7
7
  import { FilterChipGroup, Heading, ListingLayout, RowActionMenu, Span, Text } from "../../../ui";
8
8
  import { apiClient } from "../../../http";
9
- import { useApiMutation } from "../../../client";
9
+ import { useApiMutation } from "../../../client/api/useApiMutation";
10
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
11
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
12
12
  import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Div, Heading, Row, Text } from "../../../../ui";
3
3
  export function AdminTopProductsTable({ products, labels = {}, formatRevenue = (n) => String(n), renderProductLink, className = "", }) {
4
- return (_jsxs(Div, { className: `border border-[var(--appkit-color-border-subtle)] sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-[var(--appkit-color-border-subtle)]", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }), product.viewCount != null && (_jsx(Div, { className: "text-right ml-4 hidden sm:block", children: _jsxs(Text, { size: "sm", color: "muted", children: [product.viewCount, " ", labels.views ?? "views"] }) })), _jsxs(Div, { className: "text-right ml-4", children: [_jsx(Text, { weight: "semibold", children: formatRevenue(product.revenue) }), renderProductLink?.(product)] })] }, product.productId))) })] }));
4
+ return (_jsxs(Div, { className: `border border-[var(--appkit-color-border-subtle)] sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-[var(--appkit-color-border-subtle)]", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }), _jsxs(Div, { className: "text-right ml-4", children: [_jsx(Text, { weight: "semibold", children: formatRevenue(product.revenue) }), renderProductLink?.(product)] })] }, product.productId))) })] }));
5
5
  }
@@ -6,6 +6,7 @@ import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
6
6
  import { formatCurrency } from "../../../utils/number.formatter";
7
7
  import { ACTION_ID, ACTION_META } from "../../products/constants/action-defs";
8
8
  import { useCountdown } from "../../../react";
9
+ import { useIsAuctionEnded } from "../hooks/useIsAuctionEnded";
9
10
  import { Modal } from "../../../ui";
10
11
  import { isBuyNowAvailable } from "../../../_internal/shared/features/auctions/config";
11
12
  import { PlaceBidFormClient, } from "./PlaceBidFormClient";
@@ -38,7 +39,15 @@ function formatCountdownLabel(remaining) {
38
39
  * button can report an error or route to `/checkout?lane=auction`.
39
40
  */
40
41
  export function AuctionBottomActions(props) {
41
- const { currentBid, currency, bidCount, isEnded, auctionEndDate, buyNowPrice } = props;
42
+ const { currentBid, currency, bidCount, isEnded: ssrIsEnded, auctionEndDate, buyNowPrice } = props;
43
+ /*
44
+ * Derived client-side — see `useIsAuctionEnded`. This bar is a FIXED, always-
45
+ * visible row on mobile, so a stale "Place bid" here is the most prominent
46
+ * wrong affordance on the page. `useCountdown` below already knew the truth
47
+ * (it returns null past the target) but only fed a label; the bar's actions
48
+ * were gated on the frozen server boolean.
49
+ */
50
+ const isEnded = useIsAuctionEnded(auctionEndDate, ssrIsEnded);
42
51
  const [open, setOpen] = useState(false);
43
52
  const remaining = useCountdown(auctionEndDate ?? undefined);
44
53
  const { canSeePrices } = useCanSeePrices();
@@ -22,6 +22,7 @@ import { AuctionDetailView } from "../../products/components/AuctionDetailView";
22
22
  import { ProductTabsShell } from "../../products/components/ProductTabsShell";
23
23
  import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent";
24
24
  import { AuctionBottomActions } from "./AuctionBottomActions";
25
+ import { AuctionLiveStatusBadge } from "./AuctionLiveStatusBadge";
25
26
  import { RelatedProducts } from "../../products/components/RelatedProducts";
26
27
  import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
27
28
  import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges";
@@ -48,7 +49,7 @@ function toDescriptionHtml(raw) {
48
49
  }
49
50
  function renderAuctionInfoPanel(props) {
50
51
  const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, finalSale, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
51
- 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: _jsx(GatedPrice, { label: "Sign in to see the Buy Now price", 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, finalSale: finalSale, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", finalSale: "Final Sale", 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" })] }) }))] }));
52
+ 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" }), _jsx(AuctionLiveStatusBadge, { auctionEndDate: endDate, initialIsEnded: isEnded })] }), _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: _jsx(GatedPrice, { label: "Sign in to see the Buy Now price", 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, finalSale: finalSale, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", finalSale: "Final Sale", 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" })] }) }))] }));
52
53
  }
53
54
  /**
54
55
  * Store reviews for the auction's seller.
@@ -0,0 +1,20 @@
1
+ export interface AuctionLiveStatusBadgeProps {
2
+ /** The auction's end timestamp — the only thing that decides this. */
3
+ auctionEndDate: Date | string | null;
4
+ /** The server's answer, used as the initial value so SSR and hydration agree. */
5
+ initialIsEnded: boolean;
6
+ }
7
+ /**
8
+ * Active / Ended, decided on the client.
9
+ *
10
+ * The server-rendered version of this badge sat directly above a countdown that
11
+ * already flipped itself to "Ended" — so a cached auction page could render
12
+ * `Ends in Ended` beside a green **Active** pill. The countdown was right and the
13
+ * badge was stale, in the same `<Stack>`.
14
+ *
15
+ * Extracted to a client component rather than passing a derived prop down,
16
+ * because `AuctionDetailPageView` is a Server Component and cannot hold the
17
+ * timer. Colours follow the status-pair rule (Root Cause #67): a theme-relative
18
+ * surface with its matching theme-relative ink, never a literal `text-white`.
19
+ */
20
+ export declare function AuctionLiveStatusBadge({ auctionEndDate, initialIsEnded, }: AuctionLiveStatusBadgeProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Span } from "../../../ui";
4
+ import { useIsAuctionEnded } from "../hooks/useIsAuctionEnded";
5
+ /**
6
+ * Active / Ended, decided on the client.
7
+ *
8
+ * The server-rendered version of this badge sat directly above a countdown that
9
+ * already flipped itself to "Ended" — so a cached auction page could render
10
+ * `Ends in Ended` beside a green **Active** pill. The countdown was right and the
11
+ * badge was stale, in the same `<Stack>`.
12
+ *
13
+ * Extracted to a client component rather than passing a derived prop down,
14
+ * because `AuctionDetailPageView` is a Server Component and cannot hold the
15
+ * timer. Colours follow the status-pair rule (Root Cause #67): a theme-relative
16
+ * surface with its matching theme-relative ink, never a literal `text-white`.
17
+ */
18
+ export function AuctionLiveStatusBadge({ auctionEndDate, initialIsEnded, }) {
19
+ const isEnded = useIsAuctionEnded(auctionEndDate, initialIsEnded);
20
+ return 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" }));
21
+ }
@@ -45,7 +45,7 @@ export interface PlaceBidFormClientProps {
45
45
  onPlaceBid: (input: PlaceBidInput) => Promise<BidActionEnvelope<unknown>>;
46
46
  onBuyNow?: () => Promise<BidActionEnvelope<BuyNowSuccess>>;
47
47
  }
48
- export declare function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
48
+ export declare function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded: ssrIsEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
49
49
  /**
50
50
  * `PlaceBidModalButton` — opens a modal that hosts the same PlaceBidFormClient.
51
51
  *
@@ -13,6 +13,7 @@ import { FieldInput } from "../../../ui/forms/FieldInput";
13
13
  import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
14
14
  import { placeBidSchema } from "../schemas/bid-input";
15
15
  import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
16
+ import { useIsAuctionEnded } from "../hooks/useIsAuctionEnded";
16
17
  import { AUCTION_BUYOUT_WINDOW_MINUTES } from "../../../_internal/shared/checkout/lanes";
17
18
  import { BID_PRESET_MULTIPLIERS, isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
18
19
  import { normalizeError } from "../../../errors/normalize";
@@ -36,7 +37,20 @@ const __P = {
36
37
  /** Feature-specific last resort. Authored copy, never derived from an error. */
37
38
  const BID_FALLBACK = "Failed to place bid. Please try again.";
38
39
  const BUY_NOW_FALLBACK = "Buy Now failed. Please try again.";
39
- export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
40
+ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded: ssrIsEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
41
+ /*
42
+ * 🛑 Derived here, not trusted from the server.
43
+ *
44
+ * `isEnded` arrives baked into cached HTML. On a page cached while the auction
45
+ * was live it says "still open" indefinitely, so every gate below it — the
46
+ * amount field, the presets, the submit button, Buy Now — stayed enabled on a
47
+ * closed lot. The server prop is the INITIAL value only; `auctionEndDate` was
48
+ * already being passed in and used for display, and it is the real answer.
49
+ *
50
+ * The bid itself was never at risk: `placeBid` re-reads the product and
51
+ * rejects on an expired `auctionEndDate`. This fixes the affordance.
52
+ */
53
+ const isEnded = useIsAuctionEnded(auctionEndDate, ssrIsEnded);
40
54
  const router = useRouter();
41
55
  // Declared with the other hooks, never inside the early return below — a
42
56
  // conditional hook is a render-order violation, and this component early-
@@ -277,5 +291,9 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
277
291
  export function PlaceBidModalButton(props) {
278
292
  const { triggerLabel = "Place a bid", triggerClassName = "", ...formProps } = props;
279
293
  const [open, setOpen] = useState(false);
280
- return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: triggerClassName, disabled: props.isEnded, onClick: () => setOpen(true), children: props.isEnded ? "Auction Ended" : triggerLabel }), _jsx(Modal, { isOpen: open, onClose: () => setOpen(false), size: "md", title: "Place your bid", children: _jsx(PlaceBidFormClient, { ...formProps }) })] }));
294
+ // Same reason as inside the form: this trigger is THE affordance a buyer sees
295
+ // on a cached auction page, so it must decide for itself rather than trust a
296
+ // boolean that was true whenever the HTML happened to be generated.
297
+ const isEnded = useIsAuctionEnded(props.auctionEndDate, props.isEnded);
298
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: triggerClassName, disabled: isEnded, onClick: () => setOpen(true), children: isEnded ? "Auction Ended" : triggerLabel }), _jsx(Modal, { isOpen: open, onClose: () => setOpen(false), size: "md", title: "Place your bid", children: _jsx(PlaceBidFormClient, { ...formProps }) })] }));
281
299
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Has this auction ended, decided on the CLIENT from its end timestamp.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * `AuctionDetailPageView` computed `const isEnded = endDate < new Date()` on the
7
+ * SERVER and froze the answer into eleven places — the status badge, the Buy-Now
8
+ * chip, the SSE `enabled` flag, the Place-Bid button, the mobile bid card and
9
+ * the bottom action bar. That is correct only for as long as the rendered HTML
10
+ * is fresh.
11
+ *
12
+ * The page is cached. At `revalidate = 30` the window was small enough to hide
13
+ * the problem; it is not small enough to be *correct*, and raising the TTL (so
14
+ * that 95 auction pages stop regenerating up to 2,880 times a day each, which is
15
+ * what produced 3.1M ISR writes) makes it plainly visible: a page cached while
16
+ * the auction was live renders a green **Active** badge and an enabled "Place a
17
+ * bid" button on a lot that closed hours ago.
18
+ *
19
+ * The timestamp needed to know better was already being passed down — it was
20
+ * used for display only.
21
+ *
22
+ * ## What this does NOT change
23
+ *
24
+ * Server-side enforcement is independent and already correct: `placeBid` and
25
+ * `buyNowAuction` both re-read the product and reject on `endDate < now`, and
26
+ * `assertAuctionActive()` is a third copy. A stale page could never actually
27
+ * place a bid — the defect was always a misleading affordance, never a bad
28
+ * write. That is what makes this safe to adopt incrementally: every component
29
+ * converted is an improvement, and an unconverted one is no worse than today.
30
+ *
31
+ * ## Behaviour
32
+ *
33
+ * - Starts from `initial` (the server's answer) so the first paint is unchanged
34
+ * and hydration does not mismatch.
35
+ * - Re-checks on mount, which is where a cached page corrects itself.
36
+ * - Schedules ONE timer for the exact moment of expiry rather than ticking every
37
+ * second — a bid button does not need per-second resolution, and an interval
38
+ * on every auction card would be a needless wakeup. Long waits are clamped to
39
+ * the setTimeout 32-bit ceiling and re-armed.
40
+ */
41
+ export declare function useIsAuctionEnded(auctionEndDate: Date | string | null | undefined, initial?: boolean): boolean;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ /**
4
+ * Has this auction ended, decided on the CLIENT from its end timestamp.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * `AuctionDetailPageView` computed `const isEnded = endDate < new Date()` on the
9
+ * SERVER and froze the answer into eleven places — the status badge, the Buy-Now
10
+ * chip, the SSE `enabled` flag, the Place-Bid button, the mobile bid card and
11
+ * the bottom action bar. That is correct only for as long as the rendered HTML
12
+ * is fresh.
13
+ *
14
+ * The page is cached. At `revalidate = 30` the window was small enough to hide
15
+ * the problem; it is not small enough to be *correct*, and raising the TTL (so
16
+ * that 95 auction pages stop regenerating up to 2,880 times a day each, which is
17
+ * what produced 3.1M ISR writes) makes it plainly visible: a page cached while
18
+ * the auction was live renders a green **Active** badge and an enabled "Place a
19
+ * bid" button on a lot that closed hours ago.
20
+ *
21
+ * The timestamp needed to know better was already being passed down — it was
22
+ * used for display only.
23
+ *
24
+ * ## What this does NOT change
25
+ *
26
+ * Server-side enforcement is independent and already correct: `placeBid` and
27
+ * `buyNowAuction` both re-read the product and reject on `endDate < now`, and
28
+ * `assertAuctionActive()` is a third copy. A stale page could never actually
29
+ * place a bid — the defect was always a misleading affordance, never a bad
30
+ * write. That is what makes this safe to adopt incrementally: every component
31
+ * converted is an improvement, and an unconverted one is no worse than today.
32
+ *
33
+ * ## Behaviour
34
+ *
35
+ * - Starts from `initial` (the server's answer) so the first paint is unchanged
36
+ * and hydration does not mismatch.
37
+ * - Re-checks on mount, which is where a cached page corrects itself.
38
+ * - Schedules ONE timer for the exact moment of expiry rather than ticking every
39
+ * second — a bid button does not need per-second resolution, and an interval
40
+ * on every auction card would be a needless wakeup. Long waits are clamped to
41
+ * the setTimeout 32-bit ceiling and re-armed.
42
+ */
43
+ export function useIsAuctionEnded(auctionEndDate, initial = false) {
44
+ const endMs = toMs(auctionEndDate);
45
+ const [ended, setEnded] = useState(initial);
46
+ useEffect(() => {
47
+ if (endMs === null) {
48
+ // No end date — the server's answer is the only one available.
49
+ setEnded(initial);
50
+ return;
51
+ }
52
+ let timer;
53
+ const evaluate = () => {
54
+ const remaining = endMs - Date.now();
55
+ if (remaining <= 0) {
56
+ setEnded(true);
57
+ return;
58
+ }
59
+ setEnded(false);
60
+ // setTimeout overflows past ~24.8 days and fires immediately; re-arm.
61
+ timer = setTimeout(evaluate, Math.min(remaining, 2147483000));
62
+ };
63
+ evaluate();
64
+ return () => {
65
+ if (timer)
66
+ clearTimeout(timer);
67
+ };
68
+ }, [endMs, initial]);
69
+ return ended;
70
+ }
71
+ function toMs(value) {
72
+ if (!value)
73
+ return null;
74
+ const ms = value instanceof Date ? value.getTime() : new Date(value).getTime();
75
+ return Number.isFinite(ms) ? ms : null;
76
+ }
@@ -69,7 +69,6 @@ export interface UserDocument extends BaseDocument {
69
69
  storeStatus?: "pending" | "approved" | "rejected";
70
70
  isTester?: boolean;
71
71
  canTestAdmin?: boolean;
72
- isBot?: boolean;
73
72
  publicProfile?: {
74
73
  isPublic: boolean;
75
74
  showEmail: boolean;
@@ -72,7 +72,22 @@ export async function GET(request) {
72
72
  posts: result.items,
73
73
  meta,
74
74
  };
75
- return NextResponse.json({ success: true, data: body });
75
+ const response = NextResponse.json({ success: true, data: body });
76
+ /*
77
+ * The only public listing API that set NO Cache-Control — `/api/products`,
78
+ * `/api/stores`, `/api/events` and `/api/reviews` all do, and CLAUDE.md
79
+ * Rule #6.5 requires it of every public GET (20 of 378 routes complied).
80
+ *
81
+ * It mattered little while anonymous browsing never reached this route (the
82
+ * dynamic `/blog` page rendered server-side and seeded the client), but it
83
+ * is on the critical path the moment a filtered view misses the page cache.
84
+ * Window matched to its siblings deliberately — a blog post is no more
85
+ * volatile than a product.
86
+ *
87
+ * Errors below stay uncached: a 500 must not be pinned at the edge.
88
+ */
89
+ response.headers.set("Cache-Control", "public, max-age=60, s-maxage=120, stale-while-revalidate=60");
90
+ return response;
76
91
  }
77
92
  catch (error) {
78
93
  void normalizeError(error);
@@ -16,9 +16,9 @@ export interface BlogPostDocument extends BaseDocument {
16
16
  title: string;
17
17
  slug: string;
18
18
  excerpt: string;
19
- /** Tester sandbox flag — set on the shared admin-seeded test post; swept by testerSandboxCleanup. */
19
+ /** Test-data flag — set on the shared admin-seeded test post; swept by testDataCleanup. */
20
20
  isTestData?: boolean;
21
- /** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
21
+ /** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
22
22
  testDataExpiresAt?: Date;
23
23
  content: string;
24
24
  coverImage?: MediaFieldInput;