@mohasinac/appkit 4.11.1 → 4.11.3
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/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +138 -27
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +2 -1
- package/dist/_internal/server/features/products/index.js +2 -1
- package/dist/_internal/server/features/products/list-public.d.ts +130 -0
- package/dist/_internal/server/features/products/list-public.js +375 -0
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
- package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/messages.js +5 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
- package/dist/_internal/shared/checkout/lanes.js +106 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
- package/dist/_internal/shared/checkout/order-math.js +30 -6
- package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +7 -2
- package/dist/client.js +8 -2
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/NotificationBell.js +1 -0
- package/dist/features/account/components/UserOffersPanel.js +8 -1
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/actions/notification-actions.js +1 -1
- package/dist/features/admin/components/AdminAddressesView.js +3 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- package/dist/features/admin/schemas/firestore.d.ts +2 -1
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
- package/dist/features/auctions/actions/bid-actions.js +29 -15
- package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/auctions/components/AuctionsListView.js +12 -53
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
- package/dist/features/auctions/repository/bid.repository.js +19 -0
- package/dist/features/auctions/schemas/firestore.d.ts +11 -1
- package/dist/features/auctions/schemas/firestore.js +1 -0
- package/dist/features/cart/actions/cart-actions.d.ts +11 -0
- package/dist/features/cart/actions/cart-actions.js +24 -0
- package/dist/features/cart/repository/cart.repository.d.ts +24 -1
- package/dist/features/cart/repository/cart.repository.js +71 -6
- package/dist/features/cart/schemas/firestore.d.ts +23 -4
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +56 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +26 -3
- package/dist/features/layout/BottomActionsContext.d.ts +14 -0
- package/dist/features/layout/BottomActionsContext.js +13 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/repository/orders.repository.d.ts +4 -15
- package/dist/features/orders/repository/orders.repository.js +4 -27
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/orders/utils/order-splitter.js +14 -2
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
- package/dist/features/products/components/ArtStickersListView.js +10 -49
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/components/ProductsIndexPageView.js +9 -62
- package/dist/features/products/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +67 -8
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- package/dist/features/seller/actions/offer-actions.d.ts +6 -0
- package/dist/features/seller/actions/offer-actions.js +8 -9
- package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
- package/dist/features/seller/components/SellerOffersView.js +59 -8
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/seller/repository/offer.repository.d.ts +18 -2
- package/dist/features/seller/repository/offer.repository.js +38 -2
- package/dist/features/seller/schemas/firestore.d.ts +7 -2
- package/dist/features/seller/schemas/firestore.js +3 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
- package/dist/features/seller/schemas/offer-forms.js +28 -0
- package/dist/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +15 -3
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -0
- package/dist/styles.css +44 -39
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit/client";
|
|
4
4
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
5
5
|
import React from "react";
|
|
6
|
-
import { FilterChipGroup, ListingLayout, RowActionMenu } from "../../../ui";
|
|
6
|
+
import { FilterChipGroup, ListingLayout, RowActionMenu, RecordDetailModal } from "../../../ui";
|
|
7
|
+
import { QuickFormDrawer } from "../../shell/QuickFormDrawer";
|
|
8
|
+
import { counterOfferFormSchema } from "../schemas/offer-forms";
|
|
7
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
10
|
import { SELLER_OFFER_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
9
11
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
10
12
|
import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
11
13
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
12
14
|
export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCounterOffer, ...props }) {
|
|
15
|
+
const [detailRow, setDetailRow] = React.useState(null);
|
|
16
|
+
const [counterRow, setCounterRow] = React.useState(null);
|
|
17
|
+
const [isCountering, setIsCountering] = React.useState(false);
|
|
13
18
|
if (React.Children.count(children) > 0) {
|
|
14
19
|
return (_jsx(ListingLayout, { portal: "seller", ...props, children: children }));
|
|
15
20
|
}
|
|
@@ -26,26 +31,31 @@ export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCou
|
|
|
26
31
|
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
27
32
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
28
33
|
],
|
|
29
|
-
mapRows: (response) => toRecordArray(response.
|
|
34
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
|
|
30
35
|
id: toStringValue(item.id, `offer-${index}`),
|
|
31
36
|
primary: toStringValue(item.productTitle ?? item.title, "Untitled product"),
|
|
32
37
|
secondary: [
|
|
33
38
|
`Offer: ${toCurrency(item.offerAmount ?? item.amount)}`,
|
|
39
|
+
`Listed: ${toCurrency(item.listedPrice)}`,
|
|
34
40
|
toStringValue(item.buyerName ?? "Unknown buyer", "Unknown buyer"),
|
|
35
41
|
].join(" · "),
|
|
36
42
|
status: toStringValue(item.status, "Pending"),
|
|
37
43
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
44
|
+
detail: item,
|
|
38
45
|
})),
|
|
39
|
-
getTotal: (response, mappedRows) => typeof response.
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
47
|
+
// A menu of pure mutations is not a detail affordance — the seller must be
|
|
48
|
+
// able to READ the offer (buyer note, listed vs offered price, expiry)
|
|
49
|
+
// before accepting or declining it (Root Cause #56).
|
|
50
|
+
onRowClick: (row) => setDetailRow(row),
|
|
42
51
|
buildFilters: (state) => state.status && state.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, state.status) : undefined,
|
|
43
52
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
53
|
+
{ label: "View details", onClick: () => setDetailRow(row) },
|
|
44
54
|
...(onAcceptOffer
|
|
45
55
|
? [{ label: ACTIONS.STORE["accept-offer"].label, onClick: () => void onAcceptOffer(row.id) }]
|
|
46
56
|
: []),
|
|
47
57
|
...(onCounterOffer
|
|
48
|
-
? [{ label: ACTIONS.STORE["counter-offer"].label, onClick: () =>
|
|
58
|
+
? [{ label: ACTIONS.STORE["counter-offer"].label, onClick: () => setCounterRow(row) }]
|
|
49
59
|
: []),
|
|
50
60
|
...(onRejectOffer
|
|
51
61
|
? [{ label: ACTIONS.STORE["reject-offer"].label, destructive: true, onClick: () => void onRejectOffer(row.id) }]
|
|
@@ -53,5 +63,46 @@ export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCou
|
|
|
53
63
|
] })),
|
|
54
64
|
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: SELLER_OFFER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
|
|
55
65
|
};
|
|
56
|
-
|
|
66
|
+
const d = detailRow?.detail ?? {};
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detailRow !== null, onClose: () => setDetailRow(null), title: detailRow?.primary ?? "Offer", badges: detailRow ? [{ label: detailRow.status }] : undefined, description: toStringValue(d.buyerNote, ""), fields: [
|
|
68
|
+
{ label: "Buyer", value: toStringValue(d.buyerName, "Unknown buyer") },
|
|
69
|
+
{ label: "Listed price", value: toCurrency(d.listedPrice) },
|
|
70
|
+
{ label: "Offered", value: toCurrency(d.offerAmount) },
|
|
71
|
+
...(d.counterAmount
|
|
72
|
+
? [{ label: "Your counter", value: toCurrency(d.counterAmount) }]
|
|
73
|
+
: []),
|
|
74
|
+
...(d.lockedPrice
|
|
75
|
+
? [{ label: "Agreed price", value: toCurrency(d.lockedPrice) }]
|
|
76
|
+
: []),
|
|
77
|
+
{ label: "Offer expires", value: toRelativeDate(d.expiresAt) },
|
|
78
|
+
...(d.checkoutDeadline
|
|
79
|
+
? [{ label: "Buyer must pay by", value: toRelativeDate(d.checkoutDeadline) }]
|
|
80
|
+
: []),
|
|
81
|
+
...(d.sellerNote ? [{ label: "Your note", value: toStringValue(d.sellerNote, "") }] : []),
|
|
82
|
+
] }), _jsx(QuickFormDrawer, { isOpen: counterRow !== null, onClose: () => setCounterRow(null), title: `Counter offer — ${counterRow?.primary ?? ""}`, submitLabel: "Send counter", isLoading: isCountering, schema: counterOfferFormSchema, fields: [
|
|
83
|
+
{
|
|
84
|
+
name: "counterAmount",
|
|
85
|
+
label: "Your counter price (₹)",
|
|
86
|
+
type: "number",
|
|
87
|
+
required: true,
|
|
88
|
+
helperText: "Must be below your listed price and different from the buyer's offer.",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "sellerNote",
|
|
92
|
+
label: "Note to buyer (optional)",
|
|
93
|
+
type: "textarea",
|
|
94
|
+
placeholder: "Best I can do is …",
|
|
95
|
+
},
|
|
96
|
+
], onSubmit: async (values) => {
|
|
97
|
+
if (!counterRow || !onCounterOffer)
|
|
98
|
+
return;
|
|
99
|
+
setIsCountering(true);
|
|
100
|
+
try {
|
|
101
|
+
await onCounterOffer(counterRow.id, Number(values.counterAmount), values.sellerNote ? String(values.sellerNote) : undefined);
|
|
102
|
+
setCounterRow(null);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
setIsCountering(false);
|
|
106
|
+
}
|
|
107
|
+
} })] }));
|
|
57
108
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Badge, Button, Checkbox, Div, Grid, Row, Stack, Text, TextLink } from "../../../ui";
|
|
4
4
|
import { MediaImage } from "../../media/MediaImage";
|
|
5
5
|
import { ROW_ACTION_META, ROW_ACTION_ID } from "../../../features/products/constants/action-defs";
|
|
6
|
-
import {
|
|
6
|
+
import { listingBadgeVariant } from "../../products/utils/listing-badge-variant";
|
|
7
7
|
const __P = {
|
|
8
8
|
p3: "p-[var(--appkit-space-3)]",
|
|
9
9
|
};
|
|
@@ -25,6 +25,6 @@ export function SellerProductsCards({ view, rows, isLoading, listingKind, select
|
|
|
25
25
|
return (_jsxs(Container, { ...containerProps, children: [rows.length === 0 && !isLoading && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] col-span-full", size: "sm", children: listingKind !== "all" ? `No ${listingKind} listings found` : "No products listed yet" })), rows.map((row) => {
|
|
26
26
|
const isSelected = selectedIds.has(row.id);
|
|
27
27
|
const href = buildHref(row);
|
|
28
|
-
return view === "grid" ? (_jsxs(Div, { className: "group relative bg-[var(--appkit-color-surface)] overflow-hidden", shadow: "hover-md", rounded: "xl", border: isSelected ? "strong" : "default", children: [_jsx(Div, { className: "absolute top-2 left-2 z-10", children: _jsx(Checkbox, { bare: true, checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", onClick: (e) => e.stopPropagation(), "aria-label": "Select" }) }), _jsxs(TextLink, { href: href, className: "block", children: [_jsx(Div, { className: "w-full aspect-square", children: _jsx(MediaImage, { src: row.imageUrl, alt: row.primary, size: "card" }) }), _jsxs(Stack, { className: `${__P.p3}`, gap: "xs", children: [_jsx(Text, { className: "line-clamp-1", size: "sm", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant:
|
|
28
|
+
return view === "grid" ? (_jsxs(Div, { className: "group relative bg-[var(--appkit-color-surface)] overflow-hidden", shadow: "hover-md", rounded: "xl", border: isSelected ? "strong" : "default", children: [_jsx(Div, { className: "absolute top-2 left-2 z-10", children: _jsx(Checkbox, { bare: true, checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", onClick: (e) => e.stopPropagation(), "aria-label": "Select" }) }), _jsxs(TextLink, { href: href, className: "block", children: [_jsx(Div, { className: "w-full aspect-square", children: _jsx(MediaImage, { src: row.imageUrl, alt: row.primary, size: "card" }) }), _jsxs(Stack, { className: `${__P.p3}`, gap: "xs", children: [_jsx(Text, { className: "line-clamp-1", size: "sm", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant: listingBadgeVariant(row.listingKind), children: row.listingKind }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] line-clamp-1", size: "xs", children: row.secondary })] })] })] }), _jsxs(Row, { className: "border-t border-[var(--appkit-color-border)]", justify: "end", gap: "xs", padding: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onEdit(row); }, "aria-label": "Edit", children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onDuplicate(row); }, "aria-label": "Duplicate", children: ROW_ACTION_META[ROW_ACTION_ID.DUPLICATE].label }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onDelete(row); }, "aria-label": "Delete", children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label }))] })] }, row.id)) : (_jsxs(Row, { className: "bg-[var(--appkit-color-surface)] hover:bg-[var(--appkit-color-surface-raised)]", align: "center", gap: "3", rounded: "lg", padding: "inlineSm", border: isSelected ? "strong" : "default", children: [_jsx(Checkbox, { bare: true, checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", "aria-label": "Select" }), _jsx(Div, { className: `w-12 h-12 ${__O.hidden}`, rounded: "default", children: _jsx(MediaImage, { src: row.imageUrl, alt: row.primary, size: "thumbnail" }) }), _jsxs(TextLink, { href: href, className: "flex-1 min-w-0", children: [_jsx(Text, { className: "line-clamp-1", size: "sm", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant: listingBadgeVariant(row.listingKind), children: row.listingKind }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] line-clamp-1", size: "xs", children: row.secondary })] })] }), _jsxs(Row, { className: "flex-shrink-0", gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onEdit(row), "aria-label": "Edit", children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDuplicate(row), "aria-label": "Duplicate", children: ROW_ACTION_META[ROW_ACTION_ID.DUPLICATE].label }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDelete(row), "aria-label": "Delete", children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label }))] })] }, row.id));
|
|
29
29
|
})] }));
|
|
30
30
|
}
|
|
@@ -17,9 +17,11 @@ import { ROUTES } from "../../../constants";
|
|
|
17
17
|
import { normalizeListingType } from "../../products/utils/listing-type";
|
|
18
18
|
import { toRecordArray, toRelativeDate, toStringValue } from "../hooks/useSellerListingData";
|
|
19
19
|
import { useListingTypeFlags } from "../../../react/hooks/useListingTypeFlags";
|
|
20
|
+
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
|
|
21
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
20
22
|
import { SellerProductsCards } from "./SellerProductsCards";
|
|
21
23
|
import { SellerProductsFilterFields } from "./SellerProductsFilterDrawer";
|
|
22
|
-
import {
|
|
24
|
+
import { listingBadgeVariant } from "../../products/utils/listing-badge-variant";
|
|
23
25
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
24
26
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
25
27
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
|
|
@@ -43,21 +45,24 @@ const SORT_OPTIONS = [
|
|
|
43
45
|
{ value: sortBy("title", "DESC"), label: "Title Z–A" },
|
|
44
46
|
{ value: sortBy("price", "DESC"), label: "Price High" },
|
|
45
47
|
{ value: sortBy("price", "ASC"), label: "Price Low" },
|
|
48
|
+
// Now genuinely sortable — see the note on AdminProductsView's sortOptions.
|
|
49
|
+
{ value: sortBy("featured", "DESC"), label: "Featured first" },
|
|
50
|
+
{ value: sortBy("isPromoted", "DESC"), label: "Promoted first" },
|
|
46
51
|
];
|
|
47
52
|
const STATUS_OPTIONS = SELLER_PRODUCT_STATUS_TABS;
|
|
48
53
|
function TypeDropdown({ active, onChange, }) {
|
|
49
54
|
const flags = useListingTypeFlags();
|
|
55
|
+
// Derived from ALL_LISTING_TYPES + the plugin registry (2026-08-21). This
|
|
56
|
+
// used to be a hand-written list that had no `art`/`stickers` entries and
|
|
57
|
+
// still offered `bundle` — which stopped being a listingType in SB-UNI-D, so
|
|
58
|
+
// picking it filtered on a value no product can hold and returned nothing.
|
|
50
59
|
const options = [
|
|
51
60
|
{ value: "all", label: "All listings" },
|
|
52
|
-
flags.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
flags.classified && { value: "classified", label: "Classified" },
|
|
58
|
-
flags["digital-code"] && { value: "digital-code", label: "Digital Code" },
|
|
59
|
-
flags.live && { value: "live", label: "Live" },
|
|
60
|
-
].filter(Boolean);
|
|
61
|
+
...ALL_LISTING_TYPES.filter(flags.isEnabled).map((type) => ({
|
|
62
|
+
value: type,
|
|
63
|
+
label: pluginFor(type).typeLabel,
|
|
64
|
+
})),
|
|
65
|
+
];
|
|
61
66
|
return (_jsxs(Row, { paddingX: "x-sm-lg-md", border: "bottom", padding: "y-xs", gap: "sm", children: [_jsx(Text, { className: "tracking-wide text-[var(--appkit-color-text-muted)]", size: "xs", weight: "semibold", transform: "uppercase", children: "Listing type" }), _jsx(Select, { options: options, value: active, onValueChange: (v) => onChange(v), wrapperClassName: "sm:max-w-xs", "aria-label": "Filter by listing type" })] }));
|
|
62
67
|
}
|
|
63
68
|
const PRODUCT_COLUMNS = [
|
|
@@ -70,7 +75,7 @@ const PRODUCT_COLUMNS = [
|
|
|
70
75
|
{
|
|
71
76
|
key: "primary",
|
|
72
77
|
header: "Product",
|
|
73
|
-
render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] line-clamp-1", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant:
|
|
78
|
+
render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] line-clamp-1", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant: listingBadgeVariant(row.listingKind), children: row.listingKind }), _jsx(Span, { size: "xs", color: "muted", children: row.secondary })] })] })),
|
|
74
79
|
},
|
|
75
80
|
{
|
|
76
81
|
key: "price",
|
|
@@ -206,13 +211,11 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
206
211
|
mapRows: (response) => {
|
|
207
212
|
const rows = toRecordArray(response.products).map((item, index) => {
|
|
208
213
|
const lt = normalizeListingType(item);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
? "prize-draw"
|
|
215
|
-
: "standard";
|
|
214
|
+
// The row's own type, verbatim. This used to be a 4-branch ternary
|
|
215
|
+
// that collapsed classified / digital-code / live / art / stickers all
|
|
216
|
+
// down to "standard", so five of nine types displayed the wrong badge
|
|
217
|
+
// in the seller table (2026-08-21).
|
|
218
|
+
const kind = lt;
|
|
216
219
|
const priceRaw = typeof item.price === "number" ? item.price : 0;
|
|
217
220
|
const auctionSecondary = kind === "auction"
|
|
218
221
|
? [
|
|
@@ -51,15 +51,31 @@ declare class OfferRepository extends BaseRepository<OfferDocument> {
|
|
|
51
51
|
*/
|
|
52
52
|
countByBuyerAndProduct(buyerUid: string, productId: string, since: Date): Promise<number>;
|
|
53
53
|
updateStatus(offerId: string, patch: OfferUpdateInput): Promise<OfferDocument>;
|
|
54
|
-
accept(offerId: string, lockedPrice: number, sellerNote?: string): Promise<OfferDocument>;
|
|
54
|
+
accept(offerId: string, lockedPrice: number, sellerNote?: string, checkoutDeadline?: Date): Promise<OfferDocument>;
|
|
55
55
|
decline(offerId: string, sellerNote?: string): Promise<OfferDocument>;
|
|
56
56
|
counter(offerId: string, counterAmount: number, sellerNote?: string): Promise<OfferDocument>;
|
|
57
|
-
acceptCounter(offerId: string): Promise<OfferDocument>;
|
|
57
|
+
acceptCounter(offerId: string, checkoutDeadline?: Date): Promise<OfferDocument>;
|
|
58
58
|
withdraw(offerId: string): Promise<OfferDocument>;
|
|
59
|
+
/**
|
|
60
|
+
* Terminal transition, called once the buyer's order for this offer exists.
|
|
61
|
+
*
|
|
62
|
+
* `"paid"` has been in `OfferStatusValues` since the feature shipped but had
|
|
63
|
+
* NO server-side writer — the only thing that ever set it was an optimistic
|
|
64
|
+
* client-side patch in UserOffersPanel, which meant a reload showed the offer
|
|
65
|
+
* back at "accepted" and it could be added to the cart and ordered again.
|
|
66
|
+
*/
|
|
67
|
+
markPaid(offerId: string, orderId: string): Promise<OfferDocument>;
|
|
59
68
|
/**
|
|
60
69
|
* Cloud Functions compatibility: pending/countered offers already expired.
|
|
61
70
|
*/
|
|
62
71
|
findExpiredActive(now: Date): Promise<OfferDocument[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Accepted offers whose 48h checkout window has lapsed. These were never
|
|
74
|
+
* swept before — `findExpiredActive` only looks at pending/countered — so an
|
|
75
|
+
* accepted-but-never-paid offer stayed "accepted" forever and its locked
|
|
76
|
+
* price remained claimable long after the seller expected it to lapse.
|
|
77
|
+
*/
|
|
78
|
+
findExpiredAccepted(now: Date): Promise<OfferDocument[]>;
|
|
63
79
|
expireMany(offerIds: string[]): Promise<void>;
|
|
64
80
|
}
|
|
65
81
|
export declare const offerRepository: OfferRepository;
|
|
@@ -134,13 +134,18 @@ class OfferRepository extends BaseRepository {
|
|
|
134
134
|
updatedAt: new Date(),
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
-
async accept(offerId, lockedPrice, sellerNote) {
|
|
137
|
+
async accept(offerId, lockedPrice, sellerNote, checkoutDeadline) {
|
|
138
|
+
// One write, not two. `checkoutDeadline` used to be missing from
|
|
139
|
+
// OfferUpdateInput, which forced callers to follow every accept() with a
|
|
140
|
+
// separate generic update() — leaving a window where an offer was
|
|
141
|
+
// "accepted" with no deadline at all.
|
|
138
142
|
return this.updateStatus(offerId, {
|
|
139
143
|
status: "accepted",
|
|
140
144
|
lockedPrice,
|
|
141
145
|
sellerNote,
|
|
142
146
|
acceptedAt: new Date(),
|
|
143
147
|
respondedAt: new Date(),
|
|
148
|
+
...(checkoutDeadline ? { checkoutDeadline } : {}),
|
|
144
149
|
});
|
|
145
150
|
}
|
|
146
151
|
async decline(offerId, sellerNote) {
|
|
@@ -158,7 +163,7 @@ class OfferRepository extends BaseRepository {
|
|
|
158
163
|
respondedAt: new Date(),
|
|
159
164
|
});
|
|
160
165
|
}
|
|
161
|
-
async acceptCounter(offerId) {
|
|
166
|
+
async acceptCounter(offerId, checkoutDeadline) {
|
|
162
167
|
const offer = await this.findById(offerId);
|
|
163
168
|
if (!offer || !offer.counterAmount)
|
|
164
169
|
throw new Error("Offer or counter not found");
|
|
@@ -167,6 +172,7 @@ class OfferRepository extends BaseRepository {
|
|
|
167
172
|
lockedPrice: offer.counterAmount,
|
|
168
173
|
acceptedAt: new Date(),
|
|
169
174
|
respondedAt: new Date(),
|
|
175
|
+
...(checkoutDeadline ? { checkoutDeadline } : {}),
|
|
170
176
|
});
|
|
171
177
|
}
|
|
172
178
|
async withdraw(offerId) {
|
|
@@ -175,6 +181,21 @@ class OfferRepository extends BaseRepository {
|
|
|
175
181
|
respondedAt: new Date(),
|
|
176
182
|
});
|
|
177
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Terminal transition, called once the buyer's order for this offer exists.
|
|
186
|
+
*
|
|
187
|
+
* `"paid"` has been in `OfferStatusValues` since the feature shipped but had
|
|
188
|
+
* NO server-side writer — the only thing that ever set it was an optimistic
|
|
189
|
+
* client-side patch in UserOffersPanel, which meant a reload showed the offer
|
|
190
|
+
* back at "accepted" and it could be added to the cart and ordered again.
|
|
191
|
+
*/
|
|
192
|
+
async markPaid(offerId, orderId) {
|
|
193
|
+
return this.updateStatus(offerId, {
|
|
194
|
+
status: "paid",
|
|
195
|
+
paidOrderId: orderId,
|
|
196
|
+
paidAt: new Date(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
178
199
|
/**
|
|
179
200
|
* Cloud Functions compatibility: pending/countered offers already expired.
|
|
180
201
|
*/
|
|
@@ -187,6 +208,21 @@ class OfferRepository extends BaseRepository {
|
|
|
187
208
|
.get();
|
|
188
209
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
189
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Accepted offers whose 48h checkout window has lapsed. These were never
|
|
213
|
+
* swept before — `findExpiredActive` only looks at pending/countered — so an
|
|
214
|
+
* accepted-but-never-paid offer stayed "accepted" forever and its locked
|
|
215
|
+
* price remained claimable long after the seller expected it to lapse.
|
|
216
|
+
*/
|
|
217
|
+
async findExpiredAccepted(now) {
|
|
218
|
+
const snapshot = await this.db
|
|
219
|
+
.collection(this.collection)
|
|
220
|
+
.where(OFFER_FIELDS.STATUS, "==", "accepted")
|
|
221
|
+
.where(OFFER_FIELDS.CHECKOUT_DEADLINE, "<=", now)
|
|
222
|
+
.limit(500)
|
|
223
|
+
.get();
|
|
224
|
+
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
225
|
+
}
|
|
190
226
|
async expireMany(offerIds) {
|
|
191
227
|
const batch = this.db.batch();
|
|
192
228
|
const now = new Date();
|
|
@@ -36,9 +36,12 @@ export interface OfferDocument extends BaseDocument {
|
|
|
36
36
|
acceptedAt?: Date;
|
|
37
37
|
checkoutDeadline?: Date;
|
|
38
38
|
respondedAt?: Date;
|
|
39
|
+
/** Set by `markPaid` once the buyer's order exists — makes "paid" auditable. */
|
|
40
|
+
paidOrderId?: string;
|
|
41
|
+
paidAt?: Date;
|
|
39
42
|
}
|
|
40
43
|
export declare const OFFER_COLLECTION: "offers";
|
|
41
|
-
export declare const OFFER_INDEXED_FIELDS: readonly ["buyerUid", "storeId", "productId", "status", "createdAt", "expiresAt"];
|
|
44
|
+
export declare const OFFER_INDEXED_FIELDS: readonly ["buyerUid", "storeId", "productId", "status", "createdAt", "expiresAt", "checkoutDeadline"];
|
|
42
45
|
export declare const OFFER_FIELDS: {
|
|
43
46
|
readonly ID: "id";
|
|
44
47
|
readonly PRODUCT_ID: "productId";
|
|
@@ -60,11 +63,13 @@ export declare const OFFER_FIELDS: {
|
|
|
60
63
|
readonly ACCEPTED_AT: "acceptedAt";
|
|
61
64
|
readonly CHECKOUT_DEADLINE: "checkoutDeadline";
|
|
62
65
|
readonly RESPONDED_AT: "respondedAt";
|
|
66
|
+
readonly PAID_ORDER_ID: "paidOrderId";
|
|
67
|
+
readonly PAID_AT: "paidAt";
|
|
63
68
|
readonly CREATED_AT: "createdAt";
|
|
64
69
|
readonly UPDATED_AT: "updatedAt";
|
|
65
70
|
};
|
|
66
71
|
export type OfferCreateInput = Pick<OfferDocument, "productId" | "productTitle" | "productSlug" | "productImageUrl" | "buyerUid" | "buyerName" | "buyerEmail" | "storeId" | "storeName" | "offerAmount" | "listedPrice" | "currency" | "buyerNote">;
|
|
67
|
-
export type OfferUpdateInput = Partial<Pick<OfferDocument, "status" | "counterAmount" | "lockedPrice" | "sellerNote" | "acceptedAt" | "respondedAt" | "updatedAt">>;
|
|
72
|
+
export type OfferUpdateInput = Partial<Pick<OfferDocument, "status" | "counterAmount" | "lockedPrice" | "sellerNote" | "acceptedAt" | "checkoutDeadline" | "respondedAt" | "paidOrderId" | "paidAt" | "updatedAt">>;
|
|
68
73
|
export declare const offerQueryHelpers: {
|
|
69
74
|
readonly byBuyer: (buyerUid: string) => readonly ["buyerUid", "==", string];
|
|
70
75
|
readonly byStore: (storeId: string) => readonly ["storeId", "==", string];
|
|
@@ -20,6 +20,7 @@ export const OFFER_INDEXED_FIELDS = [
|
|
|
20
20
|
"status",
|
|
21
21
|
"createdAt",
|
|
22
22
|
"expiresAt",
|
|
23
|
+
"checkoutDeadline",
|
|
23
24
|
];
|
|
24
25
|
export const OFFER_FIELDS = {
|
|
25
26
|
ID: "id",
|
|
@@ -42,6 +43,8 @@ export const OFFER_FIELDS = {
|
|
|
42
43
|
ACCEPTED_AT: "acceptedAt",
|
|
43
44
|
CHECKOUT_DEADLINE: "checkoutDeadline",
|
|
44
45
|
RESPONDED_AT: "respondedAt",
|
|
46
|
+
PAID_ORDER_ID: "paidOrderId",
|
|
47
|
+
PAID_AT: "paidAt",
|
|
45
48
|
CREATED_AT: "createdAt",
|
|
46
49
|
UPDATED_AT: "updatedAt",
|
|
47
50
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const counterOfferFormSchema: z.ZodObject<{
|
|
3
|
+
counterAmount: z.ZodNumber;
|
|
4
|
+
sellerNote: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
counterAmount: number;
|
|
7
|
+
sellerNote?: string | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
counterAmount: number;
|
|
10
|
+
sellerNote?: string | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type CounterOfferFormValues = z.infer<typeof counterOfferFormSchema>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Rule #9 — every appkit form is schema-driven. The seller's counter-offer
|
|
3
|
+
* drawer had no form at all before 2026-08-21 (the view took an
|
|
4
|
+
* `(id) => void` callback and left each consumer to invent an input), so
|
|
5
|
+
* `respondToOffer`'s counter branch had zero real callers.
|
|
6
|
+
* WHAT: Zod schema for the counter-offer QuickFormDrawer. Deliberately only the
|
|
7
|
+
* field-shape rules that can be checked without loading the offer — the
|
|
8
|
+
* relational rules (below listed price, different from the buyer's offer)
|
|
9
|
+
* stay server-side in `respondToOffer`, which is the only place that can
|
|
10
|
+
* be trusted with them.
|
|
11
|
+
*
|
|
12
|
+
* EXPORTS:
|
|
13
|
+
* counterOfferFormSchema, type CounterOfferFormValues
|
|
14
|
+
*
|
|
15
|
+
* @tag domain:seller,offers
|
|
16
|
+
* @tag layer:schema
|
|
17
|
+
* @tag pattern:none
|
|
18
|
+
* @tag access:isomorphic
|
|
19
|
+
* @tag consumers:SellerOffersView
|
|
20
|
+
* @tag sideEffects:none
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
export const counterOfferFormSchema = z.object({
|
|
24
|
+
counterAmount: z.coerce
|
|
25
|
+
.number({ invalid_type_error: "Enter a counter amount." })
|
|
26
|
+
.positive("Counter amount must be greater than zero."),
|
|
27
|
+
sellerNote: z.string().max(500, "Keep your note under 500 characters.").optional(),
|
|
28
|
+
});
|
|
@@ -85,7 +85,7 @@ export async function getStoreReviews(storeSlug) {
|
|
|
85
85
|
const publishedProducts = allProducts
|
|
86
86
|
.filter((p) => p.status === ProductStatusValues.PUBLISHED)
|
|
87
87
|
.slice(0, 20);
|
|
88
|
-
const reviewArrays = await Promise.all(publishedProducts.map((p) => reviewRepository.findApprovedByProduct(p.id)));
|
|
88
|
+
const reviewArrays = await Promise.all(publishedProducts.map((p) => reviewRepository.findApprovedByProduct(p.id, 50)));
|
|
89
89
|
const allReviews = reviewArrays
|
|
90
90
|
.flat()
|
|
91
91
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
@@ -12,19 +12,13 @@ import { AuctionFilters } from "../../auctions/components/AuctionFilters";
|
|
|
12
12
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
13
13
|
import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
|
|
14
14
|
import { pushWishlistOp } from "../../cart/utils/pending-ops";
|
|
15
|
-
import {
|
|
16
|
-
import { sortBy } from "../../../constants/sort";
|
|
15
|
+
import { AUCTION_PUBLIC_SORT_OPTIONS } from "../../products/constants/sieve";
|
|
17
16
|
const __P = {
|
|
18
17
|
p3: "p-[var(--appkit-space-3)]",
|
|
19
18
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ value: sortBy(PRODUCT_FIELDS.AUCTION_END_DATE), label: "Ending Latest" },
|
|
24
|
-
{ value: sortBy(PRODUCT_FIELDS.CURRENT_BID), label: "Highest Bid" },
|
|
25
|
-
{ value: sortBy(PRODUCT_FIELDS.PRICE, "ASC"), label: "Price: Low to High" },
|
|
26
|
-
{ value: sortBy(PRODUCT_FIELDS.PRICE), label: "Price: High to Low" },
|
|
27
|
-
];
|
|
19
|
+
// Shared with /auctions — a local copy here drifted ("Ending Soonest" vs
|
|
20
|
+
// "Ending Soon", and it silently omitted Most Bids / Buy It Now).
|
|
21
|
+
const DEFAULT_SORT = AUCTION_PUBLIC_SORT_OPTIONS[0].value;
|
|
28
22
|
const FILTER_KEYS = ["minBid", "maxBid", "dateFrom", "dateTo"];
|
|
29
23
|
export function StoreAuctionsListing({ storeId, initialData }) {
|
|
30
24
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
@@ -106,7 +100,7 @@ export function StoreAuctionsListing({ storeId, initialData }) {
|
|
|
106
100
|
isWishlisted: (productId) => wishlistedIds.has(productId),
|
|
107
101
|
};
|
|
108
102
|
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-[var(--appkit-space-4)]";
|
|
109
|
-
return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search store auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions:
|
|
103
|
+
return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search store auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: AUCTION_PUBLIC_SORT_OPTIONS, onSortChange: (v) => {
|
|
110
104
|
table.set("sort", v);
|
|
111
105
|
}, view: view, onViewChange: (v) => {
|
|
112
106
|
if (v === "table")
|
|
@@ -8,19 +8,14 @@ import { Div, FilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, Sticky
|
|
|
8
8
|
import { MarketplacePreorderCard } from "../../pre-orders/components/MarketplacePreorderCard";
|
|
9
9
|
import { ProductFilters } from "../../products/components/ProductFilters";
|
|
10
10
|
import { ROUTES } from "../../../next";
|
|
11
|
-
import {
|
|
12
|
-
import { sortBy } from "../../../constants/sort";
|
|
11
|
+
import { PREORDER_PUBLIC_SORT_OPTIONS } from "../../products/constants/sieve";
|
|
13
12
|
const __P = {
|
|
14
13
|
p3: "p-[var(--appkit-space-3)]",
|
|
15
14
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{ value: sortBy(PRODUCT_FIELDS.PRE_ORDER_DELIVERY_DATE, "ASC"), label: "Delivery Soon" },
|
|
21
|
-
{ value: sortBy(PRODUCT_FIELDS.PRICE, "ASC"), label: "Price: Low to High" },
|
|
22
|
-
{ value: sortBy(PRODUCT_FIELDS.PRICE), label: "Price: High to Low" },
|
|
23
|
-
];
|
|
15
|
+
// Shared with /pre-orders. The local copy also defaulted to -createdAt,
|
|
16
|
+
// so a store tab opened Newest-first while the public page opened
|
|
17
|
+
// Earliest-Delivery-first for the same data.
|
|
18
|
+
const DEFAULT_SORT = PREORDER_PUBLIC_SORT_OPTIONS[0].value;
|
|
24
19
|
const FILTER_KEYS = ["minPrice", "maxPrice"];
|
|
25
20
|
export function StorePreOrdersListing({ storeId, initialData }) {
|
|
26
21
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
@@ -59,7 +54,7 @@ export function StorePreOrdersListing({ storeId, initialData }) {
|
|
|
59
54
|
table.set("q", searchInput.trim());
|
|
60
55
|
}, [searchInput, table]);
|
|
61
56
|
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-[var(--appkit-space-4)]";
|
|
62
|
-
return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search store pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions:
|
|
57
|
+
return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search store pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: PREORDER_PUBLIC_SORT_OPTIONS, onSortChange: (v) => {
|
|
63
58
|
table.set("sort", v);
|
|
64
59
|
}, view: view, onViewChange: (v) => {
|
|
65
60
|
if (v === "table")
|
|
@@ -2,4 +2,8 @@ import React from "react";
|
|
|
2
2
|
export interface StoreReviewsListingProps {
|
|
3
3
|
storeSlug: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* The `/stores/[slug]/reviews` tab. Delegates to the shared `ReviewsListingPanel` —
|
|
7
|
+
* see the note in `ReviewsIndexListing` for why the two were merged.
|
|
8
|
+
*/
|
|
5
9
|
export declare function StoreReviewsListing({ storeSlug }: StoreReviewsListingProps): React.JSX.Element;
|