@mohasinac/appkit 4.14.0 → 4.15.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 (157) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +2 -2
  2. package/dist/_internal/server/features/cart/service.d.ts +3 -3
  3. package/dist/_internal/server/features/checkout/actions.js +19 -0
  4. package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
  5. package/dist/_internal/server/features/checkout/source-context.js +157 -0
  6. package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
  7. package/dist/_internal/server/features/offers/adapters.js +88 -0
  8. package/dist/_internal/server/features/offers/index.d.ts +2 -0
  9. package/dist/_internal/server/features/offers/index.js +1 -0
  10. package/dist/_internal/server/features/orders/adapters.js +24 -0
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
  12. package/dist/_internal/shared/actions/action-registry.js +3 -1
  13. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  14. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  15. package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
  16. package/dist/_internal/shared/features/cart/schema.js +15 -0
  17. package/dist/_internal/shared/features/products/schema.d.ts +2 -2
  18. package/dist/_internal/shared/history/history.d.ts +22 -0
  19. package/dist/_internal/shared/history/history.js +41 -1
  20. package/dist/client.d.ts +13 -2
  21. package/dist/client.js +20 -2
  22. package/dist/constants/field-names.d.ts +8 -6
  23. package/dist/constants/field-names.js +48 -6
  24. package/dist/features/account/components/UserBidsView.js +12 -4
  25. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  26. package/dist/features/account/components/UserReturnsView.js +17 -1
  27. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  28. package/dist/features/admin/components/AdminBidsView.js +15 -2
  29. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  30. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  31. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  32. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  33. package/dist/features/admin/components/AdminOffersView.js +62 -18
  34. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  35. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  36. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  37. package/dist/features/admin/components/AdminScammersView.js +3 -8
  38. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  39. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  40. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  41. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  42. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  43. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  44. package/dist/features/admin/constants/filter-tabs.js +1 -0
  45. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  46. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  47. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  48. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  49. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  50. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  51. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  52. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  53. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  54. package/dist/features/audit-log/schemas/firestore.js +7 -0
  55. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  56. package/dist/features/blog/schemas/blog-form.js +100 -0
  57. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  58. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  59. package/dist/features/events/schemas/event-form.d.ts +113 -0
  60. package/dist/features/events/schemas/event-form.js +82 -0
  61. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  62. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  63. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  64. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  65. package/dist/features/homepage/schemas/index.d.ts +31 -0
  66. package/dist/features/homepage/schemas/index.js +24 -0
  67. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  68. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  69. package/dist/features/orders/repository/orders.repository.js +5 -0
  70. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  71. package/dist/features/orders/types/index.d.ts +35 -7
  72. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  73. package/dist/features/products/schemas/index.d.ts +2 -2
  74. package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
  75. package/dist/features/products/schemas/product-features.validators.js +63 -0
  76. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  77. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  78. package/dist/features/seller/actions/offer-actions.js +32 -2
  79. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  80. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  81. package/dist/features/seller/components/SellerBidsView.js +11 -4
  82. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  83. package/dist/features/seller/components/SellerOffersView.js +2 -1
  84. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  85. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  86. package/dist/features/seller/repository/offer.repository.js +127 -23
  87. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  88. package/dist/features/seller/schemas/coupon-form.js +116 -0
  89. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  90. package/dist/features/seller/schemas/firestore.js +27 -0
  91. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  92. package/dist/features/seller/schemas/offer-forms.js +21 -0
  93. package/dist/features/shell/build-sections.d.ts +63 -0
  94. package/dist/features/shell/build-sections.js +207 -0
  95. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  96. package/dist/features/shell/field-ui-meta.js +324 -0
  97. package/dist/features/shell/index.d.ts +4 -0
  98. package/dist/features/shell/index.js +2 -0
  99. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  100. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  101. package/dist/features/status-history/index.d.ts +11 -0
  102. package/dist/features/status-history/index.js +10 -0
  103. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  104. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  105. package/dist/features/store-extensions/index.d.ts +12 -0
  106. package/dist/features/store-extensions/index.js +12 -0
  107. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  108. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  109. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  110. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  111. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  112. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  113. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  114. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  115. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  116. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  117. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  118. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  119. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  120. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  121. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  122. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  123. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  124. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  125. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  126. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  127. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  128. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  129. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  130. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  131. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  132. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  133. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
  134. package/dist/index.d.ts +12 -2
  135. package/dist/index.js +12 -2
  136. package/dist/next/routing/route-map.d.ts +16 -2
  137. package/dist/next/routing/route-map.js +9 -2
  138. package/dist/schemas/firestore-value.d.ts +8 -0
  139. package/dist/schemas/firestore-value.js +44 -0
  140. package/dist/schemas/index.d.ts +1 -1
  141. package/dist/schemas/index.js +1 -1
  142. package/dist/schemas/registry.d.ts +634 -1
  143. package/dist/schemas/registry.js +64 -1
  144. package/dist/seed/bids-seed-data.js +30 -0
  145. package/dist/seed/offers-seed-data.js +212 -7
  146. package/dist/seed/orders-seed-data.js +336 -1
  147. package/dist/seed/products-auctions-seed-data.js +41 -0
  148. package/dist/server.d.ts +2 -0
  149. package/dist/server.js +5 -0
  150. package/dist/styles.css +1 -1
  151. package/dist/tailwind-utilities.css +1 -1
  152. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  153. package/dist/ui/components/RecordDetailModal.js +2 -2
  154. package/package.json +2 -1
  155. package/scripts/node-esm-loader.mjs +65 -0
  156. package/scripts/seed-cli-loader.mjs +6 -49
  157. package/scripts/seed-cli.mjs +1 -1
@@ -12,12 +12,7 @@ import { ADMIN_SCAMMER_STATUS_TABS } from "../constants/filter-tabs";
12
12
  import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
13
13
  import { DataListingView } from "./DataListingView";
14
14
  import { AdminScammerEditorView } from "./AdminScammerEditorView";
15
- const STATUS_BADGE = {
16
- pending_review: "bg-warning-surface text-warning",
17
- verified: "bg-success-surface text-success",
18
- rejected: "bg-error-surface text-error",
19
- removed: "bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
20
- };
15
+ import { SCAMMER_STATUS_BADGE, SCAMMER_STATUS_LABEL, toScammerStatus, } from "../../scams/constants/scammer-status-display";
21
16
  const SCAMMER_COLUMNS = [
22
17
  {
23
18
  key: "primary",
@@ -28,7 +23,7 @@ const SCAMMER_COLUMNS = [
28
23
  key: "status",
29
24
  header: "Status",
30
25
  className: "w-36",
31
- render: (row) => (_jsx(Span, { className: `inline-flex ${STATUS_BADGE[row.status] ?? STATUS_BADGE.pending_review}`, size: "xs", weight: "medium", rounded: "full", padding: "pill-sm-tall", children: row.status.replace(/_/g, " ") })),
26
+ render: (row) => (_jsx(Span, { className: `inline-flex ${SCAMMER_STATUS_BADGE[row.status]}`, size: "xs", weight: "medium", rounded: "full", padding: "pill-sm-tall", children: SCAMMER_STATUS_LABEL[row.status] })),
32
27
  },
33
28
  {
34
29
  key: "updatedAt",
@@ -72,7 +67,7 @@ export function AdminScammersView({ children, ...props }) {
72
67
  ]
73
68
  .filter(Boolean)
74
69
  .join(" · "),
75
- status: toStringValue(item.status, "pending_review"),
70
+ status: toScammerStatus(item.status),
76
71
  updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
77
72
  _raw: item,
78
73
  })),
@@ -7,6 +7,8 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
7
7
  import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
8
8
  import { CategoryInlineSelect } from "../../seller/components/CategoryInlineSelect";
9
9
  import { Button, Checkbox, Div, Form, FormActions, Input, Modal, Row, Select, Stack, Text, Textarea, useToast } from "../../../ui";
10
+ import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
11
+ import { homepageSectionFormSchema } from "../schemas/homepage-section-form";
10
12
  import { apiClient } from "../../../http";
11
13
  import { ADMIN_ENDPOINTS } from "../../../constants";
12
14
  import { ROUTES } from "../../../next/routing/route-map";
@@ -954,27 +956,34 @@ export function AdminSectionsView({ children }) {
954
956
  }));
955
957
  const hasReorderChanges = reorderDraft.some((item) => sectionOrderMap.get(item.id) !== item.order);
956
958
  const canUndoReorderChanges = reorderUndoStack.length > 0;
957
- return (_jsxs(_Fragment, { children: [_jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsx(Text, { size: "base", weight: "semibold", color: "primary", children: "Homepage Sections" }), _jsx(Text, { size: "sm", color: "muted", children: "Manage homepage sections and their display order" })] }), _jsx(Row, { align: "center", gap: "sm", children: _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setIsModalOpen(true), children: "Manage Sections" }) })] }), 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: rows, isLoading: isLoading, emptyLabel: "No sections found" })] }), _jsxs(Stack, { className: `mt-4 ${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsxs(Row, { align: "center", justify: "between", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Reorder Sections" }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: reindexDraft, disabled: reorderSections.isPending || reorderDraft.length === 0, children: "Reindex 1..N" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: undoReorderChange, disabled: reorderSections.isPending || !canUndoReorderChanges, children: "Undo unsaved" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: resetToServerOrder, disabled: reorderSections.isPending || !hasReorderChanges, children: "Reset to server" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => reorderSections.mutate(), disabled: !hasReorderChanges || reorderSections.isPending, children: reorderSections.isPending ? "Saving order..." : "Save order" })] })] }), reorderDraft.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No sections to reorder." })) : (_jsx(Stack, { gap: "sm", children: reorderDraft.map((item, index) => (_jsxs(Div, { layout: "grid", gap: "2", align: "center", className: "grid-cols-[auto_1fr_auto_auto_auto]", rounded: "md", padding: "xs", border: "default", draggable: true, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => handleReorderDrop(index), onDragEnd: () => setDragIndex(null), children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "\u2261" }), _jsxs(Text, { size: "sm", color: "primary", children: [item.type, " #", item.order] }), _jsx(Input, { type: "number", min: 1, max: reorderDraft.length, value: String(item.order), onChange: (event) => updateReorderItemOrder(index, Number(event.target.value) || 1), className: "w-24" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, -1), disabled: index === 0, children: "Up" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, 1), disabled: index === reorderDraft.length - 1, children: "Down" })] }, `reorder-${item.id}`))) }))] }), _jsx(Modal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), title: "Manage Homepage Section", size: "lg", children: _jsxs(Form, { onSubmit: (event) => {
958
- event.preventDefault();
959
- saveSection.mutate();
960
- }, spacing: "md", children: [_jsx(Select, { label: "Mode", value: mode, onValueChange: (value) => setMode(value), options: [
961
- { label: "Create new section", value: "create" },
962
- { label: "Edit existing section", value: "edit" },
963
- ] }), mode === "edit" ? (_jsx(Select, { label: "Section", value: selectedSectionId, onValueChange: setSelectedSectionId, placeholder: "Select section", options: sections.map((section) => ({
964
- label: `${section.type} (#${section.order})`,
965
- value: section.id,
966
- })) })) : null, _jsx(Select, { label: "Section type", value: sectionType, onValueChange: (value) => {
967
- setSectionType(value);
968
- if (mode === "create") {
969
- setSelectedSectionId("");
970
- }
971
- }, options: SECTION_TYPE_OPTIONS.map((type) => ({
972
- label: type,
973
- value: type,
974
- })), disabled: mode === "edit" }), _jsx(Input, { label: "Order", type: "number", value: order, onChange: (event) => setOrder(event.target.value), placeholder: "Leave empty to auto-place" }), _jsx(Select, { label: "Enabled", value: enabled ? "true" : "false", onValueChange: (value) => setEnabled(value === "true"), options: [
975
- { label: "Enabled", value: "true" },
976
- { label: "Disabled", value: "false" },
977
- ] }), isTypedBuilder ? renderTypedBuilder() : null, _jsx(Textarea, { label: isTypedBuilder ? "Generated config (JSON preview)" : "Section config (JSON)", value: configJson, onChange: (event) => setConfigJson(event.target.value), rows: 10, readOnly: isTypedBuilder, helperText: isTypedBuilder
978
- ? "This JSON is generated from typed controls above."
979
- : "Provide section config JSON for this section type." }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setIsModalOpen(false), children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: saveSection.isPending, children: saveSection.isPending ? "Saving..." : mode === "create" ? "Create section" : "Update section" })] })] }) })] }));
959
+ return (_jsxs(_Fragment, { children: [_jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsx(Text, { size: "base", weight: "semibold", color: "primary", children: "Homepage Sections" }), _jsx(Text, { size: "sm", color: "muted", children: "Manage homepage sections and their display order" })] }), _jsx(Row, { align: "center", gap: "sm", children: _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setIsModalOpen(true), children: "Manage Sections" }) })] }), 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: rows, isLoading: isLoading, emptyLabel: "No sections found" })] }), _jsxs(Stack, { className: `mt-4 ${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsxs(Row, { align: "center", justify: "between", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Reorder Sections" }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: reindexDraft, disabled: reorderSections.isPending || reorderDraft.length === 0, children: "Reindex 1..N" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: undoReorderChange, disabled: reorderSections.isPending || !canUndoReorderChanges, children: "Undo unsaved" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: resetToServerOrder, disabled: reorderSections.isPending || !hasReorderChanges, children: "Reset to server" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => reorderSections.mutate(), disabled: !hasReorderChanges || reorderSections.isPending, children: reorderSections.isPending ? "Saving order..." : "Save order" })] })] }), reorderDraft.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No sections to reorder." })) : (_jsx(Stack, { gap: "sm", children: reorderDraft.map((item, index) => (_jsxs(Div, { layout: "grid", gap: "2", align: "center", className: "grid-cols-[auto_1fr_auto_auto_auto]", rounded: "md", padding: "xs", border: "default", draggable: true, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => handleReorderDrop(index), onDragEnd: () => setDragIndex(null), children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "\u2261" }), _jsxs(Text, { size: "sm", color: "primary", children: [item.type, " #", item.order] }), _jsx(Input, { type: "number", min: 1, max: reorderDraft.length, value: String(item.order), onChange: (event) => updateReorderItemOrder(index, Number(event.target.value) || 1), className: "w-24" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, -1), disabled: index === 0, children: "Up" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, 1), disabled: index === reorderDraft.length - 1, children: "Down" })] }, `reorder-${item.id}`))) }))] }), _jsx(Modal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), title: "Manage Homepage Section", size: "lg", children: _jsx(Form, { schema: homepageSectionFormSchema, onSubmit: (event) => event.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(Select, { label: "Mode", value: mode, onValueChange: (value) => setMode(value), options: [
960
+ { label: "Create new section", value: "create" },
961
+ { label: "Edit existing section", value: "edit" },
962
+ ] }), mode === "edit" ? (_jsx(Select, { label: "Section", value: selectedSectionId, onValueChange: setSelectedSectionId, placeholder: "Select section", options: sections.map((section) => ({
963
+ label: `${section.type} (#${section.order})`,
964
+ value: section.id,
965
+ })) })) : null, _jsx(Select, { label: "Section type", value: sectionType, onValueChange: (value) => {
966
+ setSectionType(value);
967
+ if (mode === "create") {
968
+ setSelectedSectionId("");
969
+ }
970
+ }, options: SECTION_TYPE_OPTIONS.map((type) => ({
971
+ label: type,
972
+ value: type,
973
+ })), disabled: mode === "edit" }), _jsx(Input, { label: "Order", type: "number", value: order, onChange: (event) => setOrder(event.target.value), placeholder: "Leave empty to auto-place" }), _jsx(Select, { label: "Enabled", value: enabled ? "true" : "false", onValueChange: (value) => setEnabled(value === "true"), options: [
974
+ { label: "Enabled", value: "true" },
975
+ { label: "Disabled", value: "false" },
976
+ ] }), isTypedBuilder ? renderTypedBuilder() : null, _jsx(Textarea, { label: isTypedBuilder ? "Generated config (JSON preview)" : "Section config (JSON)", value: configJson, onChange: (event) => setConfigJson(event.target.value), rows: 10, readOnly: isTypedBuilder, helperText: isTypedBuilder
977
+ ? "This JSON is generated from typed controls above."
978
+ : "Provide section config JSON for this section type." }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setIsModalOpen(false), children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: saveSection.isPending, onClick: () => {
979
+ clearErrors();
980
+ const parsed = homepageSectionFormSchema.safeParse({
981
+ sectionType, order, enabled, configJson,
982
+ });
983
+ if (!parsed.success) {
984
+ applyZodIssues(parsed.error.issues, setFieldError);
985
+ return;
986
+ }
987
+ saveSection.mutate();
988
+ }, children: saveSection.isPending ? "Saving..." : mode === "create" ? "Create section" : "Update section" })] })] })) }) })] }));
980
989
  }
@@ -5,7 +5,7 @@ import { SIEVE_OP, sieveFilter } from "@mohasinac/appkit/client";
5
5
  import { sortBy } from "@mohasinac/appkit/client";
6
6
  import React, { useState } from "react";
7
7
  import { useQueryClient } from "@tanstack/react-query";
8
- import { ConfirmDeleteModal, FilterChipGroup, ListingLayout, RowActionMenu, useToast } from "../../../ui";
8
+ import { ConfirmDeleteModal, FilterChipGroup, ListingLayout, RowActionMenu, useToast, RecordDetailModal, } from "../../../ui";
9
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
10
  import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
11
11
  import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
@@ -20,6 +20,7 @@ function maskIp(ip) {
20
20
  return ip.replace(/:\w+$/, ":*");
21
21
  }
22
22
  export function AdminSessionsView({ children, ...props }) {
23
+ const [detail, setDetail] = useState(null);
23
24
  const queryClient = useQueryClient();
24
25
  const { showToast } = useToast();
25
26
  const [revokeTarget, setRevokeTarget] = useState(null);
@@ -53,6 +54,9 @@ export function AdminSessionsView({ children, ...props }) {
53
54
  { value: "lastActivity", label: "Least recent" },
54
55
  { value: sortBy("createdAt", "DESC"), label: "Newest" },
55
56
  ],
57
+ // Opening a row is what makes the destructive actions beside it
58
+ // safe to offer at all.
59
+ onRowClick: (row) => setDetail(row),
56
60
  mapRows: (response) => toRecordArray(response.sessions).map((item, index) => {
57
61
  const deviceInfo = (item.deviceInfo ?? {});
58
62
  const userLabel = toStringValue(item.user &&
@@ -60,6 +64,7 @@ export function AdminSessionsView({ children, ...props }) {
60
64
  const ipMasked = maskIp(toStringValue(deviceInfo.ip, ""));
61
65
  return {
62
66
  id: toStringValue(item.id, `session-${index}`),
67
+ detail: item,
63
68
  primary: userLabel,
64
69
  secondary: [
65
70
  toStringValue(deviceInfo.browser, "Unknown browser"),
@@ -94,7 +99,17 @@ export function AdminSessionsView({ children, ...props }) {
94
99
  { id: "true", label: "Active only" },
95
100
  ], value: pendingFilters.isActive || "", onChange: (v) => setPendingFilters((p) => ({ ...p, isActive: v })) })),
96
101
  };
97
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(ConfirmDeleteModal, { isOpen: Boolean(revokeTarget), onClose: () => setRevokeTarget(null), onConfirm: () => {
102
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: toStringValue(detail?.detail?.userId, "Session"), fields: (() => {
103
+ const d = (detail?.detail ?? {});
104
+ return detail ? [
105
+ { label: "User", value: toStringValue(d.userId, "—") },
106
+ { label: "Active", value: d.isActive ? "Yes" : "No" },
107
+ { label: "Last activity", value: toRelativeDate(d.lastActivity) },
108
+ { label: "Expires", value: toRelativeDate(d.expiresAt) },
109
+ { label: "Started", value: toRelativeDate(d.createdAt) },
110
+ { label: "Session ID", value: detail?.id ?? "—" },
111
+ ] : undefined;
112
+ })() }), _jsx(ConfirmDeleteModal, { isOpen: Boolean(revokeTarget), onClose: () => setRevokeTarget(null), onConfirm: () => {
98
113
  if (revokeTarget)
99
114
  revokeMutation.mutate(revokeTarget.id);
100
115
  }, isDeleting: revokeMutation.isPending, confirmText: "Revoke", title: "Revoke session?", message: "This will sign the user out of this device immediately.", variant: "danger" })] }));
@@ -12,6 +12,7 @@ const ACTION_LABELS = {
12
12
  checkout_bypass: "Admin checkout bypass",
13
13
  coupon_update: "Coupon updated",
14
14
  payout_mark_paid: "Payout marked paid",
15
+ offer_cancel: "Offer cancelled",
15
16
  store_status_change: "Store status changed",
16
17
  user_role_change: "User role changed",
17
18
  };
@@ -1,6 +1,8 @@
1
1
  import type { FirestoreDocument } from "@mohasinac/appkit";
2
- export declare const SECTION_TYPE_OPTIONS: readonly ["welcome", "stats", "trust-indicators", "categories", "brands", "products", "pre-orders", "auctions", "banner", "features", "reviews", "whatsapp-community", "faq", "blog-articles", "newsletter", "stores", "events", "social-feed", "carousel", "custom-cards", "google-reviews", "featured-bundles", "prize-draws", "event-raffles", "collection-cards"];
3
- export type SectionType = (typeof SECTION_TYPE_OPTIONS)[number];
2
+ import type { SectionType as CanonicalSectionType } from "../../../homepage/schemas/firestore";
3
+ export declare const SECTION_TYPE_OPTIONS: CanonicalSectionType[];
4
+ /** Re-exported, NOT re-declared. There is one `SectionType` in this codebase. */
5
+ export type SectionType = CanonicalSectionType;
4
6
  export interface SectionPatchPayload {
5
7
  enabled: boolean;
6
8
  order?: number;
@@ -1,32 +1,50 @@
1
1
  import { ROUTES } from "../../../../next/routing/route-map";
2
2
  const LABEL_VERIFIED_SELLERS = "Verified Sellers";
3
- export const SECTION_TYPE_OPTIONS = [
4
- "welcome",
5
- "stats",
6
- "trust-indicators",
7
- "categories",
8
- "brands",
9
- "products",
10
- "pre-orders",
11
- "auctions",
12
- "banner",
13
- "features",
14
- "reviews",
15
- "whatsapp-community",
16
- "faq",
17
- "blog-articles",
18
- "newsletter",
19
- "stores",
20
- "events",
21
- "social-feed",
22
- "carousel",
23
- "custom-cards",
24
- "google-reviews",
25
- "featured-bundles",
26
- "prize-draws",
27
- "event-raffles",
28
- "collection-cards",
29
- ];
3
+ /*
4
+ * The admin dropdown's section types.
5
+ *
6
+ * 🛑 This used to be a bare `as const` array with its OWN `SectionType` derived
7
+ * from it — a second declaration shadowing the real `SectionType` in
8
+ * `features/homepage/schemas/firestore.ts`, which is how the two came to
9
+ * disagree with the create route (Recurrent Root Cause #36's shape, one union
10
+ * over). The route's own copy was missing `featured-bundles`, `prize-draws`,
11
+ * `event-raffles` and `collection-cards`, so choosing any of those four in this
12
+ * dropdown produced a 400 on save — four of twenty-five section types could be
13
+ * configured in full and never created.
14
+ *
15
+ * It is now a `Record<CanonicalSectionType, true>`, so a type added to the
16
+ * union and not offered here is a COMPILE error, and an entry here that is not
17
+ * a real section type is too. Declaration order is preserved because the array
18
+ * is derived from this map's keys, and that order is the dropdown's order.
19
+ */
20
+ const SECTION_TYPE_PRESENCE = {
21
+ welcome: true,
22
+ stats: true,
23
+ "trust-indicators": true,
24
+ categories: true,
25
+ brands: true,
26
+ products: true,
27
+ "pre-orders": true,
28
+ auctions: true,
29
+ banner: true,
30
+ features: true,
31
+ reviews: true,
32
+ "whatsapp-community": true,
33
+ faq: true,
34
+ "blog-articles": true,
35
+ newsletter: true,
36
+ stores: true,
37
+ events: true,
38
+ "social-feed": true,
39
+ carousel: true,
40
+ "custom-cards": true,
41
+ "google-reviews": true,
42
+ "featured-bundles": true,
43
+ "prize-draws": true,
44
+ "event-raffles": true,
45
+ "collection-cards": true,
46
+ };
47
+ export const SECTION_TYPE_OPTIONS = Object.keys(SECTION_TYPE_PRESENCE);
30
48
  export const DEFAULT_PRODUCTS_BUILDER = {
31
49
  title: "Featured Products",
32
50
  subtitle: "",
@@ -188,6 +188,9 @@ export declare const ADMIN_AUDIT_LOG_ACTION_TABS: readonly [{
188
188
  }, {
189
189
  readonly id: "payout_mark_paid";
190
190
  readonly label: "Payout paid";
191
+ }, {
192
+ readonly id: "offer_cancel";
193
+ readonly label: "Offer cancelled";
191
194
  }, {
192
195
  readonly id: "store_status_change";
193
196
  readonly label: "Store status";
@@ -105,6 +105,7 @@ export const ADMIN_AUDIT_LOG_ACTION_TABS = [
105
105
  { id: "checkout_bypass", label: "Checkout bypass" },
106
106
  { id: "coupon_update", label: "Coupon update" },
107
107
  { id: "payout_mark_paid", label: "Payout paid" },
108
+ { id: "offer_cancel", label: "Offer cancelled" },
108
109
  { id: "store_status_change", label: "Store status" },
109
110
  { id: "user_role_change", label: "Role change" },
110
111
  ];
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
4
+ * by schema instance and every zod wrapper returns a new one.
5
+ */
6
+ export declare const homepageSectionFormSchema: z.ZodObject<{
7
+ sectionType: z.ZodString;
8
+ order: z.ZodOptional<z.ZodNumber>;
9
+ enabled: z.ZodBoolean;
10
+ configJson: z.ZodEffects<z.ZodString, string, string>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ enabled: boolean;
13
+ sectionType: string;
14
+ configJson: string;
15
+ order?: number | undefined;
16
+ }, {
17
+ enabled: boolean;
18
+ sectionType: string;
19
+ configJson: string;
20
+ order?: number | undefined;
21
+ }>;
22
+ export type HomepageSectionFormValues = z.infer<typeof homepageSectionFormSchema>;
@@ -0,0 +1,68 @@
1
+ /*
2
+ * WHY: `AdminSectionsView` rendered a `<Form>` with no `schema` prop. The route
3
+ * validated `order` / `enabled` / `config`, so nothing invalid was ever
4
+ * stored — but a bad value came back as a 400 banner after a round-trip
5
+ * rather than as an error on the field that caused it.
6
+ * WHAT: The homepage-section form schema.
7
+ *
8
+ * ## `config` stays open, deliberately
9
+ *
10
+ * Its shape differs per section type across 21 types, and the route models it
11
+ * the same way (`z.object({}).passthrough()`). Naming a fixed shape here would
12
+ * strip whatever the current type actually needs — the exact defect that made
13
+ * `productBaseSchema` eat every per-type listing field. The form parses it as
14
+ * JSON and reports malformed input; the per-type meaning stays with the
15
+ * section renderer that owns it.
16
+ *
17
+ * EXPORTS:
18
+ * homepageSectionFormSchema, type HomepageSectionFormValues
19
+ *
20
+ * @tag domain:admin,homepage
21
+ * @tag layer:schema
22
+ * @tag pattern:none
23
+ * @tag access:isomorphic
24
+ * @tag consumers:AdminSectionsView
25
+ * @tag sideEffects:none
26
+ */
27
+ import { z } from "zod";
28
+ import { annotate } from "../../shell/field-ui-meta";
29
+ import { normalizeError } from "../../../errors/normalize";
30
+ /**
31
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
32
+ * by schema instance and every zod wrapper returns a new one.
33
+ */
34
+ export const homepageSectionFormSchema = z.object({
35
+ sectionType: annotate(z.string().min(1, "Choose a section type."), {
36
+ section: "basics", sectionLabel: "Section", sectionRequired: true,
37
+ quick: true, order: 1, row: "pair",
38
+ }),
39
+ order: annotate(z.coerce
40
+ .number({ invalid_type_error: "Order must be a number." })
41
+ .int("Order must be a whole number.")
42
+ .min(0, "Order cannot be negative.")
43
+ .optional(), { section: "basics", order: 2, row: "pair" }),
44
+ enabled: annotate(z.boolean(), {
45
+ section: "basics", order: 3, row: "quarter",
46
+ }),
47
+ configJson: annotate(z.string().refine(isParsableJson, { message: "Config must be valid JSON." }), {
48
+ section: "config", sectionLabel: "Configuration",
49
+ order: 1, row: "full", kind: "list",
50
+ }),
51
+ });
52
+ /** Hoisted so the `annotate()` call above stays a simple two-argument call. */
53
+ function isParsableJson(value) {
54
+ if (!value.trim())
55
+ return true;
56
+ try {
57
+ JSON.parse(value);
58
+ return true;
59
+ }
60
+ catch (_err) {
61
+ // Normalized rather than suppressed. The parse failure IS the return value
62
+ // here — `false` answers the only question this asks — but the project
63
+ // forbids adding new suppression markers, and this is the same shape
64
+ // AdminSectionsView already uses for the very same JSON parse.
65
+ void normalizeError(_err);
66
+ return false;
67
+ }
68
+ }
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
4
+ * by schema instance and every zod wrapper returns a new one.
5
+ */
6
+ export declare const navItemFormSchema: z.ZodObject<{
7
+ label: z.ZodString;
8
+ href: z.ZodEffects<z.ZodString, string, string>;
9
+ icon: z.ZodOptional<z.ZodString>;
10
+ order: z.ZodOptional<z.ZodNumber>;
11
+ parentId: z.ZodOptional<z.ZodString>;
12
+ isVisible: z.ZodOptional<z.ZodBoolean>;
13
+ }, "strip", z.ZodTypeAny, {
14
+ label: string;
15
+ href: string;
16
+ icon?: string | undefined;
17
+ order?: number | undefined;
18
+ parentId?: string | undefined;
19
+ isVisible?: boolean | undefined;
20
+ }, {
21
+ label: string;
22
+ href: string;
23
+ icon?: string | undefined;
24
+ order?: number | undefined;
25
+ parentId?: string | undefined;
26
+ isVisible?: boolean | undefined;
27
+ }>;
28
+ export type NavItemFormValues = z.infer<typeof navItemFormSchema>;
29
+ /** Create contract — `label` and `href` are what make an item usable at all. */
30
+ export declare const navItemCreateSchema: z.ZodObject<{
31
+ label: z.ZodString;
32
+ href: z.ZodEffects<z.ZodString, string, string>;
33
+ icon: z.ZodOptional<z.ZodString>;
34
+ order: z.ZodOptional<z.ZodNumber>;
35
+ parentId: z.ZodOptional<z.ZodString>;
36
+ isVisible: z.ZodOptional<z.ZodBoolean>;
37
+ }, "strip", z.ZodTypeAny, {
38
+ label: string;
39
+ href: string;
40
+ icon?: string | undefined;
41
+ order?: number | undefined;
42
+ parentId?: string | undefined;
43
+ isVisible?: boolean | undefined;
44
+ }, {
45
+ label: string;
46
+ href: string;
47
+ icon?: string | undefined;
48
+ order?: number | undefined;
49
+ parentId?: string | undefined;
50
+ isVisible?: boolean | undefined;
51
+ }>;
52
+ /** Update contract — every field optional. */
53
+ export declare const navItemUpdateSchema: z.ZodObject<{
54
+ label: z.ZodOptional<z.ZodString>;
55
+ href: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
56
+ icon: z.ZodOptional<z.ZodOptional<z.ZodString>>;
57
+ order: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
58
+ parentId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
59
+ isVisible: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
60
+ }, "strip", z.ZodTypeAny, {
61
+ label?: string | undefined;
62
+ icon?: string | undefined;
63
+ href?: string | undefined;
64
+ order?: number | undefined;
65
+ parentId?: string | undefined;
66
+ isVisible?: boolean | undefined;
67
+ }, {
68
+ label?: string | undefined;
69
+ icon?: string | undefined;
70
+ href?: string | undefined;
71
+ order?: number | undefined;
72
+ parentId?: string | undefined;
73
+ isVisible?: boolean | undefined;
74
+ }>;
@@ -0,0 +1,72 @@
1
+ /*
2
+ * WHY: `AdminNavEditorView` rendered a `<Form>` with no `schema` prop, so the
3
+ * only validation on a navigation item was whatever the server said after
4
+ * the round-trip — a 400 banner rather than an error on the field that
5
+ * caused it. The route DID validate (`navItemSchema` /
6
+ * `updateNavItemSchema` in `api/admin/navigation`), so nothing invalid
7
+ * was ever stored; the gap was purely that the admin was told badly.
8
+ * WHAT: The nav-item form schema, shared by the editor and the routes so the
9
+ * two cannot disagree about what a valid nav item is.
10
+ *
11
+ * ## `href` is checked against real routes only loosely
12
+ *
13
+ * A nav item pointing at a page that does not exist is a real defect —
14
+ * `audit-nav-page-wiring` blocks it — but that check needs the filesystem and
15
+ * therefore cannot run in a browser. Here the rule is only "shaped like a
16
+ * path"; the audit remains the authority on whether the path resolves.
17
+ *
18
+ * EXPORTS:
19
+ * navItemFormSchema, navItemCreateSchema, navItemUpdateSchema,
20
+ * type NavItemFormValues
21
+ *
22
+ * @tag domain:admin,navigation
23
+ * @tag layer:schema
24
+ * @tag pattern:none
25
+ * @tag access:isomorphic
26
+ * @tag consumers:AdminNavEditorView,/api/admin/navigation
27
+ * @tag sideEffects:none
28
+ */
29
+ import { z } from "zod";
30
+ import { annotate } from "../../shell/field-ui-meta";
31
+ /**
32
+ * A path or an absolute URL.
33
+ *
34
+ * Hoisted for readability only. It was originally inlined, and that surfaced a
35
+ * real bug in `audit-field-ui-meta`: its comment-stripper did not understand
36
+ * regex literals, so the trailing `\/\/` of this pattern read as the start of
37
+ * a line comment and blanked the rest of the line — unbalancing the brace walk
38
+ * and reporting a false NO_SECTION. The audit now handles regex literals; this
39
+ * stays hoisted because it reads better, not because it has to.
40
+ */
41
+ const hrefSchema = z
42
+ .string()
43
+ .min(1, "A destination is required.")
44
+ .refine((v) => v.startsWith("/") || /^https?:\/\//.test(v), {
45
+ message: "Use a path starting with / or a full http(s) URL.",
46
+ });
47
+ /**
48
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
49
+ * by schema instance and every zod wrapper returns a new one.
50
+ */
51
+ export const navItemFormSchema = z.object({
52
+ label: annotate(z.string().min(1, "A label is required.").max(60, "Keep the label under 60 characters."), {
53
+ section: "basics", sectionLabel: "Nav item", sectionRequired: true,
54
+ quick: true, order: 1, row: "pair",
55
+ }),
56
+ href: annotate(hrefSchema, { section: "basics", quick: true, order: 2, row: "pair" }),
57
+ icon: annotate(z.string().max(60).optional(), {
58
+ section: "basics", order: 3, row: "pair",
59
+ }),
60
+ order: annotate(z.coerce.number().int("Order must be a whole number.").min(0, "Order cannot be negative.").optional(), { section: "placement", sectionLabel: "Placement", order: 1, row: "pair" }),
61
+ parentId: annotate(z.string().max(80).optional(), {
62
+ section: "placement", order: 2, row: "pair",
63
+ help: "Leave blank for a top-level item.",
64
+ }),
65
+ isVisible: annotate(z.boolean().optional(), {
66
+ section: "placement", order: 3, row: "quarter",
67
+ }),
68
+ });
69
+ /** Create contract — `label` and `href` are what make an item usable at all. */
70
+ export const navItemCreateSchema = navItemFormSchema;
71
+ /** Update contract — every field optional. */
72
+ export const navItemUpdateSchema = navItemFormSchema.partial();
@@ -14,5 +14,14 @@ export interface AuctionBidsTableProps {
14
14
  totalPages?: number;
15
15
  currentPage?: number;
16
16
  onPageChange?: (page: number) => void;
17
+ /**
18
+ * Open a bid. Supplied by the three dashboard bid listings, which were dead
19
+ * ends until W8 — a bid could be seen here and never opened, so its amount,
20
+ * auction, timing and outcome existed only as a table row.
21
+ *
22
+ * Optional because the PUBLIC bid history on an auction page also renders
23
+ * this table, and there a row is not something a visitor may open.
24
+ */
25
+ onRowClick?: (bid: BidDocument) => void;
17
26
  }
18
- export declare function AuctionBidsTable({ bids, portal, emptyLabel, totalPages, currentPage, onPageChange, }: AuctionBidsTableProps): React.JSX.Element;
27
+ export declare function AuctionBidsTable({ bids, portal, emptyLabel, totalPages, currentPage, onPageChange, onRowClick, }: AuctionBidsTableProps): React.JSX.Element;
@@ -52,7 +52,22 @@ function groupByAuction(bids) {
52
52
  }
53
53
  return Array.from(map.values());
54
54
  }
55
- function AuctionRow({ auction, portal, }) {
55
+ /**
56
+ * Enter/Space activate a non-button element that behaves as one.
57
+ *
58
+ * A named helper rather than an inline closure: the inline form nested six
59
+ * braces deep inside the row's JSX, which is both unreadable and what
60
+ * `audit-code-quality`'s DEEP_NESTING rule exists to prevent.
61
+ */
62
+ function activateOnKey(run) {
63
+ return (e) => {
64
+ if (e.key !== "Enter" && e.key !== " ")
65
+ return;
66
+ e.preventDefault();
67
+ run();
68
+ };
69
+ }
70
+ function AuctionRow({ auction, portal, onRowClick, }) {
56
71
  const [expanded, setExpanded] = useState(false);
57
72
  // Most-recent bid first — matches the caller's own newest-first ordering
58
73
  // (see UserBidsPage) instead of re-sorting by amount, which silently threw
@@ -60,14 +75,17 @@ function AuctionRow({ auction, portal, }) {
60
75
  const sorted = useMemo(() => [...auction.bids].sort((a, b) => +new Date(b.bidDate) - +new Date(a.bidDate)), [auction.bids]);
61
76
  const highest = auction.bids.reduce((max, b) => Math.max(max, b.bidAmount), 0);
62
77
  const isWinning = auction.bids.some((b) => b.isWinning);
63
- return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "xl", shadow: "sm", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setExpanded((v) => !v), gap: "lg", paddingX: "md", paddingY: "md", className: "w-full text-left", "aria-expanded": expanded, children: [_jsx(ChevronRight, { className: `shrink-0 text-[var(--appkit-color-text-muted)] transition-transform ${expanded ? "rotate-90" : ""}`, size: 16 }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(Text, { className: "text-[var(--appkit-color-text)] line-clamp-1", size: "sm", weight: "semibold", children: auction.productTitle }) }), _jsxs(Row, { gap: "sm", className: "shrink-0", children: [isWinning && (_jsx(Badge, { variant: "active", children: "Winning" })), _jsxs(Text, { variant: "secondary", size: "xs", children: [auction.bids.length, " bid", auction.bids.length !== 1 ? "s" : ""] }), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: formatBidAmount(highest) })] })] }), expanded && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", children: [_jsxs(Div, { textSize: "xs", textWeight: "medium", paddingX: "x-md", paddingY: "y-xs", className: `grid text-[var(--appkit-color-text-muted)] uppercase tracking-wide border-b border-[var(--appkit-color-border-subtle)] ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [_jsx(Span, { children: portal === "buyer" ? "Amount" : "Bidder" }), portal !== "buyer" && _jsx(Span, { children: "Amount" }), _jsx(Span, { children: "Status" }), _jsx(Span, { className: "text-right", children: "Time" })] }), sorted.map((bid) => (_jsxs(Div, { layout: "grid", align: "center", paddingX: "x-md", paddingY: "y-xs-tall", className: `border-b border-[var(--appkit-color-border-subtle)] last:border-0 hover:bg-[var(--appkit-color-border-subtle)] transition-colors ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [portal !== "buyer" && (_jsx(Text, { className: "text-[var(--appkit-color-text)] truncate pr-[0.75rem]", size: "sm", children: bid.userName || bid.userId })), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: formatBidAmount(bid.bidAmount) }), _jsx(Badge, { variant: STATUS_VARIANT[bid.status] ?? "pending", className: "capitalize", children: bid.status }), _jsxs(Stack, { gap: "xs", align: "end", children: [_jsx(Text, { variant: "secondary", size: "xs", align: "end", children: relDate(bid.bidDate) }), portal === "buyer" && bid.status === "won" && (_jsx(TextLink, { href: bid.orderId
78
+ return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "xl", shadow: "sm", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setExpanded((v) => !v), gap: "lg", paddingX: "md", paddingY: "md", className: "w-full text-left", "aria-expanded": expanded, children: [_jsx(ChevronRight, { className: `shrink-0 text-[var(--appkit-color-text-muted)] transition-transform ${expanded ? "rotate-90" : ""}`, size: 16 }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(Text, { className: "text-[var(--appkit-color-text)] line-clamp-1", size: "sm", weight: "semibold", children: auction.productTitle }) }), _jsxs(Row, { gap: "sm", className: "shrink-0", children: [isWinning && (_jsx(Badge, { variant: "active", children: "Winning" })), _jsxs(Text, { variant: "secondary", size: "xs", children: [auction.bids.length, " bid", auction.bids.length !== 1 ? "s" : ""] }), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: formatBidAmount(highest) })] })] }), expanded && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", children: [_jsxs(Div, { textSize: "xs", textWeight: "medium", paddingX: "x-md", paddingY: "y-xs", className: `grid text-[var(--appkit-color-text-muted)] uppercase tracking-wide border-b border-[var(--appkit-color-border-subtle)] ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [_jsx(Span, { children: portal === "buyer" ? "Amount" : "Bidder" }), portal !== "buyer" && _jsx(Span, { children: "Amount" }), _jsx(Span, { children: "Status" }), _jsx(Span, { className: "text-right", children: "Time" })] }), sorted.map((bid) => (_jsxs(Div, { layout: "grid", align: "center", paddingX: "x-md", paddingY: "y-xs-tall",
79
+ // Keyboard-reachable only when it is actually interactive —
80
+ // a row with no handler must not be a tab stop.
81
+ role: onRowClick ? "button" : undefined, tabIndex: onRowClick ? 0 : undefined, onClick: onRowClick ? () => onRowClick(bid) : undefined, onKeyDown: onRowClick ? activateOnKey(() => onRowClick(bid)) : undefined, className: `border-b border-[var(--appkit-color-border-subtle)] last:border-0 hover:bg-[var(--appkit-color-border-subtle)] transition-colors ${onRowClick ? "cursor-pointer" : ""} ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [portal !== "buyer" && (_jsx(Text, { className: "text-[var(--appkit-color-text)] truncate pr-[0.75rem]", size: "sm", children: bid.userName || bid.userId })), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: formatBidAmount(bid.bidAmount) }), _jsx(Badge, { variant: STATUS_VARIANT[bid.status] ?? "pending", className: "capitalize", children: bid.status }), _jsxs(Stack, { gap: "xs", align: "end", children: [_jsx(Text, { variant: "secondary", size: "xs", align: "end", children: relDate(bid.bidDate) }), portal === "buyer" && bid.status === "won" && (_jsx(TextLink, { href: bid.orderId
64
82
  ? String(ROUTES.USER.ORDER_DETAIL(bid.orderId))
65
83
  : `${String(ROUTES.USER.CHECKOUT)}?lane=${CART_LANE.AUCTION}`, size: "xs", weight: "semibold", children: bid.orderId ? "View order" : "Pay now →" }))] })] }, bid.id)))] }))] }));
66
84
  }
67
- export function AuctionBidsTable({ bids, portal = "buyer", emptyLabel = "No bids found.", totalPages, currentPage, onPageChange, }) {
85
+ export function AuctionBidsTable({ bids, portal = "buyer", emptyLabel = "No bids found.", totalPages, currentPage, onPageChange, onRowClick, }) {
68
86
  const auctions = useMemo(() => groupByAuction(bids), [bids]);
69
87
  if (auctions.length === 0) {
70
88
  return (_jsx(Div, { padding: "y-6xl", className: "text-left", children: _jsx(Text, { variant: "secondary", children: emptyLabel }) }));
71
89
  }
72
- return (_jsxs(Stack, { gap: "3", children: [auctions.map((auction) => (_jsx(AuctionRow, { auction: auction, portal: portal }, auction.productId))), totalPages !== undefined && totalPages > 1 && currentPage !== undefined && onPageChange && (_jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) }))] }));
90
+ return (_jsxs(Stack, { gap: "3", children: [auctions.map((auction) => (_jsx(AuctionRow, { auction: auction, portal: portal, onRowClick: onRowClick }, auction.productId))), totalPages !== undefined && totalPages > 1 && currentPage !== undefined && onPageChange && (_jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) }))] }));
73
91
  }