@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.
- package/dist/_internal/server/features/auctions/actions.js +2 -2
- package/dist/_internal/server/features/cart/service.d.ts +3 -3
- package/dist/_internal/server/features/checkout/actions.js +19 -0
- package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
- package/dist/_internal/server/features/checkout/source-context.js +157 -0
- package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
- package/dist/_internal/server/features/offers/adapters.js +88 -0
- package/dist/_internal/server/features/offers/index.d.ts +2 -0
- package/dist/_internal/server/features/offers/index.js +1 -0
- package/dist/_internal/server/features/orders/adapters.js +24 -0
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
- package/dist/_internal/shared/actions/action-registry.js +3 -1
- package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
- package/dist/_internal/shared/features/auctions/schema.js +13 -1
- package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
- package/dist/_internal/shared/features/cart/schema.js +15 -0
- package/dist/_internal/shared/features/products/schema.d.ts +2 -2
- package/dist/_internal/shared/history/history.d.ts +22 -0
- package/dist/_internal/shared/history/history.js +41 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.js +20 -2
- package/dist/constants/field-names.d.ts +8 -6
- package/dist/constants/field-names.js +48 -6
- package/dist/features/account/components/UserBidsView.js +12 -4
- package/dist/features/account/components/UserReturnsView.d.ts +3 -0
- package/dist/features/account/components/UserReturnsView.js +17 -1
- package/dist/features/admin/components/AdminAuditLogView.js +1 -0
- package/dist/features/admin/components/AdminBidsView.js +15 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
- package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
- package/dist/features/admin/components/AdminNavEditorView.js +15 -5
- package/dist/features/admin/components/AdminNewsletterView.js +18 -2
- package/dist/features/admin/components/AdminOffersView.js +62 -18
- package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
- package/dist/features/admin/components/AdminScammersView.js +3 -8
- package/dist/features/admin/components/AdminSectionsView.js +32 -23
- package/dist/features/admin/components/AdminSessionsView.js +17 -2
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
- package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
- package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
- package/dist/features/admin/constants/filter-tabs.js +1 -0
- package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
- package/dist/features/admin/schemas/homepage-section-form.js +68 -0
- package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
- package/dist/features/admin/schemas/nav-item-form.js +72 -0
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
- package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
- package/dist/features/auctions/components/bid-detail-fields.js +103 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
- package/dist/features/audit-log/schemas/firestore.js +7 -0
- package/dist/features/blog/schemas/blog-form.d.ts +80 -0
- package/dist/features/blog/schemas/blog-form.js +100 -0
- package/dist/features/events/components/AdminEventEditorView.js +32 -54
- package/dist/features/events/components/AdminEventEntriesView.js +22 -3
- package/dist/features/events/schemas/event-form.d.ts +113 -0
- package/dist/features/events/schemas/event-form.js +82 -0
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
- package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
- package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
- package/dist/features/homepage/schemas/index.d.ts +31 -0
- package/dist/features/homepage/schemas/index.js +24 -0
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
- package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
- package/dist/features/orders/repository/orders.repository.js +5 -0
- package/dist/features/orders/schemas/firestore.d.ts +36 -1
- package/dist/features/orders/types/index.d.ts +35 -7
- package/dist/features/products/components/GroupSettingsPanel.js +5 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
- package/dist/features/products/schemas/product-features.validators.js +63 -0
- package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
- package/dist/features/scams/constants/scammer-status-display.js +79 -0
- package/dist/features/seller/actions/offer-actions.js +32 -2
- package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
- package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
- package/dist/features/seller/components/SellerBidsView.js +11 -4
- package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
- package/dist/features/seller/components/SellerOffersView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
- package/dist/features/seller/repository/offer.repository.d.ts +69 -8
- package/dist/features/seller/repository/offer.repository.js +127 -23
- package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
- package/dist/features/seller/schemas/coupon-form.js +116 -0
- package/dist/features/seller/schemas/firestore.d.ts +56 -2
- package/dist/features/seller/schemas/firestore.js +27 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
- package/dist/features/seller/schemas/offer-forms.js +21 -0
- package/dist/features/shell/build-sections.d.ts +63 -0
- package/dist/features/shell/build-sections.js +207 -0
- package/dist/features/shell/field-ui-meta.d.ts +115 -0
- package/dist/features/shell/field-ui-meta.js +324 -0
- package/dist/features/shell/index.d.ts +4 -0
- package/dist/features/shell/index.js +2 -0
- package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
- package/dist/features/status-history/components/StatusTimeline.js +46 -0
- package/dist/features/status-history/index.d.ts +11 -0
- package/dist/features/status-history/index.js +10 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
- package/dist/features/store-extensions/index.d.ts +12 -0
- package/dist/features/store-extensions/index.js +12 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
- package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
- package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
- package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
- package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
- package/dist/features/store-extensions/schemas/firestore.js +60 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
- package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
- package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
- package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
- package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
- package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
- package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
- package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
- package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +12 -2
- package/dist/next/routing/route-map.d.ts +16 -2
- package/dist/next/routing/route-map.js +9 -2
- package/dist/schemas/firestore-value.d.ts +8 -0
- package/dist/schemas/firestore-value.js +44 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/registry.d.ts +634 -1
- package/dist/schemas/registry.js +64 -1
- package/dist/seed/bids-seed-data.js +30 -0
- package/dist/seed/offers-seed-data.js +212 -7
- package/dist/seed/orders-seed-data.js +336 -1
- package/dist/seed/products-auctions-seed-data.js +41 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/RecordDetailModal.d.ts +10 -1
- package/dist/ui/components/RecordDetailModal.js +2 -2
- package/package.json +2 -1
- package/scripts/node-esm-loader.mjs +65 -0
- package/scripts/seed-cli-loader.mjs +6 -49
- 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
|
-
|
|
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 ${
|
|
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:
|
|
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:
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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(
|
|
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
|
-
|
|
3
|
-
export
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
}
|